Skip to content

Commit 297fffa

Browse files
committed
library and examples
1 parent c157bd2 commit 297fffa

File tree

4 files changed

+391
-14
lines changed

4 files changed

+391
-14
lines changed

adafruit_hdc302x.py

Lines changed: 335 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries
21
# SPDX-FileCopyrightText: Copyright (c) 2024 Liz Clark for Adafruit Industries
32
#
43
# SPDX-License-Identifier: MIT
@@ -16,22 +15,349 @@
1615
1716
**Hardware:**
1817
19-
.. todo:: Add links to any specific hardware product page(s), or category page(s).
20-
Use unordered list & hyperlink rST inline format: "* `Link Text <url>`_"
18+
* `Adafruit HDC3021 Sensor - STEMMA QT / Qwiic <https://www.adafruit.com/product/5989>`_
2119
2220
**Software and Dependencies:**
2321
2422
* Adafruit CircuitPython firmware for the supported boards:
2523
https://circuitpython.org/downloads
2624
27-
.. todo:: Uncomment or remove the Bus Device and/or the Register library dependencies
28-
based on the library's use of either.
29-
30-
# * Adafruit's Bus Device library: https://github.com/adafruit/Adafruit_CircuitPython_BusDevice
31-
# * Adafruit's Register library: https://github.com/adafruit/Adafruit_CircuitPython_Register
25+
* Adafruit's Bus Device library: https://github.com/adafruit/Adafruit_CircuitPython_BusDevice
3226
"""
3327

34-
# imports
28+
import struct
29+
import busio
30+
from adafruit_bus_device import i2c_device
31+
32+
try:
33+
from typing import Tuple, List, Dict
34+
except ImportError:
35+
pass
3536

3637
__version__ = "0.0.0+auto.0"
3738
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_HDC302x.git"
39+
40+
41+
class HDC302x:
42+
"""Driver for the HDC302x temperature and humidity sensor."""
43+
44+
AUTO_MODES: Dict[str, int] = {
45+
"5MPS_LP0": 0x2032,
46+
"5MPS_LP1": 0x2024,
47+
"5MPS_LP2": 0x202F,
48+
"5MPS_LP3": 0x20FF,
49+
"1MPS_LP0": 0x2130,
50+
"1MPS_LP1": 0x2126,
51+
"1MPS_LP2": 0x212D,
52+
"1MPS_LP3": 0x21FF,
53+
"2MPS_LP0": 0x2236,
54+
"2MPS_LP1": 0x2220,
55+
"2MPS_LP2": 0x222B,
56+
"2MPS_LP3": 0x22FF,
57+
"4MPS_LP0": 0x2334,
58+
"4MPS_LP1": 0x2322,
59+
"4MPS_LP2": 0x2329,
60+
"4MPS_LP3": 0x23FF,
61+
"10MPS_LP0": 0x2737,
62+
"10MPS_LP1": 0x2721,
63+
"10MPS_LP2": 0x272A,
64+
"10MPS_LP3": 0x27FF,
65+
"EXIT_AUTO_MODE": 0x3093,
66+
}
67+
68+
HEATER_POWERS: Dict[str, int] = {
69+
"OFF": 0x0000,
70+
"QUARTER_POWER": 0x009F,
71+
"HALF_POWER": 0x03FF,
72+
"FULL_POWER": 0x3FFF,
73+
}
74+
75+
def __init__(self, i2c_bus: busio.I2C, address: int = 0x44) -> None:
76+
"""
77+
Initialize the HDC302x sensor with the given I2C bus and address.
78+
79+
:param i2c_bus: The I2C bus object.
80+
:param address: The I2C address of the sensor.
81+
"""
82+
self.i2c_device = i2c_device.I2CDevice(i2c_bus, address)
83+
self._current_auto_mode = self.AUTO_MODES["EXIT_AUTO_MODE"]
84+
85+
@property
86+
def heater(self) -> bool:
87+
"""
88+
Check if the heater is on.
89+
90+
:return: True if the heater is on, False otherwise.
91+
"""
92+
status = self._read_command(0xF32D)
93+
return bool(status & (1 << 13))
94+
95+
@heater.setter
96+
def heater(self, power: str) -> None:
97+
"""
98+
Set the heater power.
99+
100+
:param power: The heater power level.
101+
"""
102+
if power not in self.HEATER_POWERS:
103+
raise ValueError("Invalid heater power value.")
104+
_power = self.HEATER_POWERS[power]
105+
print(_power)
106+
if _power == self.HEATER_POWERS["OFF"]:
107+
self._write_command(0x3066)
108+
else:
109+
self._write_command(0x306D)
110+
self._write_command_data(0x306E, _power)
111+
112+
@property
113+
def status(self) -> int:
114+
"""
115+
Status of the sensor.
116+
117+
:return: The status of the sensor.
118+
"""
119+
return self._read_command(0xF32D)
120+
121+
@property
122+
def manufacturer_id(self) -> int:
123+
"""
124+
Manufacturer ID of the sensor.
125+
126+
:return: The manufacturer ID.
127+
"""
128+
return self._read_command(0x3781)
129+
130+
@property
131+
def nist_id(self) -> List[int]:
132+
"""
133+
Get the NIST ID of the sensor.
134+
135+
:return: NIST ID as a list of integers.
136+
"""
137+
id_part1 = self._read_command(0x3683)
138+
id_part2 = self._read_command(0x3684)
139+
id_part3 = self._read_command(0x3685)
140+
return [
141+
id_part1 >> 8,
142+
id_part1 & 0xFF,
143+
id_part2 >> 8,
144+
id_part2 & 0xFF,
145+
id_part3 >> 8,
146+
id_part3 & 0xFF,
147+
]
148+
149+
@property
150+
def auto_mode(self) -> int:
151+
"""
152+
Current auto mode.
153+
154+
:return: The current auto mode.
155+
"""
156+
return self._current_auto_mode
157+
158+
@auto_mode.setter
159+
def auto_mode(self, mode: int) -> None:
160+
"""
161+
Set the auto mode.
162+
163+
:param mode: The auto mode to set.
164+
"""
165+
if mode not in self.AUTO_MODES:
166+
raise ValueError("Invalid auto mode value.")
167+
_mode = self.AUTO_MODES[mode]
168+
self._current_auto_mode = _mode
169+
self._write_command(_mode)
170+
171+
@property
172+
def offsets(self) -> Tuple[float, float]:
173+
"""
174+
Get the temperature and humidity offsets.
175+
176+
:return: The temperature and humidity offsets.
177+
"""
178+
combined_offsets = self._read_command(0xA004)
179+
rh_offset = (combined_offsets >> 8) & 0xFF
180+
temp_offset = combined_offsets & 0xFF
181+
return self._invert_offset(temp_offset, True), self._invert_offset(
182+
rh_offset, False
183+
)
184+
185+
@offsets.setter
186+
def offsets(self, temp: float, humid: float) -> None:
187+
"""
188+
Set the temperature and humidity offsets.
189+
190+
:param values: A tuple containing the temperature and humidity offsets.
191+
"""
192+
rh_offset = self._calculate_offset(humid, False)
193+
temp_offset = self._calculate_offset(temp, True)
194+
combined_offsets = (rh_offset << 8) | temp_offset
195+
self._write_command_data(0xA004, combined_offsets)
196+
197+
@property
198+
def auto_temperature(self) -> float:
199+
"""
200+
Get the temperature in auto mode.
201+
202+
:return: The temperature in degrees Celsius.
203+
"""
204+
temp, _ = self._send_command_read_trh(0xE000)
205+
return temp
206+
207+
@property
208+
def auto_relative_humidity(self) -> float:
209+
"""
210+
Get the relative humidity in auto mode.
211+
212+
:return: The relative humidity in percent.
213+
"""
214+
_, humid = self._send_command_read_trh(0xE000)
215+
return humid
216+
217+
@property
218+
def temperature(self) -> float:
219+
"""
220+
The measured temperature in degrees Celsius.
221+
222+
:return: The temperature in degrees Celsius.
223+
"""
224+
temp, _ = self._send_command_read_trh(0x2400)
225+
return temp
226+
227+
@property
228+
def relative_humidity(self) -> float:
229+
"""
230+
The measured relative humidity in percent.
231+
232+
:return: The relative humidity in percent.
233+
"""
234+
_, humid = self._send_command_read_trh(0x2400)
235+
return humid
236+
237+
@property
238+
def high_alert(self) -> bool:
239+
"""
240+
Check if the high alert is activated.
241+
242+
:return: True if the high alert is activated, False otherwise.
243+
"""
244+
status = self._read_command(0xF32D)
245+
return bool(status & ((1 << 10) | (1 << 9)))
246+
247+
@property
248+
def low_alert(self) -> bool:
249+
"""
250+
Check if the low alert is activated.
251+
252+
:return: True if the low alert is activated, False otherwise.
253+
"""
254+
status = self._read_command(0xF32D)
255+
return bool(status & ((1 << 12) | (1 << 11)))
256+
257+
def set_high_alert(self, temp: float, humid: float) -> bool:
258+
"""
259+
Set the high alert thresholds for temperature and humidity.
260+
261+
:param temp: The temperature threshold in degrees Celsius.
262+
:param humid: The relative humidity threshold in percent.
263+
"""
264+
self._alert_command(0x611D, temp, humid)
265+
266+
def set_low_alert(self, temp: float, humid: float) -> bool:
267+
"""
268+
Set the low alert thresholds for temperature and humidity.
269+
270+
:param temp: The temperature threshold in degrees Celsius.
271+
:param humid: The relative humidity threshold in percent.
272+
"""
273+
self._alert_command(0x6100, temp, humid)
274+
275+
def clear_high_alert(self, temp: float, humid: float) -> bool:
276+
"""
277+
Clear the high alert thresholds for temperature and humidity.
278+
279+
:param temp: The temperature threshold in degrees Celsius.
280+
:param humid: The relative humidity threshold in percent.
281+
"""
282+
self._alert_command(0x6116, temp, humid)
283+
284+
def clear_low_alert(self, temp: float, humid: float) -> bool:
285+
"""
286+
Clear the low alert thresholds for temperature and humidity.
287+
288+
:param temp: The temperature threshold in degrees Celsius.
289+
:param humid: The relative humidity threshold in percent.
290+
"""
291+
self._alert_command(0x610B, temp, humid)
292+
293+
def _write_command(self, command: int) -> bool:
294+
with self.i2c_device as i2c:
295+
i2c.write(bytes([command >> 8, command & 0xFF]))
296+
297+
def _write_command_data(self, command: int, data: int) -> bool:
298+
crc = self._calculate_crc8(struct.pack(">H", data))
299+
with self.i2c_device as i2c:
300+
i2c.write(
301+
bytes([command >> 8, command & 0xFF, data >> 8, data & 0xFF, crc])
302+
)
303+
304+
def _read_command(self, command: int) -> int:
305+
with self.i2c_device as i2c:
306+
i2c.write_then_readinto(
307+
bytes([command >> 8, command & 0xFF]), result := bytearray(3)
308+
)
309+
crc = self._calculate_crc8(result[:2])
310+
if crc != result[2]:
311+
raise RuntimeError("CRC check failed")
312+
return (result[0] << 8) | result[1]
313+
314+
def _send_command_read_trh(self, command: int) -> Tuple[float, float]:
315+
self._write_command(command)
316+
with self.i2c_device as i2c:
317+
i2c.readinto(result := bytearray(6))
318+
if (
319+
self._calculate_crc8(result[:2]) != result[2]
320+
or self._calculate_crc8(result[3:5]) != result[5]
321+
):
322+
raise RuntimeError("CRC check failed")
323+
temp_raw = (result[0] << 8) | result[1]
324+
hum_raw = (result[3] << 8) | result[4]
325+
temperature = ((temp_raw / 65535.0) * 175.0) - 45.0
326+
relative_humidity = (hum_raw / 65535.0) * 100.0
327+
return temperature, relative_humidity
328+
329+
def _alert_command(self, command: int, temp: float, humid: float) -> bool:
330+
raw_temp = int(((temp + 45.0) / 175.0) * 65535.0)
331+
raw_rh = int((humid / 100.0) * 65535.0)
332+
msb_rh = (raw_rh >> 9) & 0x7F
333+
msb_temp = (raw_temp >> 7) & 0x1FF
334+
threshold = (msb_rh << 9) | msb_temp
335+
self._write_command_data(command, threshold)
336+
337+
@staticmethod
338+
def _calculate_crc8(data: bytes) -> int:
339+
"""Calculate the CRC-8 checksum."""
340+
crc = 0xFF
341+
for byte in data:
342+
crc ^= byte
343+
for _ in range(8):
344+
crc = (crc << 1) ^ 0x31 if crc & 0x80 else crc << 1
345+
return crc & 0xFF
346+
347+
@staticmethod
348+
def _calculate_offset(value: float, is_temp: bool) -> int:
349+
"""Calculate the closest matching offset byte for temperature or relative humidity."""
350+
lsb = 0.1708984375 if is_temp else 0.1953125
351+
sign = 0x00 if value < 0 else 0x80
352+
abs_value = abs(value)
353+
offset = int(round(abs_value / lsb))
354+
return sign | offset
355+
356+
@staticmethod
357+
def _invert_offset(offset: int, is_temp: bool) -> float:
358+
"""Invert the offset byte to a floating point value."""
359+
lsb = 0.1708984375 if is_temp else 0.1953125
360+
is_negative = not offset & 0x80
361+
abs_offset = offset & 0x7F
362+
value = abs_offset * lsb
363+
return -value if is_negative else value

docs/conf.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@
3333

3434

3535
intersphinx_mapping = {
36-
"python": ("https://docs.python.org/3", None),"BusDevice": ("https://docs.circuitpython.org/projects/busdevice/en/latest/", None),
37-
36+
"python": ("https://docs.python.org/3", None),
37+
"BusDevice": ("https://docs.circuitpython.org/projects/busdevice/en/latest/", None),
3838
"CircuitPython": ("https://docs.circuitpython.org/en/latest/", None),
3939
}
4040

0 commit comments

Comments
 (0)