Luthira A
commited on
Commit
·
4bec42e
1
Parent(s):
bcf4441
updated scripts and unique id generation with form for mqtt connection
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- app.py +2 -2
- scripts/adafruit_bme680.mpy +0 -0
- scripts/adafruit_bme680.py +769 -0
- scripts/adafruit_bus_device/__init__.py +0 -0
- scripts/adafruit_bus_device/i2c_device.py +187 -0
- scripts/adafruit_bus_device/spi_device.py +121 -0
- scripts/bme60.py +421 -0
- scripts/bme680.py +421 -0
- scripts/bme680i.py +418 -0
- scripts/constants.py +12 -0
- scripts/demo.py +18 -0
- scripts/hivemq-com-chain.der +0 -0
- scripts/lib/CHANGELOG.md +50 -0
- scripts/lib/LICENSE +21 -0
- scripts/lib/README.md +56 -0
- scripts/lib/adafruit_blinka-8.49.0.dist-info/METADATA +8 -0
- scripts/lib/adafruit_blinka-8.49.0.dist-info/RECORD +2 -0
- scripts/lib/as7341.py +608 -0
- scripts/lib/as7341_sensor.py +149 -0
- scripts/lib/as7341_smux_select.py +49 -0
- scripts/lib/bme680-2.0.0.dist-info/METADATA +156 -0
- scripts/lib/bme680-2.0.0.dist-info/RECORD +7 -0
- scripts/lib/bme680/__init__.py +486 -0
- scripts/lib/bme680/constants.py +413 -0
- scripts/lib/data_logging.py +166 -0
- scripts/lib/functools.py +28 -0
- scripts/lib/mqtt_as.py +824 -0
- scripts/lib/netman.py +73 -0
- scripts/lib/sdcard/LICENSE +21 -0
- scripts/lib/sdcard/sdcard.py +302 -0
- scripts/lib/sdl_demo_utils.py +276 -0
- scripts/lib/smbus2-0.5.0.dist-info/METADATA +234 -0
- scripts/lib/smbus2-0.5.0.dist-info/RECORD +6 -0
- scripts/lib/smbus2/__init__.py +26 -0
- scripts/lib/smbus2/py.typed +0 -0
- scripts/lib/smbus2/smbus2.py +660 -0
- scripts/lib/smbus2/smbus2.pyi +148 -0
- scripts/lib/ufastrsa/__init__.py +0 -0
- scripts/lib/ufastrsa/genprime.py +136 -0
- scripts/lib/ufastrsa/rsa.py +46 -0
- scripts/lib/ufastrsa/srandom.py +47 -0
- scripts/lib/ufastrsa/util.py +14 -0
- scripts/lib/umqtt/robust.py +44 -0
- scripts/lib/umqtt/simple.py +217 -0
- scripts/lib/unique_id-1.0.1.dist-info/METADATA +11 -0
- scripts/lib/unique_id-1.0.1.dist-info/RECORD +5 -0
- scripts/lib/unique_id/__init__.py +1 -0
- scripts/lib/unique_id/main.py +58 -0
- scripts/lib/unique_id/tests.py +40 -0
- scripts/lib/urequests_2.py +203 -0
app.py
CHANGED
|
@@ -12,8 +12,8 @@ st.title("Real-Time Sensor Data Dashboard")
|
|
| 12 |
with st.form("mqtt_form"):
|
| 13 |
MQTT_HOST = st.text_input("Enter your MQTT host:", "b6bdb89571144b3d8e5ca4bbe666ddb5.s1.eu.hivemq.cloud")
|
| 14 |
MQTT_PORT = st.number_input("Enter the port number:", min_value=1, max_value=65535, value=8883)
|
| 15 |
-
MQTT_USERNAME = st.text_input("Enter your MQTT username:", "
|
| 16 |
-
MQTT_PASSWORD = st.text_input("Enter your MQTT password:", "
|
| 17 |
MQTT_TOPIC = st.text_input("Enter your MQTT topic:", "sensors/bme680/data")
|
| 18 |
submit_button = st.form_submit_button(label="Submit")
|
| 19 |
|
|
|
|
| 12 |
with st.form("mqtt_form"):
|
| 13 |
MQTT_HOST = st.text_input("Enter your MQTT host:", "b6bdb89571144b3d8e5ca4bbe666ddb5.s1.eu.hivemq.cloud")
|
| 14 |
MQTT_PORT = st.number_input("Enter the port number:", min_value=1, max_value=65535, value=8883)
|
| 15 |
+
MQTT_USERNAME = st.text_input("Enter your MQTT username:", "LuthiraMQ")
|
| 16 |
+
MQTT_PASSWORD = st.text_input("Enter your MQTT password:", "jLVx8y9v83gmgERTr0AP", type="password")
|
| 17 |
MQTT_TOPIC = st.text_input("Enter your MQTT topic:", "sensors/bme680/data")
|
| 18 |
submit_button = st.form_submit_button(label="Submit")
|
| 19 |
|
scripts/adafruit_bme680.mpy
ADDED
|
Binary file (7.4 kB). View file
|
|
|
scripts/adafruit_bme680.py
ADDED
|
@@ -0,0 +1,769 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: 2017 ladyada for Adafruit Industries
|
| 2 |
+
#
|
| 3 |
+
# SPDX-License-Identifier: MIT AND BSD-3-Clause
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
"""
|
| 7 |
+
`adafruit_bme680`
|
| 8 |
+
================================================================================
|
| 9 |
+
|
| 10 |
+
CircuitPython library for BME680 temperature, pressure and humidity sensor.
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
* Author(s): Limor Fried, William Garber, many others
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
Implementation Notes
|
| 17 |
+
--------------------
|
| 18 |
+
|
| 19 |
+
**Hardware:**
|
| 20 |
+
|
| 21 |
+
* `Adafruit BME680 Temp, Humidity, Pressure and Gas Sensor <https://www.adafruit.com/product/3660>`_
|
| 22 |
+
|
| 23 |
+
**Software and Dependencies:**
|
| 24 |
+
|
| 25 |
+
* Adafruit CircuitPython firmware for the supported boards:
|
| 26 |
+
https://github.com/adafruit/circuitpython/releases
|
| 27 |
+
* Adafruit's Bus Device library: https://github.com/adafruit/Adafruit_CircuitPython_BusDevice
|
| 28 |
+
"""
|
| 29 |
+
|
| 30 |
+
import math
|
| 31 |
+
import struct
|
| 32 |
+
import time
|
| 33 |
+
|
| 34 |
+
from micropython import const
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def delay_microseconds(nusec):
|
| 38 |
+
"""HELP must be same as dev->delay_us"""
|
| 39 |
+
time.sleep(nusec / 1000000.0)
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
try:
|
| 43 |
+
# Used only for type annotations.
|
| 44 |
+
|
| 45 |
+
import typing
|
| 46 |
+
|
| 47 |
+
from busio import I2C, SPI
|
| 48 |
+
from circuitpython_typing import ReadableBuffer
|
| 49 |
+
from digitalio import DigitalInOut
|
| 50 |
+
|
| 51 |
+
except ImportError:
|
| 52 |
+
pass
|
| 53 |
+
|
| 54 |
+
__version__ = "3.7.9"
|
| 55 |
+
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_BME680.git"
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
# I2C ADDRESS/BITS/SETTINGS NEW
|
| 59 |
+
# -----------------------------------------------------------------------
|
| 60 |
+
_BME68X_ENABLE_HEATER = const(0x00)
|
| 61 |
+
_BME68X_DISABLE_HEATER = const(0x01)
|
| 62 |
+
_BME68X_DISABLE_GAS_MEAS = const(0x00)
|
| 63 |
+
_BME68X_ENABLE_GAS_MEAS_L = const(0x01)
|
| 64 |
+
_BME68X_ENABLE_GAS_MEAS_H = const(0x02)
|
| 65 |
+
_BME68X_SLEEP_MODE = const(0)
|
| 66 |
+
_BME68X_FORCED_MODE = const(1)
|
| 67 |
+
_BME68X_VARIANT_GAS_LOW = const(0x00)
|
| 68 |
+
_BME68X_VARIANT_GAS_HIGH = const(0x01)
|
| 69 |
+
_BME68X_HCTRL_MSK = const(0x08)
|
| 70 |
+
_BME68X_HCTRL_POS = const(3)
|
| 71 |
+
_BME68X_NBCONV_MSK = const(0x0F)
|
| 72 |
+
_BME68X_RUN_GAS_MSK = const(0x30)
|
| 73 |
+
_BME68X_RUN_GAS_POS = const(4)
|
| 74 |
+
_BME68X_MODE_MSK = const(0x03)
|
| 75 |
+
_BME68X_PERIOD_POLL = const(10000)
|
| 76 |
+
_BME68X_REG_CTRL_GAS_0 = const(0x70)
|
| 77 |
+
_BME68X_REG_CTRL_GAS_1 = const(0x71)
|
| 78 |
+
|
| 79 |
+
# I2C ADDRESS/BITS/SETTINGS
|
| 80 |
+
# -----------------------------------------------------------------------
|
| 81 |
+
_BME680_CHIPID = const(0x61)
|
| 82 |
+
|
| 83 |
+
_BME680_REG_CHIPID = const(0xD0)
|
| 84 |
+
_BME68X_REG_VARIANT = const(0xF0)
|
| 85 |
+
_BME680_BME680_COEFF_ADDR1 = const(0x89)
|
| 86 |
+
_BME680_BME680_COEFF_ADDR2 = const(0xE1)
|
| 87 |
+
_BME680_BME680_RES_HEAT_0 = const(0x5A)
|
| 88 |
+
_BME680_BME680_GAS_WAIT_0 = const(0x64)
|
| 89 |
+
|
| 90 |
+
_BME680_REG_SOFTRESET = const(0xE0)
|
| 91 |
+
_BME680_REG_CTRL_GAS = const(0x71)
|
| 92 |
+
_BME680_REG_CTRL_HUM = const(0x72)
|
| 93 |
+
_BME680_REG_STATUS = const(0x73)
|
| 94 |
+
_BME680_REG_CTRL_MEAS = const(0x74)
|
| 95 |
+
_BME680_REG_CONFIG = const(0x75)
|
| 96 |
+
|
| 97 |
+
_BME680_REG_MEAS_STATUS = const(0x1D)
|
| 98 |
+
_BME680_REG_PDATA = const(0x1F)
|
| 99 |
+
_BME680_REG_TDATA = const(0x22)
|
| 100 |
+
_BME680_REG_HDATA = const(0x25)
|
| 101 |
+
|
| 102 |
+
_BME680_SAMPLERATES = (0, 1, 2, 4, 8, 16)
|
| 103 |
+
_BME680_FILTERSIZES = (0, 1, 3, 7, 15, 31, 63, 127)
|
| 104 |
+
|
| 105 |
+
_BME680_RUNGAS = const(0x10)
|
| 106 |
+
|
| 107 |
+
_LOOKUP_TABLE_1 = (
|
| 108 |
+
2147483647.0,
|
| 109 |
+
2147483647.0,
|
| 110 |
+
2147483647.0,
|
| 111 |
+
2147483647.0,
|
| 112 |
+
2147483647.0,
|
| 113 |
+
2126008810.0,
|
| 114 |
+
2147483647.0,
|
| 115 |
+
2130303777.0,
|
| 116 |
+
2147483647.0,
|
| 117 |
+
2147483647.0,
|
| 118 |
+
2143188679.0,
|
| 119 |
+
2136746228.0,
|
| 120 |
+
2147483647.0,
|
| 121 |
+
2126008810.0,
|
| 122 |
+
2147483647.0,
|
| 123 |
+
2147483647.0,
|
| 124 |
+
)
|
| 125 |
+
|
| 126 |
+
_LOOKUP_TABLE_2 = (
|
| 127 |
+
4096000000.0,
|
| 128 |
+
2048000000.0,
|
| 129 |
+
1024000000.0,
|
| 130 |
+
512000000.0,
|
| 131 |
+
255744255.0,
|
| 132 |
+
127110228.0,
|
| 133 |
+
64000000.0,
|
| 134 |
+
32258064.0,
|
| 135 |
+
16016016.0,
|
| 136 |
+
8000000.0,
|
| 137 |
+
4000000.0,
|
| 138 |
+
2000000.0,
|
| 139 |
+
1000000.0,
|
| 140 |
+
500000.0,
|
| 141 |
+
250000.0,
|
| 142 |
+
125000.0,
|
| 143 |
+
)
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
def bme_set_bits(reg_data, bitname_msk, bitname_pos, data):
|
| 147 |
+
"""
|
| 148 |
+
Macro to set bits
|
| 149 |
+
data2 = data << bitname_pos
|
| 150 |
+
set masked bits from data2 in reg_data
|
| 151 |
+
"""
|
| 152 |
+
return (reg_data & ~bitname_msk) | ((data << bitname_pos) & bitname_msk)
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
def bme_set_bits_pos_0(reg_data, bitname_msk, data):
|
| 156 |
+
"""
|
| 157 |
+
Macro to set bits starting from position 0
|
| 158 |
+
set masked bits from data in reg_data
|
| 159 |
+
"""
|
| 160 |
+
return (reg_data & ~bitname_msk) | (data & bitname_msk)
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
def _read24(arr: ReadableBuffer) -> float:
|
| 164 |
+
"""Parse an unsigned 24-bit value as a floating point and return it."""
|
| 165 |
+
ret = 0.0
|
| 166 |
+
# print([hex(i) for i in arr])
|
| 167 |
+
for b in arr:
|
| 168 |
+
ret *= 256.0
|
| 169 |
+
ret += float(b & 0xFF)
|
| 170 |
+
return ret
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
class Adafruit_BME680:
|
| 174 |
+
"""Driver from BME680 air quality sensor
|
| 175 |
+
|
| 176 |
+
:param int refresh_rate: Maximum number of readings per second. Faster property reads
|
| 177 |
+
will be from the previous reading."""
|
| 178 |
+
|
| 179 |
+
def __init__(self, *, refresh_rate: int = 10) -> None:
|
| 180 |
+
"""Check the BME680 was found, read the coefficients and enable the sensor for continuous
|
| 181 |
+
reads."""
|
| 182 |
+
self._write(_BME680_REG_SOFTRESET, [0xB6])
|
| 183 |
+
time.sleep(0.005)
|
| 184 |
+
|
| 185 |
+
# Check device ID.
|
| 186 |
+
chip_id = self._read_byte(_BME680_REG_CHIPID)
|
| 187 |
+
if chip_id != _BME680_CHIPID:
|
| 188 |
+
raise RuntimeError("Failed to find BME680! Chip ID 0x%x" % chip_id)
|
| 189 |
+
|
| 190 |
+
# Get variant
|
| 191 |
+
self._chip_variant = self._read_byte(_BME68X_REG_VARIANT)
|
| 192 |
+
|
| 193 |
+
self._read_calibration()
|
| 194 |
+
|
| 195 |
+
# set up heater
|
| 196 |
+
self._write(_BME680_BME680_RES_HEAT_0, [0x73])
|
| 197 |
+
self._write(_BME680_BME680_GAS_WAIT_0, [0x65])
|
| 198 |
+
|
| 199 |
+
self.sea_level_pressure = 1013.25
|
| 200 |
+
"""Pressure in hectoPascals at sea level. Used to calibrate :attr:`altitude`."""
|
| 201 |
+
|
| 202 |
+
# Default oversampling and filter register values.
|
| 203 |
+
self._pressure_oversample = 0b011
|
| 204 |
+
self._temp_oversample = 0b100
|
| 205 |
+
self._humidity_oversample = 0b010
|
| 206 |
+
self._filter = 0b010
|
| 207 |
+
|
| 208 |
+
# Gas measurements, as a mask applied to _BME680_RUNGAS
|
| 209 |
+
self._run_gas = 0xFF
|
| 210 |
+
|
| 211 |
+
self._adc_pres = None
|
| 212 |
+
self._adc_temp = None
|
| 213 |
+
self._adc_hum = None
|
| 214 |
+
self._adc_gas = None
|
| 215 |
+
self._gas_range = None
|
| 216 |
+
self._t_fine = None
|
| 217 |
+
|
| 218 |
+
self._last_reading = 0
|
| 219 |
+
self._min_refresh_time = 1 / refresh_rate
|
| 220 |
+
|
| 221 |
+
self._amb_temp = 25 # Copy required parameters from reference bme68x_dev struct
|
| 222 |
+
self.set_gas_heater(320, 150) # heater 320 deg C for 150 msec
|
| 223 |
+
|
| 224 |
+
@property
|
| 225 |
+
def pressure_oversample(self) -> int:
|
| 226 |
+
"""The oversampling for pressure sensor"""
|
| 227 |
+
return _BME680_SAMPLERATES[self._pressure_oversample]
|
| 228 |
+
|
| 229 |
+
@pressure_oversample.setter
|
| 230 |
+
def pressure_oversample(self, sample_rate: int) -> None:
|
| 231 |
+
if sample_rate in _BME680_SAMPLERATES:
|
| 232 |
+
self._pressure_oversample = _BME680_SAMPLERATES.index(sample_rate)
|
| 233 |
+
else:
|
| 234 |
+
raise RuntimeError("Invalid oversample")
|
| 235 |
+
|
| 236 |
+
@property
|
| 237 |
+
def humidity_oversample(self) -> int:
|
| 238 |
+
"""The oversampling for humidity sensor"""
|
| 239 |
+
return _BME680_SAMPLERATES[self._humidity_oversample]
|
| 240 |
+
|
| 241 |
+
@humidity_oversample.setter
|
| 242 |
+
def humidity_oversample(self, sample_rate: int) -> None:
|
| 243 |
+
if sample_rate in _BME680_SAMPLERATES:
|
| 244 |
+
self._humidity_oversample = _BME680_SAMPLERATES.index(sample_rate)
|
| 245 |
+
else:
|
| 246 |
+
raise RuntimeError("Invalid oversample")
|
| 247 |
+
|
| 248 |
+
@property
|
| 249 |
+
def temperature_oversample(self) -> int:
|
| 250 |
+
"""The oversampling for temperature sensor"""
|
| 251 |
+
return _BME680_SAMPLERATES[self._temp_oversample]
|
| 252 |
+
|
| 253 |
+
@temperature_oversample.setter
|
| 254 |
+
def temperature_oversample(self, sample_rate: int) -> None:
|
| 255 |
+
if sample_rate in _BME680_SAMPLERATES:
|
| 256 |
+
self._temp_oversample = _BME680_SAMPLERATES.index(sample_rate)
|
| 257 |
+
else:
|
| 258 |
+
raise RuntimeError("Invalid oversample")
|
| 259 |
+
|
| 260 |
+
@property
|
| 261 |
+
def filter_size(self) -> int:
|
| 262 |
+
"""The filter size for the built in IIR filter"""
|
| 263 |
+
return _BME680_FILTERSIZES[self._filter]
|
| 264 |
+
|
| 265 |
+
@filter_size.setter
|
| 266 |
+
def filter_size(self, size: int) -> None:
|
| 267 |
+
if size in _BME680_FILTERSIZES:
|
| 268 |
+
self._filter = _BME680_FILTERSIZES.index(size)
|
| 269 |
+
else:
|
| 270 |
+
raise RuntimeError("Invalid size")
|
| 271 |
+
|
| 272 |
+
@property
|
| 273 |
+
def temperature(self) -> float:
|
| 274 |
+
"""The compensated temperature in degrees Celsius."""
|
| 275 |
+
self._perform_reading()
|
| 276 |
+
calc_temp = ((self._t_fine * 5) + 128) / 256
|
| 277 |
+
return calc_temp / 100
|
| 278 |
+
|
| 279 |
+
@property
|
| 280 |
+
def pressure(self) -> float:
|
| 281 |
+
"""The barometric pressure in hectoPascals"""
|
| 282 |
+
self._perform_reading()
|
| 283 |
+
var1 = (self._t_fine / 2) - 64000
|
| 284 |
+
var2 = ((var1 / 4) * (var1 / 4)) / 2048
|
| 285 |
+
var2 = (var2 * self._pressure_calibration[5]) / 4
|
| 286 |
+
var2 = var2 + (var1 * self._pressure_calibration[4] * 2)
|
| 287 |
+
var2 = (var2 / 4) + (self._pressure_calibration[3] * 65536)
|
| 288 |
+
var1 = ((((var1 / 4) * (var1 / 4)) / 8192) * (self._pressure_calibration[2] * 32) / 8) + (
|
| 289 |
+
(self._pressure_calibration[1] * var1) / 2
|
| 290 |
+
)
|
| 291 |
+
var1 = var1 / 262144
|
| 292 |
+
var1 = ((32768 + var1) * self._pressure_calibration[0]) / 32768
|
| 293 |
+
calc_pres = 1048576 - self._adc_pres
|
| 294 |
+
calc_pres = (calc_pres - (var2 / 4096)) * 3125
|
| 295 |
+
calc_pres = (calc_pres / var1) * 2
|
| 296 |
+
var1 = (self._pressure_calibration[8] * (((calc_pres / 8) * (calc_pres / 8)) / 8192)) / 4096
|
| 297 |
+
var2 = ((calc_pres / 4) * self._pressure_calibration[7]) / 8192
|
| 298 |
+
var3 = (((calc_pres / 256) ** 3) * self._pressure_calibration[9]) / 131072
|
| 299 |
+
calc_pres += (var1 + var2 + var3 + (self._pressure_calibration[6] * 128)) / 16
|
| 300 |
+
return calc_pres / 100
|
| 301 |
+
|
| 302 |
+
@property
|
| 303 |
+
def relative_humidity(self) -> float:
|
| 304 |
+
"""The relative humidity in RH %"""
|
| 305 |
+
return self.humidity
|
| 306 |
+
|
| 307 |
+
@property
|
| 308 |
+
def humidity(self) -> float:
|
| 309 |
+
"""The relative humidity in RH %"""
|
| 310 |
+
self._perform_reading()
|
| 311 |
+
temp_scaled = ((self._t_fine * 5) + 128) / 256
|
| 312 |
+
var1 = (self._adc_hum - (self._humidity_calibration[0] * 16)) - (
|
| 313 |
+
(temp_scaled * self._humidity_calibration[2]) / 200
|
| 314 |
+
)
|
| 315 |
+
var2 = (
|
| 316 |
+
self._humidity_calibration[1]
|
| 317 |
+
* (
|
| 318 |
+
((temp_scaled * self._humidity_calibration[3]) / 100)
|
| 319 |
+
+ (
|
| 320 |
+
((temp_scaled * ((temp_scaled * self._humidity_calibration[4]) / 100)) / 64)
|
| 321 |
+
/ 100
|
| 322 |
+
)
|
| 323 |
+
+ 16384
|
| 324 |
+
)
|
| 325 |
+
) / 1024
|
| 326 |
+
var3 = var1 * var2
|
| 327 |
+
var4 = self._humidity_calibration[5] * 128
|
| 328 |
+
var4 = (var4 + ((temp_scaled * self._humidity_calibration[6]) / 100)) / 16
|
| 329 |
+
var5 = ((var3 / 16384) * (var3 / 16384)) / 1024
|
| 330 |
+
var6 = (var4 * var5) / 2
|
| 331 |
+
calc_hum = (((var3 + var6) / 1024) * 1000) / 4096
|
| 332 |
+
calc_hum /= 1000 # get back to RH
|
| 333 |
+
|
| 334 |
+
calc_hum = min(calc_hum, 100)
|
| 335 |
+
calc_hum = max(calc_hum, 0)
|
| 336 |
+
return calc_hum
|
| 337 |
+
|
| 338 |
+
@property
|
| 339 |
+
def altitude(self) -> float:
|
| 340 |
+
"""The altitude based on current :attr:`pressure` vs the sea level pressure
|
| 341 |
+
(:attr:`sea_level_pressure`) - which you must enter ahead of time)"""
|
| 342 |
+
pressure = self.pressure # in Si units for hPascal
|
| 343 |
+
return 44330 * (1.0 - math.pow(pressure / self.sea_level_pressure, 0.1903))
|
| 344 |
+
|
| 345 |
+
@property
|
| 346 |
+
def gas(self) -> int:
|
| 347 |
+
"""The gas resistance in ohms"""
|
| 348 |
+
self._perform_reading()
|
| 349 |
+
if self._chip_variant == 0x01:
|
| 350 |
+
# taken from https://github.com/BoschSensortec/BME68x-Sensor-API
|
| 351 |
+
var1 = 262144 >> self._gas_range
|
| 352 |
+
var2 = self._adc_gas - 512
|
| 353 |
+
var2 *= 3
|
| 354 |
+
var2 = 4096 + var2
|
| 355 |
+
calc_gas_res = (10000 * var1) / var2
|
| 356 |
+
calc_gas_res = calc_gas_res * 100
|
| 357 |
+
else:
|
| 358 |
+
var1 = ((1340 + (5 * self._sw_err)) * (_LOOKUP_TABLE_1[self._gas_range])) / 65536
|
| 359 |
+
var2 = ((self._adc_gas * 32768) - 16777216) + var1
|
| 360 |
+
var3 = (_LOOKUP_TABLE_2[self._gas_range] * var1) / 512
|
| 361 |
+
calc_gas_res = (var3 + (var2 / 2)) / var2
|
| 362 |
+
return int(calc_gas_res)
|
| 363 |
+
|
| 364 |
+
def _perform_reading(self) -> None:
|
| 365 |
+
"""Perform a single-shot reading from the sensor and fill internal data structure for
|
| 366 |
+
calculations"""
|
| 367 |
+
if time.monotonic() - self._last_reading < self._min_refresh_time:
|
| 368 |
+
return
|
| 369 |
+
|
| 370 |
+
# set filter
|
| 371 |
+
self._write(_BME680_REG_CONFIG, [self._filter << 2])
|
| 372 |
+
# turn on temp oversample & pressure oversample
|
| 373 |
+
self._write(
|
| 374 |
+
_BME680_REG_CTRL_MEAS,
|
| 375 |
+
[(self._temp_oversample << 5) | (self._pressure_oversample << 2)],
|
| 376 |
+
)
|
| 377 |
+
# turn on humidity oversample
|
| 378 |
+
self._write(_BME680_REG_CTRL_HUM, [self._humidity_oversample])
|
| 379 |
+
# gas measurements enabled
|
| 380 |
+
if self._chip_variant == 0x01:
|
| 381 |
+
self._write(_BME680_REG_CTRL_GAS, [(self._run_gas & _BME680_RUNGAS) << 1])
|
| 382 |
+
else:
|
| 383 |
+
self._write(_BME680_REG_CTRL_GAS, [(self._run_gas & _BME680_RUNGAS)])
|
| 384 |
+
ctrl = self._read_byte(_BME680_REG_CTRL_MEAS)
|
| 385 |
+
ctrl = (ctrl & 0xFC) | 0x01 # enable single shot!
|
| 386 |
+
self._write(_BME680_REG_CTRL_MEAS, [ctrl])
|
| 387 |
+
new_data = False
|
| 388 |
+
while not new_data:
|
| 389 |
+
data = self._read(_BME680_REG_MEAS_STATUS, 17)
|
| 390 |
+
new_data = data[0] & 0x80 != 0
|
| 391 |
+
time.sleep(0.005)
|
| 392 |
+
self._last_reading = time.monotonic()
|
| 393 |
+
|
| 394 |
+
self._adc_pres = _read24(data[2:5]) / 16
|
| 395 |
+
self._adc_temp = _read24(data[5:8]) / 16
|
| 396 |
+
self._adc_hum = struct.unpack(">H", bytes(data[8:10]))[0]
|
| 397 |
+
if self._chip_variant == 0x01:
|
| 398 |
+
self._adc_gas = int(struct.unpack(">H", bytes(data[15:17]))[0] / 64)
|
| 399 |
+
self._gas_range = data[16] & 0x0F
|
| 400 |
+
else:
|
| 401 |
+
self._adc_gas = int(struct.unpack(">H", bytes(data[13:15]))[0] / 64)
|
| 402 |
+
self._gas_range = data[14] & 0x0F
|
| 403 |
+
|
| 404 |
+
var1 = (self._adc_temp / 8) - (self._temp_calibration[0] * 2)
|
| 405 |
+
var2 = (var1 * self._temp_calibration[1]) / 2048
|
| 406 |
+
var3 = ((var1 / 2) * (var1 / 2)) / 4096
|
| 407 |
+
var3 = (var3 * self._temp_calibration[2] * 16) / 16384
|
| 408 |
+
self._t_fine = int(var2 + var3)
|
| 409 |
+
|
| 410 |
+
def _read_calibration(self) -> None:
|
| 411 |
+
"""Read & save the calibration coefficients"""
|
| 412 |
+
coeff = self._read(_BME680_BME680_COEFF_ADDR1, 25)
|
| 413 |
+
coeff += self._read(_BME680_BME680_COEFF_ADDR2, 16)
|
| 414 |
+
|
| 415 |
+
coeff = list(struct.unpack("<hbBHhbBhhbbHhhBBBHbbbBbHhbb", bytes(coeff[1:39])))
|
| 416 |
+
# print("\n\n",coeff)
|
| 417 |
+
coeff = [float(i) for i in coeff]
|
| 418 |
+
self._temp_calibration = [coeff[x] for x in [23, 0, 1]]
|
| 419 |
+
self._pressure_calibration = [coeff[x] for x in [3, 4, 5, 7, 8, 10, 9, 12, 13, 14]]
|
| 420 |
+
self._humidity_calibration = [coeff[x] for x in [17, 16, 18, 19, 20, 21, 22]]
|
| 421 |
+
self._gas_calibration = [coeff[x] for x in [25, 24, 26]]
|
| 422 |
+
|
| 423 |
+
# flip around H1 & H2
|
| 424 |
+
self._humidity_calibration[1] *= 16
|
| 425 |
+
self._humidity_calibration[1] += self._humidity_calibration[0] % 16
|
| 426 |
+
self._humidity_calibration[0] /= 16
|
| 427 |
+
|
| 428 |
+
self._heat_range = (self._read_byte(0x02) & 0x30) / 16
|
| 429 |
+
self._heat_val = self._read_byte(0x00)
|
| 430 |
+
self._sw_err = (self._read_byte(0x04) & 0xF0) / 16
|
| 431 |
+
|
| 432 |
+
def _read_byte(self, register: int) -> int:
|
| 433 |
+
"""Read a byte register value and return it"""
|
| 434 |
+
return self._read(register, 1)[0]
|
| 435 |
+
|
| 436 |
+
def _read(self, register: int, length: int) -> bytearray:
|
| 437 |
+
raise NotImplementedError()
|
| 438 |
+
|
| 439 |
+
def _write(self, register: int, values: bytearray) -> None:
|
| 440 |
+
raise NotImplementedError()
|
| 441 |
+
|
| 442 |
+
def set_gas_heater(self, heater_temp: int, heater_time: int) -> bool:
|
| 443 |
+
"""
|
| 444 |
+
Enable and configure gas reading + heater (None disables)
|
| 445 |
+
:param heater_temp: Desired temperature in degrees Centigrade
|
| 446 |
+
:param heater_time: Time to keep heater on in milliseconds
|
| 447 |
+
:return: True on success, False on failure
|
| 448 |
+
"""
|
| 449 |
+
try:
|
| 450 |
+
if (heater_temp is None) or (heater_time is None):
|
| 451 |
+
self._set_heatr_conf(heater_temp or 0, heater_time or 0, enable=False)
|
| 452 |
+
else:
|
| 453 |
+
self._set_heatr_conf(heater_temp, heater_time)
|
| 454 |
+
except OSError:
|
| 455 |
+
return False
|
| 456 |
+
return True
|
| 457 |
+
|
| 458 |
+
def _set_heatr_conf(self, heater_temp: int, heater_time: int, enable: bool = True) -> None:
|
| 459 |
+
# restrict to BME68X_FORCED_MODE
|
| 460 |
+
op_mode: int = _BME68X_FORCED_MODE
|
| 461 |
+
nb_conv: int = 0
|
| 462 |
+
hctrl: int = _BME68X_ENABLE_HEATER
|
| 463 |
+
run_gas: int = 0
|
| 464 |
+
ctrl_gas_data_0: int = 0
|
| 465 |
+
ctrl_gas_data_1: int = 0
|
| 466 |
+
|
| 467 |
+
self._set_op_mode(_BME68X_SLEEP_MODE)
|
| 468 |
+
self._set_conf(heater_temp, heater_time, op_mode)
|
| 469 |
+
ctrl_gas_data_0 = self._read_byte(_BME68X_REG_CTRL_GAS_0)
|
| 470 |
+
ctrl_gas_data_1 = self._read_byte(_BME68X_REG_CTRL_GAS_1)
|
| 471 |
+
if enable:
|
| 472 |
+
hctrl = _BME68X_ENABLE_HEATER
|
| 473 |
+
if self._chip_variant == _BME68X_VARIANT_GAS_HIGH:
|
| 474 |
+
run_gas = _BME68X_ENABLE_GAS_MEAS_H
|
| 475 |
+
else:
|
| 476 |
+
run_gas = _BME68X_ENABLE_GAS_MEAS_L
|
| 477 |
+
else:
|
| 478 |
+
hctrl = _BME68X_DISABLE_HEATER
|
| 479 |
+
run_gas = _BME68X_DISABLE_GAS_MEAS
|
| 480 |
+
self._run_gas = ~(run_gas - 1)
|
| 481 |
+
|
| 482 |
+
ctrl_gas_data_0 = bme_set_bits(ctrl_gas_data_0, _BME68X_HCTRL_MSK, _BME68X_HCTRL_POS, hctrl)
|
| 483 |
+
ctrl_gas_data_1 = bme_set_bits_pos_0(ctrl_gas_data_1, _BME68X_NBCONV_MSK, nb_conv)
|
| 484 |
+
ctrl_gas_data_1 = bme_set_bits(
|
| 485 |
+
ctrl_gas_data_1, _BME68X_RUN_GAS_MSK, _BME68X_RUN_GAS_POS, run_gas
|
| 486 |
+
)
|
| 487 |
+
self._write(_BME68X_REG_CTRL_GAS_0, [ctrl_gas_data_0])
|
| 488 |
+
self._write(_BME68X_REG_CTRL_GAS_1, [ctrl_gas_data_1])
|
| 489 |
+
|
| 490 |
+
def _set_op_mode(self, op_mode: int) -> None:
|
| 491 |
+
"""
|
| 492 |
+
* @brief This API is used to set the operation mode of the sensor
|
| 493 |
+
"""
|
| 494 |
+
tmp_pow_mode: int = 0
|
| 495 |
+
pow_mode: int = _BME68X_FORCED_MODE
|
| 496 |
+
# Call until in sleep
|
| 497 |
+
|
| 498 |
+
# was a do {} while() loop
|
| 499 |
+
while pow_mode != _BME68X_SLEEP_MODE:
|
| 500 |
+
tmp_pow_mode = self._read_byte(_BME680_REG_CTRL_MEAS)
|
| 501 |
+
# Put to sleep before changing mode
|
| 502 |
+
pow_mode = tmp_pow_mode & _BME68X_MODE_MSK
|
| 503 |
+
if pow_mode != _BME68X_SLEEP_MODE:
|
| 504 |
+
tmp_pow_mode &= ~_BME68X_MODE_MSK # Set to sleep
|
| 505 |
+
self._write(_BME680_REG_CTRL_MEAS, [tmp_pow_mode])
|
| 506 |
+
# dev->delay_us(_BME68X_PERIOD_POLL, dev->intf_ptr) # HELP
|
| 507 |
+
delay_microseconds(_BME68X_PERIOD_POLL)
|
| 508 |
+
# Already in sleep
|
| 509 |
+
if op_mode != _BME68X_SLEEP_MODE:
|
| 510 |
+
tmp_pow_mode = (tmp_pow_mode & ~_BME68X_MODE_MSK) | (op_mode & _BME68X_MODE_MSK)
|
| 511 |
+
self._write(_BME680_REG_CTRL_MEAS, [tmp_pow_mode])
|
| 512 |
+
|
| 513 |
+
def _set_conf(self, heater_temp: int, heater_time: int, op_mode: int) -> None:
|
| 514 |
+
"""
|
| 515 |
+
This internal API is used to set heater configurations
|
| 516 |
+
"""
|
| 517 |
+
|
| 518 |
+
if op_mode != _BME68X_FORCED_MODE:
|
| 519 |
+
raise OSError("GasHeaterException: _set_conf not forced mode")
|
| 520 |
+
rh_reg_data: int = self._calc_res_heat(heater_temp)
|
| 521 |
+
gw_reg_data: int = self._calc_gas_wait(heater_time)
|
| 522 |
+
self._write(_BME680_BME680_RES_HEAT_0, [rh_reg_data])
|
| 523 |
+
self._write(_BME680_BME680_GAS_WAIT_0, [gw_reg_data])
|
| 524 |
+
|
| 525 |
+
def _calc_res_heat(self, temp: int) -> int:
|
| 526 |
+
"""
|
| 527 |
+
This internal API is used to calculate the heater resistance value using float
|
| 528 |
+
"""
|
| 529 |
+
gh1: int = self._gas_calibration[0]
|
| 530 |
+
gh2: int = self._gas_calibration[1]
|
| 531 |
+
gh3: int = self._gas_calibration[2]
|
| 532 |
+
htr: int = self._heat_range
|
| 533 |
+
htv: int = self._heat_val
|
| 534 |
+
amb: int = self._amb_temp
|
| 535 |
+
|
| 536 |
+
temp = min(temp, 400) # Cap temperature
|
| 537 |
+
|
| 538 |
+
var1: int = ((int(amb) * gh3) / 1000) * 256
|
| 539 |
+
var2: int = (gh1 + 784) * (((((gh2 + 154009) * temp * 5) / 100) + 3276800) / 10)
|
| 540 |
+
var3: int = var1 + (var2 / 2)
|
| 541 |
+
var4: int = var3 / (htr + 4)
|
| 542 |
+
var5: int = (131 * htv) + 65536
|
| 543 |
+
heatr_res_x100: int = int(((var4 / var5) - 250) * 34)
|
| 544 |
+
heatr_res: int = int((heatr_res_x100 + 50) / 100)
|
| 545 |
+
|
| 546 |
+
return heatr_res
|
| 547 |
+
|
| 548 |
+
def _calc_res_heat(self, temp: int) -> int:
|
| 549 |
+
"""
|
| 550 |
+
This internal API is used to calculate the heater resistance value
|
| 551 |
+
"""
|
| 552 |
+
gh1: float = float(self._gas_calibration[0])
|
| 553 |
+
gh2: float = float(self._gas_calibration[1])
|
| 554 |
+
gh3: float = float(self._gas_calibration[2])
|
| 555 |
+
htr: float = float(self._heat_range)
|
| 556 |
+
htv: float = float(self._heat_val)
|
| 557 |
+
amb: float = float(self._amb_temp)
|
| 558 |
+
|
| 559 |
+
temp = min(temp, 400) # Cap temperature
|
| 560 |
+
|
| 561 |
+
var1: float = (gh1 / (16.0)) + 49.0
|
| 562 |
+
var2: float = ((gh2 / (32768.0)) * (0.0005)) + 0.00235
|
| 563 |
+
var3: float = gh3 / (1024.0)
|
| 564 |
+
var4: float = var1 * (1.0 + (var2 * float(temp)))
|
| 565 |
+
var5: float = var4 + (var3 * amb)
|
| 566 |
+
res_heat: int = int(3.4 * ((var5 * (4 / (4 + htr)) * (1 / (1 + (htv * 0.002)))) - 25))
|
| 567 |
+
return res_heat
|
| 568 |
+
|
| 569 |
+
def _calc_gas_wait(self, dur: int) -> int:
|
| 570 |
+
"""
|
| 571 |
+
This internal API is used to calculate the gas wait
|
| 572 |
+
"""
|
| 573 |
+
factor: int = 0
|
| 574 |
+
durval: int = 0xFF # Max duration
|
| 575 |
+
|
| 576 |
+
if dur >= 0xFC0:
|
| 577 |
+
return durval
|
| 578 |
+
while dur > 0x3F:
|
| 579 |
+
dur = dur / 4
|
| 580 |
+
factor += 1
|
| 581 |
+
durval = int(dur + (factor * 64))
|
| 582 |
+
return durval
|
| 583 |
+
|
| 584 |
+
|
| 585 |
+
class Adafruit_BME680_I2C(Adafruit_BME680):
|
| 586 |
+
"""Driver for I2C connected BME680.
|
| 587 |
+
|
| 588 |
+
:param ~busio.I2C i2c: The I2C bus the BME680 is connected to.
|
| 589 |
+
:param int address: I2C device address. Defaults to :const:`0x77`
|
| 590 |
+
:param bool debug: Print debug statements when `True`. Defaults to `False`
|
| 591 |
+
:param int refresh_rate: Maximum number of readings per second. Faster property reads
|
| 592 |
+
will be from the previous reading.
|
| 593 |
+
|
| 594 |
+
**Quickstart: Importing and using the BME680**
|
| 595 |
+
|
| 596 |
+
Here is an example of using the :class:`BMP680_I2C` class.
|
| 597 |
+
First you will need to import the libraries to use the sensor
|
| 598 |
+
|
| 599 |
+
.. code-block:: python
|
| 600 |
+
|
| 601 |
+
import board
|
| 602 |
+
import adafruit_bme680
|
| 603 |
+
|
| 604 |
+
Once this is done you can define your ``board.I2C`` object and define your sensor object
|
| 605 |
+
|
| 606 |
+
.. code-block:: python
|
| 607 |
+
|
| 608 |
+
i2c = board.I2C() # uses board.SCL and board.SDA
|
| 609 |
+
bme680 = adafruit_bme680.Adafruit_BME680_I2C(i2c)
|
| 610 |
+
|
| 611 |
+
You need to setup the pressure at sea level
|
| 612 |
+
|
| 613 |
+
.. code-block:: python
|
| 614 |
+
|
| 615 |
+
bme680.sea_level_pressure = 1013.25
|
| 616 |
+
|
| 617 |
+
Now you have access to the :attr:`temperature`, :attr:`gas`, :attr:`relative_humidity`,
|
| 618 |
+
:attr:`pressure` and :attr:`altitude` attributes
|
| 619 |
+
|
| 620 |
+
.. code-block:: python
|
| 621 |
+
|
| 622 |
+
temperature = bme680.temperature
|
| 623 |
+
gas = bme680.gas
|
| 624 |
+
relative_humidity = bme680.relative_humidity
|
| 625 |
+
pressure = bme680.pressure
|
| 626 |
+
altitude = bme680.altitude
|
| 627 |
+
|
| 628 |
+
"""
|
| 629 |
+
|
| 630 |
+
def __init__(
|
| 631 |
+
self,
|
| 632 |
+
i2c: I2C,
|
| 633 |
+
address: int = 0x77,
|
| 634 |
+
debug: bool = False,
|
| 635 |
+
*,
|
| 636 |
+
refresh_rate: int = 10,
|
| 637 |
+
) -> None:
|
| 638 |
+
"""Initialize the I2C device at the 'address' given"""
|
| 639 |
+
from adafruit_bus_device import (
|
| 640 |
+
i2c_device,
|
| 641 |
+
)
|
| 642 |
+
|
| 643 |
+
self._i2c = i2c_device.I2CDevice(i2c, address)
|
| 644 |
+
self._debug = debug
|
| 645 |
+
super().__init__(refresh_rate=refresh_rate)
|
| 646 |
+
|
| 647 |
+
def _read(self, register: int, length: int) -> bytearray:
|
| 648 |
+
"""Returns an array of 'length' bytes from the 'register'"""
|
| 649 |
+
with self._i2c as i2c:
|
| 650 |
+
i2c.write(bytes([register & 0xFF]))
|
| 651 |
+
result = bytearray(length)
|
| 652 |
+
i2c.readinto(result)
|
| 653 |
+
if self._debug:
|
| 654 |
+
print(f"\t${register:02X} => {[hex(i) for i in result]}")
|
| 655 |
+
return result
|
| 656 |
+
|
| 657 |
+
def _write(self, register: int, values: ReadableBuffer) -> None:
|
| 658 |
+
"""Writes an array of 'length' bytes to the 'register'"""
|
| 659 |
+
with self._i2c as i2c:
|
| 660 |
+
buffer = bytearray(2 * len(values))
|
| 661 |
+
for i, value in enumerate(values):
|
| 662 |
+
buffer[2 * i] = register + i
|
| 663 |
+
buffer[2 * i + 1] = value
|
| 664 |
+
i2c.write(buffer)
|
| 665 |
+
if self._debug:
|
| 666 |
+
print(f"\t${values[0]:02X} <= {[hex(i) for i in values[1:]]}")
|
| 667 |
+
|
| 668 |
+
|
| 669 |
+
class Adafruit_BME680_SPI(Adafruit_BME680):
|
| 670 |
+
"""Driver for SPI connected BME680.
|
| 671 |
+
|
| 672 |
+
:param ~busio.SPI spi: SPI device
|
| 673 |
+
:param ~digitalio.DigitalInOut cs: Chip Select
|
| 674 |
+
:param bool debug: Print debug statements when `True`. Defaults to `False`
|
| 675 |
+
:param int baudrate: Clock rate, default is :const:`100000`
|
| 676 |
+
:param int refresh_rate: Maximum number of readings per second. Faster property reads
|
| 677 |
+
will be from the previous reading.
|
| 678 |
+
|
| 679 |
+
|
| 680 |
+
**Quickstart: Importing and using the BME680**
|
| 681 |
+
|
| 682 |
+
Here is an example of using the :class:`BMP680_SPI` class.
|
| 683 |
+
First you will need to import the libraries to use the sensor
|
| 684 |
+
|
| 685 |
+
.. code-block:: python
|
| 686 |
+
|
| 687 |
+
import board
|
| 688 |
+
from digitalio import DigitalInOut, Direction
|
| 689 |
+
import adafruit_bme680
|
| 690 |
+
|
| 691 |
+
Once this is done you can define your ``board.SPI`` object and define your sensor object
|
| 692 |
+
|
| 693 |
+
.. code-block:: python
|
| 694 |
+
|
| 695 |
+
cs = digitalio.DigitalInOut(board.D10)
|
| 696 |
+
spi = board.SPI()
|
| 697 |
+
bme680 = adafruit_bme680.Adafruit_BME680_SPI(spi, cs)
|
| 698 |
+
|
| 699 |
+
You need to setup the pressure at sea level
|
| 700 |
+
|
| 701 |
+
.. code-block:: python
|
| 702 |
+
|
| 703 |
+
bme680.sea_level_pressure = 1013.25
|
| 704 |
+
|
| 705 |
+
Now you have access to the :attr:`temperature`, :attr:`gas`, :attr:`relative_humidity`,
|
| 706 |
+
:attr:`pressure` and :attr:`altitude` attributes
|
| 707 |
+
|
| 708 |
+
.. code-block:: python
|
| 709 |
+
|
| 710 |
+
temperature = bme680.temperature
|
| 711 |
+
gas = bme680.gas
|
| 712 |
+
relative_humidity = bme680.relative_humidity
|
| 713 |
+
pressure = bme680.pressure
|
| 714 |
+
altitude = bme680.altitude
|
| 715 |
+
|
| 716 |
+
"""
|
| 717 |
+
|
| 718 |
+
def __init__( # noqa: PLR0913 Too many arguments in function definition
|
| 719 |
+
self,
|
| 720 |
+
spi: SPI,
|
| 721 |
+
cs: DigitalInOut,
|
| 722 |
+
baudrate: int = 100000,
|
| 723 |
+
debug: bool = False,
|
| 724 |
+
*,
|
| 725 |
+
refresh_rate: int = 10,
|
| 726 |
+
) -> None:
|
| 727 |
+
from adafruit_bus_device import (
|
| 728 |
+
spi_device,
|
| 729 |
+
)
|
| 730 |
+
|
| 731 |
+
self._spi = spi_device.SPIDevice(spi, cs, baudrate=baudrate)
|
| 732 |
+
self._debug = debug
|
| 733 |
+
super().__init__(refresh_rate=refresh_rate)
|
| 734 |
+
|
| 735 |
+
def _read(self, register: int, length: int) -> bytearray:
|
| 736 |
+
if register != _BME680_REG_STATUS:
|
| 737 |
+
# _BME680_REG_STATUS exists in both SPI memory pages
|
| 738 |
+
# For all other registers, we must set the correct memory page
|
| 739 |
+
self._set_spi_mem_page(register)
|
| 740 |
+
|
| 741 |
+
register = (register | 0x80) & 0xFF # Read single, bit 7 high.
|
| 742 |
+
with self._spi as spi:
|
| 743 |
+
spi.write(bytearray([register]))
|
| 744 |
+
result = bytearray(length)
|
| 745 |
+
spi.readinto(result)
|
| 746 |
+
if self._debug:
|
| 747 |
+
print(f"\t${register:02X} => {[hex(i) for i in result]}")
|
| 748 |
+
return result
|
| 749 |
+
|
| 750 |
+
def _write(self, register: int, values: ReadableBuffer) -> None:
|
| 751 |
+
if register != _BME680_REG_STATUS:
|
| 752 |
+
# _BME680_REG_STATUS exists in both SPI memory pages
|
| 753 |
+
# For all other registers, we must set the correct memory page
|
| 754 |
+
self._set_spi_mem_page(register)
|
| 755 |
+
register &= 0x7F # Write, bit 7 low.
|
| 756 |
+
with self._spi as spi:
|
| 757 |
+
buffer = bytearray(2 * len(values))
|
| 758 |
+
for i, value in enumerate(values):
|
| 759 |
+
buffer[2 * i] = register + i
|
| 760 |
+
buffer[2 * i + 1] = value & 0xFF
|
| 761 |
+
spi.write(buffer)
|
| 762 |
+
if self._debug:
|
| 763 |
+
print(f"\t${values[0]:02X} <= {[hex(i) for i in values[1:]]}")
|
| 764 |
+
|
| 765 |
+
def _set_spi_mem_page(self, register: int) -> None:
|
| 766 |
+
spi_mem_page = 0x00
|
| 767 |
+
if register < 0x80:
|
| 768 |
+
spi_mem_page = 0x10
|
| 769 |
+
self._write(_BME680_REG_STATUS, [spi_mem_page])
|
scripts/adafruit_bus_device/__init__.py
ADDED
|
File without changes
|
scripts/adafruit_bus_device/i2c_device.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: 2016 Scott Shawcroft for Adafruit Industries
|
| 2 |
+
#
|
| 3 |
+
# SPDX-License-Identifier: MIT
|
| 4 |
+
|
| 5 |
+
"""
|
| 6 |
+
`adafruit_bus_device.i2c_device` - I2C Bus Device
|
| 7 |
+
====================================================
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import time
|
| 11 |
+
|
| 12 |
+
try:
|
| 13 |
+
from typing import Optional, Type
|
| 14 |
+
from types import TracebackType
|
| 15 |
+
from circuitpython_typing import ReadableBuffer, WriteableBuffer
|
| 16 |
+
|
| 17 |
+
# Used only for type annotations.
|
| 18 |
+
from busio import I2C
|
| 19 |
+
except ImportError:
|
| 20 |
+
pass
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
__version__ = "5.2.10"
|
| 24 |
+
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_BusDevice.git"
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class I2CDevice:
|
| 28 |
+
"""
|
| 29 |
+
Represents a single I2C device and manages locking the bus and the device
|
| 30 |
+
address.
|
| 31 |
+
|
| 32 |
+
:param ~busio.I2C i2c: The I2C bus the device is on
|
| 33 |
+
:param int device_address: The 7 bit device address
|
| 34 |
+
:param bool probe: Probe for the device upon object creation, default is true
|
| 35 |
+
|
| 36 |
+
.. note:: This class is **NOT** built into CircuitPython. See
|
| 37 |
+
:ref:`here for install instructions <bus_device_installation>`.
|
| 38 |
+
|
| 39 |
+
Example:
|
| 40 |
+
|
| 41 |
+
.. code-block:: python
|
| 42 |
+
|
| 43 |
+
import busio
|
| 44 |
+
from board import *
|
| 45 |
+
from adafruit_bus_device.i2c_device import I2CDevice
|
| 46 |
+
|
| 47 |
+
with busio.I2C(SCL, SDA) as i2c:
|
| 48 |
+
device = I2CDevice(i2c, 0x70)
|
| 49 |
+
bytes_read = bytearray(4)
|
| 50 |
+
with device:
|
| 51 |
+
device.readinto(bytes_read)
|
| 52 |
+
# A second transaction
|
| 53 |
+
with device:
|
| 54 |
+
device.write(bytes_read)
|
| 55 |
+
"""
|
| 56 |
+
|
| 57 |
+
def __init__(self, i2c: I2C, device_address: int, probe: bool = True) -> None:
|
| 58 |
+
self.i2c = i2c
|
| 59 |
+
self.device_address = device_address
|
| 60 |
+
|
| 61 |
+
if probe:
|
| 62 |
+
self.__probe_for_device()
|
| 63 |
+
|
| 64 |
+
def readinto(
|
| 65 |
+
self, buf: WriteableBuffer, *, start: int = 0, end: Optional[int] = None
|
| 66 |
+
) -> None:
|
| 67 |
+
"""
|
| 68 |
+
Read into ``buf`` from the device. The number of bytes read will be the
|
| 69 |
+
length of ``buf``.
|
| 70 |
+
|
| 71 |
+
If ``start`` or ``end`` is provided, then the buffer will be sliced
|
| 72 |
+
as if ``buf[start:end]``. This will not cause an allocation like
|
| 73 |
+
``buf[start:end]`` will so it saves memory.
|
| 74 |
+
|
| 75 |
+
:param ~WriteableBuffer buffer: buffer to write into
|
| 76 |
+
:param int start: Index to start writing at
|
| 77 |
+
:param int end: Index to write up to but not include; if None, use ``len(buf)``
|
| 78 |
+
"""
|
| 79 |
+
if end is None:
|
| 80 |
+
end = len(buf)
|
| 81 |
+
self.i2c.readfrom_into(self.device_address, buf, start=start, end=end)
|
| 82 |
+
|
| 83 |
+
def write(
|
| 84 |
+
self, buf: ReadableBuffer, *, start: int = 0, end: Optional[int] = None
|
| 85 |
+
) -> None:
|
| 86 |
+
"""
|
| 87 |
+
Write the bytes from ``buffer`` to the device, then transmit a stop
|
| 88 |
+
bit.
|
| 89 |
+
|
| 90 |
+
If ``start`` or ``end`` is provided, then the buffer will be sliced
|
| 91 |
+
as if ``buffer[start:end]``. This will not cause an allocation like
|
| 92 |
+
``buffer[start:end]`` will so it saves memory.
|
| 93 |
+
|
| 94 |
+
:param ~ReadableBuffer buffer: buffer containing the bytes to write
|
| 95 |
+
:param int start: Index to start writing from
|
| 96 |
+
:param int end: Index to read up to but not include; if None, use ``len(buf)``
|
| 97 |
+
"""
|
| 98 |
+
if end is None:
|
| 99 |
+
end = len(buf)
|
| 100 |
+
self.i2c.writeto(self.device_address, buf, start=start, end=end)
|
| 101 |
+
|
| 102 |
+
# pylint: disable-msg=too-many-arguments
|
| 103 |
+
def write_then_readinto(
|
| 104 |
+
self,
|
| 105 |
+
out_buffer: ReadableBuffer,
|
| 106 |
+
in_buffer: WriteableBuffer,
|
| 107 |
+
*,
|
| 108 |
+
out_start: int = 0,
|
| 109 |
+
out_end: Optional[int] = None,
|
| 110 |
+
in_start: int = 0,
|
| 111 |
+
in_end: Optional[int] = None
|
| 112 |
+
) -> None:
|
| 113 |
+
"""
|
| 114 |
+
Write the bytes from ``out_buffer`` to the device, then immediately
|
| 115 |
+
reads into ``in_buffer`` from the device. The number of bytes read
|
| 116 |
+
will be the length of ``in_buffer``.
|
| 117 |
+
|
| 118 |
+
If ``out_start`` or ``out_end`` is provided, then the output buffer
|
| 119 |
+
will be sliced as if ``out_buffer[out_start:out_end]``. This will
|
| 120 |
+
not cause an allocation like ``buffer[out_start:out_end]`` will so
|
| 121 |
+
it saves memory.
|
| 122 |
+
|
| 123 |
+
If ``in_start`` or ``in_end`` is provided, then the input buffer
|
| 124 |
+
will be sliced as if ``in_buffer[in_start:in_end]``. This will not
|
| 125 |
+
cause an allocation like ``in_buffer[in_start:in_end]`` will so
|
| 126 |
+
it saves memory.
|
| 127 |
+
|
| 128 |
+
:param ~ReadableBuffer out_buffer: buffer containing the bytes to write
|
| 129 |
+
:param ~WriteableBuffer in_buffer: buffer containing the bytes to read into
|
| 130 |
+
:param int out_start: Index to start writing from
|
| 131 |
+
:param int out_end: Index to read up to but not include; if None, use ``len(out_buffer)``
|
| 132 |
+
:param int in_start: Index to start writing at
|
| 133 |
+
:param int in_end: Index to write up to but not include; if None, use ``len(in_buffer)``
|
| 134 |
+
"""
|
| 135 |
+
if out_end is None:
|
| 136 |
+
out_end = len(out_buffer)
|
| 137 |
+
if in_end is None:
|
| 138 |
+
in_end = len(in_buffer)
|
| 139 |
+
|
| 140 |
+
self.i2c.writeto_then_readfrom(
|
| 141 |
+
self.device_address,
|
| 142 |
+
out_buffer,
|
| 143 |
+
in_buffer,
|
| 144 |
+
out_start=out_start,
|
| 145 |
+
out_end=out_end,
|
| 146 |
+
in_start=in_start,
|
| 147 |
+
in_end=in_end,
|
| 148 |
+
)
|
| 149 |
+
|
| 150 |
+
# pylint: enable-msg=too-many-arguments
|
| 151 |
+
|
| 152 |
+
def __enter__(self) -> "I2CDevice":
|
| 153 |
+
while not self.i2c.try_lock():
|
| 154 |
+
time.sleep(0)
|
| 155 |
+
return self
|
| 156 |
+
|
| 157 |
+
def __exit__(
|
| 158 |
+
self,
|
| 159 |
+
exc_type: Optional[Type[type]],
|
| 160 |
+
exc_val: Optional[BaseException],
|
| 161 |
+
exc_tb: Optional[TracebackType],
|
| 162 |
+
) -> bool:
|
| 163 |
+
self.i2c.unlock()
|
| 164 |
+
return False
|
| 165 |
+
|
| 166 |
+
def __probe_for_device(self) -> None:
|
| 167 |
+
"""
|
| 168 |
+
Try to read a byte from an address,
|
| 169 |
+
if you get an OSError it means the device is not there
|
| 170 |
+
or that the device does not support these means of probing
|
| 171 |
+
"""
|
| 172 |
+
while not self.i2c.try_lock():
|
| 173 |
+
time.sleep(0)
|
| 174 |
+
try:
|
| 175 |
+
self.i2c.writeto(self.device_address, b"")
|
| 176 |
+
except OSError:
|
| 177 |
+
# some OS's dont like writing an empty bytesting...
|
| 178 |
+
# Retry by reading a byte
|
| 179 |
+
try:
|
| 180 |
+
result = bytearray(1)
|
| 181 |
+
self.i2c.readfrom_into(self.device_address, result)
|
| 182 |
+
except OSError:
|
| 183 |
+
# pylint: disable=raise-missing-from
|
| 184 |
+
raise ValueError("No I2C device at address: 0x%x" % self.device_address)
|
| 185 |
+
# pylint: enable=raise-missing-from
|
| 186 |
+
finally:
|
| 187 |
+
self.i2c.unlock()
|
scripts/adafruit_bus_device/spi_device.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: 2016 Scott Shawcroft for Adafruit Industries
|
| 2 |
+
#
|
| 3 |
+
# SPDX-License-Identifier: MIT
|
| 4 |
+
|
| 5 |
+
# pylint: disable=too-few-public-methods
|
| 6 |
+
|
| 7 |
+
"""
|
| 8 |
+
`adafruit_bus_device.spi_device` - SPI Bus Device
|
| 9 |
+
====================================================
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
import time
|
| 13 |
+
|
| 14 |
+
try:
|
| 15 |
+
from typing import Optional, Type
|
| 16 |
+
from types import TracebackType
|
| 17 |
+
|
| 18 |
+
# Used only for type annotations.
|
| 19 |
+
from busio import SPI
|
| 20 |
+
from digitalio import DigitalInOut
|
| 21 |
+
except ImportError:
|
| 22 |
+
pass
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
__version__ = "5.2.10"
|
| 26 |
+
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_BusDevice.git"
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class SPIDevice:
|
| 30 |
+
"""
|
| 31 |
+
Represents a single SPI device and manages locking the bus and the device
|
| 32 |
+
address.
|
| 33 |
+
|
| 34 |
+
:param ~busio.SPI spi: The SPI bus the device is on
|
| 35 |
+
:param ~digitalio.DigitalInOut chip_select: The chip select pin object that implements the
|
| 36 |
+
DigitalInOut API.
|
| 37 |
+
:param bool cs_active_value: Set to True if your device requires CS to be active high.
|
| 38 |
+
Defaults to False.
|
| 39 |
+
:param int baudrate: The desired SCK clock rate in Hertz. The actual clock rate may be
|
| 40 |
+
higher or lower due to the granularity of available clock settings (MCU dependent).
|
| 41 |
+
:param int polarity: The base state of the SCK clock pin (0 or 1).
|
| 42 |
+
:param int phase: The edge of the clock that data is captured. First (0) or second (1).
|
| 43 |
+
Rising or falling depends on SCK clock polarity.
|
| 44 |
+
:param int extra_clocks: The minimum number of clock cycles to cycle the bus after CS is high.
|
| 45 |
+
(Used for SD cards.)
|
| 46 |
+
|
| 47 |
+
.. note:: This class is **NOT** built into CircuitPython. See
|
| 48 |
+
:ref:`here for install instructions <bus_device_installation>`.
|
| 49 |
+
|
| 50 |
+
Example:
|
| 51 |
+
|
| 52 |
+
.. code-block:: python
|
| 53 |
+
|
| 54 |
+
import busio
|
| 55 |
+
import digitalio
|
| 56 |
+
from board import *
|
| 57 |
+
from adafruit_bus_device.spi_device import SPIDevice
|
| 58 |
+
|
| 59 |
+
with busio.SPI(SCK, MOSI, MISO) as spi_bus:
|
| 60 |
+
cs = digitalio.DigitalInOut(D10)
|
| 61 |
+
device = SPIDevice(spi_bus, cs)
|
| 62 |
+
bytes_read = bytearray(4)
|
| 63 |
+
# The object assigned to spi in the with statements below
|
| 64 |
+
# is the original spi_bus object. We are using the busio.SPI
|
| 65 |
+
# operations busio.SPI.readinto() and busio.SPI.write().
|
| 66 |
+
with device as spi:
|
| 67 |
+
spi.readinto(bytes_read)
|
| 68 |
+
# A second transaction
|
| 69 |
+
with device as spi:
|
| 70 |
+
spi.write(bytes_read)
|
| 71 |
+
"""
|
| 72 |
+
|
| 73 |
+
def __init__(
|
| 74 |
+
self,
|
| 75 |
+
spi: SPI,
|
| 76 |
+
chip_select: Optional[DigitalInOut] = None,
|
| 77 |
+
*,
|
| 78 |
+
cs_active_value: bool = False,
|
| 79 |
+
baudrate: int = 100000,
|
| 80 |
+
polarity: int = 0,
|
| 81 |
+
phase: int = 0,
|
| 82 |
+
extra_clocks: int = 0
|
| 83 |
+
) -> None:
|
| 84 |
+
self.spi = spi
|
| 85 |
+
self.baudrate = baudrate
|
| 86 |
+
self.polarity = polarity
|
| 87 |
+
self.phase = phase
|
| 88 |
+
self.extra_clocks = extra_clocks
|
| 89 |
+
self.chip_select = chip_select
|
| 90 |
+
self.cs_active_value = cs_active_value
|
| 91 |
+
if self.chip_select:
|
| 92 |
+
self.chip_select.switch_to_output(value=not self.cs_active_value)
|
| 93 |
+
|
| 94 |
+
def __enter__(self) -> SPI:
|
| 95 |
+
while not self.spi.try_lock():
|
| 96 |
+
time.sleep(0)
|
| 97 |
+
self.spi.configure(
|
| 98 |
+
baudrate=self.baudrate, polarity=self.polarity, phase=self.phase
|
| 99 |
+
)
|
| 100 |
+
if self.chip_select:
|
| 101 |
+
self.chip_select.value = self.cs_active_value
|
| 102 |
+
return self.spi
|
| 103 |
+
|
| 104 |
+
def __exit__(
|
| 105 |
+
self,
|
| 106 |
+
exc_type: Optional[Type[type]],
|
| 107 |
+
exc_val: Optional[BaseException],
|
| 108 |
+
exc_tb: Optional[TracebackType],
|
| 109 |
+
) -> bool:
|
| 110 |
+
if self.chip_select:
|
| 111 |
+
self.chip_select.value = not self.cs_active_value
|
| 112 |
+
if self.extra_clocks > 0:
|
| 113 |
+
buf = bytearray(1)
|
| 114 |
+
buf[0] = 0xFF
|
| 115 |
+
clocks = self.extra_clocks // 8
|
| 116 |
+
if self.extra_clocks % 8 != 0:
|
| 117 |
+
clocks += 1
|
| 118 |
+
for _ in range(clocks):
|
| 119 |
+
self.spi.write(buf)
|
| 120 |
+
self.spi.unlock()
|
| 121 |
+
return False
|
scripts/bme60.py
ADDED
|
@@ -0,0 +1,421 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# The MIT License (MIT)
|
| 2 |
+
#
|
| 3 |
+
# Copyright (c) 2017 ladyada for Adafruit Industries
|
| 4 |
+
#
|
| 5 |
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
# of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
# in the Software without restriction, including without limitation the rights
|
| 8 |
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
# copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
# furnished to do so, subject to the following conditions:
|
| 11 |
+
#
|
| 12 |
+
# The above copyright notice and this permission notice shall be included in
|
| 13 |
+
# all copies or substantial portions of the Software.
|
| 14 |
+
#
|
| 15 |
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
| 21 |
+
# THE SOFTWARE.
|
| 22 |
+
|
| 23 |
+
# We have a lot of attributes for this complex sensor.
|
| 24 |
+
# pylint: disable=too-many-instance-attributes
|
| 25 |
+
|
| 26 |
+
"""
|
| 27 |
+
`bme680` - BME680 - Temperature, Humidity, Pressure & Gas Sensor
|
| 28 |
+
================================================================
|
| 29 |
+
|
| 30 |
+
MicroPython driver from BME680 air quality sensor, based on Adafruit_bme680
|
| 31 |
+
|
| 32 |
+
* Author(s): Limor 'Ladyada' Fried of Adafruit
|
| 33 |
+
Jeff Raber (SPI support)
|
| 34 |
+
and many more contributors
|
| 35 |
+
"""
|
| 36 |
+
|
| 37 |
+
import time
|
| 38 |
+
import math
|
| 39 |
+
from micropython import const
|
| 40 |
+
from ubinascii import hexlify as hex
|
| 41 |
+
try:
|
| 42 |
+
import struct
|
| 43 |
+
except ImportError:
|
| 44 |
+
import ustruct as struct
|
| 45 |
+
|
| 46 |
+
# I2C ADDRESS/BITS/SETTINGS
|
| 47 |
+
# -----------------------------------------------------------------------
|
| 48 |
+
_BME680_CHIPID = const(0x61)
|
| 49 |
+
|
| 50 |
+
_BME680_REG_CHIPID = const(0xD0)
|
| 51 |
+
_BME680_BME680_COEFF_ADDR1 = const(0x89)
|
| 52 |
+
_BME680_BME680_COEFF_ADDR2 = const(0xE1)
|
| 53 |
+
_BME680_BME680_RES_HEAT_0 = const(0x5A)
|
| 54 |
+
_BME680_BME680_GAS_WAIT_0 = const(0x64)
|
| 55 |
+
|
| 56 |
+
_BME680_REG_SOFTRESET = const(0xE0)
|
| 57 |
+
_BME680_REG_CTRL_GAS = const(0x71)
|
| 58 |
+
_BME680_REG_CTRL_HUM = const(0x72)
|
| 59 |
+
_BME280_REG_STATUS = const(0xF3)
|
| 60 |
+
_BME680_REG_CTRL_MEAS = const(0x74)
|
| 61 |
+
_BME680_REG_CONFIG = const(0x75)
|
| 62 |
+
|
| 63 |
+
_BME680_REG_PAGE_SELECT = const(0x73)
|
| 64 |
+
_BME680_REG_MEAS_STATUS = const(0x1D)
|
| 65 |
+
_BME680_REG_PDATA = const(0x1F)
|
| 66 |
+
_BME680_REG_TDATA = const(0x22)
|
| 67 |
+
_BME680_REG_HDATA = const(0x25)
|
| 68 |
+
|
| 69 |
+
_BME680_SAMPLERATES = (0, 1, 2, 4, 8, 16)
|
| 70 |
+
_BME680_FILTERSIZES = (0, 1, 3, 7, 15, 31, 63, 127)
|
| 71 |
+
|
| 72 |
+
_BME680_RUNGAS = const(0x10)
|
| 73 |
+
|
| 74 |
+
_LOOKUP_TABLE_1 = (2147483647.0, 2147483647.0, 2147483647.0, 2147483647.0, 2147483647.0,
|
| 75 |
+
2126008810.0, 2147483647.0, 2130303777.0, 2147483647.0, 2147483647.0,
|
| 76 |
+
2143188679.0, 2136746228.0, 2147483647.0, 2126008810.0, 2147483647.0,
|
| 77 |
+
2147483647.0)
|
| 78 |
+
|
| 79 |
+
_LOOKUP_TABLE_2 = (4096000000.0, 2048000000.0, 1024000000.0, 512000000.0, 255744255.0, 127110228.0,
|
| 80 |
+
64000000.0, 32258064.0, 16016016.0, 8000000.0, 4000000.0, 2000000.0, 1000000.0,
|
| 81 |
+
500000.0, 250000.0, 125000.0)
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def _read24(arr):
|
| 85 |
+
"""Parse an unsigned 24-bit value as a floating point and return it."""
|
| 86 |
+
ret = 0.0
|
| 87 |
+
#print([hex(i) for i in arr])
|
| 88 |
+
for b in arr:
|
| 89 |
+
ret *= 256.0
|
| 90 |
+
ret += float(b & 0xFF)
|
| 91 |
+
return ret
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
class Adafruit_BME680:
|
| 95 |
+
"""Driver from BME680 air quality sensor
|
| 96 |
+
|
| 97 |
+
:param int refresh_rate: Maximum number of readings per second. Faster property reads
|
| 98 |
+
will be from the previous reading."""
|
| 99 |
+
def __init__(self, *, refresh_rate=10):
|
| 100 |
+
"""Check the BME680 was found, read the coefficients and enable the sensor for continuous
|
| 101 |
+
reads."""
|
| 102 |
+
self._write(_BME680_REG_SOFTRESET, [0xB6])
|
| 103 |
+
time.sleep(0.005)
|
| 104 |
+
|
| 105 |
+
# Check device ID.
|
| 106 |
+
chip_id = self._read_byte(_BME680_REG_CHIPID)
|
| 107 |
+
if chip_id != _BME680_CHIPID:
|
| 108 |
+
raise RuntimeError('Failed to find BME680! Chip ID 0x%x' % chip_id)
|
| 109 |
+
|
| 110 |
+
self._read_calibration()
|
| 111 |
+
|
| 112 |
+
# set up heater
|
| 113 |
+
self._write(_BME680_BME680_RES_HEAT_0, [0x73])
|
| 114 |
+
self._write(_BME680_BME680_GAS_WAIT_0, [0x65])
|
| 115 |
+
|
| 116 |
+
self.sea_level_pressure = 1013.25
|
| 117 |
+
"""Pressure in hectoPascals at sea level. Used to calibrate ``altitude``."""
|
| 118 |
+
|
| 119 |
+
# Default oversampling and filter register values.
|
| 120 |
+
self._pressure_oversample = 0b011
|
| 121 |
+
self._temp_oversample = 0b100
|
| 122 |
+
self._humidity_oversample = 0b010
|
| 123 |
+
self._filter = 0b010
|
| 124 |
+
|
| 125 |
+
self._adc_pres = None
|
| 126 |
+
self._adc_temp = None
|
| 127 |
+
self._adc_hum = None
|
| 128 |
+
self._adc_gas = None
|
| 129 |
+
self._gas_range = None
|
| 130 |
+
self._t_fine = None
|
| 131 |
+
|
| 132 |
+
self._last_reading = time.ticks_ms()
|
| 133 |
+
self._min_refresh_time = 1000 // refresh_rate
|
| 134 |
+
|
| 135 |
+
@property
|
| 136 |
+
def pressure_oversample(self):
|
| 137 |
+
"""The oversampling for pressure sensor"""
|
| 138 |
+
return _BME680_SAMPLERATES[self._pressure_oversample]
|
| 139 |
+
|
| 140 |
+
@pressure_oversample.setter
|
| 141 |
+
def pressure_oversample(self, sample_rate):
|
| 142 |
+
if sample_rate in _BME680_SAMPLERATES:
|
| 143 |
+
self._pressure_oversample = _BME680_SAMPLERATES.index(sample_rate)
|
| 144 |
+
else:
|
| 145 |
+
raise RuntimeError("Invalid oversample")
|
| 146 |
+
|
| 147 |
+
@property
|
| 148 |
+
def humidity_oversample(self):
|
| 149 |
+
"""The oversampling for humidity sensor"""
|
| 150 |
+
return _BME680_SAMPLERATES[self._humidity_oversample]
|
| 151 |
+
|
| 152 |
+
@humidity_oversample.setter
|
| 153 |
+
def humidity_oversample(self, sample_rate):
|
| 154 |
+
if sample_rate in _BME680_SAMPLERATES:
|
| 155 |
+
self._humidity_oversample = _BME680_SAMPLERATES.index(sample_rate)
|
| 156 |
+
else:
|
| 157 |
+
raise RuntimeError("Invalid oversample")
|
| 158 |
+
|
| 159 |
+
@property
|
| 160 |
+
def temperature_oversample(self):
|
| 161 |
+
"""The oversampling for temperature sensor"""
|
| 162 |
+
return _BME680_SAMPLERATES[self._temp_oversample]
|
| 163 |
+
|
| 164 |
+
@temperature_oversample.setter
|
| 165 |
+
def temperature_oversample(self, sample_rate):
|
| 166 |
+
if sample_rate in _BME680_SAMPLERATES:
|
| 167 |
+
self._temp_oversample = _BME680_SAMPLERATES.index(sample_rate)
|
| 168 |
+
else:
|
| 169 |
+
raise RuntimeError("Invalid oversample")
|
| 170 |
+
|
| 171 |
+
@property
|
| 172 |
+
def filter_size(self):
|
| 173 |
+
"""The filter size for the built in IIR filter"""
|
| 174 |
+
return _BME680_FILTERSIZES[self._filter]
|
| 175 |
+
|
| 176 |
+
@filter_size.setter
|
| 177 |
+
def filter_size(self, size):
|
| 178 |
+
if size in _BME680_FILTERSIZES:
|
| 179 |
+
self._filter = _BME680_FILTERSIZES[size]
|
| 180 |
+
else:
|
| 181 |
+
raise RuntimeError("Invalid size")
|
| 182 |
+
|
| 183 |
+
@property
|
| 184 |
+
def temperature(self):
|
| 185 |
+
"""The compensated temperature in degrees celsius."""
|
| 186 |
+
self._perform_reading()
|
| 187 |
+
calc_temp = (((self._t_fine * 5) + 128) / 256)
|
| 188 |
+
return calc_temp / 100
|
| 189 |
+
|
| 190 |
+
@property
|
| 191 |
+
def pressure(self):
|
| 192 |
+
"""The barometric pressure in hectoPascals"""
|
| 193 |
+
self._perform_reading()
|
| 194 |
+
var1 = (self._t_fine / 2) - 64000
|
| 195 |
+
var2 = ((var1 / 4) * (var1 / 4)) / 2048
|
| 196 |
+
var2 = (var2 * self._pressure_calibration[5]) / 4
|
| 197 |
+
var2 = var2 + (var1 * self._pressure_calibration[4] * 2)
|
| 198 |
+
var2 = (var2 / 4) + (self._pressure_calibration[3] * 65536)
|
| 199 |
+
var1 = (((((var1 / 4) * (var1 / 4)) / 8192) *
|
| 200 |
+
(self._pressure_calibration[2] * 32) / 8) +
|
| 201 |
+
((self._pressure_calibration[1] * var1) / 2))
|
| 202 |
+
var1 = var1 / 262144
|
| 203 |
+
var1 = ((32768 + var1) * self._pressure_calibration[0]) / 32768
|
| 204 |
+
calc_pres = 1048576 - self._adc_pres
|
| 205 |
+
calc_pres = (calc_pres - (var2 / 4096)) * 3125
|
| 206 |
+
calc_pres = (calc_pres / var1) * 2
|
| 207 |
+
var1 = (self._pressure_calibration[8] * (((calc_pres / 8) * (calc_pres / 8)) / 8192)) / 4096
|
| 208 |
+
var2 = ((calc_pres / 4) * self._pressure_calibration[7]) / 8192
|
| 209 |
+
var3 = (((calc_pres / 256) ** 3) * self._pressure_calibration[9]) / 131072
|
| 210 |
+
calc_pres += ((var1 + var2 + var3 + (self._pressure_calibration[6] * 128)) / 16)
|
| 211 |
+
return calc_pres/100
|
| 212 |
+
|
| 213 |
+
@property
|
| 214 |
+
def humidity(self):
|
| 215 |
+
"""The relative humidity in RH %"""
|
| 216 |
+
self._perform_reading()
|
| 217 |
+
temp_scaled = ((self._t_fine * 5) + 128) / 256
|
| 218 |
+
var1 = ((self._adc_hum - (self._humidity_calibration[0] * 16)) -
|
| 219 |
+
((temp_scaled * self._humidity_calibration[2]) / 200))
|
| 220 |
+
var2 = (self._humidity_calibration[1] *
|
| 221 |
+
(((temp_scaled * self._humidity_calibration[3]) / 100) +
|
| 222 |
+
(((temp_scaled * ((temp_scaled * self._humidity_calibration[4]) / 100)) /
|
| 223 |
+
64) / 100) + 16384)) / 1024
|
| 224 |
+
var3 = var1 * var2
|
| 225 |
+
var4 = self._humidity_calibration[5] * 128
|
| 226 |
+
var4 = (var4 + ((temp_scaled * self._humidity_calibration[6]) / 100)) / 16
|
| 227 |
+
var5 = ((var3 / 16384) * (var3 / 16384)) / 1024
|
| 228 |
+
var6 = (var4 * var5) / 2
|
| 229 |
+
calc_hum = (((var3 + var6) / 1024) * 1000) / 4096
|
| 230 |
+
calc_hum /= 1000 # get back to RH
|
| 231 |
+
|
| 232 |
+
if calc_hum > 100:
|
| 233 |
+
calc_hum = 100
|
| 234 |
+
if calc_hum < 0:
|
| 235 |
+
calc_hum = 0
|
| 236 |
+
return calc_hum
|
| 237 |
+
|
| 238 |
+
@property
|
| 239 |
+
def altitude(self):
|
| 240 |
+
"""The altitude based on current ``pressure`` vs the sea level pressure
|
| 241 |
+
(``sea_level_pressure``) - which you must enter ahead of time)"""
|
| 242 |
+
pressure = self.pressure # in Si units for hPascal
|
| 243 |
+
return 44330 * (1.0 - math.pow(pressure / self.sea_level_pressure, 0.1903))
|
| 244 |
+
|
| 245 |
+
@property
|
| 246 |
+
def gas(self):
|
| 247 |
+
"""The gas resistance in ohms"""
|
| 248 |
+
self._perform_reading()
|
| 249 |
+
var1 = ((1340 + (5 * self._sw_err)) * (_LOOKUP_TABLE_1[self._gas_range])) / 65536
|
| 250 |
+
var2 = ((self._adc_gas * 32768) - 16777216) + var1
|
| 251 |
+
var3 = (_LOOKUP_TABLE_2[self._gas_range] * var1) / 512
|
| 252 |
+
calc_gas_res = (var3 + (var2 / 2)) / var2
|
| 253 |
+
return int(calc_gas_res)
|
| 254 |
+
|
| 255 |
+
def _perform_reading(self):
|
| 256 |
+
"""Perform a single-shot reading from the sensor and fill internal data structure for
|
| 257 |
+
calculations"""
|
| 258 |
+
expired = time.ticks_diff(self._last_reading, time.ticks_ms()) * time.ticks_diff(0, 1)
|
| 259 |
+
if 0 <= expired < self._min_refresh_time:
|
| 260 |
+
time.sleep_ms(self._min_refresh_time - expired)
|
| 261 |
+
|
| 262 |
+
# set filter
|
| 263 |
+
self._write(_BME680_REG_CONFIG, [self._filter << 2])
|
| 264 |
+
# turn on temp oversample & pressure oversample
|
| 265 |
+
self._write(_BME680_REG_CTRL_MEAS,
|
| 266 |
+
[(self._temp_oversample << 5)|(self._pressure_oversample << 2)])
|
| 267 |
+
# turn on humidity oversample
|
| 268 |
+
self._write(_BME680_REG_CTRL_HUM, [self._humidity_oversample])
|
| 269 |
+
# gas measurements enabled
|
| 270 |
+
self._write(_BME680_REG_CTRL_GAS, [_BME680_RUNGAS])
|
| 271 |
+
|
| 272 |
+
ctrl = self._read_byte(_BME680_REG_CTRL_MEAS)
|
| 273 |
+
ctrl = (ctrl & 0xFC) | 0x01 # enable single shot!
|
| 274 |
+
self._write(_BME680_REG_CTRL_MEAS, [ctrl])
|
| 275 |
+
new_data = False
|
| 276 |
+
while not new_data:
|
| 277 |
+
data = self._read(_BME680_REG_MEAS_STATUS, 15)
|
| 278 |
+
new_data = data[0] & 0x80 != 0
|
| 279 |
+
time.sleep(0.005)
|
| 280 |
+
self._last_reading = time.ticks_ms()
|
| 281 |
+
|
| 282 |
+
self._adc_pres = _read24(data[2:5]) / 16
|
| 283 |
+
self._adc_temp = _read24(data[5:8]) / 16
|
| 284 |
+
self._adc_hum = struct.unpack('>H', bytes(data[8:10]))[0]
|
| 285 |
+
self._adc_gas = int(struct.unpack('>H', bytes(data[13:15]))[0] / 64)
|
| 286 |
+
self._gas_range = data[14] & 0x0F
|
| 287 |
+
|
| 288 |
+
var1 = (self._adc_temp / 8) - (self._temp_calibration[0] * 2)
|
| 289 |
+
var2 = (var1 * self._temp_calibration[1]) / 2048
|
| 290 |
+
var3 = ((var1 / 2) * (var1 / 2)) / 4096
|
| 291 |
+
var3 = (var3 * self._temp_calibration[2] * 16) / 16384
|
| 292 |
+
|
| 293 |
+
self._t_fine = int(var2 + var3)
|
| 294 |
+
|
| 295 |
+
def _read_calibration(self):
|
| 296 |
+
"""Read & save the calibration coefficients"""
|
| 297 |
+
coeff = self._read(_BME680_BME680_COEFF_ADDR1, 25)
|
| 298 |
+
coeff += self._read(_BME680_BME680_COEFF_ADDR2, 16)
|
| 299 |
+
|
| 300 |
+
coeff = list(struct.unpack('<hbBHhbBhhbbHhhBBBHbbbBbHhbb', bytes(coeff[1:39])))
|
| 301 |
+
# print("\n\n",coeff)
|
| 302 |
+
coeff = [float(i) for i in coeff]
|
| 303 |
+
self._temp_calibration = [coeff[x] for x in [23, 0, 1]]
|
| 304 |
+
self._pressure_calibration = [coeff[x] for x in [3, 4, 5, 7, 8, 10, 9, 12, 13, 14]]
|
| 305 |
+
self._humidity_calibration = [coeff[x] for x in [17, 16, 18, 19, 20, 21, 22]]
|
| 306 |
+
self._gas_calibration = [coeff[x] for x in [25, 24, 26]]
|
| 307 |
+
|
| 308 |
+
# flip around H1 & H2
|
| 309 |
+
self._humidity_calibration[1] *= 16
|
| 310 |
+
self._humidity_calibration[1] += self._humidity_calibration[0] % 16
|
| 311 |
+
self._humidity_calibration[0] /= 16
|
| 312 |
+
|
| 313 |
+
self._heat_range = (self._read_byte(0x02) & 0x30) / 16
|
| 314 |
+
self._heat_val = self._read_byte(0x00)
|
| 315 |
+
self._sw_err = (self._read_byte(0x04) & 0xF0) / 16
|
| 316 |
+
|
| 317 |
+
def _read_byte(self, register):
|
| 318 |
+
"""Read a byte register value and return it"""
|
| 319 |
+
return self._read(register, 1)[0]
|
| 320 |
+
|
| 321 |
+
def _read(self, register, length):
|
| 322 |
+
raise NotImplementedError()
|
| 323 |
+
|
| 324 |
+
def _write(self, register, values):
|
| 325 |
+
raise NotImplementedError()
|
| 326 |
+
|
| 327 |
+
class BME680_I2C(Adafruit_BME680):
|
| 328 |
+
"""Driver for I2C connected BME680.
|
| 329 |
+
|
| 330 |
+
:param i2c: I2C device object
|
| 331 |
+
:param int address: I2C device address
|
| 332 |
+
:param bool debug: Print debug statements when True.
|
| 333 |
+
:param int refresh_rate: Maximum number of readings per second. Faster property reads
|
| 334 |
+
will be from the previous reading."""
|
| 335 |
+
def __init__(self, i2c, address=0x77, debug=False, *, refresh_rate=10):
|
| 336 |
+
"""Initialize the I2C device at the 'address' given"""
|
| 337 |
+
self._i2c = i2c
|
| 338 |
+
self._address = address
|
| 339 |
+
self._debug = debug
|
| 340 |
+
super().__init__(refresh_rate=refresh_rate)
|
| 341 |
+
|
| 342 |
+
def _read(self, register, length):
|
| 343 |
+
"""Returns an array of 'length' bytes from the 'register'"""
|
| 344 |
+
result = bytearray(length)
|
| 345 |
+
self._i2c.readfrom_mem_into(self._address, register & 0xff, result)
|
| 346 |
+
if self._debug:
|
| 347 |
+
print("\t${:x} read ".format(register), " ".join(["{:02x}".format(i) for i in result]))
|
| 348 |
+
return result
|
| 349 |
+
|
| 350 |
+
def _write(self, register, values):
|
| 351 |
+
"""Writes an array of 'length' bytes to the 'register'"""
|
| 352 |
+
if self._debug:
|
| 353 |
+
print("\t${:x} write".format(register), " ".join(["{:02x}".format(i) for i in values]))
|
| 354 |
+
for value in values:
|
| 355 |
+
self._i2c.writeto_mem(self._address, register, bytearray([value & 0xFF]))
|
| 356 |
+
register += 1
|
| 357 |
+
|
| 358 |
+
|
| 359 |
+
class BME680_SPI(Adafruit_BME680):
|
| 360 |
+
"""Driver for SPI connected BME680.
|
| 361 |
+
|
| 362 |
+
:param spi: SPI device object, configured
|
| 363 |
+
:param cs: Chip Select Pin object, configured to OUT mode
|
| 364 |
+
:param bool debug: Print debug statements when True.
|
| 365 |
+
:param int refresh_rate: Maximum number of readings per second. Faster property reads
|
| 366 |
+
will be from the previous reading.
|
| 367 |
+
"""
|
| 368 |
+
|
| 369 |
+
def __init__(self, spi, cs, debug=False, *, refresh_rate=10):
|
| 370 |
+
self._spi = spi
|
| 371 |
+
self._cs = cs
|
| 372 |
+
self._debug = debug
|
| 373 |
+
self._cs(1)
|
| 374 |
+
super().__init__(refresh_rate=refresh_rate)
|
| 375 |
+
|
| 376 |
+
def _read(self, register, length):
|
| 377 |
+
if register != _BME680_REG_PAGE_SELECT:
|
| 378 |
+
# _BME680_REG_PAGE_SELECT exists in both SPI memory pages
|
| 379 |
+
# For all other registers, we must set the correct memory page
|
| 380 |
+
self._set_spi_mem_page(register)
|
| 381 |
+
register = (register | 0x80) & 0xFF # Read single, bit 7 high.
|
| 382 |
+
|
| 383 |
+
try:
|
| 384 |
+
self._cs(0)
|
| 385 |
+
self._spi.write(bytearray([register])) # pylint: disable=no-member
|
| 386 |
+
result = bytearray(length)
|
| 387 |
+
self._spi.readinto(result) # pylint: disable=no-member
|
| 388 |
+
if self._debug:
|
| 389 |
+
print("\t${:x} read ".format(register), " ".join(["{:02x}".format(i) for i in result]))
|
| 390 |
+
except Exception as e:
|
| 391 |
+
print (e)
|
| 392 |
+
result = None
|
| 393 |
+
finally:
|
| 394 |
+
self._cs(1)
|
| 395 |
+
return result
|
| 396 |
+
|
| 397 |
+
def _write(self, register, values):
|
| 398 |
+
if register != _BME680_REG_PAGE_SELECT:
|
| 399 |
+
# _BME680_REG_PAGE_SELECT exists in both SPI memory pages
|
| 400 |
+
# For all other registers, we must set the correct memory page
|
| 401 |
+
self._set_spi_mem_page(register)
|
| 402 |
+
register &= 0x7F # Write, bit 7 low.
|
| 403 |
+
try:
|
| 404 |
+
self._cs(0)
|
| 405 |
+
buffer = bytearray(2 * len(values))
|
| 406 |
+
for i, value in enumerate(values):
|
| 407 |
+
buffer[2 * i] = register + i
|
| 408 |
+
buffer[2 * i + 1] = value & 0xFF
|
| 409 |
+
self._spi.write(buffer) # pylint: disable=no-member
|
| 410 |
+
if self._debug:
|
| 411 |
+
print("\t${:x} write".format(register), " ".join(["{:02x}".format(i) for i in values]))
|
| 412 |
+
except Exception as e:
|
| 413 |
+
print (e)
|
| 414 |
+
finally:
|
| 415 |
+
self._cs(1)
|
| 416 |
+
|
| 417 |
+
def _set_spi_mem_page(self, register):
|
| 418 |
+
spi_mem_page = 0x00
|
| 419 |
+
if register < 0x80:
|
| 420 |
+
spi_mem_page = 0x10
|
| 421 |
+
self._write(_BME680_REG_PAGE_SELECT, [spi_mem_page])
|
scripts/bme680.py
ADDED
|
@@ -0,0 +1,421 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# The MIT License (MIT)
|
| 2 |
+
#
|
| 3 |
+
# Copyright (c) 2017 ladyada for Adafruit Industries
|
| 4 |
+
#
|
| 5 |
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
# of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
# in the Software without restriction, including without limitation the rights
|
| 8 |
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
# copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
# furnished to do so, subject to the following conditions:
|
| 11 |
+
#
|
| 12 |
+
# The above copyright notice and this permission notice shall be included in
|
| 13 |
+
# all copies or substantial portions of the Software.
|
| 14 |
+
#
|
| 15 |
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
| 21 |
+
# THE SOFTWARE.
|
| 22 |
+
|
| 23 |
+
# We have a lot of attributes for this complex sensor.
|
| 24 |
+
# pylint: disable=too-many-instance-attributes
|
| 25 |
+
|
| 26 |
+
"""
|
| 27 |
+
`bme680` - BME680 - Temperature, Humidity, Pressure & Gas Sensor
|
| 28 |
+
================================================================
|
| 29 |
+
|
| 30 |
+
MicroPython driver from BME680 air quality sensor, based on Adafruit_bme680
|
| 31 |
+
|
| 32 |
+
* Author(s): Limor 'Ladyada' Fried of Adafruit
|
| 33 |
+
Jeff Raber (SPI support)
|
| 34 |
+
and many more contributors
|
| 35 |
+
"""
|
| 36 |
+
|
| 37 |
+
import time
|
| 38 |
+
import math
|
| 39 |
+
from micropython import const
|
| 40 |
+
from ubinascii import hexlify as hex
|
| 41 |
+
try:
|
| 42 |
+
import struct
|
| 43 |
+
except ImportError:
|
| 44 |
+
import ustruct as struct
|
| 45 |
+
|
| 46 |
+
# I2C ADDRESS/BITS/SETTINGS
|
| 47 |
+
# -----------------------------------------------------------------------
|
| 48 |
+
_BME680_CHIPID = const(0x61)
|
| 49 |
+
|
| 50 |
+
_BME680_REG_CHIPID = const(0xD0)
|
| 51 |
+
_BME680_BME680_COEFF_ADDR1 = const(0x89)
|
| 52 |
+
_BME680_BME680_COEFF_ADDR2 = const(0xE1)
|
| 53 |
+
_BME680_BME680_RES_HEAT_0 = const(0x5A)
|
| 54 |
+
_BME680_BME680_GAS_WAIT_0 = const(0x64)
|
| 55 |
+
|
| 56 |
+
_BME680_REG_SOFTRESET = const(0xE0)
|
| 57 |
+
_BME680_REG_CTRL_GAS = const(0x71)
|
| 58 |
+
_BME680_REG_CTRL_HUM = const(0x72)
|
| 59 |
+
_BME280_REG_STATUS = const(0xF3)
|
| 60 |
+
_BME680_REG_CTRL_MEAS = const(0x74)
|
| 61 |
+
_BME680_REG_CONFIG = const(0x75)
|
| 62 |
+
|
| 63 |
+
_BME680_REG_PAGE_SELECT = const(0x73)
|
| 64 |
+
_BME680_REG_MEAS_STATUS = const(0x1D)
|
| 65 |
+
_BME680_REG_PDATA = const(0x1F)
|
| 66 |
+
_BME680_REG_TDATA = const(0x22)
|
| 67 |
+
_BME680_REG_HDATA = const(0x25)
|
| 68 |
+
|
| 69 |
+
_BME680_SAMPLERATES = (0, 1, 2, 4, 8, 16)
|
| 70 |
+
_BME680_FILTERSIZES = (0, 1, 3, 7, 15, 31, 63, 127)
|
| 71 |
+
|
| 72 |
+
_BME680_RUNGAS = const(0x10)
|
| 73 |
+
|
| 74 |
+
_LOOKUP_TABLE_1 = (2147483647.0, 2147483647.0, 2147483647.0, 2147483647.0, 2147483647.0,
|
| 75 |
+
2126008810.0, 2147483647.0, 2130303777.0, 2147483647.0, 2147483647.0,
|
| 76 |
+
2143188679.0, 2136746228.0, 2147483647.0, 2126008810.0, 2147483647.0,
|
| 77 |
+
2147483647.0)
|
| 78 |
+
|
| 79 |
+
_LOOKUP_TABLE_2 = (4096000000.0, 2048000000.0, 1024000000.0, 512000000.0, 255744255.0, 127110228.0,
|
| 80 |
+
64000000.0, 32258064.0, 16016016.0, 8000000.0, 4000000.0, 2000000.0, 1000000.0,
|
| 81 |
+
500000.0, 250000.0, 125000.0)
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def _read24(arr):
|
| 85 |
+
"""Parse an unsigned 24-bit value as a floating point and return it."""
|
| 86 |
+
ret = 0.0
|
| 87 |
+
#print([hex(i) for i in arr])
|
| 88 |
+
for b in arr:
|
| 89 |
+
ret *= 256.0
|
| 90 |
+
ret += float(b & 0xFF)
|
| 91 |
+
return ret
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
class Adafruit_BME680:
|
| 95 |
+
"""Driver from BME680 air quality sensor
|
| 96 |
+
|
| 97 |
+
:param int refresh_rate: Maximum number of readings per second. Faster property reads
|
| 98 |
+
will be from the previous reading."""
|
| 99 |
+
def __init__(self, *, refresh_rate=10):
|
| 100 |
+
"""Check the BME680 was found, read the coefficients and enable the sensor for continuous
|
| 101 |
+
reads."""
|
| 102 |
+
self._write(_BME680_REG_SOFTRESET, [0xB6])
|
| 103 |
+
time.sleep(0.005)
|
| 104 |
+
|
| 105 |
+
# Check device ID.
|
| 106 |
+
chip_id = self._read_byte(_BME680_REG_CHIPID)
|
| 107 |
+
if chip_id != _BME680_CHIPID:
|
| 108 |
+
raise RuntimeError('Failed to find BME680! Chip ID 0x%x' % chip_id)
|
| 109 |
+
|
| 110 |
+
self._read_calibration()
|
| 111 |
+
|
| 112 |
+
# set up heater
|
| 113 |
+
self._write(_BME680_BME680_RES_HEAT_0, [0x73])
|
| 114 |
+
self._write(_BME680_BME680_GAS_WAIT_0, [0x65])
|
| 115 |
+
|
| 116 |
+
self.sea_level_pressure = 1013.25
|
| 117 |
+
"""Pressure in hectoPascals at sea level. Used to calibrate ``altitude``."""
|
| 118 |
+
|
| 119 |
+
# Default oversampling and filter register values.
|
| 120 |
+
self._pressure_oversample = 0b011
|
| 121 |
+
self._temp_oversample = 0b100
|
| 122 |
+
self._humidity_oversample = 0b010
|
| 123 |
+
self._filter = 0b010
|
| 124 |
+
|
| 125 |
+
self._adc_pres = None
|
| 126 |
+
self._adc_temp = None
|
| 127 |
+
self._adc_hum = None
|
| 128 |
+
self._adc_gas = None
|
| 129 |
+
self._gas_range = None
|
| 130 |
+
self._t_fine = None
|
| 131 |
+
|
| 132 |
+
self._last_reading = time.ticks_ms()
|
| 133 |
+
self._min_refresh_time = 1000 // refresh_rate
|
| 134 |
+
|
| 135 |
+
@property
|
| 136 |
+
def pressure_oversample(self):
|
| 137 |
+
"""The oversampling for pressure sensor"""
|
| 138 |
+
return _BME680_SAMPLERATES[self._pressure_oversample]
|
| 139 |
+
|
| 140 |
+
@pressure_oversample.setter
|
| 141 |
+
def pressure_oversample(self, sample_rate):
|
| 142 |
+
if sample_rate in _BME680_SAMPLERATES:
|
| 143 |
+
self._pressure_oversample = _BME680_SAMPLERATES.index(sample_rate)
|
| 144 |
+
else:
|
| 145 |
+
raise RuntimeError("Invalid oversample")
|
| 146 |
+
|
| 147 |
+
@property
|
| 148 |
+
def humidity_oversample(self):
|
| 149 |
+
"""The oversampling for humidity sensor"""
|
| 150 |
+
return _BME680_SAMPLERATES[self._humidity_oversample]
|
| 151 |
+
|
| 152 |
+
@humidity_oversample.setter
|
| 153 |
+
def humidity_oversample(self, sample_rate):
|
| 154 |
+
if sample_rate in _BME680_SAMPLERATES:
|
| 155 |
+
self._humidity_oversample = _BME680_SAMPLERATES.index(sample_rate)
|
| 156 |
+
else:
|
| 157 |
+
raise RuntimeError("Invalid oversample")
|
| 158 |
+
|
| 159 |
+
@property
|
| 160 |
+
def temperature_oversample(self):
|
| 161 |
+
"""The oversampling for temperature sensor"""
|
| 162 |
+
return _BME680_SAMPLERATES[self._temp_oversample]
|
| 163 |
+
|
| 164 |
+
@temperature_oversample.setter
|
| 165 |
+
def temperature_oversample(self, sample_rate):
|
| 166 |
+
if sample_rate in _BME680_SAMPLERATES:
|
| 167 |
+
self._temp_oversample = _BME680_SAMPLERATES.index(sample_rate)
|
| 168 |
+
else:
|
| 169 |
+
raise RuntimeError("Invalid oversample")
|
| 170 |
+
|
| 171 |
+
@property
|
| 172 |
+
def filter_size(self):
|
| 173 |
+
"""The filter size for the built in IIR filter"""
|
| 174 |
+
return _BME680_FILTERSIZES[self._filter]
|
| 175 |
+
|
| 176 |
+
@filter_size.setter
|
| 177 |
+
def filter_size(self, size):
|
| 178 |
+
if size in _BME680_FILTERSIZES:
|
| 179 |
+
self._filter = _BME680_FILTERSIZES[size]
|
| 180 |
+
else:
|
| 181 |
+
raise RuntimeError("Invalid size")
|
| 182 |
+
|
| 183 |
+
@property
|
| 184 |
+
def temperature(self):
|
| 185 |
+
"""The compensated temperature in degrees celsius."""
|
| 186 |
+
self._perform_reading()
|
| 187 |
+
calc_temp = (((self._t_fine * 5) + 128) / 256)
|
| 188 |
+
return calc_temp / 100
|
| 189 |
+
|
| 190 |
+
@property
|
| 191 |
+
def pressure(self):
|
| 192 |
+
"""The barometric pressure in hectoPascals"""
|
| 193 |
+
self._perform_reading()
|
| 194 |
+
var1 = (self._t_fine / 2) - 64000
|
| 195 |
+
var2 = ((var1 / 4) * (var1 / 4)) / 2048
|
| 196 |
+
var2 = (var2 * self._pressure_calibration[5]) / 4
|
| 197 |
+
var2 = var2 + (var1 * self._pressure_calibration[4] * 2)
|
| 198 |
+
var2 = (var2 / 4) + (self._pressure_calibration[3] * 65536)
|
| 199 |
+
var1 = (((((var1 / 4) * (var1 / 4)) / 8192) *
|
| 200 |
+
(self._pressure_calibration[2] * 32) / 8) +
|
| 201 |
+
((self._pressure_calibration[1] * var1) / 2))
|
| 202 |
+
var1 = var1 / 262144
|
| 203 |
+
var1 = ((32768 + var1) * self._pressure_calibration[0]) / 32768
|
| 204 |
+
calc_pres = 1048576 - self._adc_pres
|
| 205 |
+
calc_pres = (calc_pres - (var2 / 4096)) * 3125
|
| 206 |
+
calc_pres = (calc_pres / var1) * 2
|
| 207 |
+
var1 = (self._pressure_calibration[8] * (((calc_pres / 8) * (calc_pres / 8)) / 8192)) / 4096
|
| 208 |
+
var2 = ((calc_pres / 4) * self._pressure_calibration[7]) / 8192
|
| 209 |
+
var3 = (((calc_pres / 256) ** 3) * self._pressure_calibration[9]) / 131072
|
| 210 |
+
calc_pres += ((var1 + var2 + var3 + (self._pressure_calibration[6] * 128)) / 16)
|
| 211 |
+
return calc_pres/100
|
| 212 |
+
|
| 213 |
+
@property
|
| 214 |
+
def humidity(self):
|
| 215 |
+
"""The relative humidity in RH %"""
|
| 216 |
+
self._perform_reading()
|
| 217 |
+
temp_scaled = ((self._t_fine * 5) + 128) / 256
|
| 218 |
+
var1 = ((self._adc_hum - (self._humidity_calibration[0] * 16)) -
|
| 219 |
+
((temp_scaled * self._humidity_calibration[2]) / 200))
|
| 220 |
+
var2 = (self._humidity_calibration[1] *
|
| 221 |
+
(((temp_scaled * self._humidity_calibration[3]) / 100) +
|
| 222 |
+
(((temp_scaled * ((temp_scaled * self._humidity_calibration[4]) / 100)) /
|
| 223 |
+
64) / 100) + 16384)) / 1024
|
| 224 |
+
var3 = var1 * var2
|
| 225 |
+
var4 = self._humidity_calibration[5] * 128
|
| 226 |
+
var4 = (var4 + ((temp_scaled * self._humidity_calibration[6]) / 100)) / 16
|
| 227 |
+
var5 = ((var3 / 16384) * (var3 / 16384)) / 1024
|
| 228 |
+
var6 = (var4 * var5) / 2
|
| 229 |
+
calc_hum = (((var3 + var6) / 1024) * 1000) / 4096
|
| 230 |
+
calc_hum /= 1000 # get back to RH
|
| 231 |
+
|
| 232 |
+
if calc_hum > 100:
|
| 233 |
+
calc_hum = 100
|
| 234 |
+
if calc_hum < 0:
|
| 235 |
+
calc_hum = 0
|
| 236 |
+
return calc_hum
|
| 237 |
+
|
| 238 |
+
@property
|
| 239 |
+
def altitude(self):
|
| 240 |
+
"""The altitude based on current ``pressure`` vs the sea level pressure
|
| 241 |
+
(``sea_level_pressure``) - which you must enter ahead of time)"""
|
| 242 |
+
pressure = self.pressure # in Si units for hPascal
|
| 243 |
+
return 44330 * (1.0 - math.pow(pressure / self.sea_level_pressure, 0.1903))
|
| 244 |
+
|
| 245 |
+
@property
|
| 246 |
+
def gas(self):
|
| 247 |
+
"""The gas resistance in ohms"""
|
| 248 |
+
self._perform_reading()
|
| 249 |
+
var1 = ((1340 + (5 * self._sw_err)) * (_LOOKUP_TABLE_1[self._gas_range])) / 65536
|
| 250 |
+
var2 = ((self._adc_gas * 32768) - 16777216) + var1
|
| 251 |
+
var3 = (_LOOKUP_TABLE_2[self._gas_range] * var1) / 512
|
| 252 |
+
calc_gas_res = (var3 + (var2 / 2)) / var2
|
| 253 |
+
return int(calc_gas_res)
|
| 254 |
+
|
| 255 |
+
def _perform_reading(self):
|
| 256 |
+
"""Perform a single-shot reading from the sensor and fill internal data structure for
|
| 257 |
+
calculations"""
|
| 258 |
+
expired = time.ticks_diff(self._last_reading, time.ticks_ms()) * time.ticks_diff(0, 1)
|
| 259 |
+
if 0 <= expired < self._min_refresh_time:
|
| 260 |
+
time.sleep_ms(self._min_refresh_time - expired)
|
| 261 |
+
|
| 262 |
+
# set filter
|
| 263 |
+
self._write(_BME680_REG_CONFIG, [self._filter << 2])
|
| 264 |
+
# turn on temp oversample & pressure oversample
|
| 265 |
+
self._write(_BME680_REG_CTRL_MEAS,
|
| 266 |
+
[(self._temp_oversample << 5)|(self._pressure_oversample << 2)])
|
| 267 |
+
# turn on humidity oversample
|
| 268 |
+
self._write(_BME680_REG_CTRL_HUM, [self._humidity_oversample])
|
| 269 |
+
# gas measurements enabled
|
| 270 |
+
self._write(_BME680_REG_CTRL_GAS, [_BME680_RUNGAS])
|
| 271 |
+
|
| 272 |
+
ctrl = self._read_byte(_BME680_REG_CTRL_MEAS)
|
| 273 |
+
ctrl = (ctrl & 0xFC) | 0x01 # enable single shot!
|
| 274 |
+
self._write(_BME680_REG_CTRL_MEAS, [ctrl])
|
| 275 |
+
new_data = False
|
| 276 |
+
while not new_data:
|
| 277 |
+
data = self._read(_BME680_REG_MEAS_STATUS, 15)
|
| 278 |
+
new_data = data[0] & 0x80 != 0
|
| 279 |
+
time.sleep(0.005)
|
| 280 |
+
self._last_reading = time.ticks_ms()
|
| 281 |
+
|
| 282 |
+
self._adc_pres = _read24(data[2:5]) / 16
|
| 283 |
+
self._adc_temp = _read24(data[5:8]) / 16
|
| 284 |
+
self._adc_hum = struct.unpack('>H', bytes(data[8:10]))[0]
|
| 285 |
+
self._adc_gas = int(struct.unpack('>H', bytes(data[13:15]))[0] / 64)
|
| 286 |
+
self._gas_range = data[14] & 0x0F
|
| 287 |
+
|
| 288 |
+
var1 = (self._adc_temp / 8) - (self._temp_calibration[0] * 2)
|
| 289 |
+
var2 = (var1 * self._temp_calibration[1]) / 2048
|
| 290 |
+
var3 = ((var1 / 2) * (var1 / 2)) / 4096
|
| 291 |
+
var3 = (var3 * self._temp_calibration[2] * 16) / 16384
|
| 292 |
+
|
| 293 |
+
self._t_fine = int(var2 + var3)
|
| 294 |
+
|
| 295 |
+
def _read_calibration(self):
|
| 296 |
+
"""Read & save the calibration coefficients"""
|
| 297 |
+
coeff = self._read(_BME680_BME680_COEFF_ADDR1, 25)
|
| 298 |
+
coeff += self._read(_BME680_BME680_COEFF_ADDR2, 16)
|
| 299 |
+
|
| 300 |
+
coeff = list(struct.unpack('<hbBHhbBhhbbHhhBBBHbbbBbHhbb', bytes(coeff[1:39])))
|
| 301 |
+
# print("\n\n",coeff)
|
| 302 |
+
coeff = [float(i) for i in coeff]
|
| 303 |
+
self._temp_calibration = [coeff[x] for x in [23, 0, 1]]
|
| 304 |
+
self._pressure_calibration = [coeff[x] for x in [3, 4, 5, 7, 8, 10, 9, 12, 13, 14]]
|
| 305 |
+
self._humidity_calibration = [coeff[x] for x in [17, 16, 18, 19, 20, 21, 22]]
|
| 306 |
+
self._gas_calibration = [coeff[x] for x in [25, 24, 26]]
|
| 307 |
+
|
| 308 |
+
# flip around H1 & H2
|
| 309 |
+
self._humidity_calibration[1] *= 16
|
| 310 |
+
self._humidity_calibration[1] += self._humidity_calibration[0] % 16
|
| 311 |
+
self._humidity_calibration[0] /= 16
|
| 312 |
+
|
| 313 |
+
self._heat_range = (self._read_byte(0x02) & 0x30) / 16
|
| 314 |
+
self._heat_val = self._read_byte(0x00)
|
| 315 |
+
self._sw_err = (self._read_byte(0x04) & 0xF0) / 16
|
| 316 |
+
|
| 317 |
+
def _read_byte(self, register):
|
| 318 |
+
"""Read a byte register value and return it"""
|
| 319 |
+
return self._read(register, 1)[0]
|
| 320 |
+
|
| 321 |
+
def _read(self, register, length):
|
| 322 |
+
raise NotImplementedError()
|
| 323 |
+
|
| 324 |
+
def _write(self, register, values):
|
| 325 |
+
raise NotImplementedError()
|
| 326 |
+
|
| 327 |
+
class BME680_I2C(Adafruit_BME680):
|
| 328 |
+
"""Driver for I2C connected BME680.
|
| 329 |
+
|
| 330 |
+
:param i2c: I2C device object
|
| 331 |
+
:param int address: I2C device address
|
| 332 |
+
:param bool debug: Print debug statements when True.
|
| 333 |
+
:param int refresh_rate: Maximum number of readings per second. Faster property reads
|
| 334 |
+
will be from the previous reading."""
|
| 335 |
+
def __init__(self, i2c, address=0x77, debug=False, *, refresh_rate=10):
|
| 336 |
+
"""Initialize the I2C device at the 'address' given"""
|
| 337 |
+
self._i2c = i2c
|
| 338 |
+
self._address = address
|
| 339 |
+
self._debug = debug
|
| 340 |
+
super().__init__(refresh_rate=refresh_rate)
|
| 341 |
+
|
| 342 |
+
def _read(self, register, length):
|
| 343 |
+
"""Returns an array of 'length' bytes from the 'register'"""
|
| 344 |
+
result = bytearray(length)
|
| 345 |
+
self._i2c.readfrom_mem_into(self._address, register & 0xff, result)
|
| 346 |
+
if self._debug:
|
| 347 |
+
print("\t${:x} read ".format(register), " ".join(["{:02x}".format(i) for i in result]))
|
| 348 |
+
return result
|
| 349 |
+
|
| 350 |
+
def _write(self, register, values):
|
| 351 |
+
"""Writes an array of 'length' bytes to the 'register'"""
|
| 352 |
+
if self._debug:
|
| 353 |
+
print("\t${:x} write".format(register), " ".join(["{:02x}".format(i) for i in values]))
|
| 354 |
+
for value in values:
|
| 355 |
+
self._i2c.writeto_mem(self._address, register, bytearray([value & 0xFF]))
|
| 356 |
+
register += 1
|
| 357 |
+
|
| 358 |
+
|
| 359 |
+
class BME680_SPI(Adafruit_BME680):
|
| 360 |
+
"""Driver for SPI connected BME680.
|
| 361 |
+
|
| 362 |
+
:param spi: SPI device object, configured
|
| 363 |
+
:param cs: Chip Select Pin object, configured to OUT mode
|
| 364 |
+
:param bool debug: Print debug statements when True.
|
| 365 |
+
:param int refresh_rate: Maximum number of readings per second. Faster property reads
|
| 366 |
+
will be from the previous reading.
|
| 367 |
+
"""
|
| 368 |
+
|
| 369 |
+
def __init__(self, spi, cs, debug=False, *, refresh_rate=10):
|
| 370 |
+
self._spi = spi
|
| 371 |
+
self._cs = cs
|
| 372 |
+
self._debug = debug
|
| 373 |
+
self._cs(1)
|
| 374 |
+
super().__init__(refresh_rate=refresh_rate)
|
| 375 |
+
|
| 376 |
+
def _read(self, register, length):
|
| 377 |
+
if register != _BME680_REG_PAGE_SELECT:
|
| 378 |
+
# _BME680_REG_PAGE_SELECT exists in both SPI memory pages
|
| 379 |
+
# For all other registers, we must set the correct memory page
|
| 380 |
+
self._set_spi_mem_page(register)
|
| 381 |
+
register = (register | 0x80) & 0xFF # Read single, bit 7 high.
|
| 382 |
+
|
| 383 |
+
try:
|
| 384 |
+
self._cs(0)
|
| 385 |
+
self._spi.write(bytearray([register])) # pylint: disable=no-member
|
| 386 |
+
result = bytearray(length)
|
| 387 |
+
self._spi.readinto(result) # pylint: disable=no-member
|
| 388 |
+
if self._debug:
|
| 389 |
+
print("\t${:x} read ".format(register), " ".join(["{:02x}".format(i) for i in result]))
|
| 390 |
+
except Exception as e:
|
| 391 |
+
print (e)
|
| 392 |
+
result = None
|
| 393 |
+
finally:
|
| 394 |
+
self._cs(1)
|
| 395 |
+
return result
|
| 396 |
+
|
| 397 |
+
def _write(self, register, values):
|
| 398 |
+
if register != _BME680_REG_PAGE_SELECT:
|
| 399 |
+
# _BME680_REG_PAGE_SELECT exists in both SPI memory pages
|
| 400 |
+
# For all other registers, we must set the correct memory page
|
| 401 |
+
self._set_spi_mem_page(register)
|
| 402 |
+
register &= 0x7F # Write, bit 7 low.
|
| 403 |
+
try:
|
| 404 |
+
self._cs(0)
|
| 405 |
+
buffer = bytearray(2 * len(values))
|
| 406 |
+
for i, value in enumerate(values):
|
| 407 |
+
buffer[2 * i] = register + i
|
| 408 |
+
buffer[2 * i + 1] = value & 0xFF
|
| 409 |
+
self._spi.write(buffer) # pylint: disable=no-member
|
| 410 |
+
if self._debug:
|
| 411 |
+
print("\t${:x} write".format(register), " ".join(["{:02x}".format(i) for i in values]))
|
| 412 |
+
except Exception as e:
|
| 413 |
+
print (e)
|
| 414 |
+
finally:
|
| 415 |
+
self._cs(1)
|
| 416 |
+
|
| 417 |
+
def _set_spi_mem_page(self, register):
|
| 418 |
+
spi_mem_page = 0x00
|
| 419 |
+
if register < 0x80:
|
| 420 |
+
spi_mem_page = 0x10
|
| 421 |
+
self._write(_BME680_REG_PAGE_SELECT, [spi_mem_page])
|
scripts/bme680i.py
ADDED
|
@@ -0,0 +1,418 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# The MIT License (MIT)
|
| 2 |
+
#
|
| 3 |
+
# Copyright (c) 2017 ladyada for Adafruit Industries
|
| 4 |
+
#
|
| 5 |
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
# of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
# in the Software without restriction, including without limitation the rights
|
| 8 |
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
# copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
# furnished to do so, subject to the following conditions:
|
| 11 |
+
#
|
| 12 |
+
# The above copyright notice and this permission notice shall be included in
|
| 13 |
+
# all copies or substantial portions of the Software.
|
| 14 |
+
#
|
| 15 |
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
| 21 |
+
# THE SOFTWARE.
|
| 22 |
+
|
| 23 |
+
# We have a lot of attributes for this complex sensor.
|
| 24 |
+
# pylint: disable=too-many-instance-attributes
|
| 25 |
+
|
| 26 |
+
"""
|
| 27 |
+
`adafruit_bme680` - Adafruit BME680 - Temperature, Humidity, Pressure & Gas Sensor
|
| 28 |
+
===================================================================================
|
| 29 |
+
|
| 30 |
+
CircuitPython driver from BME680 air quality sensor
|
| 31 |
+
|
| 32 |
+
* Author(s): ladyada
|
| 33 |
+
"""
|
| 34 |
+
|
| 35 |
+
import time
|
| 36 |
+
import math
|
| 37 |
+
from micropython import const
|
| 38 |
+
from ubinascii import hexlify as hex
|
| 39 |
+
try:
|
| 40 |
+
import struct
|
| 41 |
+
except ImportError:
|
| 42 |
+
import ustruct as struct
|
| 43 |
+
|
| 44 |
+
# I2C ADDRESS/BITS/SETTINGS
|
| 45 |
+
# -----------------------------------------------------------------------
|
| 46 |
+
_BME680_CHIPID = const(0x61)
|
| 47 |
+
|
| 48 |
+
_BME680_REG_CHIPID = const(0xD0)
|
| 49 |
+
_BME680_BME680_COEFF_ADDR1 = const(0x89)
|
| 50 |
+
_BME680_BME680_COEFF_ADDR2 = const(0xE1)
|
| 51 |
+
_BME680_BME680_RES_HEAT_0 = const(0x5A)
|
| 52 |
+
_BME680_BME680_GAS_WAIT_0 = const(0x64)
|
| 53 |
+
|
| 54 |
+
_BME680_REG_SOFTRESET = const(0xE0)
|
| 55 |
+
_BME680_REG_CTRL_GAS = const(0x71)
|
| 56 |
+
_BME680_REG_CTRL_HUM = const(0x72)
|
| 57 |
+
_BME280_REG_STATUS = const(0xF3)
|
| 58 |
+
_BME680_REG_CTRL_MEAS = const(0x74)
|
| 59 |
+
_BME680_REG_CONFIG = const(0x75)
|
| 60 |
+
|
| 61 |
+
_BME680_REG_PAGE_SELECT = const(0x73)
|
| 62 |
+
_BME680_REG_MEAS_STATUS = const(0x1D)
|
| 63 |
+
_BME680_REG_PDATA = const(0x1F)
|
| 64 |
+
_BME680_REG_TDATA = const(0x22)
|
| 65 |
+
_BME680_REG_HDATA = const(0x25)
|
| 66 |
+
|
| 67 |
+
_BME680_SAMPLERATES = (0, 1, 2, 4, 8, 16)
|
| 68 |
+
_BME680_FILTERSIZES = (0, 1, 3, 7, 15, 31, 63, 127)
|
| 69 |
+
|
| 70 |
+
_BME680_RUNGAS = const(0x10)
|
| 71 |
+
|
| 72 |
+
_LOOKUP_TABLE_1 = (2147483647, 2147483647, 2147483647, 2147483647, 2147483647,
|
| 73 |
+
2126008810, 2147483647, 2130303777, 2147483647, 2147483647,
|
| 74 |
+
2143188679, 2136746228, 2147483647, 2126008810, 2147483647,
|
| 75 |
+
2147483647)
|
| 76 |
+
|
| 77 |
+
_LOOKUP_TABLE_2 = (4096000000, 2048000000, 1024000000, 512000000, 255744255, 127110228,
|
| 78 |
+
64000000, 32258064, 16016016, 8000000, 4000000, 2000000, 1000000,
|
| 79 |
+
500000, 250000, 125000)
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def _read24(arr):
|
| 83 |
+
"""Parse an unsigned 24-bit value as a floating point and return it."""
|
| 84 |
+
ret = 0
|
| 85 |
+
#print([hex(i) for i in arr])
|
| 86 |
+
for b in arr:
|
| 87 |
+
ret <<= 8
|
| 88 |
+
ret += b & 0xFF
|
| 89 |
+
return ret
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
class Adafruit_BME680:
|
| 93 |
+
"""Driver from BME680 air quality sensor
|
| 94 |
+
|
| 95 |
+
:param int refresh_rate: Maximum number of readings per second. Faster property reads
|
| 96 |
+
will be from the previous reading."""
|
| 97 |
+
def __init__(self, *, refresh_rate=10):
|
| 98 |
+
"""Check the BME680 was found, read the coefficients and enable the sensor for continuous
|
| 99 |
+
reads."""
|
| 100 |
+
self._write(_BME680_REG_SOFTRESET, [0xB6])
|
| 101 |
+
time.sleep(0.005)
|
| 102 |
+
|
| 103 |
+
# Check device ID.
|
| 104 |
+
chip_id = self._read_byte(_BME680_REG_CHIPID)
|
| 105 |
+
if chip_id != _BME680_CHIPID:
|
| 106 |
+
raise RuntimeError('Failed to find BME680! Chip ID 0x%x' % chip_id)
|
| 107 |
+
|
| 108 |
+
self._read_calibration()
|
| 109 |
+
|
| 110 |
+
# set up heater
|
| 111 |
+
self._write(_BME680_BME680_RES_HEAT_0, [0x73])
|
| 112 |
+
self._write(_BME680_BME680_GAS_WAIT_0, [0x65])
|
| 113 |
+
|
| 114 |
+
self.sea_level_pressure = 1013.25
|
| 115 |
+
"""Pressure in hectoPascals at sea level. Used to calibrate ``altitude``."""
|
| 116 |
+
|
| 117 |
+
# Default oversampling and filter register values.
|
| 118 |
+
self._pressure_oversample = 0b011
|
| 119 |
+
self._temp_oversample = 0b100
|
| 120 |
+
self._humidity_oversample = 0b010
|
| 121 |
+
self._filter = 0b010
|
| 122 |
+
|
| 123 |
+
self._adc_pres = None
|
| 124 |
+
self._adc_temp = None
|
| 125 |
+
self._adc_hum = None
|
| 126 |
+
self._adc_gas = None
|
| 127 |
+
self._gas_range = None
|
| 128 |
+
self._t_fine = None
|
| 129 |
+
|
| 130 |
+
self._last_reading = time.ticks_ms()
|
| 131 |
+
self._min_refresh_time = 1000 // refresh_rate
|
| 132 |
+
|
| 133 |
+
@property
|
| 134 |
+
def pressure_oversample(self):
|
| 135 |
+
"""The oversampling for pressure sensor"""
|
| 136 |
+
return _BME680_SAMPLERATES[self._pressure_oversample]
|
| 137 |
+
|
| 138 |
+
@pressure_oversample.setter
|
| 139 |
+
def pressure_oversample(self, sample_rate):
|
| 140 |
+
if sample_rate in _BME680_SAMPLERATES:
|
| 141 |
+
self._pressure_oversample = _BME680_SAMPLERATES.index(sample_rate)
|
| 142 |
+
else:
|
| 143 |
+
raise RuntimeError("Invalid oversample")
|
| 144 |
+
|
| 145 |
+
@property
|
| 146 |
+
def humidity_oversample(self):
|
| 147 |
+
"""The oversampling for humidity sensor"""
|
| 148 |
+
return _BME680_SAMPLERATES[self._humidity_oversample]
|
| 149 |
+
|
| 150 |
+
@humidity_oversample.setter
|
| 151 |
+
def humidity_oversample(self, sample_rate):
|
| 152 |
+
if sample_rate in _BME680_SAMPLERATES:
|
| 153 |
+
self._humidity_oversample = _BME680_SAMPLERATES.index(sample_rate)
|
| 154 |
+
else:
|
| 155 |
+
raise RuntimeError("Invalid oversample")
|
| 156 |
+
|
| 157 |
+
@property
|
| 158 |
+
def temperature_oversample(self):
|
| 159 |
+
"""The oversampling for temperature sensor"""
|
| 160 |
+
return _BME680_SAMPLERATES[self._temp_oversample]
|
| 161 |
+
|
| 162 |
+
@temperature_oversample.setter
|
| 163 |
+
def temperature_oversample(self, sample_rate):
|
| 164 |
+
if sample_rate in _BME680_SAMPLERATES:
|
| 165 |
+
self._temp_oversample = _BME680_SAMPLERATES.index(sample_rate)
|
| 166 |
+
else:
|
| 167 |
+
raise RuntimeError("Invalid oversample")
|
| 168 |
+
|
| 169 |
+
@property
|
| 170 |
+
def filter_size(self):
|
| 171 |
+
"""The filter size for the built in IIR filter"""
|
| 172 |
+
return _BME680_FILTERSIZES[self._filter]
|
| 173 |
+
|
| 174 |
+
@filter_size.setter
|
| 175 |
+
def filter_size(self, size):
|
| 176 |
+
if size in _BME680_FILTERSIZES:
|
| 177 |
+
self._filter = _BME680_FILTERSIZES[size]
|
| 178 |
+
else:
|
| 179 |
+
raise RuntimeError("Invalid size")
|
| 180 |
+
|
| 181 |
+
@property
|
| 182 |
+
def temperature(self):
|
| 183 |
+
"""The compensated temperature in degrees celsius."""
|
| 184 |
+
self._perform_reading()
|
| 185 |
+
calc_temp = (((self._t_fine * 5) + 128) // 256)
|
| 186 |
+
return calc_temp / 100
|
| 187 |
+
|
| 188 |
+
@property
|
| 189 |
+
def pressure(self):
|
| 190 |
+
"""The barometric pressure in hectoPascals"""
|
| 191 |
+
self._perform_reading()
|
| 192 |
+
|
| 193 |
+
var1 = (int(self._t_fine) >> 1) - 64000
|
| 194 |
+
var2 = ((var1 >> 2) * (var1 >> 2 )) >> 11
|
| 195 |
+
var2 = (var2 * self._pressure_calibration[5]) >> 2
|
| 196 |
+
var2 = var2 + ((var1 * self._pressure_calibration[4]) << 1)
|
| 197 |
+
var2 = (var2 >> 2) + (self._pressure_calibration[3] << 16)
|
| 198 |
+
var1 = (((((var1 >> 2) * (var1 >> 2)) >> 13) *
|
| 199 |
+
(self._pressure_calibration[2] << 5) >> 3) +
|
| 200 |
+
((self._pressure_calibration[1] * var1) >> 1))
|
| 201 |
+
var1 = var1 >> 18
|
| 202 |
+
var1 = ((32768 + var1) * self._pressure_calibration[0]) >> 15
|
| 203 |
+
calc_pres = 1048576 - int(self._adc_pres)
|
| 204 |
+
calc_pres = (calc_pres - (var2 >> 12)) * 3125
|
| 205 |
+
calc_pres = (calc_pres << 1) // var1
|
| 206 |
+
var1 = (self._pressure_calibration[8] * (((calc_pres >> 3) * (calc_pres >> 3)) >> 13)) >> 12
|
| 207 |
+
var2 = ((calc_pres >> 2) * self._pressure_calibration[7]) >> 13
|
| 208 |
+
var3 = (((calc_pres >> 8) * (calc_pres >> 8) * (calc_pres >> 8)) * self._pressure_calibration[9]) >> 17
|
| 209 |
+
calc_pres += ((var1 + var2 + var3 + (self._pressure_calibration[6] << 7)) >> 4)
|
| 210 |
+
return calc_pres / 100
|
| 211 |
+
|
| 212 |
+
@property
|
| 213 |
+
def humidity(self):
|
| 214 |
+
"""The relative humidity in RH %"""
|
| 215 |
+
self._perform_reading()
|
| 216 |
+
|
| 217 |
+
temp_scaled = ((self._t_fine * 5) + 128) >> 8
|
| 218 |
+
var1 = ((self._adc_hum - (self._humidity_calibration[0] * 16)) -
|
| 219 |
+
(((temp_scaled * self._humidity_calibration[2]) // 100) >> 1))
|
| 220 |
+
var2 = (self._humidity_calibration[1] *
|
| 221 |
+
(((temp_scaled * self._humidity_calibration[3]) // 100) +
|
| 222 |
+
(((temp_scaled * ((temp_scaled * self._humidity_calibration[4])
|
| 223 |
+
// 100)) >> 6) // 100) + 16384)) >> 10
|
| 224 |
+
var3 = var1 * var2
|
| 225 |
+
var4 = self._humidity_calibration[5] << 7
|
| 226 |
+
var4 = (var4 + ((temp_scaled * self._humidity_calibration[6]) // 100)) >> 4
|
| 227 |
+
var5 = ((var3 >> 14) * (var3 >> 14)) >> 10
|
| 228 |
+
var6 = (var4 * var5) >> 1
|
| 229 |
+
calc_hum = ((var3 + var6) >> 10) / 4096
|
| 230 |
+
|
| 231 |
+
if calc_hum > 10000:
|
| 232 |
+
calc_hum = 10000
|
| 233 |
+
if calc_hum < 0:
|
| 234 |
+
calc_hum = 0
|
| 235 |
+
return calc_hum
|
| 236 |
+
|
| 237 |
+
@property
|
| 238 |
+
def altitude(self):
|
| 239 |
+
"""The altitude based on current ``pressure`` vs the sea level pressure
|
| 240 |
+
(``sea_level_pressure``) - which you must enter ahead of time)"""
|
| 241 |
+
pressure = self.pressure # in Si units for hPascal
|
| 242 |
+
return 44330 * (1.0 - math.pow(pressure / self.sea_level_pressure, 0.1903))
|
| 243 |
+
|
| 244 |
+
@property
|
| 245 |
+
def gas(self):
|
| 246 |
+
"""The gas resistance in ohms"""
|
| 247 |
+
self._perform_reading()
|
| 248 |
+
var1 = ((1340 + (5 * self._sw_err)) * (_LOOKUP_TABLE_1[self._gas_range])) >> 16
|
| 249 |
+
var2 = ((self._adc_gas << 15) - 16777216) + var1
|
| 250 |
+
var3 = (_LOOKUP_TABLE_2[self._gas_range] * var1) >> 9
|
| 251 |
+
calc_gas_res = (var3 + (var2 >> 1)) // var2
|
| 252 |
+
return int(calc_gas_res)
|
| 253 |
+
|
| 254 |
+
def _perform_reading(self):
|
| 255 |
+
"""Perform a single-shot reading from the sensor and fill internal data structure for
|
| 256 |
+
calculations"""
|
| 257 |
+
expired = time.ticks_diff(self._last_reading, time.ticks_ms()) * time.ticks_diff(0, 1)
|
| 258 |
+
if 0 <= expired < self._min_refresh_time:
|
| 259 |
+
time.sleep_ms(self._min_refresh_time - expired)
|
| 260 |
+
|
| 261 |
+
# set filter
|
| 262 |
+
self._write(_BME680_REG_CONFIG, [self._filter << 2])
|
| 263 |
+
# turn on temp oversample & pressure oversample
|
| 264 |
+
self._write(_BME680_REG_CTRL_MEAS,
|
| 265 |
+
[(self._temp_oversample << 5)|(self._pressure_oversample << 2)])
|
| 266 |
+
# turn on humidity oversample
|
| 267 |
+
self._write(_BME680_REG_CTRL_HUM, [self._humidity_oversample])
|
| 268 |
+
# gas measurements enabled
|
| 269 |
+
self._write(_BME680_REG_CTRL_GAS, [_BME680_RUNGAS])
|
| 270 |
+
|
| 271 |
+
ctrl = self._read_byte(_BME680_REG_CTRL_MEAS)
|
| 272 |
+
ctrl = (ctrl & 0xFC) | 0x01 # enable single shot!
|
| 273 |
+
self._write(_BME680_REG_CTRL_MEAS, [ctrl])
|
| 274 |
+
new_data = False
|
| 275 |
+
while not new_data:
|
| 276 |
+
data = self._read(_BME680_REG_MEAS_STATUS, 15)
|
| 277 |
+
new_data = data[0] & 0x80 != 0
|
| 278 |
+
time.sleep(0.005)
|
| 279 |
+
self._last_reading = time.ticks_ms()
|
| 280 |
+
|
| 281 |
+
self._adc_pres = _read24(data[2:5]) / 16
|
| 282 |
+
self._adc_temp = _read24(data[5:8]) / 16
|
| 283 |
+
self._adc_hum = struct.unpack('>H', bytes(data[8:10]))[0]
|
| 284 |
+
self._adc_gas = int(struct.unpack('>H', bytes(data[13:15]))[0] / 64)
|
| 285 |
+
self._gas_range = data[14] & 0x0F
|
| 286 |
+
|
| 287 |
+
var1 = (int(self._adc_temp) >> 3) - (self._temp_calibration[0] << 1)
|
| 288 |
+
var2 = (var1 * self._temp_calibration[1]) >> 11
|
| 289 |
+
var3 = ((var1 >> 1) * (var1 >> 1)) >> 12
|
| 290 |
+
var3 = (var3 * self._temp_calibration[2] << 4) >> 14
|
| 291 |
+
self._t_fine = int(var2 + var3)
|
| 292 |
+
|
| 293 |
+
def _read_calibration(self):
|
| 294 |
+
"""Read & save the calibration coefficients"""
|
| 295 |
+
coeff = self._read(_BME680_BME680_COEFF_ADDR1, 25)
|
| 296 |
+
coeff += self._read(_BME680_BME680_COEFF_ADDR2, 16)
|
| 297 |
+
|
| 298 |
+
coeff = list(struct.unpack('<hbBHhbBhhbbHhhBBBHbbbBbHhbb', bytes(coeff[1:39])))
|
| 299 |
+
# print("\n\n",coeff)
|
| 300 |
+
self._temp_calibration = [coeff[x] for x in [23, 0, 1]]
|
| 301 |
+
self._pressure_calibration = [coeff[x] for x in [3, 4, 5, 7, 8, 10, 9, 12, 13, 14]]
|
| 302 |
+
self._humidity_calibration = [coeff[x] for x in [17, 16, 18, 19, 20, 21, 22]]
|
| 303 |
+
self._gas_calibration = [coeff[x] for x in [25, 24, 26]]
|
| 304 |
+
|
| 305 |
+
# flip around H1 & H2
|
| 306 |
+
self._humidity_calibration[1] <<= 4
|
| 307 |
+
self._humidity_calibration[1] |= self._humidity_calibration[0] & 0x0f
|
| 308 |
+
self._humidity_calibration[0] >>= 4
|
| 309 |
+
|
| 310 |
+
self._heat_range = (self._read_byte(0x02) & 0x30) >> 4
|
| 311 |
+
self._heat_val = self._read_byte(0x00)
|
| 312 |
+
self._sw_err = (self._read_byte(0x04) & 0xF0) >> 4
|
| 313 |
+
|
| 314 |
+
def _read_byte(self, register):
|
| 315 |
+
"""Read a byte register value and return it"""
|
| 316 |
+
return self._read(register, 1)[0]
|
| 317 |
+
|
| 318 |
+
def _read(self, register, length):
|
| 319 |
+
raise NotImplementedError()
|
| 320 |
+
|
| 321 |
+
def _write(self, register, values):
|
| 322 |
+
raise NotImplementedError()
|
| 323 |
+
|
| 324 |
+
class BME680_I2C(Adafruit_BME680):
|
| 325 |
+
"""Driver for I2C connected BME680.
|
| 326 |
+
|
| 327 |
+
:param i2c: I2C device object
|
| 328 |
+
:param int address: I2C device address
|
| 329 |
+
:param bool debug: Print debug statements when True.
|
| 330 |
+
:param int refresh_rate: Maximum number of readings per second. Faster property reads
|
| 331 |
+
will be from the previous reading."""
|
| 332 |
+
def __init__(self, i2c, address=0x77, debug=False, *, refresh_rate=10):
|
| 333 |
+
"""Initialize the I2C device at the 'address' given"""
|
| 334 |
+
self._i2c = i2c
|
| 335 |
+
self._address = address
|
| 336 |
+
self._debug = debug
|
| 337 |
+
super().__init__(refresh_rate=refresh_rate)
|
| 338 |
+
|
| 339 |
+
def _read(self, register, length):
|
| 340 |
+
"""Returns an array of 'length' bytes from the 'register'"""
|
| 341 |
+
result = bytearray(length)
|
| 342 |
+
self._i2c.readfrom_mem_into(self._address, register & 0xff, result)
|
| 343 |
+
if self._debug:
|
| 344 |
+
print("\t${:x} read ".format(register), " ".join(["{:02x}".format(i) for i in result]))
|
| 345 |
+
return result
|
| 346 |
+
|
| 347 |
+
def _write(self, register, values):
|
| 348 |
+
"""Writes an array of 'length' bytes to the 'register'"""
|
| 349 |
+
if self._debug:
|
| 350 |
+
print("\t${:x} write".format(register), " ".join(["{:02x}".format(i) for i in values]))
|
| 351 |
+
for value in values:
|
| 352 |
+
self._i2c.writeto_mem(self._address, register, bytearray([value & 0xFF]))
|
| 353 |
+
register += 1
|
| 354 |
+
|
| 355 |
+
|
| 356 |
+
class BME680_SPI(Adafruit_BME680):
|
| 357 |
+
"""Driver for SPI connected BME680.
|
| 358 |
+
|
| 359 |
+
:param spi: SPI device object, configured
|
| 360 |
+
:param cs: Chip Select Pin object, configured to OUT mode
|
| 361 |
+
:param bool debug: Print debug statements when True.
|
| 362 |
+
:param int refresh_rate: Maximum number of readings per second. Faster property reads
|
| 363 |
+
will be from the previous reading.
|
| 364 |
+
"""
|
| 365 |
+
|
| 366 |
+
def __init__(self, spi, cs, debug=False, *, refresh_rate=10):
|
| 367 |
+
self._spi = spi
|
| 368 |
+
self._cs = cs
|
| 369 |
+
self._debug = debug
|
| 370 |
+
self._cs(1)
|
| 371 |
+
super().__init__(refresh_rate=refresh_rate)
|
| 372 |
+
|
| 373 |
+
def _read(self, register, length):
|
| 374 |
+
if register != _BME680_REG_PAGE_SELECT:
|
| 375 |
+
# _BME680_REG_PAGE_SELECT exists in both SPI memory pages
|
| 376 |
+
# For all other registers, we must set the correct memory page
|
| 377 |
+
self._set_spi_mem_page(register)
|
| 378 |
+
register = (register | 0x80) & 0xFF # Read single, bit 7 high.
|
| 379 |
+
|
| 380 |
+
try:
|
| 381 |
+
self._cs(0)
|
| 382 |
+
self._spi.write(bytearray([register])) # pylint: disable=no-member
|
| 383 |
+
result = bytearray(length)
|
| 384 |
+
self._spi.readinto(result) # pylint: disable=no-member
|
| 385 |
+
if self._debug:
|
| 386 |
+
print("\t${:x} read ".format(register), " ".join(["{:02x}".format(i) for i in result]))
|
| 387 |
+
except Exception as e:
|
| 388 |
+
print (e)
|
| 389 |
+
result = None
|
| 390 |
+
finally:
|
| 391 |
+
self._cs(1)
|
| 392 |
+
return result
|
| 393 |
+
|
| 394 |
+
def _write(self, register, values):
|
| 395 |
+
if register != _BME680_REG_PAGE_SELECT:
|
| 396 |
+
# _BME680_REG_PAGE_SELECT exists in both SPI memory pages
|
| 397 |
+
# For all other registers, we must set the correct memory page
|
| 398 |
+
self._set_spi_mem_page(register)
|
| 399 |
+
register &= 0x7F # Write, bit 7 low.
|
| 400 |
+
try:
|
| 401 |
+
self._cs(0)
|
| 402 |
+
buffer = bytearray(2 * len(values))
|
| 403 |
+
for i, value in enumerate(values):
|
| 404 |
+
buffer[2 * i] = register + i
|
| 405 |
+
buffer[2 * i + 1] = value & 0xFF
|
| 406 |
+
self._spi.write(buffer) # pylint: disable=no-member
|
| 407 |
+
if self._debug:
|
| 408 |
+
print("\t${:x} write".format(register), " ".join(["{:02x}".format(i) for i in values]))
|
| 409 |
+
except Exception as e:
|
| 410 |
+
print (e)
|
| 411 |
+
finally:
|
| 412 |
+
self._cs(1)
|
| 413 |
+
|
| 414 |
+
def _set_spi_mem_page(self, register):
|
| 415 |
+
spi_mem_page = 0x00
|
| 416 |
+
if register < 0x80:
|
| 417 |
+
spi_mem_page = 0x10
|
| 418 |
+
self._write(_BME680_REG_PAGE_SELECT, [spi_mem_page])
|
scripts/constants.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# constants.py
|
| 2 |
+
|
| 3 |
+
# Wi-Fi Configuration
|
| 4 |
+
WIFI_SSID = 'Pixel 8'
|
| 5 |
+
WIFI_PASSWORD = '123456789'
|
| 6 |
+
|
| 7 |
+
# MQTT Configuration
|
| 8 |
+
MQTT_BROKER = 'b6bdb89571144b3d8e5ca4bbe666ddb5.s1.eu.hivemq.cloud'
|
| 9 |
+
MQTT_PORT = 8883
|
| 10 |
+
MQTT_TOPIC = 'sensors/bme680/data'
|
| 11 |
+
MQTT_USER = 'LuthiraMQ'
|
| 12 |
+
MQTT_PASSWORD = 'Password1118.'
|
scripts/demo.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from bme680 import *
|
| 2 |
+
from machine import I2C, Pin
|
| 3 |
+
import time
|
| 4 |
+
|
| 5 |
+
i2c = I2C(0, scl=Pin(5), sda=Pin(4))
|
| 6 |
+
|
| 7 |
+
bme = BME680_I2C(i2c)
|
| 8 |
+
|
| 9 |
+
while True:
|
| 10 |
+
print("--------------------------------------------------")
|
| 11 |
+
print()
|
| 12 |
+
print("Temperature: {:.2f} °C".format(bme.temperature))
|
| 13 |
+
print("Humidity: {:.2f} %".format(bme.humidity))
|
| 14 |
+
print("Pressure: {:.2f} hPa".format(bme.pressure))
|
| 15 |
+
print("Gas: {:.2f} ohms".format(bme.gas))
|
| 16 |
+
print()
|
| 17 |
+
time.sleep(3)
|
| 18 |
+
|
scripts/hivemq-com-chain.der
ADDED
|
Binary file (1.29 kB). View file
|
|
|
scripts/lib/CHANGELOG.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
2.0.0
|
| 2 |
+
-----
|
| 3 |
+
|
| 4 |
+
* Repackage to hatch/pyproject.toml
|
| 5 |
+
* Drop Python 2.7 support
|
| 6 |
+
* Switch from smbu2 to smbus2
|
| 7 |
+
|
| 8 |
+
1.1.1
|
| 9 |
+
-----
|
| 10 |
+
|
| 11 |
+
* New: constants to clarify heater on/off states
|
| 12 |
+
|
| 13 |
+
1.1.0
|
| 14 |
+
-----
|
| 15 |
+
|
| 16 |
+
* New: support for BME688 "high" gas resistance variant
|
| 17 |
+
* New: set/get gas heater disable bit
|
| 18 |
+
* Enhancement: fail with descriptive RuntimeError when chip is not detected
|
| 19 |
+
|
| 20 |
+
1.0.5
|
| 21 |
+
-----
|
| 22 |
+
|
| 23 |
+
* New: set_temp_offset to calibrate temperature offset in degrees C
|
| 24 |
+
|
| 25 |
+
1.0.4
|
| 26 |
+
-----
|
| 27 |
+
|
| 28 |
+
* Fix to range_sw_err for extremely high gas readings
|
| 29 |
+
* Convert to unsigned int to fix negative gas readings
|
| 30 |
+
|
| 31 |
+
1.0.3
|
| 32 |
+
-----
|
| 33 |
+
|
| 34 |
+
* Merged temperature compensation fix from Bosch's BME680_driver 3.5.3
|
| 35 |
+
|
| 36 |
+
1.0.2
|
| 37 |
+
-----
|
| 38 |
+
|
| 39 |
+
* Fixed set_gas_heater_temperature to avoid i2c TypeError
|
| 40 |
+
|
| 41 |
+
1.0.1
|
| 42 |
+
-----
|
| 43 |
+
|
| 44 |
+
* Added Manifest to Python package
|
| 45 |
+
|
| 46 |
+
1.0.0
|
| 47 |
+
-----
|
| 48 |
+
|
| 49 |
+
* Initial release
|
| 50 |
+
|
scripts/lib/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2018 Pimoroni Ltd
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
scripts/lib/README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# BME680
|
| 2 |
+
|
| 3 |
+
[](https://github.com/pimoroni/bme680-python/actions/workflows/test.yml)
|
| 4 |
+
[](https://coveralls.io/github/pimoroni/bme680-python?branch=main)
|
| 5 |
+
[](https://pypi.python.org/pypi/bme680)
|
| 6 |
+
[](https://pypi.python.org/pypi/bme680)
|
| 7 |
+
|
| 8 |
+
https://shop.pimoroni.com/products/bme680
|
| 9 |
+
|
| 10 |
+
The state-of-the-art BME680 breakout lets you measure temperature, pressure, humidity, and indoor air quality.
|
| 11 |
+
|
| 12 |
+
## Installing
|
| 13 |
+
|
| 14 |
+
### Full install (recommended):
|
| 15 |
+
|
| 16 |
+
We've created an easy installation script that will install all pre-requisites and get your BME680
|
| 17 |
+
up and running with minimal efforts. To run it, fire up Terminal which you'll find in Menu -> Accessories -> Terminal
|
| 18 |
+
on your Raspberry Pi desktop, as illustrated below:
|
| 19 |
+
|
| 20 |
+

|
| 21 |
+
|
| 22 |
+
In the new terminal window type the command exactly as it appears below (check for typos) and follow the on-screen instructions:
|
| 23 |
+
|
| 24 |
+
```bash
|
| 25 |
+
git clone https://github.com/pimoroni/bme680-python
|
| 26 |
+
cd bme680-python
|
| 27 |
+
./install.sh
|
| 28 |
+
```
|
| 29 |
+
|
| 30 |
+
**Note** Libraries will be installed in the "pimoroni" virtual environment, you will need to activate it to run examples:
|
| 31 |
+
|
| 32 |
+
```
|
| 33 |
+
source ~/.virtualenvs/pimoroni/bin/activate
|
| 34 |
+
```
|
| 35 |
+
|
| 36 |
+
### Development:
|
| 37 |
+
|
| 38 |
+
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:
|
| 39 |
+
|
| 40 |
+
```bash
|
| 41 |
+
git clone https://github.com/pimoroni/bme680-python
|
| 42 |
+
cd bme680-python
|
| 43 |
+
./install.sh --unstable
|
| 44 |
+
```
|
| 45 |
+
|
| 46 |
+
In all cases you will have to enable the i2c bus:
|
| 47 |
+
|
| 48 |
+
```
|
| 49 |
+
sudo raspi-config nonint do_i2c 0
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
## Documentation & Support
|
| 53 |
+
|
| 54 |
+
* Guides and tutorials - https://learn.pimoroni.com/bme680-breakout
|
| 55 |
+
* Get help - http://forums.pimoroni.com/c/support
|
| 56 |
+
|
scripts/lib/adafruit_blinka-8.49.0.dist-info/METADATA
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Metadata-Version: 2.1
|
| 2 |
+
Name: adafruit-blinka
|
| 3 |
+
Version: 8.49.0
|
| 4 |
+
Summary: Dummy package for satisfying formal requirements
|
| 5 |
+
Home-page: ?
|
| 6 |
+
Author: ?
|
| 7 |
+
|
| 8 |
+
?
|
scripts/lib/adafruit_blinka-8.49.0.dist-info/RECORD
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
adafruit_blinka-8.49.0.dist-info/METADATA,,
|
| 2 |
+
adafruit_blinka-8.49.0.dist-info/RECORD,,
|
scripts/lib/as7341.py
ADDED
|
@@ -0,0 +1,608 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
This file licensed under the MIT License and incorporates work covered by
|
| 3 |
+
the following copyright and permission notice:
|
| 4 |
+
|
| 5 |
+
The MIT License (MIT)
|
| 6 |
+
|
| 7 |
+
Copyright (c) 2022-2022 Rob Hamerling
|
| 8 |
+
|
| 9 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 10 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 11 |
+
in the Software without restriction, including without limitation the rights
|
| 12 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 13 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 14 |
+
furnished to do so, subject to the following conditions:
|
| 15 |
+
|
| 16 |
+
The above copyright notice and this permission notice shall be included in
|
| 17 |
+
all copies or substantial portions of the Software.
|
| 18 |
+
|
| 19 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 20 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 21 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 22 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 23 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 24 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
| 25 |
+
THE SOFTWARE.
|
| 26 |
+
|
| 27 |
+
Rob Hamerling, Version 0.0, August 2022
|
| 28 |
+
|
| 29 |
+
Original by WaveShare for Raspberry Pi, part of:
|
| 30 |
+
https://www.waveshare.com/w/upload/b/b3/AS7341_Spectral_Color_Sensor_code.7z
|
| 31 |
+
|
| 32 |
+
Converted to Micropython for use with MicroPython devices such as ESP32
|
| 33 |
+
- pythonized (in stead of 'literal' translation of C code)
|
| 34 |
+
- instance of AS7341 requires specification of I2C interface
|
| 35 |
+
- added I2C read/write error detection
|
| 36 |
+
- added check for connected AS7341 incl. device ID
|
| 37 |
+
- some code optimization (esp. adding I2C word/block reads/writes)
|
| 38 |
+
- Replaced bit addressing like (1<<5) by symbolic name with bit mask
|
| 39 |
+
- moved SMUX settings for predefined channel mappings to a dictionary
|
| 40 |
+
and as a separate file to allow changes or additional configurations
|
| 41 |
+
by the user without changing the driver
|
| 42 |
+
- several changes of names of functions and constants
|
| 43 |
+
(incl. camel case -> word separators with underscores)
|
| 44 |
+
- added comments, doc-strings with explanation and/or argumentation
|
| 45 |
+
- several other improvements and some corrections
|
| 46 |
+
|
| 47 |
+
Remarks:
|
| 48 |
+
- Automatic Gain Control (AGC) is not supported
|
| 49 |
+
- No provisions for SYND mode
|
| 50 |
+
|
| 51 |
+
"""
|
| 52 |
+
|
| 53 |
+
from time import sleep_ms
|
| 54 |
+
|
| 55 |
+
from as7341_smux_select import * # predefined SMUX configurations
|
| 56 |
+
|
| 57 |
+
AS7341_I2C_ADDRESS = const(0x39) # I2C address of AS7341
|
| 58 |
+
AS7341_ID_VALUE = const(0x24) # AS7341 Part Number Identification
|
| 59 |
+
# (excl 2 low order bits)
|
| 60 |
+
|
| 61 |
+
# Symbolic names for registers and some selected bit fields
|
| 62 |
+
# Note: ASTATUS, ITIME and CHx_DATA in address range 0x60--0x6F are not used
|
| 63 |
+
AS7341_CONFIG = const(0x70)
|
| 64 |
+
AS7341_CONFIG_INT_MODE_SPM = const(0x00)
|
| 65 |
+
AS7341_MODE_SPM = AS7341_CONFIG_INT_MODE_SPM # alias
|
| 66 |
+
AS7341_CONFIG_INT_MODE_SYNS = const(0x01)
|
| 67 |
+
AS7341_MODE_SYNS = AS7341_CONFIG_INT_MODE_SYNS # alias
|
| 68 |
+
AS7341_CONFIG_INT_MODE_SYND = const(0x03)
|
| 69 |
+
AS7341_MODE_SYND = AS7341_CONFIG_INT_MODE_SYND # alias
|
| 70 |
+
AS7341_CONFIG_INT_SEL = const(0x04)
|
| 71 |
+
AS7341_CONFIG_LED_SEL = const(0x08)
|
| 72 |
+
AS7341_STAT = const(0x71)
|
| 73 |
+
AS7341_STAT_READY = const(0x01)
|
| 74 |
+
AS7341_STAT_WAIT_SYNC = const(0x02)
|
| 75 |
+
AS7341_EDGE = const(0x72)
|
| 76 |
+
AS7341_GPIO = const(0x73)
|
| 77 |
+
AS7341_GPIO_PD_INT = const(0x01)
|
| 78 |
+
AS7341_GPIO_PD_GPIO = const(0x02)
|
| 79 |
+
AS7341_LED = const(0x74)
|
| 80 |
+
AS7341_LED_LED_ACT = const(0x80)
|
| 81 |
+
AS7341_ENABLE = const(0x80)
|
| 82 |
+
AS7341_ENABLE_PON = const(0x01)
|
| 83 |
+
AS7341_ENABLE_SP_EN = const(0x02)
|
| 84 |
+
AS7341_ENABLE_WEN = const(0x08)
|
| 85 |
+
AS7341_ENABLE_SMUXEN = const(0x10)
|
| 86 |
+
AS7341_ENABLE_FDEN = const(0x40)
|
| 87 |
+
AS7341_ATIME = const(0x81)
|
| 88 |
+
AS7341_WTIME = const(0x83)
|
| 89 |
+
AS7341_SP_TH_LOW = const(0x84)
|
| 90 |
+
AS7341_SP_TH_L_LSB = const(0x84)
|
| 91 |
+
AS7341_SP_TH_L_MSB = const(0x85)
|
| 92 |
+
AS7341_SP_TH_HIGH = const(0x86)
|
| 93 |
+
AS7341_SP_TH_H_LSB = const(0x86)
|
| 94 |
+
AS7341_SP_TH_H_MSB = const(0x87)
|
| 95 |
+
AS7341_AUXID = const(0x90)
|
| 96 |
+
AS7341_REVID = const(0x91)
|
| 97 |
+
AS7341_ID = const(0x92)
|
| 98 |
+
AS7341_STATUS = const(0x93)
|
| 99 |
+
AS7341_STATUS_ASAT = const(0x80)
|
| 100 |
+
AS7341_STATUS_AINT = const(0x08)
|
| 101 |
+
AS7341_STATUS_FINT = const(0x04)
|
| 102 |
+
AS7341_STATUS_C_INT = const(0x02)
|
| 103 |
+
AS7341_STATUS_SINT = const(0x01)
|
| 104 |
+
AS7341_ASTATUS = const(0x94) # start of bulk read (incl 6 counts)
|
| 105 |
+
AS7341_ASTATUS_ASAT_STATUS = const(0x80)
|
| 106 |
+
AS7341_ASTATUS_AGAIN_STATUS = const(0x0F)
|
| 107 |
+
AS7341_CH_DATA = const(0x95) # start of the 6 channel counts
|
| 108 |
+
AS7341_CH0_DATA_L = const(0x95)
|
| 109 |
+
AS7341_CH0_DATA_H = const(0x96)
|
| 110 |
+
AS7341_CH1_DATA_L = const(0x97)
|
| 111 |
+
AS7341_CH1_DATA_H = const(0x98)
|
| 112 |
+
AS7341_CH2_DATA_L = const(0x99)
|
| 113 |
+
AS7341_CH2_DATA_H = const(0x9A)
|
| 114 |
+
AS7341_CH3_DATA_L = const(0x9B)
|
| 115 |
+
AS7341_CH3_DATA_H = const(0x9C)
|
| 116 |
+
AS7341_CH4_DATA_L = const(0x9D)
|
| 117 |
+
AS7341_CH4_DATA_H = const(0x9E)
|
| 118 |
+
AS7341_CH5_DATA_L = const(0x9F)
|
| 119 |
+
AS7341_CH5_DATA_H = const(0xA0)
|
| 120 |
+
AS7341_STATUS_2 = const(0xA3)
|
| 121 |
+
AS7341_STATUS_2_AVALID = const(0x40)
|
| 122 |
+
AS7341_STATUS_3 = const(0xA4)
|
| 123 |
+
AS7341_STATUS_5 = const(0xA6)
|
| 124 |
+
AS7341_STATUS_6 = const(0xA7)
|
| 125 |
+
AS7341_CFG_0 = const(0xA9)
|
| 126 |
+
AS7341_CFG_0_WLONG = const(0x04)
|
| 127 |
+
AS7341_CFG_0_REG_BANK = const(0x10) # datasheet fig 82 (! fig 32)
|
| 128 |
+
AS7341_CFG_0_LOW_POWER = const(0x20)
|
| 129 |
+
AS7341_CFG_1 = const(0xAA)
|
| 130 |
+
AS7341_CFG_3 = const(0xAC)
|
| 131 |
+
AS7341_CFG_6 = const(0xAF)
|
| 132 |
+
AS7341_CFG_6_SMUX_CMD_ROM = const(0x00)
|
| 133 |
+
AS7341_CFG_6_SMUX_CMD_READ = const(0x08)
|
| 134 |
+
AS7341_CFG_6_SMUX_CMD_WRITE = const(0x10)
|
| 135 |
+
AS7341_CFG_8 = const(0xB1)
|
| 136 |
+
AS7341_CFG_9 = const(0xB2)
|
| 137 |
+
AS7341_CFG_10 = const(0xB3)
|
| 138 |
+
AS7341_CFG_12 = const(0xB5)
|
| 139 |
+
AS7341_PERS = const(0xBD)
|
| 140 |
+
AS7341_GPIO_2 = const(0xBE)
|
| 141 |
+
AS7341_GPIO_2_GPIO_IN = const(0x01)
|
| 142 |
+
AS7341_GPIO_2_GPIO_OUT = const(0x02)
|
| 143 |
+
AS7341_GPIO_2_GPIO_IN_EN = const(0x04)
|
| 144 |
+
AS7341_GPIO_2_GPIO_INV = const(0x08)
|
| 145 |
+
AS7341_ASTEP = const(0xCA)
|
| 146 |
+
AS7341_ASTEP_L = const(0xCA)
|
| 147 |
+
AS7341_ASTEP_H = const(0xCB)
|
| 148 |
+
AS7341_AGC_GAIN_MAX = const(0xCF)
|
| 149 |
+
AS7341_AZ_CONFIG = const(0xD6)
|
| 150 |
+
AS7341_FD_TIME_1 = const(0xD8)
|
| 151 |
+
AS7341_FD_TIME_2 = const(0xDA)
|
| 152 |
+
AS7341_FD_CFG0 = const(0xD7)
|
| 153 |
+
AS7341_FD_STATUS = const(0xDB)
|
| 154 |
+
AS7341_FD_STATUS_FD_100HZ = const(0x01)
|
| 155 |
+
AS7341_FD_STATUS_FD_120HZ = const(0x02)
|
| 156 |
+
AS7341_FD_STATUS_FD_100_VALID = const(0x04)
|
| 157 |
+
AS7341_FD_STATUS_FD_120_VALID = const(0x08)
|
| 158 |
+
AS7341_FD_STATUS_FD_SAT_DETECT = const(0x10)
|
| 159 |
+
AS7341_FD_STATUS_FD_MEAS_VALID = const(0x20)
|
| 160 |
+
AS7341_INTENAB = const(0xF9)
|
| 161 |
+
AS7341_INTENAB_SP_IEN = const(0x08)
|
| 162 |
+
AS7341_CONTROL = const(0xFA)
|
| 163 |
+
AS7341_FIFO_MAP = const(0xFC)
|
| 164 |
+
AS7341_FIFO_LVL = const(0xFD)
|
| 165 |
+
AS7341_FDATA = const(0xFE)
|
| 166 |
+
AS7341_FDATA_L = const(0xFE)
|
| 167 |
+
AS7341_FDATA_H = const(0xFF)
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
class AS7341:
|
| 171 |
+
"""Class for AS7341: 11 Channel Multi-Spectral Digital Sensor"""
|
| 172 |
+
|
| 173 |
+
def __init__(self, i2c, addr=AS7341_I2C_ADDRESS):
|
| 174 |
+
"""specification of active I2C object is mandatory
|
| 175 |
+
specification of I2C address of AS7341 is optional
|
| 176 |
+
"""
|
| 177 |
+
self.__bus = i2c
|
| 178 |
+
self.__address = addr
|
| 179 |
+
self.__buffer1 = bytearray(1) # I2C I/O buffer for byte
|
| 180 |
+
self.__buffer2 = bytearray(2) # I2C I/O buffer for word
|
| 181 |
+
self.__buffer13 = bytearray(13) # I2C I/O buffer ASTATUS + 6 counts
|
| 182 |
+
self.__measuremode = AS7341_MODE_SPM # default measurement mode
|
| 183 |
+
self.__connected = self.reset() # recycle power, check AS7341 presence
|
| 184 |
+
|
| 185 |
+
""" --------- 'private' functions ----------- """
|
| 186 |
+
|
| 187 |
+
def __read_byte(self, reg):
|
| 188 |
+
"""read byte, return byte (integer) value"""
|
| 189 |
+
try:
|
| 190 |
+
self.__bus.readfrom_mem_into(self.__address, reg, self.__buffer1)
|
| 191 |
+
return self.__buffer1[0] # return integer value
|
| 192 |
+
except Exception as err:
|
| 193 |
+
print("I2C read_byte at 0x{:02X}, error".format(reg), err)
|
| 194 |
+
return -1 # indication 'no receive'
|
| 195 |
+
|
| 196 |
+
def __read_word(self, reg):
|
| 197 |
+
"""read 2 consecutive bytes, return integer value (little Endian)"""
|
| 198 |
+
try:
|
| 199 |
+
self.__bus.readfrom_mem_into(self.__address, reg, self.__buffer2)
|
| 200 |
+
return int.from_bytes(self.__buffer2, "little") # return word value
|
| 201 |
+
except Exception as err:
|
| 202 |
+
print("I2C read_word at 0x{:02X}, error".format(reg), err)
|
| 203 |
+
return -1 # indication 'no receive'
|
| 204 |
+
|
| 205 |
+
def __read_all_channels(self):
|
| 206 |
+
"""read ASTATUS register and all channels, return list of 6 integer values"""
|
| 207 |
+
try:
|
| 208 |
+
self.__bus.readfrom_mem_into(
|
| 209 |
+
self.__address, AS7341_ASTATUS, self.__buffer13
|
| 210 |
+
)
|
| 211 |
+
return [
|
| 212 |
+
int.from_bytes(self.__buffer13[1 + 2 * i : 3 + 2 * i], "little")
|
| 213 |
+
for i in range(6)
|
| 214 |
+
]
|
| 215 |
+
except Exception as err:
|
| 216 |
+
print(
|
| 217 |
+
"I2C read_all_channels at 0x{:02X}, error".format(AS7341_ASTATUS), err
|
| 218 |
+
)
|
| 219 |
+
return [] # empty list
|
| 220 |
+
|
| 221 |
+
def __write_byte(self, reg, value):
|
| 222 |
+
"""write a single byte to the specified register"""
|
| 223 |
+
self.__buffer1[0] = value & 0xFF
|
| 224 |
+
try:
|
| 225 |
+
self.__bus.writeto_mem(self.__address, reg, self.__buffer1)
|
| 226 |
+
sleep_ms(10)
|
| 227 |
+
except Exception as err:
|
| 228 |
+
print("I2C write_byte at 0x{:02X}, error".format(reg), err)
|
| 229 |
+
return False
|
| 230 |
+
return True
|
| 231 |
+
|
| 232 |
+
def __write_word(self, reg, value):
|
| 233 |
+
"""write a word as 2 bytes (little endian encoding)
|
| 234 |
+
to adresses <reg> + 0 and <reg> + 1
|
| 235 |
+
"""
|
| 236 |
+
self.__buffer2[0] = value & 0xFF # low byte
|
| 237 |
+
self.__buffer2[1] = (value >> 8) & 0xFF # high byte
|
| 238 |
+
try:
|
| 239 |
+
self.__bus.writeto_mem(self.__address, reg, self.__buffer2)
|
| 240 |
+
sleep_ms(20)
|
| 241 |
+
except Exception as err:
|
| 242 |
+
print("I2C write_word at 0x{:02X}, error".format(reg), err)
|
| 243 |
+
return False
|
| 244 |
+
return True
|
| 245 |
+
|
| 246 |
+
def __write_burst(self, reg, value):
|
| 247 |
+
"""write an array of bytes to consucutive addresses starting <reg>"""
|
| 248 |
+
try:
|
| 249 |
+
self.__bus.writeto_mem(self.__address, reg, value)
|
| 250 |
+
sleep_ms(100)
|
| 251 |
+
except Exception as err:
|
| 252 |
+
print("I2C write_burst at 0x{:02X}, error".format(reg), err)
|
| 253 |
+
return False
|
| 254 |
+
return True
|
| 255 |
+
|
| 256 |
+
def __modify_reg(self, reg, mask, flag=True):
|
| 257 |
+
"""modify register <reg> with <mask>
|
| 258 |
+
<flag> True means 'or' with <mask> : set the bit(s)
|
| 259 |
+
<flag> False means 'and' with inverted <mask> : reset the bit(s)
|
| 260 |
+
Notes: 1. Works only with '1' bits in <mask>
|
| 261 |
+
(in most cases <mask> contains a single 1-bit!)
|
| 262 |
+
2. When <reg> is in region 0x60-0x74
|
| 263 |
+
bank 1 is supposed be set by caller
|
| 264 |
+
"""
|
| 265 |
+
data = self.__read_byte(reg) # read <reg>
|
| 266 |
+
if flag:
|
| 267 |
+
data |= mask
|
| 268 |
+
else:
|
| 269 |
+
data &= ~mask
|
| 270 |
+
self.__write_byte(reg, data) # rewrite <reg>
|
| 271 |
+
|
| 272 |
+
def __set_bank(self, bank=1):
|
| 273 |
+
"""select registerbank
|
| 274 |
+
<bank> 1 for access to regs 0x60-0x74
|
| 275 |
+
<bank> 0 for access to regs 0x80-0xFF
|
| 276 |
+
Note: It seems that reg CFG_0 (0x93) is accessible
|
| 277 |
+
even when REG_BANK bit is set for 0x60-0x74,
|
| 278 |
+
otherwise it wouldn't be possible to reset REG_BANK
|
| 279 |
+
Datasheet isn't clear about this.
|
| 280 |
+
"""
|
| 281 |
+
if bank in (0, 1):
|
| 282 |
+
self.__modify_reg(AS7341_CFG_0, AS7341_CFG_0_REG_BANK, bank == 1)
|
| 283 |
+
|
| 284 |
+
""" ----------- 'public' functions ----------- """
|
| 285 |
+
|
| 286 |
+
def enable(self):
|
| 287 |
+
"""enable device (only power on)"""
|
| 288 |
+
self.__write_byte(AS7341_ENABLE, AS7341_ENABLE_PON)
|
| 289 |
+
|
| 290 |
+
def disable(self):
|
| 291 |
+
"""disable all functions and power off"""
|
| 292 |
+
self.__set_bank(1) # CONFIG register is in bank 1
|
| 293 |
+
self.__write_byte(AS7341_CONFIG, 0x00) # INT, LED off, SPM mode
|
| 294 |
+
self.__set_bank(0)
|
| 295 |
+
self.__write_byte(AS7341_ENABLE, 0x00) # power off
|
| 296 |
+
|
| 297 |
+
def isconnected(self):
|
| 298 |
+
"""determine if AS7341 is successfully initialized (True/False)"""
|
| 299 |
+
return self.__connected
|
| 300 |
+
|
| 301 |
+
def reset(self):
|
| 302 |
+
"""Cycle power and check if AS7341 is (re-)connected
|
| 303 |
+
When connected set (restore) measurement mode
|
| 304 |
+
"""
|
| 305 |
+
self.disable() # power-off ('reset')
|
| 306 |
+
sleep_ms(50) # quisce
|
| 307 |
+
self.enable() # (only) power-on
|
| 308 |
+
sleep_ms(50) # settle
|
| 309 |
+
id = self.__read_byte(AS7341_ID) # obtain Part Number ID
|
| 310 |
+
if id < 0: # read error
|
| 311 |
+
print(
|
| 312 |
+
"Failed to contact AS7341 at I2C address 0x{:02X}".format(
|
| 313 |
+
self.__address
|
| 314 |
+
)
|
| 315 |
+
)
|
| 316 |
+
return False
|
| 317 |
+
else:
|
| 318 |
+
if not (id & (~0x03)) == AS7341_ID_VALUE: # ID in bits 7..2 bits
|
| 319 |
+
print(
|
| 320 |
+
"No AS7341: ID = 0x{:02X}, expected 0x{:02X}".format(
|
| 321 |
+
id, AS7341_ID_VALUE
|
| 322 |
+
)
|
| 323 |
+
)
|
| 324 |
+
return False
|
| 325 |
+
self.set_measure_mode(self.__measuremode) # configure chip
|
| 326 |
+
return True
|
| 327 |
+
|
| 328 |
+
def measurement_completed(self):
|
| 329 |
+
"""check if measurement completed (return True) or otherwise return False"""
|
| 330 |
+
return bool(self.__read_byte(AS7341_STATUS_2) & AS7341_STATUS_2_AVALID)
|
| 331 |
+
|
| 332 |
+
def set_spectral_measurement(self, flag=True):
|
| 333 |
+
"""enable (flag == True) spectral measurement or otherwise disable it"""
|
| 334 |
+
self.__modify_reg(AS7341_ENABLE, AS7341_ENABLE_SP_EN, flag)
|
| 335 |
+
|
| 336 |
+
def set_smux(self, flag=True):
|
| 337 |
+
"""enable (flag == True) SMUX or otherwise disable it"""
|
| 338 |
+
self.__modify_reg(AS7341_ENABLE, AS7341_ENABLE_SMUXEN, flag)
|
| 339 |
+
|
| 340 |
+
def set_measure_mode(self, mode=AS7341_CONFIG_INT_MODE_SPM):
|
| 341 |
+
"""configure the AS7341 for a specific measurement mode
|
| 342 |
+
when interrupt needed it must be configured separately
|
| 343 |
+
"""
|
| 344 |
+
if mode in (
|
| 345 |
+
AS7341_CONFIG_INT_MODE_SPM, # meas. started by SP_EN
|
| 346 |
+
AS7341_CONFIG_INT_MODE_SYNS, # meas. started by GPIO
|
| 347 |
+
AS7341_CONFIG_INT_MODE_SYND,
|
| 348 |
+
): # meas. started by GPIO + EDGE
|
| 349 |
+
self.__measuremode = mode # store new measurement mode
|
| 350 |
+
self.__set_bank(1) # CONFIG register is in bank 1
|
| 351 |
+
data = self.__read_byte(AS7341_CONFIG) & (~3) # discard 2 LSbs (mode)
|
| 352 |
+
data |= mode # insert new mode
|
| 353 |
+
self.__write_byte(AS7341_CONFIG, data) # modify measurement mode
|
| 354 |
+
self.__set_bank(0)
|
| 355 |
+
|
| 356 |
+
def channel_select(self, selection):
|
| 357 |
+
"""select one from a series of predefined SMUX configurations
|
| 358 |
+
<selection> should be a key in dictionary AS7341_SMUX_SELECT
|
| 359 |
+
20 bytes of memory starting from address 0 will be overwritten.
|
| 360 |
+
"""
|
| 361 |
+
if selection in AS7341_SMUX_SELECT:
|
| 362 |
+
self.__write_burst(0x00, AS7341_SMUX_SELECT[selection])
|
| 363 |
+
else:
|
| 364 |
+
print(selection, "is unknown in AS7341_SMUX_SELECT")
|
| 365 |
+
|
| 366 |
+
def start_measure(self, selection):
|
| 367 |
+
"""select SMUX configuration, prepare and start measurement"""
|
| 368 |
+
self.__modify_reg(AS7341_CFG_0, AS7341_CFG_0_LOW_POWER, False) # no low power
|
| 369 |
+
self.set_spectral_measurement(False) # quiesce
|
| 370 |
+
self.__write_byte(AS7341_CFG_6, AS7341_CFG_6_SMUX_CMD_WRITE) # write mode
|
| 371 |
+
if self.__measuremode == AS7341_CONFIG_INT_MODE_SPM:
|
| 372 |
+
self.channel_select(selection)
|
| 373 |
+
self.set_smux(True)
|
| 374 |
+
elif self.__measuremode == AS7341_CONFIG_INT_MODE_SYNS:
|
| 375 |
+
self.channel_select(selection)
|
| 376 |
+
self.set_smux(True)
|
| 377 |
+
self.set_gpio_mode(AS7341_GPIO_2_GPIO_IN_EN)
|
| 378 |
+
self.set_spectral_measurement(True)
|
| 379 |
+
if self.__measuremode == AS7341_CONFIG_INT_MODE_SPM:
|
| 380 |
+
while not self.measurement_completed():
|
| 381 |
+
sleep_ms(50)
|
| 382 |
+
|
| 383 |
+
def get_channel_data(self, channel):
|
| 384 |
+
"""read count of a single channel (channel in range 0..5)
|
| 385 |
+
with or without measurement, just read count of one channel
|
| 386 |
+
contents depend on previous selection with 'start_measure'
|
| 387 |
+
auto-zero feature may result in value 0!
|
| 388 |
+
"""
|
| 389 |
+
data = 0 # default
|
| 390 |
+
if 0 <= channel <= 5:
|
| 391 |
+
data = self.__read_word(AS7341_CH_DATA + channel * 2)
|
| 392 |
+
return data # return integer value
|
| 393 |
+
|
| 394 |
+
def get_spectral_data(self):
|
| 395 |
+
"""obtain counts of all channels
|
| 396 |
+
return a tuple of 6 counts (integers) of the channels
|
| 397 |
+
contents depend on previous selection with 'start_measure'
|
| 398 |
+
"""
|
| 399 |
+
return self.__read_all_channels() # return a tuple!
|
| 400 |
+
|
| 401 |
+
def set_flicker_detection(self, flag=True):
|
| 402 |
+
"""enable (flag == True) flicker detection or otherwise disable it"""
|
| 403 |
+
self.__modify_reg(AS7341_ENABLE, AS7341_ENABLE_FDEN, flag)
|
| 404 |
+
|
| 405 |
+
def get_flicker_frequency(self):
|
| 406 |
+
"""Determine flicker frequency in Hz. Returns 100, 120 or 0
|
| 407 |
+
Integration time and gain for flicker detection is the same as for
|
| 408 |
+
other channels, the dedicated FD_TIME and FD_GAIN are not supported
|
| 409 |
+
"""
|
| 410 |
+
self.__modify_reg(AS7341_CFG_0, AS7341_CFG_0_LOW_POWER, False) # no low power
|
| 411 |
+
self.set_spectral_measurement(False)
|
| 412 |
+
self.__write_byte(AS7341_CFG_6, AS7341_CFG_6_SMUX_CMD_WRITE)
|
| 413 |
+
self.channel_select("FD") # select flicker detection only
|
| 414 |
+
self.set_smux(True)
|
| 415 |
+
self.set_spectral_measurement(True)
|
| 416 |
+
self.set_flicker_detection(True)
|
| 417 |
+
for _ in range(10): # limited wait for completion
|
| 418 |
+
fd_status = self.__read_byte(AS7341_FD_STATUS)
|
| 419 |
+
if fd_status & AS7341_FD_STATUS_FD_MEAS_VALID:
|
| 420 |
+
break
|
| 421 |
+
# print("Flicker measurement not completed")
|
| 422 |
+
sleep_ms(100)
|
| 423 |
+
else: # timeout
|
| 424 |
+
print("Flicker measurement timed out")
|
| 425 |
+
return 0
|
| 426 |
+
for _ in range(10): # limited wait for calculation
|
| 427 |
+
fd_status = self.__read_byte(AS7341_FD_STATUS)
|
| 428 |
+
if (fd_status & AS7341_FD_STATUS_FD_100_VALID) or (
|
| 429 |
+
fd_status & AS7341_FD_STATUS_FD_120_VALID
|
| 430 |
+
):
|
| 431 |
+
break
|
| 432 |
+
# print("Flicker calculation not completed")
|
| 433 |
+
sleep_ms(100)
|
| 434 |
+
else: # timeout
|
| 435 |
+
print("Flicker frequency calculation timed out")
|
| 436 |
+
return 0
|
| 437 |
+
# print("FD_STATUS", "0x{:02X}".format(fd_status))
|
| 438 |
+
self.set_flicker_detection(False) # disable
|
| 439 |
+
self.__write_byte(AS7341_FD_STATUS, 0x3C) # clear all FD STATUS bits
|
| 440 |
+
if (fd_status & AS7341_FD_STATUS_FD_100_VALID) and (
|
| 441 |
+
fd_status & AS7341_FD_STATUS_FD_100HZ
|
| 442 |
+
):
|
| 443 |
+
return 100
|
| 444 |
+
elif (fd_status & AS7341_FD_STATUS_FD_120_VALID) and (
|
| 445 |
+
fd_status & AS7341_FD_STATUS_FD_120HZ
|
| 446 |
+
):
|
| 447 |
+
return 120
|
| 448 |
+
return 0
|
| 449 |
+
|
| 450 |
+
def set_gpio_mode(self, mode):
|
| 451 |
+
"""Configure mode of GPIO pin.
|
| 452 |
+
Allow only input-enable or output (with or without inverted)
|
| 453 |
+
specify 0x00 to reset the mode of the GPIO pin.
|
| 454 |
+
Notes: 1. It seems that GPIO_INV bit must be set
|
| 455 |
+
together with GPIO_IN_EN.
|
| 456 |
+
Proof: Use a pull-up resistor between GPIO and 3.3V:
|
| 457 |
+
- when program is ot started GPIO is high
|
| 458 |
+
- when program is started (GPIO_IN_EN=1) GPIO becomes low
|
| 459 |
+
- when also GPIO_INV=1 GPIO behaves normally
|
| 460 |
+
Maybe it is a quirk of the used test-board.
|
| 461 |
+
2. GPIO output is not tested
|
| 462 |
+
(dataset lacks info how to set/reset GPIO)
|
| 463 |
+
"""
|
| 464 |
+
if mode in (
|
| 465 |
+
0x00,
|
| 466 |
+
AS7341_GPIO_2_GPIO_OUT,
|
| 467 |
+
AS7341_GPIO_2_GPIO_OUT | AS7341_GPIO_2_GPIO_INV,
|
| 468 |
+
AS7341_GPIO_2_GPIO_IN_EN,
|
| 469 |
+
AS7341_GPIO_2_GPIO_IN_EN | AS7341_GPIO_2_GPIO_INV,
|
| 470 |
+
):
|
| 471 |
+
if mode == AS7341_GPIO_2_GPIO_IN_EN: # input mode
|
| 472 |
+
mode |= AS7341_GPIO_2_GPIO_INV # add 'inverted'
|
| 473 |
+
self.__write_byte(AS7341_GPIO_2, mode)
|
| 474 |
+
|
| 475 |
+
def get_gpio_value(self):
|
| 476 |
+
"""Determine GPIO value (when GPIO enabled for IN_EN)
|
| 477 |
+
returns 0 (low voltage) or 1 (high voltage)
|
| 478 |
+
"""
|
| 479 |
+
# print("GPIO_2 = 0x{:02X}".format(self.__read_byte(AS7341_GPIO_2)))
|
| 480 |
+
return self.__read_byte(AS7341_GPIO_2) & AS7341_GPIO_2_GPIO_IN
|
| 481 |
+
|
| 482 |
+
def set_astep(self, value):
|
| 483 |
+
"""set ASTEP size (range 0..65534 -> 2.78 usec .. 182 msec)"""
|
| 484 |
+
if 0 <= value <= 65534:
|
| 485 |
+
self.__write_word(AS7341_ASTEP, value)
|
| 486 |
+
|
| 487 |
+
def set_atime(self, value):
|
| 488 |
+
"""set number of integration steps (range 0..255 -> 1..256 ASTEPs)"""
|
| 489 |
+
self.__write_byte(AS7341_ATIME, value)
|
| 490 |
+
|
| 491 |
+
def get_integration_time(self):
|
| 492 |
+
"""return actual total integration time (atime * astep)
|
| 493 |
+
in milliseconds (valid with SPM and SYNS measurement mode)
|
| 494 |
+
"""
|
| 495 |
+
return (
|
| 496 |
+
(self.__read_word(AS7341_ASTEP) + 1)
|
| 497 |
+
* (self.__read_byte(AS7341_ATIME) + 1)
|
| 498 |
+
* 2.78
|
| 499 |
+
/ 1000
|
| 500 |
+
)
|
| 501 |
+
|
| 502 |
+
def set_again(self, code):
|
| 503 |
+
"""set AGAIN (code in range 0..10 -> gain factor 0.5 .. 512)
|
| 504 |
+
value 0 1 2 3 4 5 6 7 8 9 10
|
| 505 |
+
gain: *0.5 | *1 | *2 | *4 | *8 | *16 | *32 | *64 | *128 | *256 | *512
|
| 506 |
+
"""
|
| 507 |
+
if 0 <= code <= 10:
|
| 508 |
+
self.__write_byte(AS7341_CFG_1, code)
|
| 509 |
+
|
| 510 |
+
def get_again(self):
|
| 511 |
+
"""obtain actual gain code (in range 0 .. 10)"""
|
| 512 |
+
return self.__read_byte(AS7341_CFG_1)
|
| 513 |
+
|
| 514 |
+
def set_again_factor(self, factor):
|
| 515 |
+
"""'inverse' of 'set_again': gain factor -> code 0 .. 10
|
| 516 |
+
<factor> is rounded down to nearest power of 2 (in range 0.5 .. 512)
|
| 517 |
+
"""
|
| 518 |
+
code = 10
|
| 519 |
+
gain = 512
|
| 520 |
+
while gain > factor and code > 0:
|
| 521 |
+
gain /= 2
|
| 522 |
+
code -= 1
|
| 523 |
+
# print("factor", factor, "gain", gain, "code", code)
|
| 524 |
+
self.__write_byte(AS7341_CFG_1, code)
|
| 525 |
+
|
| 526 |
+
def get_again_factor(self):
|
| 527 |
+
"""obtain actual gain factor (in range 0.5 .. 512)"""
|
| 528 |
+
return 2 ** (self.__read_byte(AS7341_CFG_1) - 1)
|
| 529 |
+
|
| 530 |
+
def set_wen(self, flag=True):
|
| 531 |
+
"""enable (flag=True) or otherwise disable use of WTIME (auto re-start)"""
|
| 532 |
+
self.__modify_reg(AS7341_ENABLE, AS7341_ENABLE_WEN, flag)
|
| 533 |
+
|
| 534 |
+
def set_wtime(self, wtime):
|
| 535 |
+
"""set WTIME when auto-re-start is desired (in range 0 .. 0xFF)
|
| 536 |
+
0 -> 2.78ms, 0xFF -> 711.7 ms
|
| 537 |
+
Note: The WEN bit in ENABLE should be set as well: set_wen()
|
| 538 |
+
"""
|
| 539 |
+
self.__write_byte(AS7341_WTIME, wtime)
|
| 540 |
+
|
| 541 |
+
def set_led_current(self, current):
|
| 542 |
+
"""Control current of onboard LED in milliamperes
|
| 543 |
+
LED-current is (here) limited to the range 4..20 mA
|
| 544 |
+
use only even numbers (4,6,8,... etc)
|
| 545 |
+
Specification outside this range results in LED OFF
|
| 546 |
+
"""
|
| 547 |
+
self.__set_bank(1) # CONFIG and LED registers in bank 1
|
| 548 |
+
if 4 <= current <= 20: # within limits: 4..20 mA
|
| 549 |
+
self.__modify_reg(AS7341_CONFIG, AS7341_CONFIG_LED_SEL, True)
|
| 550 |
+
# print("Reg. CONFIG (0x70) now 0x{:02X}".format(self.__read_byte(0x70)))
|
| 551 |
+
data = AS7341_LED_LED_ACT + ((current - 4) // 2) # LED on with PWM
|
| 552 |
+
else:
|
| 553 |
+
self.__modify_reg(AS7341_CONFIG, AS7341_CONFIG_LED_SEL, False)
|
| 554 |
+
data = 0 # LED off, PWM 0
|
| 555 |
+
self.__write_byte(AS7341_LED, data)
|
| 556 |
+
# print("reg 0x74 (LED) now 0x{:02X}".format(self.__read_byte(0x74)))
|
| 557 |
+
self.__set_bank(0)
|
| 558 |
+
sleep_ms(100)
|
| 559 |
+
|
| 560 |
+
def check_interrupt(self):
|
| 561 |
+
"""Check for Spectral or Flicker Detect saturation interrupt"""
|
| 562 |
+
data = self.__read_byte(AS7341_STATUS)
|
| 563 |
+
if data & AS7341_STATUS_ASAT:
|
| 564 |
+
print("Spectral interrupt generation!")
|
| 565 |
+
return True
|
| 566 |
+
return False
|
| 567 |
+
|
| 568 |
+
def clear_interrupt(self):
|
| 569 |
+
"""clear all interrupt signals"""
|
| 570 |
+
self.__write_byte(AS7341_STATUS, 0xFF)
|
| 571 |
+
|
| 572 |
+
def set_spectral_interrupt(self, flag=True):
|
| 573 |
+
"""enable (flag == True) or otherwise disable spectral interrupts"""
|
| 574 |
+
self.__modify_reg(AS7341_INTENAB, AS7341_INTENAB_SP_IEN, flag)
|
| 575 |
+
|
| 576 |
+
def set_interrupt_persistence(self, value):
|
| 577 |
+
"""configure interrupt persistance"""
|
| 578 |
+
if 0 <= value <= 15:
|
| 579 |
+
self.__write_byte(AS7341_PERS, value)
|
| 580 |
+
|
| 581 |
+
def set_spectral_threshold_channel(self, value):
|
| 582 |
+
"""select channel (0..4) for interrupts, persistence and AGC"""
|
| 583 |
+
if 0 <= value <= 4:
|
| 584 |
+
self.__write_byte(AS7341_CFG_12, value)
|
| 585 |
+
|
| 586 |
+
def set_thresholds(self, lo, hi):
|
| 587 |
+
"""Set thresholds (when lo < hi)"""
|
| 588 |
+
if lo < hi:
|
| 589 |
+
self.__write_word(AS7341_SP_TH_LOW, lo)
|
| 590 |
+
self.__write_word(AS7341_SP_TH_HIGH, hi)
|
| 591 |
+
sleep_ms(20)
|
| 592 |
+
|
| 593 |
+
def get_thresholds(self):
|
| 594 |
+
"""obtain and return tuple with low and high threshold values"""
|
| 595 |
+
lo = self.__read_word(AS7341_SP_TH_LOW)
|
| 596 |
+
hi = self.__read_word(AS7341_SP_TH_HIGH)
|
| 597 |
+
return (lo, hi)
|
| 598 |
+
|
| 599 |
+
def set_syns_int(self):
|
| 600 |
+
"""select SYNS mode and signal SYNS interrupt on Pin INT"""
|
| 601 |
+
self.__set_bank(1) # CONFIG register is in bank 1
|
| 602 |
+
self.__write_byte(
|
| 603 |
+
AS7341_CONFIG, AS7341_CONFIG_INT_SEL | AS7341_CONFIG_INT_MODE_SYNS
|
| 604 |
+
)
|
| 605 |
+
self.__set_bank(0)
|
| 606 |
+
|
| 607 |
+
|
| 608 |
+
#
|
scripts/lib/as7341_sensor.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Sterling Baird: wrapper class for AS7341 sensor."""
|
| 2 |
+
|
| 3 |
+
from math import log
|
| 4 |
+
|
| 5 |
+
from as7341 import AS7341, AS7341_MODE_SPM
|
| 6 |
+
from machine import I2C, Pin
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class ExternalDeviceNotFound(OSError):
|
| 10 |
+
pass
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class Sensor:
|
| 14 |
+
def __init__(
|
| 15 |
+
self, atime=100, astep=999, gain=8, i2c=I2C(1, scl=Pin(27), sda=Pin(26))
|
| 16 |
+
):
|
| 17 |
+
"""Wrapper for Rob Hamerling's AS7341 implementation.
|
| 18 |
+
|
| 19 |
+
Mimics the original CircuitPython class a bit more, specific to the needs of
|
| 20 |
+
SDL-Demo.
|
| 21 |
+
|
| 22 |
+
Rob Hamerling's implementation:
|
| 23 |
+
- https://gitlab.com/robhamerling/micropython-as7341
|
| 24 |
+
|
| 25 |
+
Original Circuit Python repo:
|
| 26 |
+
- https://github.com/adafruit/Adafruit_CircuitPython_AS7341
|
| 27 |
+
|
| 28 |
+
Parameters
|
| 29 |
+
----------
|
| 30 |
+
atime : int, optional
|
| 31 |
+
The integration time step size in 2.78 microsecond increments, by default 100
|
| 32 |
+
astep : int, optional
|
| 33 |
+
The integration time step count. Total integration time will be (ATIME + 1)
|
| 34 |
+
* (ASTEP + 1) * 2.78µS, by default 999, meaning 281 ms assuming atime=100
|
| 35 |
+
gain : int, optional
|
| 36 |
+
The ADC gain multiplier, by default 128
|
| 37 |
+
i2c : I2C, optional
|
| 38 |
+
The I2C bus, by default machine.I2C(1, scl=machine.Pin(27),
|
| 39 |
+
sda=machine.Pin(26))
|
| 40 |
+
|
| 41 |
+
Raises
|
| 42 |
+
------
|
| 43 |
+
ExternalDeviceNotFound
|
| 44 |
+
Couldn't connect to AS7341.
|
| 45 |
+
|
| 46 |
+
Examples
|
| 47 |
+
--------
|
| 48 |
+
>>> sensor = Sensor(atime=29, astep=599, again=4)
|
| 49 |
+
>>> channel_data = sensor.all_channels
|
| 50 |
+
"""
|
| 51 |
+
|
| 52 |
+
# i2c = machine.SoftI2C(scl=Pin(27), sda=Pin(26))
|
| 53 |
+
self.i2c = i2c
|
| 54 |
+
addrlist = " ".join(["0x{:02X}".format(x) for x in i2c.scan()]) # type: ignore
|
| 55 |
+
print("Detected devices at I2C-addresses:", addrlist)
|
| 56 |
+
|
| 57 |
+
sensor = AS7341(i2c)
|
| 58 |
+
|
| 59 |
+
if not sensor.isconnected():
|
| 60 |
+
raise ExternalDeviceNotFound("Failed to contact AS7341, terminating")
|
| 61 |
+
|
| 62 |
+
sensor.set_measure_mode(AS7341_MODE_SPM)
|
| 63 |
+
|
| 64 |
+
sensor.set_atime(atime)
|
| 65 |
+
sensor.set_astep(astep)
|
| 66 |
+
sensor.set_again(gain)
|
| 67 |
+
|
| 68 |
+
self.sensor = sensor
|
| 69 |
+
|
| 70 |
+
self.__atime = atime
|
| 71 |
+
self.__astep = astep
|
| 72 |
+
self.__gain = gain
|
| 73 |
+
|
| 74 |
+
@property
|
| 75 |
+
def _atime(self):
|
| 76 |
+
return self.__atime
|
| 77 |
+
|
| 78 |
+
@_atime.setter
|
| 79 |
+
def _atime(self, value):
|
| 80 |
+
self.__atime = value
|
| 81 |
+
self.sensor.set_atime(value)
|
| 82 |
+
|
| 83 |
+
@property
|
| 84 |
+
def _astep(self):
|
| 85 |
+
return self.__astep
|
| 86 |
+
|
| 87 |
+
@_astep.setter
|
| 88 |
+
def _astep(self, value):
|
| 89 |
+
self.__atime = value
|
| 90 |
+
self.sensor.set_astep(value)
|
| 91 |
+
|
| 92 |
+
@property
|
| 93 |
+
def _gain(self):
|
| 94 |
+
return self.__gain
|
| 95 |
+
|
| 96 |
+
@_gain.setter
|
| 97 |
+
def _gain(self, gain):
|
| 98 |
+
"""set AGAIN (code in range 0..10 -> gain factor 0.5 .. 512)
|
| 99 |
+
gain: *0.5 | *1 | *2 | *4 | *8 | *16 | *32 | *64 | *128 | *256 | *512
|
| 100 |
+
code 0 1 2 3 4 5 6 7 8 9 10
|
| 101 |
+
"""
|
| 102 |
+
self.__gain = gain
|
| 103 |
+
# gain == 0.5 * 2 ** code --> code == 1.4427 Ln[2 * gain] (via Mathematica)
|
| 104 |
+
code = int(round(1.4427 * log(2 * gain)))
|
| 105 |
+
self.sensor.set_again(code)
|
| 106 |
+
|
| 107 |
+
@property
|
| 108 |
+
def all_channels(self):
|
| 109 |
+
self.sensor.start_measure("F1F4CN")
|
| 110 |
+
f1, f2, f3, f4, clr, nir = self.sensor.get_spectral_data()
|
| 111 |
+
|
| 112 |
+
self.sensor.start_measure("F5F8CN")
|
| 113 |
+
f5, f6, f7, f8, clr, nir = self.sensor.get_spectral_data()
|
| 114 |
+
|
| 115 |
+
clr, nir # to ignore "unused" linting warnings
|
| 116 |
+
|
| 117 |
+
return [f1, f2, f3, f4, f5, f6, f7, f8]
|
| 118 |
+
|
| 119 |
+
@property
|
| 120 |
+
def all_channels_clr_nir(self):
|
| 121 |
+
self.sensor.start_measure("F1F4CN")
|
| 122 |
+
f1, f2, f3, f4, clr, nir = self.sensor.get_spectral_data()
|
| 123 |
+
|
| 124 |
+
self.sensor.start_measure("F5F8CN")
|
| 125 |
+
f5, f6, f7, f8, clr, nir = self.sensor.get_spectral_data()
|
| 126 |
+
|
| 127 |
+
clr, nir # to ignore "unused" linting warnings
|
| 128 |
+
|
| 129 |
+
return [f1, f2, f3, f4, f5, f6, f7, f8, clr, nir]
|
| 130 |
+
|
| 131 |
+
def disable(self):
|
| 132 |
+
self.sensor.disable()
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
# %% Code Graveyard
|
| 136 |
+
# gain_to_code_lookup = {
|
| 137 |
+
# 0.5: 1,
|
| 138 |
+
# 1: 1,
|
| 139 |
+
# 2: 2,
|
| 140 |
+
# 4: 3,
|
| 141 |
+
# 8: 4,
|
| 142 |
+
# 16: 5,
|
| 143 |
+
# 32: 6,
|
| 144 |
+
# 64: 7,
|
| 145 |
+
# 128: 8,
|
| 146 |
+
# 256: 9,
|
| 147 |
+
# 512: 10,
|
| 148 |
+
# }
|
| 149 |
+
# code = gain_to_code_lookup[gain]
|
scripts/lib/as7341_smux_select.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
This file licensed under the MIT License and incorporates work covered by
|
| 3 |
+
the following copyright and permission notice:
|
| 4 |
+
|
| 5 |
+
The MIT License (MIT)
|
| 6 |
+
|
| 7 |
+
Copyright (c) 2022-2022 Rob Hamerling
|
| 8 |
+
|
| 9 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 10 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 11 |
+
in the Software without restriction, including without limitation the rights
|
| 12 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 13 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 14 |
+
furnished to do so, subject to the following conditions:
|
| 15 |
+
|
| 16 |
+
The above copyright notice and this permission notice shall be included in
|
| 17 |
+
all copies or substantial portions of the Software.
|
| 18 |
+
|
| 19 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 20 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 21 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 22 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 23 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 24 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
| 25 |
+
THE SOFTWARE.
|
| 26 |
+
|
| 27 |
+
"""
|
| 28 |
+
|
| 29 |
+
""" Dictionary with specific SMUX configurations for AS7341
|
| 30 |
+
See AMS Application Note AS7341_AN000666_1.01.pdf
|
| 31 |
+
for detailed instructions how to configure the channel mapping.
|
| 32 |
+
The Application Note can be found in one of the evaluation packages, e.g.
|
| 33 |
+
AS7341_EvalSW_Reflection_v1-26-3/Documents/application notes/SMUX/
|
| 34 |
+
|
| 35 |
+
This file should be imported by AS7341.py with:
|
| 36 |
+
from as7341_smux_select import *
|
| 37 |
+
"""
|
| 38 |
+
AS7341_SMUX_SELECT = {
|
| 39 |
+
# F1 through F4, CLEAR, NIR:
|
| 40 |
+
"F1F4CN": b"\x30\x01\x00\x00\x00\x42\x00\x00\x50\x00\x00\x00\x20\x04\x00\x30\x01\x50\x00\x06",
|
| 41 |
+
# F5 through F8, CLEAR, NIR:
|
| 42 |
+
"F5F8CN": b"\x00\x00\x00\x40\x02\x00\x10\x03\x50\x10\x03\x00\x00\x00\x24\x00\x00\x50\x00\x06",
|
| 43 |
+
# F2 through F7:
|
| 44 |
+
"F2F7": b"\x20\x00\x00\x00\x05\x31\x40\x06\x00\x40\x06\x00\x10\x03\x50\x20\x00\x00\x00\x00",
|
| 45 |
+
# Flicker Detection only:
|
| 46 |
+
"FD": b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x60",
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
#
|
scripts/lib/bme680-2.0.0.dist-info/METADATA
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Metadata-Version: 2.3
|
| 2 |
+
Name: bme680
|
| 3 |
+
Version: 2.0.0
|
| 4 |
+
Summary: Python library for the BME680 temperature, humidity and gas sensor
|
| 5 |
+
Project-URL: GitHub, https://www.github.com/pimoroni/bme680-python
|
| 6 |
+
Project-URL: Homepage, https://www.pimoroni.com
|
| 7 |
+
Author-email: Philip Howard <phil@pimoroni.com>
|
| 8 |
+
Maintainer-email: Philip Howard <phil@pimoroni.com>
|
| 9 |
+
License: MIT License
|
| 10 |
+
|
| 11 |
+
Copyright (c) 2018 Pimoroni Ltd
|
| 12 |
+
|
| 13 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 14 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 15 |
+
in the Software without restriction, including without limitation the rights
|
| 16 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 17 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 18 |
+
furnished to do so, subject to the following conditions:
|
| 19 |
+
|
| 20 |
+
The above copyright notice and this permission notice shall be included in all
|
| 21 |
+
copies or substantial portions of the Software.
|
| 22 |
+
|
| 23 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 24 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 25 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 26 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 27 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 28 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 29 |
+
SOFTWARE.
|
| 30 |
+
License-File: LICENSE
|
| 31 |
+
Keywords: Pi,Raspberry
|
| 32 |
+
Classifier: Development Status :: 4 - Beta
|
| 33 |
+
Classifier: Intended Audience :: Developers
|
| 34 |
+
Classifier: License :: OSI Approved :: MIT License
|
| 35 |
+
Classifier: Operating System :: POSIX :: Linux
|
| 36 |
+
Classifier: Programming Language :: Python :: 3
|
| 37 |
+
Classifier: Programming Language :: Python :: 3 :: Only
|
| 38 |
+
Classifier: Programming Language :: Python :: 3.7
|
| 39 |
+
Classifier: Programming Language :: Python :: 3.8
|
| 40 |
+
Classifier: Programming Language :: Python :: 3.9
|
| 41 |
+
Classifier: Programming Language :: Python :: 3.10
|
| 42 |
+
Classifier: Programming Language :: Python :: 3.11
|
| 43 |
+
Classifier: Topic :: Software Development
|
| 44 |
+
Classifier: Topic :: Software Development :: Libraries
|
| 45 |
+
Classifier: Topic :: System :: Hardware
|
| 46 |
+
Requires-Python: >=3.7
|
| 47 |
+
Requires-Dist: smbus2
|
| 48 |
+
Description-Content-Type: text/markdown
|
| 49 |
+
|
| 50 |
+
# BME680
|
| 51 |
+
|
| 52 |
+
[](https://github.com/pimoroni/bme680-python/actions/workflows/test.yml)
|
| 53 |
+
[](https://coveralls.io/github/pimoroni/bme680-python?branch=main)
|
| 54 |
+
[](https://pypi.python.org/pypi/bme680)
|
| 55 |
+
[](https://pypi.python.org/pypi/bme680)
|
| 56 |
+
|
| 57 |
+
https://shop.pimoroni.com/products/bme680
|
| 58 |
+
|
| 59 |
+
The state-of-the-art BME680 breakout lets you measure temperature, pressure, humidity, and indoor air quality.
|
| 60 |
+
|
| 61 |
+
## Installing
|
| 62 |
+
|
| 63 |
+
### Full install (recommended):
|
| 64 |
+
|
| 65 |
+
We've created an easy installation script that will install all pre-requisites and get your BME680
|
| 66 |
+
up and running with minimal efforts. To run it, fire up Terminal which you'll find in Menu -> Accessories -> Terminal
|
| 67 |
+
on your Raspberry Pi desktop, as illustrated below:
|
| 68 |
+
|
| 69 |
+

|
| 70 |
+
|
| 71 |
+
In the new terminal window type the command exactly as it appears below (check for typos) and follow the on-screen instructions:
|
| 72 |
+
|
| 73 |
+
```bash
|
| 74 |
+
git clone https://github.com/pimoroni/bme680-python
|
| 75 |
+
cd bme680-python
|
| 76 |
+
./install.sh
|
| 77 |
+
```
|
| 78 |
+
|
| 79 |
+
**Note** Libraries will be installed in the "pimoroni" virtual environment, you will need to activate it to run examples:
|
| 80 |
+
|
| 81 |
+
```
|
| 82 |
+
source ~/.virtualenvs/pimoroni/bin/activate
|
| 83 |
+
```
|
| 84 |
+
|
| 85 |
+
### Development:
|
| 86 |
+
|
| 87 |
+
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:
|
| 88 |
+
|
| 89 |
+
```bash
|
| 90 |
+
git clone https://github.com/pimoroni/bme680-python
|
| 91 |
+
cd bme680-python
|
| 92 |
+
./install.sh --unstable
|
| 93 |
+
```
|
| 94 |
+
|
| 95 |
+
In all cases you will have to enable the i2c bus:
|
| 96 |
+
|
| 97 |
+
```
|
| 98 |
+
sudo raspi-config nonint do_i2c 0
|
| 99 |
+
```
|
| 100 |
+
|
| 101 |
+
## Documentation & Support
|
| 102 |
+
|
| 103 |
+
* Guides and tutorials - https://learn.pimoroni.com/bme680-breakout
|
| 104 |
+
* Get help - http://forums.pimoroni.com/c/support
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
2.0.0
|
| 108 |
+
-----
|
| 109 |
+
|
| 110 |
+
* Repackage to hatch/pyproject.toml
|
| 111 |
+
* Drop Python 2.7 support
|
| 112 |
+
* Switch from smbu2 to smbus2
|
| 113 |
+
|
| 114 |
+
1.1.1
|
| 115 |
+
-----
|
| 116 |
+
|
| 117 |
+
* New: constants to clarify heater on/off states
|
| 118 |
+
|
| 119 |
+
1.1.0
|
| 120 |
+
-----
|
| 121 |
+
|
| 122 |
+
* New: support for BME688 "high" gas resistance variant
|
| 123 |
+
* New: set/get gas heater disable bit
|
| 124 |
+
* Enhancement: fail with descriptive RuntimeError when chip is not detected
|
| 125 |
+
|
| 126 |
+
1.0.5
|
| 127 |
+
-----
|
| 128 |
+
|
| 129 |
+
* New: set_temp_offset to calibrate temperature offset in degrees C
|
| 130 |
+
|
| 131 |
+
1.0.4
|
| 132 |
+
-----
|
| 133 |
+
|
| 134 |
+
* Fix to range_sw_err for extremely high gas readings
|
| 135 |
+
* Convert to unsigned int to fix negative gas readings
|
| 136 |
+
|
| 137 |
+
1.0.3
|
| 138 |
+
-----
|
| 139 |
+
|
| 140 |
+
* Merged temperature compensation fix from Bosch's BME680_driver 3.5.3
|
| 141 |
+
|
| 142 |
+
1.0.2
|
| 143 |
+
-----
|
| 144 |
+
|
| 145 |
+
* Fixed set_gas_heater_temperature to avoid i2c TypeError
|
| 146 |
+
|
| 147 |
+
1.0.1
|
| 148 |
+
-----
|
| 149 |
+
|
| 150 |
+
* Added Manifest to Python package
|
| 151 |
+
|
| 152 |
+
1.0.0
|
| 153 |
+
-----
|
| 154 |
+
|
| 155 |
+
* Initial release
|
| 156 |
+
|
scripts/lib/bme680-2.0.0.dist-info/RECORD
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
CHANGELOG.md,,
|
| 2 |
+
LICENSE,,
|
| 3 |
+
README.md,,
|
| 4 |
+
bme680-2.0.0.dist-info/METADATA,,
|
| 5 |
+
bme680/__init__.py,,
|
| 6 |
+
bme680/constants.py,,
|
| 7 |
+
bme680-2.0.0.dist-info/RECORD,,
|
scripts/lib/bme680/__init__.py
ADDED
|
@@ -0,0 +1,486 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""BME680 Temperature, Pressure, Humidity & Gas Sensor."""
|
| 2 |
+
import math
|
| 3 |
+
import time
|
| 4 |
+
|
| 5 |
+
from . import constants
|
| 6 |
+
from .constants import BME680Data, lookupTable1, lookupTable2
|
| 7 |
+
|
| 8 |
+
__version__ = '2.0.0'
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
# Export constants to global namespace
|
| 12 |
+
# so end-users can "from BME680 import NAME"
|
| 13 |
+
if hasattr(constants, '__dict__'):
|
| 14 |
+
for key in constants.__dict__:
|
| 15 |
+
value = constants.__dict__[key]
|
| 16 |
+
if key not in globals():
|
| 17 |
+
globals()[key] = value
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class BME680(BME680Data):
|
| 21 |
+
"""BOSCH BME680.
|
| 22 |
+
|
| 23 |
+
Gas, pressure, temperature and humidity sensor.
|
| 24 |
+
|
| 25 |
+
:param i2c_addr: One of I2C_ADDR_PRIMARY (0x76) or I2C_ADDR_SECONDARY (0x77)
|
| 26 |
+
:param i2c_device: Optional smbus or compatible instance for facilitating i2c communications.
|
| 27 |
+
|
| 28 |
+
"""
|
| 29 |
+
|
| 30 |
+
def __init__(self, i2c_addr=constants.I2C_ADDR_PRIMARY, i2c_device=None):
|
| 31 |
+
"""Initialise BME680 sensor instance and verify device presence.
|
| 32 |
+
|
| 33 |
+
:param i2c_addr: i2c address of BME680
|
| 34 |
+
:param i2c_device: Optional SMBus-compatible instance for i2c transport
|
| 35 |
+
|
| 36 |
+
"""
|
| 37 |
+
BME680Data.__init__(self)
|
| 38 |
+
|
| 39 |
+
self.i2c_addr = i2c_addr
|
| 40 |
+
self._i2c = i2c_device
|
| 41 |
+
if self._i2c is None:
|
| 42 |
+
import smbus2
|
| 43 |
+
self._i2c = smbus2.SMBus(1)
|
| 44 |
+
|
| 45 |
+
try:
|
| 46 |
+
self.chip_id = self._get_regs(constants.CHIP_ID_ADDR, 1)
|
| 47 |
+
if self.chip_id != constants.CHIP_ID:
|
| 48 |
+
raise RuntimeError('BME680 Not Found. Invalid CHIP ID: 0x{0:02x}'.format(self.chip_id))
|
| 49 |
+
except IOError:
|
| 50 |
+
raise RuntimeError("Unable to identify BME680 at 0x{:02x} (IOError)".format(self.i2c_addr))
|
| 51 |
+
|
| 52 |
+
self._variant = self._get_regs(constants.CHIP_VARIANT_ADDR, 1)
|
| 53 |
+
|
| 54 |
+
self.soft_reset()
|
| 55 |
+
self.set_power_mode(constants.SLEEP_MODE)
|
| 56 |
+
|
| 57 |
+
self._get_calibration_data()
|
| 58 |
+
|
| 59 |
+
self.set_humidity_oversample(constants.OS_2X)
|
| 60 |
+
self.set_pressure_oversample(constants.OS_4X)
|
| 61 |
+
self.set_temperature_oversample(constants.OS_8X)
|
| 62 |
+
self.set_filter(constants.FILTER_SIZE_3)
|
| 63 |
+
if self._variant == constants.VARIANT_HIGH:
|
| 64 |
+
self.set_gas_status(constants.ENABLE_GAS_MEAS_HIGH)
|
| 65 |
+
else:
|
| 66 |
+
self.set_gas_status(constants.ENABLE_GAS_MEAS_LOW)
|
| 67 |
+
self.set_temp_offset(0)
|
| 68 |
+
self.get_sensor_data()
|
| 69 |
+
|
| 70 |
+
def _get_calibration_data(self):
|
| 71 |
+
"""Retrieve the sensor calibration data and store it in .calibration_data."""
|
| 72 |
+
calibration = self._get_regs(constants.COEFF_ADDR1, constants.COEFF_ADDR1_LEN)
|
| 73 |
+
calibration += self._get_regs(constants.COEFF_ADDR2, constants.COEFF_ADDR2_LEN)
|
| 74 |
+
|
| 75 |
+
heat_range = self._get_regs(constants.ADDR_RES_HEAT_RANGE_ADDR, 1)
|
| 76 |
+
heat_value = constants.twos_comp(self._get_regs(constants.ADDR_RES_HEAT_VAL_ADDR, 1), bits=8)
|
| 77 |
+
sw_error = constants.twos_comp(self._get_regs(constants.ADDR_RANGE_SW_ERR_ADDR, 1), bits=8)
|
| 78 |
+
|
| 79 |
+
self.calibration_data.set_from_array(calibration)
|
| 80 |
+
self.calibration_data.set_other(heat_range, heat_value, sw_error)
|
| 81 |
+
|
| 82 |
+
def soft_reset(self):
|
| 83 |
+
"""Trigger a soft reset."""
|
| 84 |
+
self._set_regs(constants.SOFT_RESET_ADDR, constants.SOFT_RESET_CMD)
|
| 85 |
+
time.sleep(constants.RESET_PERIOD / 1000.0)
|
| 86 |
+
|
| 87 |
+
def set_temp_offset(self, value):
|
| 88 |
+
"""Set temperature offset in celsius.
|
| 89 |
+
|
| 90 |
+
If set, the temperature t_fine will be increased by given value in celsius.
|
| 91 |
+
:param value: Temperature offset in Celsius, eg. 4, -8, 1.25
|
| 92 |
+
|
| 93 |
+
"""
|
| 94 |
+
if value == 0:
|
| 95 |
+
self.offset_temp_in_t_fine = 0
|
| 96 |
+
else:
|
| 97 |
+
self.offset_temp_in_t_fine = int(math.copysign((((int(abs(value) * 100)) << 8) - 128) / 5, value))
|
| 98 |
+
|
| 99 |
+
def set_humidity_oversample(self, value):
|
| 100 |
+
"""Set humidity oversampling.
|
| 101 |
+
|
| 102 |
+
A higher oversampling value means more stable sensor readings,
|
| 103 |
+
with less noise and jitter.
|
| 104 |
+
|
| 105 |
+
However each step of oversampling adds about 2ms to the latency,
|
| 106 |
+
causing a slower response time to fast transients.
|
| 107 |
+
|
| 108 |
+
:param value: Oversampling value, one of: OS_NONE, OS_1X, OS_2X, OS_4X, OS_8X, OS_16X
|
| 109 |
+
|
| 110 |
+
"""
|
| 111 |
+
self.tph_settings.os_hum = value
|
| 112 |
+
self._set_bits(constants.CONF_OS_H_ADDR, constants.OSH_MSK, constants.OSH_POS, value)
|
| 113 |
+
|
| 114 |
+
def get_humidity_oversample(self):
|
| 115 |
+
"""Get humidity oversampling."""
|
| 116 |
+
return (self._get_regs(constants.CONF_OS_H_ADDR, 1) & constants.OSH_MSK) >> constants.OSH_POS
|
| 117 |
+
|
| 118 |
+
def set_pressure_oversample(self, value):
|
| 119 |
+
"""Set temperature oversampling.
|
| 120 |
+
|
| 121 |
+
A higher oversampling value means more stable sensor readings,
|
| 122 |
+
with less noise and jitter.
|
| 123 |
+
|
| 124 |
+
However each step of oversampling adds about 2ms to the latency,
|
| 125 |
+
causing a slower response time to fast transients.
|
| 126 |
+
|
| 127 |
+
:param value: Oversampling value, one of: OS_NONE, OS_1X, OS_2X, OS_4X, OS_8X, OS_16X
|
| 128 |
+
|
| 129 |
+
"""
|
| 130 |
+
self.tph_settings.os_pres = value
|
| 131 |
+
self._set_bits(constants.CONF_T_P_MODE_ADDR, constants.OSP_MSK, constants.OSP_POS, value)
|
| 132 |
+
|
| 133 |
+
def get_pressure_oversample(self):
|
| 134 |
+
"""Get pressure oversampling."""
|
| 135 |
+
return (self._get_regs(constants.CONF_T_P_MODE_ADDR, 1) & constants.OSP_MSK) >> constants.OSP_POS
|
| 136 |
+
|
| 137 |
+
def set_temperature_oversample(self, value):
|
| 138 |
+
"""Set pressure oversampling.
|
| 139 |
+
|
| 140 |
+
A higher oversampling value means more stable sensor readings,
|
| 141 |
+
with less noise and jitter.
|
| 142 |
+
|
| 143 |
+
However each step of oversampling adds about 2ms to the latency,
|
| 144 |
+
causing a slower response time to fast transients.
|
| 145 |
+
|
| 146 |
+
:param value: Oversampling value, one of: OS_NONE, OS_1X, OS_2X, OS_4X, OS_8X, OS_16X
|
| 147 |
+
|
| 148 |
+
"""
|
| 149 |
+
self.tph_settings.os_temp = value
|
| 150 |
+
self._set_bits(constants.CONF_T_P_MODE_ADDR, constants.OST_MSK, constants.OST_POS, value)
|
| 151 |
+
|
| 152 |
+
def get_temperature_oversample(self):
|
| 153 |
+
"""Get temperature oversampling."""
|
| 154 |
+
return (self._get_regs(constants.CONF_T_P_MODE_ADDR, 1) & constants.OST_MSK) >> constants.OST_POS
|
| 155 |
+
|
| 156 |
+
def set_filter(self, value):
|
| 157 |
+
"""Set IIR filter size.
|
| 158 |
+
|
| 159 |
+
Optionally remove short term fluctuations from the temperature and pressure readings,
|
| 160 |
+
increasing their resolution but reducing their bandwidth.
|
| 161 |
+
|
| 162 |
+
Enabling the IIR filter does not slow down the time a reading takes, but will slow
|
| 163 |
+
down the BME680s response to changes in temperature and pressure.
|
| 164 |
+
|
| 165 |
+
When the IIR filter is enabled, the temperature and pressure resolution is effectively 20bit.
|
| 166 |
+
When it is disabled, it is 16bit + oversampling-1 bits.
|
| 167 |
+
|
| 168 |
+
"""
|
| 169 |
+
self.tph_settings.filter = value
|
| 170 |
+
self._set_bits(constants.CONF_ODR_FILT_ADDR, constants.FILTER_MSK, constants.FILTER_POS, value)
|
| 171 |
+
|
| 172 |
+
def get_filter(self):
|
| 173 |
+
"""Get filter size."""
|
| 174 |
+
return (self._get_regs(constants.CONF_ODR_FILT_ADDR, 1) & constants.FILTER_MSK) >> constants.FILTER_POS
|
| 175 |
+
|
| 176 |
+
def select_gas_heater_profile(self, value):
|
| 177 |
+
"""Set current gas sensor conversion profile.
|
| 178 |
+
|
| 179 |
+
Select one of the 10 configured heating durations/set points.
|
| 180 |
+
|
| 181 |
+
:param value: Profile index from 0 to 9
|
| 182 |
+
|
| 183 |
+
"""
|
| 184 |
+
if value > constants.NBCONV_MAX or value < constants.NBCONV_MIN:
|
| 185 |
+
raise ValueError("Profile '{}' should be between {} and {}".format(value, constants.NBCONV_MIN, constants.NBCONV_MAX))
|
| 186 |
+
|
| 187 |
+
self.gas_settings.nb_conv = value
|
| 188 |
+
self._set_bits(constants.CONF_ODR_RUN_GAS_NBC_ADDR, constants.NBCONV_MSK, constants.NBCONV_POS, value)
|
| 189 |
+
|
| 190 |
+
def get_gas_heater_profile(self):
|
| 191 |
+
"""Get gas sensor conversion profile: 0 to 9."""
|
| 192 |
+
return self._get_regs(constants.CONF_ODR_RUN_GAS_NBC_ADDR, 1) & constants.NBCONV_MSK
|
| 193 |
+
|
| 194 |
+
def set_gas_heater_status(self, value):
|
| 195 |
+
"""Enable/disable gas heater."""
|
| 196 |
+
self.gas_settings.heater = value
|
| 197 |
+
self._set_bits(constants.CONF_HEAT_CTRL_ADDR, constants.HCTRL_MSK, constants.HCTRL_POS, value)
|
| 198 |
+
|
| 199 |
+
def get_gas_heater_status(self):
|
| 200 |
+
"""Get current heater status."""
|
| 201 |
+
return (self._get_regs(constants.CONF_HEAT_CTRL_ADDR, 1) & constants.HCTRL_MSK) >> constants.HCTRL_POS
|
| 202 |
+
|
| 203 |
+
def set_gas_status(self, value):
|
| 204 |
+
"""Enable/disable gas sensor."""
|
| 205 |
+
if value == -1:
|
| 206 |
+
if self._variant == constants.VARIANT_HIGH:
|
| 207 |
+
value = constants.ENABLE_GAS_MEAS_HIGH
|
| 208 |
+
else:
|
| 209 |
+
value = constants.ENABLE_GAS_MEAS_LOW
|
| 210 |
+
self.gas_settings.run_gas = value
|
| 211 |
+
self._set_bits(constants.CONF_ODR_RUN_GAS_NBC_ADDR, constants.RUN_GAS_MSK, constants.RUN_GAS_POS, value)
|
| 212 |
+
|
| 213 |
+
def get_gas_status(self):
|
| 214 |
+
"""Get the current gas status."""
|
| 215 |
+
return (self._get_regs(constants.CONF_ODR_RUN_GAS_NBC_ADDR, 1) & constants.RUN_GAS_MSK) >> constants.RUN_GAS_POS
|
| 216 |
+
|
| 217 |
+
def set_gas_heater_profile(self, temperature, duration, nb_profile=0):
|
| 218 |
+
"""Set temperature and duration of gas sensor heater.
|
| 219 |
+
|
| 220 |
+
:param temperature: Target temperature in degrees celsius, between 200 and 400
|
| 221 |
+
:param durarion: Target duration in milliseconds, between 1 and 4032
|
| 222 |
+
:param nb_profile: Target profile, between 0 and 9
|
| 223 |
+
|
| 224 |
+
"""
|
| 225 |
+
self.set_gas_heater_temperature(temperature, nb_profile=nb_profile)
|
| 226 |
+
self.set_gas_heater_duration(duration, nb_profile=nb_profile)
|
| 227 |
+
|
| 228 |
+
def set_gas_heater_temperature(self, value, nb_profile=0):
|
| 229 |
+
"""Set gas sensor heater temperature.
|
| 230 |
+
|
| 231 |
+
:param value: Target temperature in degrees celsius, between 200 and 400
|
| 232 |
+
|
| 233 |
+
When setting an nb_profile other than 0,
|
| 234 |
+
make sure to select it with select_gas_heater_profile.
|
| 235 |
+
|
| 236 |
+
"""
|
| 237 |
+
if nb_profile > constants.NBCONV_MAX or value < constants.NBCONV_MIN:
|
| 238 |
+
raise ValueError('Profile "{}" should be between {} and {}'.format(nb_profile, constants.NBCONV_MIN, constants.NBCONV_MAX))
|
| 239 |
+
|
| 240 |
+
self.gas_settings.heatr_temp = value
|
| 241 |
+
temp = int(self._calc_heater_resistance(self.gas_settings.heatr_temp))
|
| 242 |
+
self._set_regs(constants.RES_HEAT0_ADDR + nb_profile, temp)
|
| 243 |
+
|
| 244 |
+
def set_gas_heater_duration(self, value, nb_profile=0):
|
| 245 |
+
"""Set gas sensor heater duration.
|
| 246 |
+
|
| 247 |
+
Heating durations between 1 ms and 4032 ms can be configured.
|
| 248 |
+
Approximately 20-30 ms are necessary for the heater to reach the intended target temperature.
|
| 249 |
+
|
| 250 |
+
:param value: Heating duration in milliseconds.
|
| 251 |
+
|
| 252 |
+
When setting an nb_profile other than 0,
|
| 253 |
+
make sure to select it with select_gas_heater_profile.
|
| 254 |
+
|
| 255 |
+
"""
|
| 256 |
+
if nb_profile > constants.NBCONV_MAX or value < constants.NBCONV_MIN:
|
| 257 |
+
raise ValueError('Profile "{}" should be between {} and {}'.format(nb_profile, constants.NBCONV_MIN, constants.NBCONV_MAX))
|
| 258 |
+
|
| 259 |
+
self.gas_settings.heatr_dur = value
|
| 260 |
+
temp = self._calc_heater_duration(self.gas_settings.heatr_dur)
|
| 261 |
+
self._set_regs(constants.GAS_WAIT0_ADDR + nb_profile, temp)
|
| 262 |
+
|
| 263 |
+
def set_power_mode(self, value, blocking=True):
|
| 264 |
+
"""Set power mode."""
|
| 265 |
+
if value not in (constants.SLEEP_MODE, constants.FORCED_MODE):
|
| 266 |
+
raise ValueError('Power mode should be one of SLEEP_MODE or FORCED_MODE')
|
| 267 |
+
|
| 268 |
+
self.power_mode = value
|
| 269 |
+
|
| 270 |
+
self._set_bits(constants.CONF_T_P_MODE_ADDR, constants.MODE_MSK, constants.MODE_POS, value)
|
| 271 |
+
|
| 272 |
+
while blocking and self.get_power_mode() != self.power_mode:
|
| 273 |
+
time.sleep(constants.POLL_PERIOD_MS / 1000.0)
|
| 274 |
+
|
| 275 |
+
def get_power_mode(self):
|
| 276 |
+
"""Get power mode."""
|
| 277 |
+
self.power_mode = self._get_regs(constants.CONF_T_P_MODE_ADDR, 1)
|
| 278 |
+
return self.power_mode
|
| 279 |
+
|
| 280 |
+
def get_sensor_data(self):
|
| 281 |
+
"""Get sensor data.
|
| 282 |
+
|
| 283 |
+
Stores data in .data and returns True upon success.
|
| 284 |
+
|
| 285 |
+
"""
|
| 286 |
+
self.set_power_mode(constants.FORCED_MODE)
|
| 287 |
+
|
| 288 |
+
for attempt in range(10):
|
| 289 |
+
status = self._get_regs(constants.FIELD0_ADDR, 1)
|
| 290 |
+
|
| 291 |
+
if (status & constants.NEW_DATA_MSK) == 0:
|
| 292 |
+
time.sleep(constants.POLL_PERIOD_MS / 1000.0)
|
| 293 |
+
continue
|
| 294 |
+
|
| 295 |
+
regs = self._get_regs(constants.FIELD0_ADDR, constants.FIELD_LENGTH)
|
| 296 |
+
|
| 297 |
+
self.data.status = regs[0] & constants.NEW_DATA_MSK
|
| 298 |
+
# Contains the nb_profile used to obtain the current measurement
|
| 299 |
+
self.data.gas_index = regs[0] & constants.GAS_INDEX_MSK
|
| 300 |
+
self.data.meas_index = regs[1]
|
| 301 |
+
|
| 302 |
+
adc_pres = (regs[2] << 12) | (regs[3] << 4) | (regs[4] >> 4)
|
| 303 |
+
adc_temp = (regs[5] << 12) | (regs[6] << 4) | (regs[7] >> 4)
|
| 304 |
+
adc_hum = (regs[8] << 8) | regs[9]
|
| 305 |
+
adc_gas_res_low = (regs[13] << 2) | (regs[14] >> 6)
|
| 306 |
+
adc_gas_res_high = (regs[15] << 2) | (regs[16] >> 6)
|
| 307 |
+
gas_range_l = regs[14] & constants.GAS_RANGE_MSK
|
| 308 |
+
gas_range_h = regs[16] & constants.GAS_RANGE_MSK
|
| 309 |
+
|
| 310 |
+
if self._variant == constants.VARIANT_HIGH:
|
| 311 |
+
self.data.status |= regs[16] & constants.GASM_VALID_MSK
|
| 312 |
+
self.data.status |= regs[16] & constants.HEAT_STAB_MSK
|
| 313 |
+
else:
|
| 314 |
+
self.data.status |= regs[14] & constants.GASM_VALID_MSK
|
| 315 |
+
self.data.status |= regs[14] & constants.HEAT_STAB_MSK
|
| 316 |
+
|
| 317 |
+
self.data.heat_stable = (self.data.status & constants.HEAT_STAB_MSK) > 0
|
| 318 |
+
|
| 319 |
+
temperature = self._calc_temperature(adc_temp)
|
| 320 |
+
self.data.temperature = temperature / 100.0
|
| 321 |
+
self.ambient_temperature = temperature # Saved for heater calc
|
| 322 |
+
|
| 323 |
+
self.data.pressure = self._calc_pressure(adc_pres) / 100.0
|
| 324 |
+
self.data.humidity = self._calc_humidity(adc_hum) / 1000.0
|
| 325 |
+
|
| 326 |
+
if self._variant == constants.VARIANT_HIGH:
|
| 327 |
+
self.data.gas_resistance = self._calc_gas_resistance_high(adc_gas_res_high, gas_range_h)
|
| 328 |
+
else:
|
| 329 |
+
self.data.gas_resistance = self._calc_gas_resistance_low(adc_gas_res_low, gas_range_l)
|
| 330 |
+
|
| 331 |
+
return True
|
| 332 |
+
|
| 333 |
+
return False
|
| 334 |
+
|
| 335 |
+
def _set_bits(self, register, mask, position, value):
|
| 336 |
+
"""Mask out and set one or more bits in a register."""
|
| 337 |
+
temp = self._get_regs(register, 1)
|
| 338 |
+
temp &= ~mask
|
| 339 |
+
temp |= value << position
|
| 340 |
+
self._set_regs(register, temp)
|
| 341 |
+
|
| 342 |
+
def _set_regs(self, register, value):
|
| 343 |
+
"""Set one or more registers."""
|
| 344 |
+
if isinstance(value, int):
|
| 345 |
+
self._i2c.write_byte_data(self.i2c_addr, register, value)
|
| 346 |
+
else:
|
| 347 |
+
self._i2c.write_i2c_block_data(self.i2c_addr, register, value)
|
| 348 |
+
|
| 349 |
+
def _get_regs(self, register, length):
|
| 350 |
+
"""Get one or more registers."""
|
| 351 |
+
if length == 1:
|
| 352 |
+
return self._i2c.read_byte_data(self.i2c_addr, register)
|
| 353 |
+
else:
|
| 354 |
+
return self._i2c.read_i2c_block_data(self.i2c_addr, register, length)
|
| 355 |
+
|
| 356 |
+
def _calc_temperature(self, temperature_adc):
|
| 357 |
+
"""Convert the raw temperature to degrees C using calibration_data."""
|
| 358 |
+
var1 = (temperature_adc >> 3) - (self.calibration_data.par_t1 << 1)
|
| 359 |
+
var2 = (var1 * self.calibration_data.par_t2) >> 11
|
| 360 |
+
var3 = ((var1 >> 1) * (var1 >> 1)) >> 12
|
| 361 |
+
var3 = ((var3) * (self.calibration_data.par_t3 << 4)) >> 14
|
| 362 |
+
|
| 363 |
+
# Save teperature data for pressure calculations
|
| 364 |
+
self.calibration_data.t_fine = (var2 + var3) + self.offset_temp_in_t_fine
|
| 365 |
+
calc_temp = (((self.calibration_data.t_fine * 5) + 128) >> 8)
|
| 366 |
+
|
| 367 |
+
return calc_temp
|
| 368 |
+
|
| 369 |
+
def _calc_pressure(self, pressure_adc):
|
| 370 |
+
"""Convert the raw pressure using calibration data."""
|
| 371 |
+
var1 = ((self.calibration_data.t_fine) >> 1) - 64000
|
| 372 |
+
var2 = ((((var1 >> 2) * (var1 >> 2)) >> 11) *
|
| 373 |
+
self.calibration_data.par_p6) >> 2
|
| 374 |
+
var2 = var2 + ((var1 * self.calibration_data.par_p5) << 1)
|
| 375 |
+
var2 = (var2 >> 2) + (self.calibration_data.par_p4 << 16)
|
| 376 |
+
var1 = (((((var1 >> 2) * (var1 >> 2)) >> 13) *
|
| 377 |
+
((self.calibration_data.par_p3 << 5)) >> 3) +
|
| 378 |
+
((self.calibration_data.par_p2 * var1) >> 1))
|
| 379 |
+
var1 = var1 >> 18
|
| 380 |
+
|
| 381 |
+
var1 = ((32768 + var1) * self.calibration_data.par_p1) >> 15
|
| 382 |
+
calc_pressure = 1048576 - pressure_adc
|
| 383 |
+
calc_pressure = ((calc_pressure - (var2 >> 12)) * (3125))
|
| 384 |
+
|
| 385 |
+
if calc_pressure >= (1 << 31):
|
| 386 |
+
calc_pressure = ((calc_pressure // var1) << 1)
|
| 387 |
+
else:
|
| 388 |
+
calc_pressure = ((calc_pressure << 1) // var1)
|
| 389 |
+
|
| 390 |
+
var1 = (self.calibration_data.par_p9 * (((calc_pressure >> 3) *
|
| 391 |
+
(calc_pressure >> 3)) >> 13)) >> 12
|
| 392 |
+
var2 = ((calc_pressure >> 2) *
|
| 393 |
+
self.calibration_data.par_p8) >> 13
|
| 394 |
+
var3 = ((calc_pressure >> 8) * (calc_pressure >> 8) *
|
| 395 |
+
(calc_pressure >> 8) *
|
| 396 |
+
self.calibration_data.par_p10) >> 17
|
| 397 |
+
|
| 398 |
+
calc_pressure = (calc_pressure) + ((var1 + var2 + var3 +
|
| 399 |
+
(self.calibration_data.par_p7 << 7)) >> 4)
|
| 400 |
+
|
| 401 |
+
return calc_pressure
|
| 402 |
+
|
| 403 |
+
def _calc_humidity(self, humidity_adc):
|
| 404 |
+
"""Convert the raw humidity using calibration data."""
|
| 405 |
+
temp_scaled = ((self.calibration_data.t_fine * 5) + 128) >> 8
|
| 406 |
+
var1 = (humidity_adc - ((self.calibration_data.par_h1 * 16))) -\
|
| 407 |
+
(((temp_scaled * self.calibration_data.par_h3) // (100)) >> 1)
|
| 408 |
+
var2 = (self.calibration_data.par_h2 *
|
| 409 |
+
(((temp_scaled * self.calibration_data.par_h4) // (100)) +
|
| 410 |
+
(((temp_scaled * ((temp_scaled * self.calibration_data.par_h5) // (100))) >> 6) //
|
| 411 |
+
(100)) + (1 * 16384))) >> 10
|
| 412 |
+
var3 = var1 * var2
|
| 413 |
+
var4 = self.calibration_data.par_h6 << 7
|
| 414 |
+
var4 = ((var4) + ((temp_scaled * self.calibration_data.par_h7) // (100))) >> 4
|
| 415 |
+
var5 = ((var3 >> 14) * (var3 >> 14)) >> 10
|
| 416 |
+
var6 = (var4 * var5) >> 1
|
| 417 |
+
calc_hum = (((var3 + var6) >> 10) * (1000)) >> 12
|
| 418 |
+
|
| 419 |
+
return min(max(calc_hum, 0), 100000)
|
| 420 |
+
|
| 421 |
+
def _calc_gas_resistance(self, gas_res_adc, gas_range):
|
| 422 |
+
"""Convert the raw gas resistance using calibration data."""
|
| 423 |
+
if self._variant == constants.VARIANT_HIGH:
|
| 424 |
+
return self._calc_gas_resistance_high(gas_res_adc, gas_range)
|
| 425 |
+
else:
|
| 426 |
+
return self._calc_gas_resistance_low(gas_res_adc, gas_range)
|
| 427 |
+
|
| 428 |
+
def _calc_gas_resistance_high(self, gas_res_adc, gas_range):
|
| 429 |
+
"""Convert the raw gas resistance using calibration data.
|
| 430 |
+
|
| 431 |
+
Applies to Variant ID == 0x01 only.
|
| 432 |
+
|
| 433 |
+
"""
|
| 434 |
+
var1 = 262144 >> gas_range
|
| 435 |
+
var2 = gas_res_adc - 512
|
| 436 |
+
|
| 437 |
+
var2 *= 3
|
| 438 |
+
var2 = 4096 + var2
|
| 439 |
+
|
| 440 |
+
calc_gas_res = (10000 * var1) / var2
|
| 441 |
+
calc_gas_res *= 100
|
| 442 |
+
|
| 443 |
+
return calc_gas_res
|
| 444 |
+
|
| 445 |
+
def _calc_gas_resistance_low(self, gas_res_adc, gas_range):
|
| 446 |
+
"""Convert the raw gas resistance using calibration data.
|
| 447 |
+
|
| 448 |
+
Applies to Variant ID == 0x00 only.
|
| 449 |
+
|
| 450 |
+
"""
|
| 451 |
+
var1 = ((1340 + (5 * self.calibration_data.range_sw_err)) * (lookupTable1[gas_range])) >> 16
|
| 452 |
+
var2 = (((gas_res_adc << 15) - (16777216)) + var1)
|
| 453 |
+
var3 = ((lookupTable2[gas_range] * var1) >> 9)
|
| 454 |
+
calc_gas_res = ((var3 + (var2 >> 1)) / var2)
|
| 455 |
+
|
| 456 |
+
if calc_gas_res < 0:
|
| 457 |
+
calc_gas_res = (1 << 32) + calc_gas_res
|
| 458 |
+
|
| 459 |
+
return calc_gas_res
|
| 460 |
+
|
| 461 |
+
def _calc_heater_resistance(self, temperature):
|
| 462 |
+
"""Convert raw heater resistance using calibration data."""
|
| 463 |
+
temperature = min(max(temperature, 200), 400)
|
| 464 |
+
|
| 465 |
+
var1 = ((self.ambient_temperature * self.calibration_data.par_gh3) / 1000) * 256
|
| 466 |
+
var2 = (self.calibration_data.par_gh1 + 784) * (((((self.calibration_data.par_gh2 + 154009) * temperature * 5) / 100) + 3276800) / 10)
|
| 467 |
+
var3 = var1 + (var2 / 2)
|
| 468 |
+
var4 = (var3 / (self.calibration_data.res_heat_range + 4))
|
| 469 |
+
var5 = (131 * self.calibration_data.res_heat_val) + 65536
|
| 470 |
+
heatr_res_x100 = (((var4 / var5) - 250) * 34)
|
| 471 |
+
heatr_res = ((heatr_res_x100 + 50) / 100)
|
| 472 |
+
|
| 473 |
+
return heatr_res
|
| 474 |
+
|
| 475 |
+
def _calc_heater_duration(self, duration):
|
| 476 |
+
"""Calculate correct value for heater duration setting from milliseconds."""
|
| 477 |
+
if duration < 0xfc0:
|
| 478 |
+
factor = 0
|
| 479 |
+
|
| 480 |
+
while duration > 0x3f:
|
| 481 |
+
duration /= 4
|
| 482 |
+
factor += 1
|
| 483 |
+
|
| 484 |
+
return int(duration + (factor * 64))
|
| 485 |
+
|
| 486 |
+
return 0xff
|
scripts/lib/bme680/constants.py
ADDED
|
@@ -0,0 +1,413 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""BME680 constants, structures and utilities."""
|
| 2 |
+
|
| 3 |
+
# BME680 General config
|
| 4 |
+
POLL_PERIOD_MS = 10
|
| 5 |
+
|
| 6 |
+
# BME680 I2C addresses
|
| 7 |
+
I2C_ADDR_PRIMARY = 0x76
|
| 8 |
+
I2C_ADDR_SECONDARY = 0x77
|
| 9 |
+
|
| 10 |
+
# BME680 unique chip identifier
|
| 11 |
+
CHIP_ID = 0x61
|
| 12 |
+
|
| 13 |
+
# BME680 coefficients related defines
|
| 14 |
+
COEFF_SIZE = 41
|
| 15 |
+
COEFF_ADDR1_LEN = 25
|
| 16 |
+
COEFF_ADDR2_LEN = 16
|
| 17 |
+
|
| 18 |
+
# BME680 field_x related defines
|
| 19 |
+
FIELD_LENGTH = 17
|
| 20 |
+
FIELD_ADDR_OFFSET = 17
|
| 21 |
+
|
| 22 |
+
# Soft reset command
|
| 23 |
+
SOFT_RESET_CMD = 0xb6
|
| 24 |
+
|
| 25 |
+
# Error code definitions
|
| 26 |
+
OK = 0
|
| 27 |
+
# Errors
|
| 28 |
+
E_NULL_PTR = -1
|
| 29 |
+
E_COM_FAIL = -2
|
| 30 |
+
E_DEV_NOT_FOUND = -3
|
| 31 |
+
E_INVALID_LENGTH = -4
|
| 32 |
+
|
| 33 |
+
# Warnings
|
| 34 |
+
W_DEFINE_PWR_MODE = 1
|
| 35 |
+
W_NO_NEW_DATA = 2
|
| 36 |
+
|
| 37 |
+
# Info's
|
| 38 |
+
I_MIN_CORRECTION = 1
|
| 39 |
+
I_MAX_CORRECTION = 2
|
| 40 |
+
|
| 41 |
+
# Register map
|
| 42 |
+
# Other coefficient's address
|
| 43 |
+
ADDR_RES_HEAT_VAL_ADDR = 0x00
|
| 44 |
+
ADDR_RES_HEAT_RANGE_ADDR = 0x02
|
| 45 |
+
ADDR_RANGE_SW_ERR_ADDR = 0x04
|
| 46 |
+
ADDR_SENS_CONF_START = 0x5A
|
| 47 |
+
ADDR_GAS_CONF_START = 0x64
|
| 48 |
+
|
| 49 |
+
# Field settings
|
| 50 |
+
FIELD0_ADDR = 0x1d
|
| 51 |
+
|
| 52 |
+
# Heater settings
|
| 53 |
+
RES_HEAT0_ADDR = 0x5a
|
| 54 |
+
GAS_WAIT0_ADDR = 0x64
|
| 55 |
+
|
| 56 |
+
# Sensor configuration registers
|
| 57 |
+
CONF_HEAT_CTRL_ADDR = 0x70
|
| 58 |
+
CONF_ODR_RUN_GAS_NBC_ADDR = 0x71
|
| 59 |
+
CONF_OS_H_ADDR = 0x72
|
| 60 |
+
MEM_PAGE_ADDR = 0xf3
|
| 61 |
+
CONF_T_P_MODE_ADDR = 0x74
|
| 62 |
+
CONF_ODR_FILT_ADDR = 0x75
|
| 63 |
+
|
| 64 |
+
# Coefficient's address
|
| 65 |
+
COEFF_ADDR1 = 0x89
|
| 66 |
+
COEFF_ADDR2 = 0xe1
|
| 67 |
+
|
| 68 |
+
# Chip identifier
|
| 69 |
+
CHIP_ID_ADDR = 0xd0
|
| 70 |
+
CHIP_VARIANT_ADDR = 0xf0
|
| 71 |
+
|
| 72 |
+
VARIANT_LOW = 0x00
|
| 73 |
+
VARIANT_HIGH = 0x01
|
| 74 |
+
|
| 75 |
+
# Soft reset register
|
| 76 |
+
SOFT_RESET_ADDR = 0xe0
|
| 77 |
+
|
| 78 |
+
# Heater control settings
|
| 79 |
+
ENABLE_HEATER = 0x00
|
| 80 |
+
DISABLE_HEATER = 0x08
|
| 81 |
+
|
| 82 |
+
# Gas measurement settings
|
| 83 |
+
DISABLE_GAS_MEAS = 0x00
|
| 84 |
+
ENABLE_GAS_MEAS = -1 # Now used as auto-select
|
| 85 |
+
ENABLE_GAS_MEAS_LOW = 0x01
|
| 86 |
+
ENABLE_GAS_MEAS_HIGH = 0x02
|
| 87 |
+
|
| 88 |
+
# Over-sampling settings
|
| 89 |
+
OS_NONE = 0
|
| 90 |
+
OS_1X = 1
|
| 91 |
+
OS_2X = 2
|
| 92 |
+
OS_4X = 3
|
| 93 |
+
OS_8X = 4
|
| 94 |
+
OS_16X = 5
|
| 95 |
+
|
| 96 |
+
# IIR filter settings
|
| 97 |
+
FILTER_SIZE_0 = 0
|
| 98 |
+
FILTER_SIZE_1 = 1
|
| 99 |
+
FILTER_SIZE_3 = 2
|
| 100 |
+
FILTER_SIZE_7 = 3
|
| 101 |
+
FILTER_SIZE_15 = 4
|
| 102 |
+
FILTER_SIZE_31 = 5
|
| 103 |
+
FILTER_SIZE_63 = 6
|
| 104 |
+
FILTER_SIZE_127 = 7
|
| 105 |
+
|
| 106 |
+
# Power mode settings
|
| 107 |
+
SLEEP_MODE = 0
|
| 108 |
+
FORCED_MODE = 1
|
| 109 |
+
|
| 110 |
+
# Delay related macro declaration
|
| 111 |
+
RESET_PERIOD = 10
|
| 112 |
+
|
| 113 |
+
# SPI memory page settings
|
| 114 |
+
MEM_PAGE0 = 0x10
|
| 115 |
+
MEM_PAGE1 = 0x00
|
| 116 |
+
|
| 117 |
+
# Ambient humidity shift value for compensation
|
| 118 |
+
HUM_REG_SHIFT_VAL = 4
|
| 119 |
+
|
| 120 |
+
# Run gas enable and disable settings
|
| 121 |
+
RUN_GAS_DISABLE = 0
|
| 122 |
+
RUN_GAS_ENABLE = 1
|
| 123 |
+
|
| 124 |
+
# Gas heater enable and disable settings
|
| 125 |
+
GAS_HEAT_ENABLE = 0
|
| 126 |
+
GAS_HEAT_DISABLE = 1
|
| 127 |
+
|
| 128 |
+
# Buffer length macro declaration
|
| 129 |
+
TMP_BUFFER_LENGTH = 40
|
| 130 |
+
REG_BUFFER_LENGTH = 6
|
| 131 |
+
FIELD_DATA_LENGTH = 3
|
| 132 |
+
GAS_REG_BUF_LENGTH = 20
|
| 133 |
+
GAS_HEATER_PROF_LEN_MAX = 10
|
| 134 |
+
|
| 135 |
+
# Settings selector
|
| 136 |
+
OST_SEL = 1
|
| 137 |
+
OSP_SEL = 2
|
| 138 |
+
OSH_SEL = 4
|
| 139 |
+
GAS_MEAS_SEL = 8
|
| 140 |
+
FILTER_SEL = 16
|
| 141 |
+
HCNTRL_SEL = 32
|
| 142 |
+
RUN_GAS_SEL = 64
|
| 143 |
+
NBCONV_SEL = 128
|
| 144 |
+
GAS_SENSOR_SEL = GAS_MEAS_SEL | RUN_GAS_SEL | NBCONV_SEL
|
| 145 |
+
|
| 146 |
+
# Number of conversion settings
|
| 147 |
+
NBCONV_MIN = 0
|
| 148 |
+
NBCONV_MAX = 9 # Was 10, but there are only 10 settings: 0 1 2 ... 8 9
|
| 149 |
+
|
| 150 |
+
# Mask definitions
|
| 151 |
+
GAS_MEAS_MSK = 0x30
|
| 152 |
+
NBCONV_MSK = 0X0F
|
| 153 |
+
FILTER_MSK = 0X1C
|
| 154 |
+
OST_MSK = 0XE0
|
| 155 |
+
OSP_MSK = 0X1C
|
| 156 |
+
OSH_MSK = 0X07
|
| 157 |
+
HCTRL_MSK = 0x08
|
| 158 |
+
RUN_GAS_MSK = 0x30
|
| 159 |
+
MODE_MSK = 0x03
|
| 160 |
+
RHRANGE_MSK = 0x30
|
| 161 |
+
RSERROR_MSK = 0xf0
|
| 162 |
+
NEW_DATA_MSK = 0x80
|
| 163 |
+
GAS_INDEX_MSK = 0x0f
|
| 164 |
+
GAS_RANGE_MSK = 0x0f
|
| 165 |
+
GASM_VALID_MSK = 0x20
|
| 166 |
+
HEAT_STAB_MSK = 0x10
|
| 167 |
+
MEM_PAGE_MSK = 0x10
|
| 168 |
+
SPI_RD_MSK = 0x80
|
| 169 |
+
SPI_WR_MSK = 0x7f
|
| 170 |
+
BIT_H1_DATA_MSK = 0x0F
|
| 171 |
+
|
| 172 |
+
# Bit position definitions for sensor settings
|
| 173 |
+
GAS_MEAS_POS = 4
|
| 174 |
+
FILTER_POS = 2
|
| 175 |
+
OST_POS = 5
|
| 176 |
+
OSP_POS = 2
|
| 177 |
+
OSH_POS = 0
|
| 178 |
+
HCTRL_POS = 3
|
| 179 |
+
RUN_GAS_POS = 4
|
| 180 |
+
MODE_POS = 0
|
| 181 |
+
NBCONV_POS = 0
|
| 182 |
+
|
| 183 |
+
# Array Index to Field data mapping for Calibration Data
|
| 184 |
+
T2_LSB_REG = 1
|
| 185 |
+
T2_MSB_REG = 2
|
| 186 |
+
T3_REG = 3
|
| 187 |
+
P1_LSB_REG = 5
|
| 188 |
+
P1_MSB_REG = 6
|
| 189 |
+
P2_LSB_REG = 7
|
| 190 |
+
P2_MSB_REG = 8
|
| 191 |
+
P3_REG = 9
|
| 192 |
+
P4_LSB_REG = 11
|
| 193 |
+
P4_MSB_REG = 12
|
| 194 |
+
P5_LSB_REG = 13
|
| 195 |
+
P5_MSB_REG = 14
|
| 196 |
+
P7_REG = 15
|
| 197 |
+
P6_REG = 16
|
| 198 |
+
P8_LSB_REG = 19
|
| 199 |
+
P8_MSB_REG = 20
|
| 200 |
+
P9_LSB_REG = 21
|
| 201 |
+
P9_MSB_REG = 22
|
| 202 |
+
P10_REG = 23
|
| 203 |
+
H2_MSB_REG = 25
|
| 204 |
+
H2_LSB_REG = 26
|
| 205 |
+
H1_LSB_REG = 26
|
| 206 |
+
H1_MSB_REG = 27
|
| 207 |
+
H3_REG = 28
|
| 208 |
+
H4_REG = 29
|
| 209 |
+
H5_REG = 30
|
| 210 |
+
H6_REG = 31
|
| 211 |
+
H7_REG = 32
|
| 212 |
+
T1_LSB_REG = 33
|
| 213 |
+
T1_MSB_REG = 34
|
| 214 |
+
GH2_LSB_REG = 35
|
| 215 |
+
GH2_MSB_REG = 36
|
| 216 |
+
GH1_REG = 37
|
| 217 |
+
GH3_REG = 38
|
| 218 |
+
|
| 219 |
+
# BME680 register buffer index settings
|
| 220 |
+
REG_FILTER_INDEX = 5
|
| 221 |
+
REG_TEMP_INDEX = 4
|
| 222 |
+
REG_PRES_INDEX = 4
|
| 223 |
+
REG_HUM_INDEX = 2
|
| 224 |
+
REG_NBCONV_INDEX = 1
|
| 225 |
+
REG_RUN_GAS_INDEX = 1
|
| 226 |
+
REG_HCTRL_INDEX = 0
|
| 227 |
+
|
| 228 |
+
# Look up tables for the possible gas range values
|
| 229 |
+
lookupTable1 = [2147483647, 2147483647, 2147483647, 2147483647,
|
| 230 |
+
2147483647, 2126008810, 2147483647, 2130303777, 2147483647,
|
| 231 |
+
2147483647, 2143188679, 2136746228, 2147483647, 2126008810,
|
| 232 |
+
2147483647, 2147483647]
|
| 233 |
+
|
| 234 |
+
lookupTable2 = [4096000000, 2048000000, 1024000000, 512000000,
|
| 235 |
+
255744255, 127110228, 64000000, 32258064,
|
| 236 |
+
16016016, 8000000, 4000000, 2000000,
|
| 237 |
+
1000000, 500000, 250000, 125000]
|
| 238 |
+
|
| 239 |
+
|
| 240 |
+
def bytes_to_word(msb, lsb, bits=16, signed=False):
|
| 241 |
+
"""Convert a most and least significant byte into a word."""
|
| 242 |
+
# TODO: Reimplement with struct
|
| 243 |
+
word = (msb << 8) | lsb
|
| 244 |
+
if signed:
|
| 245 |
+
word = twos_comp(word, bits)
|
| 246 |
+
return word
|
| 247 |
+
|
| 248 |
+
|
| 249 |
+
def twos_comp(val, bits=16):
|
| 250 |
+
"""Convert two bytes into a two's compliment signed word."""
|
| 251 |
+
# TODO: Reimplement with struct
|
| 252 |
+
if val & (1 << (bits - 1)) != 0:
|
| 253 |
+
val = val - (1 << bits)
|
| 254 |
+
return val
|
| 255 |
+
|
| 256 |
+
|
| 257 |
+
class FieldData:
|
| 258 |
+
"""Structure for storing BME680 sensor data."""
|
| 259 |
+
|
| 260 |
+
def __init__(self): # noqa D107
|
| 261 |
+
# Contains new_data, gasm_valid & heat_stab
|
| 262 |
+
self.status = None
|
| 263 |
+
self.heat_stable = False
|
| 264 |
+
# The index of the heater profile used
|
| 265 |
+
self.gas_index = None
|
| 266 |
+
# Measurement index to track order
|
| 267 |
+
self.meas_index = None
|
| 268 |
+
# Temperature in degree celsius x100
|
| 269 |
+
self.temperature = None
|
| 270 |
+
# Pressure in Pascal
|
| 271 |
+
self.pressure = None
|
| 272 |
+
# Humidity in % relative humidity x1000
|
| 273 |
+
self.humidity = None
|
| 274 |
+
# Gas resistance in Ohms
|
| 275 |
+
self.gas_resistance = None
|
| 276 |
+
|
| 277 |
+
|
| 278 |
+
class CalibrationData:
|
| 279 |
+
"""Structure for storing BME680 calibration data."""
|
| 280 |
+
|
| 281 |
+
def __init__(self): # noqa D107
|
| 282 |
+
self.par_h1 = None
|
| 283 |
+
self.par_h2 = None
|
| 284 |
+
self.par_h3 = None
|
| 285 |
+
self.par_h4 = None
|
| 286 |
+
self.par_h5 = None
|
| 287 |
+
self.par_h6 = None
|
| 288 |
+
self.par_h7 = None
|
| 289 |
+
self.par_gh1 = None
|
| 290 |
+
self.par_gh2 = None
|
| 291 |
+
self.par_gh3 = None
|
| 292 |
+
self.par_t1 = None
|
| 293 |
+
self.par_t2 = None
|
| 294 |
+
self.par_t3 = None
|
| 295 |
+
self.par_p1 = None
|
| 296 |
+
self.par_p2 = None
|
| 297 |
+
self.par_p3 = None
|
| 298 |
+
self.par_p4 = None
|
| 299 |
+
self.par_p5 = None
|
| 300 |
+
self.par_p6 = None
|
| 301 |
+
self.par_p7 = None
|
| 302 |
+
self.par_p8 = None
|
| 303 |
+
self.par_p9 = None
|
| 304 |
+
self.par_p10 = None
|
| 305 |
+
# Variable to store t_fine size
|
| 306 |
+
self.t_fine = None
|
| 307 |
+
# Variable to store heater resistance range
|
| 308 |
+
self.res_heat_range = None
|
| 309 |
+
# Variable to store heater resistance value
|
| 310 |
+
self.res_heat_val = None
|
| 311 |
+
# Variable to store error range
|
| 312 |
+
self.range_sw_err = None
|
| 313 |
+
|
| 314 |
+
def set_from_array(self, calibration):
|
| 315 |
+
"""Set parameters from an array of bytes."""
|
| 316 |
+
# Temperature related coefficients
|
| 317 |
+
self.par_t1 = bytes_to_word(calibration[T1_MSB_REG], calibration[T1_LSB_REG])
|
| 318 |
+
self.par_t2 = bytes_to_word(calibration[T2_MSB_REG], calibration[T2_LSB_REG], bits=16, signed=True)
|
| 319 |
+
self.par_t3 = twos_comp(calibration[T3_REG], bits=8)
|
| 320 |
+
|
| 321 |
+
# Pressure related coefficients
|
| 322 |
+
self.par_p1 = bytes_to_word(calibration[P1_MSB_REG], calibration[P1_LSB_REG])
|
| 323 |
+
self.par_p2 = bytes_to_word(calibration[P2_MSB_REG], calibration[P2_LSB_REG], bits=16, signed=True)
|
| 324 |
+
self.par_p3 = twos_comp(calibration[P3_REG], bits=8)
|
| 325 |
+
self.par_p4 = bytes_to_word(calibration[P4_MSB_REG], calibration[P4_LSB_REG], bits=16, signed=True)
|
| 326 |
+
self.par_p5 = bytes_to_word(calibration[P5_MSB_REG], calibration[P5_LSB_REG], bits=16, signed=True)
|
| 327 |
+
self.par_p6 = twos_comp(calibration[P6_REG], bits=8)
|
| 328 |
+
self.par_p7 = twos_comp(calibration[P7_REG], bits=8)
|
| 329 |
+
self.par_p8 = bytes_to_word(calibration[P8_MSB_REG], calibration[P8_LSB_REG], bits=16, signed=True)
|
| 330 |
+
self.par_p9 = bytes_to_word(calibration[P9_MSB_REG], calibration[P9_LSB_REG], bits=16, signed=True)
|
| 331 |
+
self.par_p10 = calibration[P10_REG]
|
| 332 |
+
|
| 333 |
+
# Humidity related coefficients
|
| 334 |
+
self.par_h1 = (calibration[H1_MSB_REG] << HUM_REG_SHIFT_VAL) | (calibration[H1_LSB_REG] & BIT_H1_DATA_MSK)
|
| 335 |
+
self.par_h2 = (calibration[H2_MSB_REG] << HUM_REG_SHIFT_VAL) | (calibration[H2_LSB_REG] >> HUM_REG_SHIFT_VAL)
|
| 336 |
+
self.par_h3 = twos_comp(calibration[H3_REG], bits=8)
|
| 337 |
+
self.par_h4 = twos_comp(calibration[H4_REG], bits=8)
|
| 338 |
+
self.par_h5 = twos_comp(calibration[H5_REG], bits=8)
|
| 339 |
+
self.par_h6 = calibration[H6_REG]
|
| 340 |
+
self.par_h7 = twos_comp(calibration[H7_REG], bits=8)
|
| 341 |
+
|
| 342 |
+
# Gas heater related coefficients
|
| 343 |
+
self.par_gh1 = twos_comp(calibration[GH1_REG], bits=8)
|
| 344 |
+
self.par_gh2 = bytes_to_word(calibration[GH2_MSB_REG], calibration[GH2_LSB_REG], bits=16, signed=True)
|
| 345 |
+
self.par_gh3 = twos_comp(calibration[GH3_REG], bits=8)
|
| 346 |
+
|
| 347 |
+
def set_other(self, heat_range, heat_value, sw_error):
|
| 348 |
+
"""Set other values."""
|
| 349 |
+
self.res_heat_range = (heat_range & RHRANGE_MSK) // 16
|
| 350 |
+
self.res_heat_val = heat_value
|
| 351 |
+
self.range_sw_err = (sw_error & RSERROR_MSK) // 16
|
| 352 |
+
|
| 353 |
+
|
| 354 |
+
class TPHSettings:
|
| 355 |
+
"""Structure for storing BME680 sensor settings.
|
| 356 |
+
|
| 357 |
+
Comprises of output data rate, over-sampling and filter settings.
|
| 358 |
+
|
| 359 |
+
"""
|
| 360 |
+
|
| 361 |
+
def __init__(self): # noqa D107
|
| 362 |
+
# Humidity oversampling
|
| 363 |
+
self.os_hum = None
|
| 364 |
+
# Temperature oversampling
|
| 365 |
+
self.os_temp = None
|
| 366 |
+
# Pressure oversampling
|
| 367 |
+
self.os_pres = None
|
| 368 |
+
# Filter coefficient
|
| 369 |
+
self.filter = None
|
| 370 |
+
|
| 371 |
+
|
| 372 |
+
class GasSettings:
|
| 373 |
+
"""Structure for storing BME680 gas settings and status."""
|
| 374 |
+
|
| 375 |
+
def __init__(self): # noqa D107
|
| 376 |
+
# Variable to store nb conversion
|
| 377 |
+
self.nb_conv = None
|
| 378 |
+
# Variable to store heater control
|
| 379 |
+
self.heatr_ctrl = None
|
| 380 |
+
# Run gas enable value
|
| 381 |
+
self.run_gas = None
|
| 382 |
+
# Pointer to store heater temperature
|
| 383 |
+
self.heatr_temp = None
|
| 384 |
+
# Pointer to store duration profile
|
| 385 |
+
self.heatr_dur = None
|
| 386 |
+
|
| 387 |
+
|
| 388 |
+
class BME680Data:
|
| 389 |
+
"""Structure to represent BME680 device."""
|
| 390 |
+
|
| 391 |
+
def __init__(self): # noqa D107
|
| 392 |
+
# Chip Id
|
| 393 |
+
self.chip_id = None
|
| 394 |
+
# Device Id
|
| 395 |
+
self.dev_id = None
|
| 396 |
+
# SPI/I2C interface
|
| 397 |
+
self.intf = None
|
| 398 |
+
# Memory page used
|
| 399 |
+
self.mem_page = None
|
| 400 |
+
# Ambient temperature in Degree C
|
| 401 |
+
self.ambient_temperature = None
|
| 402 |
+
# Field Data
|
| 403 |
+
self.data = FieldData()
|
| 404 |
+
# Sensor calibration data
|
| 405 |
+
self.calibration_data = CalibrationData()
|
| 406 |
+
# Sensor settings
|
| 407 |
+
self.tph_settings = TPHSettings()
|
| 408 |
+
# Gas Sensor settings
|
| 409 |
+
self.gas_settings = GasSettings()
|
| 410 |
+
# Sensor power modes
|
| 411 |
+
self.power_mode = None
|
| 412 |
+
# New sensor fields
|
| 413 |
+
self.new_fields = None
|
scripts/lib/data_logging.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import sys
|
| 3 |
+
from time import gmtime, localtime, time
|
| 4 |
+
|
| 5 |
+
import machine
|
| 6 |
+
import ntptime
|
| 7 |
+
import uos
|
| 8 |
+
import urequests
|
| 9 |
+
from machine import SPI, Pin
|
| 10 |
+
from sdcard import sdcard
|
| 11 |
+
from uio import StringIO
|
| 12 |
+
|
| 13 |
+
# # uses a more robust ntptime
|
| 14 |
+
# from lib.ntptime import ntptime
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def get_traceback(err):
|
| 18 |
+
try:
|
| 19 |
+
with StringIO() as f: # type: ignore
|
| 20 |
+
sys.print_exception(err, f)
|
| 21 |
+
return f.getvalue()
|
| 22 |
+
except Exception as err2:
|
| 23 |
+
print(err2)
|
| 24 |
+
return f"Failed to extract file and line number due to {err2}.\nOriginal error: {err}" # noqa: E501
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def initialize_sdcard(
|
| 28 |
+
spi_id=1,
|
| 29 |
+
cs_pin=15,
|
| 30 |
+
sck_pin=10,
|
| 31 |
+
mosi_pin=11,
|
| 32 |
+
miso_pin=12,
|
| 33 |
+
baudrate=1000000,
|
| 34 |
+
polarity=0,
|
| 35 |
+
phase=0,
|
| 36 |
+
bits=8,
|
| 37 |
+
firstbit=SPI.MSB,
|
| 38 |
+
verbose=True,
|
| 39 |
+
):
|
| 40 |
+
try:
|
| 41 |
+
cs = Pin(cs_pin, Pin.OUT)
|
| 42 |
+
|
| 43 |
+
spi = SPI(
|
| 44 |
+
spi_id,
|
| 45 |
+
baudrate=baudrate,
|
| 46 |
+
polarity=polarity,
|
| 47 |
+
phase=phase,
|
| 48 |
+
bits=bits,
|
| 49 |
+
firstbit=firstbit,
|
| 50 |
+
sck=Pin(sck_pin),
|
| 51 |
+
mosi=Pin(mosi_pin),
|
| 52 |
+
miso=Pin(miso_pin),
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
# Initialize SD card
|
| 56 |
+
sd = sdcard.SDCard(spi, cs)
|
| 57 |
+
|
| 58 |
+
vfs = uos.VfsFat(sd)
|
| 59 |
+
uos.mount(vfs, "/sd") # type: ignore
|
| 60 |
+
if verbose:
|
| 61 |
+
print("SD Card initialized successfully")
|
| 62 |
+
return True
|
| 63 |
+
except Exception as e:
|
| 64 |
+
if verbose:
|
| 65 |
+
print(get_traceback(e))
|
| 66 |
+
print("SD Card failed to initialize")
|
| 67 |
+
return False
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def write_payload_backup(payload_data: str, fpath: str = "/sd/experiments.txt"):
|
| 71 |
+
payload = json.dumps(payload_data)
|
| 72 |
+
with open(fpath, "a") as file:
|
| 73 |
+
# line = ",".join([str(payload[key]) for key in payload.keys()])
|
| 74 |
+
file.write(f"{payload}\r\n")
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def log_to_mongodb(
|
| 78 |
+
document: dict,
|
| 79 |
+
api_key: str,
|
| 80 |
+
url: str,
|
| 81 |
+
cluster_name: str,
|
| 82 |
+
database_name: str,
|
| 83 |
+
collection_name: str,
|
| 84 |
+
verbose: bool = True,
|
| 85 |
+
retries: int = 2,
|
| 86 |
+
):
|
| 87 |
+
# based on https://medium.com/@johnlpage/introduction-to-microcontrollers-and-the-pi-pico-w-f7a2d9ad1394
|
| 88 |
+
headers = {"api-key": api_key}
|
| 89 |
+
|
| 90 |
+
insertPayload = {
|
| 91 |
+
"dataSource": cluster_name,
|
| 92 |
+
"database": database_name,
|
| 93 |
+
"collection": collection_name,
|
| 94 |
+
"document": document,
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
if verbose:
|
| 98 |
+
print(f"sending document to {cluster_name}:{database_name}:{collection_name}")
|
| 99 |
+
|
| 100 |
+
for _ in range(retries):
|
| 101 |
+
response = None
|
| 102 |
+
if _ > 0:
|
| 103 |
+
print(f"retrying... ({_} of {retries})")
|
| 104 |
+
|
| 105 |
+
try:
|
| 106 |
+
response = urequests.post(url, headers=headers, json=insertPayload)
|
| 107 |
+
txt = str(response.text)
|
| 108 |
+
status_code = response.status_code
|
| 109 |
+
|
| 110 |
+
if verbose:
|
| 111 |
+
print(f"Response: ({status_code}), msg = {txt}")
|
| 112 |
+
if response.status_code == 201:
|
| 113 |
+
print("Added Successfully")
|
| 114 |
+
break
|
| 115 |
+
else:
|
| 116 |
+
print("Error")
|
| 117 |
+
|
| 118 |
+
# Always close response objects so we don't leak memory
|
| 119 |
+
response.close()
|
| 120 |
+
except Exception as e:
|
| 121 |
+
if response is not None:
|
| 122 |
+
response.close()
|
| 123 |
+
if _ == retries - 1:
|
| 124 |
+
raise e
|
| 125 |
+
else:
|
| 126 |
+
print(e)
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def get_timestamp(timeout=2, return_str=False):
|
| 130 |
+
ntptime.timeout = timeout # type: ignore
|
| 131 |
+
time_int = ntptime.time()
|
| 132 |
+
utc_tuple = gmtime(time_int)
|
| 133 |
+
year, month, mday, hour, minute, second, weekday, yearday = utc_tuple
|
| 134 |
+
|
| 135 |
+
time_str = f"{year}-{month}-{mday} {hour:02}:{minute:02}:{second:02}"
|
| 136 |
+
|
| 137 |
+
if return_str:
|
| 138 |
+
return time_int, time_str
|
| 139 |
+
|
| 140 |
+
return time_int
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
def get_local_timestamp(return_str=False):
|
| 144 |
+
t = time()
|
| 145 |
+
year, month, mday, hour, minute, second, _, _ = localtime(t)
|
| 146 |
+
time_str = f"{year}-{month}-{mday} {hour:02}:{minute:02}:{second:02}"
|
| 147 |
+
|
| 148 |
+
if return_str:
|
| 149 |
+
return t, time_str
|
| 150 |
+
|
| 151 |
+
return t
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
def get_onboard_temperature(unit="K"):
|
| 155 |
+
sensor_temp = machine.ADC(4)
|
| 156 |
+
conversion_factor = 3.3 / (65535)
|
| 157 |
+
reading = sensor_temp.read_u16() * conversion_factor
|
| 158 |
+
celsius_degrees = 27 - (reading - 0.706) / 0.001721
|
| 159 |
+
if unit == "C":
|
| 160 |
+
return celsius_degrees
|
| 161 |
+
elif unit == "K":
|
| 162 |
+
return celsius_degrees + 273.15
|
| 163 |
+
elif unit == "F":
|
| 164 |
+
return celsius_degrees * 9 / 5 + 32
|
| 165 |
+
else:
|
| 166 |
+
raise ValueError("Invalid unit. Must be one of 'C', 'K', or 'F")
|
scripts/lib/functools.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
def partial(func, *args, **kwargs):
|
| 2 |
+
def _partial(*more_args, **more_kwargs):
|
| 3 |
+
kw = kwargs.copy()
|
| 4 |
+
kw.update(more_kwargs)
|
| 5 |
+
func(*(args + more_args), **kw)
|
| 6 |
+
|
| 7 |
+
return _partial
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def update_wrapper(wrapper, wrapped):
|
| 11 |
+
# Dummy impl
|
| 12 |
+
return wrapper
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def wraps(wrapped):
|
| 16 |
+
# Dummy impl
|
| 17 |
+
return lambda x: x
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def reduce(function, iterable, initializer=None):
|
| 21 |
+
it = iter(iterable)
|
| 22 |
+
if initializer is None:
|
| 23 |
+
value = next(it)
|
| 24 |
+
else:
|
| 25 |
+
value = initializer
|
| 26 |
+
for element in it:
|
| 27 |
+
value = function(value, element)
|
| 28 |
+
return value
|
scripts/lib/mqtt_as.py
ADDED
|
@@ -0,0 +1,824 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# mqtt_as.py Asynchronous version of umqtt.robust
|
| 2 |
+
# (C) Copyright Peter Hinch 2017-2023.
|
| 3 |
+
# Released under the MIT licence.
|
| 4 |
+
|
| 5 |
+
# Pyboard D support added also RP2/default
|
| 6 |
+
# Various improvements contributed by Kevin Köck.
|
| 7 |
+
|
| 8 |
+
import gc
|
| 9 |
+
|
| 10 |
+
import usocket as socket
|
| 11 |
+
import ustruct as struct
|
| 12 |
+
|
| 13 |
+
gc.collect()
|
| 14 |
+
import uasyncio as asyncio
|
| 15 |
+
from ubinascii import hexlify
|
| 16 |
+
|
| 17 |
+
gc.collect()
|
| 18 |
+
from uerrno import EINPROGRESS, ETIMEDOUT
|
| 19 |
+
from utime import ticks_diff, ticks_ms
|
| 20 |
+
|
| 21 |
+
gc.collect()
|
| 22 |
+
import network
|
| 23 |
+
from machine import unique_id
|
| 24 |
+
from micropython import const
|
| 25 |
+
|
| 26 |
+
gc.collect()
|
| 27 |
+
from sys import platform
|
| 28 |
+
|
| 29 |
+
VERSION = (0, 7, 1)
|
| 30 |
+
|
| 31 |
+
# Default short delay for good SynCom throughput (avoid sleep(0) with SynCom).
|
| 32 |
+
_DEFAULT_MS = const(20)
|
| 33 |
+
_SOCKET_POLL_DELAY = const(5) # 100ms added greatly to publish latency
|
| 34 |
+
|
| 35 |
+
# Legitimate errors while waiting on a socket. See uasyncio __init__.py open_connection().
|
| 36 |
+
ESP32 = platform == "esp32"
|
| 37 |
+
RP2 = platform == "rp2"
|
| 38 |
+
if ESP32:
|
| 39 |
+
# https://forum.micropython.org/viewtopic.php?f=16&t=3608&p=20942#p20942
|
| 40 |
+
BUSY_ERRORS = [EINPROGRESS, ETIMEDOUT, 118, 119] # Add in weird ESP32 errors
|
| 41 |
+
elif RP2:
|
| 42 |
+
BUSY_ERRORS = [EINPROGRESS, ETIMEDOUT, -110]
|
| 43 |
+
else:
|
| 44 |
+
BUSY_ERRORS = [EINPROGRESS, ETIMEDOUT]
|
| 45 |
+
|
| 46 |
+
ESP8266 = platform == "esp8266"
|
| 47 |
+
PYBOARD = platform == "pyboard"
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
# Default "do little" coro for optional user replacement
|
| 51 |
+
async def eliza(*_): # e.g. via set_wifi_handler(coro): see test program
|
| 52 |
+
await asyncio.sleep_ms(_DEFAULT_MS)
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
class MsgQueue:
|
| 56 |
+
def __init__(self, size):
|
| 57 |
+
self._q = [0 for _ in range(max(size, 4))]
|
| 58 |
+
self._size = size
|
| 59 |
+
self._wi = 0
|
| 60 |
+
self._ri = 0
|
| 61 |
+
self._evt = asyncio.Event()
|
| 62 |
+
self.discards = 0
|
| 63 |
+
|
| 64 |
+
def put(self, *v):
|
| 65 |
+
self._q[self._wi] = v
|
| 66 |
+
self._evt.set()
|
| 67 |
+
self._wi = (self._wi + 1) % self._size
|
| 68 |
+
if self._wi == self._ri: # Would indicate empty
|
| 69 |
+
self._ri = (self._ri + 1) % self._size # Discard a message
|
| 70 |
+
self.discards += 1
|
| 71 |
+
|
| 72 |
+
def __aiter__(self):
|
| 73 |
+
return self
|
| 74 |
+
|
| 75 |
+
async def __anext__(self):
|
| 76 |
+
if self._ri == self._wi: # Empty
|
| 77 |
+
self._evt.clear()
|
| 78 |
+
await self._evt.wait()
|
| 79 |
+
r = self._q[self._ri]
|
| 80 |
+
self._ri = (self._ri + 1) % self._size
|
| 81 |
+
return r
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
config = {
|
| 85 |
+
"client_id": hexlify(unique_id()),
|
| 86 |
+
"server": None,
|
| 87 |
+
"port": 0,
|
| 88 |
+
"user": "",
|
| 89 |
+
"password": "",
|
| 90 |
+
"keepalive": 60,
|
| 91 |
+
"ping_interval": 0,
|
| 92 |
+
"ssl": False,
|
| 93 |
+
"ssl_params": {},
|
| 94 |
+
"response_time": 10,
|
| 95 |
+
"clean_init": True,
|
| 96 |
+
"clean": True,
|
| 97 |
+
"max_repubs": 4,
|
| 98 |
+
"will": None,
|
| 99 |
+
"subs_cb": lambda *_: None,
|
| 100 |
+
"wifi_coro": eliza,
|
| 101 |
+
"connect_coro": eliza,
|
| 102 |
+
"ssid": None,
|
| 103 |
+
"wifi_pw": None,
|
| 104 |
+
"queue_len": 0,
|
| 105 |
+
"gateway": False,
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
class MQTTException(Exception):
|
| 110 |
+
pass
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def pid_gen():
|
| 114 |
+
pid = 0
|
| 115 |
+
while True:
|
| 116 |
+
pid = pid + 1 if pid < 65535 else 1
|
| 117 |
+
yield pid
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def qos_check(qos):
|
| 121 |
+
if not (qos == 0 or qos == 1):
|
| 122 |
+
raise ValueError("Only qos 0 and 1 are supported.")
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
# MQTT_base class. Handles MQTT protocol on the basis of a good connection.
|
| 126 |
+
# Exceptions from connectivity failures are handled by MQTTClient subclass.
|
| 127 |
+
class MQTT_base:
|
| 128 |
+
REPUB_COUNT = 0 # TEST
|
| 129 |
+
DEBUG = False
|
| 130 |
+
|
| 131 |
+
def __init__(self, config):
|
| 132 |
+
self._events = config["queue_len"] > 0
|
| 133 |
+
# MQTT config
|
| 134 |
+
self._client_id = config["client_id"]
|
| 135 |
+
self._user = config["user"]
|
| 136 |
+
self._pswd = config["password"]
|
| 137 |
+
self._keepalive = config["keepalive"]
|
| 138 |
+
if self._keepalive >= 65536:
|
| 139 |
+
raise ValueError("invalid keepalive time")
|
| 140 |
+
self._response_time = (
|
| 141 |
+
config["response_time"] * 1000
|
| 142 |
+
) # Repub if no PUBACK received (ms).
|
| 143 |
+
self._max_repubs = config["max_repubs"]
|
| 144 |
+
self._clean_init = config[
|
| 145 |
+
"clean_init"
|
| 146 |
+
] # clean_session state on first connection
|
| 147 |
+
self._clean = config["clean"] # clean_session state on reconnect
|
| 148 |
+
will = config["will"]
|
| 149 |
+
if will is None:
|
| 150 |
+
self._lw_topic = False
|
| 151 |
+
else:
|
| 152 |
+
self._set_last_will(*will)
|
| 153 |
+
# WiFi config
|
| 154 |
+
self._ssid = config["ssid"] # Required for ESP32 / Pyboard D. Optional ESP8266
|
| 155 |
+
self._wifi_pw = config["wifi_pw"]
|
| 156 |
+
self._ssl = config["ssl"]
|
| 157 |
+
self._ssl_params = config["ssl_params"]
|
| 158 |
+
# Callbacks and coros
|
| 159 |
+
if self._events:
|
| 160 |
+
self.up = asyncio.Event()
|
| 161 |
+
self.down = asyncio.Event()
|
| 162 |
+
self.queue = MsgQueue(config["queue_len"])
|
| 163 |
+
else: # Callbacks
|
| 164 |
+
self._cb = config["subs_cb"]
|
| 165 |
+
self._wifi_handler = config["wifi_coro"]
|
| 166 |
+
self._connect_handler = config["connect_coro"]
|
| 167 |
+
# Network
|
| 168 |
+
self.port = config["port"]
|
| 169 |
+
if self.port == 0:
|
| 170 |
+
self.port = 8883 if self._ssl else 1883
|
| 171 |
+
self.server = config["server"]
|
| 172 |
+
if self.server is None:
|
| 173 |
+
raise ValueError("no server specified.")
|
| 174 |
+
self._sock = None
|
| 175 |
+
self._sta_if = network.WLAN(network.STA_IF)
|
| 176 |
+
self._sta_if.active(True)
|
| 177 |
+
if config["gateway"]: # Called from gateway (hence ESP32).
|
| 178 |
+
import aioespnow # Set up ESPNOW
|
| 179 |
+
|
| 180 |
+
while not (sta := self._sta_if).active():
|
| 181 |
+
time.sleep(0.1)
|
| 182 |
+
sta.config(pm=sta.PM_NONE) # No power management
|
| 183 |
+
sta.active(True)
|
| 184 |
+
self._espnow = (
|
| 185 |
+
aioespnow.AIOESPNow()
|
| 186 |
+
) # Returns AIOESPNow enhanced with async support
|
| 187 |
+
self._espnow.active(True)
|
| 188 |
+
|
| 189 |
+
self.newpid = pid_gen()
|
| 190 |
+
self.rcv_pids = set() # PUBACK and SUBACK pids awaiting ACK response
|
| 191 |
+
self.last_rx = ticks_ms() # Time of last communication from broker
|
| 192 |
+
self.lock = asyncio.Lock()
|
| 193 |
+
|
| 194 |
+
def _set_last_will(self, topic, msg, retain=False, qos=0):
|
| 195 |
+
qos_check(qos)
|
| 196 |
+
if not topic:
|
| 197 |
+
raise ValueError("Empty topic.")
|
| 198 |
+
self._lw_topic = topic
|
| 199 |
+
self._lw_msg = msg
|
| 200 |
+
self._lw_qos = qos
|
| 201 |
+
self._lw_retain = retain
|
| 202 |
+
|
| 203 |
+
def dprint(self, msg, *args):
|
| 204 |
+
if self.DEBUG:
|
| 205 |
+
print(msg % args)
|
| 206 |
+
|
| 207 |
+
def _timeout(self, t):
|
| 208 |
+
return ticks_diff(ticks_ms(), t) > self._response_time
|
| 209 |
+
|
| 210 |
+
async def _as_read(self, n, sock=None): # OSError caught by superclass
|
| 211 |
+
if sock is None:
|
| 212 |
+
sock = self._sock
|
| 213 |
+
# Declare a byte array of size n. That space is needed anyway, better
|
| 214 |
+
# to just 'allocate' it in one go instead of appending to an
|
| 215 |
+
# existing object, this prevents reallocation and fragmentation.
|
| 216 |
+
data = bytearray(n)
|
| 217 |
+
buffer = memoryview(data)
|
| 218 |
+
size = 0
|
| 219 |
+
t = ticks_ms()
|
| 220 |
+
while size < n:
|
| 221 |
+
if self._timeout(t) or not self.isconnected():
|
| 222 |
+
raise OSError(-1, "Timeout on socket read")
|
| 223 |
+
try:
|
| 224 |
+
msg_size = sock.readinto(buffer[size:], n - size)
|
| 225 |
+
except OSError as e: # ESP32 issues weird 119 errors here
|
| 226 |
+
msg_size = None
|
| 227 |
+
if e.args[0] not in BUSY_ERRORS:
|
| 228 |
+
raise
|
| 229 |
+
if msg_size == 0: # Connection closed by host
|
| 230 |
+
raise OSError(-1, "Connection closed by host")
|
| 231 |
+
if msg_size is not None: # data received
|
| 232 |
+
size += msg_size
|
| 233 |
+
t = ticks_ms()
|
| 234 |
+
self.last_rx = ticks_ms()
|
| 235 |
+
await asyncio.sleep_ms(_SOCKET_POLL_DELAY)
|
| 236 |
+
return data
|
| 237 |
+
|
| 238 |
+
async def _as_write(self, bytes_wr, length=0, sock=None):
|
| 239 |
+
if sock is None:
|
| 240 |
+
sock = self._sock
|
| 241 |
+
|
| 242 |
+
# Wrap bytes in memoryview to avoid copying during slicing
|
| 243 |
+
bytes_wr = memoryview(bytes_wr)
|
| 244 |
+
if length:
|
| 245 |
+
bytes_wr = bytes_wr[:length]
|
| 246 |
+
t = ticks_ms()
|
| 247 |
+
while bytes_wr:
|
| 248 |
+
if self._timeout(t) or not self.isconnected():
|
| 249 |
+
raise OSError(-1, "Timeout on socket write")
|
| 250 |
+
try:
|
| 251 |
+
n = sock.write(bytes_wr)
|
| 252 |
+
except OSError as e: # ESP32 issues weird 119 errors here
|
| 253 |
+
n = 0
|
| 254 |
+
if e.args[0] not in BUSY_ERRORS:
|
| 255 |
+
raise
|
| 256 |
+
if n:
|
| 257 |
+
t = ticks_ms()
|
| 258 |
+
bytes_wr = bytes_wr[n:]
|
| 259 |
+
await asyncio.sleep_ms(_SOCKET_POLL_DELAY)
|
| 260 |
+
|
| 261 |
+
async def _send_str(self, s):
|
| 262 |
+
await self._as_write(struct.pack("!H", len(s)))
|
| 263 |
+
await self._as_write(s)
|
| 264 |
+
|
| 265 |
+
async def _recv_len(self):
|
| 266 |
+
n = 0
|
| 267 |
+
sh = 0
|
| 268 |
+
while 1:
|
| 269 |
+
res = await self._as_read(1)
|
| 270 |
+
b = res[0]
|
| 271 |
+
n |= (b & 0x7F) << sh
|
| 272 |
+
if not b & 0x80:
|
| 273 |
+
return n
|
| 274 |
+
sh += 7
|
| 275 |
+
|
| 276 |
+
async def _connect(self, clean):
|
| 277 |
+
self._sock = socket.socket()
|
| 278 |
+
self._sock.setblocking(False)
|
| 279 |
+
try:
|
| 280 |
+
self._sock.connect(self._addr)
|
| 281 |
+
except OSError as e:
|
| 282 |
+
if e.args[0] not in BUSY_ERRORS:
|
| 283 |
+
raise
|
| 284 |
+
await asyncio.sleep_ms(_DEFAULT_MS)
|
| 285 |
+
self.dprint("Connecting to broker.")
|
| 286 |
+
if self._ssl:
|
| 287 |
+
import ssl
|
| 288 |
+
|
| 289 |
+
self._sock = ssl.wrap_socket(self._sock, **self._ssl_params)
|
| 290 |
+
premsg = bytearray(b"\x10\0\0\0\0\0")
|
| 291 |
+
msg = bytearray(b"\x04MQTT\x04\0\0\0") # Protocol 3.1.1
|
| 292 |
+
|
| 293 |
+
sz = 10 + 2 + len(self._client_id)
|
| 294 |
+
msg[6] = clean << 1
|
| 295 |
+
if self._user:
|
| 296 |
+
sz += 2 + len(self._user) + 2 + len(self._pswd)
|
| 297 |
+
msg[6] |= 0xC0
|
| 298 |
+
if self._keepalive:
|
| 299 |
+
msg[7] |= self._keepalive >> 8
|
| 300 |
+
msg[8] |= self._keepalive & 0x00FF
|
| 301 |
+
if self._lw_topic:
|
| 302 |
+
sz += 2 + len(self._lw_topic) + 2 + len(self._lw_msg)
|
| 303 |
+
msg[6] |= 0x4 | (self._lw_qos & 0x1) << 3 | (self._lw_qos & 0x2) << 3
|
| 304 |
+
msg[6] |= self._lw_retain << 5
|
| 305 |
+
|
| 306 |
+
i = 1
|
| 307 |
+
while sz > 0x7F:
|
| 308 |
+
premsg[i] = (sz & 0x7F) | 0x80
|
| 309 |
+
sz >>= 7
|
| 310 |
+
i += 1
|
| 311 |
+
premsg[i] = sz
|
| 312 |
+
await self._as_write(premsg, i + 2)
|
| 313 |
+
await self._as_write(msg)
|
| 314 |
+
await self._send_str(self._client_id)
|
| 315 |
+
if self._lw_topic:
|
| 316 |
+
await self._send_str(self._lw_topic)
|
| 317 |
+
await self._send_str(self._lw_msg)
|
| 318 |
+
if self._user:
|
| 319 |
+
await self._send_str(self._user)
|
| 320 |
+
await self._send_str(self._pswd)
|
| 321 |
+
# Await CONNACK
|
| 322 |
+
# read causes ECONNABORTED if broker is out; triggers a reconnect.
|
| 323 |
+
resp = await self._as_read(4)
|
| 324 |
+
self.dprint("Connected to broker.") # Got CONNACK
|
| 325 |
+
if (
|
| 326 |
+
resp[3] != 0 or resp[0] != 0x20 or resp[1] != 0x02
|
| 327 |
+
): # Bad CONNACK e.g. authentication fail.
|
| 328 |
+
raise OSError(
|
| 329 |
+
-1,
|
| 330 |
+
f"Connect fail: 0x{(resp[0] << 8) + resp[1]:04x} {resp[3]} (README 7)",
|
| 331 |
+
)
|
| 332 |
+
|
| 333 |
+
async def _ping(self):
|
| 334 |
+
async with self.lock:
|
| 335 |
+
await self._as_write(b"\xc0\0")
|
| 336 |
+
|
| 337 |
+
# Check internet connectivity by sending DNS lookup to Google's 8.8.8.8
|
| 338 |
+
async def wan_ok(
|
| 339 |
+
self,
|
| 340 |
+
packet=b"$\x1a\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x03www\x06google\x03com\x00\x00\x01\x00\x01",
|
| 341 |
+
):
|
| 342 |
+
if not self.isconnected(): # WiFi is down
|
| 343 |
+
return False
|
| 344 |
+
length = 32 # DNS query and response packet size
|
| 345 |
+
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
| 346 |
+
s.setblocking(False)
|
| 347 |
+
s.connect(("8.8.8.8", 53))
|
| 348 |
+
await asyncio.sleep(1)
|
| 349 |
+
try:
|
| 350 |
+
await self._as_write(packet, sock=s)
|
| 351 |
+
await asyncio.sleep(2)
|
| 352 |
+
res = await self._as_read(length, s)
|
| 353 |
+
if len(res) == length:
|
| 354 |
+
return True # DNS response size OK
|
| 355 |
+
except OSError: # Timeout on read: no connectivity.
|
| 356 |
+
return False
|
| 357 |
+
finally:
|
| 358 |
+
s.close()
|
| 359 |
+
return False
|
| 360 |
+
|
| 361 |
+
async def broker_up(self): # Test broker connectivity
|
| 362 |
+
if not self.isconnected():
|
| 363 |
+
return False
|
| 364 |
+
tlast = self.last_rx
|
| 365 |
+
if ticks_diff(ticks_ms(), tlast) < 1000:
|
| 366 |
+
return True
|
| 367 |
+
try:
|
| 368 |
+
await self._ping()
|
| 369 |
+
except OSError:
|
| 370 |
+
return False
|
| 371 |
+
t = ticks_ms()
|
| 372 |
+
while not self._timeout(t):
|
| 373 |
+
await asyncio.sleep_ms(100)
|
| 374 |
+
if ticks_diff(self.last_rx, tlast) > 0: # Response received
|
| 375 |
+
return True
|
| 376 |
+
return False
|
| 377 |
+
|
| 378 |
+
async def disconnect(self):
|
| 379 |
+
if self._sock is not None:
|
| 380 |
+
await self._kill_tasks(False) # Keep socket open
|
| 381 |
+
try:
|
| 382 |
+
async with self.lock:
|
| 383 |
+
self._sock.write(b"\xe0\0") # Close broker connection
|
| 384 |
+
await asyncio.sleep_ms(100)
|
| 385 |
+
except OSError:
|
| 386 |
+
pass
|
| 387 |
+
self._close()
|
| 388 |
+
self._has_connected = False
|
| 389 |
+
|
| 390 |
+
def _close(self):
|
| 391 |
+
if self._sock is not None:
|
| 392 |
+
self._sock.close()
|
| 393 |
+
|
| 394 |
+
def close(
|
| 395 |
+
self,
|
| 396 |
+
): # API. See https://github.com/peterhinch/micropython-mqtt/issues/60
|
| 397 |
+
self._close()
|
| 398 |
+
try:
|
| 399 |
+
self._sta_if.disconnect() # Disconnect Wi-Fi to avoid errors
|
| 400 |
+
except OSError:
|
| 401 |
+
self.dprint("Wi-Fi not started, unable to disconnect interface")
|
| 402 |
+
self._sta_if.active(False)
|
| 403 |
+
|
| 404 |
+
async def _await_pid(self, pid):
|
| 405 |
+
t = ticks_ms()
|
| 406 |
+
while pid in self.rcv_pids: # local copy
|
| 407 |
+
if self._timeout(t) or not self.isconnected():
|
| 408 |
+
break # Must repub or bail out
|
| 409 |
+
await asyncio.sleep_ms(100)
|
| 410 |
+
else:
|
| 411 |
+
return True # PID received. All done.
|
| 412 |
+
return False
|
| 413 |
+
|
| 414 |
+
# qos == 1: coro blocks until wait_msg gets correct PID.
|
| 415 |
+
# If WiFi fails completely subclass re-publishes with new PID.
|
| 416 |
+
async def publish(self, topic, msg, retain, qos):
|
| 417 |
+
pid = next(self.newpid)
|
| 418 |
+
if qos:
|
| 419 |
+
self.rcv_pids.add(pid)
|
| 420 |
+
async with self.lock:
|
| 421 |
+
await self._publish(topic, msg, retain, qos, 0, pid)
|
| 422 |
+
if qos == 0:
|
| 423 |
+
return
|
| 424 |
+
|
| 425 |
+
count = 0
|
| 426 |
+
while 1: # Await PUBACK, republish on timeout
|
| 427 |
+
if await self._await_pid(pid):
|
| 428 |
+
return
|
| 429 |
+
# No match
|
| 430 |
+
if count >= self._max_repubs or not self.isconnected():
|
| 431 |
+
raise OSError(-1) # Subclass to re-publish with new PID
|
| 432 |
+
async with self.lock:
|
| 433 |
+
await self._publish(topic, msg, retain, qos, dup=1, pid=pid) # Add pid
|
| 434 |
+
count += 1
|
| 435 |
+
self.REPUB_COUNT += 1
|
| 436 |
+
|
| 437 |
+
async def _publish(self, topic, msg, retain, qos, dup, pid):
|
| 438 |
+
pkt = bytearray(b"\x30\0\0\0")
|
| 439 |
+
pkt[0] |= qos << 1 | retain | dup << 3
|
| 440 |
+
sz = 2 + len(topic) + len(msg)
|
| 441 |
+
if qos > 0:
|
| 442 |
+
sz += 2
|
| 443 |
+
if sz >= 2097152:
|
| 444 |
+
raise MQTTException("Strings too long.")
|
| 445 |
+
i = 1
|
| 446 |
+
while sz > 0x7F:
|
| 447 |
+
pkt[i] = (sz & 0x7F) | 0x80
|
| 448 |
+
sz >>= 7
|
| 449 |
+
i += 1
|
| 450 |
+
pkt[i] = sz
|
| 451 |
+
await self._as_write(pkt, i + 1)
|
| 452 |
+
await self._send_str(topic)
|
| 453 |
+
if qos > 0:
|
| 454 |
+
struct.pack_into("!H", pkt, 0, pid)
|
| 455 |
+
await self._as_write(pkt, 2)
|
| 456 |
+
await self._as_write(msg)
|
| 457 |
+
|
| 458 |
+
# Can raise OSError if WiFi fails. Subclass traps.
|
| 459 |
+
async def subscribe(self, topic, qos):
|
| 460 |
+
pkt = bytearray(b"\x82\0\0\0")
|
| 461 |
+
pid = next(self.newpid)
|
| 462 |
+
self.rcv_pids.add(pid)
|
| 463 |
+
struct.pack_into("!BH", pkt, 1, 2 + 2 + len(topic) + 1, pid)
|
| 464 |
+
async with self.lock:
|
| 465 |
+
await self._as_write(pkt)
|
| 466 |
+
await self._send_str(topic)
|
| 467 |
+
await self._as_write(qos.to_bytes(1, "little"))
|
| 468 |
+
|
| 469 |
+
if not await self._await_pid(pid):
|
| 470 |
+
raise OSError(-1)
|
| 471 |
+
|
| 472 |
+
# Can raise OSError if WiFi fails. Subclass traps.
|
| 473 |
+
async def unsubscribe(self, topic):
|
| 474 |
+
pkt = bytearray(b"\xa2\0\0\0")
|
| 475 |
+
pid = next(self.newpid)
|
| 476 |
+
self.rcv_pids.add(pid)
|
| 477 |
+
struct.pack_into("!BH", pkt, 1, 2 + 2 + len(topic), pid)
|
| 478 |
+
async with self.lock:
|
| 479 |
+
await self._as_write(pkt)
|
| 480 |
+
await self._send_str(topic)
|
| 481 |
+
|
| 482 |
+
if not await self._await_pid(pid):
|
| 483 |
+
raise OSError(-1)
|
| 484 |
+
|
| 485 |
+
# Wait for a single incoming MQTT message and process it.
|
| 486 |
+
# Subscribed messages are delivered to a callback previously
|
| 487 |
+
# set by .setup() method. Other (internal) MQTT
|
| 488 |
+
# messages processed internally.
|
| 489 |
+
# Immediate return if no data available. Called from ._handle_msg().
|
| 490 |
+
async def wait_msg(self):
|
| 491 |
+
try:
|
| 492 |
+
res = self._sock.read(1) # Throws OSError on WiFi fail
|
| 493 |
+
except OSError as e:
|
| 494 |
+
if e.args[0] in BUSY_ERRORS: # Needed by RP2
|
| 495 |
+
await asyncio.sleep_ms(0)
|
| 496 |
+
return
|
| 497 |
+
raise
|
| 498 |
+
if res is None:
|
| 499 |
+
return
|
| 500 |
+
if res == b"":
|
| 501 |
+
raise OSError(-1, "Empty response")
|
| 502 |
+
|
| 503 |
+
if res == b"\xd0": # PINGRESP
|
| 504 |
+
await self._as_read(1) # Update .last_rx time
|
| 505 |
+
return
|
| 506 |
+
op = res[0]
|
| 507 |
+
|
| 508 |
+
if op == 0x40: # PUBACK: save pid
|
| 509 |
+
sz = await self._as_read(1)
|
| 510 |
+
if sz != b"\x02":
|
| 511 |
+
raise OSError(-1, "Invalid PUBACK packet")
|
| 512 |
+
rcv_pid = await self._as_read(2)
|
| 513 |
+
pid = rcv_pid[0] << 8 | rcv_pid[1]
|
| 514 |
+
if pid in self.rcv_pids:
|
| 515 |
+
self.rcv_pids.discard(pid)
|
| 516 |
+
else:
|
| 517 |
+
raise OSError(-1, "Invalid pid in PUBACK packet")
|
| 518 |
+
|
| 519 |
+
if op == 0x90: # SUBACK
|
| 520 |
+
resp = await self._as_read(4)
|
| 521 |
+
if resp[3] == 0x80:
|
| 522 |
+
raise OSError(-1, "Invalid SUBACK packet")
|
| 523 |
+
pid = resp[2] | (resp[1] << 8)
|
| 524 |
+
if pid in self.rcv_pids:
|
| 525 |
+
self.rcv_pids.discard(pid)
|
| 526 |
+
else:
|
| 527 |
+
raise OSError(-1, "Invalid pid in SUBACK packet")
|
| 528 |
+
|
| 529 |
+
if op == 0xB0: # UNSUBACK
|
| 530 |
+
resp = await self._as_read(3)
|
| 531 |
+
pid = resp[2] | (resp[1] << 8)
|
| 532 |
+
if pid in self.rcv_pids:
|
| 533 |
+
self.rcv_pids.discard(pid)
|
| 534 |
+
else:
|
| 535 |
+
raise OSError(-1)
|
| 536 |
+
|
| 537 |
+
if op & 0xF0 != 0x30:
|
| 538 |
+
return
|
| 539 |
+
sz = await self._recv_len()
|
| 540 |
+
topic_len = await self._as_read(2)
|
| 541 |
+
topic_len = (topic_len[0] << 8) | topic_len[1]
|
| 542 |
+
topic = await self._as_read(topic_len)
|
| 543 |
+
sz -= topic_len + 2
|
| 544 |
+
if op & 6:
|
| 545 |
+
pid = await self._as_read(2)
|
| 546 |
+
pid = pid[0] << 8 | pid[1]
|
| 547 |
+
sz -= 2
|
| 548 |
+
msg = await self._as_read(sz)
|
| 549 |
+
retained = op & 0x01
|
| 550 |
+
if self._events:
|
| 551 |
+
self.queue.put(topic, msg, bool(retained))
|
| 552 |
+
else:
|
| 553 |
+
self._cb(topic, msg, bool(retained))
|
| 554 |
+
if op & 6 == 2: # qos 1
|
| 555 |
+
pkt = bytearray(b"\x40\x02\0\0") # Send PUBACK
|
| 556 |
+
struct.pack_into("!H", pkt, 2, pid)
|
| 557 |
+
await self._as_write(pkt)
|
| 558 |
+
elif op & 6 == 4: # qos 2 not supported
|
| 559 |
+
raise OSError(-1, "QoS 2 not supported")
|
| 560 |
+
|
| 561 |
+
|
| 562 |
+
# MQTTClient class. Handles issues relating to connectivity.
|
| 563 |
+
|
| 564 |
+
|
| 565 |
+
class MQTTClient(MQTT_base):
|
| 566 |
+
def __init__(self, config):
|
| 567 |
+
super().__init__(config)
|
| 568 |
+
self._isconnected = False # Current connection state
|
| 569 |
+
keepalive = 1000 * self._keepalive # ms
|
| 570 |
+
self._ping_interval = keepalive // 4 if keepalive else 20000
|
| 571 |
+
p_i = (
|
| 572 |
+
config["ping_interval"] * 1000
|
| 573 |
+
) # Can specify shorter e.g. for subscribe-only
|
| 574 |
+
if p_i and p_i < self._ping_interval:
|
| 575 |
+
self._ping_interval = p_i
|
| 576 |
+
self._in_connect = False
|
| 577 |
+
self._has_connected = False # Define 'Clean Session' value to use.
|
| 578 |
+
self._tasks = []
|
| 579 |
+
if ESP8266:
|
| 580 |
+
import esp
|
| 581 |
+
|
| 582 |
+
esp.sleep_type(
|
| 583 |
+
0
|
| 584 |
+
) # Improve connection integrity at cost of power consumption.
|
| 585 |
+
|
| 586 |
+
async def wifi_connect(self, quick=False):
|
| 587 |
+
s = self._sta_if
|
| 588 |
+
if ESP8266:
|
| 589 |
+
if s.isconnected(): # 1st attempt, already connected.
|
| 590 |
+
return
|
| 591 |
+
s.active(True)
|
| 592 |
+
s.connect() # ESP8266 remembers connection.
|
| 593 |
+
for _ in range(60):
|
| 594 |
+
if (
|
| 595 |
+
s.status() != network.STAT_CONNECTING
|
| 596 |
+
): # Break out on fail or success. Check once per sec.
|
| 597 |
+
break
|
| 598 |
+
await asyncio.sleep(1)
|
| 599 |
+
if (
|
| 600 |
+
s.status() == network.STAT_CONNECTING
|
| 601 |
+
): # might hang forever awaiting dhcp lease renewal or something else
|
| 602 |
+
s.disconnect()
|
| 603 |
+
await asyncio.sleep(1)
|
| 604 |
+
if (
|
| 605 |
+
not s.isconnected()
|
| 606 |
+
and self._ssid is not None
|
| 607 |
+
and self._wifi_pw is not None
|
| 608 |
+
):
|
| 609 |
+
s.connect(self._ssid, self._wifi_pw)
|
| 610 |
+
while (
|
| 611 |
+
s.status() == network.STAT_CONNECTING
|
| 612 |
+
): # Break out on fail or success. Check once per sec.
|
| 613 |
+
await asyncio.sleep(1)
|
| 614 |
+
else:
|
| 615 |
+
s.active(True)
|
| 616 |
+
if RP2: # Disable auto-sleep.
|
| 617 |
+
# https://datasheets.raspberrypi.com/picow/connecting-to-the-internet-with-pico-w.pdf
|
| 618 |
+
# para 3.6.3
|
| 619 |
+
s.config(pm=0xA11140)
|
| 620 |
+
s.connect(self._ssid, self._wifi_pw)
|
| 621 |
+
for _ in range(60): # Break out on fail or success. Check once per sec.
|
| 622 |
+
await asyncio.sleep(1)
|
| 623 |
+
# Loop while connecting or no IP
|
| 624 |
+
if s.isconnected():
|
| 625 |
+
break
|
| 626 |
+
if ESP32:
|
| 627 |
+
if s.status() != network.STAT_CONNECTING: # 1001
|
| 628 |
+
break
|
| 629 |
+
elif PYBOARD: # No symbolic constants in network
|
| 630 |
+
if not 1 <= s.status() <= 2:
|
| 631 |
+
break
|
| 632 |
+
elif RP2: # 1 is STAT_CONNECTING. 2 reported by user (No IP?)
|
| 633 |
+
if not 1 <= s.status() <= 2:
|
| 634 |
+
break
|
| 635 |
+
else: # Timeout: still in connecting state
|
| 636 |
+
s.disconnect()
|
| 637 |
+
await asyncio.sleep(1)
|
| 638 |
+
|
| 639 |
+
if not s.isconnected(): # Timed out
|
| 640 |
+
raise OSError("Wi-Fi connect timed out")
|
| 641 |
+
if not quick: # Skip on first connection only if power saving
|
| 642 |
+
# Ensure connection stays up for a few secs.
|
| 643 |
+
self.dprint("Checking WiFi integrity.")
|
| 644 |
+
for _ in range(5):
|
| 645 |
+
if not s.isconnected():
|
| 646 |
+
raise OSError("Connection Unstable") # in 1st 5 secs
|
| 647 |
+
await asyncio.sleep(1)
|
| 648 |
+
self.dprint("Got reliable connection")
|
| 649 |
+
|
| 650 |
+
async def connect(
|
| 651 |
+
self, *, quick=False
|
| 652 |
+
): # Quick initial connect option for battery apps
|
| 653 |
+
if not self._has_connected:
|
| 654 |
+
await self.wifi_connect(quick) # On 1st call, caller handles error
|
| 655 |
+
# Note this blocks if DNS lookup occurs. Do it once to prevent
|
| 656 |
+
# blocking during later internet outage:
|
| 657 |
+
self._addr = socket.getaddrinfo(self.server, self.port)[0][-1]
|
| 658 |
+
self._in_connect = True # Disable low level ._isconnected check
|
| 659 |
+
try:
|
| 660 |
+
if not self._has_connected and self._clean_init and not self._clean:
|
| 661 |
+
# Power up. Clear previous session data but subsequently save it.
|
| 662 |
+
# Issue #40
|
| 663 |
+
await self._connect(True) # Connect with clean session
|
| 664 |
+
try:
|
| 665 |
+
async with self.lock:
|
| 666 |
+
self._sock.write(
|
| 667 |
+
b"\xe0\0"
|
| 668 |
+
) # Force disconnect but keep socket open
|
| 669 |
+
except OSError:
|
| 670 |
+
pass
|
| 671 |
+
self.dprint("Waiting for disconnect")
|
| 672 |
+
await asyncio.sleep(2) # Wait for broker to disconnect
|
| 673 |
+
self.dprint("About to reconnect with unclean session.")
|
| 674 |
+
await self._connect(self._clean)
|
| 675 |
+
except Exception:
|
| 676 |
+
self._close()
|
| 677 |
+
self._in_connect = False # Caller may run .isconnected()
|
| 678 |
+
raise
|
| 679 |
+
self.rcv_pids.clear()
|
| 680 |
+
# If we get here without error broker/LAN must be up.
|
| 681 |
+
self._isconnected = True
|
| 682 |
+
self._in_connect = False # Low level code can now check connectivity.
|
| 683 |
+
if not self._events:
|
| 684 |
+
asyncio.create_task(self._wifi_handler(True)) # User handler.
|
| 685 |
+
if not self._has_connected:
|
| 686 |
+
self._has_connected = True # Use normal clean flag on reconnect.
|
| 687 |
+
asyncio.create_task(self._keep_connected())
|
| 688 |
+
# Runs forever unless user issues .disconnect()
|
| 689 |
+
|
| 690 |
+
asyncio.create_task(self._handle_msg()) # Task quits on connection fail.
|
| 691 |
+
self._tasks.append(asyncio.create_task(self._keep_alive()))
|
| 692 |
+
if self.DEBUG:
|
| 693 |
+
self._tasks.append(asyncio.create_task(self._memory()))
|
| 694 |
+
if self._events:
|
| 695 |
+
self.up.set() # Connectivity is up
|
| 696 |
+
else:
|
| 697 |
+
asyncio.create_task(self._connect_handler(self)) # User handler.
|
| 698 |
+
|
| 699 |
+
# Launched by .connect(). Runs until connectivity fails. Checks for and
|
| 700 |
+
# handles incoming messages.
|
| 701 |
+
async def _handle_msg(self):
|
| 702 |
+
try:
|
| 703 |
+
while self.isconnected():
|
| 704 |
+
async with self.lock:
|
| 705 |
+
await self.wait_msg() # Immediate return if no message
|
| 706 |
+
await asyncio.sleep_ms(_DEFAULT_MS) # Let other tasks get lock
|
| 707 |
+
|
| 708 |
+
except OSError:
|
| 709 |
+
pass
|
| 710 |
+
self._reconnect() # Broker or WiFi fail.
|
| 711 |
+
|
| 712 |
+
# Keep broker alive MQTT spec 3.1.2.10 Keep Alive.
|
| 713 |
+
# Runs until ping failure or no response in keepalive period.
|
| 714 |
+
async def _keep_alive(self):
|
| 715 |
+
while self.isconnected():
|
| 716 |
+
pings_due = ticks_diff(ticks_ms(), self.last_rx) // self._ping_interval
|
| 717 |
+
if pings_due >= 4:
|
| 718 |
+
self.dprint("Reconnect: broker fail.")
|
| 719 |
+
break
|
| 720 |
+
await asyncio.sleep_ms(self._ping_interval)
|
| 721 |
+
try:
|
| 722 |
+
await self._ping()
|
| 723 |
+
except OSError:
|
| 724 |
+
break
|
| 725 |
+
self._reconnect() # Broker or WiFi fail.
|
| 726 |
+
|
| 727 |
+
async def _kill_tasks(self, kill_skt): # Cancel running tasks
|
| 728 |
+
for task in self._tasks:
|
| 729 |
+
task.cancel()
|
| 730 |
+
self._tasks.clear()
|
| 731 |
+
await asyncio.sleep_ms(0) # Ensure cancellation complete
|
| 732 |
+
if kill_skt: # Close socket
|
| 733 |
+
self._close()
|
| 734 |
+
|
| 735 |
+
# DEBUG: show RAM messages.
|
| 736 |
+
async def _memory(self):
|
| 737 |
+
while True:
|
| 738 |
+
await asyncio.sleep(20)
|
| 739 |
+
gc.collect()
|
| 740 |
+
self.dprint("RAM free %d alloc %d", gc.mem_free(), gc.mem_alloc())
|
| 741 |
+
|
| 742 |
+
def isconnected(self):
|
| 743 |
+
if self._in_connect: # Disable low-level check during .connect()
|
| 744 |
+
return True
|
| 745 |
+
if self._isconnected and not self._sta_if.isconnected(): # It's going down.
|
| 746 |
+
self._reconnect()
|
| 747 |
+
return self._isconnected
|
| 748 |
+
|
| 749 |
+
def _reconnect(self): # Schedule a reconnection if not underway.
|
| 750 |
+
if self._isconnected:
|
| 751 |
+
self._isconnected = False
|
| 752 |
+
asyncio.create_task(self._kill_tasks(True)) # Shut down tasks and socket
|
| 753 |
+
if self._events: # Signal an outage
|
| 754 |
+
self.down.set()
|
| 755 |
+
else:
|
| 756 |
+
asyncio.create_task(self._wifi_handler(False)) # User handler.
|
| 757 |
+
|
| 758 |
+
# Await broker connection.
|
| 759 |
+
async def _connection(self):
|
| 760 |
+
while not self._isconnected:
|
| 761 |
+
await asyncio.sleep(1)
|
| 762 |
+
|
| 763 |
+
# Scheduled on 1st successful connection. Runs forever maintaining wifi and
|
| 764 |
+
# broker connection. Must handle conditions at edge of WiFi range.
|
| 765 |
+
async def _keep_connected(self):
|
| 766 |
+
while self._has_connected:
|
| 767 |
+
if self.isconnected(): # Pause for 1 second
|
| 768 |
+
await asyncio.sleep(1)
|
| 769 |
+
gc.collect()
|
| 770 |
+
else: # Link is down, socket is closed, tasks are killed
|
| 771 |
+
try:
|
| 772 |
+
self._sta_if.disconnect()
|
| 773 |
+
except OSError:
|
| 774 |
+
self.dprint("Wi-Fi not started, unable to disconnect interface")
|
| 775 |
+
await asyncio.sleep(1)
|
| 776 |
+
try:
|
| 777 |
+
await self.wifi_connect()
|
| 778 |
+
except OSError:
|
| 779 |
+
continue
|
| 780 |
+
if (
|
| 781 |
+
not self._has_connected
|
| 782 |
+
): # User has issued the terminal .disconnect()
|
| 783 |
+
self.dprint("Disconnected, exiting _keep_connected")
|
| 784 |
+
break
|
| 785 |
+
try:
|
| 786 |
+
await self.connect()
|
| 787 |
+
# Now has set ._isconnected and scheduled _connect_handler().
|
| 788 |
+
self.dprint("Reconnect OK!")
|
| 789 |
+
except OSError as e:
|
| 790 |
+
self.dprint("Error in reconnect. %s", e)
|
| 791 |
+
# Can get ECONNABORTED or -1. The latter signifies no or bad CONNACK received.
|
| 792 |
+
self._close() # Disconnect and try again.
|
| 793 |
+
self._in_connect = False
|
| 794 |
+
self._isconnected = False
|
| 795 |
+
self.dprint("Disconnected, exited _keep_connected")
|
| 796 |
+
|
| 797 |
+
async def subscribe(self, topic, qos=0):
|
| 798 |
+
qos_check(qos)
|
| 799 |
+
while 1:
|
| 800 |
+
await self._connection()
|
| 801 |
+
try:
|
| 802 |
+
return await super().subscribe(topic, qos)
|
| 803 |
+
except OSError:
|
| 804 |
+
pass
|
| 805 |
+
self._reconnect() # Broker or WiFi fail.
|
| 806 |
+
|
| 807 |
+
async def unsubscribe(self, topic):
|
| 808 |
+
while 1:
|
| 809 |
+
await self._connection()
|
| 810 |
+
try:
|
| 811 |
+
return await super().unsubscribe(topic)
|
| 812 |
+
except OSError:
|
| 813 |
+
pass
|
| 814 |
+
self._reconnect() # Broker or WiFi fail.
|
| 815 |
+
|
| 816 |
+
async def publish(self, topic, msg, retain=False, qos=0):
|
| 817 |
+
qos_check(qos)
|
| 818 |
+
while 1:
|
| 819 |
+
await self._connection()
|
| 820 |
+
try:
|
| 821 |
+
return await super().publish(topic, msg, retain, qos)
|
| 822 |
+
except OSError:
|
| 823 |
+
pass
|
| 824 |
+
self._reconnect() # Broker or WiFi fail.
|
scripts/lib/netman.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# .';:cc;.
|
| 2 |
+
# .,',;lol::c.
|
| 3 |
+
# ;';lddddlclo
|
| 4 |
+
# lcloxxoddodxdool:,.
|
| 5 |
+
# cxdddxdodxdkOkkkkkkkd:.
|
| 6 |
+
# .ldxkkOOOOkkOO000Okkxkkkkx:.
|
| 7 |
+
# .lddxkkOkOOO0OOO0000Okxxxxkkkk:
|
| 8 |
+
# 'ooddkkkxxkO0000KK00Okxdoodxkkkko
|
| 9 |
+
# .ooodxkkxxxOO000kkkO0KOxolooxkkxxkl
|
| 10 |
+
# lolodxkkxxkOx,. .lkdolodkkxxxO.
|
| 11 |
+
# doloodxkkkOk .... .,cxO;
|
| 12 |
+
# ddoodddxkkkk: ,oxxxkOdc'..o'
|
| 13 |
+
# :kdddxxxxd, ,lolccldxxxkkOOOkkkko,
|
| 14 |
+
# lOkxkkk; :xkkkkkkkkOOO000OOkkOOk.
|
| 15 |
+
# ;00Ok' 'O000OO0000000000OOOO0Od.
|
| 16 |
+
# .l0l.;OOO000000OOOOOO000000x,
|
| 17 |
+
# .'OKKKK00000000000000kc.
|
| 18 |
+
# .:ox0KKKKKKK0kdc,.
|
| 19 |
+
# ...
|
| 20 |
+
#
|
| 21 |
+
# Author: peppe8o
|
| 22 |
+
# Date: Jul 24th, 2022
|
| 23 |
+
# Version: 1.0
|
| 24 |
+
# https://peppe8o.com
|
| 25 |
+
|
| 26 |
+
# modified by @sgbaird from source:
|
| 27 |
+
# https://peppe8o.com/getting-started-with-wifi-on-raspberry-pi-pico-w-and-micropython/
|
| 28 |
+
|
| 29 |
+
import time
|
| 30 |
+
|
| 31 |
+
import network
|
| 32 |
+
import rp2
|
| 33 |
+
from ubinascii import hexlify
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def connectWiFi(ssid, password, country=None, wifi_energy_saver=False, retries=3):
|
| 37 |
+
for _ in range(retries):
|
| 38 |
+
try:
|
| 39 |
+
if country is not None:
|
| 40 |
+
# https://www.google.com/search?q=wifi+country+codes
|
| 41 |
+
rp2.country(country)
|
| 42 |
+
wlan = network.WLAN(network.STA_IF)
|
| 43 |
+
if not wifi_energy_saver:
|
| 44 |
+
wlan.config(pm=0xA11140) # avoid the energy-saving WiFi mode
|
| 45 |
+
wlan.active(True)
|
| 46 |
+
|
| 47 |
+
mac = hexlify(network.WLAN().config("mac"), ":").decode()
|
| 48 |
+
print(f"MAC address: {mac}")
|
| 49 |
+
|
| 50 |
+
wlan.connect(ssid, password)
|
| 51 |
+
# Wait for connect or fail
|
| 52 |
+
max_wait = 10
|
| 53 |
+
while max_wait > 0:
|
| 54 |
+
if wlan.status() < 0 or wlan.status() >= 3:
|
| 55 |
+
break
|
| 56 |
+
max_wait -= 1
|
| 57 |
+
print("waiting for connection...")
|
| 58 |
+
time.sleep(1)
|
| 59 |
+
|
| 60 |
+
# Handle connection error
|
| 61 |
+
if wlan.status() != 3:
|
| 62 |
+
raise RuntimeError("network connection failed")
|
| 63 |
+
else:
|
| 64 |
+
print("connected")
|
| 65 |
+
status = wlan.ifconfig()
|
| 66 |
+
print("ip = " + status[0])
|
| 67 |
+
return status
|
| 68 |
+
except RuntimeError as e:
|
| 69 |
+
print(f"Attempt failed with error: {e}. Retrying...")
|
| 70 |
+
raise RuntimeError(
|
| 71 |
+
"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."
|
| 72 |
+
)
|
| 73 |
+
|
scripts/lib/sdcard/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
The MIT License (MIT)
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2013, 2014 Damien P. George
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in
|
| 13 |
+
all copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
| 21 |
+
THE SOFTWARE.
|
scripts/lib/sdcard/sdcard.py
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
MicroPython driver for SD cards using SPI bus.
|
| 3 |
+
|
| 4 |
+
Requires an SPI bus and a CS pin. Provides readblocks and writeblocks
|
| 5 |
+
methods so the device can be mounted as a filesystem.
|
| 6 |
+
|
| 7 |
+
Example usage on pyboard:
|
| 8 |
+
|
| 9 |
+
import pyb, sdcard, os
|
| 10 |
+
sd = sdcard.SDCard(pyb.SPI(1), pyb.Pin.board.X5)
|
| 11 |
+
pyb.mount(sd, '/sd2')
|
| 12 |
+
os.listdir('/')
|
| 13 |
+
|
| 14 |
+
Example usage on ESP8266:
|
| 15 |
+
|
| 16 |
+
import machine, sdcard, os
|
| 17 |
+
sd = sdcard.SDCard(machine.SPI(1), machine.Pin(15))
|
| 18 |
+
os.mount(sd, '/sd')
|
| 19 |
+
os.listdir('/')
|
| 20 |
+
|
| 21 |
+
Copied from source: https://raw.githubusercontent.com/micropython/micropython-lib/master/micropython/drivers/storage/sdcard/sdcard.py
|
| 22 |
+
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
import time
|
| 26 |
+
|
| 27 |
+
from micropython import const
|
| 28 |
+
|
| 29 |
+
_CMD_TIMEOUT = const(100)
|
| 30 |
+
|
| 31 |
+
_R1_IDLE_STATE = const(1 << 0)
|
| 32 |
+
# R1_ERASE_RESET = const(1 << 1)
|
| 33 |
+
_R1_ILLEGAL_COMMAND = const(1 << 2)
|
| 34 |
+
# R1_COM_CRC_ERROR = const(1 << 3)
|
| 35 |
+
# R1_ERASE_SEQUENCE_ERROR = const(1 << 4)
|
| 36 |
+
# R1_ADDRESS_ERROR = const(1 << 5)
|
| 37 |
+
# R1_PARAMETER_ERROR = const(1 << 6)
|
| 38 |
+
_TOKEN_CMD25 = const(0xFC)
|
| 39 |
+
_TOKEN_STOP_TRAN = const(0xFD)
|
| 40 |
+
_TOKEN_DATA = const(0xFE)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
class SDCard:
|
| 44 |
+
def __init__(self, spi, cs, baudrate=1320000):
|
| 45 |
+
self.spi = spi
|
| 46 |
+
self.cs = cs
|
| 47 |
+
|
| 48 |
+
self.cmdbuf = bytearray(6)
|
| 49 |
+
self.dummybuf = bytearray(512)
|
| 50 |
+
self.tokenbuf = bytearray(1)
|
| 51 |
+
for i in range(512):
|
| 52 |
+
self.dummybuf[i] = 0xFF
|
| 53 |
+
self.dummybuf_memoryview = memoryview(self.dummybuf)
|
| 54 |
+
|
| 55 |
+
# initialise the card
|
| 56 |
+
self.init_card(baudrate)
|
| 57 |
+
|
| 58 |
+
def init_spi(self, baudrate):
|
| 59 |
+
try:
|
| 60 |
+
master = self.spi.MASTER
|
| 61 |
+
except AttributeError:
|
| 62 |
+
# on ESP8266
|
| 63 |
+
self.spi.init(baudrate=baudrate, phase=0, polarity=0)
|
| 64 |
+
else:
|
| 65 |
+
# on pyboard
|
| 66 |
+
self.spi.init(master, baudrate=baudrate, phase=0, polarity=0)
|
| 67 |
+
|
| 68 |
+
def init_card(self, baudrate):
|
| 69 |
+
# init CS pin
|
| 70 |
+
self.cs.init(self.cs.OUT, value=1)
|
| 71 |
+
|
| 72 |
+
# init SPI bus; use low data rate for initialisation
|
| 73 |
+
self.init_spi(100000)
|
| 74 |
+
|
| 75 |
+
# clock card at least 100 cycles with cs high
|
| 76 |
+
for i in range(16):
|
| 77 |
+
self.spi.write(b"\xff")
|
| 78 |
+
|
| 79 |
+
# CMD0: init card; should return _R1_IDLE_STATE (allow 5 attempts)
|
| 80 |
+
for _ in range(5):
|
| 81 |
+
if self.cmd(0, 0, 0x95) == _R1_IDLE_STATE:
|
| 82 |
+
break
|
| 83 |
+
else:
|
| 84 |
+
raise OSError("no SD card")
|
| 85 |
+
|
| 86 |
+
# CMD8: determine card version
|
| 87 |
+
r = self.cmd(8, 0x01AA, 0x87, 4)
|
| 88 |
+
if r == _R1_IDLE_STATE:
|
| 89 |
+
self.init_card_v2()
|
| 90 |
+
elif r == (_R1_IDLE_STATE | _R1_ILLEGAL_COMMAND):
|
| 91 |
+
self.init_card_v1()
|
| 92 |
+
else:
|
| 93 |
+
raise OSError("couldn't determine SD card version")
|
| 94 |
+
|
| 95 |
+
# get the number of sectors
|
| 96 |
+
# CMD9: response R2 (R1 byte + 16-byte block read)
|
| 97 |
+
if self.cmd(9, 0, 0, 0, False) != 0:
|
| 98 |
+
raise OSError("no response from SD card")
|
| 99 |
+
csd = bytearray(16)
|
| 100 |
+
self.readinto(csd)
|
| 101 |
+
if csd[0] & 0xC0 == 0x40: # CSD version 2.0
|
| 102 |
+
self.sectors = ((csd[8] << 8 | csd[9]) + 1) * 1024
|
| 103 |
+
elif csd[0] & 0xC0 == 0x00: # CSD version 1.0 (old, <=2GB)
|
| 104 |
+
c_size = (csd[6] & 0b11) << 10 | csd[7] << 2 | csd[8] >> 6
|
| 105 |
+
c_size_mult = (csd[9] & 0b11) << 1 | csd[10] >> 7
|
| 106 |
+
read_bl_len = csd[5] & 0b1111
|
| 107 |
+
capacity = (c_size + 1) * (2 ** (c_size_mult + 2)) * (2**read_bl_len)
|
| 108 |
+
self.sectors = capacity // 512
|
| 109 |
+
else:
|
| 110 |
+
raise OSError("SD card CSD format not supported")
|
| 111 |
+
# print('sectors', self.sectors)
|
| 112 |
+
|
| 113 |
+
# CMD16: set block length to 512 bytes
|
| 114 |
+
if self.cmd(16, 512, 0) != 0:
|
| 115 |
+
raise OSError("can't set 512 block size")
|
| 116 |
+
|
| 117 |
+
# set to high data rate now that it's initialised
|
| 118 |
+
self.init_spi(baudrate)
|
| 119 |
+
|
| 120 |
+
def init_card_v1(self):
|
| 121 |
+
for i in range(_CMD_TIMEOUT):
|
| 122 |
+
time.sleep_ms(50)
|
| 123 |
+
self.cmd(55, 0, 0)
|
| 124 |
+
if self.cmd(41, 0, 0) == 0:
|
| 125 |
+
# SDSC card, uses byte addressing in read/write/erase commands
|
| 126 |
+
self.cdv = 512
|
| 127 |
+
# print("[SDCard] v1 card")
|
| 128 |
+
return
|
| 129 |
+
raise OSError("timeout waiting for v1 card")
|
| 130 |
+
|
| 131 |
+
def init_card_v2(self):
|
| 132 |
+
for i in range(_CMD_TIMEOUT):
|
| 133 |
+
time.sleep_ms(50)
|
| 134 |
+
self.cmd(58, 0, 0, 4)
|
| 135 |
+
self.cmd(55, 0, 0)
|
| 136 |
+
if self.cmd(41, 0x40000000, 0) == 0:
|
| 137 |
+
self.cmd(
|
| 138 |
+
58, 0, 0, -4
|
| 139 |
+
) # 4-byte response, negative means keep the first byte
|
| 140 |
+
ocr = self.tokenbuf[0] # get first byte of response, which is OCR
|
| 141 |
+
if not ocr & 0x40:
|
| 142 |
+
# SDSC card, uses byte addressing in read/write/erase commands
|
| 143 |
+
self.cdv = 512
|
| 144 |
+
else:
|
| 145 |
+
# SDHC/SDXC card, uses block addressing in read/write/erase commands
|
| 146 |
+
self.cdv = 1
|
| 147 |
+
# print("[SDCard] v2 card")
|
| 148 |
+
return
|
| 149 |
+
raise OSError("timeout waiting for v2 card")
|
| 150 |
+
|
| 151 |
+
def cmd(self, cmd, arg, crc, final=0, release=True, skip1=False):
|
| 152 |
+
self.cs(0)
|
| 153 |
+
|
| 154 |
+
# create and send the command
|
| 155 |
+
buf = self.cmdbuf
|
| 156 |
+
buf[0] = 0x40 | cmd
|
| 157 |
+
buf[1] = arg >> 24
|
| 158 |
+
buf[2] = arg >> 16
|
| 159 |
+
buf[3] = arg >> 8
|
| 160 |
+
buf[4] = arg
|
| 161 |
+
buf[5] = crc
|
| 162 |
+
self.spi.write(buf)
|
| 163 |
+
|
| 164 |
+
if skip1:
|
| 165 |
+
self.spi.readinto(self.tokenbuf, 0xFF)
|
| 166 |
+
|
| 167 |
+
# wait for the response (response[7] == 0)
|
| 168 |
+
for i in range(_CMD_TIMEOUT):
|
| 169 |
+
self.spi.readinto(self.tokenbuf, 0xFF)
|
| 170 |
+
response = self.tokenbuf[0]
|
| 171 |
+
if not (response & 0x80):
|
| 172 |
+
# this could be a big-endian integer that we are getting here
|
| 173 |
+
# if final<0 then store the first byte to tokenbuf and discard the rest
|
| 174 |
+
if final < 0:
|
| 175 |
+
self.spi.readinto(self.tokenbuf, 0xFF)
|
| 176 |
+
final = -1 - final
|
| 177 |
+
for j in range(final):
|
| 178 |
+
self.spi.write(b"\xff")
|
| 179 |
+
if release:
|
| 180 |
+
self.cs(1)
|
| 181 |
+
self.spi.write(b"\xff")
|
| 182 |
+
return response
|
| 183 |
+
|
| 184 |
+
# timeout
|
| 185 |
+
self.cs(1)
|
| 186 |
+
self.spi.write(b"\xff")
|
| 187 |
+
return -1
|
| 188 |
+
|
| 189 |
+
def readinto(self, buf):
|
| 190 |
+
self.cs(0)
|
| 191 |
+
|
| 192 |
+
# read until start byte (0xff)
|
| 193 |
+
for i in range(_CMD_TIMEOUT):
|
| 194 |
+
self.spi.readinto(self.tokenbuf, 0xFF)
|
| 195 |
+
if self.tokenbuf[0] == _TOKEN_DATA:
|
| 196 |
+
break
|
| 197 |
+
time.sleep_ms(1)
|
| 198 |
+
else:
|
| 199 |
+
self.cs(1)
|
| 200 |
+
raise OSError("timeout waiting for response")
|
| 201 |
+
|
| 202 |
+
# read data
|
| 203 |
+
mv = self.dummybuf_memoryview
|
| 204 |
+
if len(buf) != len(mv):
|
| 205 |
+
mv = mv[: len(buf)]
|
| 206 |
+
self.spi.write_readinto(mv, buf)
|
| 207 |
+
|
| 208 |
+
# read checksum
|
| 209 |
+
self.spi.write(b"\xff")
|
| 210 |
+
self.spi.write(b"\xff")
|
| 211 |
+
|
| 212 |
+
self.cs(1)
|
| 213 |
+
self.spi.write(b"\xff")
|
| 214 |
+
|
| 215 |
+
def write(self, token, buf):
|
| 216 |
+
self.cs(0)
|
| 217 |
+
|
| 218 |
+
# send: start of block, data, checksum
|
| 219 |
+
self.spi.read(1, token)
|
| 220 |
+
self.spi.write(buf)
|
| 221 |
+
self.spi.write(b"\xff")
|
| 222 |
+
self.spi.write(b"\xff")
|
| 223 |
+
|
| 224 |
+
# check the response
|
| 225 |
+
if (self.spi.read(1, 0xFF)[0] & 0x1F) != 0x05:
|
| 226 |
+
self.cs(1)
|
| 227 |
+
self.spi.write(b"\xff")
|
| 228 |
+
return
|
| 229 |
+
|
| 230 |
+
# wait for write to finish
|
| 231 |
+
while self.spi.read(1, 0xFF)[0] == 0:
|
| 232 |
+
pass
|
| 233 |
+
|
| 234 |
+
self.cs(1)
|
| 235 |
+
self.spi.write(b"\xff")
|
| 236 |
+
|
| 237 |
+
def write_token(self, token):
|
| 238 |
+
self.cs(0)
|
| 239 |
+
self.spi.read(1, token)
|
| 240 |
+
self.spi.write(b"\xff")
|
| 241 |
+
# wait for write to finish
|
| 242 |
+
while self.spi.read(1, 0xFF)[0] == 0x00:
|
| 243 |
+
pass
|
| 244 |
+
|
| 245 |
+
self.cs(1)
|
| 246 |
+
self.spi.write(b"\xff")
|
| 247 |
+
|
| 248 |
+
def readblocks(self, block_num, buf):
|
| 249 |
+
nblocks = len(buf) // 512
|
| 250 |
+
assert nblocks and not len(buf) % 512, "Buffer length is invalid"
|
| 251 |
+
if nblocks == 1:
|
| 252 |
+
# CMD17: set read address for single block
|
| 253 |
+
if self.cmd(17, block_num * self.cdv, 0, release=False) != 0:
|
| 254 |
+
# release the card
|
| 255 |
+
self.cs(1)
|
| 256 |
+
raise OSError(5) # EIO
|
| 257 |
+
# receive the data and release card
|
| 258 |
+
self.readinto(buf)
|
| 259 |
+
else:
|
| 260 |
+
# CMD18: set read address for multiple blocks
|
| 261 |
+
if self.cmd(18, block_num * self.cdv, 0, release=False) != 0:
|
| 262 |
+
# release the card
|
| 263 |
+
self.cs(1)
|
| 264 |
+
raise OSError(5) # EIO
|
| 265 |
+
offset = 0
|
| 266 |
+
mv = memoryview(buf)
|
| 267 |
+
while nblocks:
|
| 268 |
+
# receive the data and release card
|
| 269 |
+
self.readinto(mv[offset : offset + 512])
|
| 270 |
+
offset += 512
|
| 271 |
+
nblocks -= 1
|
| 272 |
+
if self.cmd(12, 0, 0xFF, skip1=True):
|
| 273 |
+
raise OSError(5) # EIO
|
| 274 |
+
|
| 275 |
+
def writeblocks(self, block_num, buf):
|
| 276 |
+
nblocks, err = divmod(len(buf), 512)
|
| 277 |
+
assert nblocks and not err, "Buffer length is invalid"
|
| 278 |
+
if nblocks == 1:
|
| 279 |
+
# CMD24: set write address for single block
|
| 280 |
+
if self.cmd(24, block_num * self.cdv, 0) != 0:
|
| 281 |
+
raise OSError(5) # EIO
|
| 282 |
+
|
| 283 |
+
# send the data
|
| 284 |
+
self.write(_TOKEN_DATA, buf)
|
| 285 |
+
else:
|
| 286 |
+
# CMD25: set write address for first block
|
| 287 |
+
if self.cmd(25, block_num * self.cdv, 0) != 0:
|
| 288 |
+
raise OSError(5) # EIO
|
| 289 |
+
# send the data
|
| 290 |
+
offset = 0
|
| 291 |
+
mv = memoryview(buf)
|
| 292 |
+
while nblocks:
|
| 293 |
+
self.write(_TOKEN_CMD25, mv[offset : offset + 512])
|
| 294 |
+
offset += 512
|
| 295 |
+
nblocks -= 1
|
| 296 |
+
self.write_token(_TOKEN_STOP_TRAN)
|
| 297 |
+
|
| 298 |
+
def ioctl(self, op, arg):
|
| 299 |
+
if op == 4: # get number of blocks
|
| 300 |
+
return self.sectors
|
| 301 |
+
if op == 5: # get block size in bytes
|
| 302 |
+
return 512
|
scripts/lib/sdl_demo_utils.py
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import sys
|
| 3 |
+
from time import localtime, sleep, ticks_diff, ticks_ms # type: ignore
|
| 4 |
+
|
| 5 |
+
import uos
|
| 6 |
+
from data_logging import (
|
| 7 |
+
get_local_timestamp,
|
| 8 |
+
get_onboard_temperature,
|
| 9 |
+
write_payload_backup,
|
| 10 |
+
)
|
| 11 |
+
from machine import PWM, Pin
|
| 12 |
+
from ufastrsa.genprime import genrsa
|
| 13 |
+
from ufastrsa.rsa import RSA
|
| 14 |
+
from uio import StringIO
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def beep(buzzer, power=0.005):
|
| 18 |
+
buzzer.freq(300)
|
| 19 |
+
buzzer.duty_u16(round(65535 * power))
|
| 20 |
+
sleep(0.15)
|
| 21 |
+
buzzer.duty_u16(0)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def get_traceback(err):
|
| 25 |
+
try:
|
| 26 |
+
with StringIO() as f: # type: ignore
|
| 27 |
+
sys.print_exception(err, f)
|
| 28 |
+
return f.getvalue()
|
| 29 |
+
except Exception as err2:
|
| 30 |
+
print(err2)
|
| 31 |
+
return f"Failed to extract file and line number due to {err2}.\nOriginal error: {err}" # noqa: E501
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def merge_two_dicts(x, y):
|
| 35 |
+
z = x.copy() # start with keys and values of x
|
| 36 |
+
z.update(y) # modifies z with keys and values of y
|
| 37 |
+
return z
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def path_exists(path):
|
| 41 |
+
# Check if path exists.
|
| 42 |
+
# Works for relative and absolute path.
|
| 43 |
+
parent = "" # parent folder name
|
| 44 |
+
name = path # name of file/folder
|
| 45 |
+
|
| 46 |
+
# Check if file/folder has a parent folder
|
| 47 |
+
index = path.rstrip("/").rfind("/")
|
| 48 |
+
if index >= 0:
|
| 49 |
+
index += 1
|
| 50 |
+
parent = path[: index - 1]
|
| 51 |
+
name = path[index:]
|
| 52 |
+
|
| 53 |
+
# Searching with iterator is more efficient if the parent contains lost of files/folders
|
| 54 |
+
# return name in uos.listdir(parent)
|
| 55 |
+
return any((name == x[0]) for x in uos.ilistdir(parent))
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def encrypt_id(my_id, verbose=False):
|
| 59 |
+
rsa_path = "rsa.json"
|
| 60 |
+
# if path_exists(rsa_path):
|
| 61 |
+
try:
|
| 62 |
+
with open(rsa_path, "r") as f:
|
| 63 |
+
cipher_data = json.load(f)
|
| 64 |
+
cipher = RSA(
|
| 65 |
+
cipher_data["bits"],
|
| 66 |
+
n=cipher_data["n"],
|
| 67 |
+
e=cipher_data["e"],
|
| 68 |
+
d=cipher_data["d"],
|
| 69 |
+
)
|
| 70 |
+
except (KeyError, OSError) as e:
|
| 71 |
+
print(e)
|
| 72 |
+
print("Generating new RSA parameters...")
|
| 73 |
+
bits = 256
|
| 74 |
+
bits, n, e, d = genrsa(bits, e=65537) # type: ignore
|
| 75 |
+
cipher = RSA(bits, n=n, e=e, d=d)
|
| 76 |
+
with open("rsa.json", "w") as f:
|
| 77 |
+
json.dump(dict(bits=bits, n=n, e=e, d=d), f)
|
| 78 |
+
|
| 79 |
+
if verbose:
|
| 80 |
+
with open(rsa_path, "r") as f:
|
| 81 |
+
cipher_data = json.load(f)
|
| 82 |
+
print("RSA parameters (keep private):")
|
| 83 |
+
print(cipher_data)
|
| 84 |
+
|
| 85 |
+
my_id = int.from_bytes(cipher.pkcs_encrypt(my_id), "big")
|
| 86 |
+
return my_id
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def decrypt_id(my_id):
|
| 90 |
+
rsa_path = "rsa.json"
|
| 91 |
+
if path_exists(rsa_path):
|
| 92 |
+
with open(rsa_path, "r") as f:
|
| 93 |
+
cipher_data = json.load(f)
|
| 94 |
+
cipher = RSA(
|
| 95 |
+
cipher_data["bits"],
|
| 96 |
+
n=cipher_data["n"],
|
| 97 |
+
e=cipher_data["e"],
|
| 98 |
+
d=cipher_data["d"],
|
| 99 |
+
)
|
| 100 |
+
else:
|
| 101 |
+
bits = 256
|
| 102 |
+
bits, n, e, d = genrsa(bits, e=65537) # type: ignore
|
| 103 |
+
cipher = RSA(bits, n=n, e=e, d=d)
|
| 104 |
+
with open("rsa.json", "w") as f:
|
| 105 |
+
json.dump(dict(bits=bits, n=n, e=e, d=d), f)
|
| 106 |
+
|
| 107 |
+
my_id = int.from_bytes(cipher.pkcs_decrypt(my_id), "big")
|
| 108 |
+
return my_id
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def get_onboard_led():
|
| 112 |
+
try:
|
| 113 |
+
onboard_led = Pin("LED", Pin.OUT) # only works for Pico W
|
| 114 |
+
except Exception as e:
|
| 115 |
+
print(e)
|
| 116 |
+
onboard_led = Pin(25, Pin.OUT)
|
| 117 |
+
return onboard_led
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
class Experiment(object):
|
| 121 |
+
def __init__(
|
| 122 |
+
self,
|
| 123 |
+
run_experiment_fn,
|
| 124 |
+
devices,
|
| 125 |
+
reset_experiment_fn=None,
|
| 126 |
+
validate_inputs_fn=None,
|
| 127 |
+
emergency_shutdown_fn=None,
|
| 128 |
+
buzzer=None,
|
| 129 |
+
sdcard_ready=False,
|
| 130 |
+
) -> None:
|
| 131 |
+
self.validate_inputs_fn = validate_inputs_fn
|
| 132 |
+
self.run_experiment_fn = run_experiment_fn
|
| 133 |
+
self.reset_experiment_fn = reset_experiment_fn
|
| 134 |
+
self.devices = devices
|
| 135 |
+
self.emergency_shutdown_fn = emergency_shutdown_fn
|
| 136 |
+
self.buzzer = buzzer
|
| 137 |
+
self.sdcard_ready = sdcard_ready
|
| 138 |
+
|
| 139 |
+
if self.reset_experiment_fn is None:
|
| 140 |
+
|
| 141 |
+
def do_nothing(*args, **kwargs):
|
| 142 |
+
pass
|
| 143 |
+
|
| 144 |
+
self.reset_experiment_fn = do_nothing
|
| 145 |
+
|
| 146 |
+
if self.emergency_shutdown_fn is None:
|
| 147 |
+
self.emergency_shutdown_fn = self.reset_experiment_fn
|
| 148 |
+
|
| 149 |
+
if self.validate_inputs_fn is None:
|
| 150 |
+
|
| 151 |
+
def no_input_validation(*args, **kwargs):
|
| 152 |
+
return True
|
| 153 |
+
|
| 154 |
+
self.validate_inputs_fn = no_input_validation
|
| 155 |
+
|
| 156 |
+
if self.buzzer is None:
|
| 157 |
+
self.buzzer = PWM(Pin(18))
|
| 158 |
+
|
| 159 |
+
def try_experiment(self, msg):
|
| 160 |
+
payload_data = {}
|
| 161 |
+
# # pin numbers not used here, but can help with organization for complex tasks
|
| 162 |
+
# p = int(t[5:]) # pin number
|
| 163 |
+
|
| 164 |
+
print(msg)
|
| 165 |
+
|
| 166 |
+
# careful not to throw an unrecoverable error due to bad request
|
| 167 |
+
# Perform the experiment and record the results
|
| 168 |
+
try:
|
| 169 |
+
parameters = json.loads(msg)
|
| 170 |
+
payload_data["_input_message"] = parameters
|
| 171 |
+
|
| 172 |
+
# don't allow access to hardware if any input values are out of bounds
|
| 173 |
+
self.validate_inputs_fn(parameters) # type: ignore
|
| 174 |
+
|
| 175 |
+
beep(self.buzzer)
|
| 176 |
+
sensor_data = self.run_experiment_fn(parameters, self.devices)
|
| 177 |
+
payload_data = merge_two_dicts(payload_data, sensor_data)
|
| 178 |
+
|
| 179 |
+
except Exception as err:
|
| 180 |
+
print(err)
|
| 181 |
+
if "_input_message" not in payload_data.keys():
|
| 182 |
+
payload_data["_input_message"] = msg
|
| 183 |
+
payload_data["error"] = get_traceback(err)
|
| 184 |
+
|
| 185 |
+
try:
|
| 186 |
+
payload_data["onboard_temperature_K"] = get_onboard_temperature(unit="K")
|
| 187 |
+
payload_data["sd_card_ready"] = self.sdcard_ready
|
| 188 |
+
stamp, time_str = get_local_timestamp(return_str=True) # type: ignore
|
| 189 |
+
payload_data["utc_timestamp"] = stamp
|
| 190 |
+
payload_data["utc_time_str"] = time_str
|
| 191 |
+
except OverflowError as e:
|
| 192 |
+
print(get_traceback(e))
|
| 193 |
+
except Exception as e:
|
| 194 |
+
print(get_traceback(e))
|
| 195 |
+
|
| 196 |
+
try:
|
| 197 |
+
parameters = json.loads(msg)
|
| 198 |
+
self.reset_experiment_fn(parameters, devices=self.devices) # type: ignore
|
| 199 |
+
except Exception as e:
|
| 200 |
+
try:
|
| 201 |
+
self.emergency_shutdown_fn(devices=self.devices) # type: ignore
|
| 202 |
+
payload_data["reset_error"] = get_traceback(e)
|
| 203 |
+
except Exception as e:
|
| 204 |
+
payload_data["emergency_error"] = get_traceback(e)
|
| 205 |
+
|
| 206 |
+
return payload_data
|
| 207 |
+
|
| 208 |
+
def write_to_sd_card(self, payload_data, fpath="/sd/experiments.txt"):
|
| 209 |
+
try:
|
| 210 |
+
write_payload_backup(payload_data, fpath=fpath)
|
| 211 |
+
except Exception as e:
|
| 212 |
+
w = f"Failed to write to SD card: {get_traceback(e)}"
|
| 213 |
+
print(w)
|
| 214 |
+
payload_data["warning"] = w
|
| 215 |
+
|
| 216 |
+
return payload_data
|
| 217 |
+
|
| 218 |
+
# def log_to_mongodb(
|
| 219 |
+
# self,
|
| 220 |
+
# payload_data,
|
| 221 |
+
# api_key: str,
|
| 222 |
+
# url: str,
|
| 223 |
+
# cluster_name: str,
|
| 224 |
+
# database_name: str,
|
| 225 |
+
# collection_name: str,
|
| 226 |
+
# verbose: bool = True,
|
| 227 |
+
# retries: int = 2,
|
| 228 |
+
# ):
|
| 229 |
+
# try:
|
| 230 |
+
# log_to_mongodb(
|
| 231 |
+
# payload_data,
|
| 232 |
+
# url=url,
|
| 233 |
+
# api_key=api_key,
|
| 234 |
+
# cluster_name=cluster_name,
|
| 235 |
+
# database_name=database_name,
|
| 236 |
+
# collection_name=collection_name,
|
| 237 |
+
# verbose=verbose,
|
| 238 |
+
# retries=retries,
|
| 239 |
+
# )
|
| 240 |
+
# except Exception as e:
|
| 241 |
+
# print(f"Failed to log to MongoDB backend: {get_traceback(e)}")
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
def heartbeat(client, first, ping_interval_ms=15000):
|
| 245 |
+
global lastping
|
| 246 |
+
if first:
|
| 247 |
+
client.ping()
|
| 248 |
+
lastping = ticks_ms()
|
| 249 |
+
if ticks_diff(ticks_ms(), lastping) >= ping_interval_ms:
|
| 250 |
+
client.ping()
|
| 251 |
+
lastping = ticks_ms()
|
| 252 |
+
return
|
| 253 |
+
|
| 254 |
+
|
| 255 |
+
def sign_of_life(led, first, blink_interval_ms=5000):
|
| 256 |
+
global last_blink
|
| 257 |
+
if first:
|
| 258 |
+
led.on()
|
| 259 |
+
last_blink = ticks_ms()
|
| 260 |
+
time_since = ticks_diff(ticks_ms(), last_blink)
|
| 261 |
+
if led.value() == 0 and time_since >= blink_interval_ms:
|
| 262 |
+
led.toggle()
|
| 263 |
+
last_blink = ticks_ms()
|
| 264 |
+
elif led.value() == 1 and time_since >= 500:
|
| 265 |
+
led.toggle()
|
| 266 |
+
last_blink = ticks_ms()
|
| 267 |
+
|
| 268 |
+
|
| 269 |
+
class DummyMotor:
|
| 270 |
+
def __init__(self):
|
| 271 |
+
pass
|
| 272 |
+
|
| 273 |
+
|
| 274 |
+
class DummySensor:
|
| 275 |
+
def __init__(self):
|
| 276 |
+
pass
|
scripts/lib/smbus2-0.5.0.dist-info/METADATA
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Metadata-Version: 2.1
|
| 2 |
+
Name: smbus2
|
| 3 |
+
Version: 0.5.0
|
| 4 |
+
Summary: smbus2 is a drop-in replacement for smbus-cffi/smbus-python in pure Python
|
| 5 |
+
Home-page: https://github.com/kplindegaard/smbus2
|
| 6 |
+
Author: Karl-Petter Lindegaard
|
| 7 |
+
Author-email: kp.lindegaard@gmail.com
|
| 8 |
+
License: MIT
|
| 9 |
+
Keywords: smbus,smbus2,python,i2c,raspberrypi,linux
|
| 10 |
+
Classifier: Development Status :: 4 - Beta
|
| 11 |
+
Classifier: Topic :: Utilities
|
| 12 |
+
Classifier: License :: OSI Approved :: MIT License
|
| 13 |
+
Classifier: Programming Language :: Python :: 2
|
| 14 |
+
Classifier: Programming Language :: Python :: 2.7
|
| 15 |
+
Classifier: Programming Language :: Python :: 3
|
| 16 |
+
Classifier: Programming Language :: Python :: 3.6
|
| 17 |
+
Classifier: Programming Language :: Python :: 3.7
|
| 18 |
+
Classifier: Programming Language :: Python :: 3.8
|
| 19 |
+
Classifier: Programming Language :: Python :: 3.9
|
| 20 |
+
Classifier: Programming Language :: Python :: 3.10
|
| 21 |
+
Classifier: Programming Language :: Python :: 3.11
|
| 22 |
+
Classifier: Programming Language :: Python :: 3.12
|
| 23 |
+
Classifier: Programming Language :: Python :: 3.13
|
| 24 |
+
Description-Content-Type: text/markdown
|
| 25 |
+
License-File: LICENSE
|
| 26 |
+
Provides-Extra: docs
|
| 27 |
+
Requires-Dist: sphinx>=1.5.3; extra == "docs"
|
| 28 |
+
Provides-Extra: qa
|
| 29 |
+
Requires-Dist: flake8; extra == "qa"
|
| 30 |
+
|
| 31 |
+
# smbus2
|
| 32 |
+
A drop-in replacement for smbus-cffi/smbus-python in pure Python
|
| 33 |
+
|
| 34 |
+
[](https://github.com/kplindegaard/smbus2/actions/workflows/python-build-test.yml)
|
| 35 |
+
[](http://smbus2.readthedocs.io/en/latest/?badge=latest)
|
| 36 |
+

|
| 37 |
+
[](https://sonarcloud.io/dashboard?id=kplindegaard_smbus2)
|
| 38 |
+
|
| 39 |
+

|
| 40 |
+
[](https://pypi.org/project/smbus2/)
|
| 41 |
+
[](https://pypi.org/project/smbus2/)
|
| 42 |
+
|
| 43 |
+
# Introduction
|
| 44 |
+
|
| 45 |
+
smbus2 is (yet another) pure Python implementation of the [python-smbus](http://www.lm-sensors.org/browser/i2c-tools/trunk/py-smbus/) package.
|
| 46 |
+
|
| 47 |
+
It was designed from the ground up with two goals in mind:
|
| 48 |
+
|
| 49 |
+
1. It should be a drop-in replacement of smbus. The syntax shall be the same.
|
| 50 |
+
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.
|
| 51 |
+
|
| 52 |
+
Currently supported features are:
|
| 53 |
+
|
| 54 |
+
* Get i2c capabilities (I2C_FUNCS)
|
| 55 |
+
* SMBus Packet Error Checking (PEC) support
|
| 56 |
+
* read_byte
|
| 57 |
+
* write_byte
|
| 58 |
+
* read_byte_data
|
| 59 |
+
* write_byte_data
|
| 60 |
+
* read_word_data
|
| 61 |
+
* write_word_data
|
| 62 |
+
* read_i2c_block_data
|
| 63 |
+
* write_i2c_block_data
|
| 64 |
+
* write_quick
|
| 65 |
+
* process_call
|
| 66 |
+
* read_block_data
|
| 67 |
+
* write_block_data
|
| 68 |
+
* block_process_call
|
| 69 |
+
* i2c_rdwr - *combined write/read transactions with repeated start*
|
| 70 |
+
|
| 71 |
+
It is developed on Python 2.7 but works without any modifications in Python 3.X too.
|
| 72 |
+
|
| 73 |
+
More information about updates and general changes are recorded in the [change log](https://github.com/kplindegaard/smbus2/blob/master/CHANGELOG.md).
|
| 74 |
+
|
| 75 |
+
# SMBus code examples
|
| 76 |
+
|
| 77 |
+
smbus2 installs next to smbus as the package, so it's not really a 100% replacement. You must change the module name.
|
| 78 |
+
|
| 79 |
+
## Example 1a: Read a byte
|
| 80 |
+
|
| 81 |
+
```python
|
| 82 |
+
from smbus2 import SMBus
|
| 83 |
+
|
| 84 |
+
# Open i2c bus 1 and read one byte from address 80, offset 0
|
| 85 |
+
bus = SMBus(1)
|
| 86 |
+
b = bus.read_byte_data(80, 0)
|
| 87 |
+
print(b)
|
| 88 |
+
bus.close()
|
| 89 |
+
```
|
| 90 |
+
|
| 91 |
+
## Example 1b: Read a byte using 'with'
|
| 92 |
+
|
| 93 |
+
This is the very same example but safer to use since the smbus will be closed automatically when exiting the with block.
|
| 94 |
+
|
| 95 |
+
```python
|
| 96 |
+
from smbus2 import SMBus
|
| 97 |
+
|
| 98 |
+
with SMBus(1) as bus:
|
| 99 |
+
b = bus.read_byte_data(80, 0)
|
| 100 |
+
print(b)
|
| 101 |
+
```
|
| 102 |
+
|
| 103 |
+
## Example 1c: Read a byte with PEC enabled
|
| 104 |
+
|
| 105 |
+
Same example with Packet Error Checking enabled.
|
| 106 |
+
|
| 107 |
+
```python
|
| 108 |
+
from smbus2 import SMBus
|
| 109 |
+
|
| 110 |
+
with SMBus(1) as bus:
|
| 111 |
+
bus.pec = 1 # Enable PEC
|
| 112 |
+
b = bus.read_byte_data(80, 0)
|
| 113 |
+
print(b)
|
| 114 |
+
```
|
| 115 |
+
|
| 116 |
+
## Example 2: Read a block of data
|
| 117 |
+
|
| 118 |
+
You can read up to 32 bytes at once.
|
| 119 |
+
|
| 120 |
+
```python
|
| 121 |
+
from smbus2 import SMBus
|
| 122 |
+
|
| 123 |
+
with SMBus(1) as bus:
|
| 124 |
+
# Read a block of 16 bytes from address 80, offset 0
|
| 125 |
+
block = bus.read_i2c_block_data(80, 0, 16)
|
| 126 |
+
# Returned value is a list of 16 bytes
|
| 127 |
+
print(block)
|
| 128 |
+
```
|
| 129 |
+
|
| 130 |
+
## Example 3: Write a byte
|
| 131 |
+
|
| 132 |
+
```python
|
| 133 |
+
from smbus2 import SMBus
|
| 134 |
+
|
| 135 |
+
with SMBus(1) as bus:
|
| 136 |
+
# Write a byte to address 80, offset 0
|
| 137 |
+
data = 45
|
| 138 |
+
bus.write_byte_data(80, 0, data)
|
| 139 |
+
```
|
| 140 |
+
|
| 141 |
+
## Example 4: Write a block of data
|
| 142 |
+
|
| 143 |
+
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.
|
| 144 |
+
|
| 145 |
+
```python
|
| 146 |
+
from smbus2 import SMBus
|
| 147 |
+
|
| 148 |
+
with SMBus(1) as bus:
|
| 149 |
+
# Write a block of 8 bytes to address 80 from offset 0
|
| 150 |
+
data = [1, 2, 3, 4, 5, 6, 7, 8]
|
| 151 |
+
bus.write_i2c_block_data(80, 0, data)
|
| 152 |
+
```
|
| 153 |
+
|
| 154 |
+
# I2C
|
| 155 |
+
|
| 156 |
+
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:
|
| 157 |
+
|
| 158 |
+
1. read or write bulks of data larger than SMBus' 32 bytes limit.
|
| 159 |
+
1. write some data and then read from the slave with a repeated start and no stop bit between.
|
| 160 |
+
|
| 161 |
+
Each operation is represented by a *i2c_msg* message object.
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
## Example 5: Single i2c_rdwr
|
| 165 |
+
|
| 166 |
+
```python
|
| 167 |
+
from smbus2 import SMBus, i2c_msg
|
| 168 |
+
|
| 169 |
+
with SMBus(1) as bus:
|
| 170 |
+
# Read 64 bytes from address 80
|
| 171 |
+
msg = i2c_msg.read(80, 64)
|
| 172 |
+
bus.i2c_rdwr(msg)
|
| 173 |
+
|
| 174 |
+
# Write a single byte to address 80
|
| 175 |
+
msg = i2c_msg.write(80, [65])
|
| 176 |
+
bus.i2c_rdwr(msg)
|
| 177 |
+
|
| 178 |
+
# Write some bytes to address 80
|
| 179 |
+
msg = i2c_msg.write(80, [65, 66, 67, 68])
|
| 180 |
+
bus.i2c_rdwr(msg)
|
| 181 |
+
```
|
| 182 |
+
|
| 183 |
+
## Example 6: Dual i2c_rdwr
|
| 184 |
+
|
| 185 |
+
To perform dual operations just add more i2c_msg instances to the bus call:
|
| 186 |
+
|
| 187 |
+
```python
|
| 188 |
+
from smbus2 import SMBus, i2c_msg
|
| 189 |
+
|
| 190 |
+
# Single transaction writing two bytes then read two at address 80
|
| 191 |
+
write = i2c_msg.write(80, [40, 50])
|
| 192 |
+
read = i2c_msg.read(80, 2)
|
| 193 |
+
with SMBus(1) as bus:
|
| 194 |
+
bus.i2c_rdwr(write, read)
|
| 195 |
+
```
|
| 196 |
+
|
| 197 |
+
## Example 7: Access i2c_msg data
|
| 198 |
+
|
| 199 |
+
All data is contained in the i2c_msg instances. Here are some data access alternatives.
|
| 200 |
+
|
| 201 |
+
```python
|
| 202 |
+
# 1: Convert message content to list
|
| 203 |
+
msg = i2c_msg.write(60, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
|
| 204 |
+
data = list(msg) # data = [1, 2, 3, ...]
|
| 205 |
+
print(len(data)) # => 10
|
| 206 |
+
|
| 207 |
+
# 2: i2c_msg is iterable
|
| 208 |
+
for value in msg:
|
| 209 |
+
print(value)
|
| 210 |
+
|
| 211 |
+
# 3: Through i2c_msg properties
|
| 212 |
+
for k in range(msg.len):
|
| 213 |
+
print(msg.buf[k])
|
| 214 |
+
```
|
| 215 |
+
|
| 216 |
+
# Installation instructions
|
| 217 |
+
|
| 218 |
+
From [PyPi](https://pypi.org/) with `pip`:
|
| 219 |
+
|
| 220 |
+
```
|
| 221 |
+
pip install smbus2
|
| 222 |
+
```
|
| 223 |
+
|
| 224 |
+
From [conda-forge](https://anaconda.org/conda-forge) using `conda`:
|
| 225 |
+
|
| 226 |
+
```
|
| 227 |
+
conda install -c conda-forge smbus2
|
| 228 |
+
```
|
| 229 |
+
|
| 230 |
+
Installation from source code is straight forward:
|
| 231 |
+
|
| 232 |
+
```
|
| 233 |
+
python setup.py install
|
| 234 |
+
```
|
scripts/lib/smbus2-0.5.0.dist-info/RECORD
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
smbus2-0.5.0.dist-info/METADATA,,
|
| 2 |
+
smbus2/__init__.py,,
|
| 3 |
+
smbus2/py.typed,,
|
| 4 |
+
smbus2/smbus2.py,,
|
| 5 |
+
smbus2/smbus2.pyi,,
|
| 6 |
+
smbus2-0.5.0.dist-info/RECORD,,
|
scripts/lib/smbus2/__init__.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""smbus2 - A drop-in replacement for smbus-cffi/smbus-python"""
|
| 2 |
+
# The MIT License (MIT)
|
| 3 |
+
# Copyright (c) 2020 Karl-Petter Lindegaard
|
| 4 |
+
#
|
| 5 |
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
# of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
# in the Software without restriction, including without limitation the rights
|
| 8 |
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
# copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
# furnished to do so, subject to the following conditions:
|
| 11 |
+
#
|
| 12 |
+
# The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
# copies or substantial portions of the Software.
|
| 14 |
+
#
|
| 15 |
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
# SOFTWARE.
|
| 22 |
+
|
| 23 |
+
from .smbus2 import SMBus, i2c_msg, I2cFunc # noqa: F401
|
| 24 |
+
|
| 25 |
+
__version__ = "0.5.0"
|
| 26 |
+
__all__ = ["SMBus", "i2c_msg", "I2cFunc"]
|
scripts/lib/smbus2/py.typed
ADDED
|
File without changes
|
scripts/lib/smbus2/smbus2.py
ADDED
|
@@ -0,0 +1,660 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""smbus2 - A drop-in replacement for smbus-cffi/smbus-python"""
|
| 2 |
+
# The MIT License (MIT)
|
| 3 |
+
# Copyright (c) 2020 Karl-Petter Lindegaard
|
| 4 |
+
#
|
| 5 |
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
# of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
# in the Software without restriction, including without limitation the rights
|
| 8 |
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
# copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
# furnished to do so, subject to the following conditions:
|
| 11 |
+
#
|
| 12 |
+
# The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
# copies or substantial portions of the Software.
|
| 14 |
+
#
|
| 15 |
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
# SOFTWARE.
|
| 22 |
+
|
| 23 |
+
import os
|
| 24 |
+
import sys
|
| 25 |
+
from fcntl import ioctl
|
| 26 |
+
from ctypes import c_uint32, c_uint8, c_uint16, c_char, POINTER, Structure, Array, Union, create_string_buffer, string_at
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
# Commands from uapi/linux/i2c-dev.h
|
| 30 |
+
I2C_SLAVE = 0x0703 # Use this slave address
|
| 31 |
+
I2C_SLAVE_FORCE = 0x0706 # Use this slave address, even if it is already in use by a driver!
|
| 32 |
+
I2C_FUNCS = 0x0705 # Get the adapter functionality mask
|
| 33 |
+
I2C_RDWR = 0x0707 # Combined R/W transfer (one STOP only)
|
| 34 |
+
I2C_SMBUS = 0x0720 # SMBus transfer. Takes pointer to i2c_smbus_ioctl_data
|
| 35 |
+
I2C_PEC = 0x0708 # != 0 to use PEC with SMBus
|
| 36 |
+
|
| 37 |
+
# SMBus transfer read or write markers from uapi/linux/i2c.h
|
| 38 |
+
I2C_SMBUS_WRITE = 0
|
| 39 |
+
I2C_SMBUS_READ = 1
|
| 40 |
+
|
| 41 |
+
# Size identifiers uapi/linux/i2c.h
|
| 42 |
+
I2C_SMBUS_QUICK = 0
|
| 43 |
+
I2C_SMBUS_BYTE = 1
|
| 44 |
+
I2C_SMBUS_BYTE_DATA = 2
|
| 45 |
+
I2C_SMBUS_WORD_DATA = 3
|
| 46 |
+
I2C_SMBUS_PROC_CALL = 4
|
| 47 |
+
I2C_SMBUS_BLOCK_DATA = 5 # This isn't supported by Pure-I2C drivers with SMBUS emulation, like those in RaspberryPi, OrangePi, etc :(
|
| 48 |
+
I2C_SMBUS_BLOCK_PROC_CALL = 7 # Like I2C_SMBUS_BLOCK_DATA, it isn't supported by Pure-I2C drivers either.
|
| 49 |
+
I2C_SMBUS_I2C_BLOCK_DATA = 8
|
| 50 |
+
I2C_SMBUS_BLOCK_MAX = 32
|
| 51 |
+
|
| 52 |
+
# To determine what functionality is present (uapi/linux/i2c.h)
|
| 53 |
+
try:
|
| 54 |
+
from enum import IntFlag
|
| 55 |
+
except ImportError:
|
| 56 |
+
IntFlag = int
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
class I2cFunc(IntFlag):
|
| 60 |
+
"""
|
| 61 |
+
These flags identify the operations supported by an I2C/SMBus device.
|
| 62 |
+
|
| 63 |
+
You can test these flags on your `smbus.funcs`
|
| 64 |
+
|
| 65 |
+
On newer python versions, I2cFunc is an IntFlag enum, but it
|
| 66 |
+
falls back to class with a bunch of int constants on older releases.
|
| 67 |
+
"""
|
| 68 |
+
I2C = 0x00000001
|
| 69 |
+
ADDR_10BIT = 0x00000002
|
| 70 |
+
PROTOCOL_MANGLING = 0x00000004 # I2C_M_IGNORE_NAK etc.
|
| 71 |
+
SMBUS_PEC = 0x00000008
|
| 72 |
+
NOSTART = 0x00000010 # I2C_M_NOSTART
|
| 73 |
+
SLAVE = 0x00000020
|
| 74 |
+
SMBUS_BLOCK_PROC_CALL = 0x00008000 # SMBus 2.0
|
| 75 |
+
SMBUS_QUICK = 0x00010000
|
| 76 |
+
SMBUS_READ_BYTE = 0x00020000
|
| 77 |
+
SMBUS_WRITE_BYTE = 0x00040000
|
| 78 |
+
SMBUS_READ_BYTE_DATA = 0x00080000
|
| 79 |
+
SMBUS_WRITE_BYTE_DATA = 0x00100000
|
| 80 |
+
SMBUS_READ_WORD_DATA = 0x00200000
|
| 81 |
+
SMBUS_WRITE_WORD_DATA = 0x00400000
|
| 82 |
+
SMBUS_PROC_CALL = 0x00800000
|
| 83 |
+
SMBUS_READ_BLOCK_DATA = 0x01000000
|
| 84 |
+
SMBUS_WRITE_BLOCK_DATA = 0x02000000
|
| 85 |
+
SMBUS_READ_I2C_BLOCK = 0x04000000 # I2C-like block xfer
|
| 86 |
+
SMBUS_WRITE_I2C_BLOCK = 0x08000000 # w/ 1-byte reg. addr.
|
| 87 |
+
SMBUS_HOST_NOTIFY = 0x10000000
|
| 88 |
+
|
| 89 |
+
SMBUS_BYTE = 0x00060000
|
| 90 |
+
SMBUS_BYTE_DATA = 0x00180000
|
| 91 |
+
SMBUS_WORD_DATA = 0x00600000
|
| 92 |
+
SMBUS_BLOCK_DATA = 0x03000000
|
| 93 |
+
SMBUS_I2C_BLOCK = 0x0c000000
|
| 94 |
+
SMBUS_EMUL = 0x0eff0008
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
# i2c_msg flags from uapi/linux/i2c.h
|
| 98 |
+
I2C_M_RD = 0x0001
|
| 99 |
+
|
| 100 |
+
# Pointer definitions
|
| 101 |
+
LP_c_uint8 = POINTER(c_uint8)
|
| 102 |
+
LP_c_uint16 = POINTER(c_uint16)
|
| 103 |
+
LP_c_uint32 = POINTER(c_uint32)
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
#############################################################
|
| 107 |
+
# Type definitions as in i2c.h
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
class i2c_smbus_data(Array):
|
| 111 |
+
"""
|
| 112 |
+
Adaptation of the i2c_smbus_data union in ``i2c.h``.
|
| 113 |
+
|
| 114 |
+
Data for SMBus messages.
|
| 115 |
+
"""
|
| 116 |
+
_length_ = I2C_SMBUS_BLOCK_MAX + 2
|
| 117 |
+
_type_ = c_uint8
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
class union_i2c_smbus_data(Union):
|
| 121 |
+
_fields_ = [
|
| 122 |
+
("byte", c_uint8),
|
| 123 |
+
("word", c_uint16),
|
| 124 |
+
("block", i2c_smbus_data)
|
| 125 |
+
]
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
union_pointer_type = POINTER(union_i2c_smbus_data)
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
class i2c_smbus_ioctl_data(Structure):
|
| 132 |
+
"""
|
| 133 |
+
As defined in ``i2c-dev.h``.
|
| 134 |
+
"""
|
| 135 |
+
_fields_ = [
|
| 136 |
+
('read_write', c_uint8),
|
| 137 |
+
('command', c_uint8),
|
| 138 |
+
('size', c_uint32),
|
| 139 |
+
('data', union_pointer_type)]
|
| 140 |
+
__slots__ = [name for name, type in _fields_]
|
| 141 |
+
|
| 142 |
+
@staticmethod
|
| 143 |
+
def create(read_write=I2C_SMBUS_READ, command=0, size=I2C_SMBUS_BYTE_DATA):
|
| 144 |
+
u = union_i2c_smbus_data()
|
| 145 |
+
return i2c_smbus_ioctl_data(
|
| 146 |
+
read_write=read_write, command=command, size=size,
|
| 147 |
+
data=union_pointer_type(u))
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
#############################################################
|
| 151 |
+
# Type definitions for i2c_rdwr combined transactions
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
class i2c_msg(Structure):
|
| 155 |
+
"""
|
| 156 |
+
As defined in ``i2c.h``.
|
| 157 |
+
"""
|
| 158 |
+
_fields_ = [
|
| 159 |
+
('addr', c_uint16),
|
| 160 |
+
('flags', c_uint16),
|
| 161 |
+
('len', c_uint16),
|
| 162 |
+
('buf', POINTER(c_char))]
|
| 163 |
+
|
| 164 |
+
def __iter__(self):
|
| 165 |
+
""" Iterator / Generator
|
| 166 |
+
|
| 167 |
+
:return: iterates over :py:attr:`buf`
|
| 168 |
+
:rtype: :py:class:`generator` which returns int values
|
| 169 |
+
"""
|
| 170 |
+
idx = 0
|
| 171 |
+
while idx < self.len:
|
| 172 |
+
yield ord(self.buf[idx])
|
| 173 |
+
idx += 1
|
| 174 |
+
|
| 175 |
+
def __len__(self):
|
| 176 |
+
return self.len
|
| 177 |
+
|
| 178 |
+
def __bytes__(self):
|
| 179 |
+
return string_at(self.buf, self.len)
|
| 180 |
+
|
| 181 |
+
def __repr__(self):
|
| 182 |
+
return 'i2c_msg(%d,%d,%r)' % (self.addr, self.flags, self.__bytes__())
|
| 183 |
+
|
| 184 |
+
def __str__(self):
|
| 185 |
+
s = self.__bytes__()
|
| 186 |
+
# Throw away non-decodable bytes
|
| 187 |
+
s = s.decode(errors="ignore")
|
| 188 |
+
return s
|
| 189 |
+
|
| 190 |
+
@staticmethod
|
| 191 |
+
def read(address, length):
|
| 192 |
+
"""
|
| 193 |
+
Prepares an i2c read transaction.
|
| 194 |
+
|
| 195 |
+
:param address: Slave address.
|
| 196 |
+
:type address: int
|
| 197 |
+
:param length: Number of bytes to read.
|
| 198 |
+
:type length: int
|
| 199 |
+
:return: New :py:class:`i2c_msg` instance for read operation.
|
| 200 |
+
:rtype: :py:class:`i2c_msg`
|
| 201 |
+
"""
|
| 202 |
+
arr = create_string_buffer(length)
|
| 203 |
+
return i2c_msg(
|
| 204 |
+
addr=address, flags=I2C_M_RD, len=length,
|
| 205 |
+
buf=arr)
|
| 206 |
+
|
| 207 |
+
@staticmethod
|
| 208 |
+
def write(address, buf):
|
| 209 |
+
"""
|
| 210 |
+
Prepares an i2c write transaction.
|
| 211 |
+
|
| 212 |
+
:param address: Slave address.
|
| 213 |
+
:type address: int
|
| 214 |
+
:param buf: Bytes to write. Either list of values or str.
|
| 215 |
+
:type buf: list
|
| 216 |
+
:return: New :py:class:`i2c_msg` instance for write operation.
|
| 217 |
+
:rtype: :py:class:`i2c_msg`
|
| 218 |
+
"""
|
| 219 |
+
if sys.version_info.major >= 3:
|
| 220 |
+
if type(buf) is str:
|
| 221 |
+
buf = bytes(map(ord, buf))
|
| 222 |
+
else:
|
| 223 |
+
buf = bytes(buf)
|
| 224 |
+
else:
|
| 225 |
+
if type(buf) is not str:
|
| 226 |
+
buf = ''.join([chr(x) for x in buf])
|
| 227 |
+
arr = create_string_buffer(buf, len(buf))
|
| 228 |
+
return i2c_msg(
|
| 229 |
+
addr=address, flags=0, len=len(arr),
|
| 230 |
+
buf=arr)
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
class i2c_rdwr_ioctl_data(Structure):
|
| 234 |
+
"""
|
| 235 |
+
As defined in ``i2c-dev.h``.
|
| 236 |
+
"""
|
| 237 |
+
_fields_ = [
|
| 238 |
+
('msgs', POINTER(i2c_msg)),
|
| 239 |
+
('nmsgs', c_uint32)
|
| 240 |
+
]
|
| 241 |
+
__slots__ = [name for name, type in _fields_]
|
| 242 |
+
|
| 243 |
+
@staticmethod
|
| 244 |
+
def create(*i2c_msg_instances):
|
| 245 |
+
"""
|
| 246 |
+
Factory method for creating a i2c_rdwr_ioctl_data struct that can
|
| 247 |
+
be called with ``ioctl(fd, I2C_RDWR, data)``.
|
| 248 |
+
|
| 249 |
+
:param i2c_msg_instances: Up to 42 i2c_msg instances
|
| 250 |
+
:rtype: i2c_rdwr_ioctl_data
|
| 251 |
+
"""
|
| 252 |
+
n_msg = len(i2c_msg_instances)
|
| 253 |
+
msg_array = (i2c_msg * n_msg)(*i2c_msg_instances)
|
| 254 |
+
return i2c_rdwr_ioctl_data(
|
| 255 |
+
msgs=msg_array,
|
| 256 |
+
nmsgs=n_msg
|
| 257 |
+
)
|
| 258 |
+
|
| 259 |
+
|
| 260 |
+
#############################################################
|
| 261 |
+
|
| 262 |
+
|
| 263 |
+
class SMBus(object):
|
| 264 |
+
|
| 265 |
+
def __init__(self, bus=None, force=False):
|
| 266 |
+
"""
|
| 267 |
+
Initialize and (optionally) open an i2c bus connection.
|
| 268 |
+
|
| 269 |
+
:param bus: i2c bus number (e.g. 0 or 1)
|
| 270 |
+
or an absolute file path (e.g. `/dev/i2c-42`).
|
| 271 |
+
If not given, a subsequent call to ``open()`` is required.
|
| 272 |
+
:type bus: int or str
|
| 273 |
+
:param force: force using the slave address even when driver is
|
| 274 |
+
already using it.
|
| 275 |
+
:type force: boolean
|
| 276 |
+
"""
|
| 277 |
+
self.fd = None
|
| 278 |
+
self.funcs = I2cFunc(0)
|
| 279 |
+
if bus is not None:
|
| 280 |
+
self.open(bus)
|
| 281 |
+
self.address = None
|
| 282 |
+
self.force = force
|
| 283 |
+
self._force_last = None
|
| 284 |
+
self._pec = 0
|
| 285 |
+
|
| 286 |
+
def __enter__(self):
|
| 287 |
+
"""Enter handler."""
|
| 288 |
+
return self
|
| 289 |
+
|
| 290 |
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
| 291 |
+
"""Exit handler."""
|
| 292 |
+
self.close()
|
| 293 |
+
|
| 294 |
+
def open(self, bus):
|
| 295 |
+
"""
|
| 296 |
+
Open a given i2c bus.
|
| 297 |
+
|
| 298 |
+
:param bus: i2c bus number (e.g. 0 or 1)
|
| 299 |
+
or an absolute file path (e.g. '/dev/i2c-42').
|
| 300 |
+
:type bus: int or str
|
| 301 |
+
:raise TypeError: if type(bus) is not in (int, str)
|
| 302 |
+
"""
|
| 303 |
+
if isinstance(bus, int):
|
| 304 |
+
filepath = "/dev/i2c-{}".format(bus)
|
| 305 |
+
elif isinstance(bus, str):
|
| 306 |
+
filepath = bus
|
| 307 |
+
else:
|
| 308 |
+
raise TypeError("Unexpected type(bus)={}".format(type(bus)))
|
| 309 |
+
|
| 310 |
+
self.fd = os.open(filepath, os.O_RDWR)
|
| 311 |
+
self.funcs = self._get_funcs()
|
| 312 |
+
|
| 313 |
+
def close(self):
|
| 314 |
+
"""
|
| 315 |
+
Close the i2c connection.
|
| 316 |
+
"""
|
| 317 |
+
if self.fd:
|
| 318 |
+
os.close(self.fd)
|
| 319 |
+
self.fd = None
|
| 320 |
+
self._pec = 0
|
| 321 |
+
self.address = None
|
| 322 |
+
self._force_last = None
|
| 323 |
+
|
| 324 |
+
def _get_pec(self):
|
| 325 |
+
return self._pec
|
| 326 |
+
|
| 327 |
+
def enable_pec(self, enable=True):
|
| 328 |
+
"""
|
| 329 |
+
Enable/Disable PEC (Packet Error Checking) - SMBus 1.1 and later
|
| 330 |
+
|
| 331 |
+
:param enable:
|
| 332 |
+
:type enable: Boolean
|
| 333 |
+
"""
|
| 334 |
+
if not (self.funcs & I2cFunc.SMBUS_PEC):
|
| 335 |
+
raise IOError('SMBUS_PEC is not a feature')
|
| 336 |
+
self._pec = int(enable)
|
| 337 |
+
ioctl(self.fd, I2C_PEC, self._pec)
|
| 338 |
+
|
| 339 |
+
pec = property(_get_pec, enable_pec) # Drop-in replacement for smbus member "pec"
|
| 340 |
+
"""Get and set SMBus PEC. 0 = disabled (default), 1 = enabled."""
|
| 341 |
+
|
| 342 |
+
def _set_address(self, address, force=None):
|
| 343 |
+
"""
|
| 344 |
+
Set i2c slave address to use for subsequent calls.
|
| 345 |
+
|
| 346 |
+
:param address:
|
| 347 |
+
:type address: int
|
| 348 |
+
:param force:
|
| 349 |
+
:type force: Boolean
|
| 350 |
+
"""
|
| 351 |
+
force = force if force is not None else self.force
|
| 352 |
+
if self.address != address or self._force_last != force:
|
| 353 |
+
if force is True:
|
| 354 |
+
ioctl(self.fd, I2C_SLAVE_FORCE, address)
|
| 355 |
+
else:
|
| 356 |
+
ioctl(self.fd, I2C_SLAVE, address)
|
| 357 |
+
self.address = address
|
| 358 |
+
self._force_last = force
|
| 359 |
+
|
| 360 |
+
def _get_funcs(self):
|
| 361 |
+
"""
|
| 362 |
+
Returns a 32-bit value stating supported I2C functions.
|
| 363 |
+
|
| 364 |
+
:rtype: int
|
| 365 |
+
"""
|
| 366 |
+
f = c_uint32()
|
| 367 |
+
ioctl(self.fd, I2C_FUNCS, f)
|
| 368 |
+
return f.value
|
| 369 |
+
|
| 370 |
+
def write_quick(self, i2c_addr, force=None):
|
| 371 |
+
"""
|
| 372 |
+
Perform quick transaction. Throws IOError if unsuccessful.
|
| 373 |
+
:param i2c_addr: i2c address
|
| 374 |
+
:type i2c_addr: int
|
| 375 |
+
:param force:
|
| 376 |
+
:type force: Boolean
|
| 377 |
+
"""
|
| 378 |
+
self._set_address(i2c_addr, force=force)
|
| 379 |
+
msg = i2c_smbus_ioctl_data.create(
|
| 380 |
+
read_write=I2C_SMBUS_WRITE, command=0, size=I2C_SMBUS_QUICK)
|
| 381 |
+
ioctl(self.fd, I2C_SMBUS, msg)
|
| 382 |
+
|
| 383 |
+
def read_byte(self, i2c_addr, force=None):
|
| 384 |
+
"""
|
| 385 |
+
Read a single byte from a device.
|
| 386 |
+
|
| 387 |
+
:rtype: int
|
| 388 |
+
:param i2c_addr: i2c address
|
| 389 |
+
:type i2c_addr: int
|
| 390 |
+
:param force:
|
| 391 |
+
:type force: Boolean
|
| 392 |
+
:return: Read byte value
|
| 393 |
+
"""
|
| 394 |
+
self._set_address(i2c_addr, force=force)
|
| 395 |
+
msg = i2c_smbus_ioctl_data.create(
|
| 396 |
+
read_write=I2C_SMBUS_READ, command=0, size=I2C_SMBUS_BYTE
|
| 397 |
+
)
|
| 398 |
+
ioctl(self.fd, I2C_SMBUS, msg)
|
| 399 |
+
return msg.data.contents.byte
|
| 400 |
+
|
| 401 |
+
def write_byte(self, i2c_addr, value, force=None):
|
| 402 |
+
"""
|
| 403 |
+
Write a single byte to a device.
|
| 404 |
+
|
| 405 |
+
:param i2c_addr: i2c address
|
| 406 |
+
:type i2c_addr: int
|
| 407 |
+
:param value: value to write
|
| 408 |
+
:type value: int
|
| 409 |
+
:param force:
|
| 410 |
+
:type force: Boolean
|
| 411 |
+
"""
|
| 412 |
+
self._set_address(i2c_addr, force=force)
|
| 413 |
+
msg = i2c_smbus_ioctl_data.create(
|
| 414 |
+
read_write=I2C_SMBUS_WRITE, command=value, size=I2C_SMBUS_BYTE
|
| 415 |
+
)
|
| 416 |
+
ioctl(self.fd, I2C_SMBUS, msg)
|
| 417 |
+
|
| 418 |
+
def read_byte_data(self, i2c_addr, register, force=None):
|
| 419 |
+
"""
|
| 420 |
+
Read a single byte from a designated register.
|
| 421 |
+
|
| 422 |
+
:param i2c_addr: i2c address
|
| 423 |
+
:type i2c_addr: int
|
| 424 |
+
:param register: Register to read
|
| 425 |
+
:type register: int
|
| 426 |
+
:param force:
|
| 427 |
+
:type force: Boolean
|
| 428 |
+
:return: Read byte value
|
| 429 |
+
:rtype: int
|
| 430 |
+
"""
|
| 431 |
+
self._set_address(i2c_addr, force=force)
|
| 432 |
+
msg = i2c_smbus_ioctl_data.create(
|
| 433 |
+
read_write=I2C_SMBUS_READ, command=register, size=I2C_SMBUS_BYTE_DATA
|
| 434 |
+
)
|
| 435 |
+
ioctl(self.fd, I2C_SMBUS, msg)
|
| 436 |
+
return msg.data.contents.byte
|
| 437 |
+
|
| 438 |
+
def write_byte_data(self, i2c_addr, register, value, force=None):
|
| 439 |
+
"""
|
| 440 |
+
Write a byte to a given register.
|
| 441 |
+
|
| 442 |
+
:param i2c_addr: i2c address
|
| 443 |
+
:type i2c_addr: int
|
| 444 |
+
:param register: Register to write to
|
| 445 |
+
:type register: int
|
| 446 |
+
:param value: Byte value to transmit
|
| 447 |
+
:type value: int
|
| 448 |
+
:param force:
|
| 449 |
+
:type force: Boolean
|
| 450 |
+
:rtype: None
|
| 451 |
+
"""
|
| 452 |
+
self._set_address(i2c_addr, force=force)
|
| 453 |
+
msg = i2c_smbus_ioctl_data.create(
|
| 454 |
+
read_write=I2C_SMBUS_WRITE, command=register, size=I2C_SMBUS_BYTE_DATA
|
| 455 |
+
)
|
| 456 |
+
msg.data.contents.byte = value
|
| 457 |
+
ioctl(self.fd, I2C_SMBUS, msg)
|
| 458 |
+
|
| 459 |
+
def read_word_data(self, i2c_addr, register, force=None):
|
| 460 |
+
"""
|
| 461 |
+
Read a single word (2 bytes) from a given register.
|
| 462 |
+
|
| 463 |
+
:param i2c_addr: i2c address
|
| 464 |
+
:type i2c_addr: int
|
| 465 |
+
:param register: Register to read
|
| 466 |
+
:type register: int
|
| 467 |
+
:param force:
|
| 468 |
+
:type force: Boolean
|
| 469 |
+
:return: 2-byte word
|
| 470 |
+
:rtype: int
|
| 471 |
+
"""
|
| 472 |
+
self._set_address(i2c_addr, force=force)
|
| 473 |
+
msg = i2c_smbus_ioctl_data.create(
|
| 474 |
+
read_write=I2C_SMBUS_READ, command=register, size=I2C_SMBUS_WORD_DATA
|
| 475 |
+
)
|
| 476 |
+
ioctl(self.fd, I2C_SMBUS, msg)
|
| 477 |
+
return msg.data.contents.word
|
| 478 |
+
|
| 479 |
+
def write_word_data(self, i2c_addr, register, value, force=None):
|
| 480 |
+
"""
|
| 481 |
+
Write a single word (2 bytes) to a given register.
|
| 482 |
+
|
| 483 |
+
:param i2c_addr: i2c address
|
| 484 |
+
:type i2c_addr: int
|
| 485 |
+
:param register: Register to write to
|
| 486 |
+
:type register: int
|
| 487 |
+
:param value: Word value to transmit
|
| 488 |
+
:type value: int
|
| 489 |
+
:param force:
|
| 490 |
+
:type force: Boolean
|
| 491 |
+
:rtype: None
|
| 492 |
+
"""
|
| 493 |
+
self._set_address(i2c_addr, force=force)
|
| 494 |
+
msg = i2c_smbus_ioctl_data.create(
|
| 495 |
+
read_write=I2C_SMBUS_WRITE, command=register, size=I2C_SMBUS_WORD_DATA
|
| 496 |
+
)
|
| 497 |
+
msg.data.contents.word = value
|
| 498 |
+
ioctl(self.fd, I2C_SMBUS, msg)
|
| 499 |
+
|
| 500 |
+
def process_call(self, i2c_addr, register, value, force=None):
|
| 501 |
+
"""
|
| 502 |
+
Executes a SMBus Process Call, sending a 16-bit value and receiving a 16-bit response
|
| 503 |
+
|
| 504 |
+
:param i2c_addr: i2c address
|
| 505 |
+
:type i2c_addr: int
|
| 506 |
+
:param register: Register to read/write to
|
| 507 |
+
:type register: int
|
| 508 |
+
:param value: Word value to transmit
|
| 509 |
+
:type value: int
|
| 510 |
+
:param force:
|
| 511 |
+
:type force: Boolean
|
| 512 |
+
:rtype: int
|
| 513 |
+
"""
|
| 514 |
+
self._set_address(i2c_addr, force=force)
|
| 515 |
+
msg = i2c_smbus_ioctl_data.create(
|
| 516 |
+
read_write=I2C_SMBUS_WRITE, command=register, size=I2C_SMBUS_PROC_CALL
|
| 517 |
+
)
|
| 518 |
+
msg.data.contents.word = value
|
| 519 |
+
ioctl(self.fd, I2C_SMBUS, msg)
|
| 520 |
+
return msg.data.contents.word
|
| 521 |
+
|
| 522 |
+
def read_block_data(self, i2c_addr, register, force=None):
|
| 523 |
+
"""
|
| 524 |
+
Read a block of up to 32-bytes from a given register.
|
| 525 |
+
|
| 526 |
+
:param i2c_addr: i2c address
|
| 527 |
+
:type i2c_addr: int
|
| 528 |
+
:param register: Start register
|
| 529 |
+
:type register: int
|
| 530 |
+
:param force:
|
| 531 |
+
:type force: Boolean
|
| 532 |
+
:return: List of bytes
|
| 533 |
+
:rtype: list
|
| 534 |
+
"""
|
| 535 |
+
self._set_address(i2c_addr, force=force)
|
| 536 |
+
msg = i2c_smbus_ioctl_data.create(
|
| 537 |
+
read_write=I2C_SMBUS_READ, command=register, size=I2C_SMBUS_BLOCK_DATA
|
| 538 |
+
)
|
| 539 |
+
ioctl(self.fd, I2C_SMBUS, msg)
|
| 540 |
+
length = msg.data.contents.block[0]
|
| 541 |
+
return msg.data.contents.block[1:length + 1]
|
| 542 |
+
|
| 543 |
+
def write_block_data(self, i2c_addr, register, data, force=None):
|
| 544 |
+
"""
|
| 545 |
+
Write a block of byte data to a given register.
|
| 546 |
+
|
| 547 |
+
:param i2c_addr: i2c address
|
| 548 |
+
:type i2c_addr: int
|
| 549 |
+
:param register: Start register
|
| 550 |
+
:type register: int
|
| 551 |
+
:param data: List of bytes
|
| 552 |
+
:type data: list
|
| 553 |
+
:param force:
|
| 554 |
+
:type force: Boolean
|
| 555 |
+
:rtype: None
|
| 556 |
+
"""
|
| 557 |
+
length = len(data)
|
| 558 |
+
if length > I2C_SMBUS_BLOCK_MAX:
|
| 559 |
+
raise ValueError("Data length cannot exceed %d bytes" % I2C_SMBUS_BLOCK_MAX)
|
| 560 |
+
self._set_address(i2c_addr, force=force)
|
| 561 |
+
msg = i2c_smbus_ioctl_data.create(
|
| 562 |
+
read_write=I2C_SMBUS_WRITE, command=register, size=I2C_SMBUS_BLOCK_DATA
|
| 563 |
+
)
|
| 564 |
+
msg.data.contents.block[0] = length
|
| 565 |
+
msg.data.contents.block[1:length + 1] = data
|
| 566 |
+
ioctl(self.fd, I2C_SMBUS, msg)
|
| 567 |
+
|
| 568 |
+
def block_process_call(self, i2c_addr, register, data, force=None):
|
| 569 |
+
"""
|
| 570 |
+
Executes a SMBus Block Process Call, sending a variable-size data
|
| 571 |
+
block and receiving another variable-size response
|
| 572 |
+
|
| 573 |
+
:param i2c_addr: i2c address
|
| 574 |
+
:type i2c_addr: int
|
| 575 |
+
:param register: Register to read/write to
|
| 576 |
+
:type register: int
|
| 577 |
+
:param data: List of bytes
|
| 578 |
+
:type data: list
|
| 579 |
+
:param force:
|
| 580 |
+
:type force: Boolean
|
| 581 |
+
:return: List of bytes
|
| 582 |
+
:rtype: list
|
| 583 |
+
"""
|
| 584 |
+
length = len(data)
|
| 585 |
+
if length > I2C_SMBUS_BLOCK_MAX:
|
| 586 |
+
raise ValueError("Data length cannot exceed %d bytes" % I2C_SMBUS_BLOCK_MAX)
|
| 587 |
+
self._set_address(i2c_addr, force=force)
|
| 588 |
+
msg = i2c_smbus_ioctl_data.create(
|
| 589 |
+
read_write=I2C_SMBUS_WRITE, command=register, size=I2C_SMBUS_BLOCK_PROC_CALL
|
| 590 |
+
)
|
| 591 |
+
msg.data.contents.block[0] = length
|
| 592 |
+
msg.data.contents.block[1:length + 1] = data
|
| 593 |
+
ioctl(self.fd, I2C_SMBUS, msg)
|
| 594 |
+
length = msg.data.contents.block[0]
|
| 595 |
+
return msg.data.contents.block[1:length + 1]
|
| 596 |
+
|
| 597 |
+
def read_i2c_block_data(self, i2c_addr, register, length, force=None):
|
| 598 |
+
"""
|
| 599 |
+
Read a block of byte data from a given register.
|
| 600 |
+
|
| 601 |
+
:param i2c_addr: i2c address
|
| 602 |
+
:type i2c_addr: int
|
| 603 |
+
:param register: Start register
|
| 604 |
+
:type register: int
|
| 605 |
+
:param length: Desired block length
|
| 606 |
+
:type length: int
|
| 607 |
+
:param force:
|
| 608 |
+
:type force: Boolean
|
| 609 |
+
:return: List of bytes
|
| 610 |
+
:rtype: list
|
| 611 |
+
"""
|
| 612 |
+
if length > I2C_SMBUS_BLOCK_MAX:
|
| 613 |
+
raise ValueError("Desired block length over %d bytes" % I2C_SMBUS_BLOCK_MAX)
|
| 614 |
+
self._set_address(i2c_addr, force=force)
|
| 615 |
+
msg = i2c_smbus_ioctl_data.create(
|
| 616 |
+
read_write=I2C_SMBUS_READ, command=register, size=I2C_SMBUS_I2C_BLOCK_DATA
|
| 617 |
+
)
|
| 618 |
+
msg.data.contents.byte = length
|
| 619 |
+
ioctl(self.fd, I2C_SMBUS, msg)
|
| 620 |
+
return msg.data.contents.block[1:length + 1]
|
| 621 |
+
|
| 622 |
+
def write_i2c_block_data(self, i2c_addr, register, data, force=None):
|
| 623 |
+
"""
|
| 624 |
+
Write a block of byte data to a given register.
|
| 625 |
+
|
| 626 |
+
:param i2c_addr: i2c address
|
| 627 |
+
:type i2c_addr: int
|
| 628 |
+
:param register: Start register
|
| 629 |
+
:type register: int
|
| 630 |
+
:param data: List of bytes
|
| 631 |
+
:type data: list
|
| 632 |
+
:param force:
|
| 633 |
+
:type force: Boolean
|
| 634 |
+
:rtype: None
|
| 635 |
+
"""
|
| 636 |
+
length = len(data)
|
| 637 |
+
if length > I2C_SMBUS_BLOCK_MAX:
|
| 638 |
+
raise ValueError("Data length cannot exceed %d bytes" % I2C_SMBUS_BLOCK_MAX)
|
| 639 |
+
self._set_address(i2c_addr, force=force)
|
| 640 |
+
msg = i2c_smbus_ioctl_data.create(
|
| 641 |
+
read_write=I2C_SMBUS_WRITE, command=register, size=I2C_SMBUS_I2C_BLOCK_DATA
|
| 642 |
+
)
|
| 643 |
+
msg.data.contents.block[0] = length
|
| 644 |
+
msg.data.contents.block[1:length + 1] = data
|
| 645 |
+
ioctl(self.fd, I2C_SMBUS, msg)
|
| 646 |
+
|
| 647 |
+
def i2c_rdwr(self, *i2c_msgs):
|
| 648 |
+
"""
|
| 649 |
+
Combine a series of i2c read and write operations in a single
|
| 650 |
+
transaction (with repeated start bits but no stop bits in between).
|
| 651 |
+
|
| 652 |
+
This method takes i2c_msg instances as input, which must be created
|
| 653 |
+
first with :py:meth:`i2c_msg.read` or :py:meth:`i2c_msg.write`.
|
| 654 |
+
|
| 655 |
+
:param i2c_msgs: One or more i2c_msg class instances.
|
| 656 |
+
:type i2c_msgs: i2c_msg
|
| 657 |
+
:rtype: None
|
| 658 |
+
"""
|
| 659 |
+
ioctl_data = i2c_rdwr_ioctl_data.create(*i2c_msgs)
|
| 660 |
+
ioctl(self.fd, I2C_RDWR, ioctl_data)
|
scripts/lib/smbus2/smbus2.pyi
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from enum import IntFlag
|
| 2 |
+
from typing import Optional, Sequence, List, Type, SupportsBytes, Iterable
|
| 3 |
+
from typing import Union as _UnionT
|
| 4 |
+
from types import TracebackType
|
| 5 |
+
from ctypes import c_uint32, c_uint8, c_uint16, pointer, Structure, Array, Union
|
| 6 |
+
|
| 7 |
+
I2C_SLAVE: int
|
| 8 |
+
I2C_SLAVE_FORCE: int
|
| 9 |
+
I2C_FUNCS: int
|
| 10 |
+
I2C_RDWR: int
|
| 11 |
+
I2C_SMBUS: int
|
| 12 |
+
I2C_PEC: int
|
| 13 |
+
I2C_SMBUS_WRITE: int
|
| 14 |
+
I2C_SMBUS_READ: int
|
| 15 |
+
I2C_SMBUS_QUICK: int
|
| 16 |
+
I2C_SMBUS_BYTE: int
|
| 17 |
+
I2C_SMBUS_BYTE_DATA: int
|
| 18 |
+
I2C_SMBUS_WORD_DATA: int
|
| 19 |
+
I2C_SMBUS_PROC_CALL: int
|
| 20 |
+
I2C_SMBUS_BLOCK_DATA: int
|
| 21 |
+
I2C_SMBUS_BLOCK_PROC_CALL: int
|
| 22 |
+
I2C_SMBUS_I2C_BLOCK_DATA: int
|
| 23 |
+
I2C_SMBUS_BLOCK_MAX: int
|
| 24 |
+
|
| 25 |
+
class I2cFunc(IntFlag):
|
| 26 |
+
I2C = ...
|
| 27 |
+
ADDR_10BIT = ...
|
| 28 |
+
PROTOCOL_MANGLING = ...
|
| 29 |
+
SMBUS_PEC = ...
|
| 30 |
+
NOSTART = ...
|
| 31 |
+
SLAVE = ...
|
| 32 |
+
SMBUS_BLOCK_PROC_CALL = ...
|
| 33 |
+
SMBUS_QUICK = ...
|
| 34 |
+
SMBUS_READ_BYTE = ...
|
| 35 |
+
SMBUS_WRITE_BYTE = ...
|
| 36 |
+
SMBUS_READ_BYTE_DATA = ...
|
| 37 |
+
SMBUS_WRITE_BYTE_DATA = ...
|
| 38 |
+
SMBUS_READ_WORD_DATA = ...
|
| 39 |
+
SMBUS_WRITE_WORD_DATA = ...
|
| 40 |
+
SMBUS_PROC_CALL = ...
|
| 41 |
+
SMBUS_READ_BLOCK_DATA = ...
|
| 42 |
+
SMBUS_WRITE_BLOCK_DATA = ...
|
| 43 |
+
SMBUS_READ_I2C_BLOCK = ...
|
| 44 |
+
SMBUS_WRITE_I2C_BLOCK = ...
|
| 45 |
+
SMBUS_HOST_NOTIFY = ...
|
| 46 |
+
SMBUS_BYTE = ...
|
| 47 |
+
SMBUS_BYTE_DATA = ...
|
| 48 |
+
SMBUS_WORD_DATA = ...
|
| 49 |
+
SMBUS_BLOCK_DATA = ...
|
| 50 |
+
SMBUS_I2C_BLOCK = ...
|
| 51 |
+
SMBUS_EMUL = ...
|
| 52 |
+
|
| 53 |
+
I2C_M_RD: int
|
| 54 |
+
LP_c_uint8: Type[pointer[c_uint8]]
|
| 55 |
+
LP_c_uint16: Type[pointer[c_uint16]]
|
| 56 |
+
LP_c_uint32: Type[pointer[c_uint32]]
|
| 57 |
+
|
| 58 |
+
class i2c_smbus_data(Array): ...
|
| 59 |
+
class union_i2c_smbus_data(Union): ...
|
| 60 |
+
|
| 61 |
+
union_pointer_type: pointer[union_i2c_smbus_data]
|
| 62 |
+
|
| 63 |
+
class i2c_smbus_ioctl_data(Structure):
|
| 64 |
+
@staticmethod
|
| 65 |
+
def create(
|
| 66 |
+
read_write: int = ..., command: int = ..., size: int = ...
|
| 67 |
+
) -> "i2c_smbus_ioctl_data": ...
|
| 68 |
+
|
| 69 |
+
class i2c_msg(Structure):
|
| 70 |
+
def __iter__(self) -> int: ...
|
| 71 |
+
def __len__(self) -> int: ...
|
| 72 |
+
def __bytes__(self) -> str: ...
|
| 73 |
+
@staticmethod
|
| 74 |
+
def read(address: int, length: int) -> "i2c_msg": ...
|
| 75 |
+
@staticmethod
|
| 76 |
+
def write(address: int, buf: _UnionT[str, Iterable[int], SupportsBytes]) -> "i2c_msg": ...
|
| 77 |
+
|
| 78 |
+
class i2c_rdwr_ioctl_data(Structure):
|
| 79 |
+
@staticmethod
|
| 80 |
+
def create(*i2c_msg_instances: Sequence[i2c_msg]) -> "i2c_rdwr_ioctl_data": ...
|
| 81 |
+
|
| 82 |
+
class SMBus:
|
| 83 |
+
fd: Optional[int] = ...
|
| 84 |
+
funcs: I2cFunc = ...
|
| 85 |
+
address: Optional[int] = ...
|
| 86 |
+
force: bool = ...
|
| 87 |
+
pec: int = ...
|
| 88 |
+
def __init__(
|
| 89 |
+
self, bus: _UnionT[None, int, str] = ..., force: bool = ...
|
| 90 |
+
) -> None: ...
|
| 91 |
+
def __enter__(self) -> "SMBus": ...
|
| 92 |
+
def __exit__(
|
| 93 |
+
self,
|
| 94 |
+
exc_type: Optional[Type[BaseException]],
|
| 95 |
+
exc_val: Optional[BaseException],
|
| 96 |
+
exc_tb: Optional[TracebackType],
|
| 97 |
+
) -> None: ...
|
| 98 |
+
def open(self, bus: _UnionT[int, str]) -> None: ...
|
| 99 |
+
def close(self) -> None: ...
|
| 100 |
+
def enable_pec(self, enable: bool = ...) -> None: ...
|
| 101 |
+
def write_quick(self, i2c_addr: int, force: Optional[bool] = ...) -> None: ...
|
| 102 |
+
def read_byte(self, i2c_addr: int, force: Optional[bool] = ...) -> int: ...
|
| 103 |
+
def write_byte(
|
| 104 |
+
self, i2c_addr: int, value: int, force: Optional[bool] = ...
|
| 105 |
+
) -> None: ...
|
| 106 |
+
def read_byte_data(
|
| 107 |
+
self, i2c_addr: int, register: int, force: Optional[bool] = ...
|
| 108 |
+
) -> int: ...
|
| 109 |
+
def write_byte_data(
|
| 110 |
+
self, i2c_addr: int, register: int, value: int, force: Optional[bool] = ...
|
| 111 |
+
) -> None: ...
|
| 112 |
+
def read_word_data(
|
| 113 |
+
self, i2c_addr: int, register: int, force: Optional[bool] = ...
|
| 114 |
+
) -> int: ...
|
| 115 |
+
def write_word_data(
|
| 116 |
+
self, i2c_addr: int, register: int, value: int, force: Optional[bool] = ...
|
| 117 |
+
) -> None: ...
|
| 118 |
+
def process_call(
|
| 119 |
+
self, i2c_addr: int, register: int, value: int, force: Optional[bool] = ...
|
| 120 |
+
): ...
|
| 121 |
+
def read_block_data(
|
| 122 |
+
self, i2c_addr: int, register: int, force: Optional[bool] = ...
|
| 123 |
+
) -> List[int]: ...
|
| 124 |
+
def write_block_data(
|
| 125 |
+
self,
|
| 126 |
+
i2c_addr: int,
|
| 127 |
+
register: int,
|
| 128 |
+
data: Sequence[int],
|
| 129 |
+
force: Optional[bool] = ...,
|
| 130 |
+
) -> None: ...
|
| 131 |
+
def block_process_call(
|
| 132 |
+
self,
|
| 133 |
+
i2c_addr: int,
|
| 134 |
+
register: int,
|
| 135 |
+
data: Sequence[int],
|
| 136 |
+
force: Optional[bool] = ...,
|
| 137 |
+
) -> List[int]: ...
|
| 138 |
+
def read_i2c_block_data(
|
| 139 |
+
self, i2c_addr: int, register: int, length: int, force: Optional[bool] = ...
|
| 140 |
+
) -> List[int]: ...
|
| 141 |
+
def write_i2c_block_data(
|
| 142 |
+
self,
|
| 143 |
+
i2c_addr: int,
|
| 144 |
+
register: int,
|
| 145 |
+
data: Sequence[int],
|
| 146 |
+
force: Optional[bool] = ...,
|
| 147 |
+
) -> None: ...
|
| 148 |
+
def i2c_rdwr(self, *i2c_msgs: i2c_msg) -> None: ...
|
scripts/lib/ufastrsa/__init__.py
ADDED
|
File without changes
|
scripts/lib/ufastrsa/genprime.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from ufastrsa import srandom
|
| 2 |
+
|
| 3 |
+
try:
|
| 4 |
+
from _crypto import NUMBER as tomsfastmath
|
| 5 |
+
|
| 6 |
+
pow3_ = tomsfastmath.exptmod
|
| 7 |
+
invmod_ = tomsfastmath.invmod
|
| 8 |
+
generate_prime_ = tomsfastmath.generate_prime
|
| 9 |
+
|
| 10 |
+
def genprime(num=1024, test=25, safe=False):
|
| 11 |
+
return generate_prime_(num, test, safe)
|
| 12 |
+
|
| 13 |
+
except ImportError:
|
| 14 |
+
pow3_ = pow
|
| 15 |
+
|
| 16 |
+
def invmod_(a, b):
|
| 17 |
+
c, d, e, f, g = 1, 0, 0, 1, b
|
| 18 |
+
while b:
|
| 19 |
+
q = a // b
|
| 20 |
+
a, c, d, b, e, f = b, e, f, a - q * b, c - q * e, d - q * f
|
| 21 |
+
assert a >= 0 and c % g >= 0
|
| 22 |
+
return a == 1 and c % g or 0
|
| 23 |
+
|
| 24 |
+
def miller_rabin_pass(a, n):
|
| 25 |
+
n_minus_one = n - 1
|
| 26 |
+
s, d = get_lowest_set_bit(n_minus_one)
|
| 27 |
+
a_to_power = pow3(a, d, n)
|
| 28 |
+
if a_to_power == 1:
|
| 29 |
+
return True
|
| 30 |
+
for i in range(s):
|
| 31 |
+
if a_to_power == n_minus_one:
|
| 32 |
+
return True
|
| 33 |
+
a_to_power = pow3(a_to_power, 2, n)
|
| 34 |
+
if a_to_power == n_minus_one:
|
| 35 |
+
return True
|
| 36 |
+
return False
|
| 37 |
+
|
| 38 |
+
class MillerRabinTest:
|
| 39 |
+
def __init__(self, randint, repeat):
|
| 40 |
+
self.randint = randint
|
| 41 |
+
self.repeat = repeat
|
| 42 |
+
|
| 43 |
+
def __call__(self, n):
|
| 44 |
+
randint = self.randint
|
| 45 |
+
n_minus_one = n - 1
|
| 46 |
+
for repeat in range(self.repeat):
|
| 47 |
+
a = randint(1, n_minus_one)
|
| 48 |
+
if not miller_rabin_pass(a, n):
|
| 49 |
+
return False
|
| 50 |
+
return True
|
| 51 |
+
|
| 52 |
+
class GenPrime:
|
| 53 |
+
def __init__(self, getrandbits, testfn):
|
| 54 |
+
self.getrandbits = getrandbits
|
| 55 |
+
self.testfn = testfn
|
| 56 |
+
|
| 57 |
+
def __call__(self, bits):
|
| 58 |
+
getrandbits = self.getrandbits
|
| 59 |
+
testfn = self.testfn
|
| 60 |
+
while True:
|
| 61 |
+
p = (1 << (bits - 1)) | getrandbits(bits - 1) | 1
|
| 62 |
+
if p % 3 != 0 and p % 5 != 0 and p % 7 != 0 and testfn(p):
|
| 63 |
+
break
|
| 64 |
+
return p
|
| 65 |
+
|
| 66 |
+
miller_rabin_test = MillerRabinTest(srandom.randint, 25)
|
| 67 |
+
genprime = GenPrime(srandom.getrandbits, miller_rabin_test)
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def pow3(x, y, z):
|
| 71 |
+
return pow3_(x, y, z)
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def invmod(a, b):
|
| 75 |
+
return invmod_(a, b)
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def get_lowest_set_bit(n):
|
| 79 |
+
i = 0
|
| 80 |
+
while n:
|
| 81 |
+
if n & 1:
|
| 82 |
+
return i, n
|
| 83 |
+
n >>= 1
|
| 84 |
+
i += 1
|
| 85 |
+
raise "Error"
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def gcd(a, b):
|
| 89 |
+
while b:
|
| 90 |
+
a, b = b, a % b
|
| 91 |
+
return a
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def get_bit_length(n):
|
| 95 |
+
return srandom.get_bit_length(n)
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
class GenRSA:
|
| 99 |
+
def __init__(self, genprime):
|
| 100 |
+
self.genprime = genprime
|
| 101 |
+
|
| 102 |
+
def __call__(self, bits, e=None, with_crt=False):
|
| 103 |
+
pbits = (bits + 1) >> 1
|
| 104 |
+
qbits = bits - pbits
|
| 105 |
+
if e is None:
|
| 106 |
+
e = 65537
|
| 107 |
+
elif e < 0:
|
| 108 |
+
e = self.genprime(-e)
|
| 109 |
+
while True:
|
| 110 |
+
p = self.genprime(pbits)
|
| 111 |
+
if gcd(e, p - 1) == 1:
|
| 112 |
+
break
|
| 113 |
+
while True:
|
| 114 |
+
while True:
|
| 115 |
+
q = self.genprime(qbits)
|
| 116 |
+
if gcd(e, q - 1) == 1 and p != q:
|
| 117 |
+
break
|
| 118 |
+
n = p * q
|
| 119 |
+
if get_bit_length(n) == bits:
|
| 120 |
+
break
|
| 121 |
+
p = max(p, q)
|
| 122 |
+
p_minus_1 = p - 1
|
| 123 |
+
q_minus_1 = q - 1
|
| 124 |
+
phi = p_minus_1 * q_minus_1
|
| 125 |
+
d = invmod(e, phi)
|
| 126 |
+
if with_crt:
|
| 127 |
+
dp = d % p_minus_1
|
| 128 |
+
dq = d % q_minus_1
|
| 129 |
+
qinv = invmod(q, p)
|
| 130 |
+
assert qinv < p
|
| 131 |
+
return bits, n, e, d, p, q, dp, dq, qinv
|
| 132 |
+
else:
|
| 133 |
+
return bits, n, e, d
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
genrsa = GenRSA(genprime)
|
scripts/lib/ufastrsa/rsa.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from ufastrsa.genprime import pow3
|
| 2 |
+
from ufastrsa.srandom import rndsrcnz
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
class RSA:
|
| 6 |
+
def __init__(self, bits, n=None, e=None, d=None):
|
| 7 |
+
self.bits = bits
|
| 8 |
+
self.bytes = (bits + 7) >> 3
|
| 9 |
+
self.n = n
|
| 10 |
+
self.e = e
|
| 11 |
+
self.d = d
|
| 12 |
+
self.rndsrcnz = rndsrcnz
|
| 13 |
+
|
| 14 |
+
def pkcs_sign(self, value):
|
| 15 |
+
len_padding = self.bytes - 3 - len(value)
|
| 16 |
+
assert len_padding >= 0, len_padding
|
| 17 |
+
base = int.from_bytes(
|
| 18 |
+
b"\x00\x01" + len_padding * b"\xff" + b"\x00" + value, "big"
|
| 19 |
+
)
|
| 20 |
+
return int.to_bytes(pow3(base, self.d, self.n), self.bytes, "big")
|
| 21 |
+
|
| 22 |
+
def pkcs_verify(self, value):
|
| 23 |
+
assert len(value) == self.bytes
|
| 24 |
+
signed = int.to_bytes(
|
| 25 |
+
pow3(int.from_bytes(value, "big"), self.e, self.n), self.bytes, "big"
|
| 26 |
+
)
|
| 27 |
+
idx = signed.find(b"\0", 1)
|
| 28 |
+
assert idx != -1 and signed[:idx] == b"\x00\x01" + (idx - 2) * b"\xff"
|
| 29 |
+
return signed[idx + 1 :]
|
| 30 |
+
|
| 31 |
+
def pkcs_encrypt(self, value):
|
| 32 |
+
len_padding = self.bytes - 3 - len(value)
|
| 33 |
+
assert len_padding >= 0
|
| 34 |
+
base = int.from_bytes(
|
| 35 |
+
b"\x00\x02" + self.rndsrcnz(len_padding) + b"\x00" + value, "big"
|
| 36 |
+
)
|
| 37 |
+
return int.to_bytes(pow3(base, self.e, self.n), self.bytes, "big")
|
| 38 |
+
|
| 39 |
+
def pkcs_decrypt(self, value):
|
| 40 |
+
assert len(value) == self.bytes
|
| 41 |
+
decrypted = int.to_bytes(
|
| 42 |
+
pow3(int.from_bytes(value, "big"), self.d, self.n), self.bytes, "big"
|
| 43 |
+
)
|
| 44 |
+
idx = decrypted.find(b"\0", 2)
|
| 45 |
+
assert idx != -1 and decrypted[:2] == b"\x00\x02"
|
| 46 |
+
return decrypted[idx + 1 :]
|
scripts/lib/ufastrsa/srandom.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from functools import reduce
|
| 2 |
+
from os import urandom
|
| 3 |
+
|
| 4 |
+
from ufastrsa.util import get_bit_length
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class Random:
|
| 8 |
+
def __init__(self, seed=None, rndsrc=None):
|
| 9 |
+
if rndsrc is None:
|
| 10 |
+
rndsrc = urandom
|
| 11 |
+
self.rndsrc = rndsrc
|
| 12 |
+
|
| 13 |
+
def getrandbits(self, k):
|
| 14 |
+
if not k >= 0:
|
| 15 |
+
raise ValueError("number of bits must be >= 0")
|
| 16 |
+
return reduce(
|
| 17 |
+
lambda x, y: x << 8 | y,
|
| 18 |
+
self.rndsrc(k >> 3),
|
| 19 |
+
int.from_bytes(self.rndsrc(1), "little") & ((1 << (k & 7)) - 1),
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
def randint(self, a, b):
|
| 23 |
+
if a > b:
|
| 24 |
+
raise ValueError("empty range for randint(): %d, %d" % (a, b))
|
| 25 |
+
c = 1 + b - a
|
| 26 |
+
k = get_bit_length(c - 1)
|
| 27 |
+
while True:
|
| 28 |
+
r = self.getrandbits(k)
|
| 29 |
+
if r <= c:
|
| 30 |
+
break
|
| 31 |
+
return a + r
|
| 32 |
+
|
| 33 |
+
def rndsrcnz(self, size):
|
| 34 |
+
rv = self.rndsrc(size).replace(b"\x00", b"")
|
| 35 |
+
mv = size - len(rv)
|
| 36 |
+
while mv > 0:
|
| 37 |
+
rv += self.rndsrc(mv).replace(b"\x00", b"")
|
| 38 |
+
mv = size - len(rv)
|
| 39 |
+
assert len(rv) == size
|
| 40 |
+
return rv
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
basernd = Random()
|
| 44 |
+
rndsrc = basernd.rndsrc
|
| 45 |
+
getrandbits = basernd.getrandbits
|
| 46 |
+
randint = basernd.randint
|
| 47 |
+
rndsrcnz = basernd.rndsrcnz
|
scripts/lib/ufastrsa/util.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
try:
|
| 2 |
+
int.bit_length(0)
|
| 3 |
+
|
| 4 |
+
def get_bit_length(n):
|
| 5 |
+
return n.bit_length()
|
| 6 |
+
|
| 7 |
+
except:
|
| 8 |
+
# Work around
|
| 9 |
+
def get_bit_length(n):
|
| 10 |
+
i = 0
|
| 11 |
+
while n:
|
| 12 |
+
n >>= 1
|
| 13 |
+
i += 1
|
| 14 |
+
return i
|
scripts/lib/umqtt/robust.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import utime
|
| 2 |
+
|
| 3 |
+
from . import simple
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class MQTTClient(simple.MQTTClient):
|
| 7 |
+
DELAY = 2
|
| 8 |
+
DEBUG = False
|
| 9 |
+
|
| 10 |
+
def delay(self, i):
|
| 11 |
+
utime.sleep(self.DELAY)
|
| 12 |
+
|
| 13 |
+
def log(self, in_reconnect, e):
|
| 14 |
+
if self.DEBUG:
|
| 15 |
+
if in_reconnect:
|
| 16 |
+
print("mqtt reconnect: %r" % e)
|
| 17 |
+
else:
|
| 18 |
+
print("mqtt: %r" % e)
|
| 19 |
+
|
| 20 |
+
def reconnect(self):
|
| 21 |
+
i = 0
|
| 22 |
+
while 1:
|
| 23 |
+
try:
|
| 24 |
+
return super().connect(False)
|
| 25 |
+
except OSError as e:
|
| 26 |
+
self.log(True, e)
|
| 27 |
+
i += 1
|
| 28 |
+
self.delay(i)
|
| 29 |
+
|
| 30 |
+
def publish(self, topic, msg, retain=False, qos=0):
|
| 31 |
+
while 1:
|
| 32 |
+
try:
|
| 33 |
+
return super().publish(topic, msg, retain, qos)
|
| 34 |
+
except OSError as e:
|
| 35 |
+
self.log(False, e)
|
| 36 |
+
self.reconnect()
|
| 37 |
+
|
| 38 |
+
def wait_msg(self):
|
| 39 |
+
while 1:
|
| 40 |
+
try:
|
| 41 |
+
return super().wait_msg()
|
| 42 |
+
except OSError as e:
|
| 43 |
+
self.log(False, e)
|
| 44 |
+
self.reconnect()
|
scripts/lib/umqtt/simple.py
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import usocket as socket
|
| 2 |
+
import ustruct as struct
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
class MQTTException(Exception):
|
| 6 |
+
pass
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class MQTTClient:
|
| 10 |
+
def __init__(
|
| 11 |
+
self,
|
| 12 |
+
client_id,
|
| 13 |
+
server,
|
| 14 |
+
port=0,
|
| 15 |
+
user=None,
|
| 16 |
+
password=None,
|
| 17 |
+
keepalive=0,
|
| 18 |
+
ssl=False,
|
| 19 |
+
ssl_params={},
|
| 20 |
+
):
|
| 21 |
+
if port == 0:
|
| 22 |
+
port = 8883 if ssl else 1883
|
| 23 |
+
self.client_id = client_id
|
| 24 |
+
self.sock = None
|
| 25 |
+
self.server = server
|
| 26 |
+
self.port = port
|
| 27 |
+
self.ssl = ssl
|
| 28 |
+
self.ssl_params = ssl_params
|
| 29 |
+
self.pid = 0
|
| 30 |
+
self.cb = None
|
| 31 |
+
self.user = user
|
| 32 |
+
self.pswd = password
|
| 33 |
+
self.keepalive = keepalive
|
| 34 |
+
self.lw_topic = None
|
| 35 |
+
self.lw_msg = None
|
| 36 |
+
self.lw_qos = 0
|
| 37 |
+
self.lw_retain = False
|
| 38 |
+
|
| 39 |
+
def _send_str(self, s):
|
| 40 |
+
self.sock.write(struct.pack("!H", len(s)))
|
| 41 |
+
self.sock.write(s)
|
| 42 |
+
|
| 43 |
+
def _recv_len(self):
|
| 44 |
+
n = 0
|
| 45 |
+
sh = 0
|
| 46 |
+
while 1:
|
| 47 |
+
b = self.sock.read(1)[0]
|
| 48 |
+
n |= (b & 0x7F) << sh
|
| 49 |
+
if not b & 0x80:
|
| 50 |
+
return n
|
| 51 |
+
sh += 7
|
| 52 |
+
|
| 53 |
+
def set_callback(self, f):
|
| 54 |
+
self.cb = f
|
| 55 |
+
|
| 56 |
+
def set_last_will(self, topic, msg, retain=False, qos=0):
|
| 57 |
+
assert 0 <= qos <= 2
|
| 58 |
+
assert topic
|
| 59 |
+
self.lw_topic = topic
|
| 60 |
+
self.lw_msg = msg
|
| 61 |
+
self.lw_qos = qos
|
| 62 |
+
self.lw_retain = retain
|
| 63 |
+
|
| 64 |
+
def connect(self, clean_session=True):
|
| 65 |
+
self.sock = socket.socket()
|
| 66 |
+
addr = socket.getaddrinfo(self.server, self.port)[0][-1]
|
| 67 |
+
self.sock.connect(addr)
|
| 68 |
+
if self.ssl:
|
| 69 |
+
# replaced ussl with ssl due to deprecation in MicroPython 1.23.0
|
| 70 |
+
# (not PR'd on source repo, but I'm using mqtt_as in my workflows
|
| 71 |
+
# instead, anyway)
|
| 72 |
+
import ssl
|
| 73 |
+
|
| 74 |
+
self.sock = ssl.wrap_socket(self.sock, **self.ssl_params)
|
| 75 |
+
premsg = bytearray(b"\x10\0\0\0\0\0")
|
| 76 |
+
msg = bytearray(b"\x04MQTT\x04\x02\0\0")
|
| 77 |
+
|
| 78 |
+
sz = 10 + 2 + len(self.client_id)
|
| 79 |
+
msg[6] = clean_session << 1
|
| 80 |
+
if self.user is not None:
|
| 81 |
+
sz += 2 + len(self.user) + 2 + len(self.pswd)
|
| 82 |
+
msg[6] |= 0xC0
|
| 83 |
+
if self.keepalive:
|
| 84 |
+
assert self.keepalive < 65536
|
| 85 |
+
msg[7] |= self.keepalive >> 8
|
| 86 |
+
msg[8] |= self.keepalive & 0x00FF
|
| 87 |
+
if self.lw_topic:
|
| 88 |
+
sz += 2 + len(self.lw_topic) + 2 + len(self.lw_msg)
|
| 89 |
+
msg[6] |= 0x4 | (self.lw_qos & 0x1) << 3 | (self.lw_qos & 0x2) << 3
|
| 90 |
+
msg[6] |= self.lw_retain << 5
|
| 91 |
+
|
| 92 |
+
i = 1
|
| 93 |
+
while sz > 0x7F:
|
| 94 |
+
premsg[i] = (sz & 0x7F) | 0x80
|
| 95 |
+
sz >>= 7
|
| 96 |
+
i += 1
|
| 97 |
+
premsg[i] = sz
|
| 98 |
+
|
| 99 |
+
self.sock.write(premsg, i + 2)
|
| 100 |
+
self.sock.write(msg)
|
| 101 |
+
# print(hex(len(msg)), hexlify(msg, ":"))
|
| 102 |
+
self._send_str(self.client_id)
|
| 103 |
+
if self.lw_topic:
|
| 104 |
+
self._send_str(self.lw_topic)
|
| 105 |
+
self._send_str(self.lw_msg)
|
| 106 |
+
if self.user is not None:
|
| 107 |
+
self._send_str(self.user)
|
| 108 |
+
self._send_str(self.pswd)
|
| 109 |
+
resp = self.sock.read(4)
|
| 110 |
+
assert resp[0] == 0x20 and resp[1] == 0x02
|
| 111 |
+
if resp[3] != 0:
|
| 112 |
+
raise MQTTException(resp[3])
|
| 113 |
+
return resp[2] & 1
|
| 114 |
+
|
| 115 |
+
def disconnect(self):
|
| 116 |
+
self.sock.write(b"\xe0\0")
|
| 117 |
+
self.sock.close()
|
| 118 |
+
|
| 119 |
+
def ping(self):
|
| 120 |
+
self.sock.write(b"\xc0\0")
|
| 121 |
+
|
| 122 |
+
def publish(self, topic, msg, retain=False, qos=0):
|
| 123 |
+
pkt = bytearray(b"\x30\0\0\0")
|
| 124 |
+
pkt[0] |= qos << 1 | retain
|
| 125 |
+
sz = 2 + len(topic) + len(msg)
|
| 126 |
+
if qos > 0:
|
| 127 |
+
sz += 2
|
| 128 |
+
assert sz < 2097152
|
| 129 |
+
i = 1
|
| 130 |
+
while sz > 0x7F:
|
| 131 |
+
pkt[i] = (sz & 0x7F) | 0x80
|
| 132 |
+
sz >>= 7
|
| 133 |
+
i += 1
|
| 134 |
+
pkt[i] = sz
|
| 135 |
+
# print(hex(len(pkt)), hexlify(pkt, ":"))
|
| 136 |
+
self.sock.write(pkt, i + 1)
|
| 137 |
+
self._send_str(topic)
|
| 138 |
+
if qos > 0:
|
| 139 |
+
self.pid += 1
|
| 140 |
+
pid = self.pid
|
| 141 |
+
struct.pack_into("!H", pkt, 0, pid)
|
| 142 |
+
self.sock.write(pkt, 2)
|
| 143 |
+
self.sock.write(msg)
|
| 144 |
+
if qos == 1:
|
| 145 |
+
while 1:
|
| 146 |
+
op = self.wait_msg()
|
| 147 |
+
if op == 0x40:
|
| 148 |
+
sz = self.sock.read(1)
|
| 149 |
+
assert sz == b"\x02"
|
| 150 |
+
rcv_pid = self.sock.read(2)
|
| 151 |
+
rcv_pid = rcv_pid[0] << 8 | rcv_pid[1]
|
| 152 |
+
if pid == rcv_pid:
|
| 153 |
+
return
|
| 154 |
+
elif qos == 2:
|
| 155 |
+
assert 0
|
| 156 |
+
|
| 157 |
+
def subscribe(self, topic, qos=0):
|
| 158 |
+
assert self.cb is not None, "Subscribe callback is not set"
|
| 159 |
+
pkt = bytearray(b"\x82\0\0\0")
|
| 160 |
+
self.pid += 1
|
| 161 |
+
struct.pack_into("!BH", pkt, 1, 2 + 2 + len(topic) + 1, self.pid)
|
| 162 |
+
# print(hex(len(pkt)), hexlify(pkt, ":"))
|
| 163 |
+
self.sock.write(pkt)
|
| 164 |
+
self._send_str(topic)
|
| 165 |
+
self.sock.write(qos.to_bytes(1, "little"))
|
| 166 |
+
while 1:
|
| 167 |
+
op = self.wait_msg()
|
| 168 |
+
if op == 0x90:
|
| 169 |
+
resp = self.sock.read(4)
|
| 170 |
+
# print(resp)
|
| 171 |
+
assert resp[1] == pkt[2] and resp[2] == pkt[3]
|
| 172 |
+
if resp[3] == 0x80:
|
| 173 |
+
raise MQTTException(resp[3])
|
| 174 |
+
return
|
| 175 |
+
|
| 176 |
+
# Wait for a single incoming MQTT message and process it.
|
| 177 |
+
# Subscribed messages are delivered to a callback previously
|
| 178 |
+
# set by .set_callback() method. Other (internal) MQTT
|
| 179 |
+
# messages processed internally.
|
| 180 |
+
def wait_msg(self):
|
| 181 |
+
res = self.sock.read(1)
|
| 182 |
+
self.sock.setblocking(True)
|
| 183 |
+
if res is None:
|
| 184 |
+
return None
|
| 185 |
+
if res == b"":
|
| 186 |
+
raise OSError(-1)
|
| 187 |
+
if res == b"\xd0": # PINGRESP
|
| 188 |
+
sz = self.sock.read(1)[0]
|
| 189 |
+
assert sz == 0
|
| 190 |
+
return None
|
| 191 |
+
op = res[0]
|
| 192 |
+
if op & 0xF0 != 0x30:
|
| 193 |
+
return op
|
| 194 |
+
sz = self._recv_len()
|
| 195 |
+
topic_len = self.sock.read(2)
|
| 196 |
+
topic_len = (topic_len[0] << 8) | topic_len[1]
|
| 197 |
+
topic = self.sock.read(topic_len)
|
| 198 |
+
sz -= topic_len + 2
|
| 199 |
+
if op & 6:
|
| 200 |
+
pid = self.sock.read(2)
|
| 201 |
+
pid = pid[0] << 8 | pid[1]
|
| 202 |
+
sz -= 2
|
| 203 |
+
msg = self.sock.read(sz)
|
| 204 |
+
self.cb(topic, msg)
|
| 205 |
+
if op & 6 == 2:
|
| 206 |
+
pkt = bytearray(b"\x40\x02\0\0")
|
| 207 |
+
struct.pack_into("!H", pkt, 2, pid)
|
| 208 |
+
self.sock.write(pkt)
|
| 209 |
+
elif op & 6 == 4:
|
| 210 |
+
assert 0
|
| 211 |
+
|
| 212 |
+
# Checks whether a pending message from server is available.
|
| 213 |
+
# If not, returns immediately with None. Otherwise, does
|
| 214 |
+
# the same processing as wait_msg.
|
| 215 |
+
def check_msg(self):
|
| 216 |
+
self.sock.setblocking(False)
|
| 217 |
+
return self.wait_msg()
|
scripts/lib/unique_id-1.0.1.dist-info/METADATA
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Metadata-Version: 2.1
|
| 2 |
+
Name: unique-id
|
| 3 |
+
Version: 1.0.1
|
| 4 |
+
Summary: Unique-ID is a small lib to generate unique ids - string values.
|
| 5 |
+
Home-page:
|
| 6 |
+
Download-URL: https://github.com/slawek87/unique-id
|
| 7 |
+
Author: Sławomir Kabik
|
| 8 |
+
Author-email: slawek@redsoftware.pl
|
| 9 |
+
Keywords: Python Unique ID,Python ID,Python Unique string
|
| 10 |
+
Requires-Dist: setuptools
|
| 11 |
+
|
scripts/lib/unique_id-1.0.1.dist-info/RECORD
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
unique_id-1.0.1.dist-info/METADATA,,
|
| 2 |
+
unique_id/__init__.py,,
|
| 3 |
+
unique_id/main.py,,
|
| 4 |
+
unique_id/tests.py,,
|
| 5 |
+
unique_id-1.0.1.dist-info/RECORD,,
|
scripts/lib/unique_id/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
from unique_id.main import get_unique_id
|
scripts/lib/unique_id/main.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import random
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
class UniqueID(object):
|
| 5 |
+
"""
|
| 6 |
+
Generates Unique ID.
|
| 7 |
+
"""
|
| 8 |
+
DEFAULT_ID_LENGTH = 14
|
| 9 |
+
DEFAULT_EXCLUDED_CHARS = ":*^`\",.~;%+-'"
|
| 10 |
+
|
| 11 |
+
def __init__(self, length=DEFAULT_ID_LENGTH, excluded_chars=DEFAULT_EXCLUDED_CHARS):
|
| 12 |
+
"""
|
| 13 |
+
`length` - defines length of unique ID.
|
| 14 |
+
`excluded_chars` - defines chars excluded during generate process of unique ID.
|
| 15 |
+
"""
|
| 16 |
+
self.id_length = length
|
| 17 |
+
self.excluded_chars = excluded_chars
|
| 18 |
+
|
| 19 |
+
def get_random_bits(self):
|
| 20 |
+
"""
|
| 21 |
+
Method returns random number included in max 8 bits.
|
| 22 |
+
"""
|
| 23 |
+
return random.getrandbits(8)
|
| 24 |
+
|
| 25 |
+
def is_approved_ascii(self, ascii_number):
|
| 26 |
+
return 126 >= ascii_number >= 33
|
| 27 |
+
|
| 28 |
+
def is_excluded_char(self, current_char):
|
| 29 |
+
"""
|
| 30 |
+
Method checks if given char is not in excluded chars list.
|
| 31 |
+
"""
|
| 32 |
+
return current_char in self.excluded_chars
|
| 33 |
+
|
| 34 |
+
def generate_id(self):
|
| 35 |
+
"""
|
| 36 |
+
Method generates unique ID.
|
| 37 |
+
"""
|
| 38 |
+
unique_id = ""
|
| 39 |
+
|
| 40 |
+
while len(unique_id) < self.id_length:
|
| 41 |
+
ascii_number = self.get_random_bits()
|
| 42 |
+
|
| 43 |
+
if self.is_approved_ascii(ascii_number):
|
| 44 |
+
random_char = chr(ascii_number)
|
| 45 |
+
|
| 46 |
+
if not self.is_excluded_char(random_char):
|
| 47 |
+
unique_id += chr(ascii_number)
|
| 48 |
+
|
| 49 |
+
return unique_id
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def get_unique_id(length=UniqueID.DEFAULT_ID_LENGTH, excluded_chars=UniqueID.DEFAULT_EXCLUDED_CHARS):
|
| 53 |
+
"""
|
| 54 |
+
Function returns unique ID.
|
| 55 |
+
"""
|
| 56 |
+
unique_id = UniqueID(length=length, excluded_chars=excluded_chars)
|
| 57 |
+
return unique_id.generate_id()
|
| 58 |
+
|
scripts/lib/unique_id/tests.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from random import randint
|
| 2 |
+
import unittest
|
| 3 |
+
|
| 4 |
+
from unique_id import get_unique_id
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class TestStringMethods(unittest.TestCase):
|
| 8 |
+
def test_unique_id(self):
|
| 9 |
+
unique_ids = list()
|
| 10 |
+
|
| 11 |
+
for item in range(1000):
|
| 12 |
+
unique_id = get_unique_id()
|
| 13 |
+
|
| 14 |
+
is_duplicated = unique_id in unique_ids
|
| 15 |
+
self.assertFalse(is_duplicated)
|
| 16 |
+
|
| 17 |
+
unique_ids.append(unique_id)
|
| 18 |
+
|
| 19 |
+
def test_max_length(self):
|
| 20 |
+
for item in range(1000):
|
| 21 |
+
id_length = randint(1, 128)
|
| 22 |
+
unique_id = get_unique_id(length=id_length)
|
| 23 |
+
|
| 24 |
+
is_over_length = len(unique_id) != id_length
|
| 25 |
+
self.assertFalse(is_over_length)
|
| 26 |
+
|
| 27 |
+
def test_excluded_chars(self):
|
| 28 |
+
id_length = 256
|
| 29 |
+
excluded_chars = [1, 'f', 'm', 'a', 4, 5, 'Z', 'w', '_']
|
| 30 |
+
|
| 31 |
+
for item in range(1000):
|
| 32 |
+
unique_id = get_unique_id(length=id_length, excluded_chars=excluded_chars)
|
| 33 |
+
|
| 34 |
+
for seed in unique_id:
|
| 35 |
+
is_excluded_char = seed in excluded_chars
|
| 36 |
+
self.assertFalse(is_excluded_char)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
if __name__ == '__main__':
|
| 40 |
+
unittest.main()
|
scripts/lib/urequests_2.py
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Workaround for the `urequests` module to support HTTP/1.1
|
| 2 |
+
# Based on https://github.com/micropython/micropython-lib/blob/e025c843b60e93689f0f991d753010bb5bd6a722/python-ecosys/requests/requests/__init__.py
|
| 3 |
+
# See https://github.com/micropython/micropython-lib/pull/861 and https://github.com/orgs/micropython/discussions/15112
|
| 4 |
+
# `1.0` replaced with `1.1, i.e.:
|
| 5 |
+
# `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))`
|
| 6 |
+
import usocket
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class Response:
|
| 10 |
+
def __init__(self, f):
|
| 11 |
+
self.raw = f
|
| 12 |
+
self.encoding = "utf-8"
|
| 13 |
+
self._cached = None
|
| 14 |
+
|
| 15 |
+
def close(self):
|
| 16 |
+
if self.raw:
|
| 17 |
+
self.raw.close()
|
| 18 |
+
self.raw = None
|
| 19 |
+
self._cached = None
|
| 20 |
+
|
| 21 |
+
@property
|
| 22 |
+
def content(self):
|
| 23 |
+
if self._cached is None:
|
| 24 |
+
try:
|
| 25 |
+
self._cached = self.raw.read()
|
| 26 |
+
finally:
|
| 27 |
+
self.raw.close()
|
| 28 |
+
self.raw = None
|
| 29 |
+
return self._cached
|
| 30 |
+
|
| 31 |
+
@property
|
| 32 |
+
def text(self):
|
| 33 |
+
return str(self.content, self.encoding)
|
| 34 |
+
|
| 35 |
+
def json(self):
|
| 36 |
+
import ujson
|
| 37 |
+
|
| 38 |
+
return ujson.loads(self.content)
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def request(
|
| 42 |
+
method,
|
| 43 |
+
url,
|
| 44 |
+
data=None,
|
| 45 |
+
json=None,
|
| 46 |
+
headers={},
|
| 47 |
+
stream=None,
|
| 48 |
+
auth=None,
|
| 49 |
+
timeout=None,
|
| 50 |
+
parse_headers=True,
|
| 51 |
+
):
|
| 52 |
+
redirect = None # redirection url, None means no redirection
|
| 53 |
+
chunked_data = (
|
| 54 |
+
data and getattr(data, "__next__", None) and not getattr(data, "__len__", None)
|
| 55 |
+
)
|
| 56 |
+
|
| 57 |
+
if auth is not None:
|
| 58 |
+
import ubinascii
|
| 59 |
+
|
| 60 |
+
username, password = auth
|
| 61 |
+
formated = b"{}:{}".format(username, password)
|
| 62 |
+
formated = str(ubinascii.b2a_base64(formated)[:-1], "ascii")
|
| 63 |
+
headers["Authorization"] = "Basic {}".format(formated)
|
| 64 |
+
|
| 65 |
+
try:
|
| 66 |
+
proto, dummy, host, path = url.split("/", 3)
|
| 67 |
+
except ValueError:
|
| 68 |
+
proto, dummy, host = url.split("/", 2)
|
| 69 |
+
path = ""
|
| 70 |
+
if proto == "http:":
|
| 71 |
+
port = 80
|
| 72 |
+
elif proto == "https:":
|
| 73 |
+
import ussl
|
| 74 |
+
|
| 75 |
+
port = 443
|
| 76 |
+
else:
|
| 77 |
+
raise ValueError("Unsupported protocol: " + proto)
|
| 78 |
+
|
| 79 |
+
if ":" in host:
|
| 80 |
+
host, port = host.split(":", 1)
|
| 81 |
+
port = int(port)
|
| 82 |
+
|
| 83 |
+
ai = usocket.getaddrinfo(host, port, 0, usocket.SOCK_STREAM)
|
| 84 |
+
ai = ai[0]
|
| 85 |
+
|
| 86 |
+
resp_d = None
|
| 87 |
+
if parse_headers is not False:
|
| 88 |
+
resp_d = {}
|
| 89 |
+
|
| 90 |
+
s = usocket.socket(ai[0], usocket.SOCK_STREAM, ai[2])
|
| 91 |
+
|
| 92 |
+
if timeout is not None:
|
| 93 |
+
# Note: settimeout is not supported on all platforms, will raise
|
| 94 |
+
# an AttributeError if not available.
|
| 95 |
+
s.settimeout(timeout)
|
| 96 |
+
|
| 97 |
+
try:
|
| 98 |
+
s.connect(ai[-1])
|
| 99 |
+
if proto == "https:":
|
| 100 |
+
s = ussl.wrap_socket(s, server_hostname=host)
|
| 101 |
+
s.write(b"%s /%s HTTP/1.1\r\n" % (method, path))
|
| 102 |
+
if "Host" not in headers:
|
| 103 |
+
s.write(b"Host: %s\r\n" % host)
|
| 104 |
+
# Iterate over keys to avoid tuple alloc
|
| 105 |
+
for k in headers:
|
| 106 |
+
s.write(k)
|
| 107 |
+
s.write(b": ")
|
| 108 |
+
s.write(headers[k])
|
| 109 |
+
s.write(b"\r\n")
|
| 110 |
+
if json is not None:
|
| 111 |
+
assert data is None
|
| 112 |
+
import ujson
|
| 113 |
+
|
| 114 |
+
data = ujson.dumps(json)
|
| 115 |
+
s.write(b"Content-Type: application/json\r\n")
|
| 116 |
+
if data:
|
| 117 |
+
if chunked_data:
|
| 118 |
+
s.write(b"Transfer-Encoding: chunked\r\n")
|
| 119 |
+
else:
|
| 120 |
+
s.write(b"Content-Length: %d\r\n" % len(data))
|
| 121 |
+
s.write(b"Connection: close\r\n\r\n")
|
| 122 |
+
if data:
|
| 123 |
+
if chunked_data:
|
| 124 |
+
for chunk in data:
|
| 125 |
+
s.write(b"%x\r\n" % len(chunk))
|
| 126 |
+
s.write(chunk)
|
| 127 |
+
s.write(b"\r\n")
|
| 128 |
+
s.write("0\r\n\r\n")
|
| 129 |
+
else:
|
| 130 |
+
s.write(data)
|
| 131 |
+
|
| 132 |
+
l = s.readline()
|
| 133 |
+
# print(l)
|
| 134 |
+
l = l.split(None, 2)
|
| 135 |
+
if len(l) < 2:
|
| 136 |
+
# Invalid response
|
| 137 |
+
raise ValueError("HTTP error: BadStatusLine:\n%s" % l)
|
| 138 |
+
status = int(l[1])
|
| 139 |
+
reason = ""
|
| 140 |
+
if len(l) > 2:
|
| 141 |
+
reason = l[2].rstrip()
|
| 142 |
+
while True:
|
| 143 |
+
l = s.readline()
|
| 144 |
+
if not l or l == b"\r\n":
|
| 145 |
+
break
|
| 146 |
+
# print(l)
|
| 147 |
+
if l.startswith(b"Transfer-Encoding:"):
|
| 148 |
+
if b"chunked" in l:
|
| 149 |
+
raise ValueError("Unsupported " + str(l, "utf-8"))
|
| 150 |
+
elif l.startswith(b"Location:") and not 200 <= status <= 299:
|
| 151 |
+
if status in [301, 302, 303, 307, 308]:
|
| 152 |
+
redirect = str(l[10:-2], "utf-8")
|
| 153 |
+
else:
|
| 154 |
+
raise NotImplementedError("Redirect %d not yet supported" % status)
|
| 155 |
+
if parse_headers is False:
|
| 156 |
+
pass
|
| 157 |
+
elif parse_headers is True:
|
| 158 |
+
l = str(l, "utf-8")
|
| 159 |
+
k, v = l.split(":", 1)
|
| 160 |
+
resp_d[k] = v.strip()
|
| 161 |
+
else:
|
| 162 |
+
parse_headers(l, resp_d)
|
| 163 |
+
except OSError:
|
| 164 |
+
s.close()
|
| 165 |
+
raise
|
| 166 |
+
|
| 167 |
+
if redirect:
|
| 168 |
+
s.close()
|
| 169 |
+
if status in [301, 302, 303]:
|
| 170 |
+
return request("GET", redirect, None, None, headers, stream)
|
| 171 |
+
else:
|
| 172 |
+
return request(method, redirect, data, json, headers, stream)
|
| 173 |
+
else:
|
| 174 |
+
resp = Response(s)
|
| 175 |
+
resp.status_code = status
|
| 176 |
+
resp.reason = reason
|
| 177 |
+
if resp_d is not None:
|
| 178 |
+
resp.headers = resp_d
|
| 179 |
+
return resp
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
def head(url, **kw):
|
| 183 |
+
return request("HEAD", url, **kw)
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
def get(url, **kw):
|
| 187 |
+
return request("GET", url, **kw)
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
def post(url, **kw):
|
| 191 |
+
return request("POST", url, **kw)
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
def put(url, **kw):
|
| 195 |
+
return request("PUT", url, **kw)
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
def patch(url, **kw):
|
| 199 |
+
return request("PATCH", url, **kw)
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
def delete(url, **kw):
|
| 203 |
+
return request("DELETE", url, **kw)
|