Luthira A commited on
Commit
4bec42e
·
1 Parent(s): bcf4441

updated scripts and unique id generation with form for mqtt connection

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. app.py +2 -2
  2. scripts/adafruit_bme680.mpy +0 -0
  3. scripts/adafruit_bme680.py +769 -0
  4. scripts/adafruit_bus_device/__init__.py +0 -0
  5. scripts/adafruit_bus_device/i2c_device.py +187 -0
  6. scripts/adafruit_bus_device/spi_device.py +121 -0
  7. scripts/bme60.py +421 -0
  8. scripts/bme680.py +421 -0
  9. scripts/bme680i.py +418 -0
  10. scripts/constants.py +12 -0
  11. scripts/demo.py +18 -0
  12. scripts/hivemq-com-chain.der +0 -0
  13. scripts/lib/CHANGELOG.md +50 -0
  14. scripts/lib/LICENSE +21 -0
  15. scripts/lib/README.md +56 -0
  16. scripts/lib/adafruit_blinka-8.49.0.dist-info/METADATA +8 -0
  17. scripts/lib/adafruit_blinka-8.49.0.dist-info/RECORD +2 -0
  18. scripts/lib/as7341.py +608 -0
  19. scripts/lib/as7341_sensor.py +149 -0
  20. scripts/lib/as7341_smux_select.py +49 -0
  21. scripts/lib/bme680-2.0.0.dist-info/METADATA +156 -0
  22. scripts/lib/bme680-2.0.0.dist-info/RECORD +7 -0
  23. scripts/lib/bme680/__init__.py +486 -0
  24. scripts/lib/bme680/constants.py +413 -0
  25. scripts/lib/data_logging.py +166 -0
  26. scripts/lib/functools.py +28 -0
  27. scripts/lib/mqtt_as.py +824 -0
  28. scripts/lib/netman.py +73 -0
  29. scripts/lib/sdcard/LICENSE +21 -0
  30. scripts/lib/sdcard/sdcard.py +302 -0
  31. scripts/lib/sdl_demo_utils.py +276 -0
  32. scripts/lib/smbus2-0.5.0.dist-info/METADATA +234 -0
  33. scripts/lib/smbus2-0.5.0.dist-info/RECORD +6 -0
  34. scripts/lib/smbus2/__init__.py +26 -0
  35. scripts/lib/smbus2/py.typed +0 -0
  36. scripts/lib/smbus2/smbus2.py +660 -0
  37. scripts/lib/smbus2/smbus2.pyi +148 -0
  38. scripts/lib/ufastrsa/__init__.py +0 -0
  39. scripts/lib/ufastrsa/genprime.py +136 -0
  40. scripts/lib/ufastrsa/rsa.py +46 -0
  41. scripts/lib/ufastrsa/srandom.py +47 -0
  42. scripts/lib/ufastrsa/util.py +14 -0
  43. scripts/lib/umqtt/robust.py +44 -0
  44. scripts/lib/umqtt/simple.py +217 -0
  45. scripts/lib/unique_id-1.0.1.dist-info/METADATA +11 -0
  46. scripts/lib/unique_id-1.0.1.dist-info/RECORD +5 -0
  47. scripts/lib/unique_id/__init__.py +1 -0
  48. scripts/lib/unique_id/main.py +58 -0
  49. scripts/lib/unique_id/tests.py +40 -0
  50. 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:", "Luthiraa")
16
- MQTT_PASSWORD = st.text_input("Enter your MQTT password:", "theboss1010", 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
 
 
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
+ [![Build Status](https://img.shields.io/github/actions/workflow/status/pimoroni/bme680-python/test.yml?branch=main)](https://github.com/pimoroni/bme680-python/actions/workflows/test.yml)
4
+ [![Coverage Status](https://coveralls.io/repos/github/pimoroni/bme680-python/badge.svg?branch=main)](https://coveralls.io/github/pimoroni/bme680-python?branch=main)
5
+ [![PyPi Package](https://img.shields.io/pypi/v/bme680.svg)](https://pypi.python.org/pypi/bme680)
6
+ [![Python Versions](https://img.shields.io/pypi/pyversions/bme680.svg)](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
+ ![Finding the terminal](http://get.pimoroni.com/resources/github-repo-terminal.png)
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
+ [![Build Status](https://img.shields.io/github/actions/workflow/status/pimoroni/bme680-python/test.yml?branch=main)](https://github.com/pimoroni/bme680-python/actions/workflows/test.yml)
53
+ [![Coverage Status](https://coveralls.io/repos/github/pimoroni/bme680-python/badge.svg?branch=main)](https://coveralls.io/github/pimoroni/bme680-python?branch=main)
54
+ [![PyPi Package](https://img.shields.io/pypi/v/bme680.svg)](https://pypi.python.org/pypi/bme680)
55
+ [![Python Versions](https://img.shields.io/pypi/pyversions/bme680.svg)](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
+ ![Finding the terminal](http://get.pimoroni.com/resources/github-repo-terminal.png)
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
+ [![Build Status](https://github.com/kplindegaard/smbus2/actions/workflows/python-build-test.yml/badge.svg?branch=master)](https://github.com/kplindegaard/smbus2/actions/workflows/python-build-test.yml)
35
+ [![Documentation Status](https://readthedocs.org/projects/smbus2/badge/?version=latest)](http://smbus2.readthedocs.io/en/latest/?badge=latest)
36
+ ![CodeQL](https://github.com/kplindegaard/smbus2/actions/workflows/codeql-analysis.yml/badge.svg?branch=master)
37
+ [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=kplindegaard_smbus2&metric=alert_status)](https://sonarcloud.io/dashboard?id=kplindegaard_smbus2)
38
+
39
+ ![Python Verions](https://img.shields.io/pypi/pyversions/smbus2.svg)
40
+ [![PyPi Version](https://img.shields.io/pypi/v/smbus2.svg)](https://pypi.org/project/smbus2/)
41
+ [![PyPI - Downloads](https://img.shields.io/pypi/dm/smbus2)](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)