From 12473670a2c107b6060b7b5ee7b863ad2acfe1d6 Mon Sep 17 00:00:00 2001 From: Zachariah Mears Date: Thu, 11 Jun 2026 11:39:29 -0700 Subject: [PATCH 1/5] Initial write of library --- README.md | 110 ++++++++++++++ example.py | 35 +++++ setup.py | 22 +++ tmp119/__init__.py | 1 + tmp119/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 189 bytes tmp119/__pycache__/tmp119.cpython-313.pyc | Bin 0 -> 6298 bytes tmp119/tmp119.py | 160 ++++++++++++++++++++ 7 files changed, 328 insertions(+) create mode 100644 README.md create mode 100644 example.py create mode 100644 setup.py create mode 100644 tmp119/__init__.py create mode 100644 tmp119/__pycache__/__init__.cpython-313.pyc create mode 100644 tmp119/__pycache__/tmp119.cpython-313.pyc create mode 100644 tmp119/tmp119.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..16d40ce --- /dev/null +++ b/README.md @@ -0,0 +1,110 @@ +# tmp119-python + +A Python module to interface with the TMP119 temperature sensor. The TMP119 is +an ultra-high-accuracy, low-power digital temperature sensor from Texas +Instruments with an I2C-compatible interface. Tested on Raspberry Pi with +Raspberry Pi OS. + +The python SMBus library must be installed. + + sudo apt-get install python3-smbus + +# Usage + + import tmp119 + +### TMP119(bus=1, address=0x48) + + sensor = tmp119.TMP119() # Use default I2C bus 1, address 0x48 + sensor = tmp119.TMP119(0) # Specify I2C bus 0 + sensor = tmp119.TMP119(1, 0x49) # Specify bus and I2C address + +The TMP119 supports up to four I2C addresses, selected by the ADD0 pin: + + 0x48 (ADD0 to GND, default) + 0x49 (ADD0 to V+) + 0x4A (ADD0 to SDA) + 0x4B (ADD0 to SCL) + +### init() + +Initialize the sensor. This needs to be called before using any other methods. +It verifies the device ID and configures the sensor for the fastest update rate +(no averaging, shortest standby delay, ~15.5 ms cycle). Call `set_averaging()` / +`set_read_delay()` afterwards to trade speed for lower noise. + + sensor.init() + +Returns True if the sensor was successfully initialized, False otherwise. + +### read() + +Read the sensor and update the temperature. The TMP119 runs in +continuous-conversion mode, so this always returns the latest result. + + sensor.read() + +Returns True if the read was successful, False otherwise. + +### temperature(conversion=UNITS_Centigrade) + +Get the most recent temperature measurement. + + sensor.temperature() # Centigrade (default) + sensor.temperature(tmp119.UNITS_Fahrenheit) # Fahrenheit + +Valid arguments are: + + tmp119.UNITS_Centigrade + tmp119.UNITS_Fahrenheit + tmp119.UNITS_Kelvin + +Returns the most recent temperature in the requested units, or temperature in +degrees Centigrade if invalid units specified. Call `read()` to update. + +### set_averaging(avg) + +Set the conversion averaging mode. More averaging reduces noise but lengthens +the conversion cycle. Returns True if the write succeeded. + + sensor.set_averaging(tmp119.TMP119_AVERAGE_64X) + +Valid arguments are: + + tmp119.TMP119_AVERAGE_1X + tmp119.TMP119_AVERAGE_8X + tmp119.TMP119_AVERAGE_32X + tmp119.TMP119_AVERAGE_64X + +### set_read_delay(delay) + +Set the minimum standby delay between conversions in continuous-conversion +mode. The resulting conversion cycle time depends on both this and the +averaging setting (see datasheet Table 8-6). Returns True if the write +succeeded. + + sensor.set_read_delay(tmp119.TMP119_DELAY_1000_MS) + +Valid arguments are: + + tmp119.TMP119_DELAY_0_MS + tmp119.TMP119_DELAY_125_MS + tmp119.TMP119_DELAY_250_MS + tmp119.TMP119_DELAY_500_MS + tmp119.TMP119_DELAY_1000_MS + tmp119.TMP119_DELAY_4000_MS + tmp119.TMP119_DELAY_8000_MS + tmp119.TMP119_DELAY_16000_MS + +### get_config() / set_config(config) + +Read or write the raw 16-bit configuration register (address 0x01) for advanced +use. `set_config()` returns True if the write succeeded; read-only bits are +ignored by the device. + + config = sensor.get_config() + sensor.set_config(config) + +# Reference + +You can find the [TMP119 datasheet here](https://www.ti.com/lit/ds/symlink/tmp119.pdf). diff --git a/example.py b/example.py new file mode 100644 index 0000000..64f4e1b --- /dev/null +++ b/example.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +import tmp119 +from time import sleep + +# Optional constructor parameters: TMP119(bus, address) +# bus - I2C bus number (default 1; busses are listed as /dev/i2c*) +# address - I2C address set by the ADD0 pin (default 0x48): +# 0x48 (GND), 0x49 (V+), 0x4A (SDA), 0x4B (SCL) +sensor = tmp119.TMP119() + +if not sensor.init(): + print("Error initializing sensor") + exit(1) + +# init() configures the fastest update rate. Override it here with low-noise +# settings: average 64 conversions and update roughly once per second. +# +# Averaging options (set_averaging): +# TMP119_AVERAGE_1X, TMP119_AVERAGE_8X, TMP119_AVERAGE_32X, TMP119_AVERAGE_64X +# +# Standby delay options (set_read_delay): +# TMP119_DELAY_0_MS, TMP119_DELAY_125_MS, TMP119_DELAY_250_MS, +# TMP119_DELAY_500_MS, TMP119_DELAY_1000_MS, TMP119_DELAY_4000_MS, +# TMP119_DELAY_8000_MS, TMP119_DELAY_16000_MS +sensor.set_averaging(tmp119.TMP119_AVERAGE_64X) +sensor.set_read_delay(tmp119.TMP119_DELAY_1000_MS) + +while True: + if not sensor.read(): + print("Error reading sensor") + exit(1) + print("Temperature: %.2f C\t%.2f F" % ( + sensor.temperature(), + sensor.temperature(tmp119.UNITS_Fahrenheit))) + sleep(1) diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..0fdfc9c --- /dev/null +++ b/setup.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python + +from setuptools import setup, find_packages + +with open("README.md", "r") as f: + long_description = f.read() + +setup(name='bluerobotics-tmp119', + version='0.0.1', + description='A python module for the TMP119 digital temperature sensor', + long_description=long_description, + long_description_content_type='text/markdown', + author='Blue Robotics', + author_email='support@bluerobotics.com', + url='https://www.bluerobotics.com', + packages=find_packages(), + classifiers=[ + "Programming Language :: Python", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + ] + ) diff --git a/tmp119/__init__.py b/tmp119/__init__.py new file mode 100644 index 0000000..4731daf --- /dev/null +++ b/tmp119/__init__.py @@ -0,0 +1 @@ +from .tmp119 import * diff --git a/tmp119/__pycache__/__init__.cpython-313.pyc b/tmp119/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ec873fd1dc4ff4f9cacdc79decb525b3faa3403a GIT binary patch literal 189 zcmey&%ge<81Yf^sWr+jn#~=<2FhLog`GAb648aV+jNT09ObQI?Oq$Fu8G(YDjJFuI z{D34|Np69mq2)@3&mcXw9GtCULW@(2iesuO3=IroT=J7kb5rw5ieua}OFT-GVj#+O z3o1)8^7Ej)nE3e2yv&mLc)fzkTO2mI`6;D2sdhyiKm$Q`6@wTbm>C%v?=pxMu>m;% D*fK85 literal 0 HcmV?d00001 diff --git a/tmp119/__pycache__/tmp119.cpython-313.pyc b/tmp119/__pycache__/tmp119.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..38ccb7cf8d39137417022ab1b984e369d07ede65 GIT binary patch literal 6298 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 literal 0 HcmV?d00001 diff --git a/tmp119/tmp119.py b/tmp119/tmp119.py new file mode 100644 index 0000000..66dbaee --- /dev/null +++ b/tmp119/tmp119.py @@ -0,0 +1,160 @@ +"""Driver for the Texas Instruments TMP119 high-accuracy temperature sensor.""" + +try: + import smbus +except ImportError: + print('Try sudo apt-get install python3-smbus') + +# Valid units +UNITS_Centigrade = 1 +UNITS_Fahrenheit = 2 +UNITS_Kelvin = 3 + +# Conversion averaging mode (AVG[1:0], config register bits 6:5). +# More averaging reduces noise but lengthens the conversion cycle. +TMP119_AVERAGE_1X = 0 # No averaging +TMP119_AVERAGE_8X = 1 # 8 averaged conversions (power-on default) +TMP119_AVERAGE_32X = 2 # 32 averaged conversions +TMP119_AVERAGE_64X = 3 # 64 averaged conversions + +# Minimum standby delay between conversions in continuous-conversion mode +# (CONV[2:0], config register bits 9:7). The total cycle time also depends on +# the averaging setting (see datasheet Table 8-6). +TMP119_DELAY_0_MS = 0 +TMP119_DELAY_125_MS = 1 +TMP119_DELAY_250_MS = 2 +TMP119_DELAY_500_MS = 3 +TMP119_DELAY_1000_MS = 4 # power-on default +TMP119_DELAY_4000_MS = 5 +TMP119_DELAY_8000_MS = 6 +TMP119_DELAY_16000_MS = 7 + + +class TMP119: + + # Registers + _TEMP_REG = 0x00 + _CONFIG_REG = 0x01 + _DEVICE_ID_REG = 0x0F + _DEVICE_ID = 0x2117 + + # CONV[2:0] (standby delay), config register bits 9:7 + _CONV_SHIFT = 7 + _CONV_MASK = 0x0380 + # AVG[1:0] (averaging), config register bits 6:5 + _AVG_SHIFT = 5 + _AVG_MASK = 0x0060 + + # Each LSB of the temperature register represents this many degrees C + _LSB_C = 0.0078125 + + def __init__(self, bus=1, address=0x48): + """Create a sensor on the given I2C bus and address. + + The TMP119 supports up to four I2C addresses (0x48 - 0x4B), selected by + the ADD0 pin. The default address (ADD0 to GND) is 0x48. + """ + self._address = address + + # Degrees C + self._temperature = 0 + + try: + self._bus = smbus.SMBus(bus) + except Exception: + print("Bus %d is not available." % bus) + print("Available busses are listed as /dev/i2c*") + self._bus = None + + def init(self): + """Verify the device and configure it for the fastest update rate. + + Returns True on success, False if the bus is unavailable or the device + ID does not match. Call set_averaging()/set_read_delay() afterwards to + trade speed for lower noise. + """ + if self._bus is None: + print("No bus!") + return False + + if self.get_device_id() != self._DEVICE_ID: + return False + + # Configure for the fastest update rate: no averaging and the shortest + # standby delay, giving a ~15.5 ms conversion cycle (datasheet Table + # 8-6). Clearing the AVG and CONV bits sets AVG = 1X and delay = 0 ms. + config = self.get_config() + config &= ~(self._AVG_MASK | self._CONV_MASK) + return self.set_config(config) + + def read(self): + """Read the latest conversion and update the stored temperature. + + Returns True on success, False if the bus is unavailable. + """ + if self._bus is None: + print("No bus!") + return False + + # The TMP119 powers up in continuous-conversion mode, so the + # temperature register always holds the most recent conversion. + raw = self._read_register(self._TEMP_REG) + + # Data is in 2's complement format + if raw > 32767: + raw -= 65536 + + self._temperature = raw * self._LSB_C + return True + + def temperature(self, conversion=UNITS_Centigrade): + """Return the most recent temperature in the requested units. + + Defaults to degrees Centigrade. Call read() to update the value. + """ + if conversion == UNITS_Fahrenheit: + return (9 / 5) * self._temperature + 32 + elif conversion == UNITS_Kelvin: + return self._temperature + 273.15 + return self._temperature + + def set_averaging(self, avg): + """Set the conversion averaging mode, preserving other config bits.""" + config = self.get_config() + config = (config & ~self._AVG_MASK) | \ + ((avg << self._AVG_SHIFT) & self._AVG_MASK) + return self.set_config(config) + + def set_read_delay(self, delay): + """Set the standby delay between conversions, preserving other bits.""" + config = self.get_config() + config = (config & ~self._CONV_MASK) | \ + ((delay << self._CONV_SHIFT) & self._CONV_MASK) + return self.set_config(config) + + def get_config(self): + """Read the raw 16-bit configuration register (address 0x01).""" + return self._read_register(self._CONFIG_REG) + + def set_config(self, config): + """Write the raw 16-bit configuration register (address 0x01). + + Read-only bits are ignored by the device. + """ + return self._write_register(self._CONFIG_REG, config) + + def get_device_id(self): + """Read the raw 16-bit device ID register (address 0x0F).""" + return self._read_register(self._DEVICE_ID_REG) + + # The TMP119 transfers 16-bit registers MSB first. smbus word transfers are + # little-endian, so we use block transfers and order the bytes ourselves. + def _read_register(self, register): + data = self._bus.read_i2c_block_data(self._address, register, 2) + return (data[0] << 8) | data[1] + + def _write_register(self, register, value): + value &= 0xFFFF + self._bus.write_i2c_block_data( + self._address, register, [(value >> 8) & 0xFF, value & 0xFF]) + return True From 52a6726cbbd78f06c786c045425b3c2aacc5a1fc Mon Sep 17 00:00:00 2001 From: Zachariah Mears Date: Thu, 11 Jun 2026 11:48:45 -0700 Subject: [PATCH 2/5] Add CI testing --- .github/workflows/test.yml | 26 +++++++++++++ .gitignore | 11 ++++++ conftest.py | 47 ++++++++++++++++++++++ tests/test_tmp119.py | 80 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 164 insertions(+) create mode 100644 .github/workflows/test.yml create mode 100644 .gitignore create mode 100644 conftest.py create mode 100644 tests/test_tmp119.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..dfa1c59 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,26 @@ +name: Test + +on: ["pull_request", "push"] + +jobs: + Test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pytest + pip install -e . + + - name: Run tests + run: pytest -v diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..482629c --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +# Byte-compiled / optimized files +__pycache__/ +*.py[cod] + +# Test / tooling caches +.pytest_cache/ + +# Packaging artifacts +build/ +dist/ +*.egg-info/ diff --git a/conftest.py b/conftest.py new file mode 100644 index 0000000..26acb4e --- /dev/null +++ b/conftest.py @@ -0,0 +1,47 @@ +"""Shared pytest fixtures. + +The tests run without hardware by substituting a fake ``smbus`` module for the +real one. The fake is injected into ``sys.modules`` here, before ``tmp119`` is +imported anywhere, so the library's top-level ``import smbus`` resolves to this +in-memory stand-in instead of a physical I2C bus. +""" + +import sys +import types + +import pytest + + +class FakeSMBus: + """In-memory stand-in for ``smbus.SMBus`` used for hardware-free testing. + + Registers are stored big-endian as ``[msb, lsb]``, matching how the TMP119 + transfers its 16-bit registers over I2C. Tests can read/modify ``regs`` to + simulate device state. + """ + + def __init__(self, bus): + self.bus = bus + self.regs = { + 0x00: [0x00, 0x00], # TEMP + 0x01: [0x02, 0x20], # CONFIG (power-on default 0x0220) + 0x0F: [0x21, 0x17], # DEVICE_ID (0x2117) + } + + def read_i2c_block_data(self, address, register, length): + return list(self.regs[register])[:length] + + def write_i2c_block_data(self, address, register, data): + self.regs[register] = list(data) + + +_fake_smbus = types.ModuleType("smbus") +_fake_smbus.SMBus = FakeSMBus +sys.modules["smbus"] = _fake_smbus + + +@pytest.fixture +def sensor(): + """A TMP119 backed by a fresh in-memory FakeSMBus.""" + import tmp119 + return tmp119.TMP119() diff --git a/tests/test_tmp119.py b/tests/test_tmp119.py new file mode 100644 index 0000000..3133e4f --- /dev/null +++ b/tests/test_tmp119.py @@ -0,0 +1,80 @@ +import pytest + +import tmp119 + +# Config register bit masks (mirrors the private constants in the driver) +AVG_MASK = 0x0060 +CONV_MASK = 0x0380 + + +def test_device_id(sensor): + assert sensor.get_device_id() == 0x2117 + + +def test_init_success_clears_avg_and_conv(sensor): + assert sensor.init() is True + assert sensor.get_config() & (AVG_MASK | CONV_MASK) == 0 + + +def test_init_preserves_unrelated_config_bits(sensor): + # Set a bit outside the AVG/CONV masks; init() must leave it untouched. + sensor._bus.regs[0x01] = [0x82, 0x20] # 0x8220 + assert sensor.init() is True + assert sensor.get_config() == 0x8000 + + +def test_init_fails_on_wrong_device_id(sensor): + sensor._bus.regs[0x0F] = [0x00, 0x00] + assert sensor.init() is False + + +def test_init_returns_false_without_bus(sensor): + sensor._bus = None + assert sensor.init() is False + + +def test_read_returns_false_without_bus(sensor): + sensor._bus = None + assert sensor.read() is False + + +def test_read_positive_temperature(sensor): + sensor._bus.regs[0x00] = [0x0C, 0x80] # 3200 * 0.0078125 = 25.0 C + assert sensor.read() is True + assert sensor.temperature() == pytest.approx(25.0) + + +def test_read_negative_temperature(sensor): + sensor._bus.regs[0x00] = [0xFF, 0x00] # -256 * 0.0078125 = -2.0 C + sensor.read() + assert sensor.temperature() == pytest.approx(-2.0) + + +def test_temperature_unit_conversions(sensor): + sensor._bus.regs[0x00] = [0x0C, 0x80] # 25.0 C + sensor.read() + assert sensor.temperature(tmp119.UNITS_Centigrade) == pytest.approx(25.0) + assert sensor.temperature(tmp119.UNITS_Fahrenheit) == pytest.approx(77.0) + assert sensor.temperature(tmp119.UNITS_Kelvin) == pytest.approx(298.15) + + +def test_set_averaging(sensor): + assert sensor.set_averaging(tmp119.TMP119_AVERAGE_64X) is True + assert sensor.get_config() & AVG_MASK == (3 << 5) + + +def test_set_read_delay(sensor): + assert sensor.set_read_delay(tmp119.TMP119_DELAY_16000_MS) is True + assert sensor.get_config() & CONV_MASK == (7 << 7) + + +def test_set_averaging_preserves_read_delay(sensor): + sensor.set_read_delay(tmp119.TMP119_DELAY_8000_MS) + sensor.set_averaging(tmp119.TMP119_AVERAGE_32X) + assert sensor.get_config() & CONV_MASK == (6 << 7) + assert sensor.get_config() & AVG_MASK == (2 << 5) + + +def test_set_config_round_trips_16_bits(sensor): + sensor.set_config(0xABCD) + assert sensor.get_config() == 0xABCD From d6474f3a42820c2b9219524c52919fc84c6bc8d7 Mon Sep 17 00:00:00 2001 From: Zachariah Mears Date: Thu, 11 Jun 2026 11:55:09 -0700 Subject: [PATCH 3/5] delete unwanted files --- tmp119/__pycache__/__init__.cpython-313.pyc | Bin 189 -> 0 bytes tmp119/__pycache__/tmp119.cpython-313.pyc | Bin 6298 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 tmp119/__pycache__/__init__.cpython-313.pyc delete mode 100644 tmp119/__pycache__/tmp119.cpython-313.pyc diff --git a/tmp119/__pycache__/__init__.cpython-313.pyc b/tmp119/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index ec873fd1dc4ff4f9cacdc79decb525b3faa3403a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 189 zcmey&%ge<81Yf^sWr+jn#~=<2FhLog`GAb648aV+jNT09ObQI?Oq$Fu8G(YDjJFuI z{D34|Np69mq2)@3&mcXw9GtCULW@(2iesuO3=IroT=J7kb5rw5ieua}OFT-GVj#+O z3o1)8^7Ej)nE3e2yv&mLc)fzkTO2mI`6;D2sdhyiKm$Q`6@wTbm>C%v?=pxMu>m;% D*fK85 diff --git a/tmp119/__pycache__/tmp119.cpython-313.pyc b/tmp119/__pycache__/tmp119.cpython-313.pyc deleted file mode 100644 index 38ccb7cf8d39137417022ab1b984e369d07ede65..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6298 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 From 926a2783b69074c9e56fc8dfee821caeecdf9bb1 Mon Sep 17 00:00:00 2001 From: Zachariah Mears Date: Thu, 11 Jun 2026 16:11:57 -0700 Subject: [PATCH 4/5] Add release workflow --- .github/workflows/publish.yml | 53 +++++++++++++++++++++++++++++++++++ README.md | 27 +++++++++--------- example.py | 16 ++++++++--- setup.py | 3 +- tmp119/tmp119.py | 33 ++++++++++++++-------- 5 files changed, 102 insertions(+), 30 deletions(-) create mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..3214278 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,53 @@ +name: Publish to PyPI + +# Builds and publishes the package whenever a version tag (e.g. v1.0.0) is +# pushed. Uses PyPI Trusted Publishing (OIDC) - no API token needs to be +# stored as a secret. See the repo README for the one-time PyPI setup. + +on: + push: + tags: + - "v*" + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + + - name: Build distributions + run: | + python -m pip install --upgrade pip build + python -m build + + - name: Check distributions + run: | + pip install twine + twine check dist/* + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + + publish: + needs: build + runs-on: ubuntu-latest + environment: pypi + permissions: + id-token: write # required for Trusted Publishing (OIDC) + steps: + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/README.md b/README.md index 16d40ce..7b421e1 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ The TMP119 supports up to four I2C addresses, selected by the ADD0 pin: Initialize the sensor. This needs to be called before using any other methods. It verifies the device ID and configures the sensor for the fastest update rate -(no averaging, shortest standby delay, ~15.5 ms cycle). Call `set_averaging()` / +(no averaging, no added standby delay, ~15.5 ms cycle). Call `set_averaging()` / `set_read_delay()` afterwards to trade speed for lower noise. sensor.init() @@ -64,30 +64,31 @@ degrees Centigrade if invalid units specified. Call `read()` to update. ### set_averaging(avg) -Set the conversion averaging mode. More averaging reduces noise but lengthens -the conversion cycle. Returns True if the write succeeded. +Set the conversion averaging mode. More averaging reduces noise but takes +longer to produce each result. Returns True if the write succeeded. sensor.set_averaging(tmp119.TMP119_AVERAGE_64X) -Valid arguments are: +Valid arguments are (with the time each takes per result): - tmp119.TMP119_AVERAGE_1X - tmp119.TMP119_AVERAGE_8X - tmp119.TMP119_AVERAGE_32X - tmp119.TMP119_AVERAGE_64X + tmp119.TMP119_AVERAGE_1X # 15.5 ms + tmp119.TMP119_AVERAGE_8X # 125 ms + tmp119.TMP119_AVERAGE_32X # 500 ms + tmp119.TMP119_AVERAGE_64X # 1 s ### set_read_delay(delay) -Set the minimum standby delay between conversions in continuous-conversion -mode. The resulting conversion cycle time depends on both this and the -averaging setting (see datasheet Table 8-6). Returns True if the write -succeeded. +Set the *minimum* standby delay between conversions in continuous-conversion +mode. The actual time between readings is the greater of this standby delay and +the averaging time (see datasheet Table 8-6); for example, `TMP119_AVERAGE_64X` +always yields at least a ~1 s cycle because the averaging alone takes 1 s, +regardless of the delay setting. Returns True if the write succeeded. sensor.set_read_delay(tmp119.TMP119_DELAY_1000_MS) Valid arguments are: - tmp119.TMP119_DELAY_0_MS + tmp119.TMP119_DELAY_NONE tmp119.TMP119_DELAY_125_MS tmp119.TMP119_DELAY_250_MS tmp119.TMP119_DELAY_500_MS diff --git a/example.py b/example.py index 64f4e1b..113f91f 100644 --- a/example.py +++ b/example.py @@ -15,13 +15,21 @@ # init() configures the fastest update rate. Override it here with low-noise # settings: average 64 conversions and update roughly once per second. # -# Averaging options (set_averaging): -# TMP119_AVERAGE_1X, TMP119_AVERAGE_8X, TMP119_AVERAGE_32X, TMP119_AVERAGE_64X +# Averaging options (set_averaging) and the time each takes per result: +# TMP119_AVERAGE_1X -> 15.5 ms +# TMP119_AVERAGE_8X -> 125 ms +# TMP119_AVERAGE_32X -> 500 ms +# TMP119_AVERAGE_64X -> 1 s # -# Standby delay options (set_read_delay): -# TMP119_DELAY_0_MS, TMP119_DELAY_125_MS, TMP119_DELAY_250_MS, +# Minimum standby delay options (set_read_delay): +# TMP119_DELAY_NONE, TMP119_DELAY_125_MS, TMP119_DELAY_250_MS, # TMP119_DELAY_500_MS, TMP119_DELAY_1000_MS, TMP119_DELAY_4000_MS, # TMP119_DELAY_8000_MS, TMP119_DELAY_16000_MS +# +# The actual time between readings is the greater of the averaging time above +# and the standby delay (datasheet Table 8-6). So 64x averaging with +# TMP119_DELAY_1000_MS gives ~1 s, but 64x with TMP119_DELAY_NONE is also ~1 s +# since the averaging itself takes 1 s. sensor.set_averaging(tmp119.TMP119_AVERAGE_64X) sensor.set_read_delay(tmp119.TMP119_DELAY_1000_MS) diff --git a/setup.py b/setup.py index 0fdfc9c..6a0e02b 100644 --- a/setup.py +++ b/setup.py @@ -14,8 +14,9 @@ author_email='support@bluerobotics.com', url='https://www.bluerobotics.com', packages=find_packages(), + python_requires='>=3.6', classifiers=[ - "Programming Language :: Python", + "Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", ] diff --git a/tmp119/tmp119.py b/tmp119/tmp119.py index 66dbaee..24b740d 100644 --- a/tmp119/tmp119.py +++ b/tmp119/tmp119.py @@ -11,16 +11,25 @@ UNITS_Kelvin = 3 # Conversion averaging mode (AVG[1:0], config register bits 6:5). -# More averaging reduces noise but lengthens the conversion cycle. -TMP119_AVERAGE_1X = 0 # No averaging -TMP119_AVERAGE_8X = 1 # 8 averaged conversions (power-on default) -TMP119_AVERAGE_32X = 2 # 32 averaged conversions -TMP119_AVERAGE_64X = 3 # 64 averaged conversions - -# Minimum standby delay between conversions in continuous-conversion mode -# (CONV[2:0], config register bits 9:7). The total cycle time also depends on -# the averaging setting (see datasheet Table 8-6). -TMP119_DELAY_0_MS = 0 +# More averaging reduces noise but takes longer to produce each result. The +# time to compute one averaged result is the per-mode minimum conversion time +# shown below (datasheet Table 8-6). Power-on default: TMP119_AVERAGE_8X. +TMP119_AVERAGE_1X = 0 # No averaging (min conversion time 15.5 ms) +TMP119_AVERAGE_8X = 1 # 8 conversions (min conversion time 125 ms, default) +TMP119_AVERAGE_32X = 2 # 32 conversions (min conversion time 500 ms) +TMP119_AVERAGE_64X = 3 # 64 conversions (min conversion time 1 s) + +# Minimum standby delay inserted between conversions in continuous-conversion +# mode (CONV[2:0], config register bits 9:7). +# +# This is not the actual time between readings on its own. The actual +# conversion cycle time is the greater of this standby delay and the time +# needed by the selected averaging mode (see the averaging constants above). +# For example, TMP119_DELAY_NONE with TMP119_AVERAGE_64X still produces a ~1 s +# cycle because the 64x average alone takes 1 s. See datasheet Table 8-6. +# +# Power-on default: TMP119_DELAY_1000_MS. +TMP119_DELAY_NONE = 0 # no added standby delay TMP119_DELAY_125_MS = 1 TMP119_DELAY_250_MS = 2 TMP119_DELAY_500_MS = 3 @@ -80,9 +89,9 @@ def init(self): if self.get_device_id() != self._DEVICE_ID: return False - # Configure for the fastest update rate: no averaging and the shortest + # Configure for the fastest update rate: no averaging and no added # standby delay, giving a ~15.5 ms conversion cycle (datasheet Table - # 8-6). Clearing the AVG and CONV bits sets AVG = 1X and delay = 0 ms. + # 8-6). Clearing the AVG and CONV bits sets AVG = 1X and delay = none. config = self.get_config() config &= ~(self._AVG_MASK | self._CONV_MASK) return self.set_config(config) From 72bf3160b46d182e126c1886221c2a99d51e738e Mon Sep 17 00:00:00 2001 From: Zachariah Mears Date: Fri, 12 Jun 2026 13:23:40 -0700 Subject: [PATCH 5/5] adjust publishing workflow --- .github/workflows/deploy_to_pypi.yml | 45 +++++++++++++++++++++++ .github/workflows/publish.yml | 53 ---------------------------- 2 files changed, 45 insertions(+), 53 deletions(-) create mode 100644 .github/workflows/deploy_to_pypi.yml delete mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/deploy_to_pypi.yml b/.github/workflows/deploy_to_pypi.yml new file mode 100644 index 0000000..ea34c44 --- /dev/null +++ b/.github/workflows/deploy_to_pypi.yml @@ -0,0 +1,45 @@ +name: Deploy to PyPI + +on: + push: + tags: + - 'v*.*.*' # This will trigger the workflow only when a tag that matches the pattern is pushed + +permissions: + id-token: write + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + + - name: Check Tag and setup.py Version Match + run: | + TAG_VERSION=${GITHUB_REF#refs/tags/v} + SETUP_VERSION=$(grep -oE "version='([^']+)" setup.py | grep -oE "[^'=]+$") + if [[ "$TAG_VERSION" != "$SETUP_VERSION" ]]; then + echo "Tag version $TAG_VERSION does not match setup.py version $SETUP_VERSION." + exit 1 + fi + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install setuptools wheel + + - name: Build package + run: | + python setup.py sdist bdist_wheel + + - name: Publish package distributions to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: ./dist diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml deleted file mode 100644 index 3214278..0000000 --- a/.github/workflows/publish.yml +++ /dev/null @@ -1,53 +0,0 @@ -name: Publish to PyPI - -# Builds and publishes the package whenever a version tag (e.g. v1.0.0) is -# pushed. Uses PyPI Trusted Publishing (OIDC) - no API token needs to be -# stored as a secret. See the repo README for the one-time PyPI setup. - -on: - push: - tags: - - "v*" - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.x" - - - name: Build distributions - run: | - python -m pip install --upgrade pip build - python -m build - - - name: Check distributions - run: | - pip install twine - twine check dist/* - - - name: Upload build artifacts - uses: actions/upload-artifact@v4 - with: - name: dist - path: dist/ - - publish: - needs: build - runs-on: ubuntu-latest - environment: pypi - permissions: - id-token: write # required for Trusted Publishing (OIDC) - steps: - - name: Download build artifacts - uses: actions/download-artifact@v4 - with: - name: dist - path: dist/ - - - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@release/v1