From 5ec5178b7b427684b1cd80e82a6746f19c857aa9 Mon Sep 17 00:00:00 2001 From: Luthira Abeykoon <79228258+Luthiraa@users.noreply.github.com> Date: Mon, 30 Sep 2024 12:07:16 -0400 Subject: [PATCH 01/19] Create temperatureSensorTest.py --- temperatureSensorTest.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 temperatureSensorTest.py diff --git a/temperatureSensorTest.py b/temperatureSensorTest.py new file mode 100644 index 00000000..7afee4a1 --- /dev/null +++ b/temperatureSensorTest.py @@ -0,0 +1,16 @@ +import time +import board +import busio +import adafruit_bme680 + +i2c = busio.I2C(scl=board.GP17, sda=board.GP16) +sensor = adafruit_bme680.Adafruit_BME680_I2C(i2c) + +sensor.sea_level_pressure = 1013.25 + +while True: + print(f"temp: {sensor.temperature:.2f} C") + print(f"humidity: {sensor.humidity:.2f} %") + print(f"pressure: {sensor.pressure:.2f} ") + + time.sleep(2) From a0b12aee1b3c547b79b12064d4f4347e5e8ee98e Mon Sep 17 00:00:00 2001 From: Luthira A <79228258+Luthiraa@users.noreply.github.com> Date: Fri, 18 Oct 2024 21:57:21 -0400 Subject: [PATCH 02/19] Update temperatureSensor with related libraries (WIP) --- .../archive/temperatureSensorTest.py | 2 +- .../tempSensor/lib/Adafruit_IO/__init__.py | 25 + .../tempSensor/lib/Adafruit_IO/_version.py | 1 + scripts/tempSensor/lib/Adafruit_IO/client.py | 482 ++ scripts/tempSensor/lib/Adafruit_IO/errors.py | 61 + scripts/tempSensor/lib/Adafruit_IO/model.py | 149 + .../tempSensor/lib/Adafruit_IO/mqtt_client.py | 306 + scripts/tempSensor/lib/CHANGELOG.md | 50 + scripts/tempSensor/lib/LICENSE | 21 + scripts/tempSensor/lib/README.md | 56 + scripts/tempSensor/lib/adafruit_bme680.py | 769 +++ .../lib/adafruit_bus_device/__init__.py | 0 .../lib/adafruit_bus_device/i2c_device.mpy | Bin 0 -> 1172 bytes .../lib/adafruit_bus_device/spi_device.mpy | Bin 0 -> 821 bytes .../lib/adafruit_io-2.8.0.dist-info/METADATA | 96 + .../lib/adafruit_io-2.8.0.dist-info/RECORD | 9 + .../lib/bme680-2.0.0.dist-info/METADATA | 156 + .../lib/bme680-2.0.0.dist-info/RECORD | 7 + scripts/tempSensor/lib/bme680/__init__.py | 486 ++ scripts/tempSensor/lib/bme680/constants.py | 413 ++ .../lib/board-1.0.dist-info/METADATA | 370 ++ .../tempSensor/lib/board-1.0.dist-info/RECORD | 3 + scripts/tempSensor/lib/board.py | 775 +++ scripts/tempSensor/lib/ez_setup.py | 332 ++ .../lib/functools-0.0.7.dist-info/METADATA | 6 + .../lib/functools-0.0.7.dist-info/RECORD | 3 + scripts/tempSensor/lib/functools.py | 31 + .../lib/itertools-0.2.3.dist-info/METADATA | 6 + .../lib/itertools-0.2.3.dist-info/RECORD | 3 + scripts/tempSensor/lib/itertools.py | 77 + scripts/tempSensor/lib/paho/__init__.py | 0 scripts/tempSensor/lib/paho/mqtt/__init__.py | 5 + scripts/tempSensor/lib/paho/mqtt/client.py | 5004 +++++++++++++++++ scripts/tempSensor/lib/paho/mqtt/enums.py | 113 + scripts/tempSensor/lib/paho/mqtt/matcher.py | 78 + .../tempSensor/lib/paho/mqtt/packettypes.py | 43 + .../tempSensor/lib/paho/mqtt/properties.py | 421 ++ scripts/tempSensor/lib/paho/mqtt/publish.py | 306 + scripts/tempSensor/lib/paho/mqtt/py.typed | 0 .../tempSensor/lib/paho/mqtt/reasoncodes.py | 223 + scripts/tempSensor/lib/paho/mqtt/subscribe.py | 281 + .../lib/paho/mqtt/subscribeoptions.py | 113 + .../lib/paho_mqtt-2.1.0.dist-info/METADATA | 635 +++ .../lib/paho_mqtt-2.1.0.dist-info/RECORD | 14 + .../lib/requests-0.10.0.dist-info/METADATA | 6 + .../lib/requests-0.10.0.dist-info/RECORD | 3 + scripts/tempSensor/lib/requests/__init__.py | 220 + .../lib/smbus2-0.4.3.dist-info/METADATA | 237 + .../lib/smbus2-0.4.3.dist-info/RECORD | 6 + scripts/tempSensor/lib/smbus2/__init__.py | 26 + scripts/tempSensor/lib/smbus2/py.typed | 0 scripts/tempSensor/lib/smbus2/smbus2.py | 658 +++ scripts/tempSensor/lib/smbus2/smbus2.pyi | 148 + scripts/tempSensor/main.py | 20 + 54 files changed, 13254 insertions(+), 1 deletion(-) rename temperatureSensorTest.py => scripts/tempSensor/archive/temperatureSensorTest.py (84%) create mode 100644 scripts/tempSensor/lib/Adafruit_IO/__init__.py create mode 100644 scripts/tempSensor/lib/Adafruit_IO/_version.py create mode 100644 scripts/tempSensor/lib/Adafruit_IO/client.py create mode 100644 scripts/tempSensor/lib/Adafruit_IO/errors.py create mode 100644 scripts/tempSensor/lib/Adafruit_IO/model.py create mode 100644 scripts/tempSensor/lib/Adafruit_IO/mqtt_client.py create mode 100644 scripts/tempSensor/lib/CHANGELOG.md create mode 100644 scripts/tempSensor/lib/LICENSE create mode 100644 scripts/tempSensor/lib/README.md create mode 100644 scripts/tempSensor/lib/adafruit_bme680.py create mode 100644 scripts/tempSensor/lib/adafruit_bus_device/__init__.py create mode 100644 scripts/tempSensor/lib/adafruit_bus_device/i2c_device.mpy create mode 100644 scripts/tempSensor/lib/adafruit_bus_device/spi_device.mpy create mode 100644 scripts/tempSensor/lib/adafruit_io-2.8.0.dist-info/METADATA create mode 100644 scripts/tempSensor/lib/adafruit_io-2.8.0.dist-info/RECORD create mode 100644 scripts/tempSensor/lib/bme680-2.0.0.dist-info/METADATA create mode 100644 scripts/tempSensor/lib/bme680-2.0.0.dist-info/RECORD create mode 100644 scripts/tempSensor/lib/bme680/__init__.py create mode 100644 scripts/tempSensor/lib/bme680/constants.py create mode 100644 scripts/tempSensor/lib/board-1.0.dist-info/METADATA create mode 100644 scripts/tempSensor/lib/board-1.0.dist-info/RECORD create mode 100644 scripts/tempSensor/lib/board.py create mode 100644 scripts/tempSensor/lib/ez_setup.py create mode 100644 scripts/tempSensor/lib/functools-0.0.7.dist-info/METADATA create mode 100644 scripts/tempSensor/lib/functools-0.0.7.dist-info/RECORD create mode 100644 scripts/tempSensor/lib/functools.py create mode 100644 scripts/tempSensor/lib/itertools-0.2.3.dist-info/METADATA create mode 100644 scripts/tempSensor/lib/itertools-0.2.3.dist-info/RECORD create mode 100644 scripts/tempSensor/lib/itertools.py create mode 100644 scripts/tempSensor/lib/paho/__init__.py create mode 100644 scripts/tempSensor/lib/paho/mqtt/__init__.py create mode 100644 scripts/tempSensor/lib/paho/mqtt/client.py create mode 100644 scripts/tempSensor/lib/paho/mqtt/enums.py create mode 100644 scripts/tempSensor/lib/paho/mqtt/matcher.py create mode 100644 scripts/tempSensor/lib/paho/mqtt/packettypes.py create mode 100644 scripts/tempSensor/lib/paho/mqtt/properties.py create mode 100644 scripts/tempSensor/lib/paho/mqtt/publish.py create mode 100644 scripts/tempSensor/lib/paho/mqtt/py.typed create mode 100644 scripts/tempSensor/lib/paho/mqtt/reasoncodes.py create mode 100644 scripts/tempSensor/lib/paho/mqtt/subscribe.py create mode 100644 scripts/tempSensor/lib/paho/mqtt/subscribeoptions.py create mode 100644 scripts/tempSensor/lib/paho_mqtt-2.1.0.dist-info/METADATA create mode 100644 scripts/tempSensor/lib/paho_mqtt-2.1.0.dist-info/RECORD create mode 100644 scripts/tempSensor/lib/requests-0.10.0.dist-info/METADATA create mode 100644 scripts/tempSensor/lib/requests-0.10.0.dist-info/RECORD create mode 100644 scripts/tempSensor/lib/requests/__init__.py create mode 100644 scripts/tempSensor/lib/smbus2-0.4.3.dist-info/METADATA create mode 100644 scripts/tempSensor/lib/smbus2-0.4.3.dist-info/RECORD create mode 100644 scripts/tempSensor/lib/smbus2/__init__.py create mode 100644 scripts/tempSensor/lib/smbus2/py.typed create mode 100644 scripts/tempSensor/lib/smbus2/smbus2.py create mode 100644 scripts/tempSensor/lib/smbus2/smbus2.pyi create mode 100644 scripts/tempSensor/main.py diff --git a/temperatureSensorTest.py b/scripts/tempSensor/archive/temperatureSensorTest.py similarity index 84% rename from temperatureSensorTest.py rename to scripts/tempSensor/archive/temperatureSensorTest.py index 7afee4a1..168b98f5 100644 --- a/temperatureSensorTest.py +++ b/scripts/tempSensor/archive/temperatureSensorTest.py @@ -1,7 +1,7 @@ import time import board import busio -import adafruit_bme680 +import scripts.tempSensor.lib.adafruit_bme680 as adafruit_bme680 i2c = busio.I2C(scl=board.GP17, sda=board.GP16) sensor = adafruit_bme680.Adafruit_BME680_I2C(i2c) diff --git a/scripts/tempSensor/lib/Adafruit_IO/__init__.py b/scripts/tempSensor/lib/Adafruit_IO/__init__.py new file mode 100644 index 00000000..e34eb3d7 --- /dev/null +++ b/scripts/tempSensor/lib/Adafruit_IO/__init__.py @@ -0,0 +1,25 @@ +# Copyright (c) 2014 Adafruit Industries +# Author: Tony DiCola + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +from .client import Client +from .mqtt_client import MQTTClient +from .errors import AdafruitIOError, RequestError, ThrottlingError, MQTTError +from .model import Data, Feed, Group, Dashboard, Block, Layout +from ._version import __version__ diff --git a/scripts/tempSensor/lib/Adafruit_IO/_version.py b/scripts/tempSensor/lib/Adafruit_IO/_version.py new file mode 100644 index 00000000..892994aa --- /dev/null +++ b/scripts/tempSensor/lib/Adafruit_IO/_version.py @@ -0,0 +1 @@ +__version__ = "2.8.0" diff --git a/scripts/tempSensor/lib/Adafruit_IO/client.py b/scripts/tempSensor/lib/Adafruit_IO/client.py new file mode 100644 index 00000000..3f923260 --- /dev/null +++ b/scripts/tempSensor/lib/Adafruit_IO/client.py @@ -0,0 +1,482 @@ +# Copyright (c) 2018 Adafruit Industries +# Authors: Justin Cooper & Tony DiCola + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +import time +from time import struct_time +import json +import platform +import pkg_resources +import re +from urllib.parse import urlparse +from urllib.parse import parse_qs +# import logging + +import requests + +from .errors import RequestError, ThrottlingError +from .model import Data, Feed, Group, Dashboard, Block, Layout + +DEFAULT_PAGE_LIMIT = 100 + +# set outgoing version, pulled from setup.py +version = pkg_resources.require("Adafruit_IO")[0].version +default_headers = { + 'User-Agent': 'AdafruitIO-Python/{0} ({1}, {2} {3})'.format(version, + platform.platform(), + platform.python_implementation(), + platform.python_version()) +} + +class Client(object): + """Client instance for interacting with the Adafruit IO service using its + REST API. Use this client class to send, receive, and enumerate feed data. + """ + + def __init__(self, username, key, proxies=None, base_url='https://io.adafruit.com'): + """Create an instance of the Adafruit IO REST API client. Key must be + provided and set to your Adafruit IO access key value. Optionaly + provide a proxies dict in the format used by the requests library, + and a base_url to point at a different Adafruit IO service + (the default is the production Adafruit IO service over SSL). + """ + self.username = username + self.key = key + self.proxies = proxies + # self.logger = logging.basicConfig(level=logging.DEBUG, + # format='%(asctime)s - %(levelname)s - %(message)s') + + # Save URL without trailing slash as it will be added later when + # constructing the path. + self.base_url = base_url.rstrip('/') + + # Store the last response of a get or post + self._last_response = None + + @staticmethod + def to_red(data): + """Hex color feed to red channel. + + :param int data: Color value, in hexadecimal. + """ + return ((int(data[1], 16))*16) + int(data[2], 16) + + @staticmethod + def to_green(data): + """Hex color feed to green channel. + + :param int data: Color value, in hexadecimal. + """ + return (int(data[3], 16) * 16) + int(data[4], 16) + + @staticmethod + def to_blue(data): + """Hex color feed to blue channel. + + :param int data: Color value, in hexadecimal. + """ + return (int(data[5], 16) * 16) + int(data[6], 16) + + @staticmethod + def _headers(given): + headers = default_headers.copy() + headers.update(given) + return headers + + @staticmethod + def _create_payload(value, metadata): + if metadata is not None: + payload = Data(value=value, lat=metadata['lat'], lon=metadata['lon'], + ele=metadata['ele'], created_at=metadata['created_at']) + return payload + return Data(value=value) + + @staticmethod + def _handle_error(response): + # Throttling Error + if response.status_code == 429: + raise ThrottlingError() + # Resource on AdafruitIO not Found Error + elif response.status_code == 400: + raise RequestError(response) + # Handle all other errors (400 & 500 level HTTP responses) + elif response.status_code >= 400: + raise RequestError(response) + # Else do nothing if there was no error. + + def _compose_url(self, path): + return '{0}/api/{1}/{2}/{3}'.format(self.base_url, 'v2', self.username, path) + + def _get(self, path, params=None): + response = requests.get(self._compose_url(path), + headers=self._headers({'X-AIO-Key': self.key}), + proxies=self.proxies, + params=params) + self._last_response = response + self._handle_error(response) + return response.json() + + def _post(self, path, data): + response = requests.post(self._compose_url(path), + headers=self._headers({'X-AIO-Key': self.key, + 'Content-Type': 'application/json'}), + proxies=self.proxies, + data=json.dumps(data)) + self._last_response = response + self._handle_error(response) + return response.json() + + def _delete(self, path): + response = requests.delete(self._compose_url(path), + headers=self._headers({'X-AIO-Key': self.key, + 'Content-Type': 'application/json'}), + proxies=self.proxies) + self._last_response = response + self._handle_error(response) + + # Data functionality. + def send_data(self, feed, value, metadata=None, precision=None): + """Helper function to simplify adding a value to a feed. Will append the + specified value to the feed identified by either name, key, or ID. + Returns a Data instance with details about the newly appended row of data. + Note that send_data now operates the same as append. + + :param string feed: Name/Key/ID of Adafruit IO feed. + :param string value: Value to send. + :param dict metadata: Optional metadata associated with the value. + :param int precision: Optional amount of precision points to send. + """ + if precision: + try: + value = round(value, precision) + except NotImplementedError: + raise NotImplementedError("Using the precision kwarg requires a float value") + payload = self._create_payload(value, metadata) + return self.create_data(feed, payload) + + send = send_data + + def send_batch_data(self, feed, data_list): + """Create a new row of data in the specified feed. Feed can be a feed + ID, feed key, or feed name. Data must be an instance of the Data class + with at least a value property set on it. Returns a Data instance with + details about the newly appended row of data. + + :param string feed: Name/Key/ID of Adafruit IO feed. + :param Data data_list: Multiple data values. + """ + path = "feeds/{0}/data/batch".format(feed) + data_dict = type(data_list)((data._asdict() for data in data_list)) + self._post(path, {"data": data_dict}) + + def append(self, feed, value): + """Helper function to simplify adding a value to a feed. Will append the + specified value to the feed identified by either name, key, or ID. + Returns a Data instance with details about the newly appended row of data. + Note that unlike send the feed should exist before calling append. + + :param string feed: Name/Key/ID of Adafruit IO feed. + :param string value: Value to append to feed. + """ + return self.create_data(feed, Data(value=value)) + + def receive_time(self, timezone=None): + """Returns a struct_time from the Adafruit IO Server based on requested + timezone, or automatically based on the device's IP address. + https://docs.python.org/3.7/library/time.html#time.struct_time + + :param string timezone: Optional timezone to return the time in. + See https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List + """ + path = 'integrations/time/struct.json' + if timezone: + path += f'?tz={timezone}' + return self._parse_time_struct(self._get(path)) + + @staticmethod + def _parse_time_struct(time_dict: dict) -> time.struct_time: + """Parse the time data returned by the server and return a time_struct + + Corrects for the weekday returned by the server in Sunday=0 format + (Python expects Monday=0) + """ + wday = (time_dict['wday'] - 1) % 7 + return struct_time((time_dict['year'], time_dict['mon'], time_dict['mday'], + time_dict['hour'], time_dict['min'], time_dict['sec'], + wday, time_dict['yday'], time_dict['isdst'])) + + def receive_weather(self, weather_id=None): + """Adafruit IO Weather Service, Powered by Dark Sky + + :param int id: optional ID for retrieving a specified weather record. + """ + if weather_id: + weather_path = "integrations/weather/{0}".format(weather_id) + else: + weather_path = "integrations/weather" + return self._get(weather_path) + + def receive_random(self, randomizer_id=None): + """Access to Adafruit IO's Random Data + service. + + :param int randomizer_id: optional ID for retrieving a specified randomizer. + """ + if randomizer_id: + random_path = "integrations/words/{0}".format(randomizer_id) + else: + random_path = "integrations/words" + return self._get(random_path) + + def receive(self, feed): + """Retrieve the most recent value for the specified feed. Returns a Data + instance whose value property holds the retrieved value. + + :param string feed: Name/Key/ID of Adafruit IO feed. + """ + path = "feeds/{0}/data/last".format(feed) + return Data.from_dict(self._get(path)) + + def receive_next(self, feed): + """Retrieve the next unread value from the specified feed. Returns a Data + instance whose value property holds the retrieved value. + + :param string feed: Name/Key/ID of Adafruit IO feed. + """ + path = "feeds/{0}/data/next".format(feed) + return Data.from_dict(self._get(path)) + + def receive_previous(self, feed): + """Retrieve the previous unread value from the specified feed. Returns a + Data instance whose value property holds the retrieved value. + + :param string feed: Name/Key/ID of Adafruit IO feed. + """ + path = "feeds/{0}/data/previous".format(feed) + return Data.from_dict(self._get(path)) + + def data(self, feed, data_id=None, max_results=DEFAULT_PAGE_LIMIT): + """Retrieve data from a feed. If data_id is not specified then all the data + for the feed will be returned in an array. + + :param string feed: Name/Key/ID of Adafruit IO feed. + :param string data_id: ID of the piece of data to delete. + :param int max_results: The maximum number of results to return. To + return all data, set to None. + """ + if max_results is None: + res = self._get(f'feeds/{feed}/details') + max_results = res['details']['data']['count'] + if data_id: + path = "feeds/{0}/data/{1}".format(feed, data_id) + return Data.from_dict(self._get(path)) + + params = {'limit': max_results} if max_results else None + data = [] + path = "feeds/{0}/data".format(feed) + while len(data) < max_results: + data.extend(list(map(Data.from_dict, self._get(path, + params=params)))) + nlink = self.get_next_link() + if not nlink: + break + # Parse the link for the query parameters + params = parse_qs(urlparse(nlink).query) + if max_results: + params['limit'] = max_results - len(data) + return data + + def get_next_link(self): + """Parse the `next` page URL in the pagination Link header. + + This is necessary because of a bug in the API's implementation of the + link header. If that bug is fixed, the link would be accesible by + response.links['next']['url'] and this method would be broken. + + :return: The url for the next page of data + :rtype: str + """ + if not self._last_response: + return + link_header = self._last_response.headers['link'] + res = re.search('rel="next", <(.+?)>', link_header) + if not res: + return + return res.groups()[0] + + def create_data(self, feed, data): + """Create a new row of data in the specified feed. + Returns a Data instance with details about the newly + appended row of data. + + :param string feed: Name/Key/ID of Adafruit IO feed. + :param Data data: Instance of the Data class. Must have a value property set. + """ + path = "feeds/{0}/data".format(feed) + return Data.from_dict(self._post(path, data._asdict())) + + def delete(self, feed, data_id): + """Delete data from a feed. + + :param string feed: Name/Key/ID of Adafruit IO feed. + :param string data_id: ID of the piece of data to delete. + """ + path = "feeds/{0}/data/{1}".format(feed, data_id) + self._delete(path) + + # feed functionality. + def feeds(self, feed=None): + """Retrieve a list of all feeds, or the specified feed. If feed is not + specified a list of all feeds will be returned. + + :param string feed: Name/Key/ID of Adafruit IO feed, defaults to None. + """ + if feed is None: + path = "feeds" + return list(map(Feed.from_dict, self._get(path))) + path = "feeds/{0}".format(feed) + return Feed.from_dict(self._get(path)) + + def create_feed(self, feed, group_key=None): + """Create the specified feed. + + :param string feed: Key of Adafruit IO feed. + :param group_key group: Group to place new feed in. + """ + f = feed._asdict() + del f['id'] # Don't pass id on create call + path = "feeds/" + if group_key is not None: # create feed in a group + path="/groups/%s/feeds"%group_key + return Feed.from_dict(self._post(path, {"feed": f})) + return Feed.from_dict(self._post(path, {"feed": f})) + + def delete_feed(self, feed): + """Delete the specified feed. + + :param string feed: Name/Key/ID of Adafruit IO feed. + """ + path = "feeds/{0}".format(feed) + self._delete(path) + + # Group functionality. + def groups(self, group=None): + """Retrieve a list of all groups, or the specified group. + + :param string group: Name/Key/ID of Adafruit IO Group. Defaults to None. + """ + if group is None: + path = "groups/" + return list(map(Group.from_dict, self._get(path))) + path = "groups/{0}".format(group) + return Group.from_dict(self._get(path)) + + def create_group(self, group): + """Create the specified group. + + :param string group: Name/Key/ID of Adafruit IO Group. + """ + path = "groups/" + return Group.from_dict(self._post(path, group._asdict())) + + def delete_group(self, group): + """Delete the specified group. + + :param string group: Name/Key/ID of Adafruit IO Group. + """ + path = "groups/{0}".format(group) + self._delete(path) + + # Dashboard functionality. + def dashboards(self, dashboard=None): + """Retrieve a list of all dashboards, or the specified dashboard. + + :param string dashboard: Key of Adafruit IO Dashboard. Defaults to None. + """ + if dashboard is None: + path = "dashboards/" + return list(map(Dashboard.from_dict, self._get(path))) + path = "dashboards/{0}".format(dashboard) + return Dashboard.from_dict(self._get(path)) + + def create_dashboard(self, dashboard): + """Create the specified dashboard. + + :param Dashboard dashboard: Dashboard object to create + """ + path = "dashboards/" + return Dashboard.from_dict(self._post(path, dashboard._asdict())) + + def delete_dashboard(self, dashboard): + """Delete the specified dashboard. + + :param string dashboard: Key of Adafruit IO Dashboard. + """ + path = "dashboards/{0}".format(dashboard) + self._delete(path) + + # Block functionality. + def blocks(self, dashboard, block=None): + """Retrieve a list of all blocks from a dashboard, or the specified block. + + :param string dashboard: Key of Adafruit IO Dashboard. + :param string block: id of Adafruit IO Block. Defaults to None. + """ + if block is None: + path = "dashboards/{0}/blocks".format(dashboard) + return list(map(Block.from_dict, self._get(path))) + path = "dashboards/{0}/blocks/{1}".format(dashboard, block) + return Block.from_dict(self._get(path)) + + def create_block(self, dashboard, block): + """Create the specified block under the specified dashboard. + + :param string dashboard: Key of Adafruit IO Dashboard. + :param Block block: Block object to create under dashboard + """ + path = "dashboards/{0}/blocks".format(dashboard) + return Block.from_dict(self._post(path, block._asdict())) + + def delete_block(self, dashboard, block): + """Delete the specified block. + + :param string dashboard: Key of Adafruit IO Dashboard. + :param string block: id of Adafruit IO Block. + """ + path = "dashboards/{0}/blocks/{1}".format(dashboard, block) + self._delete(path) + + # Layout functionality. + def layouts(self, dashboard): + """Retrieve the layouts array from a dashboard + + :param string dashboard: key of Adafruit IO Dashboard. + """ + path = "dashboards/{0}".format(dashboard) + dashboard = self._get(path) + return Layout.from_dict(dashboard['layouts']) + + def update_layout(self, dashboard, layout): + """Update the layout of the specified dashboard. + + :param string dashboard: Key of Adafruit IO Dashboard. + :param Layout layout: Layout object to update under dashboard + """ + path = "dashboards/{0}/update_layouts".format(dashboard) + return Layout.from_dict(self._post(path, {'layouts': layout._asdict()})) diff --git a/scripts/tempSensor/lib/Adafruit_IO/errors.py b/scripts/tempSensor/lib/Adafruit_IO/errors.py new file mode 100644 index 00000000..52b3fd77 --- /dev/null +++ b/scripts/tempSensor/lib/Adafruit_IO/errors.py @@ -0,0 +1,61 @@ +# Copyright (c) 2014 Adafruit Industries +# Author: Tony DiCola + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import json, requests +from paho.mqtt.client import error_string + +class AdafruitIOError(Exception): + """Base class for all Adafruit IO request failures.""" + pass + + +class RequestError(Exception): + """General error for a failed Adafruit IO request.""" + def __init__(self, response): + error_message = self._parse_error(response) + super(RequestError, self).__init__("Adafruit IO request failed: {0} {1} - {2}".format( + response.status_code, response.reason, error_message)) + + def _parse_error(self, response): + content = response.json() + try: + return content['error'] + except ValueError: + return "" + + +class ThrottlingError(AdafruitIOError): + """Too many requests have been made to Adafruit IO in a short period of time. + Reduce the rate of requests and try again later. + """ + def __init__(self): + super(ThrottlingError, self).__init__("Exceeded the limit of Adafruit IO " \ + "requests in a short period of time. Please reduce the rate of requests " \ + "and try again later.") + + +class MQTTError(Exception): + """Handles connection attempt failed errors. + """ + def __init__(self, response): + error = error_string(response) + super(MQTTError, self).__init__(error) + pass \ No newline at end of file diff --git a/scripts/tempSensor/lib/Adafruit_IO/model.py b/scripts/tempSensor/lib/Adafruit_IO/model.py new file mode 100644 index 00000000..51d56338 --- /dev/null +++ b/scripts/tempSensor/lib/Adafruit_IO/model.py @@ -0,0 +1,149 @@ +# Copyright (c) 2014 Adafruit Industries +# Author: Tony DiCola + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +from collections import namedtuple +# Handle python 2 and 3 (where map functions like itertools.imap) +try: + from itertools import imap as map +except ImportError: + # Ignore import error on python 3 since map already behaves as expected. + pass + + +# List of fields/properties that are present on a data object from IO. +DATA_FIELDS = [ 'created_epoch', + 'created_at', + 'updated_at', + 'value', + 'completed_at', + 'feed_id', + 'expiration', + 'position', + 'id', + 'lat', + 'lon', + 'ele'] + +FEED_FIELDS = [ 'name', + 'key', + 'id', + 'description', + 'unit_type', + 'unit_symbol', + 'history', + 'visibility', + 'license', + 'status_notify', + 'status_timeout'] + +GROUP_FIELDS = [ 'description', + 'source_keys', + 'id', + 'source', + 'key', + 'feeds', + 'properties', + 'name' ] + +DASHBOARD_FIELDS = [ 'name', + 'key', + 'description', + 'show_header', + 'color_mode', + 'block_borders', + 'header_image_url', + 'blocks' ] + +BLOCK_FIELDS = [ 'name', + 'id', + 'visual_type', + 'properties', + 'block_feeds' ] + +LAYOUT_FIELDS = ['xl', + 'lg', + 'md', + 'sm', + 'xs' ] + +# These are very simple data model classes that are based on namedtuple. This is +# to keep the classes simple and prevent any confusion around updating data +# locally and forgetting to send those updates back up to the IO service (since +# tuples are immutable you can't change them!). Depending on how people use the +# client it might be prudent to revisit this decision and consider making these +# full fledged classes that are mutable. +Data = namedtuple('Data', DATA_FIELDS) +Feed = namedtuple('Feed', FEED_FIELDS) +Group = namedtuple('Group', GROUP_FIELDS) +Dashboard = namedtuple('Dashboard', DASHBOARD_FIELDS) +Block = namedtuple('Block', BLOCK_FIELDS) +Layout = namedtuple('Layout', LAYOUT_FIELDS) + +# Magic incantation to make all parameters to the initializers optional with a +# default value of None. +Group.__new__.__defaults__ = tuple(None for x in GROUP_FIELDS) +Data.__new__.__defaults__ = tuple(None for x in DATA_FIELDS) +Layout.__new__.__defaults__ = tuple(None for x in LAYOUT_FIELDS) + +# explicitly set dashboard values so that 'color_mode' is 'dark' +Dashboard.__new__.__defaults__ = (None, None, None, False, "dark", True, None, None) + +# explicitly set block values so 'properties' is a dictionary +Block.__new__.__defaults__ = (None, None, None, {}, None) + +# explicitly set feed values +Feed.__new__.__defaults__ = (None, None, None, None, None, None, 'ON', 'Private', None, None, None) + +# Define methods to convert from dicts to the data types. +def _from_dict(cls, data): + # Convert dict to call to class initializer (to work with the data types + # base on namedtuple). However be very careful to preserve forwards + # compatibility by ignoring any attributes in the dict which are unknown + # by the data type. + params = {x: data.get(x, None) for x in cls._fields} + return cls(**params) + + +def _feed_from_dict(cls, data): + params = {x: data.get(x, None) for x in cls._fields} + return cls(**params) + + +def _group_from_dict(cls, data): + params = {x: data.get(x, None) for x in cls._fields} + # Parse the feeds if they're provided and generate feed instances. + params['feeds'] = tuple(map(Feed.from_dict, data.get('feeds', []))) + return cls(**params) + + +def _dashboard_from_dict(cls, data): + params = {x: data.get(x, None) for x in cls._fields} + # Parse the blocks if they're provided and generate block instances. + params['blocks'] = tuple(map(Block.from_dict, data.get('blocks', []))) + return cls(**params) + + +# Now add the from_dict class methods defined above to the data types. +Data.from_dict = classmethod(_from_dict) +Feed.from_dict = classmethod(_feed_from_dict) +Group.from_dict = classmethod(_group_from_dict) +Dashboard.from_dict = classmethod(_dashboard_from_dict) +Block.from_dict = classmethod(_from_dict) +Layout.from_dict = classmethod(_from_dict) diff --git a/scripts/tempSensor/lib/Adafruit_IO/mqtt_client.py b/scripts/tempSensor/lib/Adafruit_IO/mqtt_client.py new file mode 100644 index 00000000..198b4d6f --- /dev/null +++ b/scripts/tempSensor/lib/Adafruit_IO/mqtt_client.py @@ -0,0 +1,306 @@ +# Copyright (c) 2020 Adafruit Industries +# Author: Tony DiCola, Brent Rubell + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +import logging + +import paho.mqtt.client as mqtt +import sys +from .errors import MQTTError, RequestError + +# How long to wait before sending a keep alive (paho-mqtt configuration). +KEEP_ALIVE_SEC = 60 # One minute + +logger = logging.getLogger(__name__) + +forecast_types = ["current", "forecast_minutes_5", + "forecast_minutes_30", "forecast_hours_1", + "forecast_hours_2", "forecast_hours_6", + "forecast_hours_24", "forecast_days_1", + "forecast_days_2", "forecast_days_5",] + +class MQTTClient(object): + """Interface for publishing and subscribing to feed changes on Adafruit IO + using the MQTT protocol. + """ + + def __init__(self, username, key, service_host='io.adafruit.com', secure=True): + """Create instance of MQTT client. + + :param username: Adafruit.IO Username for your account. + :param key: Adafruit IO access key (AIO Key) for your account. + :param secure: (optional, boolean) Switches secure/insecure connections + + """ + self._username = username + self._service_host = service_host + if secure: + self._service_port = 8883 + elif not secure: + self._service_port = 1883 + # Initialize event callbacks to be None so they don't fire. + self.on_connect = None + self.on_disconnect = None + self.on_message = None + self.on_subscribe = None + # Initialize v1 MQTT client. + self._client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION1) + if secure: + self._client.tls_set_context() + self._secure = True + elif not secure: + print('**THIS CONNECTION IS INSECURE** SSL/TLS not supported for this platform') + self._secure = False + self._client.username_pw_set(username, key) + self._client.on_connect = self._mqtt_connect + self._client.on_disconnect = self._mqtt_disconnect + self._client.on_message = self._mqtt_message + self._client.on_subscribe = self._mqtt_subscribe + self._connected = False + + + def _mqtt_connect(self, client, userdata, flags, rc): + logger.debug('Client on_connect called.') + # Check if the result code is success (0) or some error (non-zero) and + # raise an exception if failed. + if rc == 0: + #raise RequestError(rc) + self._connected = True + print('Connected to Adafruit IO!') + else: + # handle RC errors within MQTTError class + raise MQTTError(rc) + # Call the on_connect callback if available. + if self.on_connect is not None: + self.on_connect(self) + + def _mqtt_disconnect(self, client, userdata, rc): + logger.debug('Client on_disconnect called.') + self._connected = False + # If this was an unexpected disconnect (non-zero result code) then just + # log the RC as an error. Continue on to call any disconnect handler + # so clients can potentially recover gracefully. + if rc != 0: + print('Unexpected disconnection.') + raise MQTTError(rc) + print('Disconnected from Adafruit IO!') + # Call the on_disconnect callback if available. + if self.on_disconnect is not None: + self.on_disconnect(self) + + def _mqtt_message(self, client, userdata, msg): + """Parse out the topic and call on_message callback + assume topic looks like `username/topic/id` + + """ + logger.debug('Client on_message called.') + parsed_topic = msg.topic.split('/') + if self.on_message is not None: + if parsed_topic[0] == 'time': + topic = parsed_topic[0] + payload = msg.payload.decode('utf-8') + elif parsed_topic[1] == 'groups': + topic = parsed_topic[3] + payload = msg.payload.decode('utf-8') + elif parsed_topic[2] == 'weather': + topic = parsed_topic[4] + payload = '' if msg.payload is None else msg.payload.decode('utf-8') + else: + topic = parsed_topic[2] + payload = '' if msg.payload is None else msg.payload.decode('utf-8') + else: + raise ValueError('on_message not defined') + self.on_message(self, topic, payload) + + def _mqtt_subscribe(self, client, userdata, mid, granted_qos): + """Called when broker responds to a subscribe request.""" + logger.debug('Client called on_subscribe') + if self.on_subscribe is not None: + self.on_subscribe(self, userdata, mid, granted_qos) + + def connect(self, **kwargs): + """Connect to the Adafruit.IO service. Must be called before any loop + or publish operations are called. Will raise an exception if a + connection cannot be made. Optional keyword arguments will be passed + to paho-mqtt client connect function. + + """ + # Skip calling connect if already connected. + if self._connected: + return + # If given, use user-provided keepalive, otherwise default to KEEP_ALIVE_SEC + keepalive = kwargs.pop('keepalive', KEEP_ALIVE_SEC) + # Connect to the Adafruit IO MQTT service. + self._client.connect(self._service_host, port=self._service_port, + keepalive=keepalive, **kwargs) + + def is_connected(self): + """Returns True if connected to Adafruit.IO and False if not connected. + + """ + return self._connected + + def disconnect(self): + """Disconnect MQTT client if connected.""" + if self._connected: + self._client.disconnect() + + def loop_background(self, stop=None): + """Starts a background thread to listen for messages from Adafruit.IO + and call the appropriate callbacks when feed events occur. Will return + immediately and will not block execution. Should only be called once. + + :param bool stop: Stops the execution of the background loop. + + """ + if stop: + self._client.loop_stop() + self._client.loop_start() + + def loop_blocking(self): + """Listen for messages from Adafruit.IO and call the appropriate + callbacks when feed events occur. This call will block execution of + your program and will not return until disconnect is explicitly called. + + This is useful if your program doesn't need to do anything else except + listen and respond to Adafruit.IO feed events. If you need to do other + processing, consider using the loop_background function to run a loop + in the background. + + """ + self._client.loop_forever() + + def loop(self, timeout_sec=1.0): + """Manually process messages from Adafruit.IO. This is meant to be used + inside your own main loop, where you periodically call this function to + make sure messages are being processed to and from Adafruit_IO. + + The optional timeout_sec parameter specifies at most how long to block + execution waiting for messages when this function is called. The default + is one second. + + """ + self._client.loop(timeout=timeout_sec) + + def subscribe(self, feed_id, feed_user=None, qos=0): + """Subscribe to changes on the specified feed. When the feed is updated + the on_message function will be called with the feed_id and new value. + + :param str feed_id: The key of the feed to subscribe to. + :param str feed_user: Optional, identifies feed owner. Used for feed sharing. + :param int qos: The QoS to use when subscribing. Defaults to 0. + + """ + if qos > 1: + raise MQTTError("Adafruit IO only supports a QoS level of 0 or 1.") + if feed_user is not None: + (res, mid) = self._client.subscribe('{0}/feeds/{1}'.format(feed_user, feed_id, qos=qos)) + else: + (res, mid) = self._client.subscribe('{0}/feeds/{1}'.format(self._username, feed_id), qos=qos) + return res, mid + + def subscribe_group(self, group_id, qos=0): + """Subscribe to changes on the specified group. When the group is updated + the on_message function will be called with the group_id and the new value. + + :param str group_id: The id of the group to subscribe to. + :param int qos: The QoS to use when subscribing. Defaults to 0. + + """ + self._client.subscribe('{0}/groups/{1}'.format(self._username, group_id), qos=qos) + + def subscribe_randomizer(self, randomizer_id): + """Subscribe to changes on a specified random data stream from + Adafruit IO's random data service. + + MQTT random word subscriptions will publish data once per minute to + every client that is subscribed to the same topic. + + :param int randomizer_id: ID of the random word record you want data for. + + """ + self._client.subscribe('{0}/integration/words/{1}'.format(self._username, randomizer_id)) + + def subscribe_weather(self, weather_id, forecast_type): + """Subscribe to Adafruit IO Weather + :param int weather_id: weather record you want data for + :param string type: type of forecast data requested + """ + if forecast_type in forecast_types: + self._client.subscribe('{0}/integration/weather/{1}/{2}'.format(self._username, weather_id, forecast_type)) + else: + raise TypeError("Invalid Forecast Type Specified.") + return + + def subscribe_time(self, time): + """Subscribe to changes on the Adafruit IO time feeds. When the feed is + updated, the on_message function will be called and publish a new value: + time feeds: + millis: milliseconds + seconds: seconds + iso: ISO-8601 (https://en.wikipedia.org/wiki/ISO_8601) + """ + if time == 'millis' or time == 'seconds': + self._client.subscribe('time/{0}'.format(time)) + elif time == 'iso': + self._client.subscribe('time/ISO-8601') + else: + raise TypeError('Invalid Time Feed Specified.') + return + + def unsubscribe(self, feed_id=None, group_id=None): + """Unsubscribes from a specified MQTT topic. + Note: this does not prevent publishing to a topic, it will unsubscribe + from receiving messages via on_message. + """ + if feed_id is not None: + self._client.unsubscribe('{0}/feeds/{1}'.format(self._username, feed_id)) + elif group_id is not None: + self._client.unsubscribe('{0}/groups/{1}'.format(self._username, group_id)) + else: + raise TypeError('Invalid topic type specified.') + return + + def receive(self, feed_id): + """Receive the last published value from a specified feed. + + :param string feed_id: The ID of the feed to update. + :parm string value: The new value to publish to the feed + """ + (res, self._pub_mid) = self._client.publish('{0}/feeds/{1}/get'.format(self._username, feed_id), + payload='') + + def publish(self, feed_id, value=None, group_id=None, feed_user=None): + """Publish a value to a specified feed. + + Params: + - feed_id: The id of the feed to update. + - value: The new value to publish to the feed. + - (optional) group_id: The id of the group to update. + - (optional) feed_user: The feed owner's username. Used for Sharing Feeds. + """ + if feed_user is not None: # shared feed + (res, self._pub_mid) = self._client.publish('{0}/feeds/{1}'.format(feed_user, feed_id), + payload=value) + elif group_id is not None: # group-specified feed + self._client.publish('{0}/feeds/{1}.{2}'.format(self._username, group_id, feed_id), + payload=value) + else: # regular feed + (res, self._pub_mid) = self._client.publish('{0}/feeds/{1}'.format(self._username, feed_id), + payload=value) diff --git a/scripts/tempSensor/lib/CHANGELOG.md b/scripts/tempSensor/lib/CHANGELOG.md new file mode 100644 index 00000000..78fec497 --- /dev/null +++ b/scripts/tempSensor/lib/CHANGELOG.md @@ -0,0 +1,50 @@ +2.0.0 +----- + +* Repackage to hatch/pyproject.toml +* Drop Python 2.7 support +* Switch from smbu2 to smbus2 + +1.1.1 +----- + +* New: constants to clarify heater on/off states + +1.1.0 +----- + +* New: support for BME688 "high" gas resistance variant +* New: set/get gas heater disable bit +* Enhancement: fail with descriptive RuntimeError when chip is not detected + +1.0.5 +----- + +* New: set_temp_offset to calibrate temperature offset in degrees C + +1.0.4 +----- + +* Fix to range_sw_err for extremely high gas readings +* Convert to unsigned int to fix negative gas readings + +1.0.3 +----- + +* Merged temperature compensation fix from Bosch's BME680_driver 3.5.3 + +1.0.2 +----- + +* Fixed set_gas_heater_temperature to avoid i2c TypeError + +1.0.1 +----- + +* Added Manifest to Python package + +1.0.0 +----- + +* Initial release + diff --git a/scripts/tempSensor/lib/LICENSE b/scripts/tempSensor/lib/LICENSE new file mode 100644 index 00000000..b3e25b24 --- /dev/null +++ b/scripts/tempSensor/lib/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Pimoroni Ltd + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/scripts/tempSensor/lib/README.md b/scripts/tempSensor/lib/README.md new file mode 100644 index 00000000..0c71b7f7 --- /dev/null +++ b/scripts/tempSensor/lib/README.md @@ -0,0 +1,56 @@ +# BME680 + +[![Build Status](https://img.shields.io/github/actions/workflow/status/pimoroni/bme680-python/test.yml?branch=main)](https://github.com/pimoroni/bme680-python/actions/workflows/test.yml) +[![Coverage Status](https://coveralls.io/repos/github/pimoroni/bme680-python/badge.svg?branch=main)](https://coveralls.io/github/pimoroni/bme680-python?branch=main) +[![PyPi Package](https://img.shields.io/pypi/v/bme680.svg)](https://pypi.python.org/pypi/bme680) +[![Python Versions](https://img.shields.io/pypi/pyversions/bme680.svg)](https://pypi.python.org/pypi/bme680) + +https://shop.pimoroni.com/products/bme680 + +The state-of-the-art BME680 breakout lets you measure temperature, pressure, humidity, and indoor air quality. + +## Installing + +### Full install (recommended): + +We've created an easy installation script that will install all pre-requisites and get your BME680 +up and running with minimal efforts. To run it, fire up Terminal which you'll find in Menu -> Accessories -> Terminal +on your Raspberry Pi desktop, as illustrated below: + +![Finding the terminal](http://get.pimoroni.com/resources/github-repo-terminal.png) + +In the new terminal window type the command exactly as it appears below (check for typos) and follow the on-screen instructions: + +```bash +git clone https://github.com/pimoroni/bme680-python +cd bme680-python +./install.sh +``` + +**Note** Libraries will be installed in the "pimoroni" virtual environment, you will need to activate it to run examples: + +``` +source ~/.virtualenvs/pimoroni/bin/activate +``` + +### Development: + +If you want to contribute, or like living on the edge of your seat by having the latest code, you can install the development version like so: + +```bash +git clone https://github.com/pimoroni/bme680-python +cd bme680-python +./install.sh --unstable +``` + +In all cases you will have to enable the i2c bus: + +``` +sudo raspi-config nonint do_i2c 0 +``` + +## Documentation & Support + +* Guides and tutorials - https://learn.pimoroni.com/bme680-breakout +* Get help - http://forums.pimoroni.com/c/support + diff --git a/scripts/tempSensor/lib/adafruit_bme680.py b/scripts/tempSensor/lib/adafruit_bme680.py new file mode 100644 index 00000000..e77207a7 --- /dev/null +++ b/scripts/tempSensor/lib/adafruit_bme680.py @@ -0,0 +1,769 @@ +# SPDX-FileCopyrightText: 2017 ladyada for Adafruit Industries +# +# SPDX-License-Identifier: MIT AND BSD-3-Clause + + +""" +`adafruit_bme680` +================================================================================ + +CircuitPython library for BME680 temperature, pressure and humidity sensor. + + +* Author(s): Limor Fried, William Garber, many others + + +Implementation Notes +-------------------- + +**Hardware:** + +* `Adafruit BME680 Temp, Humidity, Pressure and Gas Sensor `_ + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://github.com/adafruit/circuitpython/releases +* Adafruit's Bus Device library: https://github.com/adafruit/Adafruit_CircuitPython_BusDevice +""" + +import math +import struct +import time + +from micropython import const + + +def delay_microseconds(nusec): + """HELP must be same as dev->delay_us""" + time.sleep(nusec / 1000000.0) + + +try: + # Used only for type annotations. + + import typing + + from busio import I2C, SPI + from circuitpython_typing import ReadableBuffer + from digitalio import DigitalInOut + +except ImportError: + pass + +__version__ = "0.0.0+auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_BME680.git" + + +# I2C ADDRESS/BITS/SETTINGS NEW +# ----------------------------------------------------------------------- +_BME68X_ENABLE_HEATER = const(0x00) +_BME68X_DISABLE_HEATER = const(0x01) +_BME68X_DISABLE_GAS_MEAS = const(0x00) +_BME68X_ENABLE_GAS_MEAS_L = const(0x01) +_BME68X_ENABLE_GAS_MEAS_H = const(0x02) +_BME68X_SLEEP_MODE = const(0) +_BME68X_FORCED_MODE = const(1) +_BME68X_VARIANT_GAS_LOW = const(0x00) +_BME68X_VARIANT_GAS_HIGH = const(0x01) +_BME68X_HCTRL_MSK = const(0x08) +_BME68X_HCTRL_POS = const(3) +_BME68X_NBCONV_MSK = const(0x0F) +_BME68X_RUN_GAS_MSK = const(0x30) +_BME68X_RUN_GAS_POS = const(4) +_BME68X_MODE_MSK = const(0x03) +_BME68X_PERIOD_POLL = const(10000) +_BME68X_REG_CTRL_GAS_0 = const(0x70) +_BME68X_REG_CTRL_GAS_1 = const(0x71) + +# I2C ADDRESS/BITS/SETTINGS +# ----------------------------------------------------------------------- +_BME680_CHIPID = const(0x61) + +_BME680_REG_CHIPID = const(0xD0) +_BME68X_REG_VARIANT = const(0xF0) +_BME680_BME680_COEFF_ADDR1 = const(0x89) +_BME680_BME680_COEFF_ADDR2 = const(0xE1) +_BME680_BME680_RES_HEAT_0 = const(0x5A) +_BME680_BME680_GAS_WAIT_0 = const(0x64) + +_BME680_REG_SOFTRESET = const(0xE0) +_BME680_REG_CTRL_GAS = const(0x71) +_BME680_REG_CTRL_HUM = const(0x72) +_BME680_REG_STATUS = const(0x73) +_BME680_REG_CTRL_MEAS = const(0x74) +_BME680_REG_CONFIG = const(0x75) + +_BME680_REG_MEAS_STATUS = const(0x1D) +_BME680_REG_PDATA = const(0x1F) +_BME680_REG_TDATA = const(0x22) +_BME680_REG_HDATA = const(0x25) + +_BME680_SAMPLERATES = (0, 1, 2, 4, 8, 16) +_BME680_FILTERSIZES = (0, 1, 3, 7, 15, 31, 63, 127) + +_BME680_RUNGAS = const(0x10) + +_LOOKUP_TABLE_1 = ( + 2147483647.0, + 2147483647.0, + 2147483647.0, + 2147483647.0, + 2147483647.0, + 2126008810.0, + 2147483647.0, + 2130303777.0, + 2147483647.0, + 2147483647.0, + 2143188679.0, + 2136746228.0, + 2147483647.0, + 2126008810.0, + 2147483647.0, + 2147483647.0, +) + +_LOOKUP_TABLE_2 = ( + 4096000000.0, + 2048000000.0, + 1024000000.0, + 512000000.0, + 255744255.0, + 127110228.0, + 64000000.0, + 32258064.0, + 16016016.0, + 8000000.0, + 4000000.0, + 2000000.0, + 1000000.0, + 500000.0, + 250000.0, + 125000.0, +) + + +def bme_set_bits(reg_data, bitname_msk, bitname_pos, data): + """ + Macro to set bits + data2 = data << bitname_pos + set masked bits from data2 in reg_data + """ + return (reg_data & ~bitname_msk) | ((data << bitname_pos) & bitname_msk) + + +def bme_set_bits_pos_0(reg_data, bitname_msk, data): + """ + Macro to set bits starting from position 0 + set masked bits from data in reg_data + """ + return (reg_data & ~bitname_msk) | (data & bitname_msk) + + +def _read24(arr: ReadableBuffer) -> float: + """Parse an unsigned 24-bit value as a floating point and return it.""" + ret = 0.0 + # print([hex(i) for i in arr]) + for b in arr: + ret *= 256.0 + ret += float(b & 0xFF) + return ret + + +class Adafruit_BME680: + """Driver from BME680 air quality sensor + + :param int refresh_rate: Maximum number of readings per second. Faster property reads + will be from the previous reading.""" + + def __init__(self, *, refresh_rate: int = 10) -> None: + """Check the BME680 was found, read the coefficients and enable the sensor for continuous + reads.""" + self._write(_BME680_REG_SOFTRESET, [0xB6]) + time.sleep(0.005) + + # Check device ID. + chip_id = self._read_byte(_BME680_REG_CHIPID) + if chip_id != _BME680_CHIPID: + raise RuntimeError("Failed to find BME680! Chip ID 0x%x" % chip_id) + + # Get variant + self._chip_variant = self._read_byte(_BME68X_REG_VARIANT) + + self._read_calibration() + + # set up heater + self._write(_BME680_BME680_RES_HEAT_0, [0x73]) + self._write(_BME680_BME680_GAS_WAIT_0, [0x65]) + + self.sea_level_pressure = 1013.25 + """Pressure in hectoPascals at sea level. Used to calibrate :attr:`altitude`.""" + + # Default oversampling and filter register values. + self._pressure_oversample = 0b011 + self._temp_oversample = 0b100 + self._humidity_oversample = 0b010 + self._filter = 0b010 + + # Gas measurements, as a mask applied to _BME680_RUNGAS + self._run_gas = 0xFF + + self._adc_pres = None + self._adc_temp = None + self._adc_hum = None + self._adc_gas = None + self._gas_range = None + self._t_fine = None + + self._last_reading = 0 + self._min_refresh_time = 1 / refresh_rate + + self._amb_temp = 25 # Copy required parameters from reference bme68x_dev struct + self.set_gas_heater(320, 150) # heater 320 deg C for 150 msec + + @property + def pressure_oversample(self) -> int: + """The oversampling for pressure sensor""" + return _BME680_SAMPLERATES[self._pressure_oversample] + + @pressure_oversample.setter + def pressure_oversample(self, sample_rate: int) -> None: + if sample_rate in _BME680_SAMPLERATES: + self._pressure_oversample = _BME680_SAMPLERATES.index(sample_rate) + else: + raise RuntimeError("Invalid oversample") + + @property + def humidity_oversample(self) -> int: + """The oversampling for humidity sensor""" + return _BME680_SAMPLERATES[self._humidity_oversample] + + @humidity_oversample.setter + def humidity_oversample(self, sample_rate: int) -> None: + if sample_rate in _BME680_SAMPLERATES: + self._humidity_oversample = _BME680_SAMPLERATES.index(sample_rate) + else: + raise RuntimeError("Invalid oversample") + + @property + def temperature_oversample(self) -> int: + """The oversampling for temperature sensor""" + return _BME680_SAMPLERATES[self._temp_oversample] + + @temperature_oversample.setter + def temperature_oversample(self, sample_rate: int) -> None: + if sample_rate in _BME680_SAMPLERATES: + self._temp_oversample = _BME680_SAMPLERATES.index(sample_rate) + else: + raise RuntimeError("Invalid oversample") + + @property + def filter_size(self) -> int: + """The filter size for the built in IIR filter""" + return _BME680_FILTERSIZES[self._filter] + + @filter_size.setter + def filter_size(self, size: int) -> None: + if size in _BME680_FILTERSIZES: + self._filter = _BME680_FILTERSIZES.index(size) + else: + raise RuntimeError("Invalid size") + + @property + def temperature(self) -> float: + """The compensated temperature in degrees Celsius.""" + self._perform_reading() + calc_temp = ((self._t_fine * 5) + 128) / 256 + return calc_temp / 100 + + @property + def pressure(self) -> float: + """The barometric pressure in hectoPascals""" + self._perform_reading() + var1 = (self._t_fine / 2) - 64000 + var2 = ((var1 / 4) * (var1 / 4)) / 2048 + var2 = (var2 * self._pressure_calibration[5]) / 4 + var2 = var2 + (var1 * self._pressure_calibration[4] * 2) + var2 = (var2 / 4) + (self._pressure_calibration[3] * 65536) + var1 = ((((var1 / 4) * (var1 / 4)) / 8192) * (self._pressure_calibration[2] * 32) / 8) + ( + (self._pressure_calibration[1] * var1) / 2 + ) + var1 = var1 / 262144 + var1 = ((32768 + var1) * self._pressure_calibration[0]) / 32768 + calc_pres = 1048576 - self._adc_pres + calc_pres = (calc_pres - (var2 / 4096)) * 3125 + calc_pres = (calc_pres / var1) * 2 + var1 = (self._pressure_calibration[8] * (((calc_pres / 8) * (calc_pres / 8)) / 8192)) / 4096 + var2 = ((calc_pres / 4) * self._pressure_calibration[7]) / 8192 + var3 = (((calc_pres / 256) ** 3) * self._pressure_calibration[9]) / 131072 + calc_pres += (var1 + var2 + var3 + (self._pressure_calibration[6] * 128)) / 16 + return calc_pres / 100 + + @property + def relative_humidity(self) -> float: + """The relative humidity in RH %""" + return self.humidity + + @property + def humidity(self) -> float: + """The relative humidity in RH %""" + self._perform_reading() + temp_scaled = ((self._t_fine * 5) + 128) / 256 + var1 = (self._adc_hum - (self._humidity_calibration[0] * 16)) - ( + (temp_scaled * self._humidity_calibration[2]) / 200 + ) + var2 = ( + self._humidity_calibration[1] + * ( + ((temp_scaled * self._humidity_calibration[3]) / 100) + + ( + ((temp_scaled * ((temp_scaled * self._humidity_calibration[4]) / 100)) / 64) + / 100 + ) + + 16384 + ) + ) / 1024 + var3 = var1 * var2 + var4 = self._humidity_calibration[5] * 128 + var4 = (var4 + ((temp_scaled * self._humidity_calibration[6]) / 100)) / 16 + var5 = ((var3 / 16384) * (var3 / 16384)) / 1024 + var6 = (var4 * var5) / 2 + calc_hum = (((var3 + var6) / 1024) * 1000) / 4096 + calc_hum /= 1000 # get back to RH + + calc_hum = min(calc_hum, 100) + calc_hum = max(calc_hum, 0) + return calc_hum + + @property + def altitude(self) -> float: + """The altitude based on current :attr:`pressure` vs the sea level pressure + (:attr:`sea_level_pressure`) - which you must enter ahead of time)""" + pressure = self.pressure # in Si units for hPascal + return 44330 * (1.0 - math.pow(pressure / self.sea_level_pressure, 0.1903)) + + @property + def gas(self) -> int: + """The gas resistance in ohms""" + self._perform_reading() + if self._chip_variant == 0x01: + # taken from https://github.com/BoschSensortec/BME68x-Sensor-API + var1 = 262144 >> self._gas_range + var2 = self._adc_gas - 512 + var2 *= 3 + var2 = 4096 + var2 + calc_gas_res = (10000 * var1) / var2 + calc_gas_res = calc_gas_res * 100 + else: + var1 = ((1340 + (5 * self._sw_err)) * (_LOOKUP_TABLE_1[self._gas_range])) / 65536 + var2 = ((self._adc_gas * 32768) - 16777216) + var1 + var3 = (_LOOKUP_TABLE_2[self._gas_range] * var1) / 512 + calc_gas_res = (var3 + (var2 / 2)) / var2 + return int(calc_gas_res) + + def _perform_reading(self) -> None: + """Perform a single-shot reading from the sensor and fill internal data structure for + calculations""" + if time.monotonic() - self._last_reading < self._min_refresh_time: + return + + # set filter + self._write(_BME680_REG_CONFIG, [self._filter << 2]) + # turn on temp oversample & pressure oversample + self._write( + _BME680_REG_CTRL_MEAS, + [(self._temp_oversample << 5) | (self._pressure_oversample << 2)], + ) + # turn on humidity oversample + self._write(_BME680_REG_CTRL_HUM, [self._humidity_oversample]) + # gas measurements enabled + if self._chip_variant == 0x01: + self._write(_BME680_REG_CTRL_GAS, [(self._run_gas & _BME680_RUNGAS) << 1]) + else: + self._write(_BME680_REG_CTRL_GAS, [(self._run_gas & _BME680_RUNGAS)]) + ctrl = self._read_byte(_BME680_REG_CTRL_MEAS) + ctrl = (ctrl & 0xFC) | 0x01 # enable single shot! + self._write(_BME680_REG_CTRL_MEAS, [ctrl]) + new_data = False + while not new_data: + data = self._read(_BME680_REG_MEAS_STATUS, 17) + new_data = data[0] & 0x80 != 0 + time.sleep(0.005) + self._last_reading = time.monotonic() + + self._adc_pres = _read24(data[2:5]) / 16 + self._adc_temp = _read24(data[5:8]) / 16 + self._adc_hum = struct.unpack(">H", bytes(data[8:10]))[0] + if self._chip_variant == 0x01: + self._adc_gas = int(struct.unpack(">H", bytes(data[15:17]))[0] / 64) + self._gas_range = data[16] & 0x0F + else: + self._adc_gas = int(struct.unpack(">H", bytes(data[13:15]))[0] / 64) + self._gas_range = data[14] & 0x0F + + var1 = (self._adc_temp / 8) - (self._temp_calibration[0] * 2) + var2 = (var1 * self._temp_calibration[1]) / 2048 + var3 = ((var1 / 2) * (var1 / 2)) / 4096 + var3 = (var3 * self._temp_calibration[2] * 16) / 16384 + self._t_fine = int(var2 + var3) + + def _read_calibration(self) -> None: + """Read & save the calibration coefficients""" + coeff = self._read(_BME680_BME680_COEFF_ADDR1, 25) + coeff += self._read(_BME680_BME680_COEFF_ADDR2, 16) + + coeff = list(struct.unpack(" int: + """Read a byte register value and return it""" + return self._read(register, 1)[0] + + def _read(self, register: int, length: int) -> bytearray: + raise NotImplementedError() + + def _write(self, register: int, values: bytearray) -> None: + raise NotImplementedError() + + def set_gas_heater(self, heater_temp: int, heater_time: int) -> bool: + """ + Enable and configure gas reading + heater (None disables) + :param heater_temp: Desired temperature in degrees Centigrade + :param heater_time: Time to keep heater on in milliseconds + :return: True on success, False on failure + """ + try: + if (heater_temp is None) or (heater_time is None): + self._set_heatr_conf(heater_temp or 0, heater_time or 0, enable=False) + else: + self._set_heatr_conf(heater_temp, heater_time) + except OSError: + return False + return True + + def _set_heatr_conf(self, heater_temp: int, heater_time: int, enable: bool = True) -> None: + # restrict to BME68X_FORCED_MODE + op_mode: int = _BME68X_FORCED_MODE + nb_conv: int = 0 + hctrl: int = _BME68X_ENABLE_HEATER + run_gas: int = 0 + ctrl_gas_data_0: int = 0 + ctrl_gas_data_1: int = 0 + + self._set_op_mode(_BME68X_SLEEP_MODE) + self._set_conf(heater_temp, heater_time, op_mode) + ctrl_gas_data_0 = self._read_byte(_BME68X_REG_CTRL_GAS_0) + ctrl_gas_data_1 = self._read_byte(_BME68X_REG_CTRL_GAS_1) + if enable: + hctrl = _BME68X_ENABLE_HEATER + if self._chip_variant == _BME68X_VARIANT_GAS_HIGH: + run_gas = _BME68X_ENABLE_GAS_MEAS_H + else: + run_gas = _BME68X_ENABLE_GAS_MEAS_L + else: + hctrl = _BME68X_DISABLE_HEATER + run_gas = _BME68X_DISABLE_GAS_MEAS + self._run_gas = ~(run_gas - 1) + + ctrl_gas_data_0 = bme_set_bits(ctrl_gas_data_0, _BME68X_HCTRL_MSK, _BME68X_HCTRL_POS, hctrl) + ctrl_gas_data_1 = bme_set_bits_pos_0(ctrl_gas_data_1, _BME68X_NBCONV_MSK, nb_conv) + ctrl_gas_data_1 = bme_set_bits( + ctrl_gas_data_1, _BME68X_RUN_GAS_MSK, _BME68X_RUN_GAS_POS, run_gas + ) + self._write(_BME68X_REG_CTRL_GAS_0, [ctrl_gas_data_0]) + self._write(_BME68X_REG_CTRL_GAS_1, [ctrl_gas_data_1]) + + def _set_op_mode(self, op_mode: int) -> None: + """ + * @brief This API is used to set the operation mode of the sensor + """ + tmp_pow_mode: int = 0 + pow_mode: int = _BME68X_FORCED_MODE + # Call until in sleep + + # was a do {} while() loop + while pow_mode != _BME68X_SLEEP_MODE: + tmp_pow_mode = self._read_byte(_BME680_REG_CTRL_MEAS) + # Put to sleep before changing mode + pow_mode = tmp_pow_mode & _BME68X_MODE_MSK + if pow_mode != _BME68X_SLEEP_MODE: + tmp_pow_mode &= ~_BME68X_MODE_MSK # Set to sleep + self._write(_BME680_REG_CTRL_MEAS, [tmp_pow_mode]) + # dev->delay_us(_BME68X_PERIOD_POLL, dev->intf_ptr) # HELP + delay_microseconds(_BME68X_PERIOD_POLL) + # Already in sleep + if op_mode != _BME68X_SLEEP_MODE: + tmp_pow_mode = (tmp_pow_mode & ~_BME68X_MODE_MSK) | (op_mode & _BME68X_MODE_MSK) + self._write(_BME680_REG_CTRL_MEAS, [tmp_pow_mode]) + + def _set_conf(self, heater_temp: int, heater_time: int, op_mode: int) -> None: + """ + This internal API is used to set heater configurations + """ + + if op_mode != _BME68X_FORCED_MODE: + raise OSError("GasHeaterException: _set_conf not forced mode") + rh_reg_data: int = self._calc_res_heat(heater_temp) + gw_reg_data: int = self._calc_gas_wait(heater_time) + self._write(_BME680_BME680_RES_HEAT_0, [rh_reg_data]) + self._write(_BME680_BME680_GAS_WAIT_0, [gw_reg_data]) + + def _calc_res_heat(self, temp: int) -> int: + """ + This internal API is used to calculate the heater resistance value using float + """ + gh1: int = self._gas_calibration[0] + gh2: int = self._gas_calibration[1] + gh3: int = self._gas_calibration[2] + htr: int = self._heat_range + htv: int = self._heat_val + amb: int = self._amb_temp + + temp = min(temp, 400) # Cap temperature + + var1: int = ((int(amb) * gh3) / 1000) * 256 + var2: int = (gh1 + 784) * (((((gh2 + 154009) * temp * 5) / 100) + 3276800) / 10) + var3: int = var1 + (var2 / 2) + var4: int = var3 / (htr + 4) + var5: int = (131 * htv) + 65536 + heatr_res_x100: int = int(((var4 / var5) - 250) * 34) + heatr_res: int = int((heatr_res_x100 + 50) / 100) + + return heatr_res + + def _calc_res_heat(self, temp: int) -> int: + """ + This internal API is used to calculate the heater resistance value + """ + gh1: float = float(self._gas_calibration[0]) + gh2: float = float(self._gas_calibration[1]) + gh3: float = float(self._gas_calibration[2]) + htr: float = float(self._heat_range) + htv: float = float(self._heat_val) + amb: float = float(self._amb_temp) + + temp = min(temp, 400) # Cap temperature + + var1: float = (gh1 / (16.0)) + 49.0 + var2: float = ((gh2 / (32768.0)) * (0.0005)) + 0.00235 + var3: float = gh3 / (1024.0) + var4: float = var1 * (1.0 + (var2 * float(temp))) + var5: float = var4 + (var3 * amb) + res_heat: int = int(3.4 * ((var5 * (4 / (4 + htr)) * (1 / (1 + (htv * 0.002)))) - 25)) + return res_heat + + def _calc_gas_wait(self, dur: int) -> int: + """ + This internal API is used to calculate the gas wait + """ + factor: int = 0 + durval: int = 0xFF # Max duration + + if dur >= 0xFC0: + return durval + while dur > 0x3F: + dur = dur / 4 + factor += 1 + durval = int(dur + (factor * 64)) + return durval + + +class Adafruit_BME680_I2C(Adafruit_BME680): + """Driver for I2C connected BME680. + + :param ~busio.I2C i2c: The I2C bus the BME680 is connected to. + :param int address: I2C device address. Defaults to :const:`0x77` + :param bool debug: Print debug statements when `True`. Defaults to `False` + :param int refresh_rate: Maximum number of readings per second. Faster property reads + will be from the previous reading. + + **Quickstart: Importing and using the BME680** + + Here is an example of using the :class:`BMP680_I2C` class. + First you will need to import the libraries to use the sensor + + .. code-block:: python + + import board + import adafruit_bme680 + + Once this is done you can define your ``board.I2C`` object and define your sensor object + + .. code-block:: python + + i2c = board.I2C() # uses board.SCL and board.SDA + bme680 = adafruit_bme680.Adafruit_BME680_I2C(i2c) + + You need to setup the pressure at sea level + + .. code-block:: python + + bme680.sea_level_pressure = 1013.25 + + Now you have access to the :attr:`temperature`, :attr:`gas`, :attr:`relative_humidity`, + :attr:`pressure` and :attr:`altitude` attributes + + .. code-block:: python + + temperature = bme680.temperature + gas = bme680.gas + relative_humidity = bme680.relative_humidity + pressure = bme680.pressure + altitude = bme680.altitude + + """ + + def __init__( + self, + i2c: I2C, + address: int = 0x77, + debug: bool = False, + *, + refresh_rate: int = 10, + ) -> None: + """Initialize the I2C device at the 'address' given""" + from adafruit_bus_device import ( + i2c_device, + ) + + self._i2c = i2c_device.I2CDevice(i2c, address) + self._debug = debug + super().__init__(refresh_rate=refresh_rate) + + def _read(self, register: int, length: int) -> bytearray: + """Returns an array of 'length' bytes from the 'register'""" + with self._i2c as i2c: + i2c.write(bytes([register & 0xFF])) + result = bytearray(length) + i2c.readinto(result) + if self._debug: + print(f"\t${register:02X} => {[hex(i) for i in result]}") + return result + + def _write(self, register: int, values: ReadableBuffer) -> None: + """Writes an array of 'length' bytes to the 'register'""" + with self._i2c as i2c: + buffer = bytearray(2 * len(values)) + for i, value in enumerate(values): + buffer[2 * i] = register + i + buffer[2 * i + 1] = value + i2c.write(buffer) + if self._debug: + print(f"\t${values[0]:02X} <= {[hex(i) for i in values[1:]]}") + + +class Adafruit_BME680_SPI(Adafruit_BME680): + """Driver for SPI connected BME680. + + :param ~busio.SPI spi: SPI device + :param ~digitalio.DigitalInOut cs: Chip Select + :param bool debug: Print debug statements when `True`. Defaults to `False` + :param int baudrate: Clock rate, default is :const:`100000` + :param int refresh_rate: Maximum number of readings per second. Faster property reads + will be from the previous reading. + + + **Quickstart: Importing and using the BME680** + + Here is an example of using the :class:`BMP680_SPI` class. + First you will need to import the libraries to use the sensor + + .. code-block:: python + + import board + from digitalio import DigitalInOut, Direction + import adafruit_bme680 + + Once this is done you can define your ``board.SPI`` object and define your sensor object + + .. code-block:: python + + cs = digitalio.DigitalInOut(board.D10) + spi = board.SPI() + bme680 = adafruit_bme680.Adafruit_BME680_SPI(spi, cs) + + You need to setup the pressure at sea level + + .. code-block:: python + + bme680.sea_level_pressure = 1013.25 + + Now you have access to the :attr:`temperature`, :attr:`gas`, :attr:`relative_humidity`, + :attr:`pressure` and :attr:`altitude` attributes + + .. code-block:: python + + temperature = bme680.temperature + gas = bme680.gas + relative_humidity = bme680.relative_humidity + pressure = bme680.pressure + altitude = bme680.altitude + + """ + + def __init__( # noqa: PLR0913 Too many arguments in function definition + self, + spi: SPI, + cs: DigitalInOut, + baudrate: int = 100000, + debug: bool = False, + *, + refresh_rate: int = 10, + ) -> None: + from adafruit_bus_device import ( + spi_device, + ) + + self._spi = spi_device.SPIDevice(spi, cs, baudrate=baudrate) + self._debug = debug + super().__init__(refresh_rate=refresh_rate) + + def _read(self, register: int, length: int) -> bytearray: + if register != _BME680_REG_STATUS: + # _BME680_REG_STATUS exists in both SPI memory pages + # For all other registers, we must set the correct memory page + self._set_spi_mem_page(register) + + register = (register | 0x80) & 0xFF # Read single, bit 7 high. + with self._spi as spi: + spi.write(bytearray([register])) + result = bytearray(length) + spi.readinto(result) + if self._debug: + print(f"\t${register:02X} => {[hex(i) for i in result]}") + return result + + def _write(self, register: int, values: ReadableBuffer) -> None: + if register != _BME680_REG_STATUS: + # _BME680_REG_STATUS exists in both SPI memory pages + # For all other registers, we must set the correct memory page + self._set_spi_mem_page(register) + register &= 0x7F # Write, bit 7 low. + with self._spi as spi: + buffer = bytearray(2 * len(values)) + for i, value in enumerate(values): + buffer[2 * i] = register + i + buffer[2 * i + 1] = value & 0xFF + spi.write(buffer) + if self._debug: + print(f"\t${values[0]:02X} <= {[hex(i) for i in values[1:]]}") + + def _set_spi_mem_page(self, register: int) -> None: + spi_mem_page = 0x00 + if register < 0x80: + spi_mem_page = 0x10 + self._write(_BME680_REG_STATUS, [spi_mem_page]) \ No newline at end of file diff --git a/scripts/tempSensor/lib/adafruit_bus_device/__init__.py b/scripts/tempSensor/lib/adafruit_bus_device/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/scripts/tempSensor/lib/adafruit_bus_device/i2c_device.mpy b/scripts/tempSensor/lib/adafruit_bus_device/i2c_device.mpy new file mode 100644 index 0000000000000000000000000000000000000000..3382361059a1bf655ad4cbe8edae64f71d23ed29 GIT binary patch literal 1172 zcmZ`&&uJXHdxmAIJ$z;e|~{)v2cHJ0S$6rZ|MU=}35*zD>CV ztw-zqZkIav{FY;Q^u2>qeyAIc&L!FdZ`(4-zXD9&EU)G{EziKPLfZ8i*h)g~ZDWG9 z))CytiR*=O;aW*i2-%|!hna+ALBWJL)V2uOEF&y(nnqZFilCsTj*wg1_B`8NDHb0X z-gaLr=+m?UF!~FU5yE|6`v1N%c?|(@kpqE2N6p8rkOFE39w#})LeFHOt&G3opI z1%mm>@EE5~aOycuy}+rL)uYH*upBgmi{|4j@+OR|Y5pC|&5Mwb*bmt%)TfyEu{`DW zD79G?wS4KEj6G*SdX~OZg~lIW`(ZKfJ)6KV6K6l=!Iz?PqHm4>ty|$Sru%yEBIs;j z?5Vv(=+F+^Z-m9GGwkPfEnCe!%4CdKHAf%zwiGu`os?SxbbEvVt$6KP!j^k1bfu06fnp;(k*X e_(*8rp5L%C{Doykvy)N1%y@iOGNt{C`+ot4ZCDKe literal 0 HcmV?d00001 diff --git a/scripts/tempSensor/lib/adafruit_bus_device/spi_device.mpy b/scripts/tempSensor/lib/adafruit_bus_device/spi_device.mpy new file mode 100644 index 0000000000000000000000000000000000000000..75dd10b264b2c7eeea9cdc49184c2590be08fd7e GIT binary patch literal 821 zcmYL{-%b-j6vk&;3RqgZ?UwC^{{f`2V2c$R^~O^60*DE1AYhF;>`n)|p>#L9vy~<$ zOsgU!`Vz(mSQYdI#24T~5>33~M(>T6&bAOIIg_(zzM0>*XJ+2#86J6&of-#SIi;j* znzlxuYFn^`YZ}5S%g`>;q*344*vKx7-7%&gl_oHth5l}$?O>5xxDP1LFB?SDt4f9M zUacEA;pq>Mx}jCK27DstMv?M;tEPgms-W$bgzNV+3AP5jt<}zz{9=OD7+%vaYLcvN zX+)_kR+nwU79*wh_l0072-d@3;-}m}i%T4M%m-$Z>Ez96QG~FDO?FNd!Z9H75SrM~ zAzV#!e1vi2*6+=DM6Q51&dQbvF|50(6oZv*HHq|{)MYzTbC-RY*G$CvldqF9J2z)r z?NgF$Q}ebozzx10q?5z1dSCiM7{o+|^@^ackl?03o*zOMR1nc>7}k`Ejn&cpHzPc+ zDt5_K2v%7K4ZWh68mY6~ZQYX8+BS-2vX7P!|XBHnENknDMfEKP`L_RuF$Pv7h6Y~PY{hu=XfNhD%J6ndq;GQArAhWxbt87gT0UA z#`(!H&4uXtS4X%K1@DfS)5iIOhu`LZ?-iK=E*h|_ZO6rYi_YU+1l<>U)lJx~1zH95 EA9D2gNdN!< literal 0 HcmV?d00001 diff --git a/scripts/tempSensor/lib/adafruit_io-2.8.0.dist-info/METADATA b/scripts/tempSensor/lib/adafruit_io-2.8.0.dist-info/METADATA new file mode 100644 index 00000000..f2813670 --- /dev/null +++ b/scripts/tempSensor/lib/adafruit_io-2.8.0.dist-info/METADATA @@ -0,0 +1,96 @@ +Metadata-Version: 2.1 +Name: adafruit-io +Version: 2.8.0 +Summary: Python client library for Adafruit IO (http://io.adafruit.com/). +Home-page: https://github.com/adafruit/Adafruit_IO_Python +Author: Adafruit Industries +Author-email: adafruitio@adafruit.com +License: MIT +Keywords: adafruitio io python circuitpython raspberrypi hardware MQTT REST +Classifier: Development Status :: 5 - Production/Stable +Classifier: Operating System :: POSIX :: Linux +Classifier: Operating System :: Microsoft :: Windows +Classifier: Operating System :: MacOS +Classifier: License :: OSI Approved :: MIT License +Classifier: Intended Audience :: Developers +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.4 +Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Topic :: Home Automation +Classifier: Topic :: Software Development +Description-Content-Type: text/x-rst +License-File: LICENSE.md +Requires-Dist: requests +Requires-Dist: paho-mqtt + +Adafruit IO Python +================== + +.. image:: https://readthedocs.org/projects/adafruit-io-python-client/badge/?version=latest + :target: https://adafruit-io-python-client.readthedocs.io/en/latest/ + :alt: Documentation Status + +.. image:: https://img.shields.io/discord/327254708534116352.svg + :target: https://adafru.it/discord + :alt: Chat + +.. image:: https://github.com/adafruit/Adafruit_IO_Python/workflows/Build-CI/badge.svg + :target: https://github.com/adafruit/Adafruit_IO_Python/actions + :alt: Build Status + +.. image:: https://img.shields.io/badge/Try%20out-Adafruit%20IO%20Python-579ACA.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFkAAABZCAMAAABi1XidAAAB8lBMVEX///9XmsrmZYH1olJXmsr1olJXmsrmZYH1olJXmsr1olJXmsrmZYH1olL1olJXmsr1olJXmsrmZYH1olL1olJXmsrmZYH1olJXmsr1olL1olJXmsrmZYH1olL1olJXmsrmZYH1olL1olL0nFf1olJXmsrmZYH1olJXmsq8dZb1olJXmsrmZYH1olJXmspXmspXmsr1olL1olJXmsrmZYH1olJXmsr1olL1olJXmsrmZYH1olL1olLeaIVXmsrmZYH1olL1olL1olJXmsrmZYH1olLna31Xmsr1olJXmsr1olJXmsrmZYH1olLqoVr1olJXmsr1olJXmsrmZYH1olL1olKkfaPobXvviGabgadXmsqThKuofKHmZ4Dobnr1olJXmsr1olJXmspXmsr1olJXmsrfZ4TuhWn1olL1olJXmsqBi7X1olJXmspZmslbmMhbmsdemsVfl8ZgmsNim8Jpk8F0m7R4m7F5nLB6jbh7jbiDirOEibOGnKaMhq+PnaCVg6qWg6qegKaff6WhnpKofKGtnomxeZy3noG6dZi+n3vCcpPDcpPGn3bLb4/Mb47UbIrVa4rYoGjdaIbeaIXhoWHmZYHobXvpcHjqdHXreHLroVrsfG/uhGnuh2bwj2Hxk17yl1vzmljzm1j0nlX1olL3AJXWAAAAbXRSTlMAEBAQHx8gICAuLjAwMDw9PUBAQEpQUFBXV1hgYGBkcHBwcXl8gICAgoiIkJCQlJicnJ2goKCmqK+wsLC4usDAwMjP0NDQ1NbW3Nzg4ODi5+3v8PDw8/T09PX29vb39/f5+fr7+/z8/Pz9/v7+zczCxgAABC5JREFUeAHN1ul3k0UUBvCb1CTVpmpaitAGSLSpSuKCLWpbTKNJFGlcSMAFF63iUmRccNG6gLbuxkXU66JAUef/9LSpmXnyLr3T5AO/rzl5zj137p136BISy44fKJXuGN/d19PUfYeO67Znqtf2KH33Id1psXoFdW30sPZ1sMvs2D060AHqws4FHeJojLZqnw53cmfvg+XR8mC0OEjuxrXEkX5ydeVJLVIlV0e10PXk5k7dYeHu7Cj1j+49uKg7uLU61tGLw1lq27ugQYlclHC4bgv7VQ+TAyj5Zc/UjsPvs1sd5cWryWObtvWT2EPa4rtnWW3JkpjggEpbOsPr7F7EyNewtpBIslA7p43HCsnwooXTEc3UmPmCNn5lrqTJxy6nRmcavGZVt/3Da2pD5NHvsOHJCrdc1G2r3DITpU7yic7w/7Rxnjc0kt5GC4djiv2Sz3Fb2iEZg41/ddsFDoyuYrIkmFehz0HR2thPgQqMyQYb2OtB0WxsZ3BeG3+wpRb1vzl2UYBog8FfGhttFKjtAclnZYrRo9ryG9uG/FZQU4AEg8ZE9LjGMzTmqKXPLnlWVnIlQQTvxJf8ip7VgjZjyVPrjw1te5otM7RmP7xm+sK2Gv9I8Gi++BRbEkR9EBw8zRUcKxwp73xkaLiqQb+kGduJTNHG72zcW9LoJgqQxpP3/Tj//c3yB0tqzaml05/+orHLksVO+95kX7/7qgJvnjlrfr2Ggsyx0eoy9uPzN5SPd86aXggOsEKW2Prz7du3VID3/tzs/sSRs2w7ovVHKtjrX2pd7ZMlTxAYfBAL9jiDwfLkq55Tm7ifhMlTGPyCAs7RFRhn47JnlcB9RM5T97ASuZXIcVNuUDIndpDbdsfrqsOppeXl5Y+XVKdjFCTh+zGaVuj0d9zy05PPK3QzBamxdwtTCrzyg/2Rvf2EstUjordGwa/kx9mSJLr8mLLtCW8HHGJc2R5hS219IiF6PnTusOqcMl57gm0Z8kanKMAQg0qSyuZfn7zItsbGyO9QlnxY0eCuD1XL2ys/MsrQhltE7Ug0uFOzufJFE2PxBo/YAx8XPPdDwWN0MrDRYIZF0mSMKCNHgaIVFoBbNoLJ7tEQDKxGF0kcLQimojCZopv0OkNOyWCCg9XMVAi7ARJzQdM2QUh0gmBozjc3Skg6dSBRqDGYSUOu66Zg+I2fNZs/M3/f/Grl/XnyF1Gw3VKCez0PN5IUfFLqvgUN4C0qNqYs5YhPL+aVZYDE4IpUk57oSFnJm4FyCqqOE0jhY2SMyLFoo56zyo6becOS5UVDdj7Vih0zp+tcMhwRpBeLyqtIjlJKAIZSbI8SGSF3k0pA3mR5tHuwPFoa7N7reoq2bqCsAk1HqCu5uvI1n6JuRXI+S1Mco54YmYTwcn6Aeic+kssXi8XpXC4V3t7/ADuTNKaQJdScAAAAAElFTkSuQmCC + :target: https://mybinder.org/v2/gh/adafruit/adafruit_io_python_jupyter/master?filepath=adafruit-io-python-tutorial.ipynb + +.. image:: https://cdn-learn.adafruit.com/assets/assets/000/057/153/original/adafruit_io_iopython.png?1530802073 + +A Python library and examples for use with `io.adafruit.com `_. + +Compatible with Python Versions 3.6+ + +Installation +================ + +Easy Installation +~~~~~~~~~~~~~~~~~ +If you have `PIP `_ installed (typically with apt-get install python-pip on a Debian/Ubuntu-based system) then run: + +.. code-block:: shell + + pip3 install adafruit-io + +This will automatically install the Adafruit IO Python client code for your Python scripts to use. You might want to examine the examples folder in this GitHub repository to see examples of usage. + +If the above command fails, you may first need to install prerequisites: + +.. code-block:: shell + + pip3 install setuptools + pip3 install wheel + + +Manual Installation +~~~~~~~~~~~~~~~~~~~ + +Clone or download the contents of this repository. Then navigate to the folder in a terminal and run the following command: + + +.. code-block:: shell + + python setup.py install + + + + +Usage +===== + +Documentation for this project is `available on the ReadTheDocs `_. + + +Contributing +============ + +Contributions are welcome! Please read our `Code of Conduct +`_ +before contributing to help this project stay welcoming. diff --git a/scripts/tempSensor/lib/adafruit_io-2.8.0.dist-info/RECORD b/scripts/tempSensor/lib/adafruit_io-2.8.0.dist-info/RECORD new file mode 100644 index 00000000..8e831b4a --- /dev/null +++ b/scripts/tempSensor/lib/adafruit_io-2.8.0.dist-info/RECORD @@ -0,0 +1,9 @@ +Adafruit_IO/__init__.py,, +Adafruit_IO/_version.py,, +Adafruit_IO/client.py,, +Adafruit_IO/errors.py,, +Adafruit_IO/model.py,, +Adafruit_IO/mqtt_client.py,, +adafruit_io-2.8.0.dist-info/METADATA,, +ez_setup.py,, +adafruit_io-2.8.0.dist-info/RECORD,, \ No newline at end of file diff --git a/scripts/tempSensor/lib/bme680-2.0.0.dist-info/METADATA b/scripts/tempSensor/lib/bme680-2.0.0.dist-info/METADATA new file mode 100644 index 00000000..52ebdfd8 --- /dev/null +++ b/scripts/tempSensor/lib/bme680-2.0.0.dist-info/METADATA @@ -0,0 +1,156 @@ +Metadata-Version: 2.3 +Name: bme680 +Version: 2.0.0 +Summary: Python library for the BME680 temperature, humidity and gas sensor +Project-URL: GitHub, https://www.github.com/pimoroni/bme680-python +Project-URL: Homepage, https://www.pimoroni.com +Author-email: Philip Howard +Maintainer-email: Philip Howard +License: MIT License + + Copyright (c) 2018 Pimoroni Ltd + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +License-File: LICENSE +Keywords: Pi,Raspberry +Classifier: Development Status :: 4 - Beta +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: POSIX :: Linux +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Topic :: Software Development +Classifier: Topic :: Software Development :: Libraries +Classifier: Topic :: System :: Hardware +Requires-Python: >=3.7 +Requires-Dist: smbus2 +Description-Content-Type: text/markdown + +# BME680 + +[![Build Status](https://img.shields.io/github/actions/workflow/status/pimoroni/bme680-python/test.yml?branch=main)](https://github.com/pimoroni/bme680-python/actions/workflows/test.yml) +[![Coverage Status](https://coveralls.io/repos/github/pimoroni/bme680-python/badge.svg?branch=main)](https://coveralls.io/github/pimoroni/bme680-python?branch=main) +[![PyPi Package](https://img.shields.io/pypi/v/bme680.svg)](https://pypi.python.org/pypi/bme680) +[![Python Versions](https://img.shields.io/pypi/pyversions/bme680.svg)](https://pypi.python.org/pypi/bme680) + +https://shop.pimoroni.com/products/bme680 + +The state-of-the-art BME680 breakout lets you measure temperature, pressure, humidity, and indoor air quality. + +## Installing + +### Full install (recommended): + +We've created an easy installation script that will install all pre-requisites and get your BME680 +up and running with minimal efforts. To run it, fire up Terminal which you'll find in Menu -> Accessories -> Terminal +on your Raspberry Pi desktop, as illustrated below: + +![Finding the terminal](http://get.pimoroni.com/resources/github-repo-terminal.png) + +In the new terminal window type the command exactly as it appears below (check for typos) and follow the on-screen instructions: + +```bash +git clone https://github.com/pimoroni/bme680-python +cd bme680-python +./install.sh +``` + +**Note** Libraries will be installed in the "pimoroni" virtual environment, you will need to activate it to run examples: + +``` +source ~/.virtualenvs/pimoroni/bin/activate +``` + +### Development: + +If you want to contribute, or like living on the edge of your seat by having the latest code, you can install the development version like so: + +```bash +git clone https://github.com/pimoroni/bme680-python +cd bme680-python +./install.sh --unstable +``` + +In all cases you will have to enable the i2c bus: + +``` +sudo raspi-config nonint do_i2c 0 +``` + +## Documentation & Support + +* Guides and tutorials - https://learn.pimoroni.com/bme680-breakout +* Get help - http://forums.pimoroni.com/c/support + + +2.0.0 +----- + +* Repackage to hatch/pyproject.toml +* Drop Python 2.7 support +* Switch from smbu2 to smbus2 + +1.1.1 +----- + +* New: constants to clarify heater on/off states + +1.1.0 +----- + +* New: support for BME688 "high" gas resistance variant +* New: set/get gas heater disable bit +* Enhancement: fail with descriptive RuntimeError when chip is not detected + +1.0.5 +----- + +* New: set_temp_offset to calibrate temperature offset in degrees C + +1.0.4 +----- + +* Fix to range_sw_err for extremely high gas readings +* Convert to unsigned int to fix negative gas readings + +1.0.3 +----- + +* Merged temperature compensation fix from Bosch's BME680_driver 3.5.3 + +1.0.2 +----- + +* Fixed set_gas_heater_temperature to avoid i2c TypeError + +1.0.1 +----- + +* Added Manifest to Python package + +1.0.0 +----- + +* Initial release + diff --git a/scripts/tempSensor/lib/bme680-2.0.0.dist-info/RECORD b/scripts/tempSensor/lib/bme680-2.0.0.dist-info/RECORD new file mode 100644 index 00000000..35d5cc6b --- /dev/null +++ b/scripts/tempSensor/lib/bme680-2.0.0.dist-info/RECORD @@ -0,0 +1,7 @@ +CHANGELOG.md,, +LICENSE,, +README.md,, +bme680-2.0.0.dist-info/METADATA,, +bme680/__init__.py,, +bme680/constants.py,, +bme680-2.0.0.dist-info/RECORD,, \ No newline at end of file diff --git a/scripts/tempSensor/lib/bme680/__init__.py b/scripts/tempSensor/lib/bme680/__init__.py new file mode 100644 index 00000000..56d547a1 --- /dev/null +++ b/scripts/tempSensor/lib/bme680/__init__.py @@ -0,0 +1,486 @@ +"""BME680 Temperature, Pressure, Humidity & Gas Sensor.""" +import math +import time + +from . import constants +from .constants import BME680Data, lookupTable1, lookupTable2 + +__version__ = '2.0.0' + + +# Export constants to global namespace +# so end-users can "from BME680 import NAME" +if hasattr(constants, '__dict__'): + for key in constants.__dict__: + value = constants.__dict__[key] + if key not in globals(): + globals()[key] = value + + +class BME680(BME680Data): + """BOSCH BME680. + + Gas, pressure, temperature and humidity sensor. + + :param i2c_addr: One of I2C_ADDR_PRIMARY (0x76) or I2C_ADDR_SECONDARY (0x77) + :param i2c_device: Optional smbus or compatible instance for facilitating i2c communications. + + """ + + def __init__(self, i2c_addr=constants.I2C_ADDR_PRIMARY, i2c_device=None): + """Initialise BME680 sensor instance and verify device presence. + + :param i2c_addr: i2c address of BME680 + :param i2c_device: Optional SMBus-compatible instance for i2c transport + + """ + BME680Data.__init__(self) + + self.i2c_addr = i2c_addr + self._i2c = i2c_device + if self._i2c is None: + import smbus2 + self._i2c = smbus2.SMBus(1) + + try: + self.chip_id = self._get_regs(constants.CHIP_ID_ADDR, 1) + if self.chip_id != constants.CHIP_ID: + raise RuntimeError('BME680 Not Found. Invalid CHIP ID: 0x{0:02x}'.format(self.chip_id)) + except IOError: + raise RuntimeError("Unable to identify BME680 at 0x{:02x} (IOError)".format(self.i2c_addr)) + + self._variant = self._get_regs(constants.CHIP_VARIANT_ADDR, 1) + + self.soft_reset() + self.set_power_mode(constants.SLEEP_MODE) + + self._get_calibration_data() + + self.set_humidity_oversample(constants.OS_2X) + self.set_pressure_oversample(constants.OS_4X) + self.set_temperature_oversample(constants.OS_8X) + self.set_filter(constants.FILTER_SIZE_3) + if self._variant == constants.VARIANT_HIGH: + self.set_gas_status(constants.ENABLE_GAS_MEAS_HIGH) + else: + self.set_gas_status(constants.ENABLE_GAS_MEAS_LOW) + self.set_temp_offset(0) + self.get_sensor_data() + + def _get_calibration_data(self): + """Retrieve the sensor calibration data and store it in .calibration_data.""" + calibration = self._get_regs(constants.COEFF_ADDR1, constants.COEFF_ADDR1_LEN) + calibration += self._get_regs(constants.COEFF_ADDR2, constants.COEFF_ADDR2_LEN) + + heat_range = self._get_regs(constants.ADDR_RES_HEAT_RANGE_ADDR, 1) + heat_value = constants.twos_comp(self._get_regs(constants.ADDR_RES_HEAT_VAL_ADDR, 1), bits=8) + sw_error = constants.twos_comp(self._get_regs(constants.ADDR_RANGE_SW_ERR_ADDR, 1), bits=8) + + self.calibration_data.set_from_array(calibration) + self.calibration_data.set_other(heat_range, heat_value, sw_error) + + def soft_reset(self): + """Trigger a soft reset.""" + self._set_regs(constants.SOFT_RESET_ADDR, constants.SOFT_RESET_CMD) + time.sleep(constants.RESET_PERIOD / 1000.0) + + def set_temp_offset(self, value): + """Set temperature offset in celsius. + + If set, the temperature t_fine will be increased by given value in celsius. + :param value: Temperature offset in Celsius, eg. 4, -8, 1.25 + + """ + if value == 0: + self.offset_temp_in_t_fine = 0 + else: + self.offset_temp_in_t_fine = int(math.copysign((((int(abs(value) * 100)) << 8) - 128) / 5, value)) + + def set_humidity_oversample(self, value): + """Set humidity oversampling. + + A higher oversampling value means more stable sensor readings, + with less noise and jitter. + + However each step of oversampling adds about 2ms to the latency, + causing a slower response time to fast transients. + + :param value: Oversampling value, one of: OS_NONE, OS_1X, OS_2X, OS_4X, OS_8X, OS_16X + + """ + self.tph_settings.os_hum = value + self._set_bits(constants.CONF_OS_H_ADDR, constants.OSH_MSK, constants.OSH_POS, value) + + def get_humidity_oversample(self): + """Get humidity oversampling.""" + return (self._get_regs(constants.CONF_OS_H_ADDR, 1) & constants.OSH_MSK) >> constants.OSH_POS + + def set_pressure_oversample(self, value): + """Set temperature oversampling. + + A higher oversampling value means more stable sensor readings, + with less noise and jitter. + + However each step of oversampling adds about 2ms to the latency, + causing a slower response time to fast transients. + + :param value: Oversampling value, one of: OS_NONE, OS_1X, OS_2X, OS_4X, OS_8X, OS_16X + + """ + self.tph_settings.os_pres = value + self._set_bits(constants.CONF_T_P_MODE_ADDR, constants.OSP_MSK, constants.OSP_POS, value) + + def get_pressure_oversample(self): + """Get pressure oversampling.""" + return (self._get_regs(constants.CONF_T_P_MODE_ADDR, 1) & constants.OSP_MSK) >> constants.OSP_POS + + def set_temperature_oversample(self, value): + """Set pressure oversampling. + + A higher oversampling value means more stable sensor readings, + with less noise and jitter. + + However each step of oversampling adds about 2ms to the latency, + causing a slower response time to fast transients. + + :param value: Oversampling value, one of: OS_NONE, OS_1X, OS_2X, OS_4X, OS_8X, OS_16X + + """ + self.tph_settings.os_temp = value + self._set_bits(constants.CONF_T_P_MODE_ADDR, constants.OST_MSK, constants.OST_POS, value) + + def get_temperature_oversample(self): + """Get temperature oversampling.""" + return (self._get_regs(constants.CONF_T_P_MODE_ADDR, 1) & constants.OST_MSK) >> constants.OST_POS + + def set_filter(self, value): + """Set IIR filter size. + + Optionally remove short term fluctuations from the temperature and pressure readings, + increasing their resolution but reducing their bandwidth. + + Enabling the IIR filter does not slow down the time a reading takes, but will slow + down the BME680s response to changes in temperature and pressure. + + When the IIR filter is enabled, the temperature and pressure resolution is effectively 20bit. + When it is disabled, it is 16bit + oversampling-1 bits. + + """ + self.tph_settings.filter = value + self._set_bits(constants.CONF_ODR_FILT_ADDR, constants.FILTER_MSK, constants.FILTER_POS, value) + + def get_filter(self): + """Get filter size.""" + return (self._get_regs(constants.CONF_ODR_FILT_ADDR, 1) & constants.FILTER_MSK) >> constants.FILTER_POS + + def select_gas_heater_profile(self, value): + """Set current gas sensor conversion profile. + + Select one of the 10 configured heating durations/set points. + + :param value: Profile index from 0 to 9 + + """ + if value > constants.NBCONV_MAX or value < constants.NBCONV_MIN: + raise ValueError("Profile '{}' should be between {} and {}".format(value, constants.NBCONV_MIN, constants.NBCONV_MAX)) + + self.gas_settings.nb_conv = value + self._set_bits(constants.CONF_ODR_RUN_GAS_NBC_ADDR, constants.NBCONV_MSK, constants.NBCONV_POS, value) + + def get_gas_heater_profile(self): + """Get gas sensor conversion profile: 0 to 9.""" + return self._get_regs(constants.CONF_ODR_RUN_GAS_NBC_ADDR, 1) & constants.NBCONV_MSK + + def set_gas_heater_status(self, value): + """Enable/disable gas heater.""" + self.gas_settings.heater = value + self._set_bits(constants.CONF_HEAT_CTRL_ADDR, constants.HCTRL_MSK, constants.HCTRL_POS, value) + + def get_gas_heater_status(self): + """Get current heater status.""" + return (self._get_regs(constants.CONF_HEAT_CTRL_ADDR, 1) & constants.HCTRL_MSK) >> constants.HCTRL_POS + + def set_gas_status(self, value): + """Enable/disable gas sensor.""" + if value == -1: + if self._variant == constants.VARIANT_HIGH: + value = constants.ENABLE_GAS_MEAS_HIGH + else: + value = constants.ENABLE_GAS_MEAS_LOW + self.gas_settings.run_gas = value + self._set_bits(constants.CONF_ODR_RUN_GAS_NBC_ADDR, constants.RUN_GAS_MSK, constants.RUN_GAS_POS, value) + + def get_gas_status(self): + """Get the current gas status.""" + return (self._get_regs(constants.CONF_ODR_RUN_GAS_NBC_ADDR, 1) & constants.RUN_GAS_MSK) >> constants.RUN_GAS_POS + + def set_gas_heater_profile(self, temperature, duration, nb_profile=0): + """Set temperature and duration of gas sensor heater. + + :param temperature: Target temperature in degrees celsius, between 200 and 400 + :param durarion: Target duration in milliseconds, between 1 and 4032 + :param nb_profile: Target profile, between 0 and 9 + + """ + self.set_gas_heater_temperature(temperature, nb_profile=nb_profile) + self.set_gas_heater_duration(duration, nb_profile=nb_profile) + + def set_gas_heater_temperature(self, value, nb_profile=0): + """Set gas sensor heater temperature. + + :param value: Target temperature in degrees celsius, between 200 and 400 + + When setting an nb_profile other than 0, + make sure to select it with select_gas_heater_profile. + + """ + if nb_profile > constants.NBCONV_MAX or value < constants.NBCONV_MIN: + raise ValueError('Profile "{}" should be between {} and {}'.format(nb_profile, constants.NBCONV_MIN, constants.NBCONV_MAX)) + + self.gas_settings.heatr_temp = value + temp = int(self._calc_heater_resistance(self.gas_settings.heatr_temp)) + self._set_regs(constants.RES_HEAT0_ADDR + nb_profile, temp) + + def set_gas_heater_duration(self, value, nb_profile=0): + """Set gas sensor heater duration. + + Heating durations between 1 ms and 4032 ms can be configured. + Approximately 20-30 ms are necessary for the heater to reach the intended target temperature. + + :param value: Heating duration in milliseconds. + + When setting an nb_profile other than 0, + make sure to select it with select_gas_heater_profile. + + """ + if nb_profile > constants.NBCONV_MAX or value < constants.NBCONV_MIN: + raise ValueError('Profile "{}" should be between {} and {}'.format(nb_profile, constants.NBCONV_MIN, constants.NBCONV_MAX)) + + self.gas_settings.heatr_dur = value + temp = self._calc_heater_duration(self.gas_settings.heatr_dur) + self._set_regs(constants.GAS_WAIT0_ADDR + nb_profile, temp) + + def set_power_mode(self, value, blocking=True): + """Set power mode.""" + if value not in (constants.SLEEP_MODE, constants.FORCED_MODE): + raise ValueError('Power mode should be one of SLEEP_MODE or FORCED_MODE') + + self.power_mode = value + + self._set_bits(constants.CONF_T_P_MODE_ADDR, constants.MODE_MSK, constants.MODE_POS, value) + + while blocking and self.get_power_mode() != self.power_mode: + time.sleep(constants.POLL_PERIOD_MS / 1000.0) + + def get_power_mode(self): + """Get power mode.""" + self.power_mode = self._get_regs(constants.CONF_T_P_MODE_ADDR, 1) + return self.power_mode + + def get_sensor_data(self): + """Get sensor data. + + Stores data in .data and returns True upon success. + + """ + self.set_power_mode(constants.FORCED_MODE) + + for attempt in range(10): + status = self._get_regs(constants.FIELD0_ADDR, 1) + + if (status & constants.NEW_DATA_MSK) == 0: + time.sleep(constants.POLL_PERIOD_MS / 1000.0) + continue + + regs = self._get_regs(constants.FIELD0_ADDR, constants.FIELD_LENGTH) + + self.data.status = regs[0] & constants.NEW_DATA_MSK + # Contains the nb_profile used to obtain the current measurement + self.data.gas_index = regs[0] & constants.GAS_INDEX_MSK + self.data.meas_index = regs[1] + + adc_pres = (regs[2] << 12) | (regs[3] << 4) | (regs[4] >> 4) + adc_temp = (regs[5] << 12) | (regs[6] << 4) | (regs[7] >> 4) + adc_hum = (regs[8] << 8) | regs[9] + adc_gas_res_low = (regs[13] << 2) | (regs[14] >> 6) + adc_gas_res_high = (regs[15] << 2) | (regs[16] >> 6) + gas_range_l = regs[14] & constants.GAS_RANGE_MSK + gas_range_h = regs[16] & constants.GAS_RANGE_MSK + + if self._variant == constants.VARIANT_HIGH: + self.data.status |= regs[16] & constants.GASM_VALID_MSK + self.data.status |= regs[16] & constants.HEAT_STAB_MSK + else: + self.data.status |= regs[14] & constants.GASM_VALID_MSK + self.data.status |= regs[14] & constants.HEAT_STAB_MSK + + self.data.heat_stable = (self.data.status & constants.HEAT_STAB_MSK) > 0 + + temperature = self._calc_temperature(adc_temp) + self.data.temperature = temperature / 100.0 + self.ambient_temperature = temperature # Saved for heater calc + + self.data.pressure = self._calc_pressure(adc_pres) / 100.0 + self.data.humidity = self._calc_humidity(adc_hum) / 1000.0 + + if self._variant == constants.VARIANT_HIGH: + self.data.gas_resistance = self._calc_gas_resistance_high(adc_gas_res_high, gas_range_h) + else: + self.data.gas_resistance = self._calc_gas_resistance_low(adc_gas_res_low, gas_range_l) + + return True + + return False + + def _set_bits(self, register, mask, position, value): + """Mask out and set one or more bits in a register.""" + temp = self._get_regs(register, 1) + temp &= ~mask + temp |= value << position + self._set_regs(register, temp) + + def _set_regs(self, register, value): + """Set one or more registers.""" + if isinstance(value, int): + self._i2c.write_byte_data(self.i2c_addr, register, value) + else: + self._i2c.write_i2c_block_data(self.i2c_addr, register, value) + + def _get_regs(self, register, length): + """Get one or more registers.""" + if length == 1: + return self._i2c.read_byte_data(self.i2c_addr, register) + else: + return self._i2c.read_i2c_block_data(self.i2c_addr, register, length) + + def _calc_temperature(self, temperature_adc): + """Convert the raw temperature to degrees C using calibration_data.""" + var1 = (temperature_adc >> 3) - (self.calibration_data.par_t1 << 1) + var2 = (var1 * self.calibration_data.par_t2) >> 11 + var3 = ((var1 >> 1) * (var1 >> 1)) >> 12 + var3 = ((var3) * (self.calibration_data.par_t3 << 4)) >> 14 + + # Save teperature data for pressure calculations + self.calibration_data.t_fine = (var2 + var3) + self.offset_temp_in_t_fine + calc_temp = (((self.calibration_data.t_fine * 5) + 128) >> 8) + + return calc_temp + + def _calc_pressure(self, pressure_adc): + """Convert the raw pressure using calibration data.""" + var1 = ((self.calibration_data.t_fine) >> 1) - 64000 + var2 = ((((var1 >> 2) * (var1 >> 2)) >> 11) * + self.calibration_data.par_p6) >> 2 + var2 = var2 + ((var1 * self.calibration_data.par_p5) << 1) + var2 = (var2 >> 2) + (self.calibration_data.par_p4 << 16) + var1 = (((((var1 >> 2) * (var1 >> 2)) >> 13) * + ((self.calibration_data.par_p3 << 5)) >> 3) + + ((self.calibration_data.par_p2 * var1) >> 1)) + var1 = var1 >> 18 + + var1 = ((32768 + var1) * self.calibration_data.par_p1) >> 15 + calc_pressure = 1048576 - pressure_adc + calc_pressure = ((calc_pressure - (var2 >> 12)) * (3125)) + + if calc_pressure >= (1 << 31): + calc_pressure = ((calc_pressure // var1) << 1) + else: + calc_pressure = ((calc_pressure << 1) // var1) + + var1 = (self.calibration_data.par_p9 * (((calc_pressure >> 3) * + (calc_pressure >> 3)) >> 13)) >> 12 + var2 = ((calc_pressure >> 2) * + self.calibration_data.par_p8) >> 13 + var3 = ((calc_pressure >> 8) * (calc_pressure >> 8) * + (calc_pressure >> 8) * + self.calibration_data.par_p10) >> 17 + + calc_pressure = (calc_pressure) + ((var1 + var2 + var3 + + (self.calibration_data.par_p7 << 7)) >> 4) + + return calc_pressure + + def _calc_humidity(self, humidity_adc): + """Convert the raw humidity using calibration data.""" + temp_scaled = ((self.calibration_data.t_fine * 5) + 128) >> 8 + var1 = (humidity_adc - ((self.calibration_data.par_h1 * 16))) -\ + (((temp_scaled * self.calibration_data.par_h3) // (100)) >> 1) + var2 = (self.calibration_data.par_h2 * + (((temp_scaled * self.calibration_data.par_h4) // (100)) + + (((temp_scaled * ((temp_scaled * self.calibration_data.par_h5) // (100))) >> 6) // + (100)) + (1 * 16384))) >> 10 + var3 = var1 * var2 + var4 = self.calibration_data.par_h6 << 7 + var4 = ((var4) + ((temp_scaled * self.calibration_data.par_h7) // (100))) >> 4 + var5 = ((var3 >> 14) * (var3 >> 14)) >> 10 + var6 = (var4 * var5) >> 1 + calc_hum = (((var3 + var6) >> 10) * (1000)) >> 12 + + return min(max(calc_hum, 0), 100000) + + def _calc_gas_resistance(self, gas_res_adc, gas_range): + """Convert the raw gas resistance using calibration data.""" + if self._variant == constants.VARIANT_HIGH: + return self._calc_gas_resistance_high(gas_res_adc, gas_range) + else: + return self._calc_gas_resistance_low(gas_res_adc, gas_range) + + def _calc_gas_resistance_high(self, gas_res_adc, gas_range): + """Convert the raw gas resistance using calibration data. + + Applies to Variant ID == 0x01 only. + + """ + var1 = 262144 >> gas_range + var2 = gas_res_adc - 512 + + var2 *= 3 + var2 = 4096 + var2 + + calc_gas_res = (10000 * var1) / var2 + calc_gas_res *= 100 + + return calc_gas_res + + def _calc_gas_resistance_low(self, gas_res_adc, gas_range): + """Convert the raw gas resistance using calibration data. + + Applies to Variant ID == 0x00 only. + + """ + var1 = ((1340 + (5 * self.calibration_data.range_sw_err)) * (lookupTable1[gas_range])) >> 16 + var2 = (((gas_res_adc << 15) - (16777216)) + var1) + var3 = ((lookupTable2[gas_range] * var1) >> 9) + calc_gas_res = ((var3 + (var2 >> 1)) / var2) + + if calc_gas_res < 0: + calc_gas_res = (1 << 32) + calc_gas_res + + return calc_gas_res + + def _calc_heater_resistance(self, temperature): + """Convert raw heater resistance using calibration data.""" + temperature = min(max(temperature, 200), 400) + + var1 = ((self.ambient_temperature * self.calibration_data.par_gh3) / 1000) * 256 + var2 = (self.calibration_data.par_gh1 + 784) * (((((self.calibration_data.par_gh2 + 154009) * temperature * 5) / 100) + 3276800) / 10) + var3 = var1 + (var2 / 2) + var4 = (var3 / (self.calibration_data.res_heat_range + 4)) + var5 = (131 * self.calibration_data.res_heat_val) + 65536 + heatr_res_x100 = (((var4 / var5) - 250) * 34) + heatr_res = ((heatr_res_x100 + 50) / 100) + + return heatr_res + + def _calc_heater_duration(self, duration): + """Calculate correct value for heater duration setting from milliseconds.""" + if duration < 0xfc0: + factor = 0 + + while duration > 0x3f: + duration /= 4 + factor += 1 + + return int(duration + (factor * 64)) + + return 0xff diff --git a/scripts/tempSensor/lib/bme680/constants.py b/scripts/tempSensor/lib/bme680/constants.py new file mode 100644 index 00000000..d77415d3 --- /dev/null +++ b/scripts/tempSensor/lib/bme680/constants.py @@ -0,0 +1,413 @@ +"""BME680 constants, structures and utilities.""" + +# BME680 General config +POLL_PERIOD_MS = 10 + +# BME680 I2C addresses +I2C_ADDR_PRIMARY = 0x76 +I2C_ADDR_SECONDARY = 0x77 + +# BME680 unique chip identifier +CHIP_ID = 0x61 + +# BME680 coefficients related defines +COEFF_SIZE = 41 +COEFF_ADDR1_LEN = 25 +COEFF_ADDR2_LEN = 16 + +# BME680 field_x related defines +FIELD_LENGTH = 17 +FIELD_ADDR_OFFSET = 17 + +# Soft reset command +SOFT_RESET_CMD = 0xb6 + +# Error code definitions +OK = 0 +# Errors +E_NULL_PTR = -1 +E_COM_FAIL = -2 +E_DEV_NOT_FOUND = -3 +E_INVALID_LENGTH = -4 + +# Warnings +W_DEFINE_PWR_MODE = 1 +W_NO_NEW_DATA = 2 + +# Info's +I_MIN_CORRECTION = 1 +I_MAX_CORRECTION = 2 + +# Register map +# Other coefficient's address +ADDR_RES_HEAT_VAL_ADDR = 0x00 +ADDR_RES_HEAT_RANGE_ADDR = 0x02 +ADDR_RANGE_SW_ERR_ADDR = 0x04 +ADDR_SENS_CONF_START = 0x5A +ADDR_GAS_CONF_START = 0x64 + +# Field settings +FIELD0_ADDR = 0x1d + +# Heater settings +RES_HEAT0_ADDR = 0x5a +GAS_WAIT0_ADDR = 0x64 + +# Sensor configuration registers +CONF_HEAT_CTRL_ADDR = 0x70 +CONF_ODR_RUN_GAS_NBC_ADDR = 0x71 +CONF_OS_H_ADDR = 0x72 +MEM_PAGE_ADDR = 0xf3 +CONF_T_P_MODE_ADDR = 0x74 +CONF_ODR_FILT_ADDR = 0x75 + +# Coefficient's address +COEFF_ADDR1 = 0x89 +COEFF_ADDR2 = 0xe1 + +# Chip identifier +CHIP_ID_ADDR = 0xd0 +CHIP_VARIANT_ADDR = 0xf0 + +VARIANT_LOW = 0x00 +VARIANT_HIGH = 0x01 + +# Soft reset register +SOFT_RESET_ADDR = 0xe0 + +# Heater control settings +ENABLE_HEATER = 0x00 +DISABLE_HEATER = 0x08 + +# Gas measurement settings +DISABLE_GAS_MEAS = 0x00 +ENABLE_GAS_MEAS = -1 # Now used as auto-select +ENABLE_GAS_MEAS_LOW = 0x01 +ENABLE_GAS_MEAS_HIGH = 0x02 + +# Over-sampling settings +OS_NONE = 0 +OS_1X = 1 +OS_2X = 2 +OS_4X = 3 +OS_8X = 4 +OS_16X = 5 + +# IIR filter settings +FILTER_SIZE_0 = 0 +FILTER_SIZE_1 = 1 +FILTER_SIZE_3 = 2 +FILTER_SIZE_7 = 3 +FILTER_SIZE_15 = 4 +FILTER_SIZE_31 = 5 +FILTER_SIZE_63 = 6 +FILTER_SIZE_127 = 7 + +# Power mode settings +SLEEP_MODE = 0 +FORCED_MODE = 1 + +# Delay related macro declaration +RESET_PERIOD = 10 + +# SPI memory page settings +MEM_PAGE0 = 0x10 +MEM_PAGE1 = 0x00 + +# Ambient humidity shift value for compensation +HUM_REG_SHIFT_VAL = 4 + +# Run gas enable and disable settings +RUN_GAS_DISABLE = 0 +RUN_GAS_ENABLE = 1 + +# Gas heater enable and disable settings +GAS_HEAT_ENABLE = 0 +GAS_HEAT_DISABLE = 1 + +# Buffer length macro declaration +TMP_BUFFER_LENGTH = 40 +REG_BUFFER_LENGTH = 6 +FIELD_DATA_LENGTH = 3 +GAS_REG_BUF_LENGTH = 20 +GAS_HEATER_PROF_LEN_MAX = 10 + +# Settings selector +OST_SEL = 1 +OSP_SEL = 2 +OSH_SEL = 4 +GAS_MEAS_SEL = 8 +FILTER_SEL = 16 +HCNTRL_SEL = 32 +RUN_GAS_SEL = 64 +NBCONV_SEL = 128 +GAS_SENSOR_SEL = GAS_MEAS_SEL | RUN_GAS_SEL | NBCONV_SEL + +# Number of conversion settings +NBCONV_MIN = 0 +NBCONV_MAX = 9 # Was 10, but there are only 10 settings: 0 1 2 ... 8 9 + +# Mask definitions +GAS_MEAS_MSK = 0x30 +NBCONV_MSK = 0X0F +FILTER_MSK = 0X1C +OST_MSK = 0XE0 +OSP_MSK = 0X1C +OSH_MSK = 0X07 +HCTRL_MSK = 0x08 +RUN_GAS_MSK = 0x30 +MODE_MSK = 0x03 +RHRANGE_MSK = 0x30 +RSERROR_MSK = 0xf0 +NEW_DATA_MSK = 0x80 +GAS_INDEX_MSK = 0x0f +GAS_RANGE_MSK = 0x0f +GASM_VALID_MSK = 0x20 +HEAT_STAB_MSK = 0x10 +MEM_PAGE_MSK = 0x10 +SPI_RD_MSK = 0x80 +SPI_WR_MSK = 0x7f +BIT_H1_DATA_MSK = 0x0F + +# Bit position definitions for sensor settings +GAS_MEAS_POS = 4 +FILTER_POS = 2 +OST_POS = 5 +OSP_POS = 2 +OSH_POS = 0 +HCTRL_POS = 3 +RUN_GAS_POS = 4 +MODE_POS = 0 +NBCONV_POS = 0 + +# Array Index to Field data mapping for Calibration Data +T2_LSB_REG = 1 +T2_MSB_REG = 2 +T3_REG = 3 +P1_LSB_REG = 5 +P1_MSB_REG = 6 +P2_LSB_REG = 7 +P2_MSB_REG = 8 +P3_REG = 9 +P4_LSB_REG = 11 +P4_MSB_REG = 12 +P5_LSB_REG = 13 +P5_MSB_REG = 14 +P7_REG = 15 +P6_REG = 16 +P8_LSB_REG = 19 +P8_MSB_REG = 20 +P9_LSB_REG = 21 +P9_MSB_REG = 22 +P10_REG = 23 +H2_MSB_REG = 25 +H2_LSB_REG = 26 +H1_LSB_REG = 26 +H1_MSB_REG = 27 +H3_REG = 28 +H4_REG = 29 +H5_REG = 30 +H6_REG = 31 +H7_REG = 32 +T1_LSB_REG = 33 +T1_MSB_REG = 34 +GH2_LSB_REG = 35 +GH2_MSB_REG = 36 +GH1_REG = 37 +GH3_REG = 38 + +# BME680 register buffer index settings +REG_FILTER_INDEX = 5 +REG_TEMP_INDEX = 4 +REG_PRES_INDEX = 4 +REG_HUM_INDEX = 2 +REG_NBCONV_INDEX = 1 +REG_RUN_GAS_INDEX = 1 +REG_HCTRL_INDEX = 0 + +# Look up tables for the possible gas range values +lookupTable1 = [2147483647, 2147483647, 2147483647, 2147483647, + 2147483647, 2126008810, 2147483647, 2130303777, 2147483647, + 2147483647, 2143188679, 2136746228, 2147483647, 2126008810, + 2147483647, 2147483647] + +lookupTable2 = [4096000000, 2048000000, 1024000000, 512000000, + 255744255, 127110228, 64000000, 32258064, + 16016016, 8000000, 4000000, 2000000, + 1000000, 500000, 250000, 125000] + + +def bytes_to_word(msb, lsb, bits=16, signed=False): + """Convert a most and least significant byte into a word.""" + # TODO: Reimplement with struct + word = (msb << 8) | lsb + if signed: + word = twos_comp(word, bits) + return word + + +def twos_comp(val, bits=16): + """Convert two bytes into a two's compliment signed word.""" + # TODO: Reimplement with struct + if val & (1 << (bits - 1)) != 0: + val = val - (1 << bits) + return val + + +class FieldData: + """Structure for storing BME680 sensor data.""" + + def __init__(self): # noqa D107 + # Contains new_data, gasm_valid & heat_stab + self.status = None + self.heat_stable = False + # The index of the heater profile used + self.gas_index = None + # Measurement index to track order + self.meas_index = None + # Temperature in degree celsius x100 + self.temperature = None + # Pressure in Pascal + self.pressure = None + # Humidity in % relative humidity x1000 + self.humidity = None + # Gas resistance in Ohms + self.gas_resistance = None + + +class CalibrationData: + """Structure for storing BME680 calibration data.""" + + def __init__(self): # noqa D107 + self.par_h1 = None + self.par_h2 = None + self.par_h3 = None + self.par_h4 = None + self.par_h5 = None + self.par_h6 = None + self.par_h7 = None + self.par_gh1 = None + self.par_gh2 = None + self.par_gh3 = None + self.par_t1 = None + self.par_t2 = None + self.par_t3 = None + self.par_p1 = None + self.par_p2 = None + self.par_p3 = None + self.par_p4 = None + self.par_p5 = None + self.par_p6 = None + self.par_p7 = None + self.par_p8 = None + self.par_p9 = None + self.par_p10 = None + # Variable to store t_fine size + self.t_fine = None + # Variable to store heater resistance range + self.res_heat_range = None + # Variable to store heater resistance value + self.res_heat_val = None + # Variable to store error range + self.range_sw_err = None + + def set_from_array(self, calibration): + """Set parameters from an array of bytes.""" + # Temperature related coefficients + self.par_t1 = bytes_to_word(calibration[T1_MSB_REG], calibration[T1_LSB_REG]) + self.par_t2 = bytes_to_word(calibration[T2_MSB_REG], calibration[T2_LSB_REG], bits=16, signed=True) + self.par_t3 = twos_comp(calibration[T3_REG], bits=8) + + # Pressure related coefficients + self.par_p1 = bytes_to_word(calibration[P1_MSB_REG], calibration[P1_LSB_REG]) + self.par_p2 = bytes_to_word(calibration[P2_MSB_REG], calibration[P2_LSB_REG], bits=16, signed=True) + self.par_p3 = twos_comp(calibration[P3_REG], bits=8) + self.par_p4 = bytes_to_word(calibration[P4_MSB_REG], calibration[P4_LSB_REG], bits=16, signed=True) + self.par_p5 = bytes_to_word(calibration[P5_MSB_REG], calibration[P5_LSB_REG], bits=16, signed=True) + self.par_p6 = twos_comp(calibration[P6_REG], bits=8) + self.par_p7 = twos_comp(calibration[P7_REG], bits=8) + self.par_p8 = bytes_to_word(calibration[P8_MSB_REG], calibration[P8_LSB_REG], bits=16, signed=True) + self.par_p9 = bytes_to_word(calibration[P9_MSB_REG], calibration[P9_LSB_REG], bits=16, signed=True) + self.par_p10 = calibration[P10_REG] + + # Humidity related coefficients + self.par_h1 = (calibration[H1_MSB_REG] << HUM_REG_SHIFT_VAL) | (calibration[H1_LSB_REG] & BIT_H1_DATA_MSK) + self.par_h2 = (calibration[H2_MSB_REG] << HUM_REG_SHIFT_VAL) | (calibration[H2_LSB_REG] >> HUM_REG_SHIFT_VAL) + self.par_h3 = twos_comp(calibration[H3_REG], bits=8) + self.par_h4 = twos_comp(calibration[H4_REG], bits=8) + self.par_h5 = twos_comp(calibration[H5_REG], bits=8) + self.par_h6 = calibration[H6_REG] + self.par_h7 = twos_comp(calibration[H7_REG], bits=8) + + # Gas heater related coefficients + self.par_gh1 = twos_comp(calibration[GH1_REG], bits=8) + self.par_gh2 = bytes_to_word(calibration[GH2_MSB_REG], calibration[GH2_LSB_REG], bits=16, signed=True) + self.par_gh3 = twos_comp(calibration[GH3_REG], bits=8) + + def set_other(self, heat_range, heat_value, sw_error): + """Set other values.""" + self.res_heat_range = (heat_range & RHRANGE_MSK) // 16 + self.res_heat_val = heat_value + self.range_sw_err = (sw_error & RSERROR_MSK) // 16 + + +class TPHSettings: + """Structure for storing BME680 sensor settings. + + Comprises of output data rate, over-sampling and filter settings. + + """ + + def __init__(self): # noqa D107 + # Humidity oversampling + self.os_hum = None + # Temperature oversampling + self.os_temp = None + # Pressure oversampling + self.os_pres = None + # Filter coefficient + self.filter = None + + +class GasSettings: + """Structure for storing BME680 gas settings and status.""" + + def __init__(self): # noqa D107 + # Variable to store nb conversion + self.nb_conv = None + # Variable to store heater control + self.heatr_ctrl = None + # Run gas enable value + self.run_gas = None + # Pointer to store heater temperature + self.heatr_temp = None + # Pointer to store duration profile + self.heatr_dur = None + + +class BME680Data: + """Structure to represent BME680 device.""" + + def __init__(self): # noqa D107 + # Chip Id + self.chip_id = None + # Device Id + self.dev_id = None + # SPI/I2C interface + self.intf = None + # Memory page used + self.mem_page = None + # Ambient temperature in Degree C + self.ambient_temperature = None + # Field Data + self.data = FieldData() + # Sensor calibration data + self.calibration_data = CalibrationData() + # Sensor settings + self.tph_settings = TPHSettings() + # Gas Sensor settings + self.gas_settings = GasSettings() + # Sensor power modes + self.power_mode = None + # New sensor fields + self.new_fields = None diff --git a/scripts/tempSensor/lib/board-1.0.dist-info/METADATA b/scripts/tempSensor/lib/board-1.0.dist-info/METADATA new file mode 100644 index 00000000..665775d3 --- /dev/null +++ b/scripts/tempSensor/lib/board-1.0.dist-info/METADATA @@ -0,0 +1,370 @@ +Metadata-Version: 2.1 +Name: board +Version: 1.0 +Summary: Standard Board mechanism for Dojo tasks +Home-page: https://github.com/tjguk/dojo-board +Author: Tim Golden +Author-email: mail@timgolden.me.uk +Maintainer: Tim Golden +Maintainer-email: mail@timgolden.me.uk +License: unlicensed +Platform: UNKNOWN +Classifier: Environment :: Console +Classifier: Intended Audience :: Developers +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 + +Board Game for Python Dojos +=========================== + +Introduction +------------ + +Often, when running a Python Dojo, we've ended up with a challenge +based around some kind of board or tile-based landscape. In these +situations it's not uncommon to spend a lot of the time building up +your basic board functionality in order to support the more interesting +gameplay algorithm. + +This module implements a general-purpose board structure which +has the functionality needed for a range of purposes, and lends itself +to being subclassed for those particular needs. + +Dependencies +------------ + +None - stdlib only + +Tests +----- + +Fairly decent coverage (not actually checked with coverage.py): test.py + +Getting Started +--------------- + +Install with pip:: + + pip install board + +Absolutely basic usage:: + + import board + # + # Produce a 3x3 board + # + b = board.Board((3, 3)) + + b[0, 0] = "X" + b[1, 0] = "O" + +Usage +----- + +Board is an n-dimensional board, any of which dimensions can be of +infinite size. (So if you have, say, 3 infinite dimensions, you have +the basis for a Minecraft layout). Dimensions are zero-based and +negative indexes operate as they usually do in Python: working from +the end of the dimension backwards. + +Cells on the board are accessed by item access, eg board[1, 2] or +landscape[1, 1, 10]. + +A board can be copied, optionally along with its data by means of the +.copy method. Or a section of a board can be linked to the original +board by slicing the original board:: + + b1 = board.Board((9, 9)) + b1[1, 1] = 1 + b2 = b1.copy() + b3 = b1[:3, :3] + +Note that the slice must include all the dimensions of the original +board, but any of those subdimensions can be of length 1:: + + b1 = board.Board((9, 9, 9)) + b2 = b1[:3, :3, :1] + +A sentinel value of Empty indicates a position which is not populated +because it has never had a value, or because its value has been deleted:: + + b1 = board.Board((3, 3)) + assert b1[1, 1] is board.Empty + b1.populate("abcdefghi") + assert b1[1, 1] == "e" + del b1[1, 1] + assert b1[1, 1] is board.Empty + +Iterating over the board yields its coordinates:: + + b1 = board.Board((2, 2)) + for coord in b1: + print(coord) + # + # => (0, 0), (0, 1) etc. + # + +Iteration over a board with one or more infinite dimensions will work +by iterating in chunks:: + + b1 = board.Board((3, 3, board.Infinity)) + for coord in b1: + print(b1) + +To see coordinates with their data items, use iterdata:: + + b1 = board.Board((2, 2)) + b1.populate("abcd") + for coord, data in b1.iterdata(): + print(coord, "=>", data) + +To read, write and empty the data at a board position, use indexing:: + + b1 = board.Board((3, 3)) + b1.populate("abcdef") + print(b1[0, 0]) # "a" + + b1[0, 0] = "*" + print(b1[0, 0]) # "*" + + b1[-1, -1] = "*" + print(b1[2, 2]) # "*" + + del b1[0, 0] + print(b1[0, 0]) # + +To test whether a coordinate is contained with the local coordinate space, use in:: + + b1 = board.Board((3, 3)) + (1, 1) in b1 # True + (4, 4) in b1 # False + (1, 1, 1) in b1 # InvalidDimensionsError + +One board is equal to another if it has the same dimensionality and +each data item is equal:: + + b1 = board.Board((3, 3)) + b1.populate("abcdef") + b2 = b1.copy() + b1 == b2 # True + b2[0, 0] = "*" + b1 == b2 # False + + b2 = board.Board((2, 2)) + b2.populate("abcdef") + b1 == b2 # False + +To populate the board from an arbitrary iterator, use .populate:: + + def random_letters(): + import random, string + while True: + yield random.choice(string.ascii_uppercase) + + b1 = board.Board((4, 4)) + b1.populate(random_letters()) + +To clear the board, use .clear:: + + b1 = board.Board((3, 3)) + b1.populate(range(10)) + b1.clear() + list(b1.iterdata()) # [] + +A board is True if it has any data, False if it has none:: + + b1 = board.Board((2, 2)) + b1.populate("abcd") + bool(b1) # True + b1.clear() + bool(b1) # False + +The length of the board is the product of its dimension lengths. If any +dimension is infinite, the board length is infinite. NB to find the +amount of data on the board, use lendata:: + + b1 = board.Board((4, 4)) + len(b1) # 16 + b1.populate("abcd") + len(b1) # 16 + b1.lendata() # 4 + b2 = board.Board((2, board.Infinity)) + len(b2) # Infinity + +To determine the bounding box of the board which contains data, use .occupied:: + + b1 = board.Board((3, 3)) + b1.populate("abcd") + list(c for (c, d) in b1.iterdata()) # [(0, 0), (0, 1), (0, 2), (1, 0)] + b1.occupied() # ((0, 0), (1, 2)) + +For the common case of slicing a board around its occupied space, +use .occupied_board:: + + b1 = board.Board((3, 3)) + b1.populate("abcd") + b1.draw() + b2 = b1.occupied_board() + b2.draw() + +To test whether a position is on any edge of the board, use .is_edge:: + + b1 = board.Board((3, 3)) + b1.is_edge((0, 0)) # True + b1.is_edge((1, 1)) # False + b1.is_edge((2, 0)) # True + +To find the immediate on-board neighbours to a position along all dimensions:: + + b1 = board.Board((3, 3, 3)) + list(b1.neighbours((0, 0, 0))) + # [(0, 1, 1), (1, 1, 0), ..., (1, 0, 1), (0, 1, 0)] + +To iterate over all the coords in the rectangular space between +two corners, use .itercoords:: + + b1 = board.Board((3, 3)) + list(b1.itercoords((0, 0), (1, 1))) # [(0, 0), (0, 1), (1, 0), (1, 1)] + +To iterate over all the on-board positions from one point in a +particular direction, use .iterline:: + + b1 = board.Board((4, 4)) + start_from = 1, 1 + direction = 1, 1 + list(b1.iterline(start_from, direction)) # [(1, 1), (2, 2), (3, 3)] + direction = 0, 2 + list(b1.iterline(start_from, direction)) # [(1, 1), (1, 3)] + +or .iterlinedata to generate the data at each point:: + + b1 = board.Board((3, 3)) + b1.populate("ABCDEFGHJ") + start_from = 1, 1 + direction = 1, 0 + list(b1.iterlinedata(start_from, direction)) # ['A', 'D', 'G'] + +Both iterline and iterdata can take a maximum number of steps, eg for +games like Connect 4 or Battleships:: + + b1 = board.Board((8, 8)) + # + # Draw a Battleship + # + b1.populate("BBBB", b1.iterline((2, 2), (1, 0))) + +As a convenience for games which need to look for a run of so many +things, the .run_of_n method combines iterline with data to yield +every possible line on the board which is of a certain length along +with its data:: + + b1 = board.Board((3, 3)) + b1[0, 0] = 'X' + b1[1, 1] = 'O' + b1[0, 1] = 'X' + for line, data in b1.runs_of_n(3): + if all(d == "O" for d in data): + print("O wins") + break + elif all(d == "X" for d in data): + print("X wins") + break + +To iterate over the corners of the board, use .corners:: + + b1 = board.Board((3, 3)) + corners() # [(0, 0), (0, 2), (2, 0), (2, 2)] + +Properties +---------- + +To determine whether a board is offset from another (ie the result of a slice):: + + b1 = board.Board((3, 3)) + b1.is_offset # False + b2 = b1[:1, :1] + b2.is_offset # True + +To determine whether a board has any infinite or finite dimensions:: + + b1 = board.Board((3, board.Infinity)) + b1.has_finite_dimensions # True + b1.has_infinite_dimensions # True + b2 = board.Board((3, 3)) + b1.has_infinite_dimensions # False + b3 = board.Board((board.Infinity, board.Infinity)) + b3.has_finite_dimensions # False + +Display the Board +----------------- + +To get a crude view of the contents of the board, use .dump:: + + b1 = board.Board((3, 3)) + b1.populate("abcdef") + b1.dump() + +To get a grid view of a 2-dimensional board, use .draw:: + + b1 = board.Board((3, 3)) + b1.populate("OX XXOO ") + b1.draw() + +If you don't want the borders drawn, eg because you're using the board +to render ASCII art, pass use_borders=False:: + + b1 = board.Board((8, 8)) + for coord in b1.iterline((0, 0), (1, 1)): + b1[coord] = "*" + for coord in b1.iterline((7, 0), (-1, 1)): + b1[coord] = "*" + b1.draw(use_borders=False) + +To render to an image using Pillow (which isn't a hard dependency) use paint. +The default renderer treats the data items as text and renders then, scaled +to fit, into each cell. This works, obviously, for things like Noughts & Crosses +assuming that you store something like "O" and "X". But it also works for +word searches and even simple battleships where the data items are objects +whose __str__ returns blank (for undiscovered), "+" for a single hit, and "*" +for a destroyed vessel:: + + b1 = board.Board((3, 3)) + b1[0, 0] = "X" + b1[1, 1] = "O" + b1[0, 2] = "X" + b1.paint("board.png") + # ... and now look at board.png + +The text painting is achieved internally by means of a callback called +text_sprite. An alternative ready-cooked callback for paint() is +imagefile_sprite. This looks for a .png file in the current directory +(or another; you can specify). + +Local and Global coordinates +---------------------------- + +Since one board can represent a slice of another, there are two levels +of coordinates: local and global. Coordinates passed to or returned from +any of the public API methods are always local for that board. They +represent the natural coordinate space for the board. Internally, the +module will use global coordinates, translating as necessary. + +Say you're managing a viewport of a tile-based dungeon game where the +master dungeon board is 100 x 100 but the visible board is 10 x 10. +Your viewport board is currently representing the slice of the master +board from (5, 5) to (14, 14). Changing the item at position (2, 2) on +the viewport board will change the item at position (7, 7) on the master +board (and vice versa). + +As a user of the API you don't need to know this, except to understand +that a board slice is essentially a view on its parent. If you wish +to subclass or otherwise extend the board, you'll need to note where +coordinate translations are necessary. + + + + + diff --git a/scripts/tempSensor/lib/board-1.0.dist-info/RECORD b/scripts/tempSensor/lib/board-1.0.dist-info/RECORD new file mode 100644 index 00000000..ab0fcb94 --- /dev/null +++ b/scripts/tempSensor/lib/board-1.0.dist-info/RECORD @@ -0,0 +1,3 @@ +board-1.0.dist-info/METADATA,, +board.py,, +board-1.0.dist-info/RECORD,, \ No newline at end of file diff --git a/scripts/tempSensor/lib/board.py b/scripts/tempSensor/lib/board.py new file mode 100644 index 00000000..84597581 --- /dev/null +++ b/scripts/tempSensor/lib/board.py @@ -0,0 +1,775 @@ +# -*- coding: utf-8-*- # Encoding cookie added by Mu Editor + +"""Board -- an n-dimensional board with support for iteration, containership and slicing + +Boards can have any number of dimensions, any of which can be infinite. Boards +can be sliced [:1, :2], returning a linked-copy, or copied (.copy), returning a +snapshot copy. + +Boards can be iterated over for coordinates or data (.iterdata). There are also +convenience functions to determine neighbours across all dimensions (.neighbours), +the bounding box of occupied data (.occupied), all the coordinates in a space +in n-dimensions (.itercoords) and others. +""" + +# testing +# +# The semantics of 3.x range are broadly equivalent +# to xrange in 2.7 +# +try: + range = xrange +except NameError: + pass +try: + long +except NameError: + long = int + +import os, sys +import functools +import itertools +import io + +try: + from PIL import Image, ImageDraw, ImageFont +except ImportError: + Image = None + +class _Infinity(int): + + def __new__(meta): + return sys.maxsize + + def __str__(self): + return "Infinity" + + def __repr__(self): + return "" + + def __eq__(self, other): + return other == self.size + + def __lt__(self, other): + return False + + def __gt__(self, other): + return True + +Infinity = _Infinity() + +class _Empty(object): + + def __repr__(self): + return "" + + def __bool__(self): + return False + __nonzero__ = __bool__ + +Empty = _Empty() + +class BaseDimension(object): + + def __repr__(self): + return "<{}>".format(self.__class__.__name__) + +class Dimension(BaseDimension): + + is_finite = True + is_infinite = False + + def __init__(self, size): + self._size = size + self._range = range(size) + + def __iter__(self): + return iter(self._range) + + def __eq__(self, other): + return isinstance(self, type(other)) and self._size == other._size + + def __repr__(self): + return "<{}: {}>".format(self.__class__.__name__, self._size) + + def __len__(self): + return self._size + + def __contains__(self, item): + return item in self._range + + def __getitem__(self, item): + if isinstance(item, (int, long)): + return self._range[item] + elif isinstance(item, slice): + return self._range[item.start, item.stop, item.step] + else: + raise TypeError("{} can only be indexed by int or slice".format(self.__class__.__name__)) + +class _InfiniteDimension(BaseDimension): + + chunk_size = 10 + is_finite = False + is_infinite = True + + def __iter__(self): + return itertools.count() + + def __repr__(self): + return "" + + def __eq__(self, other): + # + # Ensure that any infinite dimension is equal to any other + # + return isinstance(other, self.__class__) + + def __contains__(self, item): + # + # An infinite dimension includes any non-negative coordinate + # + if item < 0: + return False + return True + + def __len__(self): + return Infinity + + def __getitem__(self, item): + if isinstance(item, (int, long)): + if item == 0: + return 0 + elif item == -1: + return Infinity + else: + raise IndexError("Infinite dimensions can only return first & last items") + + elif isinstance(item, slice): + # + # If the request is for an open-ended slice, + # just return the same infinite dimension. + # + if item.stop is None: + return self + else: + return range(*item.indices(item.stop)) + + else: + raise TypeError("{} can only be indexed by int or slice".format(self.__class__.__name__)) + +InfiniteDimension = _InfiniteDimension() + +def _centred_coord(outer_size, inner_size): + """Given an outer and an inner size, calculate the top-left coordinates + which the inner image should position at to be centred within the outer + image + """ + outer_w, outer_h = outer_size + inner_w, inner_h = inner_size + return round((outer_w - inner_w) / 2), round((outer_h - inner_h) / 2) + + +def text_sprite(font_name="arial", colour="#0000ff"): + """Text sprite generator callback from Board.paint + + Convert the object to text of approximately the right size for + the cell being painted. Typically this will be used for one or + two letter objects, but it will work for any object which can + meaningfully be converted to text + """ + + def _text_sprite(obj, size): + # + # Very roughly, one point is three quarters of + # a pixel. We pick a point size which will fill + # the smaller edge of the cell (if it's not square) + # + point_size = round(min(size) * 0.75) + + # + # Create a new transparent image to hold the + # text. Draw the text into it in blue, centred, + # using the font requested, and return the resulting image + # + image = Image.new("RGBA", size, (255, 255, 255, 0)) + draw = ImageDraw.Draw(image) + font = ImageFont.truetype("%s.ttf" % font_name, point_size) + text = str(obj) + draw.text(_centred_coord(size, font.getsize(text)), text, font=font, fill=colour) + return image + + return _text_sprite + +def imagefile_sprite(directory=".", extension=".png"): + """Image sprite generator callback for Board.paint + + Given the text form of an object, look for an image file in the + stated directory [default: current] and return it, scaled to size. + """ + + def _imagefile_sprite(obj, size): + image = Image.open(os.path.join(directory, "%s%s" % (obj, extension))) + image.thumbnail(size) + return image + + return _imagefile_sprite + +class Board(object): + """Board - represent a board of n dimensions, each possibly infinite. + + A location on the board is represented as an n-dimensional + coordinate, matching the dimensionality originally specified. + + The board is addressed by index with a coordinate: + + b = Board((4, 4)) + b[2, 2] = "*" + b.draw() + """ + + class BoardError(Exception): pass + class InvalidDimensionsError(BoardError): pass + class OutOfBoundsError(BoardError): pass + + def __init__(self, dimension_sizes, _global_board=None, _offset_from_global=None): + """Set up a n-dimensional board + """ + if not dimension_sizes: + raise self.InvalidDimensionsError("The board must have at least one dimension") + try: + iter(dimension_sizes) + except TypeError: + raise self.InvalidDimensionsError("Dimensions must be iterable (eg a tuple), not {}".format(type(dimension_sizes).__name__)) + if any(d <= 0 for d in dimension_sizes): + raise self.InvalidDimensionsError("Each dimension must be >= 1") + self.dimensions = [InfiniteDimension if size == Infinity else Dimension(size) for size in dimension_sizes] + + # + # This can be a sub-board of another board: a slice. + # If that's the case, the boards share a common data structure + # and this one is offset from the other. + # NB this means that if a slice is taken of a slice, the offset must itself be offset! + # + self._data = {} if _global_board is None else _global_board + self._offset_from_global = _offset_from_global or tuple(0 for _ in self.dimensions) + self._sprite_cache = {} + + def __repr__(self): + return "<{} ({})>".format( + self.__class__.__name__, + ", ".join(("Infinity" if d.is_infinite else str(len(d))) for d in self.dimensions) + ) + + def __eq__(self, other): + return \ + self.dimensions == other.dimensions and \ + dict(self.iterdata()) == dict(other.iterdata()) + + def __len__(self): + # + # Return the total number of positions on the board. If any of + # the dimensions is infinite, the total will be Infinity + # + if any(d.is_infinite for d in self.dimensions): + return Infinity + else: + return functools.reduce(lambda a, b: a * b, (len(d) for d in self.dimensions)) + + def __bool__(self): + return any(coord for coord in self._data if self._is_in_bounds(coord)) + __nonzero__ = __bool__ + + @property + def is_offset(self): + """Is this board offset from a different board?""" + return any(o for o in self._offset_from_global) + + @property + def has_finite_dimensions(self): + """Does this board have at least one finite dimension?""" + return any(d.is_finite for d in self.dimensions) + + @property + def has_infinite_dimensions(self): + """Does this board have at least one infinite dimension?""" + return any(d.is_infinite for d in self.dimensions) + + def dumped(self): + is_offset = any(o for o in self._offset_from_global) + if is_offset: + offset = " offset by {}".format(self._offset_from_global) + else: + offset = "" + yield repr(self) + offset + yield "{" + for coord, value in sorted(self.iterdata()): + if is_offset: + global_coord = " => {}".format(self._to_global(coord)) + else: + global_coord = "" + data = " [{}]".format(self[coord] if self[coord] is not None else "") + yield " {}{}{}".format(coord, global_coord, data) + yield "}" + + def dump(self, outf=sys.stdout): + for line in self.dumped(): + outf.write(line + "\n") + + def _is_in_bounds(self, coord): + """Is a given coordinate within the space of this board? + """ + if len(coord) != len(self.dimensions): + raise self.InvalidDimensionsError( + "Coordinate {} has {} dimensions; the board has {}".format(coord, len(coord), len(self.dimensions))) + + return all(c in d for (c, d) in zip(coord, self.dimensions)) + + def _check_in_bounds(self, coord): + """If a given coordinate is not within the space of this baord, raise + an OutOfBoundsError + """ + if not self._is_in_bounds(coord): + raise self.OutOfBoundsError("{} is out of bounds for {}".format(coord, self)) + + def __contains__(self, coord): + """Implement in + """ + return self._is_in_bounds(coord) + + def __iter__(self): + """Implement for in + + Iterate over all combinations of coordinates. If you need data, + use iterdata(). + """ + # If all the dimensions are finite (the simplest and most common + # situation) just use itertools.product. + + # If any dimension is infinite, we can't use itertools.product + # directly because it consumes its arguments in order to make + # up the axes for its Cartesian join. Instead, we chunk through + # any infinite dimensions, while repeating the finite ones. + if any(d.is_infinite for d in self.dimensions): + start, chunk = 0, InfiniteDimension.chunk_size + while True: + iterators = [d[start:start+chunk] if d[-1] == Infinity else iter(d) for d in self.dimensions] + for coord in itertools.product(*iterators): + yield coord + start += chunk + else: + for coord in itertools.product(*self.dimensions): + yield coord + + def _to_global(self, coord): + return tuple(c + o for (c, o) in zip(coord, self._offset_from_global)) + + def _from_global(self, coord): + return tuple(c - o for (c, o) in zip(coord, self._offset_from_global)) + + def iterdata(self): + """Implement: for (, ) in + + Generate the list of data in local coordinate terms. + """ + for gcoord, value in self._data.items(): + lcoord = self._from_global(gcoord) + if self._is_in_bounds(lcoord): + yield lcoord, value + + def lendata(self): + """Return the number of data items populated + """ + return sum(1 for _ in self.iterdata()) + + def iterline(self, coord, vector, max_steps=None): + """Generate coordinates starting at the given one and moving + in the direction of the vector until the edge of the board is + reached. The initial coordinate must be on the board. The vector + must have the same dimensionality as the coordinate. + + NB the vector can specify a "step", eg it could be (1, 2) + """ + self._check_in_bounds(coord) + if len(vector) != len(coord): + raise InvalidDimensionsError() + + n_steps = 0 + while self._is_in_bounds(coord): + yield coord + n_steps += 1 + if max_steps is not None and n_steps == max_steps: + break + coord = tuple(c + v for (c, v) in zip(coord, vector)) + + def iterlinedata(self, coord, vector, max_steps=None): + """Use .iterline to generate the data starting at the given + coordinate and moving in the direction of the vector until + the edge of the board is reached or the maximum number of + steps has been taken (if specified). + + This could be used, eg, to see whether you have a battleship + or a word in a word-search + """ + for coord in self.iterline(coord, vector, max_steps): + yield self[coord] + + def corners(self): + dimension_bounds = [(0, len(d) -1 if d.is_finite else Infinity) for d in self.dimensions] + return list(itertools.product(*dimension_bounds)) + + def copy(self, with_data=True): + """Return a new board with the same dimensionality as the present one. + If with_data is truthy, populate with the current data. + + NB this creates a copy, not a reference. For linked copy of the board, + use __getitem__, eg b2 = b1[:, :, :] + """ + board = self.__class__(tuple(len(d) for d in self.dimensions)) + if with_data: + for coord, value in self.iterdata(): + board._data[coord] = value + return board + + def clear(self): + """Clear the data which belongs to this board, possibly a sub-board + of a larger board. + """ + for lcoord, value in list(self.iterdata()): + del self._data[self._to_global(lcoord)] + + def __getitem__(self, item): + """The item is either a tuple of numbers, representing a single + coordinate on the board, or a tuple of slices representing a copy + of some or all of the board. + """ + if all(isinstance(i, (int, long)) for i in item): + coord = self._normalised_coord(item) + return self._data.get(coord, Empty) + elif all(isinstance(i, (int, long, slice)) for i in item): + return self._slice(item) + else: + raise TypeError("{} can only be indexed by int or slice".format(self.__class__.__name__)) + + def __setitem__(self, coord, value): + if all(isinstance(c, (int, long)) for c in coord): + coord = self._normalised_coord(coord) + self._data[coord] = value + #~ elif all(isinstance(i, (int, long, slice)) for i in item): + #~ return self._slice(item) + else: + raise TypeError("{} can only be indexed by int or slice".format(self.__class__.__name__)) + + def __delitem__(self, coord): + coord = self._normalised_coord(coord) + try: + del self._data[coord] + except KeyError: + pass + + def _normalised_coord(self, coord): + """Given a coordinate, check whether it's the right dimensionality + for this board and whether it's within bounds. Return the underlying + global coordinate. + + If a negative number is given, apply the usual subscript maths + to come up with an index from the end of the dimension. + """ + if len(coord) != len(self.dimensions): + raise IndexError("Coordinate {} has {} dimensions; the board has {}".format(coord, len(coord), len(self.dimensions))) + + # + # Account for negative indices in the usual way, allowing + # for the fact that you can't use negative indices if the + # dimension is infinite + # + if any(d is InfiniteDimension and c < 0 for (c, d) in zip(coord, self.dimensions)): + raise IndexError("Cannot use negative index {} on an infinite dimension".format(c)) + normalised_coord = tuple(len(d) + c if c < 0 else c for (c, d) in zip(coord, self.dimensions)) + self._check_in_bounds(normalised_coord) + return self._to_global(normalised_coord) + + def _slice(self, slices): + """Produce a subset of this board linked to the same underlying data. + """ + if len(slices) != len(self.dimensions): + raise IndexError("Slices {} have {} dimensions; the board has {}".format(slices, len(slices), len(self.dimensions))) + + # + # Determine the start/stop/step for all the slices + # + slice_indices = [slice.indices(len(dimension)) for (slice, dimension) in zip(slices, self.dimensions)] + if any(abs(step) != 1 for start, stop, step in slice_indices): + raise IndexError("At least one of slices {} has a stride other than 1".format(slices)) + + # + # Create the new dimensions: infinite dimensions remain infinite if + # they're sliced open-ended, eg [1:]. Otherwise they become finite + # dimensions of the appropriate lengthm eg [1:3] gives a finite dimension + # of length 2 + # + # FIXME: perhaps use the Dimension class' built-in slicers + # + sizes = tuple( + Infinity if (d is InfiniteDimension and s.stop is None) + else (stop - start) + for s, (start, stop, step), d in zip(slices, slice_indices, self.dimensions) + ) + + # + # Need to take into account the offset of this board, which might + # itself be offset from the parent board. + # + offset = tuple(o + start for (o, (start, stop, step)) in zip(self._offset_from_global, slice_indices)) + return self.__class__(sizes, self._data, offset) + + def _occupied_dimension(self, n_dimension): + """Return the min/max along a particular dimension. + (Intended for internal use, eg when displaying an infinite dimension) + """ + data_in_use = [coord for coord in self._data if coord in self] + if not data_in_use: + return (None, None) + else: + return ( + min(c[n_dimension] for c in data_in_use), + max(c[n_dimension] for c in data_in_use) + ) + + def occupied(self): + """Return the bounding box of space occupied + """ + coords_in_use = [coord for coord, _ in self.iterdata()] + min_coord = tuple(min(coord) for coord in zip(*coords_in_use)) + max_coord = tuple(max(coord) for coord in zip(*coords_in_use)) + return min_coord, max_coord + + def occupied_board(self): + """Return a sub-board containing only the portion of this board + which contains data. + """ + (x0, y0), (x1, y1) = self.occupied() + return self[x0:x1+1, y0:y1+1] + + def itercoords(self, coord1, coord2): + """Iterate over the coordinates in between the two coordinates. + + The result is all the coordinates in the rectangular section bounded + by coord1 and coord2 + """ + for coord in (coord1, coord2): + self._check_in_bounds(coord) + + for coord in itertools.product(*(range(i1, 1 + i2) for (i1, i2) in zip(*sorted([coord1, coord2])))): + yield coord + + def neighbours(self, coord, include_diagonals=True): + """Iterate over all the neighbours of a coordinate + + For a given coordinate, yield each of its nearest neighbours along + all dimensions, including diagonal neighbours if requested (the default) + """ + offsets = itertools.product(*[(-1, 0, 1) for d in self.dimensions]) + for offset in offsets: + if all(o == 0 for o in offsets): + continue + # + # Diagonal offsets have no zero component + # + if include_diagonals or any(o == 0 for o in offset): + neighbour = tuple(c + o for (c, o) in zip(coord, offset)) + if self._is_in_bounds(neighbour): + yield neighbour + + def runs_of_n(self, n, ignore_reversals=True): + """Iterate over all dimensions to yield runs of length n + + Yield each run of n cells as a tuple of coordinates and a tuple + of data. If ignore_reversals is True (the default) then don't + yield the same line in the opposite direction. + + This is useful for, eg, noughts and crosses, battleship or connect 4 + where the game engine has to detect a line of somethings in a row. + """ + all_zeroes = tuple(0 for _ in self.dimensions) + all_offsets = itertools.product(*[(-1, 0, 1) for d in self.dimensions]) + offsets = [o for o in all_offsets if o != all_zeroes] + + already_seen = set() + # + # This is brute force: running for every cell and looking in every + # direction. We check later whether we've run off the board (as + # the resulting line will fall short). We might do some kind of + # pre-check here, but we have to check against every direction + # of every dimension, which would complicate this code + # + for cell in iter(self): + for direction in offsets: + line = tuple(self.iterline(cell, direction, n)) + if len(line) == n: + if line in already_seen: + continue + already_seen.add(line) + # + # Most of the time you don't want the same line twice, + # once in each direction. + # + if ignore_reversals: + already_seen.add(line[::-1]) + + yield line, [self[c] for c in line] + + def is_edge(self, coord): + """Determine whether a position is on any edge of the board. + + Infinite dimensions only have a lower edge (zero); finite dimensions + have a lower and an upper edge. + """ + self._check_in_bounds(coord) + dimension_bounds = ((0, len(d) - 1 if d.is_finite else 0) for d in self.dimensions) + return any(c in bounds for (c, bounds) in zip(coord, dimension_bounds)) + + def is_corner(self, coord): + """Determine whether a position is on any corner of the board + + Infinite dimensions only have a lower edge (zero); finite dimensions + have a lower and an upper edge. + """ + self._check_in_bounds(coord) + dimension_bounds = ((0, len(d) - 1 if d.is_finite else 0) for d in self.dimensions) + return all(c in bounds for (c, bounds) in zip(coord, dimension_bounds)) + + def populate(self, iterable, coord_iterable=None): + """Populate all or part of the board from an iterable + + The population iterable can be shorter or longer than the board + iterable. The two are zipped together so the population will stop + when the shorter is exhausted. + + If no iterable is supplied for cooordinates, the whole board is + populated. + + This is a convenience method both to assist testing and also for, + eg, games like Boggle or word-searches where the board must start + filled with letters etc. If the data needs to be, eg, a random or + weighted choice then this should be implemented in the iterator + supplied. + + With a coordinate iterable this could be used, for example, to combine + iterline and a list of objects to populate data on a Battleships board. + """ + if coord_iterable is None: + board_iter = iter(self) + else: + board_iter = iter(coord_iterable) + for coord, value in zip(board_iter, iter(iterable)): + self[coord] = value + + def draw(self, callback=str, use_borders=True): + """Draw the board in a very simple text layout + + By default data items are rendered as strings. If a different callback + is supplied, it is called with the data item and should return a string. + + The idea is that items can be "hidden" from the board, or rendered + differently according to some state. Think of Battleships where the + same object can be hidden, revealed, or sunk. + """ + for line in self.drawn(callback, use_borders): + print(line) + + def drawn(self, callback=str, use_borders=True): + if len(self.dimensions) != 2 or self.has_infinite_dimensions: + raise self.BoardError("Can only draw a finite 2-dimensional board") + + data = dict((coord, callback(v)) for (coord, v) in self.iterdata()) + if data: + cell_w = len(max((v for v in data.values()), key=len)) + else: + cell_w = 1 + if use_borders: + corner, hedge, vedge = "+", "-", "|" + else: + corner = hedge = vedge = "" + divider = (corner + (hedge * cell_w)) * len(self.dimensions[0]) + corner + + if use_borders: yield divider + for y in self.dimensions[1]: + yield vedge + vedge.join(data.get((x, y), "").center(cell_w) for x in self.dimensions[0]) + vedge + if use_borders: yield divider + + def painted(self, callback, size, background_colour, use_borders): + if not Image: + raise NotImplementedError("Painting is not available unless Pillow is installed") + if len(self.dimensions) != 2 or self.has_infinite_dimensions: + raise self.BoardError("Can only paint a finite 2-dimensional board") + + # + # Construct a board of the requested size, containing + # cells sized equally to fit within the size for each + # of the two dimensions. Keep the border between them + # proportional to the overall image size + # + n_wide = len(self.dimensions[0]) + n_high = len(self.dimensions[1]) + image = Image.new("RGBA", size) + if use_borders: + h_border = image.height / 80 + v_border = image.width / 80 + else: + h_border = v_border = 0 + draw = ImageDraw.Draw(image) + drawable_w = image.width - (1 + n_wide) * h_border + cell_w = round(drawable_w / n_wide) + drawable_h = image.height - (1 + n_high) * v_border + cell_h = round(drawable_h / n_high) + + for (x, y) in self: + obj = self[x, y] + # + # If the cell is empty: draw nothing + # Try to fetch the relevant sprite from the cache + # If the sprite is not cached, generate and cache it + # If the sprite is larger than the cell, crop it to the correct + # size, maintaining its centre + # + if obj is Empty: + sprite = None + else: + try: + sprite = self._sprite_cache[obj] + except KeyError: + sprite = self._sprite_cache[obj] = callback(obj, (cell_w, cell_h)) + if sprite.width > cell_w or sprite.height > cell_h: + box_x = (sprite.width - cell_w) / 2 + box_y = (sprite.height - cell_h) / 2 + sprite = sprite.crop((box_x, box_y, cell_w, cell_h)) + + # + # Draw the cell and any sprite within it + # + cell_x = round(h_border + ((cell_w + h_border) * x)) + cell_y = round(v_border + ((cell_h + v_border) * y)) + draw.rectangle((cell_x, cell_y, cell_x + cell_w, cell_y + cell_h), fill=background_colour) + if sprite: + x_offset, y_offset = _centred_coord((cell_w, cell_h), sprite.size) + image.alpha_composite(sprite, (cell_x + x_offset, cell_y + y_offset)) + + # + # Return the whole image as PNG-encoded bytes + # + f = io.BytesIO() + image.save(f, "PNG") + return f.getvalue() + + def paint(self, filepath, callback=text_sprite(), size=(800, 800), background_colour="#ffffcc", use_borders=True): + with open(filepath, "wb") as f: + f.write(self.painted(callback, size, background_colour, use_borders)) + +def cornerposts(dimensions): + for d in dimensions: + yield 0 + if d.is_finite: + yield len(d) + +if __name__ == '__main__': + pass \ No newline at end of file diff --git a/scripts/tempSensor/lib/ez_setup.py b/scripts/tempSensor/lib/ez_setup.py new file mode 100644 index 00000000..1bcd3e94 --- /dev/null +++ b/scripts/tempSensor/lib/ez_setup.py @@ -0,0 +1,332 @@ +#!/usr/bin/env python +"""Bootstrap setuptools installation + +To use setuptools in your package's setup.py, include this +file in the same directory and add this to the top of your setup.py:: + + from ez_setup import use_setuptools + use_setuptools() + +To require a specific version of setuptools, set a download +mirror, or use an alternate download directory, simply supply +the appropriate options to ``use_setuptools()``. + +This file can also be run as a script to install or upgrade setuptools. +""" +import os +import shutil +import sys +import tempfile +import zipfile +import optparse +import subprocess +import platform +import textwrap +import contextlib + +from distutils import log + +try: + from urllib.request import urlopen +except ImportError: + from urllib2 import urlopen + +try: + from site import USER_SITE +except ImportError: + USER_SITE = None + +DEFAULT_VERSION = "4.0.1" +DEFAULT_URL = "https://pypi.python.org/packages/source/s/setuptools/" + +def _python_cmd(*args): + """ + Return True if the command succeeded. + """ + args = (sys.executable,) + args + return subprocess.call(args) == 0 + + +def _install(archive_filename, install_args=()): + with archive_context(archive_filename): + # installing + log.warn('Installing Setuptools') + if not _python_cmd('setup.py', 'install', *install_args): + log.warn('Something went wrong during the installation.') + log.warn('See the error message above.') + # exitcode will be 2 + return 2 + + +def _build_egg(egg, archive_filename, to_dir): + with archive_context(archive_filename): + # building an egg + log.warn('Building a Setuptools egg in %s', to_dir) + _python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir) + # returning the result + log.warn(egg) + if not os.path.exists(egg): + raise IOError('Could not build the egg.') + + +class ContextualZipFile(zipfile.ZipFile): + """ + Supplement ZipFile class to support context manager for Python 2.6 + """ + + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + self.close() + + def __new__(cls, *args, **kwargs): + """ + Construct a ZipFile or ContextualZipFile as appropriate + """ + if hasattr(zipfile.ZipFile, '__exit__'): + return zipfile.ZipFile(*args, **kwargs) + return super(ContextualZipFile, cls).__new__(cls) + + +@contextlib.contextmanager +def archive_context(filename): + # extracting the archive + tmpdir = tempfile.mkdtemp() + log.warn('Extracting in %s', tmpdir) + old_wd = os.getcwd() + try: + os.chdir(tmpdir) + with ContextualZipFile(filename) as archive: + archive.extractall() + + # going in the directory + subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) + os.chdir(subdir) + log.warn('Now working in %s', subdir) + yield + + finally: + os.chdir(old_wd) + shutil.rmtree(tmpdir) + + +def _do_download(version, download_base, to_dir, download_delay): + egg = os.path.join(to_dir, 'setuptools-%s-py%d.%d.egg' + % (version, sys.version_info[0], sys.version_info[1])) + if not os.path.exists(egg): + archive = download_setuptools(version, download_base, + to_dir, download_delay) + _build_egg(egg, archive, to_dir) + sys.path.insert(0, egg) + + # Remove previously-imported pkg_resources if present (see + # https://bitbucket.org/pypa/setuptools/pull-request/7/ for details). + if 'pkg_resources' in sys.modules: + del sys.modules['pkg_resources'] + + import setuptools + setuptools.bootstrap_install_from = egg + + +def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, + to_dir=os.curdir, download_delay=15): + to_dir = os.path.abspath(to_dir) + rep_modules = 'pkg_resources', 'setuptools' + imported = set(sys.modules).intersection(rep_modules) + try: + import pkg_resources + except ImportError: + return _do_download(version, download_base, to_dir, download_delay) + try: + pkg_resources.require("setuptools>=" + version) + return + except pkg_resources.DistributionNotFound: + return _do_download(version, download_base, to_dir, download_delay) + except pkg_resources.VersionConflict as VC_err: + if imported: + msg = textwrap.dedent(""" + The required version of setuptools (>={version}) is not available, + and can't be installed while this script is running. Please + install a more recent version first, using + 'easy_install -U setuptools'. + + (Currently using {VC_err.args[0]!r}) + """).format(VC_err=VC_err, version=version) + sys.stderr.write(msg) + sys.exit(2) + + # otherwise, reload ok + del pkg_resources, sys.modules['pkg_resources'] + return _do_download(version, download_base, to_dir, download_delay) + +def _clean_check(cmd, target): + """ + Run the command to download target. If the command fails, clean up before + re-raising the error. + """ + try: + subprocess.check_call(cmd) + except subprocess.CalledProcessError: + if os.access(target, os.F_OK): + os.unlink(target) + raise + +def download_file_powershell(url, target): + """ + Download the file at url to target using Powershell (which will validate + trust). Raise an exception if the command cannot complete. + """ + target = os.path.abspath(target) + ps_cmd = ( + "[System.Net.WebRequest]::DefaultWebProxy.Credentials = " + "[System.Net.CredentialCache]::DefaultCredentials; " + "(new-object System.Net.WebClient).DownloadFile(%(url)r, %(target)r)" + % vars() + ) + cmd = [ + 'powershell', + '-Command', + ps_cmd, + ] + _clean_check(cmd, target) + +def has_powershell(): + if platform.system() != 'Windows': + return False + cmd = ['powershell', '-Command', 'echo test'] + with open(os.path.devnull, 'wb') as devnull: + try: + subprocess.check_call(cmd, stdout=devnull, stderr=devnull) + except Exception: + return False + return True + +download_file_powershell.viable = has_powershell + +def download_file_curl(url, target): + cmd = ['curl', url, '--silent', '--output', target] + _clean_check(cmd, target) + +def has_curl(): + cmd = ['curl', '--version'] + with open(os.path.devnull, 'wb') as devnull: + try: + subprocess.check_call(cmd, stdout=devnull, stderr=devnull) + except Exception: + return False + return True + +download_file_curl.viable = has_curl + +def download_file_wget(url, target): + cmd = ['wget', url, '--quiet', '--output-document', target] + _clean_check(cmd, target) + +def has_wget(): + cmd = ['wget', '--version'] + with open(os.path.devnull, 'wb') as devnull: + try: + subprocess.check_call(cmd, stdout=devnull, stderr=devnull) + except Exception: + return False + return True + +download_file_wget.viable = has_wget + +def download_file_insecure(url, target): + """ + Use Python to download the file, even though it cannot authenticate the + connection. + """ + src = urlopen(url) + try: + # Read all the data in one block. + data = src.read() + finally: + src.close() + + # Write all the data in one block to avoid creating a partial file. + with open(target, "wb") as dst: + dst.write(data) + +download_file_insecure.viable = lambda: True + +def get_best_downloader(): + downloaders = ( + download_file_powershell, + download_file_curl, + download_file_wget, + download_file_insecure, + ) + viable_downloaders = (dl for dl in downloaders if dl.viable()) + return next(viable_downloaders, None) + +def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, + to_dir=os.curdir, delay=15, downloader_factory=get_best_downloader): + """ + Download setuptools from a specified location and return its filename + + `version` should be a valid setuptools version number that is available + as an egg for download under the `download_base` URL (which should end + with a '/'). `to_dir` is the directory where the egg will be downloaded. + `delay` is the number of seconds to pause before an actual download + attempt. + + ``downloader_factory`` should be a function taking no arguments and + returning a function for downloading a URL to a target. + """ + # making sure we use the absolute path + to_dir = os.path.abspath(to_dir) + zip_name = "setuptools-%s.zip" % version + url = download_base + zip_name + saveto = os.path.join(to_dir, zip_name) + if not os.path.exists(saveto): # Avoid repeated downloads + log.warn("Downloading %s", url) + downloader = downloader_factory() + downloader(url, saveto) + return os.path.realpath(saveto) + +def _build_install_args(options): + """ + Build the arguments to 'python setup.py install' on the setuptools package + """ + return ['--user'] if options.user_install else [] + +def _parse_args(): + """ + Parse the command line for options + """ + parser = optparse.OptionParser() + parser.add_option( + '--user', dest='user_install', action='store_true', default=False, + help='install in user site package (requires Python 2.6 or later)') + parser.add_option( + '--download-base', dest='download_base', metavar="URL", + default=DEFAULT_URL, + help='alternative URL from where to download the setuptools package') + parser.add_option( + '--insecure', dest='downloader_factory', action='store_const', + const=lambda: download_file_insecure, default=get_best_downloader, + help='Use internal, non-validating downloader' + ) + parser.add_option( + '--version', help="Specify which version to download", + default=DEFAULT_VERSION, + ) + options, args = parser.parse_args() + # positional arguments are ignored + return options + +def main(): + """Install or upgrade setuptools and EasyInstall""" + options = _parse_args() + archive = download_setuptools( + version=options.version, + download_base=options.download_base, + downloader_factory=options.downloader_factory, + ) + return _install(archive, _build_install_args(options)) + +if __name__ == '__main__': + sys.exit(main()) diff --git a/scripts/tempSensor/lib/functools-0.0.7.dist-info/METADATA b/scripts/tempSensor/lib/functools-0.0.7.dist-info/METADATA new file mode 100644 index 00000000..1cb319be --- /dev/null +++ b/scripts/tempSensor/lib/functools-0.0.7.dist-info/METADATA @@ -0,0 +1,6 @@ +Metadata-Version: 2.1 +Name: functools +Version: 0.0.7 +Summary: +Author: +License: MIT diff --git a/scripts/tempSensor/lib/functools-0.0.7.dist-info/RECORD b/scripts/tempSensor/lib/functools-0.0.7.dist-info/RECORD new file mode 100644 index 00000000..d07aa045 --- /dev/null +++ b/scripts/tempSensor/lib/functools-0.0.7.dist-info/RECORD @@ -0,0 +1,3 @@ +functools-0.0.7.dist-info/METADATA,, +functools.py,, +functools-0.0.7.dist-info/RECORD,, \ No newline at end of file diff --git a/scripts/tempSensor/lib/functools.py b/scripts/tempSensor/lib/functools.py new file mode 100644 index 00000000..faa47a8b --- /dev/null +++ b/scripts/tempSensor/lib/functools.py @@ -0,0 +1,31 @@ +def partial(func, *args, **kwargs): + def _partial(*more_args, **more_kwargs): + kw = kwargs.copy() + kw.update(more_kwargs) + return func(*(args + more_args), **kw) + + return _partial + + +def update_wrapper(wrapper, wrapped, assigned=None, updated=None): + # Dummy impl + return wrapper + + +def wraps(wrapped, assigned=None, updated=None): + # Dummy impl + return lambda x: x + + +def reduce(function, iterable, initializer=None): + it = iter(iterable) + if initializer is None: + value = next(it) + else: + value = initializer + for element in it: + value = function(value, element) + return value + + +__version__ = '0.0.7' diff --git a/scripts/tempSensor/lib/itertools-0.2.3.dist-info/METADATA b/scripts/tempSensor/lib/itertools-0.2.3.dist-info/METADATA new file mode 100644 index 00000000..c5d05217 --- /dev/null +++ b/scripts/tempSensor/lib/itertools-0.2.3.dist-info/METADATA @@ -0,0 +1,6 @@ +Metadata-Version: 2.1 +Name: itertools +Version: 0.2.3 +Summary: +Author: +License: MIT diff --git a/scripts/tempSensor/lib/itertools-0.2.3.dist-info/RECORD b/scripts/tempSensor/lib/itertools-0.2.3.dist-info/RECORD new file mode 100644 index 00000000..957baf93 --- /dev/null +++ b/scripts/tempSensor/lib/itertools-0.2.3.dist-info/RECORD @@ -0,0 +1,3 @@ +itertools-0.2.3.dist-info/METADATA,, +itertools.py,, +itertools-0.2.3.dist-info/RECORD,, \ No newline at end of file diff --git a/scripts/tempSensor/lib/itertools.py b/scripts/tempSensor/lib/itertools.py new file mode 100644 index 00000000..c715ca43 --- /dev/null +++ b/scripts/tempSensor/lib/itertools.py @@ -0,0 +1,77 @@ +def count(start=0, step=1): + while True: + yield start + start += step + + +def cycle(p): + try: + len(p) + except TypeError: + # len() is not defined for this type. Assume it is + # a finite iterable so we must cache the elements. + cache = [] + for i in p: + yield i + cache.append(i) + p = cache + while p: + yield from p + + +def repeat(el, n=None): + if n is None: + while True: + yield el + else: + for i in range(n): + yield el + + +def chain(*p): + for i in p: + yield from i + + +def islice(p, start, stop=(), step=1): + if stop == (): + stop = start + start = 0 + # TODO: optimizing or breaking semantics? + if start >= stop: + return + it = iter(p) + for i in range(start): + next(it) + + while True: + yield next(it) + for i in range(step - 1): + next(it) + start += step + if start >= stop: + return + + +def tee(iterable, n=2): + return [iter(iterable)] * n + + +def starmap(function, iterable): + for args in iterable: + yield function(*args) + + +def accumulate(iterable, func=lambda x, y: x + y): + it = iter(iterable) + try: + acc = next(it) + except StopIteration: + return + yield acc + for element in it: + acc = func(acc, element) + yield acc + + +__version__ = '0.2.3' diff --git a/scripts/tempSensor/lib/paho/__init__.py b/scripts/tempSensor/lib/paho/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/scripts/tempSensor/lib/paho/mqtt/__init__.py b/scripts/tempSensor/lib/paho/mqtt/__init__.py new file mode 100644 index 00000000..377cecc7 --- /dev/null +++ b/scripts/tempSensor/lib/paho/mqtt/__init__.py @@ -0,0 +1,5 @@ +__version__ = "2.1.0" + + +class MQTTException(Exception): + pass diff --git a/scripts/tempSensor/lib/paho/mqtt/client.py b/scripts/tempSensor/lib/paho/mqtt/client.py new file mode 100644 index 00000000..4ccc8696 --- /dev/null +++ b/scripts/tempSensor/lib/paho/mqtt/client.py @@ -0,0 +1,5004 @@ +# Copyright (c) 2012-2019 Roger Light and others +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Public License v2.0 +# and Eclipse Distribution License v1.0 which accompany this distribution. +# +# The Eclipse Public License is available at +# http://www.eclipse.org/legal/epl-v20.html +# and the Eclipse Distribution License is available at +# http://www.eclipse.org/org/documents/edl-v10.php. +# +# Contributors: +# Roger Light - initial API and implementation +# Ian Craggs - MQTT V5 support +""" +This is an MQTT client module. MQTT is a lightweight pub/sub messaging +protocol that is easy to implement and suitable for low powered devices. +""" +from __future__ import annotations + +import base64 +import collections +import errno +import hashlib +import logging +import os +import platform +import select +import socket +import string +import struct +import threading +import time +import urllib.parse +import urllib.request +import uuid +import warnings +from typing import TYPE_CHECKING, Any, Callable, Dict, Iterator, List, NamedTuple, Sequence, Tuple, Union, cast + +from paho.mqtt.packettypes import PacketTypes + +from .enums import CallbackAPIVersion, ConnackCode, LogLevel, MessageState, MessageType, MQTTErrorCode, MQTTProtocolVersion, PahoClientMode, _ConnectionState +from .matcher import MQTTMatcher +from .properties import Properties +from .reasoncodes import ReasonCode, ReasonCodes +from .subscribeoptions import SubscribeOptions + +try: + from typing import Literal +except ImportError: + from typing_extensions import Literal # type: ignore + +if TYPE_CHECKING: + try: + from typing import TypedDict # type: ignore + except ImportError: + from typing_extensions import TypedDict + + try: + from typing import Protocol # type: ignore + except ImportError: + from typing_extensions import Protocol # type: ignore + + class _InPacket(TypedDict): + command: int + have_remaining: int + remaining_count: list[int] + remaining_mult: int + remaining_length: int + packet: bytearray + to_process: int + pos: int + + + class _OutPacket(TypedDict): + command: int + mid: int + qos: int + pos: int + to_process: int + packet: bytes + info: MQTTMessageInfo | None + + class SocketLike(Protocol): + def recv(self, buffer_size: int) -> bytes: + ... + def send(self, buffer: bytes) -> int: + ... + def close(self) -> None: + ... + def fileno(self) -> int: + ... + def setblocking(self, flag: bool) -> None: + ... + + +try: + import ssl +except ImportError: + ssl = None # type: ignore[assignment] + + +try: + import socks # type: ignore[import-untyped] +except ImportError: + socks = None # type: ignore[assignment] + + +try: + # Use monotonic clock if available + time_func = time.monotonic +except AttributeError: + time_func = time.time + +try: + import dns.resolver + + HAVE_DNS = True +except ImportError: + HAVE_DNS = False + + +if platform.system() == 'Windows': + EAGAIN = errno.WSAEWOULDBLOCK # type: ignore[attr-defined] +else: + EAGAIN = errno.EAGAIN + +# Avoid linter complain. We kept importing it as ReasonCodes (plural) for compatibility +_ = ReasonCodes + +# Keep copy of enums values for compatibility. +CONNECT = MessageType.CONNECT +CONNACK = MessageType.CONNACK +PUBLISH = MessageType.PUBLISH +PUBACK = MessageType.PUBACK +PUBREC = MessageType.PUBREC +PUBREL = MessageType.PUBREL +PUBCOMP = MessageType.PUBCOMP +SUBSCRIBE = MessageType.SUBSCRIBE +SUBACK = MessageType.SUBACK +UNSUBSCRIBE = MessageType.UNSUBSCRIBE +UNSUBACK = MessageType.UNSUBACK +PINGREQ = MessageType.PINGREQ +PINGRESP = MessageType.PINGRESP +DISCONNECT = MessageType.DISCONNECT +AUTH = MessageType.AUTH + +# Log levels +MQTT_LOG_INFO = LogLevel.MQTT_LOG_INFO +MQTT_LOG_NOTICE = LogLevel.MQTT_LOG_NOTICE +MQTT_LOG_WARNING = LogLevel.MQTT_LOG_WARNING +MQTT_LOG_ERR = LogLevel.MQTT_LOG_ERR +MQTT_LOG_DEBUG = LogLevel.MQTT_LOG_DEBUG +LOGGING_LEVEL = { + LogLevel.MQTT_LOG_DEBUG: logging.DEBUG, + LogLevel.MQTT_LOG_INFO: logging.INFO, + LogLevel.MQTT_LOG_NOTICE: logging.INFO, # This has no direct equivalent level + LogLevel.MQTT_LOG_WARNING: logging.WARNING, + LogLevel.MQTT_LOG_ERR: logging.ERROR, +} + +# CONNACK codes +CONNACK_ACCEPTED = ConnackCode.CONNACK_ACCEPTED +CONNACK_REFUSED_PROTOCOL_VERSION = ConnackCode.CONNACK_REFUSED_PROTOCOL_VERSION +CONNACK_REFUSED_IDENTIFIER_REJECTED = ConnackCode.CONNACK_REFUSED_IDENTIFIER_REJECTED +CONNACK_REFUSED_SERVER_UNAVAILABLE = ConnackCode.CONNACK_REFUSED_SERVER_UNAVAILABLE +CONNACK_REFUSED_BAD_USERNAME_PASSWORD = ConnackCode.CONNACK_REFUSED_BAD_USERNAME_PASSWORD +CONNACK_REFUSED_NOT_AUTHORIZED = ConnackCode.CONNACK_REFUSED_NOT_AUTHORIZED + +# Message state +mqtt_ms_invalid = MessageState.MQTT_MS_INVALID +mqtt_ms_publish = MessageState.MQTT_MS_PUBLISH +mqtt_ms_wait_for_puback = MessageState.MQTT_MS_WAIT_FOR_PUBACK +mqtt_ms_wait_for_pubrec = MessageState.MQTT_MS_WAIT_FOR_PUBREC +mqtt_ms_resend_pubrel = MessageState.MQTT_MS_RESEND_PUBREL +mqtt_ms_wait_for_pubrel = MessageState.MQTT_MS_WAIT_FOR_PUBREL +mqtt_ms_resend_pubcomp = MessageState.MQTT_MS_RESEND_PUBCOMP +mqtt_ms_wait_for_pubcomp = MessageState.MQTT_MS_WAIT_FOR_PUBCOMP +mqtt_ms_send_pubrec = MessageState.MQTT_MS_SEND_PUBREC +mqtt_ms_queued = MessageState.MQTT_MS_QUEUED + +MQTT_ERR_AGAIN = MQTTErrorCode.MQTT_ERR_AGAIN +MQTT_ERR_SUCCESS = MQTTErrorCode.MQTT_ERR_SUCCESS +MQTT_ERR_NOMEM = MQTTErrorCode.MQTT_ERR_NOMEM +MQTT_ERR_PROTOCOL = MQTTErrorCode.MQTT_ERR_PROTOCOL +MQTT_ERR_INVAL = MQTTErrorCode.MQTT_ERR_INVAL +MQTT_ERR_NO_CONN = MQTTErrorCode.MQTT_ERR_NO_CONN +MQTT_ERR_CONN_REFUSED = MQTTErrorCode.MQTT_ERR_CONN_REFUSED +MQTT_ERR_NOT_FOUND = MQTTErrorCode.MQTT_ERR_NOT_FOUND +MQTT_ERR_CONN_LOST = MQTTErrorCode.MQTT_ERR_CONN_LOST +MQTT_ERR_TLS = MQTTErrorCode.MQTT_ERR_TLS +MQTT_ERR_PAYLOAD_SIZE = MQTTErrorCode.MQTT_ERR_PAYLOAD_SIZE +MQTT_ERR_NOT_SUPPORTED = MQTTErrorCode.MQTT_ERR_NOT_SUPPORTED +MQTT_ERR_AUTH = MQTTErrorCode.MQTT_ERR_AUTH +MQTT_ERR_ACL_DENIED = MQTTErrorCode.MQTT_ERR_ACL_DENIED +MQTT_ERR_UNKNOWN = MQTTErrorCode.MQTT_ERR_UNKNOWN +MQTT_ERR_ERRNO = MQTTErrorCode.MQTT_ERR_ERRNO +MQTT_ERR_QUEUE_SIZE = MQTTErrorCode.MQTT_ERR_QUEUE_SIZE +MQTT_ERR_KEEPALIVE = MQTTErrorCode.MQTT_ERR_KEEPALIVE + +MQTTv31 = MQTTProtocolVersion.MQTTv31 +MQTTv311 = MQTTProtocolVersion.MQTTv311 +MQTTv5 = MQTTProtocolVersion.MQTTv5 + +MQTT_CLIENT = PahoClientMode.MQTT_CLIENT +MQTT_BRIDGE = PahoClientMode.MQTT_BRIDGE + +# For MQTT V5, use the clean start flag only on the first successful connect +MQTT_CLEAN_START_FIRST_ONLY: CleanStartOption = 3 + +sockpair_data = b"0" + +# Payload support all those type and will be converted to bytes: +# * str are utf8 encoded +# * int/float are converted to string and utf8 encoded (e.g. 1 is converted to b"1") +# * None is converted to a zero-length payload (i.e. b"") +PayloadType = Union[str, bytes, bytearray, int, float, None] + +HTTPHeader = Dict[str, str] +WebSocketHeaders = Union[Callable[[HTTPHeader], HTTPHeader], HTTPHeader] + +CleanStartOption = Union[bool, Literal[3]] + + +class ConnectFlags(NamedTuple): + """Contains additional information passed to `on_connect` callback""" + + session_present: bool + """ + this flag is useful for clients that are + using clean session set to False only (MQTTv3) or clean_start = False (MQTTv5). + In that case, if client that reconnects to a broker that it has previously + connected to, this flag indicates whether the broker still has the + session information for the client. If true, the session still exists. + """ + + +class DisconnectFlags(NamedTuple): + """Contains additional information passed to `on_disconnect` callback""" + + is_disconnect_packet_from_server: bool + """ + tells whether this on_disconnect call is the result + of receiving an DISCONNECT packet from the broker or if the on_disconnect is only + generated by the client library. + When true, the reason code is generated by the broker. + """ + + +CallbackOnConnect_v1_mqtt3 = Callable[["Client", Any, Dict[str, Any], MQTTErrorCode], None] +CallbackOnConnect_v1_mqtt5 = Callable[["Client", Any, Dict[str, Any], ReasonCode, Union[Properties, None]], None] +CallbackOnConnect_v1 = Union[CallbackOnConnect_v1_mqtt5, CallbackOnConnect_v1_mqtt3] +CallbackOnConnect_v2 = Callable[["Client", Any, ConnectFlags, ReasonCode, Union[Properties, None]], None] +CallbackOnConnect = Union[CallbackOnConnect_v1, CallbackOnConnect_v2] +CallbackOnConnectFail = Callable[["Client", Any], None] +CallbackOnDisconnect_v1_mqtt3 = Callable[["Client", Any, MQTTErrorCode], None] +CallbackOnDisconnect_v1_mqtt5 = Callable[["Client", Any, Union[ReasonCode, int, None], Union[Properties, None]], None] +CallbackOnDisconnect_v1 = Union[CallbackOnDisconnect_v1_mqtt3, CallbackOnDisconnect_v1_mqtt5] +CallbackOnDisconnect_v2 = Callable[["Client", Any, DisconnectFlags, ReasonCode, Union[Properties, None]], None] +CallbackOnDisconnect = Union[CallbackOnDisconnect_v1, CallbackOnDisconnect_v2] +CallbackOnLog = Callable[["Client", Any, int, str], None] +CallbackOnMessage = Callable[["Client", Any, "MQTTMessage"], None] +CallbackOnPreConnect = Callable[["Client", Any], None] +CallbackOnPublish_v1 = Callable[["Client", Any, int], None] +CallbackOnPublish_v2 = Callable[["Client", Any, int, ReasonCode, Properties], None] +CallbackOnPublish = Union[CallbackOnPublish_v1, CallbackOnPublish_v2] +CallbackOnSocket = Callable[["Client", Any, "SocketLike"], None] +CallbackOnSubscribe_v1_mqtt3 = Callable[["Client", Any, int, Tuple[int, ...]], None] +CallbackOnSubscribe_v1_mqtt5 = Callable[["Client", Any, int, List[ReasonCode], Properties], None] +CallbackOnSubscribe_v1 = Union[CallbackOnSubscribe_v1_mqtt3, CallbackOnSubscribe_v1_mqtt5] +CallbackOnSubscribe_v2 = Callable[["Client", Any, int, List[ReasonCode], Union[Properties, None]], None] +CallbackOnSubscribe = Union[CallbackOnSubscribe_v1, CallbackOnSubscribe_v2] +CallbackOnUnsubscribe_v1_mqtt3 = Callable[["Client", Any, int], None] +CallbackOnUnsubscribe_v1_mqtt5 = Callable[["Client", Any, int, Properties, Union[ReasonCode, List[ReasonCode]]], None] +CallbackOnUnsubscribe_v1 = Union[CallbackOnUnsubscribe_v1_mqtt3, CallbackOnUnsubscribe_v1_mqtt5] +CallbackOnUnsubscribe_v2 = Callable[["Client", Any, int, List[ReasonCode], Union[Properties, None]], None] +CallbackOnUnsubscribe = Union[CallbackOnUnsubscribe_v1, CallbackOnUnsubscribe_v2] + +# This is needed for typing because class Client redefined the name "socket" +_socket = socket + + +class WebsocketConnectionError(ConnectionError): + """ WebsocketConnectionError is a subclass of ConnectionError. + + It's raised when unable to perform the Websocket handshake. + """ + pass + + +def error_string(mqtt_errno: MQTTErrorCode | int) -> str: + """Return the error string associated with an mqtt error number.""" + if mqtt_errno == MQTT_ERR_SUCCESS: + return "No error." + elif mqtt_errno == MQTT_ERR_NOMEM: + return "Out of memory." + elif mqtt_errno == MQTT_ERR_PROTOCOL: + return "A network protocol error occurred when communicating with the broker." + elif mqtt_errno == MQTT_ERR_INVAL: + return "Invalid function arguments provided." + elif mqtt_errno == MQTT_ERR_NO_CONN: + return "The client is not currently connected." + elif mqtt_errno == MQTT_ERR_CONN_REFUSED: + return "The connection was refused." + elif mqtt_errno == MQTT_ERR_NOT_FOUND: + return "Message not found (internal error)." + elif mqtt_errno == MQTT_ERR_CONN_LOST: + return "The connection was lost." + elif mqtt_errno == MQTT_ERR_TLS: + return "A TLS error occurred." + elif mqtt_errno == MQTT_ERR_PAYLOAD_SIZE: + return "Payload too large." + elif mqtt_errno == MQTT_ERR_NOT_SUPPORTED: + return "This feature is not supported." + elif mqtt_errno == MQTT_ERR_AUTH: + return "Authorisation failed." + elif mqtt_errno == MQTT_ERR_ACL_DENIED: + return "Access denied by ACL." + elif mqtt_errno == MQTT_ERR_UNKNOWN: + return "Unknown error." + elif mqtt_errno == MQTT_ERR_ERRNO: + return "Error defined by errno." + elif mqtt_errno == MQTT_ERR_QUEUE_SIZE: + return "Message queue full." + elif mqtt_errno == MQTT_ERR_KEEPALIVE: + return "Client or broker did not communicate in the keepalive interval." + else: + return "Unknown error." + + +def connack_string(connack_code: int|ReasonCode) -> str: + """Return the string associated with a CONNACK result or CONNACK reason code.""" + if isinstance(connack_code, ReasonCode): + return str(connack_code) + + if connack_code == CONNACK_ACCEPTED: + return "Connection Accepted." + elif connack_code == CONNACK_REFUSED_PROTOCOL_VERSION: + return "Connection Refused: unacceptable protocol version." + elif connack_code == CONNACK_REFUSED_IDENTIFIER_REJECTED: + return "Connection Refused: identifier rejected." + elif connack_code == CONNACK_REFUSED_SERVER_UNAVAILABLE: + return "Connection Refused: broker unavailable." + elif connack_code == CONNACK_REFUSED_BAD_USERNAME_PASSWORD: + return "Connection Refused: bad user name or password." + elif connack_code == CONNACK_REFUSED_NOT_AUTHORIZED: + return "Connection Refused: not authorised." + else: + return "Connection Refused: unknown reason." + + +def convert_connack_rc_to_reason_code(connack_code: ConnackCode) -> ReasonCode: + """Convert a MQTTv3 / MQTTv3.1.1 connack result to `ReasonCode`. + + This is used in `on_connect` callback to have a consistent API. + + Be careful that the numeric value isn't the same, for example: + + >>> ConnackCode.CONNACK_REFUSED_SERVER_UNAVAILABLE == 3 + >>> convert_connack_rc_to_reason_code(ConnackCode.CONNACK_REFUSED_SERVER_UNAVAILABLE) == 136 + + It's recommended to compare by names + + >>> code_to_test = ReasonCode(PacketTypes.CONNACK, "Server unavailable") + >>> convert_connack_rc_to_reason_code(ConnackCode.CONNACK_REFUSED_SERVER_UNAVAILABLE) == code_to_test + """ + if connack_code == ConnackCode.CONNACK_ACCEPTED: + return ReasonCode(PacketTypes.CONNACK, "Success") + if connack_code == ConnackCode.CONNACK_REFUSED_PROTOCOL_VERSION: + return ReasonCode(PacketTypes.CONNACK, "Unsupported protocol version") + if connack_code == ConnackCode.CONNACK_REFUSED_IDENTIFIER_REJECTED: + return ReasonCode(PacketTypes.CONNACK, "Client identifier not valid") + if connack_code == ConnackCode.CONNACK_REFUSED_SERVER_UNAVAILABLE: + return ReasonCode(PacketTypes.CONNACK, "Server unavailable") + if connack_code == ConnackCode.CONNACK_REFUSED_BAD_USERNAME_PASSWORD: + return ReasonCode(PacketTypes.CONNACK, "Bad user name or password") + if connack_code == ConnackCode.CONNACK_REFUSED_NOT_AUTHORIZED: + return ReasonCode(PacketTypes.CONNACK, "Not authorized") + + return ReasonCode(PacketTypes.CONNACK, "Unspecified error") + + +def convert_disconnect_error_code_to_reason_code(rc: MQTTErrorCode) -> ReasonCode: + """Convert an MQTTErrorCode to Reason code. + + This is used in `on_disconnect` callback to have a consistent API. + + Be careful that the numeric value isn't the same, for example: + + >>> MQTTErrorCode.MQTT_ERR_PROTOCOL == 2 + >>> convert_disconnect_error_code_to_reason_code(MQTTErrorCode.MQTT_ERR_PROTOCOL) == 130 + + It's recommended to compare by names + + >>> code_to_test = ReasonCode(PacketTypes.DISCONNECT, "Protocol error") + >>> convert_disconnect_error_code_to_reason_code(MQTTErrorCode.MQTT_ERR_PROTOCOL) == code_to_test + """ + if rc == MQTTErrorCode.MQTT_ERR_SUCCESS: + return ReasonCode(PacketTypes.DISCONNECT, "Success") + if rc == MQTTErrorCode.MQTT_ERR_KEEPALIVE: + return ReasonCode(PacketTypes.DISCONNECT, "Keep alive timeout") + if rc == MQTTErrorCode.MQTT_ERR_CONN_LOST: + return ReasonCode(PacketTypes.DISCONNECT, "Unspecified error") + return ReasonCode(PacketTypes.DISCONNECT, "Unspecified error") + + +def _base62( + num: int, + base: str = string.digits + string.ascii_letters, + padding: int = 1, +) -> str: + """Convert a number to base-62 representation.""" + if num < 0: + raise ValueError("Number must be positive or zero") + digits = [] + while num: + num, rest = divmod(num, 62) + digits.append(base[rest]) + digits.extend(base[0] for _ in range(len(digits), padding)) + return ''.join(reversed(digits)) + + +def topic_matches_sub(sub: str, topic: str) -> bool: + """Check whether a topic matches a subscription. + + For example: + + * Topic "foo/bar" would match the subscription "foo/#" or "+/bar" + * Topic "non/matching" would not match the subscription "non/+/+" + """ + matcher = MQTTMatcher() + matcher[sub] = True + try: + next(matcher.iter_match(topic)) + return True + except StopIteration: + return False + + +def _socketpair_compat() -> tuple[socket.socket, socket.socket]: + """TCP/IP socketpair including Windows support""" + listensock = socket.socket( + socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_IP) + listensock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + listensock.bind(("127.0.0.1", 0)) + listensock.listen(1) + + iface, port = listensock.getsockname() + sock1 = socket.socket( + socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_IP) + sock1.setblocking(False) + try: + sock1.connect(("127.0.0.1", port)) + except BlockingIOError: + pass + sock2, address = listensock.accept() + sock2.setblocking(False) + listensock.close() + return (sock1, sock2) + + +def _force_bytes(s: str | bytes) -> bytes: + if isinstance(s, str): + return s.encode("utf-8") + return s + + +def _encode_payload(payload: str | bytes | bytearray | int | float | None) -> bytes|bytearray: + if isinstance(payload, str): + return payload.encode("utf-8") + + if isinstance(payload, (int, float)): + return str(payload).encode("ascii") + + if payload is None: + return b"" + + if not isinstance(payload, (bytes, bytearray)): + raise TypeError( + "payload must be a string, bytearray, int, float or None." + ) + + return payload + + +class MQTTMessageInfo: + """This is a class returned from `Client.publish()` and can be used to find + out the mid of the message that was published, and to determine whether the + message has been published, and/or wait until it is published. + """ + + __slots__ = 'mid', '_published', '_condition', 'rc', '_iterpos' + + def __init__(self, mid: int): + self.mid = mid + """ The message Id (int)""" + self._published = False + self._condition = threading.Condition() + self.rc: MQTTErrorCode = MQTTErrorCode.MQTT_ERR_SUCCESS + """ The `MQTTErrorCode` that give status for this message. + This value could change until the message `is_published`""" + self._iterpos = 0 + + def __str__(self) -> str: + return str((self.rc, self.mid)) + + def __iter__(self) -> Iterator[MQTTErrorCode | int]: + self._iterpos = 0 + return self + + def __next__(self) -> MQTTErrorCode | int: + return self.next() + + def next(self) -> MQTTErrorCode | int: + if self._iterpos == 0: + self._iterpos = 1 + return self.rc + elif self._iterpos == 1: + self._iterpos = 2 + return self.mid + else: + raise StopIteration + + def __getitem__(self, index: int) -> MQTTErrorCode | int: + if index == 0: + return self.rc + elif index == 1: + return self.mid + else: + raise IndexError("index out of range") + + def _set_as_published(self) -> None: + with self._condition: + self._published = True + self._condition.notify() + + def wait_for_publish(self, timeout: float | None = None) -> None: + """Block until the message associated with this object is published, or + until the timeout occurs. If timeout is None, this will never time out. + Set timeout to a positive number of seconds, e.g. 1.2, to enable the + timeout. + + :raises ValueError: if the message was not queued due to the outgoing + queue being full. + + :raises RuntimeError: if the message was not published for another + reason. + """ + if self.rc == MQTT_ERR_QUEUE_SIZE: + raise ValueError('Message is not queued due to ERR_QUEUE_SIZE') + elif self.rc == MQTT_ERR_AGAIN: + pass + elif self.rc > 0: + raise RuntimeError(f'Message publish failed: {error_string(self.rc)}') + + timeout_time = None if timeout is None else time_func() + timeout + timeout_tenth = None if timeout is None else timeout / 10. + def timed_out() -> bool: + return False if timeout_time is None else time_func() > timeout_time + + with self._condition: + while not self._published and not timed_out(): + self._condition.wait(timeout_tenth) + + if self.rc > 0: + raise RuntimeError(f'Message publish failed: {error_string(self.rc)}') + + def is_published(self) -> bool: + """Returns True if the message associated with this object has been + published, else returns False. + + To wait for this to become true, look at `wait_for_publish`. + """ + if self.rc == MQTTErrorCode.MQTT_ERR_QUEUE_SIZE: + raise ValueError('Message is not queued due to ERR_QUEUE_SIZE') + elif self.rc == MQTTErrorCode.MQTT_ERR_AGAIN: + pass + elif self.rc > 0: + raise RuntimeError(f'Message publish failed: {error_string(self.rc)}') + + with self._condition: + return self._published + + +class MQTTMessage: + """ This is a class that describes an incoming message. It is + passed to the `on_message` callback as the message parameter. + """ + __slots__ = 'timestamp', 'state', 'dup', 'mid', '_topic', 'payload', 'qos', 'retain', 'info', 'properties' + + def __init__(self, mid: int = 0, topic: bytes = b""): + self.timestamp = 0.0 + self.state = mqtt_ms_invalid + self.dup = False + self.mid = mid + """ The message id (int).""" + self._topic = topic + self.payload = b"" + """the message payload (bytes)""" + self.qos = 0 + """ The message Quality of Service (0, 1 or 2).""" + self.retain = False + """ If true, the message is a retained message and not fresh.""" + self.info = MQTTMessageInfo(mid) + self.properties: Properties | None = None + """ In MQTT v5.0, the properties associated with the message. (`Properties`)""" + + def __eq__(self, other: object) -> bool: + """Override the default Equals behavior""" + if isinstance(other, self.__class__): + return self.mid == other.mid + return False + + def __ne__(self, other: object) -> bool: + """Define a non-equality test""" + return not self.__eq__(other) + + @property + def topic(self) -> str: + """topic that the message was published on. + + This property is read-only. + """ + return self._topic.decode('utf-8') + + @topic.setter + def topic(self, value: bytes) -> None: + self._topic = value + + +class Client: + """MQTT version 3.1/3.1.1/5.0 client class. + + This is the main class for use communicating with an MQTT broker. + + General usage flow: + + * Use `connect()`, `connect_async()` or `connect_srv()` to connect to a broker + * Use `loop_start()` to set a thread running to call `loop()` for you. + * Or use `loop_forever()` to handle calling `loop()` for you in a blocking function. + * Or call `loop()` frequently to maintain network traffic flow with the broker + * Use `subscribe()` to subscribe to a topic and receive messages + * Use `publish()` to send messages + * Use `disconnect()` to disconnect from the broker + + Data returned from the broker is made available with the use of callback + functions as described below. + + :param CallbackAPIVersion callback_api_version: define the API version for user-callback (on_connect, on_publish,...). + This field is required and it's recommended to use the latest version (CallbackAPIVersion.API_VERSION2). + See each callback for description of API for each version. The file docs/migrations.rst contains details on + how to migrate between version. + + :param str client_id: the unique client id string used when connecting to the + broker. If client_id is zero length or None, then the behaviour is + defined by which protocol version is in use. If using MQTT v3.1.1, then + a zero length client id will be sent to the broker and the broker will + generate a random for the client. If using MQTT v3.1 then an id will be + randomly generated. In both cases, clean_session must be True. If this + is not the case a ValueError will be raised. + + :param bool clean_session: a boolean that determines the client type. If True, + the broker will remove all information about this client when it + disconnects. If False, the client is a persistent client and + subscription information and queued messages will be retained when the + client disconnects. + Note that a client will never discard its own outgoing messages on + disconnect. Calling connect() or reconnect() will cause the messages to + be resent. Use reinitialise() to reset a client to its original state. + The clean_session argument only applies to MQTT versions v3.1.1 and v3.1. + It is not accepted if the MQTT version is v5.0 - use the clean_start + argument on connect() instead. + + :param userdata: user defined data of any type that is passed as the "userdata" + parameter to callbacks. It may be updated at a later point with the + user_data_set() function. + + :param int protocol: allows explicit setting of the MQTT version to + use for this client. Can be paho.mqtt.client.MQTTv311 (v3.1.1), + paho.mqtt.client.MQTTv31 (v3.1) or paho.mqtt.client.MQTTv5 (v5.0), + with the default being v3.1.1. + + :param transport: use "websockets" to use WebSockets as the transport + mechanism. Set to "tcp" to use raw TCP, which is the default. + Use "unix" to use Unix sockets as the transport mechanism; note that + this option is only available on platforms that support Unix sockets, + and the "host" argument is interpreted as the path to the Unix socket + file in this case. + + :param bool manual_ack: normally, when a message is received, the library automatically + acknowledges after on_message callback returns. manual_ack=True allows the application to + acknowledge receipt after it has completed processing of a message + using a the ack() method. This addresses vulnerability to message loss + if applications fails while processing a message, or while it pending + locally. + + Callbacks + ========= + + A number of callback functions are available to receive data back from the + broker. To use a callback, define a function and then assign it to the + client:: + + def on_connect(client, userdata, flags, reason_code, properties): + print(f"Connected with result code {reason_code}") + + client.on_connect = on_connect + + Callbacks can also be attached using decorators:: + + mqttc = paho.mqtt.Client() + + @mqttc.connect_callback() + def on_connect(client, userdata, flags, reason_code, properties): + print(f"Connected with result code {reason_code}") + + All of the callbacks as described below have a "client" and an "userdata" + argument. "client" is the `Client` instance that is calling the callback. + userdata" is user data of any type and can be set when creating a new client + instance or with `user_data_set()`. + + If you wish to suppress exceptions within a callback, you should set + ``mqttc.suppress_exceptions = True`` + + The callbacks are listed below, documentation for each of them can be found + at the same function name: + + `on_connect`, `on_connect_fail`, `on_disconnect`, `on_message`, `on_publish`, + `on_subscribe`, `on_unsubscribe`, `on_log`, `on_socket_open`, `on_socket_close`, + `on_socket_register_write`, `on_socket_unregister_write` + """ + + def __init__( + self, + callback_api_version: CallbackAPIVersion = CallbackAPIVersion.VERSION1, + client_id: str | None = "", + clean_session: bool | None = None, + userdata: Any = None, + protocol: MQTTProtocolVersion = MQTTv311, + transport: Literal["tcp", "websockets", "unix"] = "tcp", + reconnect_on_failure: bool = True, + manual_ack: bool = False, + ) -> None: + transport = transport.lower() # type: ignore + if transport == "unix" and not hasattr(socket, "AF_UNIX"): + raise ValueError('"unix" transport not supported') + elif transport not in ("websockets", "tcp", "unix"): + raise ValueError( + f'transport must be "websockets", "tcp" or "unix", not {transport}') + + self._manual_ack = manual_ack + self._transport = transport + self._protocol = protocol + self._userdata = userdata + self._sock: SocketLike | None = None + self._sockpairR: socket.socket | None = None + self._sockpairW: socket.socket | None = None + self._keepalive = 60 + self._connect_timeout = 5.0 + self._client_mode = MQTT_CLIENT + self._callback_api_version = callback_api_version + + if self._callback_api_version == CallbackAPIVersion.VERSION1: + warnings.warn( + "Callback API version 1 is deprecated, update to latest version", + category=DeprecationWarning, + stacklevel=2, + ) + if isinstance(self._callback_api_version, str): + # Help user to migrate, it probably provided a client id + # as first arguments + raise ValueError( + "Unsupported callback API version: version 2.0 added a callback_api_version, see docs/migrations.rst for details" + ) + if self._callback_api_version not in CallbackAPIVersion: + raise ValueError("Unsupported callback API version") + + self._clean_start: int = MQTT_CLEAN_START_FIRST_ONLY + + if protocol == MQTTv5: + if clean_session is not None: + raise ValueError('Clean session is not used for MQTT 5.0') + else: + if clean_session is None: + clean_session = True + if not clean_session and (client_id == "" or client_id is None): + raise ValueError( + 'A client id must be provided if clean session is False.') + self._clean_session = clean_session + + # [MQTT-3.1.3-4] Client Id must be UTF-8 encoded string. + if client_id == "" or client_id is None: + if protocol == MQTTv31: + self._client_id = _base62(uuid.uuid4().int, padding=22).encode("utf8") + else: + self._client_id = b"" + else: + self._client_id = _force_bytes(client_id) + + self._username: bytes | None = None + self._password: bytes | None = None + self._in_packet: _InPacket = { + "command": 0, + "have_remaining": 0, + "remaining_count": [], + "remaining_mult": 1, + "remaining_length": 0, + "packet": bytearray(b""), + "to_process": 0, + "pos": 0, + } + self._out_packet: collections.deque[_OutPacket] = collections.deque() + self._last_msg_in = time_func() + self._last_msg_out = time_func() + self._reconnect_min_delay = 1 + self._reconnect_max_delay = 120 + self._reconnect_delay: int | None = None + self._reconnect_on_failure = reconnect_on_failure + self._ping_t = 0.0 + self._last_mid = 0 + self._state = _ConnectionState.MQTT_CS_NEW + self._out_messages: collections.OrderedDict[ + int, MQTTMessage + ] = collections.OrderedDict() + self._in_messages: collections.OrderedDict[ + int, MQTTMessage + ] = collections.OrderedDict() + self._max_inflight_messages = 20 + self._inflight_messages = 0 + self._max_queued_messages = 0 + self._connect_properties: Properties | None = None + self._will_properties: Properties | None = None + self._will = False + self._will_topic = b"" + self._will_payload = b"" + self._will_qos = 0 + self._will_retain = False + self._on_message_filtered = MQTTMatcher() + self._host = "" + self._port = 1883 + self._bind_address = "" + self._bind_port = 0 + self._proxy: Any = {} + self._in_callback_mutex = threading.Lock() + self._callback_mutex = threading.RLock() + self._msgtime_mutex = threading.Lock() + self._out_message_mutex = threading.RLock() + self._in_message_mutex = threading.Lock() + self._reconnect_delay_mutex = threading.Lock() + self._mid_generate_mutex = threading.Lock() + self._thread: threading.Thread | None = None + self._thread_terminate = False + self._ssl = False + self._ssl_context: ssl.SSLContext | None = None + # Only used when SSL context does not have check_hostname attribute + self._tls_insecure = False + self._logger: logging.Logger | None = None + self._registered_write = False + # No default callbacks + self._on_log: CallbackOnLog | None = None + self._on_pre_connect: CallbackOnPreConnect | None = None + self._on_connect: CallbackOnConnect | None = None + self._on_connect_fail: CallbackOnConnectFail | None = None + self._on_subscribe: CallbackOnSubscribe | None = None + self._on_message: CallbackOnMessage | None = None + self._on_publish: CallbackOnPublish | None = None + self._on_unsubscribe: CallbackOnUnsubscribe | None = None + self._on_disconnect: CallbackOnDisconnect | None = None + self._on_socket_open: CallbackOnSocket | None = None + self._on_socket_close: CallbackOnSocket | None = None + self._on_socket_register_write: CallbackOnSocket | None = None + self._on_socket_unregister_write: CallbackOnSocket | None = None + self._websocket_path = "/mqtt" + self._websocket_extra_headers: WebSocketHeaders | None = None + # for clean_start == MQTT_CLEAN_START_FIRST_ONLY + self._mqttv5_first_connect = True + self.suppress_exceptions = False # For callbacks + + def __del__(self) -> None: + self._reset_sockets() + + @property + def host(self) -> str: + """ + Host to connect to. If `connect()` hasn't been called yet, returns an empty string. + + This property may not be changed if the connection is already open. + """ + return self._host + + @host.setter + def host(self, value: str) -> None: + if not self._connection_closed(): + raise RuntimeError("updating host on established connection is not supported") + + if not value: + raise ValueError("Invalid host.") + self._host = value + + @property + def port(self) -> int: + """ + Broker TCP port to connect to. + + This property may not be changed if the connection is already open. + """ + return self._port + + @port.setter + def port(self, value: int) -> None: + if not self._connection_closed(): + raise RuntimeError("updating port on established connection is not supported") + + if value <= 0: + raise ValueError("Invalid port number.") + self._port = value + + @property + def keepalive(self) -> int: + """ + Client keepalive interval (in seconds). + + This property may not be changed if the connection is already open. + """ + return self._keepalive + + @keepalive.setter + def keepalive(self, value: int) -> None: + if not self._connection_closed(): + # The issue here is that the previous value of keepalive matter to possibly + # sent ping packet. + raise RuntimeError("updating keepalive on established connection is not supported") + + if value < 0: + raise ValueError("Keepalive must be >=0.") + + self._keepalive = value + + @property + def transport(self) -> Literal["tcp", "websockets", "unix"]: + """ + Transport method used for the connection ("tcp" or "websockets"). + + This property may not be changed if the connection is already open. + """ + return self._transport + + @transport.setter + def transport(self, value: Literal["tcp", "websockets"]) -> None: + if not self._connection_closed(): + raise RuntimeError("updating transport on established connection is not supported") + + self._transport = value + + @property + def protocol(self) -> MQTTProtocolVersion: + """ + Protocol version used (MQTT v3, MQTT v3.11, MQTTv5) + + This property is read-only. + """ + return self._protocol + + @property + def connect_timeout(self) -> float: + """ + Connection establishment timeout in seconds. + + This property may not be changed if the connection is already open. + """ + return self._connect_timeout + + @connect_timeout.setter + def connect_timeout(self, value: float) -> None: + if not self._connection_closed(): + raise RuntimeError("updating connect_timeout on established connection is not supported") + + if value <= 0.0: + raise ValueError("timeout must be a positive number") + + self._connect_timeout = value + + @property + def username(self) -> str | None: + """The username used to connect to the MQTT broker, or None if no username is used. + + This property may not be changed if the connection is already open. + """ + if self._username is None: + return None + return self._username.decode("utf-8") + + @username.setter + def username(self, value: str | None) -> None: + if not self._connection_closed(): + raise RuntimeError("updating username on established connection is not supported") + + if value is None: + self._username = None + else: + self._username = value.encode("utf-8") + + @property + def password(self) -> str | None: + """The password used to connect to the MQTT broker, or None if no password is used. + + This property may not be changed if the connection is already open. + """ + if self._password is None: + return None + return self._password.decode("utf-8") + + @password.setter + def password(self, value: str | None) -> None: + if not self._connection_closed(): + raise RuntimeError("updating password on established connection is not supported") + + if value is None: + self._password = None + else: + self._password = value.encode("utf-8") + + @property + def max_inflight_messages(self) -> int: + """ + Maximum number of messages with QoS > 0 that can be partway through the network flow at once + + This property may not be changed if the connection is already open. + """ + return self._max_inflight_messages + + @max_inflight_messages.setter + def max_inflight_messages(self, value: int) -> None: + if not self._connection_closed(): + # Not tested. Some doubt that everything is okay when max_inflight change between 0 + # and > 0 value because _update_inflight is skipped when _max_inflight_messages == 0 + raise RuntimeError("updating max_inflight_messages on established connection is not supported") + + if value < 0: + raise ValueError("Invalid inflight.") + + self._max_inflight_messages = value + + @property + def max_queued_messages(self) -> int: + """ + Maximum number of message in the outgoing message queue, 0 means unlimited + + This property may not be changed if the connection is already open. + """ + return self._max_queued_messages + + @max_queued_messages.setter + def max_queued_messages(self, value: int) -> None: + if not self._connection_closed(): + # Not tested. + raise RuntimeError("updating max_queued_messages on established connection is not supported") + + if value < 0: + raise ValueError("Invalid queue size.") + + self._max_queued_messages = value + + @property + def will_topic(self) -> str | None: + """ + The topic name a will message is sent to when disconnecting unexpectedly. None if a will shall not be sent. + + This property is read-only. Use `will_set()` to change its value. + """ + if self._will_topic is None: + return None + + return self._will_topic.decode("utf-8") + + @property + def will_payload(self) -> bytes | None: + """ + The payload for the will message that is sent when disconnecting unexpectedly. None if a will shall not be sent. + + This property is read-only. Use `will_set()` to change its value. + """ + return self._will_payload + + @property + def logger(self) -> logging.Logger | None: + return self._logger + + @logger.setter + def logger(self, value: logging.Logger | None) -> None: + self._logger = value + + def _sock_recv(self, bufsize: int) -> bytes: + if self._sock is None: + raise ConnectionError("self._sock is None") + try: + return self._sock.recv(bufsize) + except ssl.SSLWantReadError as err: + raise BlockingIOError() from err + except ssl.SSLWantWriteError as err: + self._call_socket_register_write() + raise BlockingIOError() from err + except AttributeError as err: + self._easy_log( + MQTT_LOG_DEBUG, "socket was None: %s", err) + raise ConnectionError() from err + + def _sock_send(self, buf: bytes) -> int: + if self._sock is None: + raise ConnectionError("self._sock is None") + + try: + return self._sock.send(buf) + except ssl.SSLWantReadError as err: + raise BlockingIOError() from err + except ssl.SSLWantWriteError as err: + self._call_socket_register_write() + raise BlockingIOError() from err + except BlockingIOError as err: + self._call_socket_register_write() + raise BlockingIOError() from err + + def _sock_close(self) -> None: + """Close the connection to the server.""" + if not self._sock: + return + + try: + sock = self._sock + self._sock = None + self._call_socket_unregister_write(sock) + self._call_socket_close(sock) + finally: + # In case a callback fails, still close the socket to avoid leaking the file descriptor. + sock.close() + + def _reset_sockets(self, sockpair_only: bool = False) -> None: + if not sockpair_only: + self._sock_close() + + if self._sockpairR: + self._sockpairR.close() + self._sockpairR = None + if self._sockpairW: + self._sockpairW.close() + self._sockpairW = None + + def reinitialise( + self, + client_id: str = "", + clean_session: bool = True, + userdata: Any = None, + ) -> None: + self._reset_sockets() + + self.__init__(client_id, clean_session, userdata) # type: ignore[misc] + + def ws_set_options( + self, + path: str = "/mqtt", + headers: WebSocketHeaders | None = None, + ) -> None: + """ Set the path and headers for a websocket connection + + :param str path: a string starting with / which should be the endpoint of the + mqtt connection on the remote server + + :param headers: can be either a dict or a callable object. If it is a dict then + the extra items in the dict are added to the websocket headers. If it is + a callable, then the default websocket headers are passed into this + function and the result is used as the new headers. + """ + self._websocket_path = path + + if headers is not None: + if isinstance(headers, dict) or callable(headers): + self._websocket_extra_headers = headers + else: + raise ValueError( + "'headers' option to ws_set_options has to be either a dictionary or callable") + + def tls_set_context( + self, + context: ssl.SSLContext | None = None, + ) -> None: + """Configure network encryption and authentication context. Enables SSL/TLS support. + + :param context: an ssl.SSLContext object. By default this is given by + ``ssl.create_default_context()``, if available. + + Must be called before `connect()`, `connect_async()` or `connect_srv()`.""" + if self._ssl_context is not None: + raise ValueError('SSL/TLS has already been configured.') + + if context is None: + context = ssl.create_default_context() + + self._ssl = True + self._ssl_context = context + + # Ensure _tls_insecure is consistent with check_hostname attribute + if hasattr(context, 'check_hostname'): + self._tls_insecure = not context.check_hostname + + def tls_set( + self, + ca_certs: str | None = None, + certfile: str | None = None, + keyfile: str | None = None, + cert_reqs: ssl.VerifyMode | None = None, + tls_version: int | None = None, + ciphers: str | None = None, + keyfile_password: str | None = None, + alpn_protocols: list[str] | None = None, + ) -> None: + """Configure network encryption and authentication options. Enables SSL/TLS support. + + :param str ca_certs: a string path to the Certificate Authority certificate files + that are to be treated as trusted by this client. If this is the only + option given then the client will operate in a similar manner to a web + browser. That is to say it will require the broker to have a + certificate signed by the Certificate Authorities in ca_certs and will + communicate using TLS v1,2, but will not attempt any form of + authentication. This provides basic network encryption but may not be + sufficient depending on how the broker is configured. + + By default, on Python 2.7.9+ or 3.4+, the default certification + authority of the system is used. On older Python version this parameter + is mandatory. + :param str certfile: PEM encoded client certificate filename. Used with + keyfile for client TLS based authentication. Support for this feature is + broker dependent. Note that if the files in encrypted and needs a password to + decrypt it, then this can be passed using the keyfile_password argument - you + should take precautions to ensure that your password is + not hard coded into your program by loading the password from a file + for example. If you do not provide keyfile_password, the password will + be requested to be typed in at a terminal window. + :param str keyfile: PEM encoded client private keys filename. Used with + certfile for client TLS based authentication. Support for this feature is + broker dependent. Note that if the files in encrypted and needs a password to + decrypt it, then this can be passed using the keyfile_password argument - you + should take precautions to ensure that your password is + not hard coded into your program by loading the password from a file + for example. If you do not provide keyfile_password, the password will + be requested to be typed in at a terminal window. + :param cert_reqs: the certificate requirements that the client imposes + on the broker to be changed. By default this is ssl.CERT_REQUIRED, + which means that the broker must provide a certificate. See the ssl + pydoc for more information on this parameter. + :param tls_version: the version of the SSL/TLS protocol used to be + specified. By default TLS v1.2 is used. Previous versions are allowed + but not recommended due to possible security problems. + :param str ciphers: encryption ciphers that are allowed + for this connection, or None to use the defaults. See the ssl pydoc for + more information. + + Must be called before `connect()`, `connect_async()` or `connect_srv()`.""" + if ssl is None: + raise ValueError('This platform has no SSL/TLS.') + + if not hasattr(ssl, 'SSLContext'): + # Require Python version that has SSL context support in standard library + raise ValueError( + 'Python 2.7.9 and 3.2 are the minimum supported versions for TLS.') + + if ca_certs is None and not hasattr(ssl.SSLContext, 'load_default_certs'): + raise ValueError('ca_certs must not be None.') + + # Create SSLContext object + if tls_version is None: + tls_version = ssl.PROTOCOL_TLSv1_2 + # If the python version supports it, use highest TLS version automatically + if hasattr(ssl, "PROTOCOL_TLS_CLIENT"): + # This also enables CERT_REQUIRED and check_hostname by default. + tls_version = ssl.PROTOCOL_TLS_CLIENT + elif hasattr(ssl, "PROTOCOL_TLS"): + tls_version = ssl.PROTOCOL_TLS + context = ssl.SSLContext(tls_version) + + # Configure context + if ciphers is not None: + context.set_ciphers(ciphers) + + if certfile is not None: + context.load_cert_chain(certfile, keyfile, keyfile_password) + + if cert_reqs == ssl.CERT_NONE and hasattr(context, 'check_hostname'): + context.check_hostname = False + + context.verify_mode = ssl.CERT_REQUIRED if cert_reqs is None else cert_reqs + + if ca_certs is not None: + context.load_verify_locations(ca_certs) + else: + context.load_default_certs() + + if alpn_protocols is not None: + if not getattr(ssl, "HAS_ALPN", None): + raise ValueError("SSL library has no support for ALPN") + context.set_alpn_protocols(alpn_protocols) + + self.tls_set_context(context) + + if cert_reqs != ssl.CERT_NONE: + # Default to secure, sets context.check_hostname attribute + # if available + self.tls_insecure_set(False) + else: + # But with ssl.CERT_NONE, we can not check_hostname + self.tls_insecure_set(True) + + def tls_insecure_set(self, value: bool) -> None: + """Configure verification of the server hostname in the server certificate. + + If value is set to true, it is impossible to guarantee that the host + you are connecting to is not impersonating your server. This can be + useful in initial server testing, but makes it possible for a malicious + third party to impersonate your server through DNS spoofing, for + example. + + Do not use this function in a real system. Setting value to true means + there is no point using encryption. + + Must be called before `connect()` and after either `tls_set()` or + `tls_set_context()`.""" + + if self._ssl_context is None: + raise ValueError( + 'Must configure SSL context before using tls_insecure_set.') + + self._tls_insecure = value + + # Ensure check_hostname is consistent with _tls_insecure attribute + if hasattr(self._ssl_context, 'check_hostname'): + # Rely on SSLContext to check host name + # If verify_mode is CERT_NONE then the host name will never be checked + self._ssl_context.check_hostname = not value + + def proxy_set(self, **proxy_args: Any) -> None: + """Configure proxying of MQTT connection. Enables support for SOCKS or + HTTP proxies. + + Proxying is done through the PySocks library. Brief descriptions of the + proxy_args parameters are below; see the PySocks docs for more info. + + (Required) + + :param proxy_type: One of {socks.HTTP, socks.SOCKS4, or socks.SOCKS5} + :param proxy_addr: IP address or DNS name of proxy server + + (Optional) + + :param proxy_port: (int) port number of the proxy server. If not provided, + the PySocks package default value will be utilized, which differs by proxy_type. + :param proxy_rdns: boolean indicating whether proxy lookup should be performed + remotely (True, default) or locally (False) + :param proxy_username: username for SOCKS5 proxy, or userid for SOCKS4 proxy + :param proxy_password: password for SOCKS5 proxy + + Example:: + + mqttc.proxy_set(proxy_type=socks.HTTP, proxy_addr='1.2.3.4', proxy_port=4231) + """ + if socks is None: + raise ValueError("PySocks must be installed for proxy support.") + elif not self._proxy_is_valid(proxy_args): + raise ValueError("proxy_type and/or proxy_addr are invalid.") + else: + self._proxy = proxy_args + + def enable_logger(self, logger: logging.Logger | None = None) -> None: + """ + Enables a logger to send log messages to + + :param logging.Logger logger: if specified, that ``logging.Logger`` object will be used, otherwise + one will be created automatically. + + See `disable_logger` to undo this action. + """ + if logger is None: + if self._logger is not None: + # Do not replace existing logger + return + logger = logging.getLogger(__name__) + self.logger = logger + + def disable_logger(self) -> None: + """ + Disable logging using standard python logging package. This has no effect on the `on_log` callback. + """ + self._logger = None + + def connect( + self, + host: str, + port: int = 1883, + keepalive: int = 60, + bind_address: str = "", + bind_port: int = 0, + clean_start: CleanStartOption = MQTT_CLEAN_START_FIRST_ONLY, + properties: Properties | None = None, + ) -> MQTTErrorCode: + """Connect to a remote broker. This is a blocking call that establishes + the underlying connection and transmits a CONNECT packet. + Note that the connection status will not be updated until a CONNACK is received and + processed (this requires a running network loop, see `loop_start`, `loop_forever`, `loop`...). + + :param str host: the hostname or IP address of the remote broker. + :param int port: the network port of the server host to connect to. Defaults to + 1883. Note that the default port for MQTT over SSL/TLS is 8883 so if you + are using `tls_set()` the port may need providing. + :param int keepalive: Maximum period in seconds between communications with the + broker. If no other messages are being exchanged, this controls the + rate at which the client will send ping messages to the broker. + :param bool clean_start: (MQTT v5.0 only) True, False or MQTT_CLEAN_START_FIRST_ONLY. + Sets the MQTT v5.0 clean_start flag always, never or on the first successful connect only, + respectively. MQTT session data (such as outstanding messages and subscriptions) + is cleared on successful connect when the clean_start flag is set. + For MQTT v3.1.1, the ``clean_session`` argument of `Client` should be used for similar + result. + :param Properties properties: (MQTT v5.0 only) the MQTT v5.0 properties to be sent in the + MQTT connect packet. + """ + + if self._protocol == MQTTv5: + self._mqttv5_first_connect = True + else: + if clean_start != MQTT_CLEAN_START_FIRST_ONLY: + raise ValueError("Clean start only applies to MQTT V5") + if properties: + raise ValueError("Properties only apply to MQTT V5") + + self.connect_async(host, port, keepalive, + bind_address, bind_port, clean_start, properties) + return self.reconnect() + + def connect_srv( + self, + domain: str | None = None, + keepalive: int = 60, + bind_address: str = "", + bind_port: int = 0, + clean_start: CleanStartOption = MQTT_CLEAN_START_FIRST_ONLY, + properties: Properties | None = None, + ) -> MQTTErrorCode: + """Connect to a remote broker. + + :param str domain: the DNS domain to search for SRV records; if None, + try to determine local domain name. + :param keepalive, bind_address, clean_start and properties: see `connect()` + """ + + if HAVE_DNS is False: + raise ValueError( + 'No DNS resolver library found, try "pip install dnspython".') + + if domain is None: + domain = socket.getfqdn() + domain = domain[domain.find('.') + 1:] + + try: + rr = f'_mqtt._tcp.{domain}' + if self._ssl: + # IANA specifies secure-mqtt (not mqtts) for port 8883 + rr = f'_secure-mqtt._tcp.{domain}' + answers = [] + for answer in dns.resolver.query(rr, dns.rdatatype.SRV): + addr = answer.target.to_text()[:-1] + answers.append( + (addr, answer.port, answer.priority, answer.weight)) + except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer, dns.resolver.NoNameservers) as err: + raise ValueError(f"No answer/NXDOMAIN for SRV in {domain}") from err + + # FIXME: doesn't account for weight + for answer in answers: + host, port, prio, weight = answer + + try: + return self.connect(host, port, keepalive, bind_address, bind_port, clean_start, properties) + except Exception: # noqa: S110 + pass + + raise ValueError("No SRV hosts responded") + + def connect_async( + self, + host: str, + port: int = 1883, + keepalive: int = 60, + bind_address: str = "", + bind_port: int = 0, + clean_start: CleanStartOption = MQTT_CLEAN_START_FIRST_ONLY, + properties: Properties | None = None, + ) -> None: + """Connect to a remote broker asynchronously. This is a non-blocking + connect call that can be used with `loop_start()` to provide very quick + start. + + Any already established connection will be terminated immediately. + + :param str host: the hostname or IP address of the remote broker. + :param int port: the network port of the server host to connect to. Defaults to + 1883. Note that the default port for MQTT over SSL/TLS is 8883 so if you + are using `tls_set()` the port may need providing. + :param int keepalive: Maximum period in seconds between communications with the + broker. If no other messages are being exchanged, this controls the + rate at which the client will send ping messages to the broker. + :param bool clean_start: (MQTT v5.0 only) True, False or MQTT_CLEAN_START_FIRST_ONLY. + Sets the MQTT v5.0 clean_start flag always, never or on the first successful connect only, + respectively. MQTT session data (such as outstanding messages and subscriptions) + is cleared on successful connect when the clean_start flag is set. + For MQTT v3.1.1, the ``clean_session`` argument of `Client` should be used for similar + result. + :param Properties properties: (MQTT v5.0 only) the MQTT v5.0 properties to be sent in the + MQTT connect packet. + """ + if bind_port < 0: + raise ValueError('Invalid bind port number.') + + # Switch to state NEW to allow update of host, port & co. + self._sock_close() + self._state = _ConnectionState.MQTT_CS_NEW + + self.host = host + self.port = port + self.keepalive = keepalive + self._bind_address = bind_address + self._bind_port = bind_port + self._clean_start = clean_start + self._connect_properties = properties + self._state = _ConnectionState.MQTT_CS_CONNECT_ASYNC + + def reconnect_delay_set(self, min_delay: int = 1, max_delay: int = 120) -> None: + """ Configure the exponential reconnect delay + + When connection is lost, wait initially min_delay seconds and + double this time every attempt. The wait is capped at max_delay. + Once the client is fully connected (e.g. not only TCP socket, but + received a success CONNACK), the wait timer is reset to min_delay. + """ + with self._reconnect_delay_mutex: + self._reconnect_min_delay = min_delay + self._reconnect_max_delay = max_delay + self._reconnect_delay = None + + def reconnect(self) -> MQTTErrorCode: + """Reconnect the client after a disconnect. Can only be called after + connect()/connect_async().""" + if len(self._host) == 0: + raise ValueError('Invalid host.') + if self._port <= 0: + raise ValueError('Invalid port number.') + + self._in_packet = { + "command": 0, + "have_remaining": 0, + "remaining_count": [], + "remaining_mult": 1, + "remaining_length": 0, + "packet": bytearray(b""), + "to_process": 0, + "pos": 0, + } + + self._ping_t = 0.0 + self._state = _ConnectionState.MQTT_CS_CONNECTING + + self._sock_close() + + # Mark all currently outgoing QoS = 0 packets as lost, + # or `wait_for_publish()` could hang forever + for pkt in self._out_packet: + if pkt["command"] & 0xF0 == PUBLISH and pkt["qos"] == 0 and pkt["info"] is not None: + pkt["info"].rc = MQTT_ERR_CONN_LOST + pkt["info"]._set_as_published() + + self._out_packet.clear() + + with self._msgtime_mutex: + self._last_msg_in = time_func() + self._last_msg_out = time_func() + + # Put messages in progress in a valid state. + self._messages_reconnect_reset() + + with self._callback_mutex: + on_pre_connect = self.on_pre_connect + + if on_pre_connect: + try: + on_pre_connect(self, self._userdata) + except Exception as err: + self._easy_log( + MQTT_LOG_ERR, 'Caught exception in on_pre_connect: %s', err) + if not self.suppress_exceptions: + raise + + self._sock = self._create_socket() + + self._sock.setblocking(False) # type: ignore[attr-defined] + self._registered_write = False + self._call_socket_open(self._sock) + + return self._send_connect(self._keepalive) + + def loop(self, timeout: float = 1.0) -> MQTTErrorCode: + """Process network events. + + It is strongly recommended that you use `loop_start()`, or + `loop_forever()`, or if you are using an external event loop using + `loop_read()`, `loop_write()`, and `loop_misc()`. Using loop() on it's own is + no longer recommended. + + This function must be called regularly to ensure communication with the + broker is carried out. It calls select() on the network socket to wait + for network events. If incoming data is present it will then be + processed. Outgoing commands, from e.g. `publish()`, are normally sent + immediately that their function is called, but this is not always + possible. loop() will also attempt to send any remaining outgoing + messages, which also includes commands that are part of the flow for + messages with QoS>0. + + :param int timeout: The time in seconds to wait for incoming/outgoing network + traffic before timing out and returning. + + Returns MQTT_ERR_SUCCESS on success. + Returns >0 on error. + + A ValueError will be raised if timeout < 0""" + + if self._sockpairR is None or self._sockpairW is None: + self._reset_sockets(sockpair_only=True) + self._sockpairR, self._sockpairW = _socketpair_compat() + + return self._loop(timeout) + + def _loop(self, timeout: float = 1.0) -> MQTTErrorCode: + if timeout < 0.0: + raise ValueError('Invalid timeout.') + + if self.want_write(): + wlist = [self._sock] + else: + wlist = [] + + # used to check if there are any bytes left in the (SSL) socket + pending_bytes = 0 + if hasattr(self._sock, 'pending'): + pending_bytes = self._sock.pending() # type: ignore[union-attr] + + # if bytes are pending do not wait in select + if pending_bytes > 0: + timeout = 0.0 + + # sockpairR is used to break out of select() before the timeout, on a + # call to publish() etc. + if self._sockpairR is None: + rlist = [self._sock] + else: + rlist = [self._sock, self._sockpairR] + + try: + socklist = select.select(rlist, wlist, [], timeout) + except TypeError: + # Socket isn't correct type, in likelihood connection is lost + # ... or we called disconnect(). In that case the socket will + # be closed but some loop (like loop_forever) will continue to + # call _loop(). We still want to break that loop by returning an + # rc != MQTT_ERR_SUCCESS and we don't want state to change from + # mqtt_cs_disconnecting. + if self._state not in (_ConnectionState.MQTT_CS_DISCONNECTING, _ConnectionState.MQTT_CS_DISCONNECTED): + self._state = _ConnectionState.MQTT_CS_CONNECTION_LOST + return MQTTErrorCode.MQTT_ERR_CONN_LOST + except ValueError: + # Can occur if we just reconnected but rlist/wlist contain a -1 for + # some reason. + if self._state not in (_ConnectionState.MQTT_CS_DISCONNECTING, _ConnectionState.MQTT_CS_DISCONNECTED): + self._state = _ConnectionState.MQTT_CS_CONNECTION_LOST + return MQTTErrorCode.MQTT_ERR_CONN_LOST + except Exception: + # Note that KeyboardInterrupt, etc. can still terminate since they + # are not derived from Exception + return MQTTErrorCode.MQTT_ERR_UNKNOWN + + if self._sock in socklist[0] or pending_bytes > 0: + rc = self.loop_read() + if rc or self._sock is None: + return rc + + if self._sockpairR and self._sockpairR in socklist[0]: + # Stimulate output write even though we didn't ask for it, because + # at that point the publish or other command wasn't present. + socklist[1].insert(0, self._sock) + # Clear sockpairR - only ever a single byte written. + try: + # Read many bytes at once - this allows up to 10000 calls to + # publish() inbetween calls to loop(). + self._sockpairR.recv(10000) + except BlockingIOError: + pass + + if self._sock in socklist[1]: + rc = self.loop_write() + if rc or self._sock is None: + return rc + + return self.loop_misc() + + def publish( + self, + topic: str, + payload: PayloadType = None, + qos: int = 0, + retain: bool = False, + properties: Properties | None = None, + ) -> MQTTMessageInfo: + """Publish a message on a topic. + + This causes a message to be sent to the broker and subsequently from + the broker to any clients subscribing to matching topics. + + :param str topic: The topic that the message should be published on. + :param payload: The actual message to send. If not given, or set to None a + zero length message will be used. Passing an int or float will result + in the payload being converted to a string representing that number. If + you wish to send a true int/float, use struct.pack() to create the + payload you require. + :param int qos: The quality of service level to use. + :param bool retain: If set to true, the message will be set as the "last known + good"/retained message for the topic. + :param Properties properties: (MQTT v5.0 only) the MQTT v5.0 properties to be included. + + Returns a `MQTTMessageInfo` class, which can be used to determine whether + the message has been delivered (using `is_published()`) or to block + waiting for the message to be delivered (`wait_for_publish()`). The + message ID and return code of the publish() call can be found at + :py:attr:`info.mid ` and :py:attr:`info.rc `. + + For backwards compatibility, the `MQTTMessageInfo` class is iterable so + the old construct of ``(rc, mid) = client.publish(...)`` is still valid. + + rc is MQTT_ERR_SUCCESS to indicate success or MQTT_ERR_NO_CONN if the + client is not currently connected. mid is the message ID for the + publish request. The mid value can be used to track the publish request + by checking against the mid argument in the on_publish() callback if it + is defined. + + :raises ValueError: if topic is None, has zero length or is + invalid (contains a wildcard), except if the MQTT version used is v5.0. + For v5.0, a zero length topic can be used when a Topic Alias has been set. + :raises ValueError: if qos is not one of 0, 1 or 2 + :raises ValueError: if the length of the payload is greater than 268435455 bytes. + """ + if self._protocol != MQTTv5: + if topic is None or len(topic) == 0: + raise ValueError('Invalid topic.') + + topic_bytes = topic.encode('utf-8') + + self._raise_for_invalid_topic(topic_bytes) + + if qos < 0 or qos > 2: + raise ValueError('Invalid QoS level.') + + local_payload = _encode_payload(payload) + + if len(local_payload) > 268435455: + raise ValueError('Payload too large.') + + local_mid = self._mid_generate() + + if qos == 0: + info = MQTTMessageInfo(local_mid) + rc = self._send_publish( + local_mid, topic_bytes, local_payload, qos, retain, False, info, properties) + info.rc = rc + return info + else: + message = MQTTMessage(local_mid, topic_bytes) + message.timestamp = time_func() + message.payload = local_payload + message.qos = qos + message.retain = retain + message.dup = False + message.properties = properties + + with self._out_message_mutex: + if self._max_queued_messages > 0 and len(self._out_messages) >= self._max_queued_messages: + message.info.rc = MQTTErrorCode.MQTT_ERR_QUEUE_SIZE + return message.info + + if local_mid in self._out_messages: + message.info.rc = MQTTErrorCode.MQTT_ERR_QUEUE_SIZE + return message.info + + self._out_messages[message.mid] = message + if self._max_inflight_messages == 0 or self._inflight_messages < self._max_inflight_messages: + self._inflight_messages += 1 + if qos == 1: + message.state = mqtt_ms_wait_for_puback + elif qos == 2: + message.state = mqtt_ms_wait_for_pubrec + + rc = self._send_publish(message.mid, topic_bytes, message.payload, message.qos, message.retain, + message.dup, message.info, message.properties) + + # remove from inflight messages so it will be send after a connection is made + if rc == MQTTErrorCode.MQTT_ERR_NO_CONN: + self._inflight_messages -= 1 + message.state = mqtt_ms_publish + + message.info.rc = rc + return message.info + else: + message.state = mqtt_ms_queued + message.info.rc = MQTTErrorCode.MQTT_ERR_SUCCESS + return message.info + + def username_pw_set( + self, username: str | None, password: str | None = None + ) -> None: + """Set a username and optionally a password for broker authentication. + + Must be called before connect() to have any effect. + Requires a broker that supports MQTT v3.1 or more. + + :param str username: The username to authenticate with. Need have no relationship to the client id. Must be str + [MQTT-3.1.3-11]. + Set to None to reset client back to not using username/password for broker authentication. + :param str password: The password to authenticate with. Optional, set to None if not required. If it is str, then it + will be encoded as UTF-8. + """ + + # [MQTT-3.1.3-11] User name must be UTF-8 encoded string + self._username = None if username is None else username.encode('utf-8') + if isinstance(password, str): + self._password = password.encode('utf-8') + else: + self._password = password + + def enable_bridge_mode(self) -> None: + """Sets the client in a bridge mode instead of client mode. + + Must be called before `connect()` to have any effect. + Requires brokers that support bridge mode. + + Under bridge mode, the broker will identify the client as a bridge and + not send it's own messages back to it. Hence a subsciption of # is + possible without message loops. This feature also correctly propagates + the retain flag on the messages. + + Currently Mosquitto and RSMB support this feature. This feature can + be used to create a bridge between multiple broker. + """ + self._client_mode = MQTT_BRIDGE + + def _connection_closed(self) -> bool: + """ + Return true if the connection is closed (and not trying to be opened). + """ + return ( + self._state == _ConnectionState.MQTT_CS_NEW + or (self._state in (_ConnectionState.MQTT_CS_DISCONNECTING, _ConnectionState.MQTT_CS_DISCONNECTED) and self._sock is None)) + + def is_connected(self) -> bool: + """Returns the current status of the connection + + True if connection exists + False if connection is closed + """ + return self._state == _ConnectionState.MQTT_CS_CONNECTED + + def disconnect( + self, + reasoncode: ReasonCode | None = None, + properties: Properties | None = None, + ) -> MQTTErrorCode: + """Disconnect a connected client from the broker. + + :param ReasonCode reasoncode: (MQTT v5.0 only) a ReasonCode instance setting the MQTT v5.0 + reasoncode to be sent with the disconnect packet. It is optional, the receiver + then assuming that 0 (success) is the value. + :param Properties properties: (MQTT v5.0 only) a Properties instance setting the MQTT v5.0 properties + to be included. Optional - if not set, no properties are sent. + """ + if self._sock is None: + self._state = _ConnectionState.MQTT_CS_DISCONNECTED + return MQTT_ERR_NO_CONN + else: + self._state = _ConnectionState.MQTT_CS_DISCONNECTING + + return self._send_disconnect(reasoncode, properties) + + def subscribe( + self, + topic: str | tuple[str, int] | tuple[str, SubscribeOptions] | list[tuple[str, int]] | list[tuple[str, SubscribeOptions]], + qos: int = 0, + options: SubscribeOptions | None = None, + properties: Properties | None = None, + ) -> tuple[MQTTErrorCode, int | None]: + """Subscribe the client to one or more topics. + + This function may be called in three different ways (and a further three for MQTT v5.0): + + Simple string and integer + ------------------------- + e.g. subscribe("my/topic", 2) + + :topic: A string specifying the subscription topic to subscribe to. + :qos: The desired quality of service level for the subscription. + Defaults to 0. + :options and properties: Not used. + + Simple string and subscribe options (MQTT v5.0 only) + ---------------------------------------------------- + e.g. subscribe("my/topic", options=SubscribeOptions(qos=2)) + + :topic: A string specifying the subscription topic to subscribe to. + :qos: Not used. + :options: The MQTT v5.0 subscribe options. + :properties: a Properties instance setting the MQTT v5.0 properties + to be included. Optional - if not set, no properties are sent. + + String and integer tuple + ------------------------ + e.g. subscribe(("my/topic", 1)) + + :topic: A tuple of (topic, qos). Both topic and qos must be present in + the tuple. + :qos and options: Not used. + :properties: Only used for MQTT v5.0. A Properties instance setting the + MQTT v5.0 properties. Optional - if not set, no properties are sent. + + String and subscribe options tuple (MQTT v5.0 only) + --------------------------------------------------- + e.g. subscribe(("my/topic", SubscribeOptions(qos=1))) + + :topic: A tuple of (topic, SubscribeOptions). Both topic and subscribe + options must be present in the tuple. + :qos and options: Not used. + :properties: a Properties instance setting the MQTT v5.0 properties + to be included. Optional - if not set, no properties are sent. + + List of string and integer tuples + --------------------------------- + e.g. subscribe([("my/topic", 0), ("another/topic", 2)]) + + This allows multiple topic subscriptions in a single SUBSCRIPTION + command, which is more efficient than using multiple calls to + subscribe(). + + :topic: A list of tuple of format (topic, qos). Both topic and qos must + be present in all of the tuples. + :qos, options and properties: Not used. + + List of string and subscribe option tuples (MQTT v5.0 only) + ----------------------------------------------------------- + e.g. subscribe([("my/topic", SubscribeOptions(qos=0), ("another/topic", SubscribeOptions(qos=2)]) + + This allows multiple topic subscriptions in a single SUBSCRIPTION + command, which is more efficient than using multiple calls to + subscribe(). + + :topic: A list of tuple of format (topic, SubscribeOptions). Both topic and subscribe + options must be present in all of the tuples. + :qos and options: Not used. + :properties: a Properties instance setting the MQTT v5.0 properties + to be included. Optional - if not set, no properties are sent. + + The function returns a tuple (result, mid), where result is + MQTT_ERR_SUCCESS to indicate success or (MQTT_ERR_NO_CONN, None) if the + client is not currently connected. mid is the message ID for the + subscribe request. The mid value can be used to track the subscribe + request by checking against the mid argument in the on_subscribe() + callback if it is defined. + + Raises a ValueError if qos is not 0, 1 or 2, or if topic is None or has + zero string length, or if topic is not a string, tuple or list. + """ + topic_qos_list = None + + if isinstance(topic, tuple): + if self._protocol == MQTTv5: + topic, options = topic # type: ignore + if not isinstance(options, SubscribeOptions): + raise ValueError( + 'Subscribe options must be instance of SubscribeOptions class.') + else: + topic, qos = topic # type: ignore + + if isinstance(topic, (bytes, str)): + if qos < 0 or qos > 2: + raise ValueError('Invalid QoS level.') + if self._protocol == MQTTv5: + if options is None: + # if no options are provided, use the QoS passed instead + options = SubscribeOptions(qos=qos) + elif qos != 0: + raise ValueError( + 'Subscribe options and qos parameters cannot be combined.') + if not isinstance(options, SubscribeOptions): + raise ValueError( + 'Subscribe options must be instance of SubscribeOptions class.') + topic_qos_list = [(topic.encode('utf-8'), options)] + else: + if topic is None or len(topic) == 0: + raise ValueError('Invalid topic.') + topic_qos_list = [(topic.encode('utf-8'), qos)] # type: ignore + elif isinstance(topic, list): + if len(topic) == 0: + raise ValueError('Empty topic list') + topic_qos_list = [] + if self._protocol == MQTTv5: + for t, o in topic: + if not isinstance(o, SubscribeOptions): + # then the second value should be QoS + if o < 0 or o > 2: + raise ValueError('Invalid QoS level.') + o = SubscribeOptions(qos=o) + topic_qos_list.append((t.encode('utf-8'), o)) + else: + for t, q in topic: + if isinstance(q, SubscribeOptions) or q < 0 or q > 2: + raise ValueError('Invalid QoS level.') + if t is None or len(t) == 0 or not isinstance(t, (bytes, str)): + raise ValueError('Invalid topic.') + topic_qos_list.append((t.encode('utf-8'), q)) # type: ignore + + if topic_qos_list is None: + raise ValueError("No topic specified, or incorrect topic type.") + + if any(self._filter_wildcard_len_check(topic) != MQTT_ERR_SUCCESS for topic, _ in topic_qos_list): + raise ValueError('Invalid subscription filter.') + + if self._sock is None: + return (MQTT_ERR_NO_CONN, None) + + return self._send_subscribe(False, topic_qos_list, properties) + + def unsubscribe( + self, topic: str | list[str], properties: Properties | None = None + ) -> tuple[MQTTErrorCode, int | None]: + """Unsubscribe the client from one or more topics. + + :param topic: A single string, or list of strings that are the subscription + topics to unsubscribe from. + :param properties: (MQTT v5.0 only) a Properties instance setting the MQTT v5.0 properties + to be included. Optional - if not set, no properties are sent. + + Returns a tuple (result, mid), where result is MQTT_ERR_SUCCESS + to indicate success or (MQTT_ERR_NO_CONN, None) if the client is not + currently connected. + mid is the message ID for the unsubscribe request. The mid value can be + used to track the unsubscribe request by checking against the mid + argument in the on_unsubscribe() callback if it is defined. + + :raises ValueError: if topic is None or has zero string length, or is + not a string or list. + """ + topic_list = None + if topic is None: + raise ValueError('Invalid topic.') + if isinstance(topic, (bytes, str)): + if len(topic) == 0: + raise ValueError('Invalid topic.') + topic_list = [topic.encode('utf-8')] + elif isinstance(topic, list): + topic_list = [] + for t in topic: + if len(t) == 0 or not isinstance(t, (bytes, str)): + raise ValueError('Invalid topic.') + topic_list.append(t.encode('utf-8')) + + if topic_list is None: + raise ValueError("No topic specified, or incorrect topic type.") + + if self._sock is None: + return (MQTTErrorCode.MQTT_ERR_NO_CONN, None) + + return self._send_unsubscribe(False, topic_list, properties) + + def loop_read(self, max_packets: int = 1) -> MQTTErrorCode: + """Process read network events. Use in place of calling `loop()` if you + wish to handle your client reads as part of your own application. + + Use `socket()` to obtain the client socket to call select() or equivalent + on. + + Do not use if you are using `loop_start()` or `loop_forever()`.""" + if self._sock is None: + return MQTTErrorCode.MQTT_ERR_NO_CONN + + max_packets = len(self._out_messages) + len(self._in_messages) + if max_packets < 1: + max_packets = 1 + + for _ in range(0, max_packets): + if self._sock is None: + return MQTTErrorCode.MQTT_ERR_NO_CONN + rc = self._packet_read() + if rc > 0: + return self._loop_rc_handle(rc) + elif rc == MQTTErrorCode.MQTT_ERR_AGAIN: + return MQTTErrorCode.MQTT_ERR_SUCCESS + return MQTTErrorCode.MQTT_ERR_SUCCESS + + def loop_write(self) -> MQTTErrorCode: + """Process write network events. Use in place of calling `loop()` if you + wish to handle your client writes as part of your own application. + + Use `socket()` to obtain the client socket to call select() or equivalent + on. + + Use `want_write()` to determine if there is data waiting to be written. + + Do not use if you are using `loop_start()` or `loop_forever()`.""" + if self._sock is None: + return MQTTErrorCode.MQTT_ERR_NO_CONN + + try: + rc = self._packet_write() + if rc == MQTTErrorCode.MQTT_ERR_AGAIN: + return MQTTErrorCode.MQTT_ERR_SUCCESS + elif rc > 0: + return self._loop_rc_handle(rc) + else: + return MQTTErrorCode.MQTT_ERR_SUCCESS + finally: + if self.want_write(): + self._call_socket_register_write() + else: + self._call_socket_unregister_write() + + def want_write(self) -> bool: + """Call to determine if there is network data waiting to be written. + Useful if you are calling select() yourself rather than using `loop()`, `loop_start()` or `loop_forever()`. + """ + return len(self._out_packet) > 0 + + def loop_misc(self) -> MQTTErrorCode: + """Process miscellaneous network events. Use in place of calling `loop()` if you + wish to call select() or equivalent on. + + Do not use if you are using `loop_start()` or `loop_forever()`.""" + if self._sock is None: + return MQTTErrorCode.MQTT_ERR_NO_CONN + + now = time_func() + self._check_keepalive() + + if self._ping_t > 0 and now - self._ping_t >= self._keepalive: + # client->ping_t != 0 means we are waiting for a pingresp. + # This hasn't happened in the keepalive time so we should disconnect. + self._sock_close() + + if self._state in (_ConnectionState.MQTT_CS_DISCONNECTING, _ConnectionState.MQTT_CS_DISCONNECTED): + self._state = _ConnectionState.MQTT_CS_DISCONNECTED + rc = MQTTErrorCode.MQTT_ERR_SUCCESS + else: + self._state = _ConnectionState.MQTT_CS_CONNECTION_LOST + rc = MQTTErrorCode.MQTT_ERR_KEEPALIVE + + self._do_on_disconnect( + packet_from_broker=False, + v1_rc=rc, + ) + + return MQTTErrorCode.MQTT_ERR_CONN_LOST + + return MQTTErrorCode.MQTT_ERR_SUCCESS + + def max_inflight_messages_set(self, inflight: int) -> None: + """Set the maximum number of messages with QoS>0 that can be part way + through their network flow at once. Defaults to 20.""" + self.max_inflight_messages = inflight + + def max_queued_messages_set(self, queue_size: int) -> Client: + """Set the maximum number of messages in the outgoing message queue. + 0 means unlimited.""" + if not isinstance(queue_size, int): + raise ValueError('Invalid type of queue size.') + self.max_queued_messages = queue_size + return self + + def user_data_set(self, userdata: Any) -> None: + """Set the user data variable passed to callbacks. May be any data type.""" + self._userdata = userdata + + def user_data_get(self) -> Any: + """Get the user data variable passed to callbacks. May be any data type.""" + return self._userdata + + def will_set( + self, + topic: str, + payload: PayloadType = None, + qos: int = 0, + retain: bool = False, + properties: Properties | None = None, + ) -> None: + """Set a Will to be sent by the broker in case the client disconnects unexpectedly. + + This must be called before connect() to have any effect. + + :param str topic: The topic that the will message should be published on. + :param payload: The message to send as a will. If not given, or set to None a + zero length message will be used as the will. Passing an int or float + will result in the payload being converted to a string representing + that number. If you wish to send a true int/float, use struct.pack() to + create the payload you require. + :param int qos: The quality of service level to use for the will. + :param bool retain: If set to true, the will message will be set as the "last known + good"/retained message for the topic. + :param Properties properties: (MQTT v5.0 only) the MQTT v5.0 properties + to be included with the will message. Optional - if not set, no properties are sent. + + :raises ValueError: if qos is not 0, 1 or 2, or if topic is None or has + zero string length. + + See `will_clear` to clear will. Note that will are NOT send if the client disconnect cleanly + for example by calling `disconnect()`. + """ + if topic is None or len(topic) == 0: + raise ValueError('Invalid topic.') + + if qos < 0 or qos > 2: + raise ValueError('Invalid QoS level.') + + if properties and not isinstance(properties, Properties): + raise ValueError( + "The properties argument must be an instance of the Properties class.") + + self._will_payload = _encode_payload(payload) + self._will = True + self._will_topic = topic.encode('utf-8') + self._will_qos = qos + self._will_retain = retain + self._will_properties = properties + + def will_clear(self) -> None: + """ Removes a will that was previously configured with `will_set()`. + + Must be called before connect() to have any effect.""" + self._will = False + self._will_topic = b"" + self._will_payload = b"" + self._will_qos = 0 + self._will_retain = False + + def socket(self) -> SocketLike | None: + """Return the socket or ssl object for this client.""" + return self._sock + + def loop_forever( + self, + timeout: float = 1.0, + retry_first_connection: bool = False, + ) -> MQTTErrorCode: + """This function calls the network loop functions for you in an + infinite blocking loop. It is useful for the case where you only want + to run the MQTT client loop in your program. + + loop_forever() will handle reconnecting for you if reconnect_on_failure is + true (this is the default behavior). If you call `disconnect()` in a callback + it will return. + + :param int timeout: The time in seconds to wait for incoming/outgoing network + traffic before timing out and returning. + :param bool retry_first_connection: Should the first connection attempt be retried on failure. + This is independent of the reconnect_on_failure setting. + + :raises OSError: if the first connection fail unless retry_first_connection=True + """ + + run = True + + while run: + if self._thread_terminate is True: + break + + if self._state == _ConnectionState.MQTT_CS_CONNECT_ASYNC: + try: + self.reconnect() + except OSError: + self._handle_on_connect_fail() + if not retry_first_connection: + raise + self._easy_log( + MQTT_LOG_DEBUG, "Connection failed, retrying") + self._reconnect_wait() + else: + break + + while run: + rc = MQTTErrorCode.MQTT_ERR_SUCCESS + while rc == MQTTErrorCode.MQTT_ERR_SUCCESS: + rc = self._loop(timeout) + # We don't need to worry about locking here, because we've + # either called loop_forever() when in single threaded mode, or + # in multi threaded mode when loop_stop() has been called and + # so no other threads can access _out_packet or _messages. + if (self._thread_terminate is True + and len(self._out_packet) == 0 + and len(self._out_messages) == 0): + rc = MQTTErrorCode.MQTT_ERR_NOMEM + run = False + + def should_exit() -> bool: + return ( + self._state in (_ConnectionState.MQTT_CS_DISCONNECTING, _ConnectionState.MQTT_CS_DISCONNECTED) or + run is False or # noqa: B023 (uses the run variable from the outer scope on purpose) + self._thread_terminate is True + ) + + if should_exit() or not self._reconnect_on_failure: + run = False + else: + self._reconnect_wait() + + if should_exit(): + run = False + else: + try: + self.reconnect() + except OSError: + self._handle_on_connect_fail() + self._easy_log( + MQTT_LOG_DEBUG, "Connection failed, retrying") + + return rc + + def loop_start(self) -> MQTTErrorCode: + """This is part of the threaded client interface. Call this once to + start a new thread to process network traffic. This provides an + alternative to repeatedly calling `loop()` yourself. + + Under the hood, this will call `loop_forever` in a thread, which means that + the thread will terminate if you call `disconnect()` + """ + if self._thread is not None: + return MQTTErrorCode.MQTT_ERR_INVAL + + self._sockpairR, self._sockpairW = _socketpair_compat() + self._thread_terminate = False + self._thread = threading.Thread(target=self._thread_main, name=f"paho-mqtt-client-{self._client_id.decode()}") + self._thread.daemon = True + self._thread.start() + + return MQTTErrorCode.MQTT_ERR_SUCCESS + + def loop_stop(self) -> MQTTErrorCode: + """This is part of the threaded client interface. Call this once to + stop the network thread previously created with `loop_start()`. This call + will block until the network thread finishes. + + This don't guarantee that publish packet are sent, use `wait_for_publish` or + `on_publish` to ensure `publish` are sent. + """ + if self._thread is None: + return MQTTErrorCode.MQTT_ERR_INVAL + + self._thread_terminate = True + if threading.current_thread() != self._thread: + self._thread.join() + + return MQTTErrorCode.MQTT_ERR_SUCCESS + + @property + def callback_api_version(self) -> CallbackAPIVersion: + """ + Return the callback API version used for user-callback. See docstring for + each user-callback (`on_connect`, `on_publish`, ...) for details. + + This property is read-only. + """ + return self._callback_api_version + + @property + def on_log(self) -> CallbackOnLog | None: + """The callback called when the client has log information. + Defined to allow debugging. + + Expected signature is:: + + log_callback(client, userdata, level, buf) + + :param Client client: the client instance for this callback + :param userdata: the private user data as set in Client() or user_data_set() + :param int level: gives the severity of the message and will be one of + MQTT_LOG_INFO, MQTT_LOG_NOTICE, MQTT_LOG_WARNING, + MQTT_LOG_ERR, and MQTT_LOG_DEBUG. + :param str buf: the message itself + + Decorator: @client.log_callback() (``client`` is the name of the + instance which this callback is being attached to) + """ + return self._on_log + + @on_log.setter + def on_log(self, func: CallbackOnLog | None) -> None: + self._on_log = func + + def log_callback(self) -> Callable[[CallbackOnLog], CallbackOnLog]: + def decorator(func: CallbackOnLog) -> CallbackOnLog: + self.on_log = func + return func + return decorator + + @property + def on_pre_connect(self) -> CallbackOnPreConnect | None: + """The callback called immediately prior to the connection is made + request. + + Expected signature (for all callback API version):: + + connect_callback(client, userdata) + + :parama Client client: the client instance for this callback + :parama userdata: the private user data as set in Client() or user_data_set() + + Decorator: @client.pre_connect_callback() (``client`` is the name of the + instance which this callback is being attached to) + + """ + return self._on_pre_connect + + @on_pre_connect.setter + def on_pre_connect(self, func: CallbackOnPreConnect | None) -> None: + with self._callback_mutex: + self._on_pre_connect = func + + def pre_connect_callback( + self, + ) -> Callable[[CallbackOnPreConnect], CallbackOnPreConnect]: + def decorator(func: CallbackOnPreConnect) -> CallbackOnPreConnect: + self.on_pre_connect = func + return func + return decorator + + @property + def on_connect(self) -> CallbackOnConnect | None: + """The callback called when the broker reponds to our connection request. + + Expected signature for callback API version 2:: + + connect_callback(client, userdata, connect_flags, reason_code, properties) + + Expected signature for callback API version 1 change with MQTT protocol version: + * For MQTT v3.1 and v3.1.1 it's:: + + connect_callback(client, userdata, flags, rc) + + * For MQTT v5.0 it's:: + + connect_callback(client, userdata, flags, reason_code, properties) + + + :param Client client: the client instance for this callback + :param userdata: the private user data as set in Client() or user_data_set() + :param ConnectFlags connect_flags: the flags for this connection + :param ReasonCode reason_code: the connection reason code received from the broken. + In MQTT v5.0 it's the reason code defined by the standard. + In MQTT v3, we convert return code to a reason code, see + `convert_connack_rc_to_reason_code()`. + `ReasonCode` may be compared to integer. + :param Properties properties: the MQTT v5.0 properties received from the broker. + For MQTT v3.1 and v3.1.1 properties is not provided and an empty Properties + object is always used. + :param dict flags: response flags sent by the broker + :param int rc: the connection result, should have a value of `ConnackCode` + + flags is a dict that contains response flags from the broker: + flags['session present'] - this flag is useful for clients that are + using clean session set to 0 only. If a client with clean + session=0, that reconnects to a broker that it has previously + connected to, this flag indicates whether the broker still has the + session information for the client. If 1, the session still exists. + + The value of rc indicates success or not: + - 0: Connection successful + - 1: Connection refused - incorrect protocol version + - 2: Connection refused - invalid client identifier + - 3: Connection refused - server unavailable + - 4: Connection refused - bad username or password + - 5: Connection refused - not authorised + - 6-255: Currently unused. + + Decorator: @client.connect_callback() (``client`` is the name of the + instance which this callback is being attached to) + """ + return self._on_connect + + @on_connect.setter + def on_connect(self, func: CallbackOnConnect | None) -> None: + with self._callback_mutex: + self._on_connect = func + + def connect_callback( + self, + ) -> Callable[[CallbackOnConnect], CallbackOnConnect]: + def decorator(func: CallbackOnConnect) -> CallbackOnConnect: + self.on_connect = func + return func + return decorator + + @property + def on_connect_fail(self) -> CallbackOnConnectFail | None: + """The callback called when the client failed to connect + to the broker. + + Expected signature is (for all callback_api_version):: + + connect_fail_callback(client, userdata) + + :param Client client: the client instance for this callback + :parama userdata: the private user data as set in Client() or user_data_set() + + Decorator: @client.connect_fail_callback() (``client`` is the name of the + instance which this callback is being attached to) + """ + return self._on_connect_fail + + @on_connect_fail.setter + def on_connect_fail(self, func: CallbackOnConnectFail | None) -> None: + with self._callback_mutex: + self._on_connect_fail = func + + def connect_fail_callback( + self, + ) -> Callable[[CallbackOnConnectFail], CallbackOnConnectFail]: + def decorator(func: CallbackOnConnectFail) -> CallbackOnConnectFail: + self.on_connect_fail = func + return func + return decorator + + @property + def on_subscribe(self) -> CallbackOnSubscribe | None: + """The callback called when the broker responds to a subscribe + request. + + Expected signature for callback API version 2:: + + subscribe_callback(client, userdata, mid, reason_code_list, properties) + + Expected signature for callback API version 1 change with MQTT protocol version: + * For MQTT v3.1 and v3.1.1 it's:: + + subscribe_callback(client, userdata, mid, granted_qos) + + * For MQTT v5.0 it's:: + + subscribe_callback(client, userdata, mid, reason_code_list, properties) + + :param Client client: the client instance for this callback + :param userdata: the private user data as set in Client() or user_data_set() + :param int mid: matches the mid variable returned from the corresponding + subscribe() call. + :param list[ReasonCode] reason_code_list: reason codes received from the broker for each subscription. + In MQTT v5.0 it's the reason code defined by the standard. + In MQTT v3, we convert granted QoS to a reason code. + It's a list of ReasonCode instances. + :param Properties properties: the MQTT v5.0 properties received from the broker. + For MQTT v3.1 and v3.1.1 properties is not provided and an empty Properties + object is always used. + :param list[int] granted_qos: list of integers that give the QoS level the broker has + granted for each of the different subscription requests. + + Decorator: @client.subscribe_callback() (``client`` is the name of the + instance which this callback is being attached to) + """ + return self._on_subscribe + + @on_subscribe.setter + def on_subscribe(self, func: CallbackOnSubscribe | None) -> None: + with self._callback_mutex: + self._on_subscribe = func + + def subscribe_callback( + self, + ) -> Callable[[CallbackOnSubscribe], CallbackOnSubscribe]: + def decorator(func: CallbackOnSubscribe) -> CallbackOnSubscribe: + self.on_subscribe = func + return func + return decorator + + @property + def on_message(self) -> CallbackOnMessage | None: + """The callback called when a message has been received on a topic + that the client subscribes to. + + This callback will be called for every message received unless a + `message_callback_add()` matched the message. + + Expected signature is (for all callback API version): + message_callback(client, userdata, message) + + :param Client client: the client instance for this callback + :param userdata: the private user data as set in Client() or user_data_set() + :param MQTTMessage message: the received message. + This is a class with members topic, payload, qos, retain. + + Decorator: @client.message_callback() (``client`` is the name of the + instance which this callback is being attached to) + """ + return self._on_message + + @on_message.setter + def on_message(self, func: CallbackOnMessage | None) -> None: + with self._callback_mutex: + self._on_message = func + + def message_callback( + self, + ) -> Callable[[CallbackOnMessage], CallbackOnMessage]: + def decorator(func: CallbackOnMessage) -> CallbackOnMessage: + self.on_message = func + return func + return decorator + + @property + def on_publish(self) -> CallbackOnPublish | None: + """The callback called when a message that was to be sent using the + `publish()` call has completed transmission to the broker. + + For messages with QoS levels 1 and 2, this means that the appropriate + handshakes have completed. For QoS 0, this simply means that the message + has left the client. + This callback is important because even if the `publish()` call returns + success, it does not always mean that the message has been sent. + + See also `wait_for_publish` which could be simpler to use. + + Expected signature for callback API version 2:: + + publish_callback(client, userdata, mid, reason_code, properties) + + Expected signature for callback API version 1:: + + publish_callback(client, userdata, mid) + + :param Client client: the client instance for this callback + :param userdata: the private user data as set in Client() or user_data_set() + :param int mid: matches the mid variable returned from the corresponding + `publish()` call, to allow outgoing messages to be tracked. + :param ReasonCode reason_code: the connection reason code received from the broken. + In MQTT v5.0 it's the reason code defined by the standard. + In MQTT v3 it's always the reason code Success + :parama Properties properties: the MQTT v5.0 properties received from the broker. + For MQTT v3.1 and v3.1.1 properties is not provided and an empty Properties + object is always used. + + Note: for QoS = 0, the reason_code and the properties don't really exist, it's the client + library that generate them. It's always an empty properties and a success reason code. + Because the (MQTTv5) standard don't have reason code for PUBLISH packet, the library create them + at PUBACK packet, as if the message was sent with QoS = 1. + + Decorator: @client.publish_callback() (``client`` is the name of the + instance which this callback is being attached to) + + """ + return self._on_publish + + @on_publish.setter + def on_publish(self, func: CallbackOnPublish | None) -> None: + with self._callback_mutex: + self._on_publish = func + + def publish_callback( + self, + ) -> Callable[[CallbackOnPublish], CallbackOnPublish]: + def decorator(func: CallbackOnPublish) -> CallbackOnPublish: + self.on_publish = func + return func + return decorator + + @property + def on_unsubscribe(self) -> CallbackOnUnsubscribe | None: + """The callback called when the broker responds to an unsubscribe + request. + + Expected signature for callback API version 2:: + + unsubscribe_callback(client, userdata, mid, reason_code_list, properties) + + Expected signature for callback API version 1 change with MQTT protocol version: + * For MQTT v3.1 and v3.1.1 it's:: + + unsubscribe_callback(client, userdata, mid) + + * For MQTT v5.0 it's:: + + unsubscribe_callback(client, userdata, mid, properties, v1_reason_codes) + + :param Client client: the client instance for this callback + :param userdata: the private user data as set in Client() or user_data_set() + :param mid: matches the mid variable returned from the corresponding + unsubscribe() call. + :param list[ReasonCode] reason_code_list: reason codes received from the broker for each unsubscription. + In MQTT v5.0 it's the reason code defined by the standard. + In MQTT v3, there is not equivalent from broken and empty list + is always used. + :param Properties properties: the MQTT v5.0 properties received from the broker. + For MQTT v3.1 and v3.1.1 properties is not provided and an empty Properties + object is always used. + :param v1_reason_codes: the MQTT v5.0 reason codes received from the broker for each + unsubscribe topic. A list of ReasonCode instances OR a single + ReasonCode when we unsubscribe from a single topic. + + Decorator: @client.unsubscribe_callback() (``client`` is the name of the + instance which this callback is being attached to) + """ + return self._on_unsubscribe + + @on_unsubscribe.setter + def on_unsubscribe(self, func: CallbackOnUnsubscribe | None) -> None: + with self._callback_mutex: + self._on_unsubscribe = func + + def unsubscribe_callback( + self, + ) -> Callable[[CallbackOnUnsubscribe], CallbackOnUnsubscribe]: + def decorator(func: CallbackOnUnsubscribe) -> CallbackOnUnsubscribe: + self.on_unsubscribe = func + return func + return decorator + + @property + def on_disconnect(self) -> CallbackOnDisconnect | None: + """The callback called when the client disconnects from the broker. + + Expected signature for callback API version 2:: + + disconnect_callback(client, userdata, disconnect_flags, reason_code, properties) + + Expected signature for callback API version 1 change with MQTT protocol version: + * For MQTT v3.1 and v3.1.1 it's:: + + disconnect_callback(client, userdata, rc) + + * For MQTT v5.0 it's:: + + disconnect_callback(client, userdata, reason_code, properties) + + :param Client client: the client instance for this callback + :param userdata: the private user data as set in Client() or user_data_set() + :param DisconnectFlag disconnect_flags: the flags for this disconnection. + :param ReasonCode reason_code: the disconnection reason code possibly received from the broker (see disconnect_flags). + In MQTT v5.0 it's the reason code defined by the standard. + In MQTT v3 it's never received from the broker, we convert an MQTTErrorCode, + see `convert_disconnect_error_code_to_reason_code()`. + `ReasonCode` may be compared to integer. + :param Properties properties: the MQTT v5.0 properties received from the broker. + For MQTT v3.1 and v3.1.1 properties is not provided and an empty Properties + object is always used. + :param int rc: the disconnection result + The rc parameter indicates the disconnection state. If + MQTT_ERR_SUCCESS (0), the callback was called in response to + a disconnect() call. If any other value the disconnection + was unexpected, such as might be caused by a network error. + + Decorator: @client.disconnect_callback() (``client`` is the name of the + instance which this callback is being attached to) + + """ + return self._on_disconnect + + @on_disconnect.setter + def on_disconnect(self, func: CallbackOnDisconnect | None) -> None: + with self._callback_mutex: + self._on_disconnect = func + + def disconnect_callback( + self, + ) -> Callable[[CallbackOnDisconnect], CallbackOnDisconnect]: + def decorator(func: CallbackOnDisconnect) -> CallbackOnDisconnect: + self.on_disconnect = func + return func + return decorator + + @property + def on_socket_open(self) -> CallbackOnSocket | None: + """The callback called just after the socket was opend. + + This should be used to register the socket to an external event loop for reading. + + Expected signature is (for all callback API version):: + + socket_open_callback(client, userdata, socket) + + :param Client client: the client instance for this callback + :param userdata: the private user data as set in Client() or user_data_set() + :param SocketLike sock: the socket which was just opened. + + Decorator: @client.socket_open_callback() (``client`` is the name of the + instance which this callback is being attached to) + """ + return self._on_socket_open + + @on_socket_open.setter + def on_socket_open(self, func: CallbackOnSocket | None) -> None: + with self._callback_mutex: + self._on_socket_open = func + + def socket_open_callback( + self, + ) -> Callable[[CallbackOnSocket], CallbackOnSocket]: + def decorator(func: CallbackOnSocket) -> CallbackOnSocket: + self.on_socket_open = func + return func + return decorator + + def _call_socket_open(self, sock: SocketLike) -> None: + """Call the socket_open callback with the just-opened socket""" + with self._callback_mutex: + on_socket_open = self.on_socket_open + + if on_socket_open: + with self._in_callback_mutex: + try: + on_socket_open(self, self._userdata, sock) + except Exception as err: + self._easy_log( + MQTT_LOG_ERR, 'Caught exception in on_socket_open: %s', err) + if not self.suppress_exceptions: + raise + + @property + def on_socket_close(self) -> CallbackOnSocket | None: + """The callback called just before the socket is closed. + + This should be used to unregister the socket from an external event loop for reading. + + Expected signature is (for all callback API version):: + + socket_close_callback(client, userdata, socket) + + :param Client client: the client instance for this callback + :param userdata: the private user data as set in Client() or user_data_set() + :param SocketLike sock: the socket which is about to be closed. + + Decorator: @client.socket_close_callback() (``client`` is the name of the + instance which this callback is being attached to) + """ + return self._on_socket_close + + @on_socket_close.setter + def on_socket_close(self, func: CallbackOnSocket | None) -> None: + with self._callback_mutex: + self._on_socket_close = func + + def socket_close_callback( + self, + ) -> Callable[[CallbackOnSocket], CallbackOnSocket]: + def decorator(func: CallbackOnSocket) -> CallbackOnSocket: + self.on_socket_close = func + return func + return decorator + + def _call_socket_close(self, sock: SocketLike) -> None: + """Call the socket_close callback with the about-to-be-closed socket""" + with self._callback_mutex: + on_socket_close = self.on_socket_close + + if on_socket_close: + with self._in_callback_mutex: + try: + on_socket_close(self, self._userdata, sock) + except Exception as err: + self._easy_log( + MQTT_LOG_ERR, 'Caught exception in on_socket_close: %s', err) + if not self.suppress_exceptions: + raise + + @property + def on_socket_register_write(self) -> CallbackOnSocket | None: + """The callback called when the socket needs writing but can't. + + This should be used to register the socket with an external event loop for writing. + + Expected signature is (for all callback API version):: + + socket_register_write_callback(client, userdata, socket) + + :param Client client: the client instance for this callback + :param userdata: the private user data as set in Client() or user_data_set() + :param SocketLike sock: the socket which should be registered for writing + + Decorator: @client.socket_register_write_callback() (``client`` is the name of the + instance which this callback is being attached to) + """ + return self._on_socket_register_write + + @on_socket_register_write.setter + def on_socket_register_write(self, func: CallbackOnSocket | None) -> None: + with self._callback_mutex: + self._on_socket_register_write = func + + def socket_register_write_callback( + self, + ) -> Callable[[CallbackOnSocket], CallbackOnSocket]: + def decorator(func: CallbackOnSocket) -> CallbackOnSocket: + self._on_socket_register_write = func + return func + return decorator + + def _call_socket_register_write(self) -> None: + """Call the socket_register_write callback with the unwritable socket""" + if not self._sock or self._registered_write: + return + self._registered_write = True + with self._callback_mutex: + on_socket_register_write = self.on_socket_register_write + + if on_socket_register_write: + try: + on_socket_register_write( + self, self._userdata, self._sock) + except Exception as err: + self._easy_log( + MQTT_LOG_ERR, 'Caught exception in on_socket_register_write: %s', err) + if not self.suppress_exceptions: + raise + + @property + def on_socket_unregister_write( + self, + ) -> CallbackOnSocket | None: + """The callback called when the socket doesn't need writing anymore. + + This should be used to unregister the socket from an external event loop for writing. + + Expected signature is (for all callback API version):: + + socket_unregister_write_callback(client, userdata, socket) + + :param Client client: the client instance for this callback + :param userdata: the private user data as set in Client() or user_data_set() + :param SocketLike sock: the socket which should be unregistered for writing + + Decorator: @client.socket_unregister_write_callback() (``client`` is the name of the + instance which this callback is being attached to) + """ + return self._on_socket_unregister_write + + @on_socket_unregister_write.setter + def on_socket_unregister_write( + self, func: CallbackOnSocket | None + ) -> None: + with self._callback_mutex: + self._on_socket_unregister_write = func + + def socket_unregister_write_callback( + self, + ) -> Callable[[CallbackOnSocket], CallbackOnSocket]: + def decorator( + func: CallbackOnSocket, + ) -> CallbackOnSocket: + self._on_socket_unregister_write = func + return func + return decorator + + def _call_socket_unregister_write( + self, sock: SocketLike | None = None + ) -> None: + """Call the socket_unregister_write callback with the writable socket""" + sock = sock or self._sock + if not sock or not self._registered_write: + return + self._registered_write = False + + with self._callback_mutex: + on_socket_unregister_write = self.on_socket_unregister_write + + if on_socket_unregister_write: + try: + on_socket_unregister_write(self, self._userdata, sock) + except Exception as err: + self._easy_log( + MQTT_LOG_ERR, 'Caught exception in on_socket_unregister_write: %s', err) + if not self.suppress_exceptions: + raise + + def message_callback_add(self, sub: str, callback: CallbackOnMessage) -> None: + """Register a message callback for a specific topic. + Messages that match 'sub' will be passed to 'callback'. Any + non-matching messages will be passed to the default `on_message` + callback. + + Call multiple times with different 'sub' to define multiple topic + specific callbacks. + + Topic specific callbacks may be removed with + `message_callback_remove()`. + + See `on_message` for the expected signature of the callback. + + Decorator: @client.topic_callback(sub) (``client`` is the name of the + instance which this callback is being attached to) + + Example:: + + @client.topic_callback("mytopic/#") + def handle_mytopic(client, userdata, message): + ... + """ + if callback is None or sub is None: + raise ValueError("sub and callback must both be defined.") + + with self._callback_mutex: + self._on_message_filtered[sub] = callback + + def topic_callback( + self, sub: str + ) -> Callable[[CallbackOnMessage], CallbackOnMessage]: + def decorator(func: CallbackOnMessage) -> CallbackOnMessage: + self.message_callback_add(sub, func) + return func + return decorator + + def message_callback_remove(self, sub: str) -> None: + """Remove a message callback previously registered with + `message_callback_add()`.""" + if sub is None: + raise ValueError("sub must defined.") + + with self._callback_mutex: + try: + del self._on_message_filtered[sub] + except KeyError: # no such subscription + pass + + # ============================================================ + # Private functions + # ============================================================ + + def _loop_rc_handle( + self, + rc: MQTTErrorCode, + ) -> MQTTErrorCode: + if rc: + self._sock_close() + + if self._state in (_ConnectionState.MQTT_CS_DISCONNECTING, _ConnectionState.MQTT_CS_DISCONNECTED): + self._state = _ConnectionState.MQTT_CS_DISCONNECTED + rc = MQTTErrorCode.MQTT_ERR_SUCCESS + + self._do_on_disconnect(packet_from_broker=False, v1_rc=rc) + + if rc == MQTT_ERR_CONN_LOST: + self._state = _ConnectionState.MQTT_CS_CONNECTION_LOST + + return rc + + def _packet_read(self) -> MQTTErrorCode: + # This gets called if pselect() indicates that there is network data + # available - ie. at least one byte. What we do depends on what data we + # already have. + # If we've not got a command, attempt to read one and save it. This should + # always work because it's only a single byte. + # Then try to read the remaining length. This may fail because it is may + # be more than one byte - will need to save data pending next read if it + # does fail. + # Then try to read the remaining payload, where 'payload' here means the + # combined variable header and actual payload. This is the most likely to + # fail due to longer length, so save current data and current position. + # After all data is read, send to _mqtt_handle_packet() to deal with. + # Finally, free the memory and reset everything to starting conditions. + if self._in_packet['command'] == 0: + try: + command = self._sock_recv(1) + except BlockingIOError: + return MQTTErrorCode.MQTT_ERR_AGAIN + except TimeoutError as err: + self._easy_log( + MQTT_LOG_ERR, 'timeout on socket: %s', err) + return MQTTErrorCode.MQTT_ERR_CONN_LOST + except OSError as err: + self._easy_log( + MQTT_LOG_ERR, 'failed to receive on socket: %s', err) + return MQTTErrorCode.MQTT_ERR_CONN_LOST + else: + if len(command) == 0: + return MQTTErrorCode.MQTT_ERR_CONN_LOST + self._in_packet['command'] = command[0] + + if self._in_packet['have_remaining'] == 0: + # Read remaining + # Algorithm for decoding taken from pseudo code at + # http://publib.boulder.ibm.com/infocenter/wmbhelp/v6r0m0/topic/com.ibm.etools.mft.doc/ac10870_.htm + while True: + try: + byte = self._sock_recv(1) + except BlockingIOError: + return MQTTErrorCode.MQTT_ERR_AGAIN + except OSError as err: + self._easy_log( + MQTT_LOG_ERR, 'failed to receive on socket: %s', err) + return MQTTErrorCode.MQTT_ERR_CONN_LOST + else: + if len(byte) == 0: + return MQTTErrorCode.MQTT_ERR_CONN_LOST + byte_value = byte[0] + self._in_packet['remaining_count'].append(byte_value) + # Max 4 bytes length for remaining length as defined by protocol. + # Anything more likely means a broken/malicious client. + if len(self._in_packet['remaining_count']) > 4: + return MQTTErrorCode.MQTT_ERR_PROTOCOL + + self._in_packet['remaining_length'] += ( + byte_value & 127) * self._in_packet['remaining_mult'] + self._in_packet['remaining_mult'] = self._in_packet['remaining_mult'] * 128 + + if (byte_value & 128) == 0: + break + + self._in_packet['have_remaining'] = 1 + self._in_packet['to_process'] = self._in_packet['remaining_length'] + + count = 100 # Don't get stuck in this loop if we have a huge message. + while self._in_packet['to_process'] > 0: + try: + data = self._sock_recv(self._in_packet['to_process']) + except BlockingIOError: + return MQTTErrorCode.MQTT_ERR_AGAIN + except OSError as err: + self._easy_log( + MQTT_LOG_ERR, 'failed to receive on socket: %s', err) + return MQTTErrorCode.MQTT_ERR_CONN_LOST + else: + if len(data) == 0: + return MQTTErrorCode.MQTT_ERR_CONN_LOST + self._in_packet['to_process'] -= len(data) + self._in_packet['packet'] += data + count -= 1 + if count == 0: + with self._msgtime_mutex: + self._last_msg_in = time_func() + return MQTTErrorCode.MQTT_ERR_AGAIN + + # All data for this packet is read. + self._in_packet['pos'] = 0 + rc = self._packet_handle() + + # Free data and reset values + self._in_packet = { + "command": 0, + "have_remaining": 0, + "remaining_count": [], + "remaining_mult": 1, + "remaining_length": 0, + "packet": bytearray(b""), + "to_process": 0, + "pos": 0, + } + + with self._msgtime_mutex: + self._last_msg_in = time_func() + return rc + + def _packet_write(self) -> MQTTErrorCode: + while True: + try: + packet = self._out_packet.popleft() + except IndexError: + return MQTTErrorCode.MQTT_ERR_SUCCESS + + try: + write_length = self._sock_send( + packet['packet'][packet['pos']:]) + except (AttributeError, ValueError): + self._out_packet.appendleft(packet) + return MQTTErrorCode.MQTT_ERR_SUCCESS + except BlockingIOError: + self._out_packet.appendleft(packet) + return MQTTErrorCode.MQTT_ERR_AGAIN + except OSError as err: + self._out_packet.appendleft(packet) + self._easy_log( + MQTT_LOG_ERR, 'failed to receive on socket: %s', err) + return MQTTErrorCode.MQTT_ERR_CONN_LOST + + if write_length > 0: + packet['to_process'] -= write_length + packet['pos'] += write_length + + if packet['to_process'] == 0: + if (packet['command'] & 0xF0) == PUBLISH and packet['qos'] == 0: + with self._callback_mutex: + on_publish = self.on_publish + + if on_publish: + with self._in_callback_mutex: + try: + if self._callback_api_version == CallbackAPIVersion.VERSION1: + on_publish = cast(CallbackOnPublish_v1, on_publish) + + on_publish(self, self._userdata, packet["mid"]) + elif self._callback_api_version == CallbackAPIVersion.VERSION2: + on_publish = cast(CallbackOnPublish_v2, on_publish) + + on_publish( + self, + self._userdata, + packet["mid"], + ReasonCode(PacketTypes.PUBACK), + Properties(PacketTypes.PUBACK), + ) + else: + raise RuntimeError("Unsupported callback API version") + except Exception as err: + self._easy_log( + MQTT_LOG_ERR, 'Caught exception in on_publish: %s', err) + if not self.suppress_exceptions: + raise + + # TODO: Something is odd here. I don't see why packet["info"] can't be None. + # A packet could be produced by _handle_connack with qos=0 and no info + # (around line 3645). Ignore the mypy check for now but I feel there is a bug + # somewhere. + packet['info']._set_as_published() # type: ignore + + if (packet['command'] & 0xF0) == DISCONNECT: + with self._msgtime_mutex: + self._last_msg_out = time_func() + + self._do_on_disconnect( + packet_from_broker=False, + v1_rc=MQTTErrorCode.MQTT_ERR_SUCCESS, + ) + self._sock_close() + # Only change to disconnected if the disconnection was wanted + # by the client (== state was disconnecting). If the broker disconnected + # use unilaterally don't change the state and client may reconnect. + if self._state == _ConnectionState.MQTT_CS_DISCONNECTING: + self._state = _ConnectionState.MQTT_CS_DISCONNECTED + return MQTTErrorCode.MQTT_ERR_SUCCESS + + else: + # We haven't finished with this packet + self._out_packet.appendleft(packet) + else: + break + + with self._msgtime_mutex: + self._last_msg_out = time_func() + + return MQTTErrorCode.MQTT_ERR_SUCCESS + + def _easy_log(self, level: LogLevel, fmt: str, *args: Any) -> None: + if self.on_log is not None: + buf = fmt % args + try: + self.on_log(self, self._userdata, level, buf) + except Exception: # noqa: S110 + # Can't _easy_log this, as we'll recurse until we break + pass # self._logger will pick this up, so we're fine + if self._logger is not None: + level_std = LOGGING_LEVEL[level] + self._logger.log(level_std, fmt, *args) + + def _check_keepalive(self) -> None: + if self._keepalive == 0: + return + + now = time_func() + + with self._msgtime_mutex: + last_msg_out = self._last_msg_out + last_msg_in = self._last_msg_in + + if self._sock is not None and (now - last_msg_out >= self._keepalive or now - last_msg_in >= self._keepalive): + if self._state == _ConnectionState.MQTT_CS_CONNECTED and self._ping_t == 0: + try: + self._send_pingreq() + except Exception: + self._sock_close() + self._do_on_disconnect( + packet_from_broker=False, + v1_rc=MQTTErrorCode.MQTT_ERR_CONN_LOST, + ) + else: + with self._msgtime_mutex: + self._last_msg_out = now + self._last_msg_in = now + else: + self._sock_close() + + if self._state in (_ConnectionState.MQTT_CS_DISCONNECTING, _ConnectionState.MQTT_CS_DISCONNECTED): + self._state = _ConnectionState.MQTT_CS_DISCONNECTED + rc = MQTTErrorCode.MQTT_ERR_SUCCESS + else: + rc = MQTTErrorCode.MQTT_ERR_KEEPALIVE + + self._do_on_disconnect( + packet_from_broker=False, + v1_rc=rc, + ) + + def _mid_generate(self) -> int: + with self._mid_generate_mutex: + self._last_mid += 1 + if self._last_mid == 65536: + self._last_mid = 1 + return self._last_mid + + @staticmethod + def _raise_for_invalid_topic(topic: bytes) -> None: + """ Check if the topic is a topic without wildcard and valid length. + + Raise ValueError if the topic isn't valid. + """ + if b'+' in topic or b'#' in topic: + raise ValueError('Publish topic cannot contain wildcards.') + if len(topic) > 65535: + raise ValueError('Publish topic is too long.') + + @staticmethod + def _filter_wildcard_len_check(sub: bytes) -> MQTTErrorCode: + if (len(sub) == 0 or len(sub) > 65535 + or any(b'+' in p or b'#' in p for p in sub.split(b'/') if len(p) > 1) + or b'#/' in sub): + return MQTTErrorCode.MQTT_ERR_INVAL + else: + return MQTTErrorCode.MQTT_ERR_SUCCESS + + def _send_pingreq(self) -> MQTTErrorCode: + self._easy_log(MQTT_LOG_DEBUG, "Sending PINGREQ") + rc = self._send_simple_command(PINGREQ) + if rc == MQTTErrorCode.MQTT_ERR_SUCCESS: + self._ping_t = time_func() + return rc + + def _send_pingresp(self) -> MQTTErrorCode: + self._easy_log(MQTT_LOG_DEBUG, "Sending PINGRESP") + return self._send_simple_command(PINGRESP) + + def _send_puback(self, mid: int) -> MQTTErrorCode: + self._easy_log(MQTT_LOG_DEBUG, "Sending PUBACK (Mid: %d)", mid) + return self._send_command_with_mid(PUBACK, mid, False) + + def _send_pubcomp(self, mid: int) -> MQTTErrorCode: + self._easy_log(MQTT_LOG_DEBUG, "Sending PUBCOMP (Mid: %d)", mid) + return self._send_command_with_mid(PUBCOMP, mid, False) + + def _pack_remaining_length( + self, packet: bytearray, remaining_length: int + ) -> bytearray: + remaining_bytes = [] + while True: + byte = remaining_length % 128 + remaining_length = remaining_length // 128 + # If there are more digits to encode, set the top bit of this digit + if remaining_length > 0: + byte |= 0x80 + + remaining_bytes.append(byte) + packet.append(byte) + if remaining_length == 0: + # FIXME - this doesn't deal with incorrectly large payloads + return packet + + def _pack_str16(self, packet: bytearray, data: bytes | str) -> None: + data = _force_bytes(data) + packet.extend(struct.pack("!H", len(data))) + packet.extend(data) + + def _send_publish( + self, + mid: int, + topic: bytes, + payload: bytes|bytearray = b"", + qos: int = 0, + retain: bool = False, + dup: bool = False, + info: MQTTMessageInfo | None = None, + properties: Properties | None = None, + ) -> MQTTErrorCode: + # we assume that topic and payload are already properly encoded + if not isinstance(topic, bytes): + raise TypeError('topic must be bytes, not str') + if payload and not isinstance(payload, (bytes, bytearray)): + raise TypeError('payload must be bytes if set') + + if self._sock is None: + return MQTTErrorCode.MQTT_ERR_NO_CONN + + command = PUBLISH | ((dup & 0x1) << 3) | (qos << 1) | retain + packet = bytearray() + packet.append(command) + + payloadlen = len(payload) + remaining_length = 2 + len(topic) + payloadlen + + if payloadlen == 0: + if self._protocol == MQTTv5: + self._easy_log( + MQTT_LOG_DEBUG, + "Sending PUBLISH (d%d, q%d, r%d, m%d), '%s', properties=%s (NULL payload)", + dup, qos, retain, mid, topic, properties + ) + else: + self._easy_log( + MQTT_LOG_DEBUG, + "Sending PUBLISH (d%d, q%d, r%d, m%d), '%s' (NULL payload)", + dup, qos, retain, mid, topic + ) + else: + if self._protocol == MQTTv5: + self._easy_log( + MQTT_LOG_DEBUG, + "Sending PUBLISH (d%d, q%d, r%d, m%d), '%s', properties=%s, ... (%d bytes)", + dup, qos, retain, mid, topic, properties, payloadlen + ) + else: + self._easy_log( + MQTT_LOG_DEBUG, + "Sending PUBLISH (d%d, q%d, r%d, m%d), '%s', ... (%d bytes)", + dup, qos, retain, mid, topic, payloadlen + ) + + if qos > 0: + # For message id + remaining_length += 2 + + if self._protocol == MQTTv5: + if properties is None: + packed_properties = b'\x00' + else: + packed_properties = properties.pack() + remaining_length += len(packed_properties) + + self._pack_remaining_length(packet, remaining_length) + self._pack_str16(packet, topic) + + if qos > 0: + # For message id + packet.extend(struct.pack("!H", mid)) + + if self._protocol == MQTTv5: + packet.extend(packed_properties) + + packet.extend(payload) + + return self._packet_queue(PUBLISH, packet, mid, qos, info) + + def _send_pubrec(self, mid: int) -> MQTTErrorCode: + self._easy_log(MQTT_LOG_DEBUG, "Sending PUBREC (Mid: %d)", mid) + return self._send_command_with_mid(PUBREC, mid, False) + + def _send_pubrel(self, mid: int) -> MQTTErrorCode: + self._easy_log(MQTT_LOG_DEBUG, "Sending PUBREL (Mid: %d)", mid) + return self._send_command_with_mid(PUBREL | 2, mid, False) + + def _send_command_with_mid(self, command: int, mid: int, dup: int) -> MQTTErrorCode: + # For PUBACK, PUBCOMP, PUBREC, and PUBREL + if dup: + command |= 0x8 + + remaining_length = 2 + packet = struct.pack('!BBH', command, remaining_length, mid) + return self._packet_queue(command, packet, mid, 1) + + def _send_simple_command(self, command: int) -> MQTTErrorCode: + # For DISCONNECT, PINGREQ and PINGRESP + remaining_length = 0 + packet = struct.pack('!BB', command, remaining_length) + return self._packet_queue(command, packet, 0, 0) + + def _send_connect(self, keepalive: int) -> MQTTErrorCode: + proto_ver = int(self._protocol) + # hard-coded UTF-8 encoded string + protocol = b"MQTT" if proto_ver >= MQTTv311 else b"MQIsdp" + + remaining_length = 2 + len(protocol) + 1 + \ + 1 + 2 + 2 + len(self._client_id) + + connect_flags = 0 + if self._protocol == MQTTv5: + if self._clean_start is True: + connect_flags |= 0x02 + elif self._clean_start == MQTT_CLEAN_START_FIRST_ONLY and self._mqttv5_first_connect: + connect_flags |= 0x02 + elif self._clean_session: + connect_flags |= 0x02 + + if self._will: + remaining_length += 2 + \ + len(self._will_topic) + 2 + len(self._will_payload) + connect_flags |= 0x04 | ((self._will_qos & 0x03) << 3) | ( + (self._will_retain & 0x01) << 5) + + if self._username is not None: + remaining_length += 2 + len(self._username) + connect_flags |= 0x80 + if self._password is not None: + connect_flags |= 0x40 + remaining_length += 2 + len(self._password) + + if self._protocol == MQTTv5: + if self._connect_properties is None: + packed_connect_properties = b'\x00' + else: + packed_connect_properties = self._connect_properties.pack() + remaining_length += len(packed_connect_properties) + if self._will: + if self._will_properties is None: + packed_will_properties = b'\x00' + else: + packed_will_properties = self._will_properties.pack() + remaining_length += len(packed_will_properties) + + command = CONNECT + packet = bytearray() + packet.append(command) + + # as per the mosquitto broker, if the MSB of this version is set + # to 1, then it treats the connection as a bridge + if self._client_mode == MQTT_BRIDGE: + proto_ver |= 0x80 + + self._pack_remaining_length(packet, remaining_length) + packet.extend(struct.pack( + f"!H{len(protocol)}sBBH", + len(protocol), protocol, proto_ver, connect_flags, keepalive, + )) + + if self._protocol == MQTTv5: + packet += packed_connect_properties + + self._pack_str16(packet, self._client_id) + + if self._will: + if self._protocol == MQTTv5: + packet += packed_will_properties + self._pack_str16(packet, self._will_topic) + self._pack_str16(packet, self._will_payload) + + if self._username is not None: + self._pack_str16(packet, self._username) + + if self._password is not None: + self._pack_str16(packet, self._password) + + self._keepalive = keepalive + if self._protocol == MQTTv5: + self._easy_log( + MQTT_LOG_DEBUG, + "Sending CONNECT (u%d, p%d, wr%d, wq%d, wf%d, c%d, k%d) client_id=%s properties=%s", + (connect_flags & 0x80) >> 7, + (connect_flags & 0x40) >> 6, + (connect_flags & 0x20) >> 5, + (connect_flags & 0x18) >> 3, + (connect_flags & 0x4) >> 2, + (connect_flags & 0x2) >> 1, + keepalive, + self._client_id, + self._connect_properties + ) + else: + self._easy_log( + MQTT_LOG_DEBUG, + "Sending CONNECT (u%d, p%d, wr%d, wq%d, wf%d, c%d, k%d) client_id=%s", + (connect_flags & 0x80) >> 7, + (connect_flags & 0x40) >> 6, + (connect_flags & 0x20) >> 5, + (connect_flags & 0x18) >> 3, + (connect_flags & 0x4) >> 2, + (connect_flags & 0x2) >> 1, + keepalive, + self._client_id + ) + return self._packet_queue(command, packet, 0, 0) + + def _send_disconnect( + self, + reasoncode: ReasonCode | None = None, + properties: Properties | None = None, + ) -> MQTTErrorCode: + if self._protocol == MQTTv5: + self._easy_log(MQTT_LOG_DEBUG, "Sending DISCONNECT reasonCode=%s properties=%s", + reasoncode, + properties + ) + else: + self._easy_log(MQTT_LOG_DEBUG, "Sending DISCONNECT") + + remaining_length = 0 + + command = DISCONNECT + packet = bytearray() + packet.append(command) + + if self._protocol == MQTTv5: + if properties is not None or reasoncode is not None: + if reasoncode is None: + reasoncode = ReasonCode(DISCONNECT >> 4, identifier=0) + remaining_length += 1 + if properties is not None: + packed_props = properties.pack() + remaining_length += len(packed_props) + + self._pack_remaining_length(packet, remaining_length) + + if self._protocol == MQTTv5: + if reasoncode is not None: + packet += reasoncode.pack() + if properties is not None: + packet += packed_props + + return self._packet_queue(command, packet, 0, 0) + + def _send_subscribe( + self, + dup: int, + topics: Sequence[tuple[bytes, SubscribeOptions | int]], + properties: Properties | None = None, + ) -> tuple[MQTTErrorCode, int]: + remaining_length = 2 + if self._protocol == MQTTv5: + if properties is None: + packed_subscribe_properties = b'\x00' + else: + packed_subscribe_properties = properties.pack() + remaining_length += len(packed_subscribe_properties) + for t, _ in topics: + remaining_length += 2 + len(t) + 1 + + command = SUBSCRIBE | (dup << 3) | 0x2 + packet = bytearray() + packet.append(command) + self._pack_remaining_length(packet, remaining_length) + local_mid = self._mid_generate() + packet.extend(struct.pack("!H", local_mid)) + + if self._protocol == MQTTv5: + packet += packed_subscribe_properties + + for t, q in topics: + self._pack_str16(packet, t) + if self._protocol == MQTTv5: + packet += q.pack() # type: ignore + else: + packet.append(q) # type: ignore + + self._easy_log( + MQTT_LOG_DEBUG, + "Sending SUBSCRIBE (d%d, m%d) %s", + dup, + local_mid, + topics, + ) + return (self._packet_queue(command, packet, local_mid, 1), local_mid) + + def _send_unsubscribe( + self, + dup: int, + topics: list[bytes], + properties: Properties | None = None, + ) -> tuple[MQTTErrorCode, int]: + remaining_length = 2 + if self._protocol == MQTTv5: + if properties is None: + packed_unsubscribe_properties = b'\x00' + else: + packed_unsubscribe_properties = properties.pack() + remaining_length += len(packed_unsubscribe_properties) + for t in topics: + remaining_length += 2 + len(t) + + command = UNSUBSCRIBE | (dup << 3) | 0x2 + packet = bytearray() + packet.append(command) + self._pack_remaining_length(packet, remaining_length) + local_mid = self._mid_generate() + packet.extend(struct.pack("!H", local_mid)) + + if self._protocol == MQTTv5: + packet += packed_unsubscribe_properties + + for t in topics: + self._pack_str16(packet, t) + + # topics_repr = ", ".join("'"+topic.decode('utf8')+"'" for topic in topics) + if self._protocol == MQTTv5: + self._easy_log( + MQTT_LOG_DEBUG, + "Sending UNSUBSCRIBE (d%d, m%d) %s %s", + dup, + local_mid, + properties, + topics, + ) + else: + self._easy_log( + MQTT_LOG_DEBUG, + "Sending UNSUBSCRIBE (d%d, m%d) %s", + dup, + local_mid, + topics, + ) + return (self._packet_queue(command, packet, local_mid, 1), local_mid) + + def _check_clean_session(self) -> bool: + if self._protocol == MQTTv5: + if self._clean_start == MQTT_CLEAN_START_FIRST_ONLY: + return self._mqttv5_first_connect + else: + return self._clean_start # type: ignore + else: + return self._clean_session + + def _messages_reconnect_reset_out(self) -> None: + with self._out_message_mutex: + self._inflight_messages = 0 + for m in self._out_messages.values(): + m.timestamp = 0 + if self._max_inflight_messages == 0 or self._inflight_messages < self._max_inflight_messages: + if m.qos == 0: + m.state = mqtt_ms_publish + elif m.qos == 1: + # self._inflight_messages = self._inflight_messages + 1 + if m.state == mqtt_ms_wait_for_puback: + m.dup = True + m.state = mqtt_ms_publish + elif m.qos == 2: + # self._inflight_messages = self._inflight_messages + 1 + if self._check_clean_session(): + if m.state != mqtt_ms_publish: + m.dup = True + m.state = mqtt_ms_publish + else: + if m.state == mqtt_ms_wait_for_pubcomp: + m.state = mqtt_ms_resend_pubrel + else: + if m.state == mqtt_ms_wait_for_pubrec: + m.dup = True + m.state = mqtt_ms_publish + else: + m.state = mqtt_ms_queued + + def _messages_reconnect_reset_in(self) -> None: + with self._in_message_mutex: + if self._check_clean_session(): + self._in_messages = collections.OrderedDict() + return + for m in self._in_messages.values(): + m.timestamp = 0 + if m.qos != 2: + self._in_messages.pop(m.mid) + else: + # Preserve current state + pass + + def _messages_reconnect_reset(self) -> None: + self._messages_reconnect_reset_out() + self._messages_reconnect_reset_in() + + def _packet_queue( + self, + command: int, + packet: bytes, + mid: int, + qos: int, + info: MQTTMessageInfo | None = None, + ) -> MQTTErrorCode: + mpkt: _OutPacket = { + "command": command, + "mid": mid, + "qos": qos, + "pos": 0, + "to_process": len(packet), + "packet": packet, + "info": info, + } + + self._out_packet.append(mpkt) + + # Write a single byte to sockpairW (connected to sockpairR) to break + # out of select() if in threaded mode. + if self._sockpairW is not None: + try: + self._sockpairW.send(sockpair_data) + except BlockingIOError: + pass + + # If we have an external event loop registered, use that instead + # of calling loop_write() directly. + if self._thread is None and self._on_socket_register_write is None: + if self._in_callback_mutex.acquire(False): + self._in_callback_mutex.release() + return self.loop_write() + + self._call_socket_register_write() + + return MQTTErrorCode.MQTT_ERR_SUCCESS + + def _packet_handle(self) -> MQTTErrorCode: + cmd = self._in_packet['command'] & 0xF0 + if cmd == PINGREQ: + return self._handle_pingreq() + elif cmd == PINGRESP: + return self._handle_pingresp() + elif cmd == PUBACK: + return self._handle_pubackcomp("PUBACK") + elif cmd == PUBCOMP: + return self._handle_pubackcomp("PUBCOMP") + elif cmd == PUBLISH: + return self._handle_publish() + elif cmd == PUBREC: + return self._handle_pubrec() + elif cmd == PUBREL: + return self._handle_pubrel() + elif cmd == CONNACK: + return self._handle_connack() + elif cmd == SUBACK: + self._handle_suback() + return MQTTErrorCode.MQTT_ERR_SUCCESS + elif cmd == UNSUBACK: + return self._handle_unsuback() + elif cmd == DISCONNECT and self._protocol == MQTTv5: # only allowed in MQTT 5.0 + self._handle_disconnect() + return MQTTErrorCode.MQTT_ERR_SUCCESS + else: + # If we don't recognise the command, return an error straight away. + self._easy_log(MQTT_LOG_ERR, "Error: Unrecognised command %s", cmd) + return MQTTErrorCode.MQTT_ERR_PROTOCOL + + def _handle_pingreq(self) -> MQTTErrorCode: + if self._in_packet['remaining_length'] != 0: + return MQTTErrorCode.MQTT_ERR_PROTOCOL + + self._easy_log(MQTT_LOG_DEBUG, "Received PINGREQ") + return self._send_pingresp() + + def _handle_pingresp(self) -> MQTTErrorCode: + if self._in_packet['remaining_length'] != 0: + return MQTTErrorCode.MQTT_ERR_PROTOCOL + + # No longer waiting for a PINGRESP. + self._ping_t = 0 + self._easy_log(MQTT_LOG_DEBUG, "Received PINGRESP") + return MQTTErrorCode.MQTT_ERR_SUCCESS + + def _handle_connack(self) -> MQTTErrorCode: + if self._protocol == MQTTv5: + if self._in_packet['remaining_length'] < 2: + return MQTTErrorCode.MQTT_ERR_PROTOCOL + elif self._in_packet['remaining_length'] != 2: + return MQTTErrorCode.MQTT_ERR_PROTOCOL + + if self._protocol == MQTTv5: + (flags, result) = struct.unpack( + "!BB", self._in_packet['packet'][:2]) + if result == 1: + # This is probably a failure from a broker that doesn't support + # MQTT v5. + reason = ReasonCode(CONNACK >> 4, aName="Unsupported protocol version") + properties = None + else: + reason = ReasonCode(CONNACK >> 4, identifier=result) + properties = Properties(CONNACK >> 4) + properties.unpack(self._in_packet['packet'][2:]) + else: + (flags, result) = struct.unpack("!BB", self._in_packet['packet']) + reason = convert_connack_rc_to_reason_code(result) + properties = None + if self._protocol == MQTTv311: + if result == CONNACK_REFUSED_PROTOCOL_VERSION: + if not self._reconnect_on_failure: + return MQTT_ERR_PROTOCOL + self._easy_log( + MQTT_LOG_DEBUG, + "Received CONNACK (%s, %s), attempting downgrade to MQTT v3.1.", + flags, result + ) + # Downgrade to MQTT v3.1 + self._protocol = MQTTv31 + return self.reconnect() + elif (result == CONNACK_REFUSED_IDENTIFIER_REJECTED + and self._client_id == b''): + if not self._reconnect_on_failure: + return MQTT_ERR_PROTOCOL + self._easy_log( + MQTT_LOG_DEBUG, + "Received CONNACK (%s, %s), attempting to use non-empty CID", + flags, result, + ) + self._client_id = _base62(uuid.uuid4().int, padding=22).encode("utf8") + return self.reconnect() + + if result == 0: + self._state = _ConnectionState.MQTT_CS_CONNECTED + self._reconnect_delay = None + + if self._protocol == MQTTv5: + self._easy_log( + MQTT_LOG_DEBUG, "Received CONNACK (%s, %s) properties=%s", flags, reason, properties) + else: + self._easy_log( + MQTT_LOG_DEBUG, "Received CONNACK (%s, %s)", flags, result) + + # it won't be the first successful connect any more + self._mqttv5_first_connect = False + + with self._callback_mutex: + on_connect = self.on_connect + + if on_connect: + flags_dict = {} + flags_dict['session present'] = flags & 0x01 + with self._in_callback_mutex: + try: + if self._callback_api_version == CallbackAPIVersion.VERSION1: + if self._protocol == MQTTv5: + on_connect = cast(CallbackOnConnect_v1_mqtt5, on_connect) + + on_connect(self, self._userdata, + flags_dict, reason, properties) + else: + on_connect = cast(CallbackOnConnect_v1_mqtt3, on_connect) + + on_connect( + self, self._userdata, flags_dict, result) + elif self._callback_api_version == CallbackAPIVersion.VERSION2: + on_connect = cast(CallbackOnConnect_v2, on_connect) + + connect_flags = ConnectFlags( + session_present=flags_dict['session present'] > 0 + ) + + if properties is None: + properties = Properties(PacketTypes.CONNACK) + + on_connect( + self, + self._userdata, + connect_flags, + reason, + properties, + ) + else: + raise RuntimeError("Unsupported callback API version") + except Exception as err: + self._easy_log( + MQTT_LOG_ERR, 'Caught exception in on_connect: %s', err) + if not self.suppress_exceptions: + raise + + if result == 0: + rc = MQTTErrorCode.MQTT_ERR_SUCCESS + with self._out_message_mutex: + for m in self._out_messages.values(): + m.timestamp = time_func() + if m.state == mqtt_ms_queued: + self.loop_write() # Process outgoing messages that have just been queued up + return MQTT_ERR_SUCCESS + + if m.qos == 0: + with self._in_callback_mutex: # Don't call loop_write after _send_publish() + rc = self._send_publish( + m.mid, + m.topic.encode('utf-8'), + m.payload, + m.qos, + m.retain, + m.dup, + properties=m.properties + ) + if rc != MQTTErrorCode.MQTT_ERR_SUCCESS: + return rc + elif m.qos == 1: + if m.state == mqtt_ms_publish: + self._inflight_messages += 1 + m.state = mqtt_ms_wait_for_puback + with self._in_callback_mutex: # Don't call loop_write after _send_publish() + rc = self._send_publish( + m.mid, + m.topic.encode('utf-8'), + m.payload, + m.qos, + m.retain, + m.dup, + properties=m.properties + ) + if rc != MQTTErrorCode.MQTT_ERR_SUCCESS: + return rc + elif m.qos == 2: + if m.state == mqtt_ms_publish: + self._inflight_messages += 1 + m.state = mqtt_ms_wait_for_pubrec + with self._in_callback_mutex: # Don't call loop_write after _send_publish() + rc = self._send_publish( + m.mid, + m.topic.encode('utf-8'), + m.payload, + m.qos, + m.retain, + m.dup, + properties=m.properties + ) + if rc != MQTTErrorCode.MQTT_ERR_SUCCESS: + return rc + elif m.state == mqtt_ms_resend_pubrel: + self._inflight_messages += 1 + m.state = mqtt_ms_wait_for_pubcomp + with self._in_callback_mutex: # Don't call loop_write after _send_publish() + rc = self._send_pubrel(m.mid) + if rc != MQTTErrorCode.MQTT_ERR_SUCCESS: + return rc + self.loop_write() # Process outgoing messages that have just been queued up + + return rc + elif result > 0 and result < 6: + return MQTTErrorCode.MQTT_ERR_CONN_REFUSED + else: + return MQTTErrorCode.MQTT_ERR_PROTOCOL + + def _handle_disconnect(self) -> None: + packet_type = DISCONNECT >> 4 + reasonCode = properties = None + if self._in_packet['remaining_length'] > 2: + reasonCode = ReasonCode(packet_type) + reasonCode.unpack(self._in_packet['packet']) + if self._in_packet['remaining_length'] > 3: + properties = Properties(packet_type) + props, props_len = properties.unpack( + self._in_packet['packet'][1:]) + self._easy_log(MQTT_LOG_DEBUG, "Received DISCONNECT %s %s", + reasonCode, + properties + ) + + self._sock_close() + self._do_on_disconnect( + packet_from_broker=True, + v1_rc=MQTTErrorCode.MQTT_ERR_SUCCESS, # If reason is absent (remaining length < 1), it means normal disconnection + reason=reasonCode, + properties=properties, + ) + + def _handle_suback(self) -> None: + self._easy_log(MQTT_LOG_DEBUG, "Received SUBACK") + pack_format = f"!H{len(self._in_packet['packet']) - 2}s" + (mid, packet) = struct.unpack(pack_format, self._in_packet['packet']) + + if self._protocol == MQTTv5: + properties = Properties(SUBACK >> 4) + props, props_len = properties.unpack(packet) + reasoncodes = [ReasonCode(SUBACK >> 4, identifier=c) for c in packet[props_len:]] + else: + pack_format = f"!{'B' * len(packet)}" + granted_qos = struct.unpack(pack_format, packet) + reasoncodes = [ReasonCode(SUBACK >> 4, identifier=c) for c in granted_qos] + properties = Properties(SUBACK >> 4) + + with self._callback_mutex: + on_subscribe = self.on_subscribe + + if on_subscribe: + with self._in_callback_mutex: # Don't call loop_write after _send_publish() + try: + if self._callback_api_version == CallbackAPIVersion.VERSION1: + if self._protocol == MQTTv5: + on_subscribe = cast(CallbackOnSubscribe_v1_mqtt5, on_subscribe) + + on_subscribe( + self, self._userdata, mid, reasoncodes, properties) + else: + on_subscribe = cast(CallbackOnSubscribe_v1_mqtt3, on_subscribe) + + on_subscribe( + self, self._userdata, mid, granted_qos) + elif self._callback_api_version == CallbackAPIVersion.VERSION2: + on_subscribe = cast(CallbackOnSubscribe_v2, on_subscribe) + + on_subscribe( + self, + self._userdata, + mid, + reasoncodes, + properties, + ) + else: + raise RuntimeError("Unsupported callback API version") + except Exception as err: + self._easy_log( + MQTT_LOG_ERR, 'Caught exception in on_subscribe: %s', err) + if not self.suppress_exceptions: + raise + + def _handle_publish(self) -> MQTTErrorCode: + header = self._in_packet['command'] + message = MQTTMessage() + message.dup = ((header & 0x08) >> 3) != 0 + message.qos = (header & 0x06) >> 1 + message.retain = (header & 0x01) != 0 + + pack_format = f"!H{len(self._in_packet['packet']) - 2}s" + (slen, packet) = struct.unpack(pack_format, self._in_packet['packet']) + pack_format = f"!{slen}s{len(packet) - slen}s" + (topic, packet) = struct.unpack(pack_format, packet) + + if self._protocol != MQTTv5 and len(topic) == 0: + return MQTTErrorCode.MQTT_ERR_PROTOCOL + + # Handle topics with invalid UTF-8 + # This replaces an invalid topic with a message and the hex + # representation of the topic for logging. When the user attempts to + # access message.topic in the callback, an exception will be raised. + try: + print_topic = topic.decode('utf-8') + except UnicodeDecodeError: + print_topic = f"TOPIC WITH INVALID UTF-8: {topic!r}" + + message.topic = topic + + if message.qos > 0: + pack_format = f"!H{len(packet) - 2}s" + (message.mid, packet) = struct.unpack(pack_format, packet) + + if self._protocol == MQTTv5: + message.properties = Properties(PUBLISH >> 4) + props, props_len = message.properties.unpack(packet) + packet = packet[props_len:] + + message.payload = packet + + if self._protocol == MQTTv5: + self._easy_log( + MQTT_LOG_DEBUG, + "Received PUBLISH (d%d, q%d, r%d, m%d), '%s', properties=%s, ... (%d bytes)", + message.dup, message.qos, message.retain, message.mid, + print_topic, message.properties, len(message.payload) + ) + else: + self._easy_log( + MQTT_LOG_DEBUG, + "Received PUBLISH (d%d, q%d, r%d, m%d), '%s', ... (%d bytes)", + message.dup, message.qos, message.retain, message.mid, + print_topic, len(message.payload) + ) + + message.timestamp = time_func() + if message.qos == 0: + self._handle_on_message(message) + return MQTTErrorCode.MQTT_ERR_SUCCESS + elif message.qos == 1: + self._handle_on_message(message) + if self._manual_ack: + return MQTTErrorCode.MQTT_ERR_SUCCESS + else: + return self._send_puback(message.mid) + elif message.qos == 2: + + rc = self._send_pubrec(message.mid) + + message.state = mqtt_ms_wait_for_pubrel + with self._in_message_mutex: + self._in_messages[message.mid] = message + + return rc + else: + return MQTTErrorCode.MQTT_ERR_PROTOCOL + + def ack(self, mid: int, qos: int) -> MQTTErrorCode: + """ + send an acknowledgement for a given message id (stored in :py:attr:`message.mid `). + only useful in QoS>=1 and ``manual_ack=True`` (option of `Client`) + """ + if self._manual_ack : + if qos == 1: + return self._send_puback(mid) + elif qos == 2: + return self._send_pubcomp(mid) + + return MQTTErrorCode.MQTT_ERR_SUCCESS + + def manual_ack_set(self, on: bool) -> None: + """ + The paho library normally acknowledges messages as soon as they are delivered to the caller. + If manual_ack is turned on, then the caller MUST manually acknowledge every message once + application processing is complete using `ack()` + """ + self._manual_ack = on + + + def _handle_pubrel(self) -> MQTTErrorCode: + if self._protocol == MQTTv5: + if self._in_packet['remaining_length'] < 2: + return MQTTErrorCode.MQTT_ERR_PROTOCOL + elif self._in_packet['remaining_length'] != 2: + return MQTTErrorCode.MQTT_ERR_PROTOCOL + + mid, = struct.unpack("!H", self._in_packet['packet'][:2]) + if self._protocol == MQTTv5: + if self._in_packet['remaining_length'] > 2: + reasonCode = ReasonCode(PUBREL >> 4) + reasonCode.unpack(self._in_packet['packet'][2:]) + if self._in_packet['remaining_length'] > 3: + properties = Properties(PUBREL >> 4) + props, props_len = properties.unpack( + self._in_packet['packet'][3:]) + self._easy_log(MQTT_LOG_DEBUG, "Received PUBREL (Mid: %d)", mid) + + with self._in_message_mutex: + if mid in self._in_messages: + # Only pass the message on if we have removed it from the queue - this + # prevents multiple callbacks for the same message. + message = self._in_messages.pop(mid) + self._handle_on_message(message) + self._inflight_messages -= 1 + if self._max_inflight_messages > 0: + with self._out_message_mutex: + rc = self._update_inflight() + if rc != MQTTErrorCode.MQTT_ERR_SUCCESS: + return rc + + # FIXME: this should only be done if the message is known + # If unknown it's a protocol error and we should close the connection. + # But since we don't have (on disk) persistence for the session, it + # is possible that we must known about this message. + # Choose to acknowledge this message (thus losing a message) but + # avoid hanging. See #284. + if self._manual_ack: + return MQTTErrorCode.MQTT_ERR_SUCCESS + else: + return self._send_pubcomp(mid) + + def _update_inflight(self) -> MQTTErrorCode: + # Dont lock message_mutex here + for m in self._out_messages.values(): + if self._inflight_messages < self._max_inflight_messages: + if m.qos > 0 and m.state == mqtt_ms_queued: + self._inflight_messages += 1 + if m.qos == 1: + m.state = mqtt_ms_wait_for_puback + elif m.qos == 2: + m.state = mqtt_ms_wait_for_pubrec + rc = self._send_publish( + m.mid, + m.topic.encode('utf-8'), + m.payload, + m.qos, + m.retain, + m.dup, + properties=m.properties, + ) + if rc != MQTTErrorCode.MQTT_ERR_SUCCESS: + return rc + else: + return MQTTErrorCode.MQTT_ERR_SUCCESS + return MQTTErrorCode.MQTT_ERR_SUCCESS + + def _handle_pubrec(self) -> MQTTErrorCode: + if self._protocol == MQTTv5: + if self._in_packet['remaining_length'] < 2: + return MQTTErrorCode.MQTT_ERR_PROTOCOL + elif self._in_packet['remaining_length'] != 2: + return MQTTErrorCode.MQTT_ERR_PROTOCOL + + mid, = struct.unpack("!H", self._in_packet['packet'][:2]) + if self._protocol == MQTTv5: + if self._in_packet['remaining_length'] > 2: + reasonCode = ReasonCode(PUBREC >> 4) + reasonCode.unpack(self._in_packet['packet'][2:]) + if self._in_packet['remaining_length'] > 3: + properties = Properties(PUBREC >> 4) + props, props_len = properties.unpack( + self._in_packet['packet'][3:]) + self._easy_log(MQTT_LOG_DEBUG, "Received PUBREC (Mid: %d)", mid) + + with self._out_message_mutex: + if mid in self._out_messages: + msg = self._out_messages[mid] + msg.state = mqtt_ms_wait_for_pubcomp + msg.timestamp = time_func() + return self._send_pubrel(mid) + + return MQTTErrorCode.MQTT_ERR_SUCCESS + + def _handle_unsuback(self) -> MQTTErrorCode: + if self._protocol == MQTTv5: + if self._in_packet['remaining_length'] < 4: + return MQTTErrorCode.MQTT_ERR_PROTOCOL + elif self._in_packet['remaining_length'] != 2: + return MQTTErrorCode.MQTT_ERR_PROTOCOL + + mid, = struct.unpack("!H", self._in_packet['packet'][:2]) + if self._protocol == MQTTv5: + packet = self._in_packet['packet'][2:] + properties = Properties(UNSUBACK >> 4) + props, props_len = properties.unpack(packet) + reasoncodes_list = [ + ReasonCode(UNSUBACK >> 4, identifier=c) + for c in packet[props_len:] + ] + else: + reasoncodes_list = [] + properties = Properties(UNSUBACK >> 4) + + self._easy_log(MQTT_LOG_DEBUG, "Received UNSUBACK (Mid: %d)", mid) + with self._callback_mutex: + on_unsubscribe = self.on_unsubscribe + + if on_unsubscribe: + with self._in_callback_mutex: + try: + if self._callback_api_version == CallbackAPIVersion.VERSION1: + if self._protocol == MQTTv5: + on_unsubscribe = cast(CallbackOnUnsubscribe_v1_mqtt5, on_unsubscribe) + + reasoncodes: ReasonCode | list[ReasonCode] = reasoncodes_list + if len(reasoncodes_list) == 1: + reasoncodes = reasoncodes_list[0] + + on_unsubscribe( + self, self._userdata, mid, properties, reasoncodes) + else: + on_unsubscribe = cast(CallbackOnUnsubscribe_v1_mqtt3, on_unsubscribe) + + on_unsubscribe(self, self._userdata, mid) + elif self._callback_api_version == CallbackAPIVersion.VERSION2: + on_unsubscribe = cast(CallbackOnUnsubscribe_v2, on_unsubscribe) + + if properties is None: + properties = Properties(PacketTypes.CONNACK) + + on_unsubscribe( + self, + self._userdata, + mid, + reasoncodes_list, + properties, + ) + else: + raise RuntimeError("Unsupported callback API version") + except Exception as err: + self._easy_log( + MQTT_LOG_ERR, 'Caught exception in on_unsubscribe: %s', err) + if not self.suppress_exceptions: + raise + + return MQTTErrorCode.MQTT_ERR_SUCCESS + + def _do_on_disconnect( + self, + packet_from_broker: bool, + v1_rc: MQTTErrorCode, + reason: ReasonCode | None = None, + properties: Properties | None = None, + ) -> None: + with self._callback_mutex: + on_disconnect = self.on_disconnect + + if on_disconnect: + with self._in_callback_mutex: + try: + if self._callback_api_version == CallbackAPIVersion.VERSION1: + if self._protocol == MQTTv5: + on_disconnect = cast(CallbackOnDisconnect_v1_mqtt5, on_disconnect) + + if packet_from_broker: + on_disconnect(self, self._userdata, reason, properties) + else: + on_disconnect(self, self._userdata, v1_rc, None) + else: + on_disconnect = cast(CallbackOnDisconnect_v1_mqtt3, on_disconnect) + + on_disconnect(self, self._userdata, v1_rc) + elif self._callback_api_version == CallbackAPIVersion.VERSION2: + on_disconnect = cast(CallbackOnDisconnect_v2, on_disconnect) + + disconnect_flags = DisconnectFlags( + is_disconnect_packet_from_server=packet_from_broker + ) + + if reason is None: + reason = convert_disconnect_error_code_to_reason_code(v1_rc) + + if properties is None: + properties = Properties(PacketTypes.DISCONNECT) + + on_disconnect( + self, + self._userdata, + disconnect_flags, + reason, + properties, + ) + else: + raise RuntimeError("Unsupported callback API version") + except Exception as err: + self._easy_log( + MQTT_LOG_ERR, 'Caught exception in on_disconnect: %s', err) + if not self.suppress_exceptions: + raise + + def _do_on_publish(self, mid: int, reason_code: ReasonCode, properties: Properties) -> MQTTErrorCode: + with self._callback_mutex: + on_publish = self.on_publish + + if on_publish: + with self._in_callback_mutex: + try: + if self._callback_api_version == CallbackAPIVersion.VERSION1: + on_publish = cast(CallbackOnPublish_v1, on_publish) + + on_publish(self, self._userdata, mid) + elif self._callback_api_version == CallbackAPIVersion.VERSION2: + on_publish = cast(CallbackOnPublish_v2, on_publish) + + on_publish( + self, + self._userdata, + mid, + reason_code, + properties, + ) + else: + raise RuntimeError("Unsupported callback API version") + except Exception as err: + self._easy_log( + MQTT_LOG_ERR, 'Caught exception in on_publish: %s', err) + if not self.suppress_exceptions: + raise + + msg = self._out_messages.pop(mid) + msg.info._set_as_published() + if msg.qos > 0: + self._inflight_messages -= 1 + if self._max_inflight_messages > 0: + rc = self._update_inflight() + if rc != MQTTErrorCode.MQTT_ERR_SUCCESS: + return rc + return MQTTErrorCode.MQTT_ERR_SUCCESS + + def _handle_pubackcomp( + self, cmd: Literal['PUBACK'] | Literal['PUBCOMP'] + ) -> MQTTErrorCode: + if self._protocol == MQTTv5: + if self._in_packet['remaining_length'] < 2: + return MQTTErrorCode.MQTT_ERR_PROTOCOL + elif self._in_packet['remaining_length'] != 2: + return MQTTErrorCode.MQTT_ERR_PROTOCOL + + packet_type_enum = PUBACK if cmd == "PUBACK" else PUBCOMP + packet_type = packet_type_enum.value >> 4 + mid, = struct.unpack("!H", self._in_packet['packet'][:2]) + reasonCode = ReasonCode(packet_type) + properties = Properties(packet_type) + if self._protocol == MQTTv5: + if self._in_packet['remaining_length'] > 2: + reasonCode.unpack(self._in_packet['packet'][2:]) + if self._in_packet['remaining_length'] > 3: + props, props_len = properties.unpack( + self._in_packet['packet'][3:]) + self._easy_log(MQTT_LOG_DEBUG, "Received %s (Mid: %d)", cmd, mid) + + with self._out_message_mutex: + if mid in self._out_messages: + # Only inform the client the message has been sent once. + rc = self._do_on_publish(mid, reasonCode, properties) + return rc + + return MQTTErrorCode.MQTT_ERR_SUCCESS + + def _handle_on_message(self, message: MQTTMessage) -> None: + + try: + topic = message.topic + except UnicodeDecodeError: + topic = None + + on_message_callbacks = [] + with self._callback_mutex: + if topic is not None: + on_message_callbacks = list(self._on_message_filtered.iter_match(message.topic)) + + if len(on_message_callbacks) == 0: + on_message = self.on_message + else: + on_message = None + + for callback in on_message_callbacks: + with self._in_callback_mutex: + try: + callback(self, self._userdata, message) + except Exception as err: + self._easy_log( + MQTT_LOG_ERR, + 'Caught exception in user defined callback function %s: %s', + callback.__name__, + err + ) + if not self.suppress_exceptions: + raise + + if on_message: + with self._in_callback_mutex: + try: + on_message(self, self._userdata, message) + except Exception as err: + self._easy_log( + MQTT_LOG_ERR, 'Caught exception in on_message: %s', err) + if not self.suppress_exceptions: + raise + + + def _handle_on_connect_fail(self) -> None: + with self._callback_mutex: + on_connect_fail = self.on_connect_fail + + if on_connect_fail: + with self._in_callback_mutex: + try: + on_connect_fail(self, self._userdata) + except Exception as err: + self._easy_log( + MQTT_LOG_ERR, 'Caught exception in on_connect_fail: %s', err) + + def _thread_main(self) -> None: + try: + self.loop_forever(retry_first_connection=True) + finally: + self._thread = None + + def _reconnect_wait(self) -> None: + # See reconnect_delay_set for details + now = time_func() + with self._reconnect_delay_mutex: + if self._reconnect_delay is None: + self._reconnect_delay = self._reconnect_min_delay + else: + self._reconnect_delay = min( + self._reconnect_delay * 2, + self._reconnect_max_delay, + ) + + target_time = now + self._reconnect_delay + + remaining = target_time - now + while (self._state not in (_ConnectionState.MQTT_CS_DISCONNECTING, _ConnectionState.MQTT_CS_DISCONNECTED) + and not self._thread_terminate + and remaining > 0): + + time.sleep(min(remaining, 1)) + remaining = target_time - time_func() + + @staticmethod + def _proxy_is_valid(p) -> bool: # type: ignore[no-untyped-def] + def check(t, a) -> bool: # type: ignore[no-untyped-def] + return (socks is not None and + t in {socks.HTTP, socks.SOCKS4, socks.SOCKS5} and a) + + if isinstance(p, dict): + return check(p.get("proxy_type"), p.get("proxy_addr")) + elif isinstance(p, (list, tuple)): + return len(p) == 6 and check(p[0], p[1]) + else: + return False + + def _get_proxy(self) -> dict[str, Any] | None: + if socks is None: + return None + + # First, check if the user explicitly passed us a proxy to use + if self._proxy_is_valid(self._proxy): + return self._proxy + + # Next, check for an mqtt_proxy environment variable as long as the host + # we're trying to connect to isn't listed under the no_proxy environment + # variable (matches built-in module urllib's behavior) + if not (hasattr(urllib.request, "proxy_bypass") and + urllib.request.proxy_bypass(self._host)): + env_proxies = urllib.request.getproxies() + if "mqtt" in env_proxies: + parts = urllib.parse.urlparse(env_proxies["mqtt"]) + if parts.scheme == "http": + proxy = { + "proxy_type": socks.HTTP, + "proxy_addr": parts.hostname, + "proxy_port": parts.port + } + return proxy + elif parts.scheme == "socks": + proxy = { + "proxy_type": socks.SOCKS5, + "proxy_addr": parts.hostname, + "proxy_port": parts.port + } + return proxy + + # Finally, check if the user has monkeypatched the PySocks library with + # a default proxy + socks_default = socks.get_default_proxy() + if self._proxy_is_valid(socks_default): + proxy_keys = ("proxy_type", "proxy_addr", "proxy_port", + "proxy_rdns", "proxy_username", "proxy_password") + return dict(zip(proxy_keys, socks_default)) + + # If we didn't find a proxy through any of the above methods, return + # None to indicate that the connection should be handled normally + return None + + def _create_socket(self) -> SocketLike: + if self._transport == "unix": + sock = self._create_unix_socket_connection() + else: + sock = self._create_socket_connection() + + if self._ssl: + sock = self._ssl_wrap_socket(sock) + + if self._transport == "websockets": + sock.settimeout(self._keepalive) + return _WebsocketWrapper( + socket=sock, + host=self._host, + port=self._port, + is_ssl=self._ssl, + path=self._websocket_path, + extra_headers=self._websocket_extra_headers, + ) + + return sock + + def _create_unix_socket_connection(self) -> _socket.socket: + unix_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + unix_socket.connect(self._host) + return unix_socket + + def _create_socket_connection(self) -> _socket.socket: + proxy = self._get_proxy() + addr = (self._host, self._port) + source = (self._bind_address, self._bind_port) + + if proxy: + return socks.create_connection(addr, timeout=self._connect_timeout, source_address=source, **proxy) + else: + return socket.create_connection(addr, timeout=self._connect_timeout, source_address=source) + + def _ssl_wrap_socket(self, tcp_sock: _socket.socket) -> ssl.SSLSocket: + if self._ssl_context is None: + raise ValueError( + "Impossible condition. _ssl_context should never be None if _ssl is True" + ) + + verify_host = not self._tls_insecure + try: + # Try with server_hostname, even it's not supported in certain scenarios + ssl_sock = self._ssl_context.wrap_socket( + tcp_sock, + server_hostname=self._host, + do_handshake_on_connect=False, + ) + except ssl.CertificateError: + # CertificateError is derived from ValueError + raise + except ValueError: + # Python version requires SNI in order to handle server_hostname, but SNI is not available + ssl_sock = self._ssl_context.wrap_socket( + tcp_sock, + do_handshake_on_connect=False, + ) + else: + # If SSL context has already checked hostname, then don't need to do it again + if getattr(self._ssl_context, 'check_hostname', False): # type: ignore + verify_host = False + + ssl_sock.settimeout(self._keepalive) + ssl_sock.do_handshake() + + if verify_host: + # TODO: this type error is a true error: + # error: Module has no attribute "match_hostname" [attr-defined] + # Python 3.12 no longer have this method. + ssl.match_hostname(ssl_sock.getpeercert(), self._host) # type: ignore + + return ssl_sock + +class _WebsocketWrapper: + OPCODE_CONTINUATION = 0x0 + OPCODE_TEXT = 0x1 + OPCODE_BINARY = 0x2 + OPCODE_CONNCLOSE = 0x8 + OPCODE_PING = 0x9 + OPCODE_PONG = 0xa + + def __init__( + self, + socket: socket.socket | ssl.SSLSocket, + host: str, + port: int, + is_ssl: bool, + path: str, + extra_headers: WebSocketHeaders | None, + ): + self.connected = False + + self._ssl = is_ssl + self._host = host + self._port = port + self._socket = socket + self._path = path + + self._sendbuffer = bytearray() + self._readbuffer = bytearray() + + self._requested_size = 0 + self._payload_head = 0 + self._readbuffer_head = 0 + + self._do_handshake(extra_headers) + + def __del__(self) -> None: + self._sendbuffer = bytearray() + self._readbuffer = bytearray() + + def _do_handshake(self, extra_headers: WebSocketHeaders | None) -> None: + + sec_websocket_key = uuid.uuid4().bytes + sec_websocket_key = base64.b64encode(sec_websocket_key) + + if self._ssl: + default_port = 443 + http_schema = "https" + else: + default_port = 80 + http_schema = "http" + + if default_port == self._port: + host_port = f"{self._host}" + else: + host_port = f"{self._host}:{self._port}" + + websocket_headers = { + "Host": host_port, + "Upgrade": "websocket", + "Connection": "Upgrade", + "Origin": f"{http_schema}://{host_port}", + "Sec-WebSocket-Key": sec_websocket_key.decode("utf8"), + "Sec-Websocket-Version": "13", + "Sec-Websocket-Protocol": "mqtt", + } + + # This is checked in ws_set_options so it will either be None, a + # dictionary, or a callable + if isinstance(extra_headers, dict): + websocket_headers.update(extra_headers) + elif callable(extra_headers): + websocket_headers = extra_headers(websocket_headers) + + header = "\r\n".join([ + f"GET {self._path} HTTP/1.1", + "\r\n".join(f"{i}: {j}" for i, j in websocket_headers.items()), + "\r\n", + ]).encode("utf8") + + self._socket.send(header) + + has_secret = False + has_upgrade = False + + while True: + # read HTTP response header as lines + try: + byte = self._socket.recv(1) + except ConnectionResetError: + byte = b"" + + self._readbuffer.extend(byte) + + # line end + if byte == b"\n": + if len(self._readbuffer) > 2: + # check upgrade + if b"connection" in str(self._readbuffer).lower().encode('utf-8'): + if b"upgrade" not in str(self._readbuffer).lower().encode('utf-8'): + raise WebsocketConnectionError( + "WebSocket handshake error, connection not upgraded") + else: + has_upgrade = True + + # check key hash + if b"sec-websocket-accept" in str(self._readbuffer).lower().encode('utf-8'): + GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" + + server_hash_str = self._readbuffer.decode( + 'utf-8').split(": ", 1)[1] + server_hash = server_hash_str.strip().encode('utf-8') + + client_hash_key = sec_websocket_key.decode('utf-8') + GUID + # Use of SHA-1 is OK here; it's according to the Websocket spec. + client_hash_digest = hashlib.sha1(client_hash_key.encode('utf-8')) # noqa: S324 + client_hash = base64.b64encode(client_hash_digest.digest()) + + if server_hash != client_hash: + raise WebsocketConnectionError( + "WebSocket handshake error, invalid secret key") + else: + has_secret = True + else: + # ending linebreak + break + + # reset linebuffer + self._readbuffer = bytearray() + + # connection reset + elif not byte: + raise WebsocketConnectionError("WebSocket handshake error") + + if not has_upgrade or not has_secret: + raise WebsocketConnectionError("WebSocket handshake error") + + self._readbuffer = bytearray() + self.connected = True + + def _create_frame( + self, opcode: int, data: bytearray, do_masking: int = 1 + ) -> bytearray: + header = bytearray() + length = len(data) + + mask_key = bytearray(os.urandom(4)) + mask_flag = do_masking + + # 1 << 7 is the final flag, we don't send continuated data + header.append(1 << 7 | opcode) + + if length < 126: + header.append(mask_flag << 7 | length) + + elif length < 65536: + header.append(mask_flag << 7 | 126) + header += struct.pack("!H", length) + + elif length < 0x8000000000000001: + header.append(mask_flag << 7 | 127) + header += struct.pack("!Q", length) + + else: + raise ValueError("Maximum payload size is 2^63") + + if mask_flag == 1: + for index in range(length): + data[index] ^= mask_key[index % 4] + data = mask_key + data + + return header + data + + def _buffered_read(self, length: int) -> bytearray: + + # try to recv and store needed bytes + wanted_bytes = length - (len(self._readbuffer) - self._readbuffer_head) + if wanted_bytes > 0: + + data = self._socket.recv(wanted_bytes) + + if not data: + raise ConnectionAbortedError + else: + self._readbuffer.extend(data) + + if len(data) < wanted_bytes: + raise BlockingIOError + + self._readbuffer_head += length + return self._readbuffer[self._readbuffer_head - length:self._readbuffer_head] + + def _recv_impl(self, length: int) -> bytes: + + # try to decode websocket payload part from data + try: + + self._readbuffer_head = 0 + + result = b"" + + chunk_startindex = self._payload_head + chunk_endindex = self._payload_head + length + + header1 = self._buffered_read(1) + header2 = self._buffered_read(1) + + opcode = (header1[0] & 0x0f) + maskbit = (header2[0] & 0x80) == 0x80 + lengthbits = (header2[0] & 0x7f) + payload_length = lengthbits + mask_key = None + + # read length + if lengthbits == 0x7e: + + value = self._buffered_read(2) + payload_length, = struct.unpack("!H", value) + + elif lengthbits == 0x7f: + + value = self._buffered_read(8) + payload_length, = struct.unpack("!Q", value) + + # read mask + if maskbit: + mask_key = self._buffered_read(4) + + # if frame payload is shorter than the requested data, read only the possible part + readindex = chunk_endindex + if payload_length < readindex: + readindex = payload_length + + if readindex > 0: + # get payload chunk + payload = self._buffered_read(readindex) + + # unmask only the needed part + if mask_key is not None: + for index in range(chunk_startindex, readindex): + payload[index] ^= mask_key[index % 4] + + result = payload[chunk_startindex:readindex] + self._payload_head = readindex + else: + payload = bytearray() + + # check if full frame arrived and reset readbuffer and payloadhead if needed + if readindex == payload_length: + self._readbuffer = bytearray() + self._payload_head = 0 + + # respond to non-binary opcodes, their arrival is not guaranteed because of non-blocking sockets + if opcode == _WebsocketWrapper.OPCODE_CONNCLOSE: + frame = self._create_frame( + _WebsocketWrapper.OPCODE_CONNCLOSE, payload, 0) + self._socket.send(frame) + + if opcode == _WebsocketWrapper.OPCODE_PING: + frame = self._create_frame( + _WebsocketWrapper.OPCODE_PONG, payload, 0) + self._socket.send(frame) + + # This isn't *proper* handling of continuation frames, but given + # that we only support binary frames, it is *probably* good enough. + if (opcode == _WebsocketWrapper.OPCODE_BINARY or opcode == _WebsocketWrapper.OPCODE_CONTINUATION) \ + and payload_length > 0: + return result + else: + raise BlockingIOError + + except ConnectionError: + self.connected = False + return b'' + + def _send_impl(self, data: bytes) -> int: + + # if previous frame was sent successfully + if len(self._sendbuffer) == 0: + # create websocket frame + frame = self._create_frame( + _WebsocketWrapper.OPCODE_BINARY, bytearray(data)) + self._sendbuffer.extend(frame) + self._requested_size = len(data) + + # try to write out as much as possible + length = self._socket.send(self._sendbuffer) + + self._sendbuffer = self._sendbuffer[length:] + + if len(self._sendbuffer) == 0: + # buffer sent out completely, return with payload's size + return self._requested_size + else: + # couldn't send whole data, request the same data again with 0 as sent length + return 0 + + def recv(self, length: int) -> bytes: + return self._recv_impl(length) + + def read(self, length: int) -> bytes: + return self._recv_impl(length) + + def send(self, data: bytes) -> int: + return self._send_impl(data) + + def write(self, data: bytes) -> int: + return self._send_impl(data) + + def close(self) -> None: + self._socket.close() + + def fileno(self) -> int: + return self._socket.fileno() + + def pending(self) -> int: + # Fix for bug #131: a SSL socket may still have data available + # for reading without select() being aware of it. + if self._ssl: + return self._socket.pending() # type: ignore[union-attr] + else: + # normal socket rely only on select() + return 0 + + def setblocking(self, flag: bool) -> None: + self._socket.setblocking(flag) diff --git a/scripts/tempSensor/lib/paho/mqtt/enums.py b/scripts/tempSensor/lib/paho/mqtt/enums.py new file mode 100644 index 00000000..5428769f --- /dev/null +++ b/scripts/tempSensor/lib/paho/mqtt/enums.py @@ -0,0 +1,113 @@ +import enum + + +class MQTTErrorCode(enum.IntEnum): + MQTT_ERR_AGAIN = -1 + MQTT_ERR_SUCCESS = 0 + MQTT_ERR_NOMEM = 1 + MQTT_ERR_PROTOCOL = 2 + MQTT_ERR_INVAL = 3 + MQTT_ERR_NO_CONN = 4 + MQTT_ERR_CONN_REFUSED = 5 + MQTT_ERR_NOT_FOUND = 6 + MQTT_ERR_CONN_LOST = 7 + MQTT_ERR_TLS = 8 + MQTT_ERR_PAYLOAD_SIZE = 9 + MQTT_ERR_NOT_SUPPORTED = 10 + MQTT_ERR_AUTH = 11 + MQTT_ERR_ACL_DENIED = 12 + MQTT_ERR_UNKNOWN = 13 + MQTT_ERR_ERRNO = 14 + MQTT_ERR_QUEUE_SIZE = 15 + MQTT_ERR_KEEPALIVE = 16 + + +class MQTTProtocolVersion(enum.IntEnum): + MQTTv31 = 3 + MQTTv311 = 4 + MQTTv5 = 5 + + +class CallbackAPIVersion(enum.Enum): + """Defined the arguments passed to all user-callback. + + See each callbacks for details: `on_connect`, `on_connect_fail`, `on_disconnect`, `on_message`, `on_publish`, + `on_subscribe`, `on_unsubscribe`, `on_log`, `on_socket_open`, `on_socket_close`, + `on_socket_register_write`, `on_socket_unregister_write` + """ + VERSION1 = 1 + """The version used with paho-mqtt 1.x before introducing CallbackAPIVersion. + + This version had different arguments depending if MQTTv5 or MQTTv3 was used. `Properties` & `ReasonCode` were missing + on some callback (apply only to MQTTv5). + + This version is deprecated and will be removed in version 3.0. + """ + VERSION2 = 2 + """ This version fix some of the shortcoming of previous version. + + Callback have the same signature if using MQTTv5 or MQTTv3. `ReasonCode` are used in MQTTv3. + """ + + +class MessageType(enum.IntEnum): + CONNECT = 0x10 + CONNACK = 0x20 + PUBLISH = 0x30 + PUBACK = 0x40 + PUBREC = 0x50 + PUBREL = 0x60 + PUBCOMP = 0x70 + SUBSCRIBE = 0x80 + SUBACK = 0x90 + UNSUBSCRIBE = 0xA0 + UNSUBACK = 0xB0 + PINGREQ = 0xC0 + PINGRESP = 0xD0 + DISCONNECT = 0xE0 + AUTH = 0xF0 + + +class LogLevel(enum.IntEnum): + MQTT_LOG_INFO = 0x01 + MQTT_LOG_NOTICE = 0x02 + MQTT_LOG_WARNING = 0x04 + MQTT_LOG_ERR = 0x08 + MQTT_LOG_DEBUG = 0x10 + + +class ConnackCode(enum.IntEnum): + CONNACK_ACCEPTED = 0 + CONNACK_REFUSED_PROTOCOL_VERSION = 1 + CONNACK_REFUSED_IDENTIFIER_REJECTED = 2 + CONNACK_REFUSED_SERVER_UNAVAILABLE = 3 + CONNACK_REFUSED_BAD_USERNAME_PASSWORD = 4 + CONNACK_REFUSED_NOT_AUTHORIZED = 5 + + +class _ConnectionState(enum.Enum): + MQTT_CS_NEW = enum.auto() + MQTT_CS_CONNECT_ASYNC = enum.auto() + MQTT_CS_CONNECTING = enum.auto() + MQTT_CS_CONNECTED = enum.auto() + MQTT_CS_CONNECTION_LOST = enum.auto() + MQTT_CS_DISCONNECTING = enum.auto() + MQTT_CS_DISCONNECTED = enum.auto() + + +class MessageState(enum.IntEnum): + MQTT_MS_INVALID = 0 + MQTT_MS_PUBLISH = 1 + MQTT_MS_WAIT_FOR_PUBACK = 2 + MQTT_MS_WAIT_FOR_PUBREC = 3 + MQTT_MS_RESEND_PUBREL = 4 + MQTT_MS_WAIT_FOR_PUBREL = 5 + MQTT_MS_RESEND_PUBCOMP = 6 + MQTT_MS_WAIT_FOR_PUBCOMP = 7 + MQTT_MS_SEND_PUBREC = 8 + MQTT_MS_QUEUED = 9 + + +class PahoClientMode(enum.IntEnum): + MQTT_CLIENT = 0 + MQTT_BRIDGE = 1 diff --git a/scripts/tempSensor/lib/paho/mqtt/matcher.py b/scripts/tempSensor/lib/paho/mqtt/matcher.py new file mode 100644 index 00000000..b73c13ac --- /dev/null +++ b/scripts/tempSensor/lib/paho/mqtt/matcher.py @@ -0,0 +1,78 @@ +class MQTTMatcher: + """Intended to manage topic filters including wildcards. + + Internally, MQTTMatcher use a prefix tree (trie) to store + values associated with filters, and has an iter_match() + method to iterate efficiently over all filters that match + some topic name.""" + + class Node: + __slots__ = '_children', '_content' + + def __init__(self): + self._children = {} + self._content = None + + def __init__(self): + self._root = self.Node() + + def __setitem__(self, key, value): + """Add a topic filter :key to the prefix tree + and associate it to :value""" + node = self._root + for sym in key.split('/'): + node = node._children.setdefault(sym, self.Node()) + node._content = value + + def __getitem__(self, key): + """Retrieve the value associated with some topic filter :key""" + try: + node = self._root + for sym in key.split('/'): + node = node._children[sym] + if node._content is None: + raise KeyError(key) + return node._content + except KeyError as ke: + raise KeyError(key) from ke + + def __delitem__(self, key): + """Delete the value associated with some topic filter :key""" + lst = [] + try: + parent, node = None, self._root + for k in key.split('/'): + parent, node = node, node._children[k] + lst.append((parent, k, node)) + # TODO + node._content = None + except KeyError as ke: + raise KeyError(key) from ke + else: # cleanup + for parent, k, node in reversed(lst): + if node._children or node._content is not None: + break + del parent._children[k] + + def iter_match(self, topic): + """Return an iterator on all values associated with filters + that match the :topic""" + lst = topic.split('/') + normal = not topic.startswith('$') + def rec(node, i=0): + if i == len(lst): + if node._content is not None: + yield node._content + else: + part = lst[i] + if part in node._children: + for content in rec(node._children[part], i + 1): + yield content + if '+' in node._children and (normal or i > 0): + for content in rec(node._children['+'], i + 1): + yield content + if '#' in node._children and (normal or i > 0): + content = node._children['#']._content + if content is not None: + yield content + return rec(self._root) diff --git a/scripts/tempSensor/lib/paho/mqtt/packettypes.py b/scripts/tempSensor/lib/paho/mqtt/packettypes.py new file mode 100644 index 00000000..d2051490 --- /dev/null +++ b/scripts/tempSensor/lib/paho/mqtt/packettypes.py @@ -0,0 +1,43 @@ +""" +******************************************************************* + Copyright (c) 2017, 2019 IBM Corp. + + All rights reserved. This program and the accompanying materials + are made available under the terms of the Eclipse Public License v2.0 + and Eclipse Distribution License v1.0 which accompany this distribution. + + The Eclipse Public License is available at + http://www.eclipse.org/legal/epl-v20.html + and the Eclipse Distribution License is available at + http://www.eclipse.org/org/documents/edl-v10.php. + + Contributors: + Ian Craggs - initial implementation and/or documentation +******************************************************************* +""" + + +class PacketTypes: + + """ + Packet types class. Includes the AUTH packet for MQTT v5.0. + + Holds constants for each packet type such as PacketTypes.PUBLISH + and packet name strings: PacketTypes.Names[PacketTypes.PUBLISH]. + + """ + + indexes = range(1, 16) + + # Packet types + CONNECT, CONNACK, PUBLISH, PUBACK, PUBREC, PUBREL, \ + PUBCOMP, SUBSCRIBE, SUBACK, UNSUBSCRIBE, UNSUBACK, \ + PINGREQ, PINGRESP, DISCONNECT, AUTH = indexes + + # Dummy packet type for properties use - will delay only applies to will + WILLMESSAGE = 99 + + Names = ( "reserved", \ + "Connect", "Connack", "Publish", "Puback", "Pubrec", "Pubrel", \ + "Pubcomp", "Subscribe", "Suback", "Unsubscribe", "Unsuback", \ + "Pingreq", "Pingresp", "Disconnect", "Auth") diff --git a/scripts/tempSensor/lib/paho/mqtt/properties.py b/scripts/tempSensor/lib/paho/mqtt/properties.py new file mode 100644 index 00000000..f307b865 --- /dev/null +++ b/scripts/tempSensor/lib/paho/mqtt/properties.py @@ -0,0 +1,421 @@ +# ******************************************************************* +# Copyright (c) 2017, 2019 IBM Corp. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Public License v2.0 +# and Eclipse Distribution License v1.0 which accompany this distribution. +# +# The Eclipse Public License is available at +# http://www.eclipse.org/legal/epl-v20.html +# and the Eclipse Distribution License is available at +# http://www.eclipse.org/org/documents/edl-v10.php. +# +# Contributors: +# Ian Craggs - initial implementation and/or documentation +# ******************************************************************* + +import struct + +from .packettypes import PacketTypes + + +class MQTTException(Exception): + pass + + +class MalformedPacket(MQTTException): + pass + + +def writeInt16(length): + # serialize a 16 bit integer to network format + return bytearray(struct.pack("!H", length)) + + +def readInt16(buf): + # deserialize a 16 bit integer from network format + return struct.unpack("!H", buf[:2])[0] + + +def writeInt32(length): + # serialize a 32 bit integer to network format + return bytearray(struct.pack("!L", length)) + + +def readInt32(buf): + # deserialize a 32 bit integer from network format + return struct.unpack("!L", buf[:4])[0] + + +def writeUTF(data): + # data could be a string, or bytes. If string, encode into bytes with utf-8 + if not isinstance(data, bytes): + data = bytes(data, "utf-8") + return writeInt16(len(data)) + data + + +def readUTF(buffer, maxlen): + if maxlen >= 2: + length = readInt16(buffer) + else: + raise MalformedPacket("Not enough data to read string length") + maxlen -= 2 + if length > maxlen: + raise MalformedPacket("Length delimited string too long") + buf = buffer[2:2+length].decode("utf-8") + # look for chars which are invalid for MQTT + for c in buf: # look for D800-DFFF in the UTF string + ord_c = ord(c) + if ord_c >= 0xD800 and ord_c <= 0xDFFF: + raise MalformedPacket("[MQTT-1.5.4-1] D800-DFFF found in UTF-8 data") + if ord_c == 0x00: # look for null in the UTF string + raise MalformedPacket("[MQTT-1.5.4-2] Null found in UTF-8 data") + if ord_c == 0xFEFF: + raise MalformedPacket("[MQTT-1.5.4-3] U+FEFF in UTF-8 data") + return buf, length+2 + + +def writeBytes(buffer): + return writeInt16(len(buffer)) + buffer + + +def readBytes(buffer): + length = readInt16(buffer) + return buffer[2:2+length], length+2 + + +class VariableByteIntegers: # Variable Byte Integer + """ + MQTT variable byte integer helper class. Used + in several places in MQTT v5.0 properties. + + """ + + @staticmethod + def encode(x): + """ + Convert an integer 0 <= x <= 268435455 into multi-byte format. + Returns the buffer converted from the integer. + """ + if not 0 <= x <= 268435455: + raise ValueError(f"Value {x!r} must be in range 0-268435455") + buffer = b'' + while 1: + digit = x % 128 + x //= 128 + if x > 0: + digit |= 0x80 + buffer += bytes([digit]) + if x == 0: + break + return buffer + + @staticmethod + def decode(buffer): + """ + Get the value of a multi-byte integer from a buffer + Return the value, and the number of bytes used. + + [MQTT-1.5.5-1] the encoded value MUST use the minimum number of bytes necessary to represent the value + """ + multiplier = 1 + value = 0 + bytes = 0 + while 1: + bytes += 1 + digit = buffer[0] + buffer = buffer[1:] + value += (digit & 127) * multiplier + if digit & 128 == 0: + break + multiplier *= 128 + return (value, bytes) + + +class Properties: + """MQTT v5.0 properties class. + + See Properties.names for a list of accepted property names along with their numeric values. + + See Properties.properties for the data type of each property. + + Example of use:: + + publish_properties = Properties(PacketTypes.PUBLISH) + publish_properties.UserProperty = ("a", "2") + publish_properties.UserProperty = ("c", "3") + + First the object is created with packet type as argument, no properties will be present at + this point. Then properties are added as attributes, the name of which is the string property + name without the spaces. + + """ + + def __init__(self, packetType): + self.packetType = packetType + self.types = ["Byte", "Two Byte Integer", "Four Byte Integer", "Variable Byte Integer", + "Binary Data", "UTF-8 Encoded String", "UTF-8 String Pair"] + + self.names = { + "Payload Format Indicator": 1, + "Message Expiry Interval": 2, + "Content Type": 3, + "Response Topic": 8, + "Correlation Data": 9, + "Subscription Identifier": 11, + "Session Expiry Interval": 17, + "Assigned Client Identifier": 18, + "Server Keep Alive": 19, + "Authentication Method": 21, + "Authentication Data": 22, + "Request Problem Information": 23, + "Will Delay Interval": 24, + "Request Response Information": 25, + "Response Information": 26, + "Server Reference": 28, + "Reason String": 31, + "Receive Maximum": 33, + "Topic Alias Maximum": 34, + "Topic Alias": 35, + "Maximum QoS": 36, + "Retain Available": 37, + "User Property": 38, + "Maximum Packet Size": 39, + "Wildcard Subscription Available": 40, + "Subscription Identifier Available": 41, + "Shared Subscription Available": 42 + } + + self.properties = { + # id: type, packets + # payload format indicator + 1: (self.types.index("Byte"), [PacketTypes.PUBLISH, PacketTypes.WILLMESSAGE]), + 2: (self.types.index("Four Byte Integer"), [PacketTypes.PUBLISH, PacketTypes.WILLMESSAGE]), + 3: (self.types.index("UTF-8 Encoded String"), [PacketTypes.PUBLISH, PacketTypes.WILLMESSAGE]), + 8: (self.types.index("UTF-8 Encoded String"), [PacketTypes.PUBLISH, PacketTypes.WILLMESSAGE]), + 9: (self.types.index("Binary Data"), [PacketTypes.PUBLISH, PacketTypes.WILLMESSAGE]), + 11: (self.types.index("Variable Byte Integer"), + [PacketTypes.PUBLISH, PacketTypes.SUBSCRIBE]), + 17: (self.types.index("Four Byte Integer"), + [PacketTypes.CONNECT, PacketTypes.CONNACK, PacketTypes.DISCONNECT]), + 18: (self.types.index("UTF-8 Encoded String"), [PacketTypes.CONNACK]), + 19: (self.types.index("Two Byte Integer"), [PacketTypes.CONNACK]), + 21: (self.types.index("UTF-8 Encoded String"), + [PacketTypes.CONNECT, PacketTypes.CONNACK, PacketTypes.AUTH]), + 22: (self.types.index("Binary Data"), + [PacketTypes.CONNECT, PacketTypes.CONNACK, PacketTypes.AUTH]), + 23: (self.types.index("Byte"), + [PacketTypes.CONNECT]), + 24: (self.types.index("Four Byte Integer"), [PacketTypes.WILLMESSAGE]), + 25: (self.types.index("Byte"), [PacketTypes.CONNECT]), + 26: (self.types.index("UTF-8 Encoded String"), [PacketTypes.CONNACK]), + 28: (self.types.index("UTF-8 Encoded String"), + [PacketTypes.CONNACK, PacketTypes.DISCONNECT]), + 31: (self.types.index("UTF-8 Encoded String"), + [PacketTypes.CONNACK, PacketTypes.PUBACK, PacketTypes.PUBREC, + PacketTypes.PUBREL, PacketTypes.PUBCOMP, PacketTypes.SUBACK, + PacketTypes.UNSUBACK, PacketTypes.DISCONNECT, PacketTypes.AUTH]), + 33: (self.types.index("Two Byte Integer"), + [PacketTypes.CONNECT, PacketTypes.CONNACK]), + 34: (self.types.index("Two Byte Integer"), + [PacketTypes.CONNECT, PacketTypes.CONNACK]), + 35: (self.types.index("Two Byte Integer"), [PacketTypes.PUBLISH]), + 36: (self.types.index("Byte"), [PacketTypes.CONNACK]), + 37: (self.types.index("Byte"), [PacketTypes.CONNACK]), + 38: (self.types.index("UTF-8 String Pair"), + [PacketTypes.CONNECT, PacketTypes.CONNACK, + PacketTypes.PUBLISH, PacketTypes.PUBACK, + PacketTypes.PUBREC, PacketTypes.PUBREL, PacketTypes.PUBCOMP, + PacketTypes.SUBSCRIBE, PacketTypes.SUBACK, + PacketTypes.UNSUBSCRIBE, PacketTypes.UNSUBACK, + PacketTypes.DISCONNECT, PacketTypes.AUTH, PacketTypes.WILLMESSAGE]), + 39: (self.types.index("Four Byte Integer"), + [PacketTypes.CONNECT, PacketTypes.CONNACK]), + 40: (self.types.index("Byte"), [PacketTypes.CONNACK]), + 41: (self.types.index("Byte"), [PacketTypes.CONNACK]), + 42: (self.types.index("Byte"), [PacketTypes.CONNACK]), + } + + def allowsMultiple(self, compressedName): + return self.getIdentFromName(compressedName) in [11, 38] + + def getIdentFromName(self, compressedName): + # return the identifier corresponding to the property name + result = -1 + for name in self.names.keys(): + if compressedName == name.replace(' ', ''): + result = self.names[name] + break + return result + + def __setattr__(self, name, value): + name = name.replace(' ', '') + privateVars = ["packetType", "types", "names", "properties"] + if name in privateVars: + object.__setattr__(self, name, value) + else: + # the name could have spaces in, or not. Remove spaces before assignment + if name not in [aname.replace(' ', '') for aname in self.names.keys()]: + raise MQTTException( + f"Property name must be one of {self.names.keys()}") + # check that this attribute applies to the packet type + if self.packetType not in self.properties[self.getIdentFromName(name)][1]: + raise MQTTException(f"Property {name} does not apply to packet type {PacketTypes.Names[self.packetType]}") + + # Check for forbidden values + if not isinstance(value, list): + if name in ["ReceiveMaximum", "TopicAlias"] \ + and (value < 1 or value > 65535): + + raise MQTTException(f"{name} property value must be in the range 1-65535") + elif name in ["TopicAliasMaximum"] \ + and (value < 0 or value > 65535): + + raise MQTTException(f"{name} property value must be in the range 0-65535") + elif name in ["MaximumPacketSize", "SubscriptionIdentifier"] \ + and (value < 1 or value > 268435455): + + raise MQTTException(f"{name} property value must be in the range 1-268435455") + elif name in ["RequestResponseInformation", "RequestProblemInformation", "PayloadFormatIndicator"] \ + and (value != 0 and value != 1): + + raise MQTTException( + f"{name} property value must be 0 or 1") + + if self.allowsMultiple(name): + if not isinstance(value, list): + value = [value] + if hasattr(self, name): + value = object.__getattribute__(self, name) + value + object.__setattr__(self, name, value) + + def __str__(self): + buffer = "[" + first = True + for name in self.names.keys(): + compressedName = name.replace(' ', '') + if hasattr(self, compressedName): + if not first: + buffer += ", " + buffer += f"{compressedName} : {getattr(self, compressedName)}" + first = False + buffer += "]" + return buffer + + def json(self): + data = {} + for name in self.names.keys(): + compressedName = name.replace(' ', '') + if hasattr(self, compressedName): + val = getattr(self, compressedName) + if compressedName == 'CorrelationData' and isinstance(val, bytes): + data[compressedName] = val.hex() + else: + data[compressedName] = val + return data + + def isEmpty(self): + rc = True + for name in self.names.keys(): + compressedName = name.replace(' ', '') + if hasattr(self, compressedName): + rc = False + break + return rc + + def clear(self): + for name in self.names.keys(): + compressedName = name.replace(' ', '') + if hasattr(self, compressedName): + delattr(self, compressedName) + + def writeProperty(self, identifier, type, value): + buffer = b"" + buffer += VariableByteIntegers.encode(identifier) # identifier + if type == self.types.index("Byte"): # value + buffer += bytes([value]) + elif type == self.types.index("Two Byte Integer"): + buffer += writeInt16(value) + elif type == self.types.index("Four Byte Integer"): + buffer += writeInt32(value) + elif type == self.types.index("Variable Byte Integer"): + buffer += VariableByteIntegers.encode(value) + elif type == self.types.index("Binary Data"): + buffer += writeBytes(value) + elif type == self.types.index("UTF-8 Encoded String"): + buffer += writeUTF(value) + elif type == self.types.index("UTF-8 String Pair"): + buffer += writeUTF(value[0]) + writeUTF(value[1]) + return buffer + + def pack(self): + # serialize properties into buffer for sending over network + buffer = b"" + for name in self.names.keys(): + compressedName = name.replace(' ', '') + if hasattr(self, compressedName): + identifier = self.getIdentFromName(compressedName) + attr_type = self.properties[identifier][0] + if self.allowsMultiple(compressedName): + for prop in getattr(self, compressedName): + buffer += self.writeProperty(identifier, + attr_type, prop) + else: + buffer += self.writeProperty(identifier, attr_type, + getattr(self, compressedName)) + return VariableByteIntegers.encode(len(buffer)) + buffer + + def readProperty(self, buffer, type, propslen): + if type == self.types.index("Byte"): + value = buffer[0] + valuelen = 1 + elif type == self.types.index("Two Byte Integer"): + value = readInt16(buffer) + valuelen = 2 + elif type == self.types.index("Four Byte Integer"): + value = readInt32(buffer) + valuelen = 4 + elif type == self.types.index("Variable Byte Integer"): + value, valuelen = VariableByteIntegers.decode(buffer) + elif type == self.types.index("Binary Data"): + value, valuelen = readBytes(buffer) + elif type == self.types.index("UTF-8 Encoded String"): + value, valuelen = readUTF(buffer, propslen) + elif type == self.types.index("UTF-8 String Pair"): + value, valuelen = readUTF(buffer, propslen) + buffer = buffer[valuelen:] # strip the bytes used by the value + value1, valuelen1 = readUTF(buffer, propslen - valuelen) + value = (value, value1) + valuelen += valuelen1 + return value, valuelen + + def getNameFromIdent(self, identifier): + rc = None + for name in self.names: + if self.names[name] == identifier: + rc = name + return rc + + def unpack(self, buffer): + self.clear() + # deserialize properties into attributes from buffer received from network + propslen, VBIlen = VariableByteIntegers.decode(buffer) + buffer = buffer[VBIlen:] # strip the bytes used by the VBI + propslenleft = propslen + while propslenleft > 0: # properties length is 0 if there are none + identifier, VBIlen2 = VariableByteIntegers.decode( + buffer) # property identifier + buffer = buffer[VBIlen2:] # strip the bytes used by the VBI + propslenleft -= VBIlen2 + attr_type = self.properties[identifier][0] + value, valuelen = self.readProperty( + buffer, attr_type, propslenleft) + buffer = buffer[valuelen:] # strip the bytes used by the value + propslenleft -= valuelen + propname = self.getNameFromIdent(identifier) + compressedName = propname.replace(' ', '') + if not self.allowsMultiple(compressedName) and hasattr(self, compressedName): + raise MQTTException( + f"Property '{property}' must not exist more than once") + setattr(self, propname, value) + return self, propslen + VBIlen diff --git a/scripts/tempSensor/lib/paho/mqtt/publish.py b/scripts/tempSensor/lib/paho/mqtt/publish.py new file mode 100644 index 00000000..333c190a --- /dev/null +++ b/scripts/tempSensor/lib/paho/mqtt/publish.py @@ -0,0 +1,306 @@ +# Copyright (c) 2014 Roger Light +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Public License v2.0 +# and Eclipse Distribution License v1.0 which accompany this distribution. +# +# The Eclipse Public License is available at +# http://www.eclipse.org/legal/epl-v20.html +# and the Eclipse Distribution License is available at +# http://www.eclipse.org/org/documents/edl-v10.php. +# +# Contributors: +# Roger Light - initial API and implementation + +""" +This module provides some helper functions to allow straightforward publishing +of messages in a one-shot manner. In other words, they are useful for the +situation where you have a single/multiple messages you want to publish to a +broker, then disconnect and nothing else is required. +""" +from __future__ import annotations + +import collections +from collections.abc import Iterable +from typing import TYPE_CHECKING, Any, List, Tuple, Union + +from paho.mqtt.enums import CallbackAPIVersion, MQTTProtocolVersion +from paho.mqtt.properties import Properties +from paho.mqtt.reasoncodes import ReasonCode + +from .. import mqtt +from . import client as paho + +if TYPE_CHECKING: + try: + from typing import NotRequired, Required, TypedDict # type: ignore + except ImportError: + from typing_extensions import NotRequired, Required, TypedDict + + try: + from typing import Literal + except ImportError: + from typing_extensions import Literal # type: ignore + + + + class AuthParameter(TypedDict, total=False): + username: Required[str] + password: NotRequired[str] + + + class TLSParameter(TypedDict, total=False): + ca_certs: Required[str] + certfile: NotRequired[str] + keyfile: NotRequired[str] + tls_version: NotRequired[int] + ciphers: NotRequired[str] + insecure: NotRequired[bool] + + + class MessageDict(TypedDict, total=False): + topic: Required[str] + payload: NotRequired[paho.PayloadType] + qos: NotRequired[int] + retain: NotRequired[bool] + + MessageTuple = Tuple[str, paho.PayloadType, int, bool] + + MessagesList = List[Union[MessageDict, MessageTuple]] + + +def _do_publish(client: paho.Client): + """Internal function""" + + message = client._userdata.popleft() + + if isinstance(message, dict): + client.publish(**message) + elif isinstance(message, (tuple, list)): + client.publish(*message) + else: + raise TypeError('message must be a dict, tuple, or list') + + +def _on_connect(client: paho.Client, userdata: MessagesList, flags, reason_code, properties): + """Internal v5 callback""" + if reason_code == 0: + if len(userdata) > 0: + _do_publish(client) + else: + raise mqtt.MQTTException(paho.connack_string(reason_code)) + + +def _on_publish( + client: paho.Client, userdata: collections.deque[MessagesList], mid: int, reason_codes: ReasonCode, properties: Properties, +) -> None: + """Internal callback""" + #pylint: disable=unused-argument + + if len(userdata) == 0: + client.disconnect() + else: + _do_publish(client) + + +def multiple( + msgs: MessagesList, + hostname: str = "localhost", + port: int = 1883, + client_id: str = "", + keepalive: int = 60, + will: MessageDict | None = None, + auth: AuthParameter | None = None, + tls: TLSParameter | None = None, + protocol: MQTTProtocolVersion = paho.MQTTv311, + transport: Literal["tcp", "websockets"] = "tcp", + proxy_args: Any | None = None, +) -> None: + """Publish multiple messages to a broker, then disconnect cleanly. + + This function creates an MQTT client, connects to a broker and publishes a + list of messages. Once the messages have been delivered, it disconnects + cleanly from the broker. + + :param msgs: a list of messages to publish. Each message is either a dict or a + tuple. + + If a dict, only the topic must be present. Default values will be + used for any missing arguments. The dict must be of the form: + + msg = {'topic':"", 'payload':"", 'qos':, + 'retain':} + topic must be present and may not be empty. + If payload is "", None or not present then a zero length payload + will be published. + If qos is not present, the default of 0 is used. + If retain is not present, the default of False is used. + + If a tuple, then it must be of the form: + ("", "", qos, retain) + + :param str hostname: the address of the broker to connect to. + Defaults to localhost. + + :param int port: the port to connect to the broker on. Defaults to 1883. + + :param str client_id: the MQTT client id to use. If "" or None, the Paho library will + generate a client id automatically. + + :param int keepalive: the keepalive timeout value for the client. Defaults to 60 + seconds. + + :param will: a dict containing will parameters for the client: will = {'topic': + "", 'payload':", 'qos':, 'retain':}. + Topic is required, all other parameters are optional and will + default to None, 0 and False respectively. + Defaults to None, which indicates no will should be used. + + :param auth: a dict containing authentication parameters for the client: + auth = {'username':"", 'password':""} + Username is required, password is optional and will default to None + if not provided. + Defaults to None, which indicates no authentication is to be used. + + :param tls: a dict containing TLS configuration parameters for the client: + dict = {'ca_certs':"", 'certfile':"", + 'keyfile':"", 'tls_version':"", + 'ciphers':", 'insecure':""} + ca_certs is required, all other parameters are optional and will + default to None if not provided, which results in the client using + the default behaviour - see the paho.mqtt.client documentation. + Alternatively, tls input can be an SSLContext object, which will be + processed using the tls_set_context method. + Defaults to None, which indicates that TLS should not be used. + + :param str transport: set to "tcp" to use the default setting of transport which is + raw TCP. Set to "websockets" to use WebSockets as the transport. + + :param proxy_args: a dictionary that will be given to the client. + """ + + if not isinstance(msgs, Iterable): + raise TypeError('msgs must be an iterable') + if len(msgs) == 0: + raise ValueError('msgs is empty') + + client = paho.Client( + CallbackAPIVersion.VERSION2, + client_id=client_id, + userdata=collections.deque(msgs), + protocol=protocol, + transport=transport, + ) + + client.enable_logger() + client.on_publish = _on_publish + client.on_connect = _on_connect # type: ignore + + if proxy_args is not None: + client.proxy_set(**proxy_args) + + if auth: + username = auth.get('username') + if username: + password = auth.get('password') + client.username_pw_set(username, password) + else: + raise KeyError("The 'username' key was not found, this is " + "required for auth") + + if will is not None: + client.will_set(**will) + + if tls is not None: + if isinstance(tls, dict): + insecure = tls.pop('insecure', False) + # mypy don't get that tls no longer contains the key insecure + client.tls_set(**tls) # type: ignore[misc] + if insecure: + # Must be set *after* the `client.tls_set()` call since it sets + # up the SSL context that `client.tls_insecure_set` alters. + client.tls_insecure_set(insecure) + else: + # Assume input is SSLContext object + client.tls_set_context(tls) + + client.connect(hostname, port, keepalive) + client.loop_forever() + + +def single( + topic: str, + payload: paho.PayloadType = None, + qos: int = 0, + retain: bool = False, + hostname: str = "localhost", + port: int = 1883, + client_id: str = "", + keepalive: int = 60, + will: MessageDict | None = None, + auth: AuthParameter | None = None, + tls: TLSParameter | None = None, + protocol: MQTTProtocolVersion = paho.MQTTv311, + transport: Literal["tcp", "websockets"] = "tcp", + proxy_args: Any | None = None, +) -> None: + """Publish a single message to a broker, then disconnect cleanly. + + This function creates an MQTT client, connects to a broker and publishes a + single message. Once the message has been delivered, it disconnects cleanly + from the broker. + + :param str topic: the only required argument must be the topic string to which the + payload will be published. + + :param payload: the payload to be published. If "" or None, a zero length payload + will be published. + + :param int qos: the qos to use when publishing, default to 0. + + :param bool retain: set the message to be retained (True) or not (False). + + :param str hostname: the address of the broker to connect to. + Defaults to localhost. + + :param int port: the port to connect to the broker on. Defaults to 1883. + + :param str client_id: the MQTT client id to use. If "" or None, the Paho library will + generate a client id automatically. + + :param int keepalive: the keepalive timeout value for the client. Defaults to 60 + seconds. + + :param will: a dict containing will parameters for the client: will = {'topic': + "", 'payload':", 'qos':, 'retain':}. + Topic is required, all other parameters are optional and will + default to None, 0 and False respectively. + Defaults to None, which indicates no will should be used. + + :param auth: a dict containing authentication parameters for the client: + Username is required, password is optional and will default to None + auth = {'username':"", 'password':""} + if not provided. + Defaults to None, which indicates no authentication is to be used. + + :param tls: a dict containing TLS configuration parameters for the client: + dict = {'ca_certs':"", 'certfile':"", + 'keyfile':"", 'tls_version':"", + 'ciphers':", 'insecure':""} + ca_certs is required, all other parameters are optional and will + default to None if not provided, which results in the client using + the default behaviour - see the paho.mqtt.client documentation. + Defaults to None, which indicates that TLS should not be used. + Alternatively, tls input can be an SSLContext object, which will be + processed using the tls_set_context method. + + :param transport: set to "tcp" to use the default setting of transport which is + raw TCP. Set to "websockets" to use WebSockets as the transport. + + :param proxy_args: a dictionary that will be given to the client. + """ + + msg: MessageDict = {'topic':topic, 'payload':payload, 'qos':qos, 'retain':retain} + + multiple([msg], hostname, port, client_id, keepalive, will, auth, tls, + protocol, transport, proxy_args) diff --git a/scripts/tempSensor/lib/paho/mqtt/py.typed b/scripts/tempSensor/lib/paho/mqtt/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/scripts/tempSensor/lib/paho/mqtt/reasoncodes.py b/scripts/tempSensor/lib/paho/mqtt/reasoncodes.py new file mode 100644 index 00000000..243ac96f --- /dev/null +++ b/scripts/tempSensor/lib/paho/mqtt/reasoncodes.py @@ -0,0 +1,223 @@ +# ******************************************************************* +# Copyright (c) 2017, 2019 IBM Corp. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Public License v2.0 +# and Eclipse Distribution License v1.0 which accompany this distribution. +# +# The Eclipse Public License is available at +# http://www.eclipse.org/legal/epl-v20.html +# and the Eclipse Distribution License is available at +# http://www.eclipse.org/org/documents/edl-v10.php. +# +# Contributors: +# Ian Craggs - initial implementation and/or documentation +# ******************************************************************* + +import functools +import warnings +from typing import Any + +from .packettypes import PacketTypes + + +@functools.total_ordering +class ReasonCode: + """MQTT version 5.0 reason codes class. + + See ReasonCode.names for a list of possible numeric values along with their + names and the packets to which they apply. + + """ + + def __init__(self, packetType: int, aName: str ="Success", identifier: int =-1): + """ + packetType: the type of the packet, such as PacketTypes.CONNECT that + this reason code will be used with. Some reason codes have different + names for the same identifier when used a different packet type. + + aName: the String name of the reason code to be created. Ignored + if the identifier is set. + + identifier: an integer value of the reason code to be created. + + """ + + self.packetType = packetType + self.names = { + 0: {"Success": [PacketTypes.CONNACK, PacketTypes.PUBACK, + PacketTypes.PUBREC, PacketTypes.PUBREL, PacketTypes.PUBCOMP, + PacketTypes.UNSUBACK, PacketTypes.AUTH], + "Normal disconnection": [PacketTypes.DISCONNECT], + "Granted QoS 0": [PacketTypes.SUBACK]}, + 1: {"Granted QoS 1": [PacketTypes.SUBACK]}, + 2: {"Granted QoS 2": [PacketTypes.SUBACK]}, + 4: {"Disconnect with will message": [PacketTypes.DISCONNECT]}, + 16: {"No matching subscribers": + [PacketTypes.PUBACK, PacketTypes.PUBREC]}, + 17: {"No subscription found": [PacketTypes.UNSUBACK]}, + 24: {"Continue authentication": [PacketTypes.AUTH]}, + 25: {"Re-authenticate": [PacketTypes.AUTH]}, + 128: {"Unspecified error": [PacketTypes.CONNACK, PacketTypes.PUBACK, + PacketTypes.PUBREC, PacketTypes.SUBACK, PacketTypes.UNSUBACK, + PacketTypes.DISCONNECT], }, + 129: {"Malformed packet": + [PacketTypes.CONNACK, PacketTypes.DISCONNECT]}, + 130: {"Protocol error": + [PacketTypes.CONNACK, PacketTypes.DISCONNECT]}, + 131: {"Implementation specific error": [PacketTypes.CONNACK, + PacketTypes.PUBACK, PacketTypes.PUBREC, PacketTypes.SUBACK, + PacketTypes.UNSUBACK, PacketTypes.DISCONNECT], }, + 132: {"Unsupported protocol version": [PacketTypes.CONNACK]}, + 133: {"Client identifier not valid": [PacketTypes.CONNACK]}, + 134: {"Bad user name or password": [PacketTypes.CONNACK]}, + 135: {"Not authorized": [PacketTypes.CONNACK, PacketTypes.PUBACK, + PacketTypes.PUBREC, PacketTypes.SUBACK, PacketTypes.UNSUBACK, + PacketTypes.DISCONNECT], }, + 136: {"Server unavailable": [PacketTypes.CONNACK]}, + 137: {"Server busy": [PacketTypes.CONNACK, PacketTypes.DISCONNECT]}, + 138: {"Banned": [PacketTypes.CONNACK]}, + 139: {"Server shutting down": [PacketTypes.DISCONNECT]}, + 140: {"Bad authentication method": + [PacketTypes.CONNACK, PacketTypes.DISCONNECT]}, + 141: {"Keep alive timeout": [PacketTypes.DISCONNECT]}, + 142: {"Session taken over": [PacketTypes.DISCONNECT]}, + 143: {"Topic filter invalid": + [PacketTypes.SUBACK, PacketTypes.UNSUBACK, PacketTypes.DISCONNECT]}, + 144: {"Topic name invalid": + [PacketTypes.CONNACK, PacketTypes.PUBACK, + PacketTypes.PUBREC, PacketTypes.DISCONNECT]}, + 145: {"Packet identifier in use": + [PacketTypes.PUBACK, PacketTypes.PUBREC, + PacketTypes.SUBACK, PacketTypes.UNSUBACK]}, + 146: {"Packet identifier not found": + [PacketTypes.PUBREL, PacketTypes.PUBCOMP]}, + 147: {"Receive maximum exceeded": [PacketTypes.DISCONNECT]}, + 148: {"Topic alias invalid": [PacketTypes.DISCONNECT]}, + 149: {"Packet too large": [PacketTypes.CONNACK, PacketTypes.DISCONNECT]}, + 150: {"Message rate too high": [PacketTypes.DISCONNECT]}, + 151: {"Quota exceeded": [PacketTypes.CONNACK, PacketTypes.PUBACK, + PacketTypes.PUBREC, PacketTypes.SUBACK, PacketTypes.DISCONNECT], }, + 152: {"Administrative action": [PacketTypes.DISCONNECT]}, + 153: {"Payload format invalid": + [PacketTypes.PUBACK, PacketTypes.PUBREC, PacketTypes.DISCONNECT]}, + 154: {"Retain not supported": + [PacketTypes.CONNACK, PacketTypes.DISCONNECT]}, + 155: {"QoS not supported": + [PacketTypes.CONNACK, PacketTypes.DISCONNECT]}, + 156: {"Use another server": + [PacketTypes.CONNACK, PacketTypes.DISCONNECT]}, + 157: {"Server moved": + [PacketTypes.CONNACK, PacketTypes.DISCONNECT]}, + 158: {"Shared subscription not supported": + [PacketTypes.SUBACK, PacketTypes.DISCONNECT]}, + 159: {"Connection rate exceeded": + [PacketTypes.CONNACK, PacketTypes.DISCONNECT]}, + 160: {"Maximum connect time": + [PacketTypes.DISCONNECT]}, + 161: {"Subscription identifiers not supported": + [PacketTypes.SUBACK, PacketTypes.DISCONNECT]}, + 162: {"Wildcard subscription not supported": + [PacketTypes.SUBACK, PacketTypes.DISCONNECT]}, + } + if identifier == -1: + if packetType == PacketTypes.DISCONNECT and aName == "Success": + aName = "Normal disconnection" + self.set(aName) + else: + self.value = identifier + self.getName() # check it's good + + def __getName__(self, packetType, identifier): + """ + Get the reason code string name for a specific identifier. + The name can vary by packet type for the same identifier, which + is why the packet type is also required. + + Used when displaying the reason code. + """ + if identifier not in self.names: + raise KeyError(identifier) + names = self.names[identifier] + namelist = [name for name in names.keys() if packetType in names[name]] + if len(namelist) != 1: + raise ValueError(f"Expected exactly one name, found {namelist!r}") + return namelist[0] + + def getId(self, name): + """ + Get the numeric id corresponding to a reason code name. + + Used when setting the reason code for a packetType + check that only valid codes for the packet are set. + """ + for code in self.names.keys(): + if name in self.names[code].keys(): + if self.packetType in self.names[code][name]: + return code + raise KeyError(f"Reason code name not found: {name}") + + def set(self, name): + self.value = self.getId(name) + + def unpack(self, buffer): + c = buffer[0] + name = self.__getName__(self.packetType, c) + self.value = self.getId(name) + return 1 + + def getName(self): + """Returns the reason code name corresponding to the numeric value which is set. + """ + return self.__getName__(self.packetType, self.value) + + def __eq__(self, other): + if isinstance(other, int): + return self.value == other + if isinstance(other, str): + return other == str(self) + if isinstance(other, ReasonCode): + return self.value == other.value + return False + + def __lt__(self, other): + if isinstance(other, int): + return self.value < other + if isinstance(other, ReasonCode): + return self.value < other.value + return NotImplemented + + def __repr__(self): + try: + packet_name = PacketTypes.Names[self.packetType] + except IndexError: + packet_name = "Unknown" + + return f"ReasonCode({packet_name}, {self.getName()!r})" + + def __str__(self): + return self.getName() + + def json(self): + return self.getName() + + def pack(self): + return bytearray([self.value]) + + @property + def is_failure(self) -> bool: + return self.value >= 0x80 + + +class _CompatibilityIsInstance(type): + def __instancecheck__(self, other: Any) -> bool: + return isinstance(other, ReasonCode) + + +class ReasonCodes(ReasonCode, metaclass=_CompatibilityIsInstance): + def __init__(self, *args, **kwargs): + warnings.warn("ReasonCodes is deprecated, use ReasonCode (singular) instead", + category=DeprecationWarning, + stacklevel=2, + ) + super().__init__(*args, **kwargs) diff --git a/scripts/tempSensor/lib/paho/mqtt/subscribe.py b/scripts/tempSensor/lib/paho/mqtt/subscribe.py new file mode 100644 index 00000000..b6c80f44 --- /dev/null +++ b/scripts/tempSensor/lib/paho/mqtt/subscribe.py @@ -0,0 +1,281 @@ +# Copyright (c) 2016 Roger Light +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Public License v2.0 +# and Eclipse Distribution License v1.0 which accompany this distribution. +# +# The Eclipse Public License is available at +# http://www.eclipse.org/legal/epl-v20.html +# and the Eclipse Distribution License is available at +# http://www.eclipse.org/org/documents/edl-v10.php. +# +# Contributors: +# Roger Light - initial API and implementation + +""" +This module provides some helper functions to allow straightforward subscribing +to topics and retrieving messages. The two functions are simple(), which +returns one or messages matching a set of topics, and callback() which allows +you to pass a callback for processing of messages. +""" + +from .. import mqtt +from . import client as paho + + +def _on_connect(client, userdata, flags, reason_code, properties): + """Internal callback""" + if reason_code != 0: + raise mqtt.MQTTException(paho.connack_string(reason_code)) + + if isinstance(userdata['topics'], list): + for topic in userdata['topics']: + client.subscribe(topic, userdata['qos']) + else: + client.subscribe(userdata['topics'], userdata['qos']) + + +def _on_message_callback(client, userdata, message): + """Internal callback""" + userdata['callback'](client, userdata['userdata'], message) + + +def _on_message_simple(client, userdata, message): + """Internal callback""" + + if userdata['msg_count'] == 0: + return + + # Don't process stale retained messages if 'retained' was false + if message.retain and not userdata['retained']: + return + + userdata['msg_count'] = userdata['msg_count'] - 1 + + if userdata['messages'] is None and userdata['msg_count'] == 0: + userdata['messages'] = message + client.disconnect() + return + + userdata['messages'].append(message) + if userdata['msg_count'] == 0: + client.disconnect() + + +def callback(callback, topics, qos=0, userdata=None, hostname="localhost", + port=1883, client_id="", keepalive=60, will=None, auth=None, + tls=None, protocol=paho.MQTTv311, transport="tcp", + clean_session=True, proxy_args=None): + """Subscribe to a list of topics and process them in a callback function. + + This function creates an MQTT client, connects to a broker and subscribes + to a list of topics. Incoming messages are processed by the user provided + callback. This is a blocking function and will never return. + + :param callback: function with the same signature as `on_message` for + processing the messages received. + + :param topics: either a string containing a single topic to subscribe to, or a + list of topics to subscribe to. + + :param int qos: the qos to use when subscribing. This is applied to all topics. + + :param userdata: passed to the callback + + :param str hostname: the address of the broker to connect to. + Defaults to localhost. + + :param int port: the port to connect to the broker on. Defaults to 1883. + + :param str client_id: the MQTT client id to use. If "" or None, the Paho library will + generate a client id automatically. + + :param int keepalive: the keepalive timeout value for the client. Defaults to 60 + seconds. + + :param will: a dict containing will parameters for the client: will = {'topic': + "", 'payload':", 'qos':, 'retain':}. + Topic is required, all other parameters are optional and will + default to None, 0 and False respectively. + + Defaults to None, which indicates no will should be used. + + :param auth: a dict containing authentication parameters for the client: + auth = {'username':"", 'password':""} + Username is required, password is optional and will default to None + if not provided. + Defaults to None, which indicates no authentication is to be used. + + :param tls: a dict containing TLS configuration parameters for the client: + dict = {'ca_certs':"", 'certfile':"", + 'keyfile':"", 'tls_version':"", + 'ciphers':", 'insecure':""} + ca_certs is required, all other parameters are optional and will + default to None if not provided, which results in the client using + the default behaviour - see the paho.mqtt.client documentation. + Alternatively, tls input can be an SSLContext object, which will be + processed using the tls_set_context method. + Defaults to None, which indicates that TLS should not be used. + + :param str transport: set to "tcp" to use the default setting of transport which is + raw TCP. Set to "websockets" to use WebSockets as the transport. + + :param clean_session: a boolean that determines the client type. If True, + the broker will remove all information about this client + when it disconnects. If False, the client is a persistent + client and subscription information and queued messages + will be retained when the client disconnects. + Defaults to True. + + :param proxy_args: a dictionary that will be given to the client. + """ + + if qos < 0 or qos > 2: + raise ValueError('qos must be in the range 0-2') + + callback_userdata = { + 'callback':callback, + 'topics':topics, + 'qos':qos, + 'userdata':userdata} + + client = paho.Client( + paho.CallbackAPIVersion.VERSION2, + client_id=client_id, + userdata=callback_userdata, + protocol=protocol, + transport=transport, + clean_session=clean_session, + ) + client.enable_logger() + + client.on_message = _on_message_callback + client.on_connect = _on_connect + + if proxy_args is not None: + client.proxy_set(**proxy_args) + + if auth: + username = auth.get('username') + if username: + password = auth.get('password') + client.username_pw_set(username, password) + else: + raise KeyError("The 'username' key was not found, this is " + "required for auth") + + if will is not None: + client.will_set(**will) + + if tls is not None: + if isinstance(tls, dict): + insecure = tls.pop('insecure', False) + client.tls_set(**tls) + if insecure: + # Must be set *after* the `client.tls_set()` call since it sets + # up the SSL context that `client.tls_insecure_set` alters. + client.tls_insecure_set(insecure) + else: + # Assume input is SSLContext object + client.tls_set_context(tls) + + client.connect(hostname, port, keepalive) + client.loop_forever() + + +def simple(topics, qos=0, msg_count=1, retained=True, hostname="localhost", + port=1883, client_id="", keepalive=60, will=None, auth=None, + tls=None, protocol=paho.MQTTv311, transport="tcp", + clean_session=True, proxy_args=None): + """Subscribe to a list of topics and return msg_count messages. + + This function creates an MQTT client, connects to a broker and subscribes + to a list of topics. Once "msg_count" messages have been received, it + disconnects cleanly from the broker and returns the messages. + + :param topics: either a string containing a single topic to subscribe to, or a + list of topics to subscribe to. + + :param int qos: the qos to use when subscribing. This is applied to all topics. + + :param int msg_count: the number of messages to retrieve from the broker. + if msg_count == 1 then a single MQTTMessage will be returned. + if msg_count > 1 then a list of MQTTMessages will be returned. + + :param bool retained: If set to True, retained messages will be processed the same as + non-retained messages. If set to False, retained messages will + be ignored. This means that with retained=False and msg_count=1, + the function will return the first message received that does + not have the retained flag set. + + :param str hostname: the address of the broker to connect to. + Defaults to localhost. + + :param int port: the port to connect to the broker on. Defaults to 1883. + + :param str client_id: the MQTT client id to use. If "" or None, the Paho library will + generate a client id automatically. + + :param int keepalive: the keepalive timeout value for the client. Defaults to 60 + seconds. + + :param will: a dict containing will parameters for the client: will = {'topic': + "", 'payload':", 'qos':, 'retain':}. + Topic is required, all other parameters are optional and will + default to None, 0 and False respectively. + Defaults to None, which indicates no will should be used. + + :param auth: a dict containing authentication parameters for the client: + auth = {'username':"", 'password':""} + Username is required, password is optional and will default to None + if not provided. + Defaults to None, which indicates no authentication is to be used. + + :param tls: a dict containing TLS configuration parameters for the client: + dict = {'ca_certs':"", 'certfile':"", + 'keyfile':"", 'tls_version':"", + 'ciphers':", 'insecure':""} + ca_certs is required, all other parameters are optional and will + default to None if not provided, which results in the client using + the default behaviour - see the paho.mqtt.client documentation. + Alternatively, tls input can be an SSLContext object, which will be + processed using the tls_set_context method. + Defaults to None, which indicates that TLS should not be used. + + :param protocol: the MQTT protocol version to use. Defaults to MQTTv311. + + :param transport: set to "tcp" to use the default setting of transport which is + raw TCP. Set to "websockets" to use WebSockets as the transport. + + :param clean_session: a boolean that determines the client type. If True, + the broker will remove all information about this client + when it disconnects. If False, the client is a persistent + client and subscription information and queued messages + will be retained when the client disconnects. + Defaults to True. If protocol is MQTTv50, clean_session + is ignored. + + :param proxy_args: a dictionary that will be given to the client. + """ + + if msg_count < 1: + raise ValueError('msg_count must be > 0') + + # Set ourselves up to return a single message if msg_count == 1, or a list + # if > 1. + if msg_count == 1: + messages = None + else: + messages = [] + + # Ignore clean_session if protocol is MQTTv50, otherwise Client will raise + if protocol == paho.MQTTv5: + clean_session = None + + userdata = {'retained':retained, 'msg_count':msg_count, 'messages':messages} + + callback(_on_message_simple, topics, qos, userdata, hostname, port, + client_id, keepalive, will, auth, tls, protocol, transport, + clean_session, proxy_args) + + return userdata['messages'] diff --git a/scripts/tempSensor/lib/paho/mqtt/subscribeoptions.py b/scripts/tempSensor/lib/paho/mqtt/subscribeoptions.py new file mode 100644 index 00000000..7e0605de --- /dev/null +++ b/scripts/tempSensor/lib/paho/mqtt/subscribeoptions.py @@ -0,0 +1,113 @@ +""" +******************************************************************* + Copyright (c) 2017, 2019 IBM Corp. + + All rights reserved. This program and the accompanying materials + are made available under the terms of the Eclipse Public License v2.0 + and Eclipse Distribution License v1.0 which accompany this distribution. + + The Eclipse Public License is available at + http://www.eclipse.org/legal/epl-v20.html + and the Eclipse Distribution License is available at + http://www.eclipse.org/org/documents/edl-v10.php. + + Contributors: + Ian Craggs - initial implementation and/or documentation +******************************************************************* +""" + + + +class MQTTException(Exception): + pass + + +class SubscribeOptions: + """The MQTT v5.0 subscribe options class. + + The options are: + qos: As in MQTT v3.1.1. + noLocal: True or False. If set to True, the subscriber will not receive its own publications. + retainAsPublished: True or False. If set to True, the retain flag on received publications will be as set + by the publisher. + retainHandling: RETAIN_SEND_ON_SUBSCRIBE, RETAIN_SEND_IF_NEW_SUB or RETAIN_DO_NOT_SEND + Controls when the broker should send retained messages: + - RETAIN_SEND_ON_SUBSCRIBE: on any successful subscribe request + - RETAIN_SEND_IF_NEW_SUB: only if the subscribe request is new + - RETAIN_DO_NOT_SEND: never send retained messages + """ + + # retain handling options + RETAIN_SEND_ON_SUBSCRIBE, RETAIN_SEND_IF_NEW_SUB, RETAIN_DO_NOT_SEND = range( + 0, 3) + + def __init__( + self, + qos: int = 0, + noLocal: bool = False, + retainAsPublished: bool = False, + retainHandling: int = RETAIN_SEND_ON_SUBSCRIBE, + ): + """ + qos: 0, 1 or 2. 0 is the default. + noLocal: True or False. False is the default and corresponds to MQTT v3.1.1 behavior. + retainAsPublished: True or False. False is the default and corresponds to MQTT v3.1.1 behavior. + retainHandling: RETAIN_SEND_ON_SUBSCRIBE, RETAIN_SEND_IF_NEW_SUB or RETAIN_DO_NOT_SEND + RETAIN_SEND_ON_SUBSCRIBE is the default and corresponds to MQTT v3.1.1 behavior. + """ + object.__setattr__(self, "names", + ["QoS", "noLocal", "retainAsPublished", "retainHandling"]) + self.QoS = qos # bits 0,1 + self.noLocal = noLocal # bit 2 + self.retainAsPublished = retainAsPublished # bit 3 + self.retainHandling = retainHandling # bits 4 and 5: 0, 1 or 2 + if self.retainHandling not in (0, 1, 2): + raise AssertionError(f"Retain handling should be 0, 1 or 2, not {self.retainHandling}") + if self.QoS not in (0, 1, 2): + raise AssertionError(f"QoS should be 0, 1 or 2, not {self.QoS}") + + def __setattr__(self, name, value): + if name not in self.names: + raise MQTTException( + f"{name} Attribute name must be one of {self.names}") + object.__setattr__(self, name, value) + + def pack(self): + if self.retainHandling not in (0, 1, 2): + raise AssertionError(f"Retain handling should be 0, 1 or 2, not {self.retainHandling}") + if self.QoS not in (0, 1, 2): + raise AssertionError(f"QoS should be 0, 1 or 2, not {self.QoS}") + noLocal = 1 if self.noLocal else 0 + retainAsPublished = 1 if self.retainAsPublished else 0 + data = [(self.retainHandling << 4) | (retainAsPublished << 3) | + (noLocal << 2) | self.QoS] + return bytes(data) + + def unpack(self, buffer): + b0 = buffer[0] + self.retainHandling = ((b0 >> 4) & 0x03) + self.retainAsPublished = True if ((b0 >> 3) & 0x01) == 1 else False + self.noLocal = True if ((b0 >> 2) & 0x01) == 1 else False + self.QoS = (b0 & 0x03) + if self.retainHandling not in (0, 1, 2): + raise AssertionError(f"Retain handling should be 0, 1 or 2, not {self.retainHandling}") + if self.QoS not in (0, 1, 2): + raise AssertionError(f"QoS should be 0, 1 or 2, not {self.QoS}") + return 1 + + def __repr__(self): + return str(self) + + def __str__(self): + return "{QoS="+str(self.QoS)+", noLocal="+str(self.noLocal) +\ + ", retainAsPublished="+str(self.retainAsPublished) +\ + ", retainHandling="+str(self.retainHandling)+"}" + + def json(self): + data = { + "QoS": self.QoS, + "noLocal": self.noLocal, + "retainAsPublished": self.retainAsPublished, + "retainHandling": self.retainHandling, + } + return data diff --git a/scripts/tempSensor/lib/paho_mqtt-2.1.0.dist-info/METADATA b/scripts/tempSensor/lib/paho_mqtt-2.1.0.dist-info/METADATA new file mode 100644 index 00000000..bf1408ca --- /dev/null +++ b/scripts/tempSensor/lib/paho_mqtt-2.1.0.dist-info/METADATA @@ -0,0 +1,635 @@ +Metadata-Version: 2.3 +Name: paho-mqtt +Version: 2.1.0 +Summary: MQTT version 5.0/3.1.1 client class +Project-URL: Homepage, http://eclipse.org/paho +Author-email: Roger Light +License: EPL-2.0 OR BSD-3-Clause +License-File: LICENSE.txt +Keywords: paho +Classifier: Development Status :: 4 - Beta +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved +Classifier: Natural Language :: English +Classifier: Operating System :: MacOS :: MacOS X +Classifier: Operating System :: Microsoft :: Windows +Classifier: Operating System :: POSIX +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Topic :: Communications +Classifier: Topic :: Internet +Requires-Python: >=3.7 +Provides-Extra: proxy +Requires-Dist: pysocks; extra == 'proxy' +Description-Content-Type: text/x-rst + +Eclipse Paho™ MQTT Python Client +================================ + +The `full documentation is available here `_. + +**Warning breaking change** - Release 2.0 contains a breaking change; see the `release notes `_ and `migration details `_. + +This document describes the source code for the `Eclipse Paho `_ MQTT Python client library, which implements versions 5.0, 3.1.1, and 3.1 of the MQTT protocol. + +This code provides a client class which enables applications to connect to an `MQTT `_ broker to publish messages, and to subscribe to topics and receive published messages. It also provides some helper functions to make publishing one off messages to an MQTT server very straightforward. + +It supports Python 3.7+. + +The MQTT protocol is a machine-to-machine (M2M)/"Internet of Things" connectivity protocol. Designed as an extremely lightweight publish/subscribe messaging transport, it is useful for connections with remote locations where a small code footprint is required and/or network bandwidth is at a premium. + +Paho is an `Eclipse Foundation `_ project. + +Contents +-------- + +* Installation_ +* `Known limitations`_ +* `Usage and API`_ + * `Getting Started`_ + * `Client`_ + * `Network loop`_ + * `Callbacks`_ + * `Logger`_ + * `External event loop support`_ + * `Global helper functions`_ + * `Publish`_ + * `Single`_ + * `Multiple`_ + * `Subscribe`_ + * `Simple`_ + * `Using Callback`_ +* `Reporting bugs`_ +* `More information`_ + + +Installation +------------ + +The latest stable version is available in the Python Package Index (PyPi) and can be installed using + +:: + + pip install paho-mqtt + +Or with ``virtualenv``: + +:: + + virtualenv paho-mqtt + source paho-mqtt/bin/activate + pip install paho-mqtt + +To obtain the full code, including examples and tests, you can clone the git repository: + +:: + + git clone https://github.com/eclipse/paho.mqtt.python + + +Once you have the code, it can be installed from your repository as well: + +:: + + cd paho.mqtt.python + pip install -e . + +To perform all tests (including MQTT v5 tests), you also need to clone paho.mqtt.testing in paho.mqtt.python folder:: + + git clone https://github.com/eclipse/paho.mqtt.testing.git + cd paho.mqtt.testing + git checkout a4dc694010217b291ee78ee13a6d1db812f9babd + +Known limitations +----------------- + +The following are the known unimplemented MQTT features. + +When ``clean_session`` is False, the session is only stored in memory and not persisted. This means that +when the client is restarted (not just reconnected, the object is recreated usually because the +program was restarted) the session is lost. This results in a possible message loss. + +The following part of the client session is lost: + +* QoS 2 messages which have been received from the server, but have not been completely acknowledged. + + Since the client will blindly acknowledge any PUBCOMP (last message of a QoS 2 transaction), it + won't hang but will lose this QoS 2 message. + +* QoS 1 and QoS 2 messages which have been sent to the server, but have not been completely acknowledged. + + This means that messages passed to ``publish()`` may be lost. This could be mitigated by taking care + that all messages passed to ``publish()`` have a corresponding ``on_publish()`` call or use `wait_for_publish`. + + It also means that the broker may have the QoS2 message in the session. Since the client starts + with an empty session it don't know it and will reuse the mid. This is not yet fixed. + +Also, when ``clean_session`` is True, this library will republish QoS > 0 message across network +reconnection. This means that QoS > 0 message won't be lost. But the standard says that +we should discard any message for which the publish packet was sent. Our choice means that +we are not compliant with the standard and it's possible for QoS 2 to be received twice. + +You should set ``clean_session = False`` if you need the QoS 2 guarantee of only one delivery. + +Usage and API +------------- + +Detailed API documentation `is available online `_ or could be built from ``docs/`` and samples are available in the `examples`_ directory. + +The package provides two modules, a full `Client` and few `helpers` for simple publishing or subscribing. + +Getting Started +*************** + +Here is a very simple example that subscribes to the broker $SYS topic tree and prints out the resulting messages: + +.. code:: python + + import paho.mqtt.client as mqtt + + # The callback for when the client receives a CONNACK response from the server. + def on_connect(client, userdata, flags, reason_code, properties): + print(f"Connected with result code {reason_code}") + # Subscribing in on_connect() means that if we lose the connection and + # reconnect then subscriptions will be renewed. + client.subscribe("$SYS/#") + + # The callback for when a PUBLISH message is received from the server. + def on_message(client, userdata, msg): + print(msg.topic+" "+str(msg.payload)) + + mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2) + mqttc.on_connect = on_connect + mqttc.on_message = on_message + + mqttc.connect("mqtt.eclipseprojects.io", 1883, 60) + + # Blocking call that processes network traffic, dispatches callbacks and + # handles reconnecting. + # Other loop*() functions are available that give a threaded interface and a + # manual interface. + mqttc.loop_forever() + +Client +****** + +You can use the client class as an instance, within a class or by subclassing. The general usage flow is as follows: + +* Create a client instance +* Connect to a broker using one of the ``connect*()`` functions +* Call one of the ``loop*()`` functions to maintain network traffic flow with the broker +* Use ``subscribe()`` to subscribe to a topic and receive messages +* Use ``publish()`` to publish messages to the broker +* Use ``disconnect()`` to disconnect from the broker + +Callbacks will be called to allow the application to process events as necessary. These callbacks are described below. + +Network loop +```````````` + +These functions are the driving force behind the client. If they are not +called, incoming network data will not be processed and outgoing network data +will not be sent. There are four options for managing the +network loop. Three are described here, the fourth in "External event loop +support" below. Do not mix the different loop functions. + +loop_start() / loop_stop() +'''''''''''''''''''''''''' + +.. code:: python + + mqttc.loop_start() + + while True: + temperature = sensor.blocking_read() + mqttc.publish("paho/temperature", temperature) + + mqttc.loop_stop() + +These functions implement a threaded interface to the network loop. Calling +`loop_start()` once, before or after ``connect*()``, runs a thread in the +background to call `loop()` automatically. This frees up the main thread for +other work that may be blocking. This call also handles reconnecting to the +broker. Call `loop_stop()` to stop the background thread. +The loop is also stopped if you call `disconnect()`. + +loop_forever() +'''''''''''''' + +.. code:: python + + mqttc.loop_forever(retry_first_connection=False) + +This is a blocking form of the network loop and will not return until the +client calls `disconnect()`. It automatically handles reconnecting. + +Except for the first connection attempt when using `connect_async`, use +``retry_first_connection=True`` to make it retry the first connection. + +*Warning*: This might lead to situations where the client keeps connecting to an +non existing host without failing. + +loop() +'''''' + +.. code:: python + + run = True + while run: + rc = mqttc.loop(timeout=1.0) + if rc != 0: + # need to handle error, possible reconnecting or stopping the application + +Call regularly to process network events. This call waits in ``select()`` until +the network socket is available for reading or writing, if appropriate, then +handles the incoming/outgoing data. This function blocks for up to ``timeout`` +seconds. ``timeout`` must not exceed the ``keepalive`` value for the client or +your client will be regularly disconnected by the broker. + +Using this kind of loop, require you to handle reconnection strategie. + + +Callbacks +````````` + +The interface to interact with paho-mqtt include various callback that are called by +the library when some events occur. + +The callbacks are functions defined in your code, to implement the require action on those events. This could +be simply printing received message or much more complex behaviour. + +Callbacks API is versioned, and the selected version is the `CallbackAPIVersion` you provided to `Client` +constructor. Currently two version are supported: + +* ``CallbackAPIVersion.VERSION1``: it's the historical version used in paho-mqtt before version 2.0. + It's the API used before the introduction of `CallbackAPIVersion`. + This version is deprecated and will be removed in paho-mqtt version 3.0. +* ``CallbackAPIVersion.VERSION2``: This version is more consistent between protocol MQTT 3.x and MQTT 5.x. It's also + much more usable with MQTT 5.x since reason code and properties are always provided when available. + It's recommended for all user to upgrade to this version. It's highly recommended for MQTT 5.x user. + +The following callbacks exists: + +* `on_connect()`: called when the CONNACK from the broker is received. The call could be for a refused connection, + check the reason_code to see if the connection is successful or rejected. +* `on_connect_fail()`: called by `loop_forever()` and `loop_start()` when the TCP connection failed to establish. + This callback is not called when using `connect()` or `reconnect()` directly. It's only called following + an automatic (re)connection made by `loop_start()` and `loop_forever()` +* `on_disconnect()`: called when the connection is closed. +* `on_message()`: called when a MQTT message is received from the broker. +* `on_publish()`: called when an MQTT message was sent to the broker. Depending on QoS level the callback is called + at different moment: + + * For QoS == 0, it's called as soon as the message is sent over the network. This could be before the corresponding ``publish()`` return. + * For QoS == 1, it's called when the corresponding PUBACK is received from the broker + * For QoS == 2, it's called when the corresponding PUBCOMP is received from the broker +* `on_subscribe()`: called when the SUBACK is received from the broker +* `on_unsubscribe()`: called when the UNSUBACK is received from the broker +* `on_log()`: called when the library log a message +* `on_socket_open`, `on_socket_close`, `on_socket_register_write`, `on_socket_unregister_write`: callbacks used for external loop support. See below for details. + +For the signature of each callback, see the `online documentation `_. + +Subscriber example +'''''''''''''''''' + +.. code:: python + + import paho.mqtt.client as mqtt + + def on_subscribe(client, userdata, mid, reason_code_list, properties): + # Since we subscribed only for a single channel, reason_code_list contains + # a single entry + if reason_code_list[0].is_failure: + print(f"Broker rejected you subscription: {reason_code_list[0]}") + else: + print(f"Broker granted the following QoS: {reason_code_list[0].value}") + + def on_unsubscribe(client, userdata, mid, reason_code_list, properties): + # Be careful, the reason_code_list is only present in MQTTv5. + # In MQTTv3 it will always be empty + if len(reason_code_list) == 0 or not reason_code_list[0].is_failure: + print("unsubscribe succeeded (if SUBACK is received in MQTTv3 it success)") + else: + print(f"Broker replied with failure: {reason_code_list[0]}") + client.disconnect() + + def on_message(client, userdata, message): + # userdata is the structure we choose to provide, here it's a list() + userdata.append(message.payload) + # We only want to process 10 messages + if len(userdata) >= 10: + client.unsubscribe("$SYS/#") + + def on_connect(client, userdata, flags, reason_code, properties): + if reason_code.is_failure: + print(f"Failed to connect: {reason_code}. loop_forever() will retry connection") + else: + # we should always subscribe from on_connect callback to be sure + # our subscribed is persisted across reconnections. + client.subscribe("$SYS/#") + + mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2) + mqttc.on_connect = on_connect + mqttc.on_message = on_message + mqttc.on_subscribe = on_subscribe + mqttc.on_unsubscribe = on_unsubscribe + + mqttc.user_data_set([]) + mqttc.connect("mqtt.eclipseprojects.io") + mqttc.loop_forever() + print(f"Received the following message: {mqttc.user_data_get()}") + +publisher example +''''''''''''''''' + +.. code:: python + + import time + import paho.mqtt.client as mqtt + + def on_publish(client, userdata, mid, reason_code, properties): + # reason_code and properties will only be present in MQTTv5. It's always unset in MQTTv3 + try: + userdata.remove(mid) + except KeyError: + print("on_publish() is called with a mid not present in unacked_publish") + print("This is due to an unavoidable race-condition:") + print("* publish() return the mid of the message sent.") + print("* mid from publish() is added to unacked_publish by the main thread") + print("* on_publish() is called by the loop_start thread") + print("While unlikely (because on_publish() will be called after a network round-trip),") + print(" this is a race-condition that COULD happen") + print("") + print("The best solution to avoid race-condition is using the msg_info from publish()") + print("We could also try using a list of acknowledged mid rather than removing from pending list,") + print("but remember that mid could be re-used !") + + unacked_publish = set() + mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2) + mqttc.on_publish = on_publish + + mqttc.user_data_set(unacked_publish) + mqttc.connect("mqtt.eclipseprojects.io") + mqttc.loop_start() + + # Our application produce some messages + msg_info = mqttc.publish("paho/test/topic", "my message", qos=1) + unacked_publish.add(msg_info.mid) + + msg_info2 = mqttc.publish("paho/test/topic", "my message2", qos=1) + unacked_publish.add(msg_info2.mid) + + # Wait for all message to be published + while len(unacked_publish): + time.sleep(0.1) + + # Due to race-condition described above, the following way to wait for all publish is safer + msg_info.wait_for_publish() + msg_info2.wait_for_publish() + + mqttc.disconnect() + mqttc.loop_stop() + + +Logger +`````` + +The Client emit some log message that could be useful during troubleshooting. The easiest way to +enable logs is the call `enable_logger()`. It's possible to provide a custom logger or let the +default logger being used. + +Example: + +.. code:: python + + import logging + import paho.mqtt.client as mqtt + + logging.basicConfig(level=logging.DEBUG) + + mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2) + mqttc.enable_logger() + + mqttc.connect("mqtt.eclipseprojects.io", 1883, 60) + mqttc.loop_start() + + # Do additional action needed, publish, subscribe, ... + [...] + +It's also possible to define a on_log callback that will receive a copy of all log messages. Example: + +.. code:: python + + import paho.mqtt.client as mqtt + + def on_log(client, userdata, paho_log_level, messages): + if paho_log_level == mqtt.LogLevel.MQTT_LOG_ERR: + print(message) + + mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2) + mqttc.on_log = on_log + + mqttc.connect("mqtt.eclipseprojects.io", 1883, 60) + mqttc.loop_start() + + # Do additional action needed, publish, subscribe, ... + [...] + + +The correspondence with Paho logging levels and standard ones is the following: + +==================== =============== +Paho logging +==================== =============== +``MQTT_LOG_ERR`` ``logging.ERROR`` +``MQTT_LOG_WARNING`` ``logging.WARNING`` +``MQTT_LOG_NOTICE`` ``logging.INFO`` *(no direct equivalent)* +``MQTT_LOG_INFO`` ``logging.INFO`` +``MQTT_LOG_DEBUG`` ``logging.DEBUG`` +==================== =============== + + +External event loop support +``````````````````````````` + +To support other network loop like asyncio (see examples_), the library expose some +method and callback to support those use-case. + +The following loop method exists: + +* `loop_read`: should be called when the socket is ready for reading. +* `loop_write`: should be called when the socket is ready for writing AND the library want to write data. +* `loop_misc`: should be called every few seconds to handle message retrying and pings. + +In pseudo code, it give the following: + +.. code:: python + + while run: + if need_read: + mqttc.loop_read() + if need_write: + mqttc.loop_write() + mqttc.loop_misc() + + if not need_read and not need_write: + # But don't wait more than few seconds, loop_misc() need to be called regularly + wait_for_change_in_need_read_or_write() + updated_need_read_and_write() + +The tricky part is implementing the update of need_read / need_write and wait for condition change. To support +this, the following method exists: + +* `socket()`: which return the socket object when the TCP connection is open. + This call is particularly useful for select_ based loops. See ``examples/loop_select.py``. +* `want_write()`: return true if there is data waiting to be written. This is close to the + ``need_writew`` of above pseudo-code, but you should also check whether the socket is ready for writing. +* callbacks ``on_socket_*``: + + * `on_socket_open`: called when the socket is opened. + * `on_socket_close`: called when the socket is about to be closed. + * `on_socket_register_write`: called when there is data the client want to write on the socket + * `on_socket_unregister_write`: called when there is no more data to write on the socket. + + Callbacks are particularly useful for event loops where you register or unregister a socket + for reading+writing. See ``examples/loop_asyncio.py`` for an example. + +.. _select: https://docs.python.org/3/library/select.html#select.select + +The callbacks are always called in this order: + +- `on_socket_open` +- Zero or more times: + + - `on_socket_register_write` + - `on_socket_unregister_write` + +- `on_socket_close` + +Global helper functions +``````````````````````` + +The client module also offers some global helper functions. + +``topic_matches_sub(sub, topic)`` can be used to check whether a ``topic`` +matches a ``subscription``. + +For example: + + the topic ``foo/bar`` would match the subscription ``foo/#`` or ``+/bar`` + + the topic ``non/matching`` would not match the subscription ``non/+/+`` + + +Publish +******* + +This module provides some helper functions to allow straightforward publishing +of messages in a one-shot manner. In other words, they are useful for the +situation where you have a single/multiple messages you want to publish to a +broker, then disconnect with nothing else required. + +The two functions provided are `single()` and `multiple()`. + +Both functions include support for MQTT v5.0, but do not currently let you +set any properties on connection or when sending messages. + +Single +`````` + +Publish a single message to a broker, then disconnect cleanly. + +Example: + +.. code:: python + + import paho.mqtt.publish as publish + + publish.single("paho/test/topic", "payload", hostname="mqtt.eclipseprojects.io") + +Multiple +```````` + +Publish multiple messages to a broker, then disconnect cleanly. + +Example: + +.. code:: python + + from paho.mqtt.enums import MQTTProtocolVersion + import paho.mqtt.publish as publish + + msgs = [{'topic':"paho/test/topic", 'payload':"multiple 1"}, + ("paho/test/topic", "multiple 2", 0, False)] + publish.multiple(msgs, hostname="mqtt.eclipseprojects.io", protocol=MQTTProtocolVersion.MQTTv5) + + +Subscribe +********* + +This module provides some helper functions to allow straightforward subscribing +and processing of messages. + +The two functions provided are `simple()` and `callback()`. + +Both functions include support for MQTT v5.0, but do not currently let you +set any properties on connection or when subscribing. + +Simple +`````` + +Subscribe to a set of topics and return the messages received. This is a +blocking function. + +Example: + +.. code:: python + + import paho.mqtt.subscribe as subscribe + + msg = subscribe.simple("paho/test/topic", hostname="mqtt.eclipseprojects.io") + print("%s %s" % (msg.topic, msg.payload)) + +Using Callback +`````````````` + +Subscribe to a set of topics and process the messages received using a user +provided callback. + +Example: + +.. code:: python + + import paho.mqtt.subscribe as subscribe + + def on_message_print(client, userdata, message): + print("%s %s" % (message.topic, message.payload)) + userdata["message_count"] += 1 + if userdata["message_count"] >= 5: + # it's possible to stop the program by disconnecting + client.disconnect() + + subscribe.callback(on_message_print, "paho/test/topic", hostname="mqtt.eclipseprojects.io", userdata={"message_count": 0}) + + +Reporting bugs +-------------- + +Please report bugs in the issues tracker at https://github.com/eclipse/paho.mqtt.python/issues. + +More information +---------------- + +Discussion of the Paho clients takes place on the `Eclipse paho-dev mailing list `_. + +General questions about the MQTT protocol itself (not this library) are discussed in the `MQTT Google Group `_. + +There is much more information available via the `MQTT community site `_. + +.. _examples: https://github.com/eclipse/paho.mqtt.python/tree/master/examples +.. _documentation: https://eclipse.dev/paho/files/paho.mqtt.python/html/client.html diff --git a/scripts/tempSensor/lib/paho_mqtt-2.1.0.dist-info/RECORD b/scripts/tempSensor/lib/paho_mqtt-2.1.0.dist-info/RECORD new file mode 100644 index 00000000..ee00eada --- /dev/null +++ b/scripts/tempSensor/lib/paho_mqtt-2.1.0.dist-info/RECORD @@ -0,0 +1,14 @@ +paho/__init__.py,, +paho/mqtt/__init__.py,, +paho/mqtt/client.py,, +paho/mqtt/enums.py,, +paho/mqtt/matcher.py,, +paho/mqtt/packettypes.py,, +paho/mqtt/properties.py,, +paho/mqtt/publish.py,, +paho/mqtt/py.typed,, +paho/mqtt/reasoncodes.py,, +paho/mqtt/subscribe.py,, +paho/mqtt/subscribeoptions.py,, +paho_mqtt-2.1.0.dist-info/METADATA,, +paho_mqtt-2.1.0.dist-info/RECORD,, \ No newline at end of file diff --git a/scripts/tempSensor/lib/requests-0.10.0.dist-info/METADATA b/scripts/tempSensor/lib/requests-0.10.0.dist-info/METADATA new file mode 100644 index 00000000..5d4b463d --- /dev/null +++ b/scripts/tempSensor/lib/requests-0.10.0.dist-info/METADATA @@ -0,0 +1,6 @@ +Metadata-Version: 2.1 +Name: requests +Version: 0.10.0 +Summary: +Author: +License: MIT diff --git a/scripts/tempSensor/lib/requests-0.10.0.dist-info/RECORD b/scripts/tempSensor/lib/requests-0.10.0.dist-info/RECORD new file mode 100644 index 00000000..cc88f4b5 --- /dev/null +++ b/scripts/tempSensor/lib/requests-0.10.0.dist-info/RECORD @@ -0,0 +1,3 @@ +requests-0.10.0.dist-info/METADATA,, +requests/__init__.py,, +requests-0.10.0.dist-info/RECORD,, \ No newline at end of file diff --git a/scripts/tempSensor/lib/requests/__init__.py b/scripts/tempSensor/lib/requests/__init__.py new file mode 100644 index 00000000..bf529b6d --- /dev/null +++ b/scripts/tempSensor/lib/requests/__init__.py @@ -0,0 +1,220 @@ +import socket + + +class Response: + def __init__(self, f): + self.raw = f + self.encoding = "utf-8" + self._cached = None + + def close(self): + if self.raw: + self.raw.close() + self.raw = None + self._cached = None + + @property + def content(self): + if self._cached is None: + try: + self._cached = self.raw.read() + finally: + self.raw.close() + self.raw = None + return self._cached + + @property + def text(self): + return str(self.content, self.encoding) + + def json(self): + import json + + return json.loads(self.content) + + +def request( + method, + url, + data=None, + json=None, + headers=None, + stream=None, + auth=None, + timeout=None, + parse_headers=True, +): + if headers is None: + headers = {} + + redirect = None # redirection url, None means no redirection + chunked_data = data and getattr(data, "__next__", None) and not getattr(data, "__len__", None) + + if auth is not None: + import binascii + + username, password = auth + formated = b"{}:{}".format(username, password) + formated = str(binascii.b2a_base64(formated)[:-1], "ascii") + headers["Authorization"] = "Basic {}".format(formated) + + try: + proto, dummy, host, path = url.split("/", 3) + except ValueError: + proto, dummy, host = url.split("/", 2) + path = "" + if proto == "http:": + port = 80 + elif proto == "https:": + import tls + + port = 443 + else: + raise ValueError("Unsupported protocol: " + proto) + + if ":" in host: + host, port = host.split(":", 1) + port = int(port) + + ai = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM) + ai = ai[0] + + resp_d = None + if parse_headers is not False: + resp_d = {} + + s = socket.socket(ai[0], socket.SOCK_STREAM, ai[2]) + + if timeout is not None: + # Note: settimeout is not supported on all platforms, will raise + # an AttributeError if not available. + s.settimeout(timeout) + + try: + s.connect(ai[-1]) + if proto == "https:": + context = tls.SSLContext(tls.PROTOCOL_TLS_CLIENT) + context.verify_mode = tls.CERT_NONE + s = context.wrap_socket(s, server_hostname=host) + s.write(b"%s /%s HTTP/1.0\r\n" % (method, path)) + + if "Host" not in headers: + headers["Host"] = host + + if json is not None: + assert data is None + from json import dumps + + data = dumps(json) + + if "Content-Type" not in headers: + headers["Content-Type"] = "application/json" + + if data: + if chunked_data: + if "Transfer-Encoding" not in headers and "Content-Length" not in headers: + headers["Transfer-Encoding"] = "chunked" + elif "Content-Length" not in headers: + headers["Content-Length"] = str(len(data)) + + if "Connection" not in headers: + headers["Connection"] = "close" + + # Iterate over keys to avoid tuple alloc + for k in headers: + s.write(k) + s.write(b": ") + s.write(headers[k]) + s.write(b"\r\n") + + s.write(b"\r\n") + + if data: + if chunked_data: + if headers.get("Transfer-Encoding", None) == "chunked": + for chunk in data: + s.write(b"%x\r\n" % len(chunk)) + s.write(chunk) + s.write(b"\r\n") + s.write("0\r\n\r\n") + else: + for chunk in data: + s.write(chunk) + else: + s.write(data) + + l = s.readline() + # print(l) + l = l.split(None, 2) + if len(l) < 2: + # Invalid response + raise ValueError("HTTP error: BadStatusLine:\n%s" % l) + status = int(l[1]) + reason = "" + if len(l) > 2: + reason = l[2].rstrip() + while True: + l = s.readline() + if not l or l == b"\r\n": + break + # print(l) + if l.startswith(b"Transfer-Encoding:"): + if b"chunked" in l: + raise ValueError("Unsupported " + str(l, "utf-8")) + elif l.startswith(b"Location:") and not 200 <= status <= 299: + if status in [301, 302, 303, 307, 308]: + redirect = str(l[10:-2], "utf-8") + else: + raise NotImplementedError("Redirect %d not yet supported" % status) + if parse_headers is False: + pass + elif parse_headers is True: + l = str(l, "utf-8") + k, v = l.split(":", 1) + resp_d[k] = v.strip() + else: + parse_headers(l, resp_d) + except OSError: + s.close() + raise + + if redirect: + s.close() + if status in [301, 302, 303]: + return request("GET", redirect, None, None, headers, stream) + else: + return request(method, redirect, data, json, headers, stream) + else: + resp = Response(s) + resp.status_code = status + resp.reason = reason + if resp_d is not None: + resp.headers = resp_d + return resp + + +def head(url, **kw): + return request("HEAD", url, **kw) + + +def get(url, **kw): + return request("GET", url, **kw) + + +def post(url, **kw): + return request("POST", url, **kw) + + +def put(url, **kw): + return request("PUT", url, **kw) + + +def patch(url, **kw): + return request("PATCH", url, **kw) + + +def delete(url, **kw): + return request("DELETE", url, **kw) + + +__version__ = '0.10.0' diff --git a/scripts/tempSensor/lib/smbus2-0.4.3.dist-info/METADATA b/scripts/tempSensor/lib/smbus2-0.4.3.dist-info/METADATA new file mode 100644 index 00000000..3db43c8e --- /dev/null +++ b/scripts/tempSensor/lib/smbus2-0.4.3.dist-info/METADATA @@ -0,0 +1,237 @@ +Metadata-Version: 2.1 +Name: smbus2 +Version: 0.4.3 +Summary: smbus2 is a drop-in replacement for smbus-cffi/smbus-python in pure Python +Home-page: https://github.com/kplindegaard/smbus2 +Author: Karl-Petter Lindegaard +Author-email: kp.lindegaard@gmail.com +License: MIT +Keywords: smbus,smbus2,python,i2c,raspberrypi,linux +Classifier: Development Status :: 4 - Beta +Classifier: Topic :: Utilities +Classifier: License :: OSI Approved :: MIT License +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.4 +Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Description-Content-Type: text/markdown +License-File: LICENSE +Provides-Extra: docs +Requires-Dist: sphinx >=1.5.3 ; extra == 'docs' +Provides-Extra: qa +Requires-Dist: flake8 ; extra == 'qa' +Provides-Extra: test +Requires-Dist: nose ; extra == 'test' +Requires-Dist: mock ; (python_version < "3.3") and extra == 'test' + +# smbus2 +A drop-in replacement for smbus-cffi/smbus-python in pure Python + +[![Build Status](https://github.com/kplindegaard/smbus2/actions/workflows/python-build-test.yml/badge.svg?branch=master)](https://github.com/kplindegaard/smbus2/actions/workflows/python-build-test.yml) +[![Documentation Status](https://readthedocs.org/projects/smbus2/badge/?version=latest)](http://smbus2.readthedocs.io/en/latest/?badge=latest) +![CodeQL](https://github.com/kplindegaard/smbus2/actions/workflows/codeql-analysis.yml/badge.svg?branch=master) +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=kplindegaard_smbus2&metric=alert_status)](https://sonarcloud.io/dashboard?id=kplindegaard_smbus2) + +![Python Verions](https://img.shields.io/pypi/pyversions/smbus2.svg) +[![PyPi Version](https://img.shields.io/pypi/v/smbus2.svg)](https://pypi.org/project/smbus2/) +[![PyPI - Downloads](https://img.shields.io/pypi/dm/smbus2)](https://pypi.org/project/smbus2/) + +# Introduction + +smbus2 is (yet another) pure Python implementation of the [python-smbus](http://www.lm-sensors.org/browser/i2c-tools/trunk/py-smbus/) package. + +It was designed from the ground up with two goals in mind: + +1. It should be a drop-in replacement of smbus. The syntax shall be the same. +2. Use the inherent i2c structs and unions to a greater extent than other pure Python implementations like [pysmbus](https://github.com/bjornt/pysmbus) does. By doing so, it will be more feature complete and easier to extend. + +Currently supported features are: + +* Get i2c capabilities (I2C_FUNCS) +* SMBus Packet Error Checking (PEC) support +* read_byte +* write_byte +* read_byte_data +* write_byte_data +* read_word_data +* write_word_data +* read_i2c_block_data +* write_i2c_block_data +* write_quick +* process_call +* read_block_data +* write_block_data +* block_process_call +* i2c_rdwr - *combined write/read transactions with repeated start* + +It is developed on Python 2.7 but works without any modifications in Python 3.X too. + +More information about updates and general changes are recorded in the [change log](https://github.com/kplindegaard/smbus2/blob/master/CHANGELOG.md). + +# SMBus code examples + +smbus2 installs next to smbus as the package, so it's not really a 100% replacement. You must change the module name. + +## Example 1a: Read a byte + +```python +from smbus2 import SMBus + +# Open i2c bus 1 and read one byte from address 80, offset 0 +bus = SMBus(1) +b = bus.read_byte_data(80, 0) +print(b) +bus.close() +``` + +## Example 1b: Read a byte using 'with' + +This is the very same example but safer to use since the smbus will be closed automatically when exiting the with block. + +```python +from smbus2 import SMBus + +with SMBus(1) as bus: + b = bus.read_byte_data(80, 0) + print(b) +``` + +## Example 1c: Read a byte with PEC enabled + +Same example with Packet Error Checking enabled. + +```python +from smbus2 import SMBus + +with SMBus(1) as bus: + bus.pec = 1 # Enable PEC + b = bus.read_byte_data(80, 0) + print(b) +``` + +## Example 2: Read a block of data + +You can read up to 32 bytes at once. + +```python +from smbus2 import SMBus + +with SMBus(1) as bus: + # Read a block of 16 bytes from address 80, offset 0 + block = bus.read_i2c_block_data(80, 0, 16) + # Returned value is a list of 16 bytes + print(block) +``` + +## Example 3: Write a byte + +```python +from smbus2 import SMBus + +with SMBus(1) as bus: + # Write a byte to address 80, offset 0 + data = 45 + bus.write_byte_data(80, 0, data) +``` + +## Example 4: Write a block of data + +It is possible to write 32 bytes at the time, but I have found that error-prone. Write less and add a delay in between if you run into trouble. + +```python +from smbus2 import SMBus + +with SMBus(1) as bus: + # Write a block of 8 bytes to address 80 from offset 0 + data = [1, 2, 3, 4, 5, 6, 7, 8] + bus.write_i2c_block_data(80, 0, data) +``` + +# I2C + +Starting with v0.2, the smbus2 library also has support for combined read and write transactions. *i2c_rdwr* is not really a SMBus feature but comes in handy when the master needs to: + +1. read or write bulks of data larger than SMBus' 32 bytes limit. +1. write some data and then read from the slave with a repeated start and no stop bit between. + +Each operation is represented by a *i2c_msg* message object. + + +## Example 5: Single i2c_rdwr + +```python +from smbus2 import SMBus, i2c_msg + +with SMBus(1) as bus: + # Read 64 bytes from address 80 + msg = i2c_msg.read(80, 64) + bus.i2c_rdwr(msg) + + # Write a single byte to address 80 + msg = i2c_msg.write(80, [65]) + bus.i2c_rdwr(msg) + + # Write some bytes to address 80 + msg = i2c_msg.write(80, [65, 66, 67, 68]) + bus.i2c_rdwr(msg) +``` + +## Example 6: Dual i2c_rdwr + +To perform dual operations just add more i2c_msg instances to the bus call: + +```python +from smbus2 import SMBus, i2c_msg + +# Single transaction writing two bytes then read two at address 80 +write = i2c_msg.write(80, [40, 50]) +read = i2c_msg.read(80, 2) +with SMBus(1) as bus: + bus.i2c_rdwr(write, read) +``` + +## Example 7: Access i2c_msg data + +All data is contained in the i2c_msg instances. Here are some data access alternatives. + +```python +# 1: Convert message content to list +msg = i2c_msg.write(60, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) +data = list(msg) # data = [1, 2, 3, ...] +print(len(data)) # => 10 + +# 2: i2c_msg is iterable +for value in msg: + print(value) + +# 3: Through i2c_msg properties +for k in range(msg.len): + print(msg.buf[k]) +``` + +# Installation instructions + +From [PyPi](https://pypi.org/) with `pip`: + +``` +pip install smbus2 +``` + +From [conda-forge](https://anaconda.org/conda-forge) using `conda`: + +``` +conda install -c conda-forge smbus2 +``` + +Installation from source code is straight forward: + +``` +python setup.py install +``` diff --git a/scripts/tempSensor/lib/smbus2-0.4.3.dist-info/RECORD b/scripts/tempSensor/lib/smbus2-0.4.3.dist-info/RECORD new file mode 100644 index 00000000..7df5fae3 --- /dev/null +++ b/scripts/tempSensor/lib/smbus2-0.4.3.dist-info/RECORD @@ -0,0 +1,6 @@ +smbus2-0.4.3.dist-info/METADATA,, +smbus2/__init__.py,, +smbus2/py.typed,, +smbus2/smbus2.py,, +smbus2/smbus2.pyi,, +smbus2-0.4.3.dist-info/RECORD,, \ No newline at end of file diff --git a/scripts/tempSensor/lib/smbus2/__init__.py b/scripts/tempSensor/lib/smbus2/__init__.py new file mode 100644 index 00000000..f52948f9 --- /dev/null +++ b/scripts/tempSensor/lib/smbus2/__init__.py @@ -0,0 +1,26 @@ +"""smbus2 - A drop-in replacement for smbus-cffi/smbus-python""" +# The MIT License (MIT) +# Copyright (c) 2020 Karl-Petter Lindegaard +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from .smbus2 import SMBus, i2c_msg, I2cFunc # noqa: F401 + +__version__ = "0.4.3" +__all__ = ["SMBus", "i2c_msg", "I2cFunc"] diff --git a/scripts/tempSensor/lib/smbus2/py.typed b/scripts/tempSensor/lib/smbus2/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/scripts/tempSensor/lib/smbus2/smbus2.py b/scripts/tempSensor/lib/smbus2/smbus2.py new file mode 100644 index 00000000..e9d7477f --- /dev/null +++ b/scripts/tempSensor/lib/smbus2/smbus2.py @@ -0,0 +1,658 @@ +"""smbus2 - A drop-in replacement for smbus-cffi/smbus-python""" +# The MIT License (MIT) +# Copyright (c) 2020 Karl-Petter Lindegaard +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import os +import sys +from fcntl import ioctl +from ctypes import c_uint32, c_uint8, c_uint16, c_char, POINTER, Structure, Array, Union, create_string_buffer, string_at + + +# Commands from uapi/linux/i2c-dev.h +I2C_SLAVE = 0x0703 # Use this slave address +I2C_SLAVE_FORCE = 0x0706 # Use this slave address, even if it is already in use by a driver! +I2C_FUNCS = 0x0705 # Get the adapter functionality mask +I2C_RDWR = 0x0707 # Combined R/W transfer (one STOP only) +I2C_SMBUS = 0x0720 # SMBus transfer. Takes pointer to i2c_smbus_ioctl_data +I2C_PEC = 0x0708 # != 0 to use PEC with SMBus + +# SMBus transfer read or write markers from uapi/linux/i2c.h +I2C_SMBUS_WRITE = 0 +I2C_SMBUS_READ = 1 + +# Size identifiers uapi/linux/i2c.h +I2C_SMBUS_QUICK = 0 +I2C_SMBUS_BYTE = 1 +I2C_SMBUS_BYTE_DATA = 2 +I2C_SMBUS_WORD_DATA = 3 +I2C_SMBUS_PROC_CALL = 4 +I2C_SMBUS_BLOCK_DATA = 5 # This isn't supported by Pure-I2C drivers with SMBUS emulation, like those in RaspberryPi, OrangePi, etc :( +I2C_SMBUS_BLOCK_PROC_CALL = 7 # Like I2C_SMBUS_BLOCK_DATA, it isn't supported by Pure-I2C drivers either. +I2C_SMBUS_I2C_BLOCK_DATA = 8 +I2C_SMBUS_BLOCK_MAX = 32 + +# To determine what functionality is present (uapi/linux/i2c.h) +try: + from enum import IntFlag +except ImportError: + IntFlag = int + + +class I2cFunc(IntFlag): + """ + These flags identify the operations supported by an I2C/SMBus device. + + You can test these flags on your `smbus.funcs` + + On newer python versions, I2cFunc is an IntFlag enum, but it + falls back to class with a bunch of int constants on older releases. + """ + I2C = 0x00000001 + ADDR_10BIT = 0x00000002 + PROTOCOL_MANGLING = 0x00000004 # I2C_M_IGNORE_NAK etc. + SMBUS_PEC = 0x00000008 + NOSTART = 0x00000010 # I2C_M_NOSTART + SLAVE = 0x00000020 + SMBUS_BLOCK_PROC_CALL = 0x00008000 # SMBus 2.0 + SMBUS_QUICK = 0x00010000 + SMBUS_READ_BYTE = 0x00020000 + SMBUS_WRITE_BYTE = 0x00040000 + SMBUS_READ_BYTE_DATA = 0x00080000 + SMBUS_WRITE_BYTE_DATA = 0x00100000 + SMBUS_READ_WORD_DATA = 0x00200000 + SMBUS_WRITE_WORD_DATA = 0x00400000 + SMBUS_PROC_CALL = 0x00800000 + SMBUS_READ_BLOCK_DATA = 0x01000000 + SMBUS_WRITE_BLOCK_DATA = 0x02000000 + SMBUS_READ_I2C_BLOCK = 0x04000000 # I2C-like block xfer + SMBUS_WRITE_I2C_BLOCK = 0x08000000 # w/ 1-byte reg. addr. + SMBUS_HOST_NOTIFY = 0x10000000 + + SMBUS_BYTE = 0x00060000 + SMBUS_BYTE_DATA = 0x00180000 + SMBUS_WORD_DATA = 0x00600000 + SMBUS_BLOCK_DATA = 0x03000000 + SMBUS_I2C_BLOCK = 0x0c000000 + SMBUS_EMUL = 0x0eff0008 + + +# i2c_msg flags from uapi/linux/i2c.h +I2C_M_RD = 0x0001 + +# Pointer definitions +LP_c_uint8 = POINTER(c_uint8) +LP_c_uint16 = POINTER(c_uint16) +LP_c_uint32 = POINTER(c_uint32) + + +############################################################# +# Type definitions as in i2c.h + + +class i2c_smbus_data(Array): + """ + Adaptation of the i2c_smbus_data union in ``i2c.h``. + + Data for SMBus messages. + """ + _length_ = I2C_SMBUS_BLOCK_MAX + 2 + _type_ = c_uint8 + + +class union_i2c_smbus_data(Union): + _fields_ = [ + ("byte", c_uint8), + ("word", c_uint16), + ("block", i2c_smbus_data) + ] + + +union_pointer_type = POINTER(union_i2c_smbus_data) + + +class i2c_smbus_ioctl_data(Structure): + """ + As defined in ``i2c-dev.h``. + """ + _fields_ = [ + ('read_write', c_uint8), + ('command', c_uint8), + ('size', c_uint32), + ('data', union_pointer_type)] + __slots__ = [name for name, type in _fields_] + + @staticmethod + def create(read_write=I2C_SMBUS_READ, command=0, size=I2C_SMBUS_BYTE_DATA): + u = union_i2c_smbus_data() + return i2c_smbus_ioctl_data( + read_write=read_write, command=command, size=size, + data=union_pointer_type(u)) + + +############################################################# +# Type definitions for i2c_rdwr combined transactions + + +class i2c_msg(Structure): + """ + As defined in ``i2c.h``. + """ + _fields_ = [ + ('addr', c_uint16), + ('flags', c_uint16), + ('len', c_uint16), + ('buf', POINTER(c_char))] + + def __iter__(self): + """ Iterator / Generator + + :return: iterates over :py:attr:`buf` + :rtype: :py:class:`generator` which returns int values + """ + idx = 0 + while idx < self.len: + yield ord(self.buf[idx]) + idx += 1 + + def __len__(self): + return self.len + + def __bytes__(self): + return string_at(self.buf, self.len) + + def __repr__(self): + return 'i2c_msg(%d,%d,%r)' % (self.addr, self.flags, self.__bytes__()) + + def __str__(self): + s = self.__bytes__() + # Throw away non-decodable bytes + s = s.decode(errors="ignore") + return s + + @staticmethod + def read(address, length): + """ + Prepares an i2c read transaction. + + :param address: Slave address. + :type: address: int + :param length: Number of bytes to read. + :type: length: int + :return: New :py:class:`i2c_msg` instance for read operation. + :rtype: :py:class:`i2c_msg` + """ + arr = create_string_buffer(length) + return i2c_msg( + addr=address, flags=I2C_M_RD, len=length, + buf=arr) + + @staticmethod + def write(address, buf): + """ + Prepares an i2c write transaction. + + :param address: Slave address. + :type address: int + :param buf: Bytes to write. Either list of values or str. + :type buf: list + :return: New :py:class:`i2c_msg` instance for write operation. + :rtype: :py:class:`i2c_msg` + """ + if sys.version_info.major >= 3: + if type(buf) is str: + buf = bytes(map(ord, buf)) + else: + buf = bytes(buf) + else: + if type(buf) is not str: + buf = ''.join([chr(x) for x in buf]) + arr = create_string_buffer(buf, len(buf)) + return i2c_msg( + addr=address, flags=0, len=len(arr), + buf=arr) + + +class i2c_rdwr_ioctl_data(Structure): + """ + As defined in ``i2c-dev.h``. + """ + _fields_ = [ + ('msgs', POINTER(i2c_msg)), + ('nmsgs', c_uint32) + ] + __slots__ = [name for name, type in _fields_] + + @staticmethod + def create(*i2c_msg_instances): + """ + Factory method for creating a i2c_rdwr_ioctl_data struct that can + be called with ``ioctl(fd, I2C_RDWR, data)``. + + :param i2c_msg_instances: Up to 42 i2c_msg instances + :rtype: i2c_rdwr_ioctl_data + """ + n_msg = len(i2c_msg_instances) + msg_array = (i2c_msg * n_msg)(*i2c_msg_instances) + return i2c_rdwr_ioctl_data( + msgs=msg_array, + nmsgs=n_msg + ) + + +############################################################# + + +class SMBus(object): + + def __init__(self, bus=None, force=False): + """ + Initialize and (optionally) open an i2c bus connection. + + :param bus: i2c bus number (e.g. 0 or 1) + or an absolute file path (e.g. `/dev/i2c-42`). + If not given, a subsequent call to ``open()`` is required. + :type bus: int or str + :param force: force using the slave address even when driver is + already using it. + :type force: boolean + """ + self.fd = None + self.funcs = I2cFunc(0) + if bus is not None: + self.open(bus) + self.address = None + self.force = force + self._force_last = None + self._pec = 0 + + def __enter__(self): + """Enter handler.""" + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """Exit handler.""" + self.close() + + def open(self, bus): + """ + Open a given i2c bus. + + :param bus: i2c bus number (e.g. 0 or 1) + or an absolute file path (e.g. '/dev/i2c-42'). + :type bus: int or str + :raise TypeError: if type(bus) is not in (int, str) + """ + if isinstance(bus, int): + filepath = "/dev/i2c-{}".format(bus) + elif isinstance(bus, str): + filepath = bus + else: + raise TypeError("Unexpected type(bus)={}".format(type(bus))) + + self.fd = os.open(filepath, os.O_RDWR) + self.funcs = self._get_funcs() + + def close(self): + """ + Close the i2c connection. + """ + if self.fd: + os.close(self.fd) + self.fd = None + self._pec = 0 + + def _get_pec(self): + return self._pec + + def enable_pec(self, enable=True): + """ + Enable/Disable PEC (Packet Error Checking) - SMBus 1.1 and later + + :param enable: + :type enable: Boolean + """ + if not (self.funcs & I2cFunc.SMBUS_PEC): + raise IOError('SMBUS_PEC is not a feature') + self._pec = int(enable) + ioctl(self.fd, I2C_PEC, self._pec) + + pec = property(_get_pec, enable_pec) # Drop-in replacement for smbus member "pec" + """Get and set SMBus PEC. 0 = disabled (default), 1 = enabled.""" + + def _set_address(self, address, force=None): + """ + Set i2c slave address to use for subsequent calls. + + :param address: + :type address: int + :param force: + :type force: Boolean + """ + force = force if force is not None else self.force + if self.address != address or self._force_last != force: + if force is True: + ioctl(self.fd, I2C_SLAVE_FORCE, address) + else: + ioctl(self.fd, I2C_SLAVE, address) + self.address = address + self._force_last = force + + def _get_funcs(self): + """ + Returns a 32-bit value stating supported I2C functions. + + :rtype: int + """ + f = c_uint32() + ioctl(self.fd, I2C_FUNCS, f) + return f.value + + def write_quick(self, i2c_addr, force=None): + """ + Perform quick transaction. Throws IOError if unsuccessful. + :param i2c_addr: i2c address + :type i2c_addr: int + :param force: + :type force: Boolean + """ + self._set_address(i2c_addr, force=force) + msg = i2c_smbus_ioctl_data.create( + read_write=I2C_SMBUS_WRITE, command=0, size=I2C_SMBUS_QUICK) + ioctl(self.fd, I2C_SMBUS, msg) + + def read_byte(self, i2c_addr, force=None): + """ + Read a single byte from a device. + + :rtype: int + :param i2c_addr: i2c address + :type i2c_addr: int + :param force: + :type force: Boolean + :return: Read byte value + """ + self._set_address(i2c_addr, force=force) + msg = i2c_smbus_ioctl_data.create( + read_write=I2C_SMBUS_READ, command=0, size=I2C_SMBUS_BYTE + ) + ioctl(self.fd, I2C_SMBUS, msg) + return msg.data.contents.byte + + def write_byte(self, i2c_addr, value, force=None): + """ + Write a single byte to a device. + + :param i2c_addr: i2c address + :type i2c_addr: int + :param value: value to write + :type value: int + :param force: + :type force: Boolean + """ + self._set_address(i2c_addr, force=force) + msg = i2c_smbus_ioctl_data.create( + read_write=I2C_SMBUS_WRITE, command=value, size=I2C_SMBUS_BYTE + ) + ioctl(self.fd, I2C_SMBUS, msg) + + def read_byte_data(self, i2c_addr, register, force=None): + """ + Read a single byte from a designated register. + + :param i2c_addr: i2c address + :type i2c_addr: int + :param register: Register to read + :type register: int + :param force: + :type force: Boolean + :return: Read byte value + :rtype: int + """ + self._set_address(i2c_addr, force=force) + msg = i2c_smbus_ioctl_data.create( + read_write=I2C_SMBUS_READ, command=register, size=I2C_SMBUS_BYTE_DATA + ) + ioctl(self.fd, I2C_SMBUS, msg) + return msg.data.contents.byte + + def write_byte_data(self, i2c_addr, register, value, force=None): + """ + Write a byte to a given register. + + :param i2c_addr: i2c address + :type i2c_addr: int + :param register: Register to write to + :type register: int + :param value: Byte value to transmit + :type value: int + :param force: + :type force: Boolean + :rtype: None + """ + self._set_address(i2c_addr, force=force) + msg = i2c_smbus_ioctl_data.create( + read_write=I2C_SMBUS_WRITE, command=register, size=I2C_SMBUS_BYTE_DATA + ) + msg.data.contents.byte = value + ioctl(self.fd, I2C_SMBUS, msg) + + def read_word_data(self, i2c_addr, register, force=None): + """ + Read a single word (2 bytes) from a given register. + + :param i2c_addr: i2c address + :type i2c_addr: int + :param register: Register to read + :type register: int + :param force: + :type force: Boolean + :return: 2-byte word + :rtype: int + """ + self._set_address(i2c_addr, force=force) + msg = i2c_smbus_ioctl_data.create( + read_write=I2C_SMBUS_READ, command=register, size=I2C_SMBUS_WORD_DATA + ) + ioctl(self.fd, I2C_SMBUS, msg) + return msg.data.contents.word + + def write_word_data(self, i2c_addr, register, value, force=None): + """ + Write a single word (2 bytes) to a given register. + + :param i2c_addr: i2c address + :type i2c_addr: int + :param register: Register to write to + :type register: int + :param value: Word value to transmit + :type value: int + :param force: + :type force: Boolean + :rtype: None + """ + self._set_address(i2c_addr, force=force) + msg = i2c_smbus_ioctl_data.create( + read_write=I2C_SMBUS_WRITE, command=register, size=I2C_SMBUS_WORD_DATA + ) + msg.data.contents.word = value + ioctl(self.fd, I2C_SMBUS, msg) + + def process_call(self, i2c_addr, register, value, force=None): + """ + Executes a SMBus Process Call, sending a 16-bit value and receiving a 16-bit response + + :param i2c_addr: i2c address + :type i2c_addr: int + :param register: Register to read/write to + :type register: int + :param value: Word value to transmit + :type value: int + :param force: + :type force: Boolean + :rtype: int + """ + self._set_address(i2c_addr, force=force) + msg = i2c_smbus_ioctl_data.create( + read_write=I2C_SMBUS_WRITE, command=register, size=I2C_SMBUS_PROC_CALL + ) + msg.data.contents.word = value + ioctl(self.fd, I2C_SMBUS, msg) + return msg.data.contents.word + + def read_block_data(self, i2c_addr, register, force=None): + """ + Read a block of up to 32-bytes from a given register. + + :param i2c_addr: i2c address + :type i2c_addr: int + :param register: Start register + :type register: int + :param force: + :type force: Boolean + :return: List of bytes + :rtype: list + """ + self._set_address(i2c_addr, force=force) + msg = i2c_smbus_ioctl_data.create( + read_write=I2C_SMBUS_READ, command=register, size=I2C_SMBUS_BLOCK_DATA + ) + ioctl(self.fd, I2C_SMBUS, msg) + length = msg.data.contents.block[0] + return msg.data.contents.block[1:length + 1] + + def write_block_data(self, i2c_addr, register, data, force=None): + """ + Write a block of byte data to a given register. + + :param i2c_addr: i2c address + :type i2c_addr: int + :param register: Start register + :type register: int + :param data: List of bytes + :type data: list + :param force: + :type force: Boolean + :rtype: None + """ + length = len(data) + if length > I2C_SMBUS_BLOCK_MAX: + raise ValueError("Data length cannot exceed %d bytes" % I2C_SMBUS_BLOCK_MAX) + self._set_address(i2c_addr, force=force) + msg = i2c_smbus_ioctl_data.create( + read_write=I2C_SMBUS_WRITE, command=register, size=I2C_SMBUS_BLOCK_DATA + ) + msg.data.contents.block[0] = length + msg.data.contents.block[1:length + 1] = data + ioctl(self.fd, I2C_SMBUS, msg) + + def block_process_call(self, i2c_addr, register, data, force=None): + """ + Executes a SMBus Block Process Call, sending a variable-size data + block and receiving another variable-size response + + :param i2c_addr: i2c address + :type i2c_addr: int + :param register: Register to read/write to + :type register: int + :param data: List of bytes + :type data: list + :param force: + :type force: Boolean + :return: List of bytes + :rtype: list + """ + length = len(data) + if length > I2C_SMBUS_BLOCK_MAX: + raise ValueError("Data length cannot exceed %d bytes" % I2C_SMBUS_BLOCK_MAX) + self._set_address(i2c_addr, force=force) + msg = i2c_smbus_ioctl_data.create( + read_write=I2C_SMBUS_WRITE, command=register, size=I2C_SMBUS_BLOCK_PROC_CALL + ) + msg.data.contents.block[0] = length + msg.data.contents.block[1:length + 1] = data + ioctl(self.fd, I2C_SMBUS, msg) + length = msg.data.contents.block[0] + return msg.data.contents.block[1:length + 1] + + def read_i2c_block_data(self, i2c_addr, register, length, force=None): + """ + Read a block of byte data from a given register. + + :param i2c_addr: i2c address + :type i2c_addr: int + :param register: Start register + :type register: int + :param length: Desired block length + :type length: int + :param force: + :type force: Boolean + :return: List of bytes + :rtype: list + """ + if length > I2C_SMBUS_BLOCK_MAX: + raise ValueError("Desired block length over %d bytes" % I2C_SMBUS_BLOCK_MAX) + self._set_address(i2c_addr, force=force) + msg = i2c_smbus_ioctl_data.create( + read_write=I2C_SMBUS_READ, command=register, size=I2C_SMBUS_I2C_BLOCK_DATA + ) + msg.data.contents.byte = length + ioctl(self.fd, I2C_SMBUS, msg) + return msg.data.contents.block[1:length + 1] + + def write_i2c_block_data(self, i2c_addr, register, data, force=None): + """ + Write a block of byte data to a given register. + + :param i2c_addr: i2c address + :type i2c_addr: int + :param register: Start register + :type register: int + :param data: List of bytes + :type data: list + :param force: + :type force: Boolean + :rtype: None + """ + length = len(data) + if length > I2C_SMBUS_BLOCK_MAX: + raise ValueError("Data length cannot exceed %d bytes" % I2C_SMBUS_BLOCK_MAX) + self._set_address(i2c_addr, force=force) + msg = i2c_smbus_ioctl_data.create( + read_write=I2C_SMBUS_WRITE, command=register, size=I2C_SMBUS_I2C_BLOCK_DATA + ) + msg.data.contents.block[0] = length + msg.data.contents.block[1:length + 1] = data + ioctl(self.fd, I2C_SMBUS, msg) + + def i2c_rdwr(self, *i2c_msgs): + """ + Combine a series of i2c read and write operations in a single + transaction (with repeated start bits but no stop bits in between). + + This method takes i2c_msg instances as input, which must be created + first with :py:meth:`i2c_msg.read` or :py:meth:`i2c_msg.write`. + + :param i2c_msgs: One or more i2c_msg class instances. + :type i2c_msgs: i2c_msg + :rtype: None + """ + ioctl_data = i2c_rdwr_ioctl_data.create(*i2c_msgs) + ioctl(self.fd, I2C_RDWR, ioctl_data) diff --git a/scripts/tempSensor/lib/smbus2/smbus2.pyi b/scripts/tempSensor/lib/smbus2/smbus2.pyi new file mode 100644 index 00000000..f111ec26 --- /dev/null +++ b/scripts/tempSensor/lib/smbus2/smbus2.pyi @@ -0,0 +1,148 @@ +from enum import IntFlag +from typing import Optional, Sequence, List, Type, SupportsBytes, Iterable +from typing import Union as _UnionT +from types import TracebackType +from ctypes import c_uint32, c_uint8, c_uint16, pointer, Structure, Array, Union + +I2C_SLAVE: int +I2C_SLAVE_FORCE: int +I2C_FUNCS: int +I2C_RDWR: int +I2C_SMBUS: int +I2C_PEC: int +I2C_SMBUS_WRITE: int +I2C_SMBUS_READ: int +I2C_SMBUS_QUICK: int +I2C_SMBUS_BYTE: int +I2C_SMBUS_BYTE_DATA: int +I2C_SMBUS_WORD_DATA: int +I2C_SMBUS_PROC_CALL: int +I2C_SMBUS_BLOCK_DATA: int +I2C_SMBUS_BLOCK_PROC_CALL: int +I2C_SMBUS_I2C_BLOCK_DATA: int +I2C_SMBUS_BLOCK_MAX: int + +class I2cFunc(IntFlag): + I2C = ... + ADDR_10BIT = ... + PROTOCOL_MANGLING = ... + SMBUS_PEC = ... + NOSTART = ... + SLAVE = ... + SMBUS_BLOCK_PROC_CALL = ... + SMBUS_QUICK = ... + SMBUS_READ_BYTE = ... + SMBUS_WRITE_BYTE = ... + SMBUS_READ_BYTE_DATA = ... + SMBUS_WRITE_BYTE_DATA = ... + SMBUS_READ_WORD_DATA = ... + SMBUS_WRITE_WORD_DATA = ... + SMBUS_PROC_CALL = ... + SMBUS_READ_BLOCK_DATA = ... + SMBUS_WRITE_BLOCK_DATA = ... + SMBUS_READ_I2C_BLOCK = ... + SMBUS_WRITE_I2C_BLOCK = ... + SMBUS_HOST_NOTIFY = ... + SMBUS_BYTE = ... + SMBUS_BYTE_DATA = ... + SMBUS_WORD_DATA = ... + SMBUS_BLOCK_DATA = ... + SMBUS_I2C_BLOCK = ... + SMBUS_EMUL = ... + +I2C_M_RD: int +LP_c_uint8: Type[pointer[c_uint8]] +LP_c_uint16: Type[pointer[c_uint16]] +LP_c_uint32: Type[pointer[c_uint32]] + +class i2c_smbus_data(Array): ... +class union_i2c_smbus_data(Union): ... + +union_pointer_type: pointer[union_i2c_smbus_data] + +class i2c_smbus_ioctl_data(Structure): + @staticmethod + def create( + read_write: int = ..., command: int = ..., size: int = ... + ) -> "i2c_smbus_ioctl_data": ... + +class i2c_msg(Structure): + def __iter__(self) -> int: ... + def __len__(self) -> int: ... + def __bytes__(self) -> str: ... + @staticmethod + def read(address: int, length: int) -> "i2c_msg": ... + @staticmethod + def write(address: int, buf: _UnionT[str, Iterable[int], SupportsBytes]) -> "i2c_msg": ... + +class i2c_rdwr_ioctl_data(Structure): + @staticmethod + def create(*i2c_msg_instances: Sequence[i2c_msg]) -> "i2c_rdwr_ioctl_data": ... + +class SMBus: + fd: int = ... + funcs: I2cFunc = ... + address: Optional[int] = ... + force: Optional[bool] = ... + pec: int = ... + def __init__( + self, bus: _UnionT[None, int, str] = ..., force: bool = ... + ) -> None: ... + def __enter__(self) -> "SMBus": ... + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> None: ... + def open(self, bus: _UnionT[int, str]) -> None: ... + def close(self) -> None: ... + def enable_pec(self, enable: bool) -> None: ... + def write_quick(self, i2c_addr: int, force: Optional[bool] = ...) -> None: ... + def read_byte(self, i2c_addr: int, force: Optional[bool] = ...) -> int: ... + def write_byte( + self, i2c_addr: int, value: int, force: Optional[bool] = ... + ) -> None: ... + def read_byte_data( + self, i2c_addr: int, register: int, force: Optional[bool] = ... + ) -> int: ... + def write_byte_data( + self, i2c_addr: int, register: int, value: int, force: Optional[bool] = ... + ) -> None: ... + def read_word_data( + self, i2c_addr: int, register: int, force: Optional[bool] = ... + ) -> int: ... + def write_word_data( + self, i2c_addr: int, register: int, value: int, force: Optional[bool] = ... + ) -> None: ... + def process_call( + self, i2c_addr: int, register: int, value: int, force: Optional[bool] = ... + ): ... + def read_block_data( + self, i2c_addr: int, register: int, force: Optional[bool] = ... + ) -> List[int]: ... + def write_block_data( + self, + i2c_addr: int, + register: int, + data: Sequence[int], + force: Optional[bool] = ..., + ) -> None: ... + def block_process_call( + self, + i2c_addr: int, + register: int, + data: Sequence[int], + force: Optional[bool] = ..., + ) -> List[int]: ... + def read_i2c_block_data( + self, i2c_addr: int, register: int, length: int, force: Optional[bool] = ... + ) -> List[int]: ... + def write_i2c_block_data( + self, + i2c_addr: int, + register: int, + data: Sequence[int], + force: Optional[bool] = ..., + ) -> None: ... + def i2c_rdwr(self, *i2c_msgs: i2c_msg) -> None: ... diff --git a/scripts/tempSensor/main.py b/scripts/tempSensor/main.py new file mode 100644 index 00000000..24eab72e --- /dev/null +++ b/scripts/tempSensor/main.py @@ -0,0 +1,20 @@ +import time +import machine +import scripts.tempSensor.lib.adafruit_bme680 as adafruit_bme680 + +# Initialize I2C (using GPIO pins directly) +i2c = machine.I2C(0, scl=machine.Pin(5), sda=machine.Pin(4)) # Use I2C0 on GPIO4 (SDA) and GPIO5 (SCL) + +# Initialize BME680 sensor +bme680 = adafruit_bme680.Adafruit_BME680_I2C(i2c) + +# Adjust for temperature offset +temperature_offset = -5 + +while True: + print("\nTemperature: %0.1f C" % (bme680.temperature + temperature_offset)) + print("Gas: %d ohm" % bme680.gas) + print("Humidity: %0.1f %%" % bme680.relative_humidity) + print("Pressure: %0.3f hPa" % bme680.pressure) + print("Altitude = %0.2f meters" % bme680.altitude) + time.sleep(1) From ce0f222c6f9f804d0701b64e2b8af57f5300a316 Mon Sep 17 00:00:00 2001 From: Luthira A <79228258+Luthiraa@users.noreply.github.com> Date: Fri, 18 Oct 2024 22:07:54 -0400 Subject: [PATCH 03/19] Refactor temperatureSensor initialization and remove temperature offset --- scripts/tempSensor/main.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/scripts/tempSensor/main.py b/scripts/tempSensor/main.py index 24eab72e..b6d8f991 100644 --- a/scripts/tempSensor/main.py +++ b/scripts/tempSensor/main.py @@ -2,13 +2,10 @@ import machine import scripts.tempSensor.lib.adafruit_bme680 as adafruit_bme680 -# Initialize I2C (using GPIO pins directly) -i2c = machine.I2C(0, scl=machine.Pin(5), sda=machine.Pin(4)) # Use I2C0 on GPIO4 (SDA) and GPIO5 (SCL) +i2c = machine.I2C(0, scl=machine.Pin(5), sda=machine.Pin(4)) -# Initialize BME680 sensor bme680 = adafruit_bme680.Adafruit_BME680_I2C(i2c) -# Adjust for temperature offset temperature_offset = -5 while True: From 8f9dd0fef5cfe0e5d82f83fcdd43004eb536dae8 Mon Sep 17 00:00:00 2001 From: Luthira A <79228258+Luthiraa@users.noreply.github.com> Date: Sat, 26 Oct 2024 14:57:39 -0400 Subject: [PATCH 04/19] Remove unnecessary dependencies and unused code, simpler/streamlined code for adafruit bme680 temperature sensor --- .../archive/temperatureSensorTest.py | 16 - scripts/tempSensor/bme60.py | 421 ++ scripts/tempSensor/bme680i.py | 418 ++ .../tempSensor/lib/Adafruit_IO/__init__.py | 25 - .../tempSensor/lib/Adafruit_IO/_version.py | 1 - scripts/tempSensor/lib/Adafruit_IO/client.py | 482 -- scripts/tempSensor/lib/Adafruit_IO/errors.py | 61 - scripts/tempSensor/lib/Adafruit_IO/model.py | 149 - .../tempSensor/lib/Adafruit_IO/mqtt_client.py | 306 - scripts/tempSensor/lib/CHANGELOG.md | 50 - scripts/tempSensor/lib/LICENSE | 21 - scripts/tempSensor/lib/README.md | 56 - scripts/tempSensor/lib/adafruit_bme680.py | 769 --- .../lib/adafruit_bus_device/__init__.py | 0 .../lib/adafruit_bus_device/i2c_device.mpy | Bin 1172 -> 0 bytes .../lib/adafruit_bus_device/spi_device.mpy | Bin 821 -> 0 bytes .../lib/adafruit_io-2.8.0.dist-info/METADATA | 96 - .../lib/adafruit_io-2.8.0.dist-info/RECORD | 9 - .../lib/bme680-2.0.0.dist-info/METADATA | 156 - .../lib/bme680-2.0.0.dist-info/RECORD | 7 - scripts/tempSensor/lib/bme680/__init__.py | 486 -- scripts/tempSensor/lib/bme680/constants.py | 413 -- .../lib/board-1.0.dist-info/METADATA | 370 -- .../tempSensor/lib/board-1.0.dist-info/RECORD | 3 - scripts/tempSensor/lib/board.py | 775 --- scripts/tempSensor/lib/ez_setup.py | 332 -- .../lib/functools-0.0.7.dist-info/METADATA | 6 - .../lib/functools-0.0.7.dist-info/RECORD | 3 - scripts/tempSensor/lib/functools.py | 31 - .../lib/itertools-0.2.3.dist-info/METADATA | 6 - .../lib/itertools-0.2.3.dist-info/RECORD | 3 - scripts/tempSensor/lib/itertools.py | 77 - scripts/tempSensor/lib/paho/__init__.py | 0 scripts/tempSensor/lib/paho/mqtt/__init__.py | 5 - scripts/tempSensor/lib/paho/mqtt/client.py | 5004 ----------------- scripts/tempSensor/lib/paho/mqtt/enums.py | 113 - scripts/tempSensor/lib/paho/mqtt/matcher.py | 78 - .../tempSensor/lib/paho/mqtt/packettypes.py | 43 - .../tempSensor/lib/paho/mqtt/properties.py | 421 -- scripts/tempSensor/lib/paho/mqtt/publish.py | 306 - scripts/tempSensor/lib/paho/mqtt/py.typed | 0 .../tempSensor/lib/paho/mqtt/reasoncodes.py | 223 - scripts/tempSensor/lib/paho/mqtt/subscribe.py | 281 - .../lib/paho/mqtt/subscribeoptions.py | 113 - .../lib/paho_mqtt-2.1.0.dist-info/METADATA | 635 --- .../lib/paho_mqtt-2.1.0.dist-info/RECORD | 14 - .../lib/requests-0.10.0.dist-info/METADATA | 6 - .../lib/requests-0.10.0.dist-info/RECORD | 3 - scripts/tempSensor/lib/requests/__init__.py | 220 - .../lib/smbus2-0.4.3.dist-info/METADATA | 237 - .../lib/smbus2-0.4.3.dist-info/RECORD | 6 - scripts/tempSensor/lib/smbus2/__init__.py | 26 - scripts/tempSensor/lib/smbus2/py.typed | 0 scripts/tempSensor/lib/smbus2/smbus2.py | 658 --- scripts/tempSensor/lib/smbus2/smbus2.pyi | 148 - scripts/tempSensor/main.py | 17 - scripts/tempSensor/tempSensor.py | 18 + 57 files changed, 857 insertions(+), 13266 deletions(-) delete mode 100644 scripts/tempSensor/archive/temperatureSensorTest.py create mode 100644 scripts/tempSensor/bme60.py create mode 100644 scripts/tempSensor/bme680i.py delete mode 100644 scripts/tempSensor/lib/Adafruit_IO/__init__.py delete mode 100644 scripts/tempSensor/lib/Adafruit_IO/_version.py delete mode 100644 scripts/tempSensor/lib/Adafruit_IO/client.py delete mode 100644 scripts/tempSensor/lib/Adafruit_IO/errors.py delete mode 100644 scripts/tempSensor/lib/Adafruit_IO/model.py delete mode 100644 scripts/tempSensor/lib/Adafruit_IO/mqtt_client.py delete mode 100644 scripts/tempSensor/lib/CHANGELOG.md delete mode 100644 scripts/tempSensor/lib/LICENSE delete mode 100644 scripts/tempSensor/lib/README.md delete mode 100644 scripts/tempSensor/lib/adafruit_bme680.py delete mode 100644 scripts/tempSensor/lib/adafruit_bus_device/__init__.py delete mode 100644 scripts/tempSensor/lib/adafruit_bus_device/i2c_device.mpy delete mode 100644 scripts/tempSensor/lib/adafruit_bus_device/spi_device.mpy delete mode 100644 scripts/tempSensor/lib/adafruit_io-2.8.0.dist-info/METADATA delete mode 100644 scripts/tempSensor/lib/adafruit_io-2.8.0.dist-info/RECORD delete mode 100644 scripts/tempSensor/lib/bme680-2.0.0.dist-info/METADATA delete mode 100644 scripts/tempSensor/lib/bme680-2.0.0.dist-info/RECORD delete mode 100644 scripts/tempSensor/lib/bme680/__init__.py delete mode 100644 scripts/tempSensor/lib/bme680/constants.py delete mode 100644 scripts/tempSensor/lib/board-1.0.dist-info/METADATA delete mode 100644 scripts/tempSensor/lib/board-1.0.dist-info/RECORD delete mode 100644 scripts/tempSensor/lib/board.py delete mode 100644 scripts/tempSensor/lib/ez_setup.py delete mode 100644 scripts/tempSensor/lib/functools-0.0.7.dist-info/METADATA delete mode 100644 scripts/tempSensor/lib/functools-0.0.7.dist-info/RECORD delete mode 100644 scripts/tempSensor/lib/functools.py delete mode 100644 scripts/tempSensor/lib/itertools-0.2.3.dist-info/METADATA delete mode 100644 scripts/tempSensor/lib/itertools-0.2.3.dist-info/RECORD delete mode 100644 scripts/tempSensor/lib/itertools.py delete mode 100644 scripts/tempSensor/lib/paho/__init__.py delete mode 100644 scripts/tempSensor/lib/paho/mqtt/__init__.py delete mode 100644 scripts/tempSensor/lib/paho/mqtt/client.py delete mode 100644 scripts/tempSensor/lib/paho/mqtt/enums.py delete mode 100644 scripts/tempSensor/lib/paho/mqtt/matcher.py delete mode 100644 scripts/tempSensor/lib/paho/mqtt/packettypes.py delete mode 100644 scripts/tempSensor/lib/paho/mqtt/properties.py delete mode 100644 scripts/tempSensor/lib/paho/mqtt/publish.py delete mode 100644 scripts/tempSensor/lib/paho/mqtt/py.typed delete mode 100644 scripts/tempSensor/lib/paho/mqtt/reasoncodes.py delete mode 100644 scripts/tempSensor/lib/paho/mqtt/subscribe.py delete mode 100644 scripts/tempSensor/lib/paho/mqtt/subscribeoptions.py delete mode 100644 scripts/tempSensor/lib/paho_mqtt-2.1.0.dist-info/METADATA delete mode 100644 scripts/tempSensor/lib/paho_mqtt-2.1.0.dist-info/RECORD delete mode 100644 scripts/tempSensor/lib/requests-0.10.0.dist-info/METADATA delete mode 100644 scripts/tempSensor/lib/requests-0.10.0.dist-info/RECORD delete mode 100644 scripts/tempSensor/lib/requests/__init__.py delete mode 100644 scripts/tempSensor/lib/smbus2-0.4.3.dist-info/METADATA delete mode 100644 scripts/tempSensor/lib/smbus2-0.4.3.dist-info/RECORD delete mode 100644 scripts/tempSensor/lib/smbus2/__init__.py delete mode 100644 scripts/tempSensor/lib/smbus2/py.typed delete mode 100644 scripts/tempSensor/lib/smbus2/smbus2.py delete mode 100644 scripts/tempSensor/lib/smbus2/smbus2.pyi delete mode 100644 scripts/tempSensor/main.py create mode 100644 scripts/tempSensor/tempSensor.py diff --git a/scripts/tempSensor/archive/temperatureSensorTest.py b/scripts/tempSensor/archive/temperatureSensorTest.py deleted file mode 100644 index 168b98f5..00000000 --- a/scripts/tempSensor/archive/temperatureSensorTest.py +++ /dev/null @@ -1,16 +0,0 @@ -import time -import board -import busio -import scripts.tempSensor.lib.adafruit_bme680 as adafruit_bme680 - -i2c = busio.I2C(scl=board.GP17, sda=board.GP16) -sensor = adafruit_bme680.Adafruit_BME680_I2C(i2c) - -sensor.sea_level_pressure = 1013.25 - -while True: - print(f"temp: {sensor.temperature:.2f} C") - print(f"humidity: {sensor.humidity:.2f} %") - print(f"pressure: {sensor.pressure:.2f} ") - - time.sleep(2) diff --git a/scripts/tempSensor/bme60.py b/scripts/tempSensor/bme60.py new file mode 100644 index 00000000..bd2757ee --- /dev/null +++ b/scripts/tempSensor/bme60.py @@ -0,0 +1,421 @@ +# The MIT License (MIT) +# +# Copyright (c) 2017 ladyada for Adafruit Industries +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +# We have a lot of attributes for this complex sensor. +# pylint: disable=too-many-instance-attributes + +""" +`bme680` - BME680 - Temperature, Humidity, Pressure & Gas Sensor +================================================================ + +MicroPython driver from BME680 air quality sensor, based on Adafruit_bme680 + +* Author(s): Limor 'Ladyada' Fried of Adafruit + Jeff Raber (SPI support) + and many more contributors +""" + +import time +import math +from micropython import const +from ubinascii import hexlify as hex +try: + import struct +except ImportError: + import ustruct as struct + +# I2C ADDRESS/BITS/SETTINGS +# ----------------------------------------------------------------------- +_BME680_CHIPID = const(0x61) + +_BME680_REG_CHIPID = const(0xD0) +_BME680_BME680_COEFF_ADDR1 = const(0x89) +_BME680_BME680_COEFF_ADDR2 = const(0xE1) +_BME680_BME680_RES_HEAT_0 = const(0x5A) +_BME680_BME680_GAS_WAIT_0 = const(0x64) + +_BME680_REG_SOFTRESET = const(0xE0) +_BME680_REG_CTRL_GAS = const(0x71) +_BME680_REG_CTRL_HUM = const(0x72) +_BME280_REG_STATUS = const(0xF3) +_BME680_REG_CTRL_MEAS = const(0x74) +_BME680_REG_CONFIG = const(0x75) + +_BME680_REG_PAGE_SELECT = const(0x73) +_BME680_REG_MEAS_STATUS = const(0x1D) +_BME680_REG_PDATA = const(0x1F) +_BME680_REG_TDATA = const(0x22) +_BME680_REG_HDATA = const(0x25) + +_BME680_SAMPLERATES = (0, 1, 2, 4, 8, 16) +_BME680_FILTERSIZES = (0, 1, 3, 7, 15, 31, 63, 127) + +_BME680_RUNGAS = const(0x10) + +_LOOKUP_TABLE_1 = (2147483647.0, 2147483647.0, 2147483647.0, 2147483647.0, 2147483647.0, + 2126008810.0, 2147483647.0, 2130303777.0, 2147483647.0, 2147483647.0, + 2143188679.0, 2136746228.0, 2147483647.0, 2126008810.0, 2147483647.0, + 2147483647.0) + +_LOOKUP_TABLE_2 = (4096000000.0, 2048000000.0, 1024000000.0, 512000000.0, 255744255.0, 127110228.0, + 64000000.0, 32258064.0, 16016016.0, 8000000.0, 4000000.0, 2000000.0, 1000000.0, + 500000.0, 250000.0, 125000.0) + + +def _read24(arr): + """Parse an unsigned 24-bit value as a floating point and return it.""" + ret = 0.0 + #print([hex(i) for i in arr]) + for b in arr: + ret *= 256.0 + ret += float(b & 0xFF) + return ret + + +class Adafruit_BME680: + """Driver from BME680 air quality sensor + + :param int refresh_rate: Maximum number of readings per second. Faster property reads + will be from the previous reading.""" + def __init__(self, *, refresh_rate=10): + """Check the BME680 was found, read the coefficients and enable the sensor for continuous + reads.""" + self._write(_BME680_REG_SOFTRESET, [0xB6]) + time.sleep(0.005) + + # Check device ID. + chip_id = self._read_byte(_BME680_REG_CHIPID) + if chip_id != _BME680_CHIPID: + raise RuntimeError('Failed to find BME680! Chip ID 0x%x' % chip_id) + + self._read_calibration() + + # set up heater + self._write(_BME680_BME680_RES_HEAT_0, [0x73]) + self._write(_BME680_BME680_GAS_WAIT_0, [0x65]) + + self.sea_level_pressure = 1013.25 + """Pressure in hectoPascals at sea level. Used to calibrate ``altitude``.""" + + # Default oversampling and filter register values. + self._pressure_oversample = 0b011 + self._temp_oversample = 0b100 + self._humidity_oversample = 0b010 + self._filter = 0b010 + + self._adc_pres = None + self._adc_temp = None + self._adc_hum = None + self._adc_gas = None + self._gas_range = None + self._t_fine = None + + self._last_reading = time.ticks_ms() + self._min_refresh_time = 1000 // refresh_rate + + @property + def pressure_oversample(self): + """The oversampling for pressure sensor""" + return _BME680_SAMPLERATES[self._pressure_oversample] + + @pressure_oversample.setter + def pressure_oversample(self, sample_rate): + if sample_rate in _BME680_SAMPLERATES: + self._pressure_oversample = _BME680_SAMPLERATES.index(sample_rate) + else: + raise RuntimeError("Invalid oversample") + + @property + def humidity_oversample(self): + """The oversampling for humidity sensor""" + return _BME680_SAMPLERATES[self._humidity_oversample] + + @humidity_oversample.setter + def humidity_oversample(self, sample_rate): + if sample_rate in _BME680_SAMPLERATES: + self._humidity_oversample = _BME680_SAMPLERATES.index(sample_rate) + else: + raise RuntimeError("Invalid oversample") + + @property + def temperature_oversample(self): + """The oversampling for temperature sensor""" + return _BME680_SAMPLERATES[self._temp_oversample] + + @temperature_oversample.setter + def temperature_oversample(self, sample_rate): + if sample_rate in _BME680_SAMPLERATES: + self._temp_oversample = _BME680_SAMPLERATES.index(sample_rate) + else: + raise RuntimeError("Invalid oversample") + + @property + def filter_size(self): + """The filter size for the built in IIR filter""" + return _BME680_FILTERSIZES[self._filter] + + @filter_size.setter + def filter_size(self, size): + if size in _BME680_FILTERSIZES: + self._filter = _BME680_FILTERSIZES[size] + else: + raise RuntimeError("Invalid size") + + @property + def temperature(self): + """The compensated temperature in degrees celsius.""" + self._perform_reading() + calc_temp = (((self._t_fine * 5) + 128) / 256) + return calc_temp / 100 + + @property + def pressure(self): + """The barometric pressure in hectoPascals""" + self._perform_reading() + var1 = (self._t_fine / 2) - 64000 + var2 = ((var1 / 4) * (var1 / 4)) / 2048 + var2 = (var2 * self._pressure_calibration[5]) / 4 + var2 = var2 + (var1 * self._pressure_calibration[4] * 2) + var2 = (var2 / 4) + (self._pressure_calibration[3] * 65536) + var1 = (((((var1 / 4) * (var1 / 4)) / 8192) * + (self._pressure_calibration[2] * 32) / 8) + + ((self._pressure_calibration[1] * var1) / 2)) + var1 = var1 / 262144 + var1 = ((32768 + var1) * self._pressure_calibration[0]) / 32768 + calc_pres = 1048576 - self._adc_pres + calc_pres = (calc_pres - (var2 / 4096)) * 3125 + calc_pres = (calc_pres / var1) * 2 + var1 = (self._pressure_calibration[8] * (((calc_pres / 8) * (calc_pres / 8)) / 8192)) / 4096 + var2 = ((calc_pres / 4) * self._pressure_calibration[7]) / 8192 + var3 = (((calc_pres / 256) ** 3) * self._pressure_calibration[9]) / 131072 + calc_pres += ((var1 + var2 + var3 + (self._pressure_calibration[6] * 128)) / 16) + return calc_pres/100 + + @property + def humidity(self): + """The relative humidity in RH %""" + self._perform_reading() + temp_scaled = ((self._t_fine * 5) + 128) / 256 + var1 = ((self._adc_hum - (self._humidity_calibration[0] * 16)) - + ((temp_scaled * self._humidity_calibration[2]) / 200)) + var2 = (self._humidity_calibration[1] * + (((temp_scaled * self._humidity_calibration[3]) / 100) + + (((temp_scaled * ((temp_scaled * self._humidity_calibration[4]) / 100)) / + 64) / 100) + 16384)) / 1024 + var3 = var1 * var2 + var4 = self._humidity_calibration[5] * 128 + var4 = (var4 + ((temp_scaled * self._humidity_calibration[6]) / 100)) / 16 + var5 = ((var3 / 16384) * (var3 / 16384)) / 1024 + var6 = (var4 * var5) / 2 + calc_hum = (((var3 + var6) / 1024) * 1000) / 4096 + calc_hum /= 1000 # get back to RH + + if calc_hum > 100: + calc_hum = 100 + if calc_hum < 0: + calc_hum = 0 + return calc_hum + + @property + def altitude(self): + """The altitude based on current ``pressure`` vs the sea level pressure + (``sea_level_pressure``) - which you must enter ahead of time)""" + pressure = self.pressure # in Si units for hPascal + return 44330 * (1.0 - math.pow(pressure / self.sea_level_pressure, 0.1903)) + + @property + def gas(self): + """The gas resistance in ohms""" + self._perform_reading() + var1 = ((1340 + (5 * self._sw_err)) * (_LOOKUP_TABLE_1[self._gas_range])) / 65536 + var2 = ((self._adc_gas * 32768) - 16777216) + var1 + var3 = (_LOOKUP_TABLE_2[self._gas_range] * var1) / 512 + calc_gas_res = (var3 + (var2 / 2)) / var2 + return int(calc_gas_res) + + def _perform_reading(self): + """Perform a single-shot reading from the sensor and fill internal data structure for + calculations""" + expired = time.ticks_diff(self._last_reading, time.ticks_ms()) * time.ticks_diff(0, 1) + if 0 <= expired < self._min_refresh_time: + time.sleep_ms(self._min_refresh_time - expired) + + # set filter + self._write(_BME680_REG_CONFIG, [self._filter << 2]) + # turn on temp oversample & pressure oversample + self._write(_BME680_REG_CTRL_MEAS, + [(self._temp_oversample << 5)|(self._pressure_oversample << 2)]) + # turn on humidity oversample + self._write(_BME680_REG_CTRL_HUM, [self._humidity_oversample]) + # gas measurements enabled + self._write(_BME680_REG_CTRL_GAS, [_BME680_RUNGAS]) + + ctrl = self._read_byte(_BME680_REG_CTRL_MEAS) + ctrl = (ctrl & 0xFC) | 0x01 # enable single shot! + self._write(_BME680_REG_CTRL_MEAS, [ctrl]) + new_data = False + while not new_data: + data = self._read(_BME680_REG_MEAS_STATUS, 15) + new_data = data[0] & 0x80 != 0 + time.sleep(0.005) + self._last_reading = time.ticks_ms() + + self._adc_pres = _read24(data[2:5]) / 16 + self._adc_temp = _read24(data[5:8]) / 16 + self._adc_hum = struct.unpack('>H', bytes(data[8:10]))[0] + self._adc_gas = int(struct.unpack('>H', bytes(data[13:15]))[0] / 64) + self._gas_range = data[14] & 0x0F + + var1 = (self._adc_temp / 8) - (self._temp_calibration[0] * 2) + var2 = (var1 * self._temp_calibration[1]) / 2048 + var3 = ((var1 / 2) * (var1 / 2)) / 4096 + var3 = (var3 * self._temp_calibration[2] * 16) / 16384 + + self._t_fine = int(var2 + var3) + + def _read_calibration(self): + """Read & save the calibration coefficients""" + coeff = self._read(_BME680_BME680_COEFF_ADDR1, 25) + coeff += self._read(_BME680_BME680_COEFF_ADDR2, 16) + + coeff = list(struct.unpack('> 1) - 64000 + var2 = ((var1 >> 2) * (var1 >> 2 )) >> 11 + var2 = (var2 * self._pressure_calibration[5]) >> 2 + var2 = var2 + ((var1 * self._pressure_calibration[4]) << 1) + var2 = (var2 >> 2) + (self._pressure_calibration[3] << 16) + var1 = (((((var1 >> 2) * (var1 >> 2)) >> 13) * + (self._pressure_calibration[2] << 5) >> 3) + + ((self._pressure_calibration[1] * var1) >> 1)) + var1 = var1 >> 18 + var1 = ((32768 + var1) * self._pressure_calibration[0]) >> 15 + calc_pres = 1048576 - int(self._adc_pres) + calc_pres = (calc_pres - (var2 >> 12)) * 3125 + calc_pres = (calc_pres << 1) // var1 + var1 = (self._pressure_calibration[8] * (((calc_pres >> 3) * (calc_pres >> 3)) >> 13)) >> 12 + var2 = ((calc_pres >> 2) * self._pressure_calibration[7]) >> 13 + var3 = (((calc_pres >> 8) * (calc_pres >> 8) * (calc_pres >> 8)) * self._pressure_calibration[9]) >> 17 + calc_pres += ((var1 + var2 + var3 + (self._pressure_calibration[6] << 7)) >> 4) + return calc_pres / 100 + + @property + def humidity(self): + """The relative humidity in RH %""" + self._perform_reading() + + temp_scaled = ((self._t_fine * 5) + 128) >> 8 + var1 = ((self._adc_hum - (self._humidity_calibration[0] * 16)) - + (((temp_scaled * self._humidity_calibration[2]) // 100) >> 1)) + var2 = (self._humidity_calibration[1] * + (((temp_scaled * self._humidity_calibration[3]) // 100) + + (((temp_scaled * ((temp_scaled * self._humidity_calibration[4]) + // 100)) >> 6) // 100) + 16384)) >> 10 + var3 = var1 * var2 + var4 = self._humidity_calibration[5] << 7 + var4 = (var4 + ((temp_scaled * self._humidity_calibration[6]) // 100)) >> 4 + var5 = ((var3 >> 14) * (var3 >> 14)) >> 10 + var6 = (var4 * var5) >> 1 + calc_hum = ((var3 + var6) >> 10) / 4096 + + if calc_hum > 10000: + calc_hum = 10000 + if calc_hum < 0: + calc_hum = 0 + return calc_hum + + @property + def altitude(self): + """The altitude based on current ``pressure`` vs the sea level pressure + (``sea_level_pressure``) - which you must enter ahead of time)""" + pressure = self.pressure # in Si units for hPascal + return 44330 * (1.0 - math.pow(pressure / self.sea_level_pressure, 0.1903)) + + @property + def gas(self): + """The gas resistance in ohms""" + self._perform_reading() + var1 = ((1340 + (5 * self._sw_err)) * (_LOOKUP_TABLE_1[self._gas_range])) >> 16 + var2 = ((self._adc_gas << 15) - 16777216) + var1 + var3 = (_LOOKUP_TABLE_2[self._gas_range] * var1) >> 9 + calc_gas_res = (var3 + (var2 >> 1)) // var2 + return int(calc_gas_res) + + def _perform_reading(self): + """Perform a single-shot reading from the sensor and fill internal data structure for + calculations""" + expired = time.ticks_diff(self._last_reading, time.ticks_ms()) * time.ticks_diff(0, 1) + if 0 <= expired < self._min_refresh_time: + time.sleep_ms(self._min_refresh_time - expired) + + # set filter + self._write(_BME680_REG_CONFIG, [self._filter << 2]) + # turn on temp oversample & pressure oversample + self._write(_BME680_REG_CTRL_MEAS, + [(self._temp_oversample << 5)|(self._pressure_oversample << 2)]) + # turn on humidity oversample + self._write(_BME680_REG_CTRL_HUM, [self._humidity_oversample]) + # gas measurements enabled + self._write(_BME680_REG_CTRL_GAS, [_BME680_RUNGAS]) + + ctrl = self._read_byte(_BME680_REG_CTRL_MEAS) + ctrl = (ctrl & 0xFC) | 0x01 # enable single shot! + self._write(_BME680_REG_CTRL_MEAS, [ctrl]) + new_data = False + while not new_data: + data = self._read(_BME680_REG_MEAS_STATUS, 15) + new_data = data[0] & 0x80 != 0 + time.sleep(0.005) + self._last_reading = time.ticks_ms() + + self._adc_pres = _read24(data[2:5]) / 16 + self._adc_temp = _read24(data[5:8]) / 16 + self._adc_hum = struct.unpack('>H', bytes(data[8:10]))[0] + self._adc_gas = int(struct.unpack('>H', bytes(data[13:15]))[0] / 64) + self._gas_range = data[14] & 0x0F + + var1 = (int(self._adc_temp) >> 3) - (self._temp_calibration[0] << 1) + var2 = (var1 * self._temp_calibration[1]) >> 11 + var3 = ((var1 >> 1) * (var1 >> 1)) >> 12 + var3 = (var3 * self._temp_calibration[2] << 4) >> 14 + self._t_fine = int(var2 + var3) + + def _read_calibration(self): + """Read & save the calibration coefficients""" + coeff = self._read(_BME680_BME680_COEFF_ADDR1, 25) + coeff += self._read(_BME680_BME680_COEFF_ADDR2, 16) + + coeff = list(struct.unpack('>= 4 + + self._heat_range = (self._read_byte(0x02) & 0x30) >> 4 + self._heat_val = self._read_byte(0x00) + self._sw_err = (self._read_byte(0x04) & 0xF0) >> 4 + + def _read_byte(self, register): + """Read a byte register value and return it""" + return self._read(register, 1)[0] + + def _read(self, register, length): + raise NotImplementedError() + + def _write(self, register, values): + raise NotImplementedError() + +class BME680_I2C(Adafruit_BME680): + """Driver for I2C connected BME680. + + :param i2c: I2C device object + :param int address: I2C device address + :param bool debug: Print debug statements when True. + :param int refresh_rate: Maximum number of readings per second. Faster property reads + will be from the previous reading.""" + def __init__(self, i2c, address=0x77, debug=False, *, refresh_rate=10): + """Initialize the I2C device at the 'address' given""" + self._i2c = i2c + self._address = address + self._debug = debug + super().__init__(refresh_rate=refresh_rate) + + def _read(self, register, length): + """Returns an array of 'length' bytes from the 'register'""" + result = bytearray(length) + self._i2c.readfrom_mem_into(self._address, register & 0xff, result) + if self._debug: + print("\t${:x} read ".format(register), " ".join(["{:02x}".format(i) for i in result])) + return result + + def _write(self, register, values): + """Writes an array of 'length' bytes to the 'register'""" + if self._debug: + print("\t${:x} write".format(register), " ".join(["{:02x}".format(i) for i in values])) + for value in values: + self._i2c.writeto_mem(self._address, register, bytearray([value & 0xFF])) + register += 1 + + +class BME680_SPI(Adafruit_BME680): + """Driver for SPI connected BME680. + + :param spi: SPI device object, configured + :param cs: Chip Select Pin object, configured to OUT mode + :param bool debug: Print debug statements when True. + :param int refresh_rate: Maximum number of readings per second. Faster property reads + will be from the previous reading. + """ + + def __init__(self, spi, cs, debug=False, *, refresh_rate=10): + self._spi = spi + self._cs = cs + self._debug = debug + self._cs(1) + super().__init__(refresh_rate=refresh_rate) + + def _read(self, register, length): + if register != _BME680_REG_PAGE_SELECT: + # _BME680_REG_PAGE_SELECT exists in both SPI memory pages + # For all other registers, we must set the correct memory page + self._set_spi_mem_page(register) + register = (register | 0x80) & 0xFF # Read single, bit 7 high. + + try: + self._cs(0) + self._spi.write(bytearray([register])) # pylint: disable=no-member + result = bytearray(length) + self._spi.readinto(result) # pylint: disable=no-member + if self._debug: + print("\t${:x} read ".format(register), " ".join(["{:02x}".format(i) for i in result])) + except Exception as e: + print (e) + result = None + finally: + self._cs(1) + return result + + def _write(self, register, values): + if register != _BME680_REG_PAGE_SELECT: + # _BME680_REG_PAGE_SELECT exists in both SPI memory pages + # For all other registers, we must set the correct memory page + self._set_spi_mem_page(register) + register &= 0x7F # Write, bit 7 low. + try: + self._cs(0) + buffer = bytearray(2 * len(values)) + for i, value in enumerate(values): + buffer[2 * i] = register + i + buffer[2 * i + 1] = value & 0xFF + self._spi.write(buffer) # pylint: disable=no-member + if self._debug: + print("\t${:x} write".format(register), " ".join(["{:02x}".format(i) for i in values])) + except Exception as e: + print (e) + finally: + self._cs(1) + + def _set_spi_mem_page(self, register): + spi_mem_page = 0x00 + if register < 0x80: + spi_mem_page = 0x10 + self._write(_BME680_REG_PAGE_SELECT, [spi_mem_page]) diff --git a/scripts/tempSensor/lib/Adafruit_IO/__init__.py b/scripts/tempSensor/lib/Adafruit_IO/__init__.py deleted file mode 100644 index e34eb3d7..00000000 --- a/scripts/tempSensor/lib/Adafruit_IO/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright (c) 2014 Adafruit Industries -# Author: Tony DiCola - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. -from .client import Client -from .mqtt_client import MQTTClient -from .errors import AdafruitIOError, RequestError, ThrottlingError, MQTTError -from .model import Data, Feed, Group, Dashboard, Block, Layout -from ._version import __version__ diff --git a/scripts/tempSensor/lib/Adafruit_IO/_version.py b/scripts/tempSensor/lib/Adafruit_IO/_version.py deleted file mode 100644 index 892994aa..00000000 --- a/scripts/tempSensor/lib/Adafruit_IO/_version.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = "2.8.0" diff --git a/scripts/tempSensor/lib/Adafruit_IO/client.py b/scripts/tempSensor/lib/Adafruit_IO/client.py deleted file mode 100644 index 3f923260..00000000 --- a/scripts/tempSensor/lib/Adafruit_IO/client.py +++ /dev/null @@ -1,482 +0,0 @@ -# Copyright (c) 2018 Adafruit Industries -# Authors: Justin Cooper & Tony DiCola - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. -import time -from time import struct_time -import json -import platform -import pkg_resources -import re -from urllib.parse import urlparse -from urllib.parse import parse_qs -# import logging - -import requests - -from .errors import RequestError, ThrottlingError -from .model import Data, Feed, Group, Dashboard, Block, Layout - -DEFAULT_PAGE_LIMIT = 100 - -# set outgoing version, pulled from setup.py -version = pkg_resources.require("Adafruit_IO")[0].version -default_headers = { - 'User-Agent': 'AdafruitIO-Python/{0} ({1}, {2} {3})'.format(version, - platform.platform(), - platform.python_implementation(), - platform.python_version()) -} - -class Client(object): - """Client instance for interacting with the Adafruit IO service using its - REST API. Use this client class to send, receive, and enumerate feed data. - """ - - def __init__(self, username, key, proxies=None, base_url='https://io.adafruit.com'): - """Create an instance of the Adafruit IO REST API client. Key must be - provided and set to your Adafruit IO access key value. Optionaly - provide a proxies dict in the format used by the requests library, - and a base_url to point at a different Adafruit IO service - (the default is the production Adafruit IO service over SSL). - """ - self.username = username - self.key = key - self.proxies = proxies - # self.logger = logging.basicConfig(level=logging.DEBUG, - # format='%(asctime)s - %(levelname)s - %(message)s') - - # Save URL without trailing slash as it will be added later when - # constructing the path. - self.base_url = base_url.rstrip('/') - - # Store the last response of a get or post - self._last_response = None - - @staticmethod - def to_red(data): - """Hex color feed to red channel. - - :param int data: Color value, in hexadecimal. - """ - return ((int(data[1], 16))*16) + int(data[2], 16) - - @staticmethod - def to_green(data): - """Hex color feed to green channel. - - :param int data: Color value, in hexadecimal. - """ - return (int(data[3], 16) * 16) + int(data[4], 16) - - @staticmethod - def to_blue(data): - """Hex color feed to blue channel. - - :param int data: Color value, in hexadecimal. - """ - return (int(data[5], 16) * 16) + int(data[6], 16) - - @staticmethod - def _headers(given): - headers = default_headers.copy() - headers.update(given) - return headers - - @staticmethod - def _create_payload(value, metadata): - if metadata is not None: - payload = Data(value=value, lat=metadata['lat'], lon=metadata['lon'], - ele=metadata['ele'], created_at=metadata['created_at']) - return payload - return Data(value=value) - - @staticmethod - def _handle_error(response): - # Throttling Error - if response.status_code == 429: - raise ThrottlingError() - # Resource on AdafruitIO not Found Error - elif response.status_code == 400: - raise RequestError(response) - # Handle all other errors (400 & 500 level HTTP responses) - elif response.status_code >= 400: - raise RequestError(response) - # Else do nothing if there was no error. - - def _compose_url(self, path): - return '{0}/api/{1}/{2}/{3}'.format(self.base_url, 'v2', self.username, path) - - def _get(self, path, params=None): - response = requests.get(self._compose_url(path), - headers=self._headers({'X-AIO-Key': self.key}), - proxies=self.proxies, - params=params) - self._last_response = response - self._handle_error(response) - return response.json() - - def _post(self, path, data): - response = requests.post(self._compose_url(path), - headers=self._headers({'X-AIO-Key': self.key, - 'Content-Type': 'application/json'}), - proxies=self.proxies, - data=json.dumps(data)) - self._last_response = response - self._handle_error(response) - return response.json() - - def _delete(self, path): - response = requests.delete(self._compose_url(path), - headers=self._headers({'X-AIO-Key': self.key, - 'Content-Type': 'application/json'}), - proxies=self.proxies) - self._last_response = response - self._handle_error(response) - - # Data functionality. - def send_data(self, feed, value, metadata=None, precision=None): - """Helper function to simplify adding a value to a feed. Will append the - specified value to the feed identified by either name, key, or ID. - Returns a Data instance with details about the newly appended row of data. - Note that send_data now operates the same as append. - - :param string feed: Name/Key/ID of Adafruit IO feed. - :param string value: Value to send. - :param dict metadata: Optional metadata associated with the value. - :param int precision: Optional amount of precision points to send. - """ - if precision: - try: - value = round(value, precision) - except NotImplementedError: - raise NotImplementedError("Using the precision kwarg requires a float value") - payload = self._create_payload(value, metadata) - return self.create_data(feed, payload) - - send = send_data - - def send_batch_data(self, feed, data_list): - """Create a new row of data in the specified feed. Feed can be a feed - ID, feed key, or feed name. Data must be an instance of the Data class - with at least a value property set on it. Returns a Data instance with - details about the newly appended row of data. - - :param string feed: Name/Key/ID of Adafruit IO feed. - :param Data data_list: Multiple data values. - """ - path = "feeds/{0}/data/batch".format(feed) - data_dict = type(data_list)((data._asdict() for data in data_list)) - self._post(path, {"data": data_dict}) - - def append(self, feed, value): - """Helper function to simplify adding a value to a feed. Will append the - specified value to the feed identified by either name, key, or ID. - Returns a Data instance with details about the newly appended row of data. - Note that unlike send the feed should exist before calling append. - - :param string feed: Name/Key/ID of Adafruit IO feed. - :param string value: Value to append to feed. - """ - return self.create_data(feed, Data(value=value)) - - def receive_time(self, timezone=None): - """Returns a struct_time from the Adafruit IO Server based on requested - timezone, or automatically based on the device's IP address. - https://docs.python.org/3.7/library/time.html#time.struct_time - - :param string timezone: Optional timezone to return the time in. - See https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List - """ - path = 'integrations/time/struct.json' - if timezone: - path += f'?tz={timezone}' - return self._parse_time_struct(self._get(path)) - - @staticmethod - def _parse_time_struct(time_dict: dict) -> time.struct_time: - """Parse the time data returned by the server and return a time_struct - - Corrects for the weekday returned by the server in Sunday=0 format - (Python expects Monday=0) - """ - wday = (time_dict['wday'] - 1) % 7 - return struct_time((time_dict['year'], time_dict['mon'], time_dict['mday'], - time_dict['hour'], time_dict['min'], time_dict['sec'], - wday, time_dict['yday'], time_dict['isdst'])) - - def receive_weather(self, weather_id=None): - """Adafruit IO Weather Service, Powered by Dark Sky - - :param int id: optional ID for retrieving a specified weather record. - """ - if weather_id: - weather_path = "integrations/weather/{0}".format(weather_id) - else: - weather_path = "integrations/weather" - return self._get(weather_path) - - def receive_random(self, randomizer_id=None): - """Access to Adafruit IO's Random Data - service. - - :param int randomizer_id: optional ID for retrieving a specified randomizer. - """ - if randomizer_id: - random_path = "integrations/words/{0}".format(randomizer_id) - else: - random_path = "integrations/words" - return self._get(random_path) - - def receive(self, feed): - """Retrieve the most recent value for the specified feed. Returns a Data - instance whose value property holds the retrieved value. - - :param string feed: Name/Key/ID of Adafruit IO feed. - """ - path = "feeds/{0}/data/last".format(feed) - return Data.from_dict(self._get(path)) - - def receive_next(self, feed): - """Retrieve the next unread value from the specified feed. Returns a Data - instance whose value property holds the retrieved value. - - :param string feed: Name/Key/ID of Adafruit IO feed. - """ - path = "feeds/{0}/data/next".format(feed) - return Data.from_dict(self._get(path)) - - def receive_previous(self, feed): - """Retrieve the previous unread value from the specified feed. Returns a - Data instance whose value property holds the retrieved value. - - :param string feed: Name/Key/ID of Adafruit IO feed. - """ - path = "feeds/{0}/data/previous".format(feed) - return Data.from_dict(self._get(path)) - - def data(self, feed, data_id=None, max_results=DEFAULT_PAGE_LIMIT): - """Retrieve data from a feed. If data_id is not specified then all the data - for the feed will be returned in an array. - - :param string feed: Name/Key/ID of Adafruit IO feed. - :param string data_id: ID of the piece of data to delete. - :param int max_results: The maximum number of results to return. To - return all data, set to None. - """ - if max_results is None: - res = self._get(f'feeds/{feed}/details') - max_results = res['details']['data']['count'] - if data_id: - path = "feeds/{0}/data/{1}".format(feed, data_id) - return Data.from_dict(self._get(path)) - - params = {'limit': max_results} if max_results else None - data = [] - path = "feeds/{0}/data".format(feed) - while len(data) < max_results: - data.extend(list(map(Data.from_dict, self._get(path, - params=params)))) - nlink = self.get_next_link() - if not nlink: - break - # Parse the link for the query parameters - params = parse_qs(urlparse(nlink).query) - if max_results: - params['limit'] = max_results - len(data) - return data - - def get_next_link(self): - """Parse the `next` page URL in the pagination Link header. - - This is necessary because of a bug in the API's implementation of the - link header. If that bug is fixed, the link would be accesible by - response.links['next']['url'] and this method would be broken. - - :return: The url for the next page of data - :rtype: str - """ - if not self._last_response: - return - link_header = self._last_response.headers['link'] - res = re.search('rel="next", <(.+?)>', link_header) - if not res: - return - return res.groups()[0] - - def create_data(self, feed, data): - """Create a new row of data in the specified feed. - Returns a Data instance with details about the newly - appended row of data. - - :param string feed: Name/Key/ID of Adafruit IO feed. - :param Data data: Instance of the Data class. Must have a value property set. - """ - path = "feeds/{0}/data".format(feed) - return Data.from_dict(self._post(path, data._asdict())) - - def delete(self, feed, data_id): - """Delete data from a feed. - - :param string feed: Name/Key/ID of Adafruit IO feed. - :param string data_id: ID of the piece of data to delete. - """ - path = "feeds/{0}/data/{1}".format(feed, data_id) - self._delete(path) - - # feed functionality. - def feeds(self, feed=None): - """Retrieve a list of all feeds, or the specified feed. If feed is not - specified a list of all feeds will be returned. - - :param string feed: Name/Key/ID of Adafruit IO feed, defaults to None. - """ - if feed is None: - path = "feeds" - return list(map(Feed.from_dict, self._get(path))) - path = "feeds/{0}".format(feed) - return Feed.from_dict(self._get(path)) - - def create_feed(self, feed, group_key=None): - """Create the specified feed. - - :param string feed: Key of Adafruit IO feed. - :param group_key group: Group to place new feed in. - """ - f = feed._asdict() - del f['id'] # Don't pass id on create call - path = "feeds/" - if group_key is not None: # create feed in a group - path="/groups/%s/feeds"%group_key - return Feed.from_dict(self._post(path, {"feed": f})) - return Feed.from_dict(self._post(path, {"feed": f})) - - def delete_feed(self, feed): - """Delete the specified feed. - - :param string feed: Name/Key/ID of Adafruit IO feed. - """ - path = "feeds/{0}".format(feed) - self._delete(path) - - # Group functionality. - def groups(self, group=None): - """Retrieve a list of all groups, or the specified group. - - :param string group: Name/Key/ID of Adafruit IO Group. Defaults to None. - """ - if group is None: - path = "groups/" - return list(map(Group.from_dict, self._get(path))) - path = "groups/{0}".format(group) - return Group.from_dict(self._get(path)) - - def create_group(self, group): - """Create the specified group. - - :param string group: Name/Key/ID of Adafruit IO Group. - """ - path = "groups/" - return Group.from_dict(self._post(path, group._asdict())) - - def delete_group(self, group): - """Delete the specified group. - - :param string group: Name/Key/ID of Adafruit IO Group. - """ - path = "groups/{0}".format(group) - self._delete(path) - - # Dashboard functionality. - def dashboards(self, dashboard=None): - """Retrieve a list of all dashboards, or the specified dashboard. - - :param string dashboard: Key of Adafruit IO Dashboard. Defaults to None. - """ - if dashboard is None: - path = "dashboards/" - return list(map(Dashboard.from_dict, self._get(path))) - path = "dashboards/{0}".format(dashboard) - return Dashboard.from_dict(self._get(path)) - - def create_dashboard(self, dashboard): - """Create the specified dashboard. - - :param Dashboard dashboard: Dashboard object to create - """ - path = "dashboards/" - return Dashboard.from_dict(self._post(path, dashboard._asdict())) - - def delete_dashboard(self, dashboard): - """Delete the specified dashboard. - - :param string dashboard: Key of Adafruit IO Dashboard. - """ - path = "dashboards/{0}".format(dashboard) - self._delete(path) - - # Block functionality. - def blocks(self, dashboard, block=None): - """Retrieve a list of all blocks from a dashboard, or the specified block. - - :param string dashboard: Key of Adafruit IO Dashboard. - :param string block: id of Adafruit IO Block. Defaults to None. - """ - if block is None: - path = "dashboards/{0}/blocks".format(dashboard) - return list(map(Block.from_dict, self._get(path))) - path = "dashboards/{0}/blocks/{1}".format(dashboard, block) - return Block.from_dict(self._get(path)) - - def create_block(self, dashboard, block): - """Create the specified block under the specified dashboard. - - :param string dashboard: Key of Adafruit IO Dashboard. - :param Block block: Block object to create under dashboard - """ - path = "dashboards/{0}/blocks".format(dashboard) - return Block.from_dict(self._post(path, block._asdict())) - - def delete_block(self, dashboard, block): - """Delete the specified block. - - :param string dashboard: Key of Adafruit IO Dashboard. - :param string block: id of Adafruit IO Block. - """ - path = "dashboards/{0}/blocks/{1}".format(dashboard, block) - self._delete(path) - - # Layout functionality. - def layouts(self, dashboard): - """Retrieve the layouts array from a dashboard - - :param string dashboard: key of Adafruit IO Dashboard. - """ - path = "dashboards/{0}".format(dashboard) - dashboard = self._get(path) - return Layout.from_dict(dashboard['layouts']) - - def update_layout(self, dashboard, layout): - """Update the layout of the specified dashboard. - - :param string dashboard: Key of Adafruit IO Dashboard. - :param Layout layout: Layout object to update under dashboard - """ - path = "dashboards/{0}/update_layouts".format(dashboard) - return Layout.from_dict(self._post(path, {'layouts': layout._asdict()})) diff --git a/scripts/tempSensor/lib/Adafruit_IO/errors.py b/scripts/tempSensor/lib/Adafruit_IO/errors.py deleted file mode 100644 index 52b3fd77..00000000 --- a/scripts/tempSensor/lib/Adafruit_IO/errors.py +++ /dev/null @@ -1,61 +0,0 @@ -# Copyright (c) 2014 Adafruit Industries -# Author: Tony DiCola - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -import json, requests -from paho.mqtt.client import error_string - -class AdafruitIOError(Exception): - """Base class for all Adafruit IO request failures.""" - pass - - -class RequestError(Exception): - """General error for a failed Adafruit IO request.""" - def __init__(self, response): - error_message = self._parse_error(response) - super(RequestError, self).__init__("Adafruit IO request failed: {0} {1} - {2}".format( - response.status_code, response.reason, error_message)) - - def _parse_error(self, response): - content = response.json() - try: - return content['error'] - except ValueError: - return "" - - -class ThrottlingError(AdafruitIOError): - """Too many requests have been made to Adafruit IO in a short period of time. - Reduce the rate of requests and try again later. - """ - def __init__(self): - super(ThrottlingError, self).__init__("Exceeded the limit of Adafruit IO " \ - "requests in a short period of time. Please reduce the rate of requests " \ - "and try again later.") - - -class MQTTError(Exception): - """Handles connection attempt failed errors. - """ - def __init__(self, response): - error = error_string(response) - super(MQTTError, self).__init__(error) - pass \ No newline at end of file diff --git a/scripts/tempSensor/lib/Adafruit_IO/model.py b/scripts/tempSensor/lib/Adafruit_IO/model.py deleted file mode 100644 index 51d56338..00000000 --- a/scripts/tempSensor/lib/Adafruit_IO/model.py +++ /dev/null @@ -1,149 +0,0 @@ -# Copyright (c) 2014 Adafruit Industries -# Author: Tony DiCola - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. -from collections import namedtuple -# Handle python 2 and 3 (where map functions like itertools.imap) -try: - from itertools import imap as map -except ImportError: - # Ignore import error on python 3 since map already behaves as expected. - pass - - -# List of fields/properties that are present on a data object from IO. -DATA_FIELDS = [ 'created_epoch', - 'created_at', - 'updated_at', - 'value', - 'completed_at', - 'feed_id', - 'expiration', - 'position', - 'id', - 'lat', - 'lon', - 'ele'] - -FEED_FIELDS = [ 'name', - 'key', - 'id', - 'description', - 'unit_type', - 'unit_symbol', - 'history', - 'visibility', - 'license', - 'status_notify', - 'status_timeout'] - -GROUP_FIELDS = [ 'description', - 'source_keys', - 'id', - 'source', - 'key', - 'feeds', - 'properties', - 'name' ] - -DASHBOARD_FIELDS = [ 'name', - 'key', - 'description', - 'show_header', - 'color_mode', - 'block_borders', - 'header_image_url', - 'blocks' ] - -BLOCK_FIELDS = [ 'name', - 'id', - 'visual_type', - 'properties', - 'block_feeds' ] - -LAYOUT_FIELDS = ['xl', - 'lg', - 'md', - 'sm', - 'xs' ] - -# These are very simple data model classes that are based on namedtuple. This is -# to keep the classes simple and prevent any confusion around updating data -# locally and forgetting to send those updates back up to the IO service (since -# tuples are immutable you can't change them!). Depending on how people use the -# client it might be prudent to revisit this decision and consider making these -# full fledged classes that are mutable. -Data = namedtuple('Data', DATA_FIELDS) -Feed = namedtuple('Feed', FEED_FIELDS) -Group = namedtuple('Group', GROUP_FIELDS) -Dashboard = namedtuple('Dashboard', DASHBOARD_FIELDS) -Block = namedtuple('Block', BLOCK_FIELDS) -Layout = namedtuple('Layout', LAYOUT_FIELDS) - -# Magic incantation to make all parameters to the initializers optional with a -# default value of None. -Group.__new__.__defaults__ = tuple(None for x in GROUP_FIELDS) -Data.__new__.__defaults__ = tuple(None for x in DATA_FIELDS) -Layout.__new__.__defaults__ = tuple(None for x in LAYOUT_FIELDS) - -# explicitly set dashboard values so that 'color_mode' is 'dark' -Dashboard.__new__.__defaults__ = (None, None, None, False, "dark", True, None, None) - -# explicitly set block values so 'properties' is a dictionary -Block.__new__.__defaults__ = (None, None, None, {}, None) - -# explicitly set feed values -Feed.__new__.__defaults__ = (None, None, None, None, None, None, 'ON', 'Private', None, None, None) - -# Define methods to convert from dicts to the data types. -def _from_dict(cls, data): - # Convert dict to call to class initializer (to work with the data types - # base on namedtuple). However be very careful to preserve forwards - # compatibility by ignoring any attributes in the dict which are unknown - # by the data type. - params = {x: data.get(x, None) for x in cls._fields} - return cls(**params) - - -def _feed_from_dict(cls, data): - params = {x: data.get(x, None) for x in cls._fields} - return cls(**params) - - -def _group_from_dict(cls, data): - params = {x: data.get(x, None) for x in cls._fields} - # Parse the feeds if they're provided and generate feed instances. - params['feeds'] = tuple(map(Feed.from_dict, data.get('feeds', []))) - return cls(**params) - - -def _dashboard_from_dict(cls, data): - params = {x: data.get(x, None) for x in cls._fields} - # Parse the blocks if they're provided and generate block instances. - params['blocks'] = tuple(map(Block.from_dict, data.get('blocks', []))) - return cls(**params) - - -# Now add the from_dict class methods defined above to the data types. -Data.from_dict = classmethod(_from_dict) -Feed.from_dict = classmethod(_feed_from_dict) -Group.from_dict = classmethod(_group_from_dict) -Dashboard.from_dict = classmethod(_dashboard_from_dict) -Block.from_dict = classmethod(_from_dict) -Layout.from_dict = classmethod(_from_dict) diff --git a/scripts/tempSensor/lib/Adafruit_IO/mqtt_client.py b/scripts/tempSensor/lib/Adafruit_IO/mqtt_client.py deleted file mode 100644 index 198b4d6f..00000000 --- a/scripts/tempSensor/lib/Adafruit_IO/mqtt_client.py +++ /dev/null @@ -1,306 +0,0 @@ -# Copyright (c) 2020 Adafruit Industries -# Author: Tony DiCola, Brent Rubell - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. -import logging - -import paho.mqtt.client as mqtt -import sys -from .errors import MQTTError, RequestError - -# How long to wait before sending a keep alive (paho-mqtt configuration). -KEEP_ALIVE_SEC = 60 # One minute - -logger = logging.getLogger(__name__) - -forecast_types = ["current", "forecast_minutes_5", - "forecast_minutes_30", "forecast_hours_1", - "forecast_hours_2", "forecast_hours_6", - "forecast_hours_24", "forecast_days_1", - "forecast_days_2", "forecast_days_5",] - -class MQTTClient(object): - """Interface for publishing and subscribing to feed changes on Adafruit IO - using the MQTT protocol. - """ - - def __init__(self, username, key, service_host='io.adafruit.com', secure=True): - """Create instance of MQTT client. - - :param username: Adafruit.IO Username for your account. - :param key: Adafruit IO access key (AIO Key) for your account. - :param secure: (optional, boolean) Switches secure/insecure connections - - """ - self._username = username - self._service_host = service_host - if secure: - self._service_port = 8883 - elif not secure: - self._service_port = 1883 - # Initialize event callbacks to be None so they don't fire. - self.on_connect = None - self.on_disconnect = None - self.on_message = None - self.on_subscribe = None - # Initialize v1 MQTT client. - self._client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION1) - if secure: - self._client.tls_set_context() - self._secure = True - elif not secure: - print('**THIS CONNECTION IS INSECURE** SSL/TLS not supported for this platform') - self._secure = False - self._client.username_pw_set(username, key) - self._client.on_connect = self._mqtt_connect - self._client.on_disconnect = self._mqtt_disconnect - self._client.on_message = self._mqtt_message - self._client.on_subscribe = self._mqtt_subscribe - self._connected = False - - - def _mqtt_connect(self, client, userdata, flags, rc): - logger.debug('Client on_connect called.') - # Check if the result code is success (0) or some error (non-zero) and - # raise an exception if failed. - if rc == 0: - #raise RequestError(rc) - self._connected = True - print('Connected to Adafruit IO!') - else: - # handle RC errors within MQTTError class - raise MQTTError(rc) - # Call the on_connect callback if available. - if self.on_connect is not None: - self.on_connect(self) - - def _mqtt_disconnect(self, client, userdata, rc): - logger.debug('Client on_disconnect called.') - self._connected = False - # If this was an unexpected disconnect (non-zero result code) then just - # log the RC as an error. Continue on to call any disconnect handler - # so clients can potentially recover gracefully. - if rc != 0: - print('Unexpected disconnection.') - raise MQTTError(rc) - print('Disconnected from Adafruit IO!') - # Call the on_disconnect callback if available. - if self.on_disconnect is not None: - self.on_disconnect(self) - - def _mqtt_message(self, client, userdata, msg): - """Parse out the topic and call on_message callback - assume topic looks like `username/topic/id` - - """ - logger.debug('Client on_message called.') - parsed_topic = msg.topic.split('/') - if self.on_message is not None: - if parsed_topic[0] == 'time': - topic = parsed_topic[0] - payload = msg.payload.decode('utf-8') - elif parsed_topic[1] == 'groups': - topic = parsed_topic[3] - payload = msg.payload.decode('utf-8') - elif parsed_topic[2] == 'weather': - topic = parsed_topic[4] - payload = '' if msg.payload is None else msg.payload.decode('utf-8') - else: - topic = parsed_topic[2] - payload = '' if msg.payload is None else msg.payload.decode('utf-8') - else: - raise ValueError('on_message not defined') - self.on_message(self, topic, payload) - - def _mqtt_subscribe(self, client, userdata, mid, granted_qos): - """Called when broker responds to a subscribe request.""" - logger.debug('Client called on_subscribe') - if self.on_subscribe is not None: - self.on_subscribe(self, userdata, mid, granted_qos) - - def connect(self, **kwargs): - """Connect to the Adafruit.IO service. Must be called before any loop - or publish operations are called. Will raise an exception if a - connection cannot be made. Optional keyword arguments will be passed - to paho-mqtt client connect function. - - """ - # Skip calling connect if already connected. - if self._connected: - return - # If given, use user-provided keepalive, otherwise default to KEEP_ALIVE_SEC - keepalive = kwargs.pop('keepalive', KEEP_ALIVE_SEC) - # Connect to the Adafruit IO MQTT service. - self._client.connect(self._service_host, port=self._service_port, - keepalive=keepalive, **kwargs) - - def is_connected(self): - """Returns True if connected to Adafruit.IO and False if not connected. - - """ - return self._connected - - def disconnect(self): - """Disconnect MQTT client if connected.""" - if self._connected: - self._client.disconnect() - - def loop_background(self, stop=None): - """Starts a background thread to listen for messages from Adafruit.IO - and call the appropriate callbacks when feed events occur. Will return - immediately and will not block execution. Should only be called once. - - :param bool stop: Stops the execution of the background loop. - - """ - if stop: - self._client.loop_stop() - self._client.loop_start() - - def loop_blocking(self): - """Listen for messages from Adafruit.IO and call the appropriate - callbacks when feed events occur. This call will block execution of - your program and will not return until disconnect is explicitly called. - - This is useful if your program doesn't need to do anything else except - listen and respond to Adafruit.IO feed events. If you need to do other - processing, consider using the loop_background function to run a loop - in the background. - - """ - self._client.loop_forever() - - def loop(self, timeout_sec=1.0): - """Manually process messages from Adafruit.IO. This is meant to be used - inside your own main loop, where you periodically call this function to - make sure messages are being processed to and from Adafruit_IO. - - The optional timeout_sec parameter specifies at most how long to block - execution waiting for messages when this function is called. The default - is one second. - - """ - self._client.loop(timeout=timeout_sec) - - def subscribe(self, feed_id, feed_user=None, qos=0): - """Subscribe to changes on the specified feed. When the feed is updated - the on_message function will be called with the feed_id and new value. - - :param str feed_id: The key of the feed to subscribe to. - :param str feed_user: Optional, identifies feed owner. Used for feed sharing. - :param int qos: The QoS to use when subscribing. Defaults to 0. - - """ - if qos > 1: - raise MQTTError("Adafruit IO only supports a QoS level of 0 or 1.") - if feed_user is not None: - (res, mid) = self._client.subscribe('{0}/feeds/{1}'.format(feed_user, feed_id, qos=qos)) - else: - (res, mid) = self._client.subscribe('{0}/feeds/{1}'.format(self._username, feed_id), qos=qos) - return res, mid - - def subscribe_group(self, group_id, qos=0): - """Subscribe to changes on the specified group. When the group is updated - the on_message function will be called with the group_id and the new value. - - :param str group_id: The id of the group to subscribe to. - :param int qos: The QoS to use when subscribing. Defaults to 0. - - """ - self._client.subscribe('{0}/groups/{1}'.format(self._username, group_id), qos=qos) - - def subscribe_randomizer(self, randomizer_id): - """Subscribe to changes on a specified random data stream from - Adafruit IO's random data service. - - MQTT random word subscriptions will publish data once per minute to - every client that is subscribed to the same topic. - - :param int randomizer_id: ID of the random word record you want data for. - - """ - self._client.subscribe('{0}/integration/words/{1}'.format(self._username, randomizer_id)) - - def subscribe_weather(self, weather_id, forecast_type): - """Subscribe to Adafruit IO Weather - :param int weather_id: weather record you want data for - :param string type: type of forecast data requested - """ - if forecast_type in forecast_types: - self._client.subscribe('{0}/integration/weather/{1}/{2}'.format(self._username, weather_id, forecast_type)) - else: - raise TypeError("Invalid Forecast Type Specified.") - return - - def subscribe_time(self, time): - """Subscribe to changes on the Adafruit IO time feeds. When the feed is - updated, the on_message function will be called and publish a new value: - time feeds: - millis: milliseconds - seconds: seconds - iso: ISO-8601 (https://en.wikipedia.org/wiki/ISO_8601) - """ - if time == 'millis' or time == 'seconds': - self._client.subscribe('time/{0}'.format(time)) - elif time == 'iso': - self._client.subscribe('time/ISO-8601') - else: - raise TypeError('Invalid Time Feed Specified.') - return - - def unsubscribe(self, feed_id=None, group_id=None): - """Unsubscribes from a specified MQTT topic. - Note: this does not prevent publishing to a topic, it will unsubscribe - from receiving messages via on_message. - """ - if feed_id is not None: - self._client.unsubscribe('{0}/feeds/{1}'.format(self._username, feed_id)) - elif group_id is not None: - self._client.unsubscribe('{0}/groups/{1}'.format(self._username, group_id)) - else: - raise TypeError('Invalid topic type specified.') - return - - def receive(self, feed_id): - """Receive the last published value from a specified feed. - - :param string feed_id: The ID of the feed to update. - :parm string value: The new value to publish to the feed - """ - (res, self._pub_mid) = self._client.publish('{0}/feeds/{1}/get'.format(self._username, feed_id), - payload='') - - def publish(self, feed_id, value=None, group_id=None, feed_user=None): - """Publish a value to a specified feed. - - Params: - - feed_id: The id of the feed to update. - - value: The new value to publish to the feed. - - (optional) group_id: The id of the group to update. - - (optional) feed_user: The feed owner's username. Used for Sharing Feeds. - """ - if feed_user is not None: # shared feed - (res, self._pub_mid) = self._client.publish('{0}/feeds/{1}'.format(feed_user, feed_id), - payload=value) - elif group_id is not None: # group-specified feed - self._client.publish('{0}/feeds/{1}.{2}'.format(self._username, group_id, feed_id), - payload=value) - else: # regular feed - (res, self._pub_mid) = self._client.publish('{0}/feeds/{1}'.format(self._username, feed_id), - payload=value) diff --git a/scripts/tempSensor/lib/CHANGELOG.md b/scripts/tempSensor/lib/CHANGELOG.md deleted file mode 100644 index 78fec497..00000000 --- a/scripts/tempSensor/lib/CHANGELOG.md +++ /dev/null @@ -1,50 +0,0 @@ -2.0.0 ------ - -* Repackage to hatch/pyproject.toml -* Drop Python 2.7 support -* Switch from smbu2 to smbus2 - -1.1.1 ------ - -* New: constants to clarify heater on/off states - -1.1.0 ------ - -* New: support for BME688 "high" gas resistance variant -* New: set/get gas heater disable bit -* Enhancement: fail with descriptive RuntimeError when chip is not detected - -1.0.5 ------ - -* New: set_temp_offset to calibrate temperature offset in degrees C - -1.0.4 ------ - -* Fix to range_sw_err for extremely high gas readings -* Convert to unsigned int to fix negative gas readings - -1.0.3 ------ - -* Merged temperature compensation fix from Bosch's BME680_driver 3.5.3 - -1.0.2 ------ - -* Fixed set_gas_heater_temperature to avoid i2c TypeError - -1.0.1 ------ - -* Added Manifest to Python package - -1.0.0 ------ - -* Initial release - diff --git a/scripts/tempSensor/lib/LICENSE b/scripts/tempSensor/lib/LICENSE deleted file mode 100644 index b3e25b24..00000000 --- a/scripts/tempSensor/lib/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2018 Pimoroni Ltd - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/scripts/tempSensor/lib/README.md b/scripts/tempSensor/lib/README.md deleted file mode 100644 index 0c71b7f7..00000000 --- a/scripts/tempSensor/lib/README.md +++ /dev/null @@ -1,56 +0,0 @@ -# BME680 - -[![Build Status](https://img.shields.io/github/actions/workflow/status/pimoroni/bme680-python/test.yml?branch=main)](https://github.com/pimoroni/bme680-python/actions/workflows/test.yml) -[![Coverage Status](https://coveralls.io/repos/github/pimoroni/bme680-python/badge.svg?branch=main)](https://coveralls.io/github/pimoroni/bme680-python?branch=main) -[![PyPi Package](https://img.shields.io/pypi/v/bme680.svg)](https://pypi.python.org/pypi/bme680) -[![Python Versions](https://img.shields.io/pypi/pyversions/bme680.svg)](https://pypi.python.org/pypi/bme680) - -https://shop.pimoroni.com/products/bme680 - -The state-of-the-art BME680 breakout lets you measure temperature, pressure, humidity, and indoor air quality. - -## Installing - -### Full install (recommended): - -We've created an easy installation script that will install all pre-requisites and get your BME680 -up and running with minimal efforts. To run it, fire up Terminal which you'll find in Menu -> Accessories -> Terminal -on your Raspberry Pi desktop, as illustrated below: - -![Finding the terminal](http://get.pimoroni.com/resources/github-repo-terminal.png) - -In the new terminal window type the command exactly as it appears below (check for typos) and follow the on-screen instructions: - -```bash -git clone https://github.com/pimoroni/bme680-python -cd bme680-python -./install.sh -``` - -**Note** Libraries will be installed in the "pimoroni" virtual environment, you will need to activate it to run examples: - -``` -source ~/.virtualenvs/pimoroni/bin/activate -``` - -### Development: - -If you want to contribute, or like living on the edge of your seat by having the latest code, you can install the development version like so: - -```bash -git clone https://github.com/pimoroni/bme680-python -cd bme680-python -./install.sh --unstable -``` - -In all cases you will have to enable the i2c bus: - -``` -sudo raspi-config nonint do_i2c 0 -``` - -## Documentation & Support - -* Guides and tutorials - https://learn.pimoroni.com/bme680-breakout -* Get help - http://forums.pimoroni.com/c/support - diff --git a/scripts/tempSensor/lib/adafruit_bme680.py b/scripts/tempSensor/lib/adafruit_bme680.py deleted file mode 100644 index e77207a7..00000000 --- a/scripts/tempSensor/lib/adafruit_bme680.py +++ /dev/null @@ -1,769 +0,0 @@ -# SPDX-FileCopyrightText: 2017 ladyada for Adafruit Industries -# -# SPDX-License-Identifier: MIT AND BSD-3-Clause - - -""" -`adafruit_bme680` -================================================================================ - -CircuitPython library for BME680 temperature, pressure and humidity sensor. - - -* Author(s): Limor Fried, William Garber, many others - - -Implementation Notes --------------------- - -**Hardware:** - -* `Adafruit BME680 Temp, Humidity, Pressure and Gas Sensor `_ - -**Software and Dependencies:** - -* Adafruit CircuitPython firmware for the supported boards: - https://github.com/adafruit/circuitpython/releases -* Adafruit's Bus Device library: https://github.com/adafruit/Adafruit_CircuitPython_BusDevice -""" - -import math -import struct -import time - -from micropython import const - - -def delay_microseconds(nusec): - """HELP must be same as dev->delay_us""" - time.sleep(nusec / 1000000.0) - - -try: - # Used only for type annotations. - - import typing - - from busio import I2C, SPI - from circuitpython_typing import ReadableBuffer - from digitalio import DigitalInOut - -except ImportError: - pass - -__version__ = "0.0.0+auto.0" -__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_BME680.git" - - -# I2C ADDRESS/BITS/SETTINGS NEW -# ----------------------------------------------------------------------- -_BME68X_ENABLE_HEATER = const(0x00) -_BME68X_DISABLE_HEATER = const(0x01) -_BME68X_DISABLE_GAS_MEAS = const(0x00) -_BME68X_ENABLE_GAS_MEAS_L = const(0x01) -_BME68X_ENABLE_GAS_MEAS_H = const(0x02) -_BME68X_SLEEP_MODE = const(0) -_BME68X_FORCED_MODE = const(1) -_BME68X_VARIANT_GAS_LOW = const(0x00) -_BME68X_VARIANT_GAS_HIGH = const(0x01) -_BME68X_HCTRL_MSK = const(0x08) -_BME68X_HCTRL_POS = const(3) -_BME68X_NBCONV_MSK = const(0x0F) -_BME68X_RUN_GAS_MSK = const(0x30) -_BME68X_RUN_GAS_POS = const(4) -_BME68X_MODE_MSK = const(0x03) -_BME68X_PERIOD_POLL = const(10000) -_BME68X_REG_CTRL_GAS_0 = const(0x70) -_BME68X_REG_CTRL_GAS_1 = const(0x71) - -# I2C ADDRESS/BITS/SETTINGS -# ----------------------------------------------------------------------- -_BME680_CHIPID = const(0x61) - -_BME680_REG_CHIPID = const(0xD0) -_BME68X_REG_VARIANT = const(0xF0) -_BME680_BME680_COEFF_ADDR1 = const(0x89) -_BME680_BME680_COEFF_ADDR2 = const(0xE1) -_BME680_BME680_RES_HEAT_0 = const(0x5A) -_BME680_BME680_GAS_WAIT_0 = const(0x64) - -_BME680_REG_SOFTRESET = const(0xE0) -_BME680_REG_CTRL_GAS = const(0x71) -_BME680_REG_CTRL_HUM = const(0x72) -_BME680_REG_STATUS = const(0x73) -_BME680_REG_CTRL_MEAS = const(0x74) -_BME680_REG_CONFIG = const(0x75) - -_BME680_REG_MEAS_STATUS = const(0x1D) -_BME680_REG_PDATA = const(0x1F) -_BME680_REG_TDATA = const(0x22) -_BME680_REG_HDATA = const(0x25) - -_BME680_SAMPLERATES = (0, 1, 2, 4, 8, 16) -_BME680_FILTERSIZES = (0, 1, 3, 7, 15, 31, 63, 127) - -_BME680_RUNGAS = const(0x10) - -_LOOKUP_TABLE_1 = ( - 2147483647.0, - 2147483647.0, - 2147483647.0, - 2147483647.0, - 2147483647.0, - 2126008810.0, - 2147483647.0, - 2130303777.0, - 2147483647.0, - 2147483647.0, - 2143188679.0, - 2136746228.0, - 2147483647.0, - 2126008810.0, - 2147483647.0, - 2147483647.0, -) - -_LOOKUP_TABLE_2 = ( - 4096000000.0, - 2048000000.0, - 1024000000.0, - 512000000.0, - 255744255.0, - 127110228.0, - 64000000.0, - 32258064.0, - 16016016.0, - 8000000.0, - 4000000.0, - 2000000.0, - 1000000.0, - 500000.0, - 250000.0, - 125000.0, -) - - -def bme_set_bits(reg_data, bitname_msk, bitname_pos, data): - """ - Macro to set bits - data2 = data << bitname_pos - set masked bits from data2 in reg_data - """ - return (reg_data & ~bitname_msk) | ((data << bitname_pos) & bitname_msk) - - -def bme_set_bits_pos_0(reg_data, bitname_msk, data): - """ - Macro to set bits starting from position 0 - set masked bits from data in reg_data - """ - return (reg_data & ~bitname_msk) | (data & bitname_msk) - - -def _read24(arr: ReadableBuffer) -> float: - """Parse an unsigned 24-bit value as a floating point and return it.""" - ret = 0.0 - # print([hex(i) for i in arr]) - for b in arr: - ret *= 256.0 - ret += float(b & 0xFF) - return ret - - -class Adafruit_BME680: - """Driver from BME680 air quality sensor - - :param int refresh_rate: Maximum number of readings per second. Faster property reads - will be from the previous reading.""" - - def __init__(self, *, refresh_rate: int = 10) -> None: - """Check the BME680 was found, read the coefficients and enable the sensor for continuous - reads.""" - self._write(_BME680_REG_SOFTRESET, [0xB6]) - time.sleep(0.005) - - # Check device ID. - chip_id = self._read_byte(_BME680_REG_CHIPID) - if chip_id != _BME680_CHIPID: - raise RuntimeError("Failed to find BME680! Chip ID 0x%x" % chip_id) - - # Get variant - self._chip_variant = self._read_byte(_BME68X_REG_VARIANT) - - self._read_calibration() - - # set up heater - self._write(_BME680_BME680_RES_HEAT_0, [0x73]) - self._write(_BME680_BME680_GAS_WAIT_0, [0x65]) - - self.sea_level_pressure = 1013.25 - """Pressure in hectoPascals at sea level. Used to calibrate :attr:`altitude`.""" - - # Default oversampling and filter register values. - self._pressure_oversample = 0b011 - self._temp_oversample = 0b100 - self._humidity_oversample = 0b010 - self._filter = 0b010 - - # Gas measurements, as a mask applied to _BME680_RUNGAS - self._run_gas = 0xFF - - self._adc_pres = None - self._adc_temp = None - self._adc_hum = None - self._adc_gas = None - self._gas_range = None - self._t_fine = None - - self._last_reading = 0 - self._min_refresh_time = 1 / refresh_rate - - self._amb_temp = 25 # Copy required parameters from reference bme68x_dev struct - self.set_gas_heater(320, 150) # heater 320 deg C for 150 msec - - @property - def pressure_oversample(self) -> int: - """The oversampling for pressure sensor""" - return _BME680_SAMPLERATES[self._pressure_oversample] - - @pressure_oversample.setter - def pressure_oversample(self, sample_rate: int) -> None: - if sample_rate in _BME680_SAMPLERATES: - self._pressure_oversample = _BME680_SAMPLERATES.index(sample_rate) - else: - raise RuntimeError("Invalid oversample") - - @property - def humidity_oversample(self) -> int: - """The oversampling for humidity sensor""" - return _BME680_SAMPLERATES[self._humidity_oversample] - - @humidity_oversample.setter - def humidity_oversample(self, sample_rate: int) -> None: - if sample_rate in _BME680_SAMPLERATES: - self._humidity_oversample = _BME680_SAMPLERATES.index(sample_rate) - else: - raise RuntimeError("Invalid oversample") - - @property - def temperature_oversample(self) -> int: - """The oversampling for temperature sensor""" - return _BME680_SAMPLERATES[self._temp_oversample] - - @temperature_oversample.setter - def temperature_oversample(self, sample_rate: int) -> None: - if sample_rate in _BME680_SAMPLERATES: - self._temp_oversample = _BME680_SAMPLERATES.index(sample_rate) - else: - raise RuntimeError("Invalid oversample") - - @property - def filter_size(self) -> int: - """The filter size for the built in IIR filter""" - return _BME680_FILTERSIZES[self._filter] - - @filter_size.setter - def filter_size(self, size: int) -> None: - if size in _BME680_FILTERSIZES: - self._filter = _BME680_FILTERSIZES.index(size) - else: - raise RuntimeError("Invalid size") - - @property - def temperature(self) -> float: - """The compensated temperature in degrees Celsius.""" - self._perform_reading() - calc_temp = ((self._t_fine * 5) + 128) / 256 - return calc_temp / 100 - - @property - def pressure(self) -> float: - """The barometric pressure in hectoPascals""" - self._perform_reading() - var1 = (self._t_fine / 2) - 64000 - var2 = ((var1 / 4) * (var1 / 4)) / 2048 - var2 = (var2 * self._pressure_calibration[5]) / 4 - var2 = var2 + (var1 * self._pressure_calibration[4] * 2) - var2 = (var2 / 4) + (self._pressure_calibration[3] * 65536) - var1 = ((((var1 / 4) * (var1 / 4)) / 8192) * (self._pressure_calibration[2] * 32) / 8) + ( - (self._pressure_calibration[1] * var1) / 2 - ) - var1 = var1 / 262144 - var1 = ((32768 + var1) * self._pressure_calibration[0]) / 32768 - calc_pres = 1048576 - self._adc_pres - calc_pres = (calc_pres - (var2 / 4096)) * 3125 - calc_pres = (calc_pres / var1) * 2 - var1 = (self._pressure_calibration[8] * (((calc_pres / 8) * (calc_pres / 8)) / 8192)) / 4096 - var2 = ((calc_pres / 4) * self._pressure_calibration[7]) / 8192 - var3 = (((calc_pres / 256) ** 3) * self._pressure_calibration[9]) / 131072 - calc_pres += (var1 + var2 + var3 + (self._pressure_calibration[6] * 128)) / 16 - return calc_pres / 100 - - @property - def relative_humidity(self) -> float: - """The relative humidity in RH %""" - return self.humidity - - @property - def humidity(self) -> float: - """The relative humidity in RH %""" - self._perform_reading() - temp_scaled = ((self._t_fine * 5) + 128) / 256 - var1 = (self._adc_hum - (self._humidity_calibration[0] * 16)) - ( - (temp_scaled * self._humidity_calibration[2]) / 200 - ) - var2 = ( - self._humidity_calibration[1] - * ( - ((temp_scaled * self._humidity_calibration[3]) / 100) - + ( - ((temp_scaled * ((temp_scaled * self._humidity_calibration[4]) / 100)) / 64) - / 100 - ) - + 16384 - ) - ) / 1024 - var3 = var1 * var2 - var4 = self._humidity_calibration[5] * 128 - var4 = (var4 + ((temp_scaled * self._humidity_calibration[6]) / 100)) / 16 - var5 = ((var3 / 16384) * (var3 / 16384)) / 1024 - var6 = (var4 * var5) / 2 - calc_hum = (((var3 + var6) / 1024) * 1000) / 4096 - calc_hum /= 1000 # get back to RH - - calc_hum = min(calc_hum, 100) - calc_hum = max(calc_hum, 0) - return calc_hum - - @property - def altitude(self) -> float: - """The altitude based on current :attr:`pressure` vs the sea level pressure - (:attr:`sea_level_pressure`) - which you must enter ahead of time)""" - pressure = self.pressure # in Si units for hPascal - return 44330 * (1.0 - math.pow(pressure / self.sea_level_pressure, 0.1903)) - - @property - def gas(self) -> int: - """The gas resistance in ohms""" - self._perform_reading() - if self._chip_variant == 0x01: - # taken from https://github.com/BoschSensortec/BME68x-Sensor-API - var1 = 262144 >> self._gas_range - var2 = self._adc_gas - 512 - var2 *= 3 - var2 = 4096 + var2 - calc_gas_res = (10000 * var1) / var2 - calc_gas_res = calc_gas_res * 100 - else: - var1 = ((1340 + (5 * self._sw_err)) * (_LOOKUP_TABLE_1[self._gas_range])) / 65536 - var2 = ((self._adc_gas * 32768) - 16777216) + var1 - var3 = (_LOOKUP_TABLE_2[self._gas_range] * var1) / 512 - calc_gas_res = (var3 + (var2 / 2)) / var2 - return int(calc_gas_res) - - def _perform_reading(self) -> None: - """Perform a single-shot reading from the sensor and fill internal data structure for - calculations""" - if time.monotonic() - self._last_reading < self._min_refresh_time: - return - - # set filter - self._write(_BME680_REG_CONFIG, [self._filter << 2]) - # turn on temp oversample & pressure oversample - self._write( - _BME680_REG_CTRL_MEAS, - [(self._temp_oversample << 5) | (self._pressure_oversample << 2)], - ) - # turn on humidity oversample - self._write(_BME680_REG_CTRL_HUM, [self._humidity_oversample]) - # gas measurements enabled - if self._chip_variant == 0x01: - self._write(_BME680_REG_CTRL_GAS, [(self._run_gas & _BME680_RUNGAS) << 1]) - else: - self._write(_BME680_REG_CTRL_GAS, [(self._run_gas & _BME680_RUNGAS)]) - ctrl = self._read_byte(_BME680_REG_CTRL_MEAS) - ctrl = (ctrl & 0xFC) | 0x01 # enable single shot! - self._write(_BME680_REG_CTRL_MEAS, [ctrl]) - new_data = False - while not new_data: - data = self._read(_BME680_REG_MEAS_STATUS, 17) - new_data = data[0] & 0x80 != 0 - time.sleep(0.005) - self._last_reading = time.monotonic() - - self._adc_pres = _read24(data[2:5]) / 16 - self._adc_temp = _read24(data[5:8]) / 16 - self._adc_hum = struct.unpack(">H", bytes(data[8:10]))[0] - if self._chip_variant == 0x01: - self._adc_gas = int(struct.unpack(">H", bytes(data[15:17]))[0] / 64) - self._gas_range = data[16] & 0x0F - else: - self._adc_gas = int(struct.unpack(">H", bytes(data[13:15]))[0] / 64) - self._gas_range = data[14] & 0x0F - - var1 = (self._adc_temp / 8) - (self._temp_calibration[0] * 2) - var2 = (var1 * self._temp_calibration[1]) / 2048 - var3 = ((var1 / 2) * (var1 / 2)) / 4096 - var3 = (var3 * self._temp_calibration[2] * 16) / 16384 - self._t_fine = int(var2 + var3) - - def _read_calibration(self) -> None: - """Read & save the calibration coefficients""" - coeff = self._read(_BME680_BME680_COEFF_ADDR1, 25) - coeff += self._read(_BME680_BME680_COEFF_ADDR2, 16) - - coeff = list(struct.unpack(" int: - """Read a byte register value and return it""" - return self._read(register, 1)[0] - - def _read(self, register: int, length: int) -> bytearray: - raise NotImplementedError() - - def _write(self, register: int, values: bytearray) -> None: - raise NotImplementedError() - - def set_gas_heater(self, heater_temp: int, heater_time: int) -> bool: - """ - Enable and configure gas reading + heater (None disables) - :param heater_temp: Desired temperature in degrees Centigrade - :param heater_time: Time to keep heater on in milliseconds - :return: True on success, False on failure - """ - try: - if (heater_temp is None) or (heater_time is None): - self._set_heatr_conf(heater_temp or 0, heater_time or 0, enable=False) - else: - self._set_heatr_conf(heater_temp, heater_time) - except OSError: - return False - return True - - def _set_heatr_conf(self, heater_temp: int, heater_time: int, enable: bool = True) -> None: - # restrict to BME68X_FORCED_MODE - op_mode: int = _BME68X_FORCED_MODE - nb_conv: int = 0 - hctrl: int = _BME68X_ENABLE_HEATER - run_gas: int = 0 - ctrl_gas_data_0: int = 0 - ctrl_gas_data_1: int = 0 - - self._set_op_mode(_BME68X_SLEEP_MODE) - self._set_conf(heater_temp, heater_time, op_mode) - ctrl_gas_data_0 = self._read_byte(_BME68X_REG_CTRL_GAS_0) - ctrl_gas_data_1 = self._read_byte(_BME68X_REG_CTRL_GAS_1) - if enable: - hctrl = _BME68X_ENABLE_HEATER - if self._chip_variant == _BME68X_VARIANT_GAS_HIGH: - run_gas = _BME68X_ENABLE_GAS_MEAS_H - else: - run_gas = _BME68X_ENABLE_GAS_MEAS_L - else: - hctrl = _BME68X_DISABLE_HEATER - run_gas = _BME68X_DISABLE_GAS_MEAS - self._run_gas = ~(run_gas - 1) - - ctrl_gas_data_0 = bme_set_bits(ctrl_gas_data_0, _BME68X_HCTRL_MSK, _BME68X_HCTRL_POS, hctrl) - ctrl_gas_data_1 = bme_set_bits_pos_0(ctrl_gas_data_1, _BME68X_NBCONV_MSK, nb_conv) - ctrl_gas_data_1 = bme_set_bits( - ctrl_gas_data_1, _BME68X_RUN_GAS_MSK, _BME68X_RUN_GAS_POS, run_gas - ) - self._write(_BME68X_REG_CTRL_GAS_0, [ctrl_gas_data_0]) - self._write(_BME68X_REG_CTRL_GAS_1, [ctrl_gas_data_1]) - - def _set_op_mode(self, op_mode: int) -> None: - """ - * @brief This API is used to set the operation mode of the sensor - """ - tmp_pow_mode: int = 0 - pow_mode: int = _BME68X_FORCED_MODE - # Call until in sleep - - # was a do {} while() loop - while pow_mode != _BME68X_SLEEP_MODE: - tmp_pow_mode = self._read_byte(_BME680_REG_CTRL_MEAS) - # Put to sleep before changing mode - pow_mode = tmp_pow_mode & _BME68X_MODE_MSK - if pow_mode != _BME68X_SLEEP_MODE: - tmp_pow_mode &= ~_BME68X_MODE_MSK # Set to sleep - self._write(_BME680_REG_CTRL_MEAS, [tmp_pow_mode]) - # dev->delay_us(_BME68X_PERIOD_POLL, dev->intf_ptr) # HELP - delay_microseconds(_BME68X_PERIOD_POLL) - # Already in sleep - if op_mode != _BME68X_SLEEP_MODE: - tmp_pow_mode = (tmp_pow_mode & ~_BME68X_MODE_MSK) | (op_mode & _BME68X_MODE_MSK) - self._write(_BME680_REG_CTRL_MEAS, [tmp_pow_mode]) - - def _set_conf(self, heater_temp: int, heater_time: int, op_mode: int) -> None: - """ - This internal API is used to set heater configurations - """ - - if op_mode != _BME68X_FORCED_MODE: - raise OSError("GasHeaterException: _set_conf not forced mode") - rh_reg_data: int = self._calc_res_heat(heater_temp) - gw_reg_data: int = self._calc_gas_wait(heater_time) - self._write(_BME680_BME680_RES_HEAT_0, [rh_reg_data]) - self._write(_BME680_BME680_GAS_WAIT_0, [gw_reg_data]) - - def _calc_res_heat(self, temp: int) -> int: - """ - This internal API is used to calculate the heater resistance value using float - """ - gh1: int = self._gas_calibration[0] - gh2: int = self._gas_calibration[1] - gh3: int = self._gas_calibration[2] - htr: int = self._heat_range - htv: int = self._heat_val - amb: int = self._amb_temp - - temp = min(temp, 400) # Cap temperature - - var1: int = ((int(amb) * gh3) / 1000) * 256 - var2: int = (gh1 + 784) * (((((gh2 + 154009) * temp * 5) / 100) + 3276800) / 10) - var3: int = var1 + (var2 / 2) - var4: int = var3 / (htr + 4) - var5: int = (131 * htv) + 65536 - heatr_res_x100: int = int(((var4 / var5) - 250) * 34) - heatr_res: int = int((heatr_res_x100 + 50) / 100) - - return heatr_res - - def _calc_res_heat(self, temp: int) -> int: - """ - This internal API is used to calculate the heater resistance value - """ - gh1: float = float(self._gas_calibration[0]) - gh2: float = float(self._gas_calibration[1]) - gh3: float = float(self._gas_calibration[2]) - htr: float = float(self._heat_range) - htv: float = float(self._heat_val) - amb: float = float(self._amb_temp) - - temp = min(temp, 400) # Cap temperature - - var1: float = (gh1 / (16.0)) + 49.0 - var2: float = ((gh2 / (32768.0)) * (0.0005)) + 0.00235 - var3: float = gh3 / (1024.0) - var4: float = var1 * (1.0 + (var2 * float(temp))) - var5: float = var4 + (var3 * amb) - res_heat: int = int(3.4 * ((var5 * (4 / (4 + htr)) * (1 / (1 + (htv * 0.002)))) - 25)) - return res_heat - - def _calc_gas_wait(self, dur: int) -> int: - """ - This internal API is used to calculate the gas wait - """ - factor: int = 0 - durval: int = 0xFF # Max duration - - if dur >= 0xFC0: - return durval - while dur > 0x3F: - dur = dur / 4 - factor += 1 - durval = int(dur + (factor * 64)) - return durval - - -class Adafruit_BME680_I2C(Adafruit_BME680): - """Driver for I2C connected BME680. - - :param ~busio.I2C i2c: The I2C bus the BME680 is connected to. - :param int address: I2C device address. Defaults to :const:`0x77` - :param bool debug: Print debug statements when `True`. Defaults to `False` - :param int refresh_rate: Maximum number of readings per second. Faster property reads - will be from the previous reading. - - **Quickstart: Importing and using the BME680** - - Here is an example of using the :class:`BMP680_I2C` class. - First you will need to import the libraries to use the sensor - - .. code-block:: python - - import board - import adafruit_bme680 - - Once this is done you can define your ``board.I2C`` object and define your sensor object - - .. code-block:: python - - i2c = board.I2C() # uses board.SCL and board.SDA - bme680 = adafruit_bme680.Adafruit_BME680_I2C(i2c) - - You need to setup the pressure at sea level - - .. code-block:: python - - bme680.sea_level_pressure = 1013.25 - - Now you have access to the :attr:`temperature`, :attr:`gas`, :attr:`relative_humidity`, - :attr:`pressure` and :attr:`altitude` attributes - - .. code-block:: python - - temperature = bme680.temperature - gas = bme680.gas - relative_humidity = bme680.relative_humidity - pressure = bme680.pressure - altitude = bme680.altitude - - """ - - def __init__( - self, - i2c: I2C, - address: int = 0x77, - debug: bool = False, - *, - refresh_rate: int = 10, - ) -> None: - """Initialize the I2C device at the 'address' given""" - from adafruit_bus_device import ( - i2c_device, - ) - - self._i2c = i2c_device.I2CDevice(i2c, address) - self._debug = debug - super().__init__(refresh_rate=refresh_rate) - - def _read(self, register: int, length: int) -> bytearray: - """Returns an array of 'length' bytes from the 'register'""" - with self._i2c as i2c: - i2c.write(bytes([register & 0xFF])) - result = bytearray(length) - i2c.readinto(result) - if self._debug: - print(f"\t${register:02X} => {[hex(i) for i in result]}") - return result - - def _write(self, register: int, values: ReadableBuffer) -> None: - """Writes an array of 'length' bytes to the 'register'""" - with self._i2c as i2c: - buffer = bytearray(2 * len(values)) - for i, value in enumerate(values): - buffer[2 * i] = register + i - buffer[2 * i + 1] = value - i2c.write(buffer) - if self._debug: - print(f"\t${values[0]:02X} <= {[hex(i) for i in values[1:]]}") - - -class Adafruit_BME680_SPI(Adafruit_BME680): - """Driver for SPI connected BME680. - - :param ~busio.SPI spi: SPI device - :param ~digitalio.DigitalInOut cs: Chip Select - :param bool debug: Print debug statements when `True`. Defaults to `False` - :param int baudrate: Clock rate, default is :const:`100000` - :param int refresh_rate: Maximum number of readings per second. Faster property reads - will be from the previous reading. - - - **Quickstart: Importing and using the BME680** - - Here is an example of using the :class:`BMP680_SPI` class. - First you will need to import the libraries to use the sensor - - .. code-block:: python - - import board - from digitalio import DigitalInOut, Direction - import adafruit_bme680 - - Once this is done you can define your ``board.SPI`` object and define your sensor object - - .. code-block:: python - - cs = digitalio.DigitalInOut(board.D10) - spi = board.SPI() - bme680 = adafruit_bme680.Adafruit_BME680_SPI(spi, cs) - - You need to setup the pressure at sea level - - .. code-block:: python - - bme680.sea_level_pressure = 1013.25 - - Now you have access to the :attr:`temperature`, :attr:`gas`, :attr:`relative_humidity`, - :attr:`pressure` and :attr:`altitude` attributes - - .. code-block:: python - - temperature = bme680.temperature - gas = bme680.gas - relative_humidity = bme680.relative_humidity - pressure = bme680.pressure - altitude = bme680.altitude - - """ - - def __init__( # noqa: PLR0913 Too many arguments in function definition - self, - spi: SPI, - cs: DigitalInOut, - baudrate: int = 100000, - debug: bool = False, - *, - refresh_rate: int = 10, - ) -> None: - from adafruit_bus_device import ( - spi_device, - ) - - self._spi = spi_device.SPIDevice(spi, cs, baudrate=baudrate) - self._debug = debug - super().__init__(refresh_rate=refresh_rate) - - def _read(self, register: int, length: int) -> bytearray: - if register != _BME680_REG_STATUS: - # _BME680_REG_STATUS exists in both SPI memory pages - # For all other registers, we must set the correct memory page - self._set_spi_mem_page(register) - - register = (register | 0x80) & 0xFF # Read single, bit 7 high. - with self._spi as spi: - spi.write(bytearray([register])) - result = bytearray(length) - spi.readinto(result) - if self._debug: - print(f"\t${register:02X} => {[hex(i) for i in result]}") - return result - - def _write(self, register: int, values: ReadableBuffer) -> None: - if register != _BME680_REG_STATUS: - # _BME680_REG_STATUS exists in both SPI memory pages - # For all other registers, we must set the correct memory page - self._set_spi_mem_page(register) - register &= 0x7F # Write, bit 7 low. - with self._spi as spi: - buffer = bytearray(2 * len(values)) - for i, value in enumerate(values): - buffer[2 * i] = register + i - buffer[2 * i + 1] = value & 0xFF - spi.write(buffer) - if self._debug: - print(f"\t${values[0]:02X} <= {[hex(i) for i in values[1:]]}") - - def _set_spi_mem_page(self, register: int) -> None: - spi_mem_page = 0x00 - if register < 0x80: - spi_mem_page = 0x10 - self._write(_BME680_REG_STATUS, [spi_mem_page]) \ No newline at end of file diff --git a/scripts/tempSensor/lib/adafruit_bus_device/__init__.py b/scripts/tempSensor/lib/adafruit_bus_device/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/scripts/tempSensor/lib/adafruit_bus_device/i2c_device.mpy b/scripts/tempSensor/lib/adafruit_bus_device/i2c_device.mpy deleted file mode 100644 index 3382361059a1bf655ad4cbe8edae64f71d23ed29..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1172 zcmZ`&&uJXHdxmAIJ$z;e|~{)v2cHJ0S$6rZ|MU=}35*zD>CV ztw-zqZkIav{FY;Q^u2>qeyAIc&L!FdZ`(4-zXD9&EU)G{EziKPLfZ8i*h)g~ZDWG9 z))CytiR*=O;aW*i2-%|!hna+ALBWJL)V2uOEF&y(nnqZFilCsTj*wg1_B`8NDHb0X z-gaLr=+m?UF!~FU5yE|6`v1N%c?|(@kpqE2N6p8rkOFE39w#})LeFHOt&G3opI z1%mm>@EE5~aOycuy}+rL)uYH*upBgmi{|4j@+OR|Y5pC|&5Mwb*bmt%)TfyEu{`DW zD79G?wS4KEj6G*SdX~OZg~lIW`(ZKfJ)6KV6K6l=!Iz?PqHm4>ty|$Sru%yEBIs;j z?5Vv(=+F+^Z-m9GGwkPfEnCe!%4CdKHAf%zwiGu`os?SxbbEvVt$6KP!j^k1bfu06fnp;(k*X e_(*8rp5L%C{Doykvy)N1%y@iOGNt{C`+ot4ZCDKe diff --git a/scripts/tempSensor/lib/adafruit_bus_device/spi_device.mpy b/scripts/tempSensor/lib/adafruit_bus_device/spi_device.mpy deleted file mode 100644 index 75dd10b264b2c7eeea9cdc49184c2590be08fd7e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 821 zcmYL{-%b-j6vk&;3RqgZ?UwC^{{f`2V2c$R^~O^60*DE1AYhF;>`n)|p>#L9vy~<$ zOsgU!`Vz(mSQYdI#24T~5>33~M(>T6&bAOIIg_(zzM0>*XJ+2#86J6&of-#SIi;j* znzlxuYFn^`YZ}5S%g`>;q*344*vKx7-7%&gl_oHth5l}$?O>5xxDP1LFB?SDt4f9M zUacEA;pq>Mx}jCK27DstMv?M;tEPgms-W$bgzNV+3AP5jt<}zz{9=OD7+%vaYLcvN zX+)_kR+nwU79*wh_l0072-d@3;-}m}i%T4M%m-$Z>Ez96QG~FDO?FNd!Z9H75SrM~ zAzV#!e1vi2*6+=DM6Q51&dQbvF|50(6oZv*HHq|{)MYzTbC-RY*G$CvldqF9J2z)r z?NgF$Q}ebozzx10q?5z1dSCiM7{o+|^@^ackl?03o*zOMR1nc>7}k`Ejn&cpHzPc+ zDt5_K2v%7K4ZWh68mY6~ZQYX8+BS-2vX7P!|XBHnENknDMfEKP`L_RuF$Pv7h6Y~PY{hu=XfNhD%J6ndq;GQArAhWxbt87gT0UA z#`(!H&4uXtS4X%K1@DfS)5iIOhu`LZ?-iK=E*h|_ZO6rYi_YU+1l<>U)lJx~1zH95 EA9D2gNdN!< diff --git a/scripts/tempSensor/lib/adafruit_io-2.8.0.dist-info/METADATA b/scripts/tempSensor/lib/adafruit_io-2.8.0.dist-info/METADATA deleted file mode 100644 index f2813670..00000000 --- a/scripts/tempSensor/lib/adafruit_io-2.8.0.dist-info/METADATA +++ /dev/null @@ -1,96 +0,0 @@ -Metadata-Version: 2.1 -Name: adafruit-io -Version: 2.8.0 -Summary: Python client library for Adafruit IO (http://io.adafruit.com/). -Home-page: https://github.com/adafruit/Adafruit_IO_Python -Author: Adafruit Industries -Author-email: adafruitio@adafruit.com -License: MIT -Keywords: adafruitio io python circuitpython raspberrypi hardware MQTT REST -Classifier: Development Status :: 5 - Production/Stable -Classifier: Operating System :: POSIX :: Linux -Classifier: Operating System :: Microsoft :: Windows -Classifier: Operating System :: MacOS -Classifier: License :: OSI Approved :: MIT License -Classifier: Intended Audience :: Developers -Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3.4 -Classifier: Programming Language :: Python :: 3.5 -Classifier: Programming Language :: Python :: 3.6 -Classifier: Topic :: Home Automation -Classifier: Topic :: Software Development -Description-Content-Type: text/x-rst -License-File: LICENSE.md -Requires-Dist: requests -Requires-Dist: paho-mqtt - -Adafruit IO Python -================== - -.. image:: https://readthedocs.org/projects/adafruit-io-python-client/badge/?version=latest - :target: https://adafruit-io-python-client.readthedocs.io/en/latest/ - :alt: Documentation Status - -.. image:: https://img.shields.io/discord/327254708534116352.svg - :target: https://adafru.it/discord - :alt: Chat - -.. image:: https://github.com/adafruit/Adafruit_IO_Python/workflows/Build-CI/badge.svg - :target: https://github.com/adafruit/Adafruit_IO_Python/actions - :alt: Build Status - -.. image:: https://img.shields.io/badge/Try%20out-Adafruit%20IO%20Python-579ACA.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFkAAABZCAMAAABi1XidAAAB8lBMVEX///9XmsrmZYH1olJXmsr1olJXmsrmZYH1olJXmsr1olJXmsrmZYH1olL1olJXmsr1olJXmsrmZYH1olL1olJXmsrmZYH1olJXmsr1olL1olJXmsrmZYH1olL1olJXmsrmZYH1olL1olL0nFf1olJXmsrmZYH1olJXmsq8dZb1olJXmsrmZYH1olJXmspXmspXmsr1olL1olJXmsrmZYH1olJXmsr1olL1olJXmsrmZYH1olL1olLeaIVXmsrmZYH1olL1olL1olJXmsrmZYH1olLna31Xmsr1olJXmsr1olJXmsrmZYH1olLqoVr1olJXmsr1olJXmsrmZYH1olL1olKkfaPobXvviGabgadXmsqThKuofKHmZ4Dobnr1olJXmsr1olJXmspXmsr1olJXmsrfZ4TuhWn1olL1olJXmsqBi7X1olJXmspZmslbmMhbmsdemsVfl8ZgmsNim8Jpk8F0m7R4m7F5nLB6jbh7jbiDirOEibOGnKaMhq+PnaCVg6qWg6qegKaff6WhnpKofKGtnomxeZy3noG6dZi+n3vCcpPDcpPGn3bLb4/Mb47UbIrVa4rYoGjdaIbeaIXhoWHmZYHobXvpcHjqdHXreHLroVrsfG/uhGnuh2bwj2Hxk17yl1vzmljzm1j0nlX1olL3AJXWAAAAbXRSTlMAEBAQHx8gICAuLjAwMDw9PUBAQEpQUFBXV1hgYGBkcHBwcXl8gICAgoiIkJCQlJicnJ2goKCmqK+wsLC4usDAwMjP0NDQ1NbW3Nzg4ODi5+3v8PDw8/T09PX29vb39/f5+fr7+/z8/Pz9/v7+zczCxgAABC5JREFUeAHN1ul3k0UUBvCb1CTVpmpaitAGSLSpSuKCLWpbTKNJFGlcSMAFF63iUmRccNG6gLbuxkXU66JAUef/9LSpmXnyLr3T5AO/rzl5zj137p136BISy44fKJXuGN/d19PUfYeO67Znqtf2KH33Id1psXoFdW30sPZ1sMvs2D060AHqws4FHeJojLZqnw53cmfvg+XR8mC0OEjuxrXEkX5ydeVJLVIlV0e10PXk5k7dYeHu7Cj1j+49uKg7uLU61tGLw1lq27ugQYlclHC4bgv7VQ+TAyj5Zc/UjsPvs1sd5cWryWObtvWT2EPa4rtnWW3JkpjggEpbOsPr7F7EyNewtpBIslA7p43HCsnwooXTEc3UmPmCNn5lrqTJxy6nRmcavGZVt/3Da2pD5NHvsOHJCrdc1G2r3DITpU7yic7w/7Rxnjc0kt5GC4djiv2Sz3Fb2iEZg41/ddsFDoyuYrIkmFehz0HR2thPgQqMyQYb2OtB0WxsZ3BeG3+wpRb1vzl2UYBog8FfGhttFKjtAclnZYrRo9ryG9uG/FZQU4AEg8ZE9LjGMzTmqKXPLnlWVnIlQQTvxJf8ip7VgjZjyVPrjw1te5otM7RmP7xm+sK2Gv9I8Gi++BRbEkR9EBw8zRUcKxwp73xkaLiqQb+kGduJTNHG72zcW9LoJgqQxpP3/Tj//c3yB0tqzaml05/+orHLksVO+95kX7/7qgJvnjlrfr2Ggsyx0eoy9uPzN5SPd86aXggOsEKW2Prz7du3VID3/tzs/sSRs2w7ovVHKtjrX2pd7ZMlTxAYfBAL9jiDwfLkq55Tm7ifhMlTGPyCAs7RFRhn47JnlcB9RM5T97ASuZXIcVNuUDIndpDbdsfrqsOppeXl5Y+XVKdjFCTh+zGaVuj0d9zy05PPK3QzBamxdwtTCrzyg/2Rvf2EstUjordGwa/kx9mSJLr8mLLtCW8HHGJc2R5hS219IiF6PnTusOqcMl57gm0Z8kanKMAQg0qSyuZfn7zItsbGyO9QlnxY0eCuD1XL2ys/MsrQhltE7Ug0uFOzufJFE2PxBo/YAx8XPPdDwWN0MrDRYIZF0mSMKCNHgaIVFoBbNoLJ7tEQDKxGF0kcLQimojCZopv0OkNOyWCCg9XMVAi7ARJzQdM2QUh0gmBozjc3Skg6dSBRqDGYSUOu66Zg+I2fNZs/M3/f/Grl/XnyF1Gw3VKCez0PN5IUfFLqvgUN4C0qNqYs5YhPL+aVZYDE4IpUk57oSFnJm4FyCqqOE0jhY2SMyLFoo56zyo6becOS5UVDdj7Vih0zp+tcMhwRpBeLyqtIjlJKAIZSbI8SGSF3k0pA3mR5tHuwPFoa7N7reoq2bqCsAk1HqCu5uvI1n6JuRXI+S1Mco54YmYTwcn6Aeic+kssXi8XpXC4V3t7/ADuTNKaQJdScAAAAAElFTkSuQmCC - :target: https://mybinder.org/v2/gh/adafruit/adafruit_io_python_jupyter/master?filepath=adafruit-io-python-tutorial.ipynb - -.. image:: https://cdn-learn.adafruit.com/assets/assets/000/057/153/original/adafruit_io_iopython.png?1530802073 - -A Python library and examples for use with `io.adafruit.com `_. - -Compatible with Python Versions 3.6+ - -Installation -================ - -Easy Installation -~~~~~~~~~~~~~~~~~ -If you have `PIP `_ installed (typically with apt-get install python-pip on a Debian/Ubuntu-based system) then run: - -.. code-block:: shell - - pip3 install adafruit-io - -This will automatically install the Adafruit IO Python client code for your Python scripts to use. You might want to examine the examples folder in this GitHub repository to see examples of usage. - -If the above command fails, you may first need to install prerequisites: - -.. code-block:: shell - - pip3 install setuptools - pip3 install wheel - - -Manual Installation -~~~~~~~~~~~~~~~~~~~ - -Clone or download the contents of this repository. Then navigate to the folder in a terminal and run the following command: - - -.. code-block:: shell - - python setup.py install - - - - -Usage -===== - -Documentation for this project is `available on the ReadTheDocs `_. - - -Contributing -============ - -Contributions are welcome! Please read our `Code of Conduct -`_ -before contributing to help this project stay welcoming. diff --git a/scripts/tempSensor/lib/adafruit_io-2.8.0.dist-info/RECORD b/scripts/tempSensor/lib/adafruit_io-2.8.0.dist-info/RECORD deleted file mode 100644 index 8e831b4a..00000000 --- a/scripts/tempSensor/lib/adafruit_io-2.8.0.dist-info/RECORD +++ /dev/null @@ -1,9 +0,0 @@ -Adafruit_IO/__init__.py,, -Adafruit_IO/_version.py,, -Adafruit_IO/client.py,, -Adafruit_IO/errors.py,, -Adafruit_IO/model.py,, -Adafruit_IO/mqtt_client.py,, -adafruit_io-2.8.0.dist-info/METADATA,, -ez_setup.py,, -adafruit_io-2.8.0.dist-info/RECORD,, \ No newline at end of file diff --git a/scripts/tempSensor/lib/bme680-2.0.0.dist-info/METADATA b/scripts/tempSensor/lib/bme680-2.0.0.dist-info/METADATA deleted file mode 100644 index 52ebdfd8..00000000 --- a/scripts/tempSensor/lib/bme680-2.0.0.dist-info/METADATA +++ /dev/null @@ -1,156 +0,0 @@ -Metadata-Version: 2.3 -Name: bme680 -Version: 2.0.0 -Summary: Python library for the BME680 temperature, humidity and gas sensor -Project-URL: GitHub, https://www.github.com/pimoroni/bme680-python -Project-URL: Homepage, https://www.pimoroni.com -Author-email: Philip Howard -Maintainer-email: Philip Howard -License: MIT License - - Copyright (c) 2018 Pimoroni Ltd - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. -License-File: LICENSE -Keywords: Pi,Raspberry -Classifier: Development Status :: 4 - Beta -Classifier: Intended Audience :: Developers -Classifier: License :: OSI Approved :: MIT License -Classifier: Operating System :: POSIX :: Linux -Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3 :: Only -Classifier: Programming Language :: Python :: 3.7 -Classifier: Programming Language :: Python :: 3.8 -Classifier: Programming Language :: Python :: 3.9 -Classifier: Programming Language :: Python :: 3.10 -Classifier: Programming Language :: Python :: 3.11 -Classifier: Topic :: Software Development -Classifier: Topic :: Software Development :: Libraries -Classifier: Topic :: System :: Hardware -Requires-Python: >=3.7 -Requires-Dist: smbus2 -Description-Content-Type: text/markdown - -# BME680 - -[![Build Status](https://img.shields.io/github/actions/workflow/status/pimoroni/bme680-python/test.yml?branch=main)](https://github.com/pimoroni/bme680-python/actions/workflows/test.yml) -[![Coverage Status](https://coveralls.io/repos/github/pimoroni/bme680-python/badge.svg?branch=main)](https://coveralls.io/github/pimoroni/bme680-python?branch=main) -[![PyPi Package](https://img.shields.io/pypi/v/bme680.svg)](https://pypi.python.org/pypi/bme680) -[![Python Versions](https://img.shields.io/pypi/pyversions/bme680.svg)](https://pypi.python.org/pypi/bme680) - -https://shop.pimoroni.com/products/bme680 - -The state-of-the-art BME680 breakout lets you measure temperature, pressure, humidity, and indoor air quality. - -## Installing - -### Full install (recommended): - -We've created an easy installation script that will install all pre-requisites and get your BME680 -up and running with minimal efforts. To run it, fire up Terminal which you'll find in Menu -> Accessories -> Terminal -on your Raspberry Pi desktop, as illustrated below: - -![Finding the terminal](http://get.pimoroni.com/resources/github-repo-terminal.png) - -In the new terminal window type the command exactly as it appears below (check for typos) and follow the on-screen instructions: - -```bash -git clone https://github.com/pimoroni/bme680-python -cd bme680-python -./install.sh -``` - -**Note** Libraries will be installed in the "pimoroni" virtual environment, you will need to activate it to run examples: - -``` -source ~/.virtualenvs/pimoroni/bin/activate -``` - -### Development: - -If you want to contribute, or like living on the edge of your seat by having the latest code, you can install the development version like so: - -```bash -git clone https://github.com/pimoroni/bme680-python -cd bme680-python -./install.sh --unstable -``` - -In all cases you will have to enable the i2c bus: - -``` -sudo raspi-config nonint do_i2c 0 -``` - -## Documentation & Support - -* Guides and tutorials - https://learn.pimoroni.com/bme680-breakout -* Get help - http://forums.pimoroni.com/c/support - - -2.0.0 ------ - -* Repackage to hatch/pyproject.toml -* Drop Python 2.7 support -* Switch from smbu2 to smbus2 - -1.1.1 ------ - -* New: constants to clarify heater on/off states - -1.1.0 ------ - -* New: support for BME688 "high" gas resistance variant -* New: set/get gas heater disable bit -* Enhancement: fail with descriptive RuntimeError when chip is not detected - -1.0.5 ------ - -* New: set_temp_offset to calibrate temperature offset in degrees C - -1.0.4 ------ - -* Fix to range_sw_err for extremely high gas readings -* Convert to unsigned int to fix negative gas readings - -1.0.3 ------ - -* Merged temperature compensation fix from Bosch's BME680_driver 3.5.3 - -1.0.2 ------ - -* Fixed set_gas_heater_temperature to avoid i2c TypeError - -1.0.1 ------ - -* Added Manifest to Python package - -1.0.0 ------ - -* Initial release - diff --git a/scripts/tempSensor/lib/bme680-2.0.0.dist-info/RECORD b/scripts/tempSensor/lib/bme680-2.0.0.dist-info/RECORD deleted file mode 100644 index 35d5cc6b..00000000 --- a/scripts/tempSensor/lib/bme680-2.0.0.dist-info/RECORD +++ /dev/null @@ -1,7 +0,0 @@ -CHANGELOG.md,, -LICENSE,, -README.md,, -bme680-2.0.0.dist-info/METADATA,, -bme680/__init__.py,, -bme680/constants.py,, -bme680-2.0.0.dist-info/RECORD,, \ No newline at end of file diff --git a/scripts/tempSensor/lib/bme680/__init__.py b/scripts/tempSensor/lib/bme680/__init__.py deleted file mode 100644 index 56d547a1..00000000 --- a/scripts/tempSensor/lib/bme680/__init__.py +++ /dev/null @@ -1,486 +0,0 @@ -"""BME680 Temperature, Pressure, Humidity & Gas Sensor.""" -import math -import time - -from . import constants -from .constants import BME680Data, lookupTable1, lookupTable2 - -__version__ = '2.0.0' - - -# Export constants to global namespace -# so end-users can "from BME680 import NAME" -if hasattr(constants, '__dict__'): - for key in constants.__dict__: - value = constants.__dict__[key] - if key not in globals(): - globals()[key] = value - - -class BME680(BME680Data): - """BOSCH BME680. - - Gas, pressure, temperature and humidity sensor. - - :param i2c_addr: One of I2C_ADDR_PRIMARY (0x76) or I2C_ADDR_SECONDARY (0x77) - :param i2c_device: Optional smbus or compatible instance for facilitating i2c communications. - - """ - - def __init__(self, i2c_addr=constants.I2C_ADDR_PRIMARY, i2c_device=None): - """Initialise BME680 sensor instance and verify device presence. - - :param i2c_addr: i2c address of BME680 - :param i2c_device: Optional SMBus-compatible instance for i2c transport - - """ - BME680Data.__init__(self) - - self.i2c_addr = i2c_addr - self._i2c = i2c_device - if self._i2c is None: - import smbus2 - self._i2c = smbus2.SMBus(1) - - try: - self.chip_id = self._get_regs(constants.CHIP_ID_ADDR, 1) - if self.chip_id != constants.CHIP_ID: - raise RuntimeError('BME680 Not Found. Invalid CHIP ID: 0x{0:02x}'.format(self.chip_id)) - except IOError: - raise RuntimeError("Unable to identify BME680 at 0x{:02x} (IOError)".format(self.i2c_addr)) - - self._variant = self._get_regs(constants.CHIP_VARIANT_ADDR, 1) - - self.soft_reset() - self.set_power_mode(constants.SLEEP_MODE) - - self._get_calibration_data() - - self.set_humidity_oversample(constants.OS_2X) - self.set_pressure_oversample(constants.OS_4X) - self.set_temperature_oversample(constants.OS_8X) - self.set_filter(constants.FILTER_SIZE_3) - if self._variant == constants.VARIANT_HIGH: - self.set_gas_status(constants.ENABLE_GAS_MEAS_HIGH) - else: - self.set_gas_status(constants.ENABLE_GAS_MEAS_LOW) - self.set_temp_offset(0) - self.get_sensor_data() - - def _get_calibration_data(self): - """Retrieve the sensor calibration data and store it in .calibration_data.""" - calibration = self._get_regs(constants.COEFF_ADDR1, constants.COEFF_ADDR1_LEN) - calibration += self._get_regs(constants.COEFF_ADDR2, constants.COEFF_ADDR2_LEN) - - heat_range = self._get_regs(constants.ADDR_RES_HEAT_RANGE_ADDR, 1) - heat_value = constants.twos_comp(self._get_regs(constants.ADDR_RES_HEAT_VAL_ADDR, 1), bits=8) - sw_error = constants.twos_comp(self._get_regs(constants.ADDR_RANGE_SW_ERR_ADDR, 1), bits=8) - - self.calibration_data.set_from_array(calibration) - self.calibration_data.set_other(heat_range, heat_value, sw_error) - - def soft_reset(self): - """Trigger a soft reset.""" - self._set_regs(constants.SOFT_RESET_ADDR, constants.SOFT_RESET_CMD) - time.sleep(constants.RESET_PERIOD / 1000.0) - - def set_temp_offset(self, value): - """Set temperature offset in celsius. - - If set, the temperature t_fine will be increased by given value in celsius. - :param value: Temperature offset in Celsius, eg. 4, -8, 1.25 - - """ - if value == 0: - self.offset_temp_in_t_fine = 0 - else: - self.offset_temp_in_t_fine = int(math.copysign((((int(abs(value) * 100)) << 8) - 128) / 5, value)) - - def set_humidity_oversample(self, value): - """Set humidity oversampling. - - A higher oversampling value means more stable sensor readings, - with less noise and jitter. - - However each step of oversampling adds about 2ms to the latency, - causing a slower response time to fast transients. - - :param value: Oversampling value, one of: OS_NONE, OS_1X, OS_2X, OS_4X, OS_8X, OS_16X - - """ - self.tph_settings.os_hum = value - self._set_bits(constants.CONF_OS_H_ADDR, constants.OSH_MSK, constants.OSH_POS, value) - - def get_humidity_oversample(self): - """Get humidity oversampling.""" - return (self._get_regs(constants.CONF_OS_H_ADDR, 1) & constants.OSH_MSK) >> constants.OSH_POS - - def set_pressure_oversample(self, value): - """Set temperature oversampling. - - A higher oversampling value means more stable sensor readings, - with less noise and jitter. - - However each step of oversampling adds about 2ms to the latency, - causing a slower response time to fast transients. - - :param value: Oversampling value, one of: OS_NONE, OS_1X, OS_2X, OS_4X, OS_8X, OS_16X - - """ - self.tph_settings.os_pres = value - self._set_bits(constants.CONF_T_P_MODE_ADDR, constants.OSP_MSK, constants.OSP_POS, value) - - def get_pressure_oversample(self): - """Get pressure oversampling.""" - return (self._get_regs(constants.CONF_T_P_MODE_ADDR, 1) & constants.OSP_MSK) >> constants.OSP_POS - - def set_temperature_oversample(self, value): - """Set pressure oversampling. - - A higher oversampling value means more stable sensor readings, - with less noise and jitter. - - However each step of oversampling adds about 2ms to the latency, - causing a slower response time to fast transients. - - :param value: Oversampling value, one of: OS_NONE, OS_1X, OS_2X, OS_4X, OS_8X, OS_16X - - """ - self.tph_settings.os_temp = value - self._set_bits(constants.CONF_T_P_MODE_ADDR, constants.OST_MSK, constants.OST_POS, value) - - def get_temperature_oversample(self): - """Get temperature oversampling.""" - return (self._get_regs(constants.CONF_T_P_MODE_ADDR, 1) & constants.OST_MSK) >> constants.OST_POS - - def set_filter(self, value): - """Set IIR filter size. - - Optionally remove short term fluctuations from the temperature and pressure readings, - increasing their resolution but reducing their bandwidth. - - Enabling the IIR filter does not slow down the time a reading takes, but will slow - down the BME680s response to changes in temperature and pressure. - - When the IIR filter is enabled, the temperature and pressure resolution is effectively 20bit. - When it is disabled, it is 16bit + oversampling-1 bits. - - """ - self.tph_settings.filter = value - self._set_bits(constants.CONF_ODR_FILT_ADDR, constants.FILTER_MSK, constants.FILTER_POS, value) - - def get_filter(self): - """Get filter size.""" - return (self._get_regs(constants.CONF_ODR_FILT_ADDR, 1) & constants.FILTER_MSK) >> constants.FILTER_POS - - def select_gas_heater_profile(self, value): - """Set current gas sensor conversion profile. - - Select one of the 10 configured heating durations/set points. - - :param value: Profile index from 0 to 9 - - """ - if value > constants.NBCONV_MAX or value < constants.NBCONV_MIN: - raise ValueError("Profile '{}' should be between {} and {}".format(value, constants.NBCONV_MIN, constants.NBCONV_MAX)) - - self.gas_settings.nb_conv = value - self._set_bits(constants.CONF_ODR_RUN_GAS_NBC_ADDR, constants.NBCONV_MSK, constants.NBCONV_POS, value) - - def get_gas_heater_profile(self): - """Get gas sensor conversion profile: 0 to 9.""" - return self._get_regs(constants.CONF_ODR_RUN_GAS_NBC_ADDR, 1) & constants.NBCONV_MSK - - def set_gas_heater_status(self, value): - """Enable/disable gas heater.""" - self.gas_settings.heater = value - self._set_bits(constants.CONF_HEAT_CTRL_ADDR, constants.HCTRL_MSK, constants.HCTRL_POS, value) - - def get_gas_heater_status(self): - """Get current heater status.""" - return (self._get_regs(constants.CONF_HEAT_CTRL_ADDR, 1) & constants.HCTRL_MSK) >> constants.HCTRL_POS - - def set_gas_status(self, value): - """Enable/disable gas sensor.""" - if value == -1: - if self._variant == constants.VARIANT_HIGH: - value = constants.ENABLE_GAS_MEAS_HIGH - else: - value = constants.ENABLE_GAS_MEAS_LOW - self.gas_settings.run_gas = value - self._set_bits(constants.CONF_ODR_RUN_GAS_NBC_ADDR, constants.RUN_GAS_MSK, constants.RUN_GAS_POS, value) - - def get_gas_status(self): - """Get the current gas status.""" - return (self._get_regs(constants.CONF_ODR_RUN_GAS_NBC_ADDR, 1) & constants.RUN_GAS_MSK) >> constants.RUN_GAS_POS - - def set_gas_heater_profile(self, temperature, duration, nb_profile=0): - """Set temperature and duration of gas sensor heater. - - :param temperature: Target temperature in degrees celsius, between 200 and 400 - :param durarion: Target duration in milliseconds, between 1 and 4032 - :param nb_profile: Target profile, between 0 and 9 - - """ - self.set_gas_heater_temperature(temperature, nb_profile=nb_profile) - self.set_gas_heater_duration(duration, nb_profile=nb_profile) - - def set_gas_heater_temperature(self, value, nb_profile=0): - """Set gas sensor heater temperature. - - :param value: Target temperature in degrees celsius, between 200 and 400 - - When setting an nb_profile other than 0, - make sure to select it with select_gas_heater_profile. - - """ - if nb_profile > constants.NBCONV_MAX or value < constants.NBCONV_MIN: - raise ValueError('Profile "{}" should be between {} and {}'.format(nb_profile, constants.NBCONV_MIN, constants.NBCONV_MAX)) - - self.gas_settings.heatr_temp = value - temp = int(self._calc_heater_resistance(self.gas_settings.heatr_temp)) - self._set_regs(constants.RES_HEAT0_ADDR + nb_profile, temp) - - def set_gas_heater_duration(self, value, nb_profile=0): - """Set gas sensor heater duration. - - Heating durations between 1 ms and 4032 ms can be configured. - Approximately 20-30 ms are necessary for the heater to reach the intended target temperature. - - :param value: Heating duration in milliseconds. - - When setting an nb_profile other than 0, - make sure to select it with select_gas_heater_profile. - - """ - if nb_profile > constants.NBCONV_MAX or value < constants.NBCONV_MIN: - raise ValueError('Profile "{}" should be between {} and {}'.format(nb_profile, constants.NBCONV_MIN, constants.NBCONV_MAX)) - - self.gas_settings.heatr_dur = value - temp = self._calc_heater_duration(self.gas_settings.heatr_dur) - self._set_regs(constants.GAS_WAIT0_ADDR + nb_profile, temp) - - def set_power_mode(self, value, blocking=True): - """Set power mode.""" - if value not in (constants.SLEEP_MODE, constants.FORCED_MODE): - raise ValueError('Power mode should be one of SLEEP_MODE or FORCED_MODE') - - self.power_mode = value - - self._set_bits(constants.CONF_T_P_MODE_ADDR, constants.MODE_MSK, constants.MODE_POS, value) - - while blocking and self.get_power_mode() != self.power_mode: - time.sleep(constants.POLL_PERIOD_MS / 1000.0) - - def get_power_mode(self): - """Get power mode.""" - self.power_mode = self._get_regs(constants.CONF_T_P_MODE_ADDR, 1) - return self.power_mode - - def get_sensor_data(self): - """Get sensor data. - - Stores data in .data and returns True upon success. - - """ - self.set_power_mode(constants.FORCED_MODE) - - for attempt in range(10): - status = self._get_regs(constants.FIELD0_ADDR, 1) - - if (status & constants.NEW_DATA_MSK) == 0: - time.sleep(constants.POLL_PERIOD_MS / 1000.0) - continue - - regs = self._get_regs(constants.FIELD0_ADDR, constants.FIELD_LENGTH) - - self.data.status = regs[0] & constants.NEW_DATA_MSK - # Contains the nb_profile used to obtain the current measurement - self.data.gas_index = regs[0] & constants.GAS_INDEX_MSK - self.data.meas_index = regs[1] - - adc_pres = (regs[2] << 12) | (regs[3] << 4) | (regs[4] >> 4) - adc_temp = (regs[5] << 12) | (regs[6] << 4) | (regs[7] >> 4) - adc_hum = (regs[8] << 8) | regs[9] - adc_gas_res_low = (regs[13] << 2) | (regs[14] >> 6) - adc_gas_res_high = (regs[15] << 2) | (regs[16] >> 6) - gas_range_l = regs[14] & constants.GAS_RANGE_MSK - gas_range_h = regs[16] & constants.GAS_RANGE_MSK - - if self._variant == constants.VARIANT_HIGH: - self.data.status |= regs[16] & constants.GASM_VALID_MSK - self.data.status |= regs[16] & constants.HEAT_STAB_MSK - else: - self.data.status |= regs[14] & constants.GASM_VALID_MSK - self.data.status |= regs[14] & constants.HEAT_STAB_MSK - - self.data.heat_stable = (self.data.status & constants.HEAT_STAB_MSK) > 0 - - temperature = self._calc_temperature(adc_temp) - self.data.temperature = temperature / 100.0 - self.ambient_temperature = temperature # Saved for heater calc - - self.data.pressure = self._calc_pressure(adc_pres) / 100.0 - self.data.humidity = self._calc_humidity(adc_hum) / 1000.0 - - if self._variant == constants.VARIANT_HIGH: - self.data.gas_resistance = self._calc_gas_resistance_high(adc_gas_res_high, gas_range_h) - else: - self.data.gas_resistance = self._calc_gas_resistance_low(adc_gas_res_low, gas_range_l) - - return True - - return False - - def _set_bits(self, register, mask, position, value): - """Mask out and set one or more bits in a register.""" - temp = self._get_regs(register, 1) - temp &= ~mask - temp |= value << position - self._set_regs(register, temp) - - def _set_regs(self, register, value): - """Set one or more registers.""" - if isinstance(value, int): - self._i2c.write_byte_data(self.i2c_addr, register, value) - else: - self._i2c.write_i2c_block_data(self.i2c_addr, register, value) - - def _get_regs(self, register, length): - """Get one or more registers.""" - if length == 1: - return self._i2c.read_byte_data(self.i2c_addr, register) - else: - return self._i2c.read_i2c_block_data(self.i2c_addr, register, length) - - def _calc_temperature(self, temperature_adc): - """Convert the raw temperature to degrees C using calibration_data.""" - var1 = (temperature_adc >> 3) - (self.calibration_data.par_t1 << 1) - var2 = (var1 * self.calibration_data.par_t2) >> 11 - var3 = ((var1 >> 1) * (var1 >> 1)) >> 12 - var3 = ((var3) * (self.calibration_data.par_t3 << 4)) >> 14 - - # Save teperature data for pressure calculations - self.calibration_data.t_fine = (var2 + var3) + self.offset_temp_in_t_fine - calc_temp = (((self.calibration_data.t_fine * 5) + 128) >> 8) - - return calc_temp - - def _calc_pressure(self, pressure_adc): - """Convert the raw pressure using calibration data.""" - var1 = ((self.calibration_data.t_fine) >> 1) - 64000 - var2 = ((((var1 >> 2) * (var1 >> 2)) >> 11) * - self.calibration_data.par_p6) >> 2 - var2 = var2 + ((var1 * self.calibration_data.par_p5) << 1) - var2 = (var2 >> 2) + (self.calibration_data.par_p4 << 16) - var1 = (((((var1 >> 2) * (var1 >> 2)) >> 13) * - ((self.calibration_data.par_p3 << 5)) >> 3) + - ((self.calibration_data.par_p2 * var1) >> 1)) - var1 = var1 >> 18 - - var1 = ((32768 + var1) * self.calibration_data.par_p1) >> 15 - calc_pressure = 1048576 - pressure_adc - calc_pressure = ((calc_pressure - (var2 >> 12)) * (3125)) - - if calc_pressure >= (1 << 31): - calc_pressure = ((calc_pressure // var1) << 1) - else: - calc_pressure = ((calc_pressure << 1) // var1) - - var1 = (self.calibration_data.par_p9 * (((calc_pressure >> 3) * - (calc_pressure >> 3)) >> 13)) >> 12 - var2 = ((calc_pressure >> 2) * - self.calibration_data.par_p8) >> 13 - var3 = ((calc_pressure >> 8) * (calc_pressure >> 8) * - (calc_pressure >> 8) * - self.calibration_data.par_p10) >> 17 - - calc_pressure = (calc_pressure) + ((var1 + var2 + var3 + - (self.calibration_data.par_p7 << 7)) >> 4) - - return calc_pressure - - def _calc_humidity(self, humidity_adc): - """Convert the raw humidity using calibration data.""" - temp_scaled = ((self.calibration_data.t_fine * 5) + 128) >> 8 - var1 = (humidity_adc - ((self.calibration_data.par_h1 * 16))) -\ - (((temp_scaled * self.calibration_data.par_h3) // (100)) >> 1) - var2 = (self.calibration_data.par_h2 * - (((temp_scaled * self.calibration_data.par_h4) // (100)) + - (((temp_scaled * ((temp_scaled * self.calibration_data.par_h5) // (100))) >> 6) // - (100)) + (1 * 16384))) >> 10 - var3 = var1 * var2 - var4 = self.calibration_data.par_h6 << 7 - var4 = ((var4) + ((temp_scaled * self.calibration_data.par_h7) // (100))) >> 4 - var5 = ((var3 >> 14) * (var3 >> 14)) >> 10 - var6 = (var4 * var5) >> 1 - calc_hum = (((var3 + var6) >> 10) * (1000)) >> 12 - - return min(max(calc_hum, 0), 100000) - - def _calc_gas_resistance(self, gas_res_adc, gas_range): - """Convert the raw gas resistance using calibration data.""" - if self._variant == constants.VARIANT_HIGH: - return self._calc_gas_resistance_high(gas_res_adc, gas_range) - else: - return self._calc_gas_resistance_low(gas_res_adc, gas_range) - - def _calc_gas_resistance_high(self, gas_res_adc, gas_range): - """Convert the raw gas resistance using calibration data. - - Applies to Variant ID == 0x01 only. - - """ - var1 = 262144 >> gas_range - var2 = gas_res_adc - 512 - - var2 *= 3 - var2 = 4096 + var2 - - calc_gas_res = (10000 * var1) / var2 - calc_gas_res *= 100 - - return calc_gas_res - - def _calc_gas_resistance_low(self, gas_res_adc, gas_range): - """Convert the raw gas resistance using calibration data. - - Applies to Variant ID == 0x00 only. - - """ - var1 = ((1340 + (5 * self.calibration_data.range_sw_err)) * (lookupTable1[gas_range])) >> 16 - var2 = (((gas_res_adc << 15) - (16777216)) + var1) - var3 = ((lookupTable2[gas_range] * var1) >> 9) - calc_gas_res = ((var3 + (var2 >> 1)) / var2) - - if calc_gas_res < 0: - calc_gas_res = (1 << 32) + calc_gas_res - - return calc_gas_res - - def _calc_heater_resistance(self, temperature): - """Convert raw heater resistance using calibration data.""" - temperature = min(max(temperature, 200), 400) - - var1 = ((self.ambient_temperature * self.calibration_data.par_gh3) / 1000) * 256 - var2 = (self.calibration_data.par_gh1 + 784) * (((((self.calibration_data.par_gh2 + 154009) * temperature * 5) / 100) + 3276800) / 10) - var3 = var1 + (var2 / 2) - var4 = (var3 / (self.calibration_data.res_heat_range + 4)) - var5 = (131 * self.calibration_data.res_heat_val) + 65536 - heatr_res_x100 = (((var4 / var5) - 250) * 34) - heatr_res = ((heatr_res_x100 + 50) / 100) - - return heatr_res - - def _calc_heater_duration(self, duration): - """Calculate correct value for heater duration setting from milliseconds.""" - if duration < 0xfc0: - factor = 0 - - while duration > 0x3f: - duration /= 4 - factor += 1 - - return int(duration + (factor * 64)) - - return 0xff diff --git a/scripts/tempSensor/lib/bme680/constants.py b/scripts/tempSensor/lib/bme680/constants.py deleted file mode 100644 index d77415d3..00000000 --- a/scripts/tempSensor/lib/bme680/constants.py +++ /dev/null @@ -1,413 +0,0 @@ -"""BME680 constants, structures and utilities.""" - -# BME680 General config -POLL_PERIOD_MS = 10 - -# BME680 I2C addresses -I2C_ADDR_PRIMARY = 0x76 -I2C_ADDR_SECONDARY = 0x77 - -# BME680 unique chip identifier -CHIP_ID = 0x61 - -# BME680 coefficients related defines -COEFF_SIZE = 41 -COEFF_ADDR1_LEN = 25 -COEFF_ADDR2_LEN = 16 - -# BME680 field_x related defines -FIELD_LENGTH = 17 -FIELD_ADDR_OFFSET = 17 - -# Soft reset command -SOFT_RESET_CMD = 0xb6 - -# Error code definitions -OK = 0 -# Errors -E_NULL_PTR = -1 -E_COM_FAIL = -2 -E_DEV_NOT_FOUND = -3 -E_INVALID_LENGTH = -4 - -# Warnings -W_DEFINE_PWR_MODE = 1 -W_NO_NEW_DATA = 2 - -# Info's -I_MIN_CORRECTION = 1 -I_MAX_CORRECTION = 2 - -# Register map -# Other coefficient's address -ADDR_RES_HEAT_VAL_ADDR = 0x00 -ADDR_RES_HEAT_RANGE_ADDR = 0x02 -ADDR_RANGE_SW_ERR_ADDR = 0x04 -ADDR_SENS_CONF_START = 0x5A -ADDR_GAS_CONF_START = 0x64 - -# Field settings -FIELD0_ADDR = 0x1d - -# Heater settings -RES_HEAT0_ADDR = 0x5a -GAS_WAIT0_ADDR = 0x64 - -# Sensor configuration registers -CONF_HEAT_CTRL_ADDR = 0x70 -CONF_ODR_RUN_GAS_NBC_ADDR = 0x71 -CONF_OS_H_ADDR = 0x72 -MEM_PAGE_ADDR = 0xf3 -CONF_T_P_MODE_ADDR = 0x74 -CONF_ODR_FILT_ADDR = 0x75 - -# Coefficient's address -COEFF_ADDR1 = 0x89 -COEFF_ADDR2 = 0xe1 - -# Chip identifier -CHIP_ID_ADDR = 0xd0 -CHIP_VARIANT_ADDR = 0xf0 - -VARIANT_LOW = 0x00 -VARIANT_HIGH = 0x01 - -# Soft reset register -SOFT_RESET_ADDR = 0xe0 - -# Heater control settings -ENABLE_HEATER = 0x00 -DISABLE_HEATER = 0x08 - -# Gas measurement settings -DISABLE_GAS_MEAS = 0x00 -ENABLE_GAS_MEAS = -1 # Now used as auto-select -ENABLE_GAS_MEAS_LOW = 0x01 -ENABLE_GAS_MEAS_HIGH = 0x02 - -# Over-sampling settings -OS_NONE = 0 -OS_1X = 1 -OS_2X = 2 -OS_4X = 3 -OS_8X = 4 -OS_16X = 5 - -# IIR filter settings -FILTER_SIZE_0 = 0 -FILTER_SIZE_1 = 1 -FILTER_SIZE_3 = 2 -FILTER_SIZE_7 = 3 -FILTER_SIZE_15 = 4 -FILTER_SIZE_31 = 5 -FILTER_SIZE_63 = 6 -FILTER_SIZE_127 = 7 - -# Power mode settings -SLEEP_MODE = 0 -FORCED_MODE = 1 - -# Delay related macro declaration -RESET_PERIOD = 10 - -# SPI memory page settings -MEM_PAGE0 = 0x10 -MEM_PAGE1 = 0x00 - -# Ambient humidity shift value for compensation -HUM_REG_SHIFT_VAL = 4 - -# Run gas enable and disable settings -RUN_GAS_DISABLE = 0 -RUN_GAS_ENABLE = 1 - -# Gas heater enable and disable settings -GAS_HEAT_ENABLE = 0 -GAS_HEAT_DISABLE = 1 - -# Buffer length macro declaration -TMP_BUFFER_LENGTH = 40 -REG_BUFFER_LENGTH = 6 -FIELD_DATA_LENGTH = 3 -GAS_REG_BUF_LENGTH = 20 -GAS_HEATER_PROF_LEN_MAX = 10 - -# Settings selector -OST_SEL = 1 -OSP_SEL = 2 -OSH_SEL = 4 -GAS_MEAS_SEL = 8 -FILTER_SEL = 16 -HCNTRL_SEL = 32 -RUN_GAS_SEL = 64 -NBCONV_SEL = 128 -GAS_SENSOR_SEL = GAS_MEAS_SEL | RUN_GAS_SEL | NBCONV_SEL - -# Number of conversion settings -NBCONV_MIN = 0 -NBCONV_MAX = 9 # Was 10, but there are only 10 settings: 0 1 2 ... 8 9 - -# Mask definitions -GAS_MEAS_MSK = 0x30 -NBCONV_MSK = 0X0F -FILTER_MSK = 0X1C -OST_MSK = 0XE0 -OSP_MSK = 0X1C -OSH_MSK = 0X07 -HCTRL_MSK = 0x08 -RUN_GAS_MSK = 0x30 -MODE_MSK = 0x03 -RHRANGE_MSK = 0x30 -RSERROR_MSK = 0xf0 -NEW_DATA_MSK = 0x80 -GAS_INDEX_MSK = 0x0f -GAS_RANGE_MSK = 0x0f -GASM_VALID_MSK = 0x20 -HEAT_STAB_MSK = 0x10 -MEM_PAGE_MSK = 0x10 -SPI_RD_MSK = 0x80 -SPI_WR_MSK = 0x7f -BIT_H1_DATA_MSK = 0x0F - -# Bit position definitions for sensor settings -GAS_MEAS_POS = 4 -FILTER_POS = 2 -OST_POS = 5 -OSP_POS = 2 -OSH_POS = 0 -HCTRL_POS = 3 -RUN_GAS_POS = 4 -MODE_POS = 0 -NBCONV_POS = 0 - -# Array Index to Field data mapping for Calibration Data -T2_LSB_REG = 1 -T2_MSB_REG = 2 -T3_REG = 3 -P1_LSB_REG = 5 -P1_MSB_REG = 6 -P2_LSB_REG = 7 -P2_MSB_REG = 8 -P3_REG = 9 -P4_LSB_REG = 11 -P4_MSB_REG = 12 -P5_LSB_REG = 13 -P5_MSB_REG = 14 -P7_REG = 15 -P6_REG = 16 -P8_LSB_REG = 19 -P8_MSB_REG = 20 -P9_LSB_REG = 21 -P9_MSB_REG = 22 -P10_REG = 23 -H2_MSB_REG = 25 -H2_LSB_REG = 26 -H1_LSB_REG = 26 -H1_MSB_REG = 27 -H3_REG = 28 -H4_REG = 29 -H5_REG = 30 -H6_REG = 31 -H7_REG = 32 -T1_LSB_REG = 33 -T1_MSB_REG = 34 -GH2_LSB_REG = 35 -GH2_MSB_REG = 36 -GH1_REG = 37 -GH3_REG = 38 - -# BME680 register buffer index settings -REG_FILTER_INDEX = 5 -REG_TEMP_INDEX = 4 -REG_PRES_INDEX = 4 -REG_HUM_INDEX = 2 -REG_NBCONV_INDEX = 1 -REG_RUN_GAS_INDEX = 1 -REG_HCTRL_INDEX = 0 - -# Look up tables for the possible gas range values -lookupTable1 = [2147483647, 2147483647, 2147483647, 2147483647, - 2147483647, 2126008810, 2147483647, 2130303777, 2147483647, - 2147483647, 2143188679, 2136746228, 2147483647, 2126008810, - 2147483647, 2147483647] - -lookupTable2 = [4096000000, 2048000000, 1024000000, 512000000, - 255744255, 127110228, 64000000, 32258064, - 16016016, 8000000, 4000000, 2000000, - 1000000, 500000, 250000, 125000] - - -def bytes_to_word(msb, lsb, bits=16, signed=False): - """Convert a most and least significant byte into a word.""" - # TODO: Reimplement with struct - word = (msb << 8) | lsb - if signed: - word = twos_comp(word, bits) - return word - - -def twos_comp(val, bits=16): - """Convert two bytes into a two's compliment signed word.""" - # TODO: Reimplement with struct - if val & (1 << (bits - 1)) != 0: - val = val - (1 << bits) - return val - - -class FieldData: - """Structure for storing BME680 sensor data.""" - - def __init__(self): # noqa D107 - # Contains new_data, gasm_valid & heat_stab - self.status = None - self.heat_stable = False - # The index of the heater profile used - self.gas_index = None - # Measurement index to track order - self.meas_index = None - # Temperature in degree celsius x100 - self.temperature = None - # Pressure in Pascal - self.pressure = None - # Humidity in % relative humidity x1000 - self.humidity = None - # Gas resistance in Ohms - self.gas_resistance = None - - -class CalibrationData: - """Structure for storing BME680 calibration data.""" - - def __init__(self): # noqa D107 - self.par_h1 = None - self.par_h2 = None - self.par_h3 = None - self.par_h4 = None - self.par_h5 = None - self.par_h6 = None - self.par_h7 = None - self.par_gh1 = None - self.par_gh2 = None - self.par_gh3 = None - self.par_t1 = None - self.par_t2 = None - self.par_t3 = None - self.par_p1 = None - self.par_p2 = None - self.par_p3 = None - self.par_p4 = None - self.par_p5 = None - self.par_p6 = None - self.par_p7 = None - self.par_p8 = None - self.par_p9 = None - self.par_p10 = None - # Variable to store t_fine size - self.t_fine = None - # Variable to store heater resistance range - self.res_heat_range = None - # Variable to store heater resistance value - self.res_heat_val = None - # Variable to store error range - self.range_sw_err = None - - def set_from_array(self, calibration): - """Set parameters from an array of bytes.""" - # Temperature related coefficients - self.par_t1 = bytes_to_word(calibration[T1_MSB_REG], calibration[T1_LSB_REG]) - self.par_t2 = bytes_to_word(calibration[T2_MSB_REG], calibration[T2_LSB_REG], bits=16, signed=True) - self.par_t3 = twos_comp(calibration[T3_REG], bits=8) - - # Pressure related coefficients - self.par_p1 = bytes_to_word(calibration[P1_MSB_REG], calibration[P1_LSB_REG]) - self.par_p2 = bytes_to_word(calibration[P2_MSB_REG], calibration[P2_LSB_REG], bits=16, signed=True) - self.par_p3 = twos_comp(calibration[P3_REG], bits=8) - self.par_p4 = bytes_to_word(calibration[P4_MSB_REG], calibration[P4_LSB_REG], bits=16, signed=True) - self.par_p5 = bytes_to_word(calibration[P5_MSB_REG], calibration[P5_LSB_REG], bits=16, signed=True) - self.par_p6 = twos_comp(calibration[P6_REG], bits=8) - self.par_p7 = twos_comp(calibration[P7_REG], bits=8) - self.par_p8 = bytes_to_word(calibration[P8_MSB_REG], calibration[P8_LSB_REG], bits=16, signed=True) - self.par_p9 = bytes_to_word(calibration[P9_MSB_REG], calibration[P9_LSB_REG], bits=16, signed=True) - self.par_p10 = calibration[P10_REG] - - # Humidity related coefficients - self.par_h1 = (calibration[H1_MSB_REG] << HUM_REG_SHIFT_VAL) | (calibration[H1_LSB_REG] & BIT_H1_DATA_MSK) - self.par_h2 = (calibration[H2_MSB_REG] << HUM_REG_SHIFT_VAL) | (calibration[H2_LSB_REG] >> HUM_REG_SHIFT_VAL) - self.par_h3 = twos_comp(calibration[H3_REG], bits=8) - self.par_h4 = twos_comp(calibration[H4_REG], bits=8) - self.par_h5 = twos_comp(calibration[H5_REG], bits=8) - self.par_h6 = calibration[H6_REG] - self.par_h7 = twos_comp(calibration[H7_REG], bits=8) - - # Gas heater related coefficients - self.par_gh1 = twos_comp(calibration[GH1_REG], bits=8) - self.par_gh2 = bytes_to_word(calibration[GH2_MSB_REG], calibration[GH2_LSB_REG], bits=16, signed=True) - self.par_gh3 = twos_comp(calibration[GH3_REG], bits=8) - - def set_other(self, heat_range, heat_value, sw_error): - """Set other values.""" - self.res_heat_range = (heat_range & RHRANGE_MSK) // 16 - self.res_heat_val = heat_value - self.range_sw_err = (sw_error & RSERROR_MSK) // 16 - - -class TPHSettings: - """Structure for storing BME680 sensor settings. - - Comprises of output data rate, over-sampling and filter settings. - - """ - - def __init__(self): # noqa D107 - # Humidity oversampling - self.os_hum = None - # Temperature oversampling - self.os_temp = None - # Pressure oversampling - self.os_pres = None - # Filter coefficient - self.filter = None - - -class GasSettings: - """Structure for storing BME680 gas settings and status.""" - - def __init__(self): # noqa D107 - # Variable to store nb conversion - self.nb_conv = None - # Variable to store heater control - self.heatr_ctrl = None - # Run gas enable value - self.run_gas = None - # Pointer to store heater temperature - self.heatr_temp = None - # Pointer to store duration profile - self.heatr_dur = None - - -class BME680Data: - """Structure to represent BME680 device.""" - - def __init__(self): # noqa D107 - # Chip Id - self.chip_id = None - # Device Id - self.dev_id = None - # SPI/I2C interface - self.intf = None - # Memory page used - self.mem_page = None - # Ambient temperature in Degree C - self.ambient_temperature = None - # Field Data - self.data = FieldData() - # Sensor calibration data - self.calibration_data = CalibrationData() - # Sensor settings - self.tph_settings = TPHSettings() - # Gas Sensor settings - self.gas_settings = GasSettings() - # Sensor power modes - self.power_mode = None - # New sensor fields - self.new_fields = None diff --git a/scripts/tempSensor/lib/board-1.0.dist-info/METADATA b/scripts/tempSensor/lib/board-1.0.dist-info/METADATA deleted file mode 100644 index 665775d3..00000000 --- a/scripts/tempSensor/lib/board-1.0.dist-info/METADATA +++ /dev/null @@ -1,370 +0,0 @@ -Metadata-Version: 2.1 -Name: board -Version: 1.0 -Summary: Standard Board mechanism for Dojo tasks -Home-page: https://github.com/tjguk/dojo-board -Author: Tim Golden -Author-email: mail@timgolden.me.uk -Maintainer: Tim Golden -Maintainer-email: mail@timgolden.me.uk -License: unlicensed -Platform: UNKNOWN -Classifier: Environment :: Console -Classifier: Intended Audience :: Developers -Classifier: Operating System :: OS Independent -Classifier: Programming Language :: Python -Classifier: Programming Language :: Python :: 2 -Classifier: Programming Language :: Python :: 2.7 -Classifier: Programming Language :: Python :: 3 - -Board Game for Python Dojos -=========================== - -Introduction ------------- - -Often, when running a Python Dojo, we've ended up with a challenge -based around some kind of board or tile-based landscape. In these -situations it's not uncommon to spend a lot of the time building up -your basic board functionality in order to support the more interesting -gameplay algorithm. - -This module implements a general-purpose board structure which -has the functionality needed for a range of purposes, and lends itself -to being subclassed for those particular needs. - -Dependencies ------------- - -None - stdlib only - -Tests ------ - -Fairly decent coverage (not actually checked with coverage.py): test.py - -Getting Started ---------------- - -Install with pip:: - - pip install board - -Absolutely basic usage:: - - import board - # - # Produce a 3x3 board - # - b = board.Board((3, 3)) - - b[0, 0] = "X" - b[1, 0] = "O" - -Usage ------ - -Board is an n-dimensional board, any of which dimensions can be of -infinite size. (So if you have, say, 3 infinite dimensions, you have -the basis for a Minecraft layout). Dimensions are zero-based and -negative indexes operate as they usually do in Python: working from -the end of the dimension backwards. - -Cells on the board are accessed by item access, eg board[1, 2] or -landscape[1, 1, 10]. - -A board can be copied, optionally along with its data by means of the -.copy method. Or a section of a board can be linked to the original -board by slicing the original board:: - - b1 = board.Board((9, 9)) - b1[1, 1] = 1 - b2 = b1.copy() - b3 = b1[:3, :3] - -Note that the slice must include all the dimensions of the original -board, but any of those subdimensions can be of length 1:: - - b1 = board.Board((9, 9, 9)) - b2 = b1[:3, :3, :1] - -A sentinel value of Empty indicates a position which is not populated -because it has never had a value, or because its value has been deleted:: - - b1 = board.Board((3, 3)) - assert b1[1, 1] is board.Empty - b1.populate("abcdefghi") - assert b1[1, 1] == "e" - del b1[1, 1] - assert b1[1, 1] is board.Empty - -Iterating over the board yields its coordinates:: - - b1 = board.Board((2, 2)) - for coord in b1: - print(coord) - # - # => (0, 0), (0, 1) etc. - # - -Iteration over a board with one or more infinite dimensions will work -by iterating in chunks:: - - b1 = board.Board((3, 3, board.Infinity)) - for coord in b1: - print(b1) - -To see coordinates with their data items, use iterdata:: - - b1 = board.Board((2, 2)) - b1.populate("abcd") - for coord, data in b1.iterdata(): - print(coord, "=>", data) - -To read, write and empty the data at a board position, use indexing:: - - b1 = board.Board((3, 3)) - b1.populate("abcdef") - print(b1[0, 0]) # "a" - - b1[0, 0] = "*" - print(b1[0, 0]) # "*" - - b1[-1, -1] = "*" - print(b1[2, 2]) # "*" - - del b1[0, 0] - print(b1[0, 0]) # - -To test whether a coordinate is contained with the local coordinate space, use in:: - - b1 = board.Board((3, 3)) - (1, 1) in b1 # True - (4, 4) in b1 # False - (1, 1, 1) in b1 # InvalidDimensionsError - -One board is equal to another if it has the same dimensionality and -each data item is equal:: - - b1 = board.Board((3, 3)) - b1.populate("abcdef") - b2 = b1.copy() - b1 == b2 # True - b2[0, 0] = "*" - b1 == b2 # False - - b2 = board.Board((2, 2)) - b2.populate("abcdef") - b1 == b2 # False - -To populate the board from an arbitrary iterator, use .populate:: - - def random_letters(): - import random, string - while True: - yield random.choice(string.ascii_uppercase) - - b1 = board.Board((4, 4)) - b1.populate(random_letters()) - -To clear the board, use .clear:: - - b1 = board.Board((3, 3)) - b1.populate(range(10)) - b1.clear() - list(b1.iterdata()) # [] - -A board is True if it has any data, False if it has none:: - - b1 = board.Board((2, 2)) - b1.populate("abcd") - bool(b1) # True - b1.clear() - bool(b1) # False - -The length of the board is the product of its dimension lengths. If any -dimension is infinite, the board length is infinite. NB to find the -amount of data on the board, use lendata:: - - b1 = board.Board((4, 4)) - len(b1) # 16 - b1.populate("abcd") - len(b1) # 16 - b1.lendata() # 4 - b2 = board.Board((2, board.Infinity)) - len(b2) # Infinity - -To determine the bounding box of the board which contains data, use .occupied:: - - b1 = board.Board((3, 3)) - b1.populate("abcd") - list(c for (c, d) in b1.iterdata()) # [(0, 0), (0, 1), (0, 2), (1, 0)] - b1.occupied() # ((0, 0), (1, 2)) - -For the common case of slicing a board around its occupied space, -use .occupied_board:: - - b1 = board.Board((3, 3)) - b1.populate("abcd") - b1.draw() - b2 = b1.occupied_board() - b2.draw() - -To test whether a position is on any edge of the board, use .is_edge:: - - b1 = board.Board((3, 3)) - b1.is_edge((0, 0)) # True - b1.is_edge((1, 1)) # False - b1.is_edge((2, 0)) # True - -To find the immediate on-board neighbours to a position along all dimensions:: - - b1 = board.Board((3, 3, 3)) - list(b1.neighbours((0, 0, 0))) - # [(0, 1, 1), (1, 1, 0), ..., (1, 0, 1), (0, 1, 0)] - -To iterate over all the coords in the rectangular space between -two corners, use .itercoords:: - - b1 = board.Board((3, 3)) - list(b1.itercoords((0, 0), (1, 1))) # [(0, 0), (0, 1), (1, 0), (1, 1)] - -To iterate over all the on-board positions from one point in a -particular direction, use .iterline:: - - b1 = board.Board((4, 4)) - start_from = 1, 1 - direction = 1, 1 - list(b1.iterline(start_from, direction)) # [(1, 1), (2, 2), (3, 3)] - direction = 0, 2 - list(b1.iterline(start_from, direction)) # [(1, 1), (1, 3)] - -or .iterlinedata to generate the data at each point:: - - b1 = board.Board((3, 3)) - b1.populate("ABCDEFGHJ") - start_from = 1, 1 - direction = 1, 0 - list(b1.iterlinedata(start_from, direction)) # ['A', 'D', 'G'] - -Both iterline and iterdata can take a maximum number of steps, eg for -games like Connect 4 or Battleships:: - - b1 = board.Board((8, 8)) - # - # Draw a Battleship - # - b1.populate("BBBB", b1.iterline((2, 2), (1, 0))) - -As a convenience for games which need to look for a run of so many -things, the .run_of_n method combines iterline with data to yield -every possible line on the board which is of a certain length along -with its data:: - - b1 = board.Board((3, 3)) - b1[0, 0] = 'X' - b1[1, 1] = 'O' - b1[0, 1] = 'X' - for line, data in b1.runs_of_n(3): - if all(d == "O" for d in data): - print("O wins") - break - elif all(d == "X" for d in data): - print("X wins") - break - -To iterate over the corners of the board, use .corners:: - - b1 = board.Board((3, 3)) - corners() # [(0, 0), (0, 2), (2, 0), (2, 2)] - -Properties ----------- - -To determine whether a board is offset from another (ie the result of a slice):: - - b1 = board.Board((3, 3)) - b1.is_offset # False - b2 = b1[:1, :1] - b2.is_offset # True - -To determine whether a board has any infinite or finite dimensions:: - - b1 = board.Board((3, board.Infinity)) - b1.has_finite_dimensions # True - b1.has_infinite_dimensions # True - b2 = board.Board((3, 3)) - b1.has_infinite_dimensions # False - b3 = board.Board((board.Infinity, board.Infinity)) - b3.has_finite_dimensions # False - -Display the Board ------------------ - -To get a crude view of the contents of the board, use .dump:: - - b1 = board.Board((3, 3)) - b1.populate("abcdef") - b1.dump() - -To get a grid view of a 2-dimensional board, use .draw:: - - b1 = board.Board((3, 3)) - b1.populate("OX XXOO ") - b1.draw() - -If you don't want the borders drawn, eg because you're using the board -to render ASCII art, pass use_borders=False:: - - b1 = board.Board((8, 8)) - for coord in b1.iterline((0, 0), (1, 1)): - b1[coord] = "*" - for coord in b1.iterline((7, 0), (-1, 1)): - b1[coord] = "*" - b1.draw(use_borders=False) - -To render to an image using Pillow (which isn't a hard dependency) use paint. -The default renderer treats the data items as text and renders then, scaled -to fit, into each cell. This works, obviously, for things like Noughts & Crosses -assuming that you store something like "O" and "X". But it also works for -word searches and even simple battleships where the data items are objects -whose __str__ returns blank (for undiscovered), "+" for a single hit, and "*" -for a destroyed vessel:: - - b1 = board.Board((3, 3)) - b1[0, 0] = "X" - b1[1, 1] = "O" - b1[0, 2] = "X" - b1.paint("board.png") - # ... and now look at board.png - -The text painting is achieved internally by means of a callback called -text_sprite. An alternative ready-cooked callback for paint() is -imagefile_sprite. This looks for a .png file in the current directory -(or another; you can specify). - -Local and Global coordinates ----------------------------- - -Since one board can represent a slice of another, there are two levels -of coordinates: local and global. Coordinates passed to or returned from -any of the public API methods are always local for that board. They -represent the natural coordinate space for the board. Internally, the -module will use global coordinates, translating as necessary. - -Say you're managing a viewport of a tile-based dungeon game where the -master dungeon board is 100 x 100 but the visible board is 10 x 10. -Your viewport board is currently representing the slice of the master -board from (5, 5) to (14, 14). Changing the item at position (2, 2) on -the viewport board will change the item at position (7, 7) on the master -board (and vice versa). - -As a user of the API you don't need to know this, except to understand -that a board slice is essentially a view on its parent. If you wish -to subclass or otherwise extend the board, you'll need to note where -coordinate translations are necessary. - - - - - diff --git a/scripts/tempSensor/lib/board-1.0.dist-info/RECORD b/scripts/tempSensor/lib/board-1.0.dist-info/RECORD deleted file mode 100644 index ab0fcb94..00000000 --- a/scripts/tempSensor/lib/board-1.0.dist-info/RECORD +++ /dev/null @@ -1,3 +0,0 @@ -board-1.0.dist-info/METADATA,, -board.py,, -board-1.0.dist-info/RECORD,, \ No newline at end of file diff --git a/scripts/tempSensor/lib/board.py b/scripts/tempSensor/lib/board.py deleted file mode 100644 index 84597581..00000000 --- a/scripts/tempSensor/lib/board.py +++ /dev/null @@ -1,775 +0,0 @@ -# -*- coding: utf-8-*- # Encoding cookie added by Mu Editor - -"""Board -- an n-dimensional board with support for iteration, containership and slicing - -Boards can have any number of dimensions, any of which can be infinite. Boards -can be sliced [:1, :2], returning a linked-copy, or copied (.copy), returning a -snapshot copy. - -Boards can be iterated over for coordinates or data (.iterdata). There are also -convenience functions to determine neighbours across all dimensions (.neighbours), -the bounding box of occupied data (.occupied), all the coordinates in a space -in n-dimensions (.itercoords) and others. -""" - -# testing -# -# The semantics of 3.x range are broadly equivalent -# to xrange in 2.7 -# -try: - range = xrange -except NameError: - pass -try: - long -except NameError: - long = int - -import os, sys -import functools -import itertools -import io - -try: - from PIL import Image, ImageDraw, ImageFont -except ImportError: - Image = None - -class _Infinity(int): - - def __new__(meta): - return sys.maxsize - - def __str__(self): - return "Infinity" - - def __repr__(self): - return "" - - def __eq__(self, other): - return other == self.size - - def __lt__(self, other): - return False - - def __gt__(self, other): - return True - -Infinity = _Infinity() - -class _Empty(object): - - def __repr__(self): - return "" - - def __bool__(self): - return False - __nonzero__ = __bool__ - -Empty = _Empty() - -class BaseDimension(object): - - def __repr__(self): - return "<{}>".format(self.__class__.__name__) - -class Dimension(BaseDimension): - - is_finite = True - is_infinite = False - - def __init__(self, size): - self._size = size - self._range = range(size) - - def __iter__(self): - return iter(self._range) - - def __eq__(self, other): - return isinstance(self, type(other)) and self._size == other._size - - def __repr__(self): - return "<{}: {}>".format(self.__class__.__name__, self._size) - - def __len__(self): - return self._size - - def __contains__(self, item): - return item in self._range - - def __getitem__(self, item): - if isinstance(item, (int, long)): - return self._range[item] - elif isinstance(item, slice): - return self._range[item.start, item.stop, item.step] - else: - raise TypeError("{} can only be indexed by int or slice".format(self.__class__.__name__)) - -class _InfiniteDimension(BaseDimension): - - chunk_size = 10 - is_finite = False - is_infinite = True - - def __iter__(self): - return itertools.count() - - def __repr__(self): - return "" - - def __eq__(self, other): - # - # Ensure that any infinite dimension is equal to any other - # - return isinstance(other, self.__class__) - - def __contains__(self, item): - # - # An infinite dimension includes any non-negative coordinate - # - if item < 0: - return False - return True - - def __len__(self): - return Infinity - - def __getitem__(self, item): - if isinstance(item, (int, long)): - if item == 0: - return 0 - elif item == -1: - return Infinity - else: - raise IndexError("Infinite dimensions can only return first & last items") - - elif isinstance(item, slice): - # - # If the request is for an open-ended slice, - # just return the same infinite dimension. - # - if item.stop is None: - return self - else: - return range(*item.indices(item.stop)) - - else: - raise TypeError("{} can only be indexed by int or slice".format(self.__class__.__name__)) - -InfiniteDimension = _InfiniteDimension() - -def _centred_coord(outer_size, inner_size): - """Given an outer and an inner size, calculate the top-left coordinates - which the inner image should position at to be centred within the outer - image - """ - outer_w, outer_h = outer_size - inner_w, inner_h = inner_size - return round((outer_w - inner_w) / 2), round((outer_h - inner_h) / 2) - - -def text_sprite(font_name="arial", colour="#0000ff"): - """Text sprite generator callback from Board.paint - - Convert the object to text of approximately the right size for - the cell being painted. Typically this will be used for one or - two letter objects, but it will work for any object which can - meaningfully be converted to text - """ - - def _text_sprite(obj, size): - # - # Very roughly, one point is three quarters of - # a pixel. We pick a point size which will fill - # the smaller edge of the cell (if it's not square) - # - point_size = round(min(size) * 0.75) - - # - # Create a new transparent image to hold the - # text. Draw the text into it in blue, centred, - # using the font requested, and return the resulting image - # - image = Image.new("RGBA", size, (255, 255, 255, 0)) - draw = ImageDraw.Draw(image) - font = ImageFont.truetype("%s.ttf" % font_name, point_size) - text = str(obj) - draw.text(_centred_coord(size, font.getsize(text)), text, font=font, fill=colour) - return image - - return _text_sprite - -def imagefile_sprite(directory=".", extension=".png"): - """Image sprite generator callback for Board.paint - - Given the text form of an object, look for an image file in the - stated directory [default: current] and return it, scaled to size. - """ - - def _imagefile_sprite(obj, size): - image = Image.open(os.path.join(directory, "%s%s" % (obj, extension))) - image.thumbnail(size) - return image - - return _imagefile_sprite - -class Board(object): - """Board - represent a board of n dimensions, each possibly infinite. - - A location on the board is represented as an n-dimensional - coordinate, matching the dimensionality originally specified. - - The board is addressed by index with a coordinate: - - b = Board((4, 4)) - b[2, 2] = "*" - b.draw() - """ - - class BoardError(Exception): pass - class InvalidDimensionsError(BoardError): pass - class OutOfBoundsError(BoardError): pass - - def __init__(self, dimension_sizes, _global_board=None, _offset_from_global=None): - """Set up a n-dimensional board - """ - if not dimension_sizes: - raise self.InvalidDimensionsError("The board must have at least one dimension") - try: - iter(dimension_sizes) - except TypeError: - raise self.InvalidDimensionsError("Dimensions must be iterable (eg a tuple), not {}".format(type(dimension_sizes).__name__)) - if any(d <= 0 for d in dimension_sizes): - raise self.InvalidDimensionsError("Each dimension must be >= 1") - self.dimensions = [InfiniteDimension if size == Infinity else Dimension(size) for size in dimension_sizes] - - # - # This can be a sub-board of another board: a slice. - # If that's the case, the boards share a common data structure - # and this one is offset from the other. - # NB this means that if a slice is taken of a slice, the offset must itself be offset! - # - self._data = {} if _global_board is None else _global_board - self._offset_from_global = _offset_from_global or tuple(0 for _ in self.dimensions) - self._sprite_cache = {} - - def __repr__(self): - return "<{} ({})>".format( - self.__class__.__name__, - ", ".join(("Infinity" if d.is_infinite else str(len(d))) for d in self.dimensions) - ) - - def __eq__(self, other): - return \ - self.dimensions == other.dimensions and \ - dict(self.iterdata()) == dict(other.iterdata()) - - def __len__(self): - # - # Return the total number of positions on the board. If any of - # the dimensions is infinite, the total will be Infinity - # - if any(d.is_infinite for d in self.dimensions): - return Infinity - else: - return functools.reduce(lambda a, b: a * b, (len(d) for d in self.dimensions)) - - def __bool__(self): - return any(coord for coord in self._data if self._is_in_bounds(coord)) - __nonzero__ = __bool__ - - @property - def is_offset(self): - """Is this board offset from a different board?""" - return any(o for o in self._offset_from_global) - - @property - def has_finite_dimensions(self): - """Does this board have at least one finite dimension?""" - return any(d.is_finite for d in self.dimensions) - - @property - def has_infinite_dimensions(self): - """Does this board have at least one infinite dimension?""" - return any(d.is_infinite for d in self.dimensions) - - def dumped(self): - is_offset = any(o for o in self._offset_from_global) - if is_offset: - offset = " offset by {}".format(self._offset_from_global) - else: - offset = "" - yield repr(self) + offset - yield "{" - for coord, value in sorted(self.iterdata()): - if is_offset: - global_coord = " => {}".format(self._to_global(coord)) - else: - global_coord = "" - data = " [{}]".format(self[coord] if self[coord] is not None else "") - yield " {}{}{}".format(coord, global_coord, data) - yield "}" - - def dump(self, outf=sys.stdout): - for line in self.dumped(): - outf.write(line + "\n") - - def _is_in_bounds(self, coord): - """Is a given coordinate within the space of this board? - """ - if len(coord) != len(self.dimensions): - raise self.InvalidDimensionsError( - "Coordinate {} has {} dimensions; the board has {}".format(coord, len(coord), len(self.dimensions))) - - return all(c in d for (c, d) in zip(coord, self.dimensions)) - - def _check_in_bounds(self, coord): - """If a given coordinate is not within the space of this baord, raise - an OutOfBoundsError - """ - if not self._is_in_bounds(coord): - raise self.OutOfBoundsError("{} is out of bounds for {}".format(coord, self)) - - def __contains__(self, coord): - """Implement in - """ - return self._is_in_bounds(coord) - - def __iter__(self): - """Implement for in - - Iterate over all combinations of coordinates. If you need data, - use iterdata(). - """ - # If all the dimensions are finite (the simplest and most common - # situation) just use itertools.product. - - # If any dimension is infinite, we can't use itertools.product - # directly because it consumes its arguments in order to make - # up the axes for its Cartesian join. Instead, we chunk through - # any infinite dimensions, while repeating the finite ones. - if any(d.is_infinite for d in self.dimensions): - start, chunk = 0, InfiniteDimension.chunk_size - while True: - iterators = [d[start:start+chunk] if d[-1] == Infinity else iter(d) for d in self.dimensions] - for coord in itertools.product(*iterators): - yield coord - start += chunk - else: - for coord in itertools.product(*self.dimensions): - yield coord - - def _to_global(self, coord): - return tuple(c + o for (c, o) in zip(coord, self._offset_from_global)) - - def _from_global(self, coord): - return tuple(c - o for (c, o) in zip(coord, self._offset_from_global)) - - def iterdata(self): - """Implement: for (, ) in - - Generate the list of data in local coordinate terms. - """ - for gcoord, value in self._data.items(): - lcoord = self._from_global(gcoord) - if self._is_in_bounds(lcoord): - yield lcoord, value - - def lendata(self): - """Return the number of data items populated - """ - return sum(1 for _ in self.iterdata()) - - def iterline(self, coord, vector, max_steps=None): - """Generate coordinates starting at the given one and moving - in the direction of the vector until the edge of the board is - reached. The initial coordinate must be on the board. The vector - must have the same dimensionality as the coordinate. - - NB the vector can specify a "step", eg it could be (1, 2) - """ - self._check_in_bounds(coord) - if len(vector) != len(coord): - raise InvalidDimensionsError() - - n_steps = 0 - while self._is_in_bounds(coord): - yield coord - n_steps += 1 - if max_steps is not None and n_steps == max_steps: - break - coord = tuple(c + v for (c, v) in zip(coord, vector)) - - def iterlinedata(self, coord, vector, max_steps=None): - """Use .iterline to generate the data starting at the given - coordinate and moving in the direction of the vector until - the edge of the board is reached or the maximum number of - steps has been taken (if specified). - - This could be used, eg, to see whether you have a battleship - or a word in a word-search - """ - for coord in self.iterline(coord, vector, max_steps): - yield self[coord] - - def corners(self): - dimension_bounds = [(0, len(d) -1 if d.is_finite else Infinity) for d in self.dimensions] - return list(itertools.product(*dimension_bounds)) - - def copy(self, with_data=True): - """Return a new board with the same dimensionality as the present one. - If with_data is truthy, populate with the current data. - - NB this creates a copy, not a reference. For linked copy of the board, - use __getitem__, eg b2 = b1[:, :, :] - """ - board = self.__class__(tuple(len(d) for d in self.dimensions)) - if with_data: - for coord, value in self.iterdata(): - board._data[coord] = value - return board - - def clear(self): - """Clear the data which belongs to this board, possibly a sub-board - of a larger board. - """ - for lcoord, value in list(self.iterdata()): - del self._data[self._to_global(lcoord)] - - def __getitem__(self, item): - """The item is either a tuple of numbers, representing a single - coordinate on the board, or a tuple of slices representing a copy - of some or all of the board. - """ - if all(isinstance(i, (int, long)) for i in item): - coord = self._normalised_coord(item) - return self._data.get(coord, Empty) - elif all(isinstance(i, (int, long, slice)) for i in item): - return self._slice(item) - else: - raise TypeError("{} can only be indexed by int or slice".format(self.__class__.__name__)) - - def __setitem__(self, coord, value): - if all(isinstance(c, (int, long)) for c in coord): - coord = self._normalised_coord(coord) - self._data[coord] = value - #~ elif all(isinstance(i, (int, long, slice)) for i in item): - #~ return self._slice(item) - else: - raise TypeError("{} can only be indexed by int or slice".format(self.__class__.__name__)) - - def __delitem__(self, coord): - coord = self._normalised_coord(coord) - try: - del self._data[coord] - except KeyError: - pass - - def _normalised_coord(self, coord): - """Given a coordinate, check whether it's the right dimensionality - for this board and whether it's within bounds. Return the underlying - global coordinate. - - If a negative number is given, apply the usual subscript maths - to come up with an index from the end of the dimension. - """ - if len(coord) != len(self.dimensions): - raise IndexError("Coordinate {} has {} dimensions; the board has {}".format(coord, len(coord), len(self.dimensions))) - - # - # Account for negative indices in the usual way, allowing - # for the fact that you can't use negative indices if the - # dimension is infinite - # - if any(d is InfiniteDimension and c < 0 for (c, d) in zip(coord, self.dimensions)): - raise IndexError("Cannot use negative index {} on an infinite dimension".format(c)) - normalised_coord = tuple(len(d) + c if c < 0 else c for (c, d) in zip(coord, self.dimensions)) - self._check_in_bounds(normalised_coord) - return self._to_global(normalised_coord) - - def _slice(self, slices): - """Produce a subset of this board linked to the same underlying data. - """ - if len(slices) != len(self.dimensions): - raise IndexError("Slices {} have {} dimensions; the board has {}".format(slices, len(slices), len(self.dimensions))) - - # - # Determine the start/stop/step for all the slices - # - slice_indices = [slice.indices(len(dimension)) for (slice, dimension) in zip(slices, self.dimensions)] - if any(abs(step) != 1 for start, stop, step in slice_indices): - raise IndexError("At least one of slices {} has a stride other than 1".format(slices)) - - # - # Create the new dimensions: infinite dimensions remain infinite if - # they're sliced open-ended, eg [1:]. Otherwise they become finite - # dimensions of the appropriate lengthm eg [1:3] gives a finite dimension - # of length 2 - # - # FIXME: perhaps use the Dimension class' built-in slicers - # - sizes = tuple( - Infinity if (d is InfiniteDimension and s.stop is None) - else (stop - start) - for s, (start, stop, step), d in zip(slices, slice_indices, self.dimensions) - ) - - # - # Need to take into account the offset of this board, which might - # itself be offset from the parent board. - # - offset = tuple(o + start for (o, (start, stop, step)) in zip(self._offset_from_global, slice_indices)) - return self.__class__(sizes, self._data, offset) - - def _occupied_dimension(self, n_dimension): - """Return the min/max along a particular dimension. - (Intended for internal use, eg when displaying an infinite dimension) - """ - data_in_use = [coord for coord in self._data if coord in self] - if not data_in_use: - return (None, None) - else: - return ( - min(c[n_dimension] for c in data_in_use), - max(c[n_dimension] for c in data_in_use) - ) - - def occupied(self): - """Return the bounding box of space occupied - """ - coords_in_use = [coord for coord, _ in self.iterdata()] - min_coord = tuple(min(coord) for coord in zip(*coords_in_use)) - max_coord = tuple(max(coord) for coord in zip(*coords_in_use)) - return min_coord, max_coord - - def occupied_board(self): - """Return a sub-board containing only the portion of this board - which contains data. - """ - (x0, y0), (x1, y1) = self.occupied() - return self[x0:x1+1, y0:y1+1] - - def itercoords(self, coord1, coord2): - """Iterate over the coordinates in between the two coordinates. - - The result is all the coordinates in the rectangular section bounded - by coord1 and coord2 - """ - for coord in (coord1, coord2): - self._check_in_bounds(coord) - - for coord in itertools.product(*(range(i1, 1 + i2) for (i1, i2) in zip(*sorted([coord1, coord2])))): - yield coord - - def neighbours(self, coord, include_diagonals=True): - """Iterate over all the neighbours of a coordinate - - For a given coordinate, yield each of its nearest neighbours along - all dimensions, including diagonal neighbours if requested (the default) - """ - offsets = itertools.product(*[(-1, 0, 1) for d in self.dimensions]) - for offset in offsets: - if all(o == 0 for o in offsets): - continue - # - # Diagonal offsets have no zero component - # - if include_diagonals or any(o == 0 for o in offset): - neighbour = tuple(c + o for (c, o) in zip(coord, offset)) - if self._is_in_bounds(neighbour): - yield neighbour - - def runs_of_n(self, n, ignore_reversals=True): - """Iterate over all dimensions to yield runs of length n - - Yield each run of n cells as a tuple of coordinates and a tuple - of data. If ignore_reversals is True (the default) then don't - yield the same line in the opposite direction. - - This is useful for, eg, noughts and crosses, battleship or connect 4 - where the game engine has to detect a line of somethings in a row. - """ - all_zeroes = tuple(0 for _ in self.dimensions) - all_offsets = itertools.product(*[(-1, 0, 1) for d in self.dimensions]) - offsets = [o for o in all_offsets if o != all_zeroes] - - already_seen = set() - # - # This is brute force: running for every cell and looking in every - # direction. We check later whether we've run off the board (as - # the resulting line will fall short). We might do some kind of - # pre-check here, but we have to check against every direction - # of every dimension, which would complicate this code - # - for cell in iter(self): - for direction in offsets: - line = tuple(self.iterline(cell, direction, n)) - if len(line) == n: - if line in already_seen: - continue - already_seen.add(line) - # - # Most of the time you don't want the same line twice, - # once in each direction. - # - if ignore_reversals: - already_seen.add(line[::-1]) - - yield line, [self[c] for c in line] - - def is_edge(self, coord): - """Determine whether a position is on any edge of the board. - - Infinite dimensions only have a lower edge (zero); finite dimensions - have a lower and an upper edge. - """ - self._check_in_bounds(coord) - dimension_bounds = ((0, len(d) - 1 if d.is_finite else 0) for d in self.dimensions) - return any(c in bounds for (c, bounds) in zip(coord, dimension_bounds)) - - def is_corner(self, coord): - """Determine whether a position is on any corner of the board - - Infinite dimensions only have a lower edge (zero); finite dimensions - have a lower and an upper edge. - """ - self._check_in_bounds(coord) - dimension_bounds = ((0, len(d) - 1 if d.is_finite else 0) for d in self.dimensions) - return all(c in bounds for (c, bounds) in zip(coord, dimension_bounds)) - - def populate(self, iterable, coord_iterable=None): - """Populate all or part of the board from an iterable - - The population iterable can be shorter or longer than the board - iterable. The two are zipped together so the population will stop - when the shorter is exhausted. - - If no iterable is supplied for cooordinates, the whole board is - populated. - - This is a convenience method both to assist testing and also for, - eg, games like Boggle or word-searches where the board must start - filled with letters etc. If the data needs to be, eg, a random or - weighted choice then this should be implemented in the iterator - supplied. - - With a coordinate iterable this could be used, for example, to combine - iterline and a list of objects to populate data on a Battleships board. - """ - if coord_iterable is None: - board_iter = iter(self) - else: - board_iter = iter(coord_iterable) - for coord, value in zip(board_iter, iter(iterable)): - self[coord] = value - - def draw(self, callback=str, use_borders=True): - """Draw the board in a very simple text layout - - By default data items are rendered as strings. If a different callback - is supplied, it is called with the data item and should return a string. - - The idea is that items can be "hidden" from the board, or rendered - differently according to some state. Think of Battleships where the - same object can be hidden, revealed, or sunk. - """ - for line in self.drawn(callback, use_borders): - print(line) - - def drawn(self, callback=str, use_borders=True): - if len(self.dimensions) != 2 or self.has_infinite_dimensions: - raise self.BoardError("Can only draw a finite 2-dimensional board") - - data = dict((coord, callback(v)) for (coord, v) in self.iterdata()) - if data: - cell_w = len(max((v for v in data.values()), key=len)) - else: - cell_w = 1 - if use_borders: - corner, hedge, vedge = "+", "-", "|" - else: - corner = hedge = vedge = "" - divider = (corner + (hedge * cell_w)) * len(self.dimensions[0]) + corner - - if use_borders: yield divider - for y in self.dimensions[1]: - yield vedge + vedge.join(data.get((x, y), "").center(cell_w) for x in self.dimensions[0]) + vedge - if use_borders: yield divider - - def painted(self, callback, size, background_colour, use_borders): - if not Image: - raise NotImplementedError("Painting is not available unless Pillow is installed") - if len(self.dimensions) != 2 or self.has_infinite_dimensions: - raise self.BoardError("Can only paint a finite 2-dimensional board") - - # - # Construct a board of the requested size, containing - # cells sized equally to fit within the size for each - # of the two dimensions. Keep the border between them - # proportional to the overall image size - # - n_wide = len(self.dimensions[0]) - n_high = len(self.dimensions[1]) - image = Image.new("RGBA", size) - if use_borders: - h_border = image.height / 80 - v_border = image.width / 80 - else: - h_border = v_border = 0 - draw = ImageDraw.Draw(image) - drawable_w = image.width - (1 + n_wide) * h_border - cell_w = round(drawable_w / n_wide) - drawable_h = image.height - (1 + n_high) * v_border - cell_h = round(drawable_h / n_high) - - for (x, y) in self: - obj = self[x, y] - # - # If the cell is empty: draw nothing - # Try to fetch the relevant sprite from the cache - # If the sprite is not cached, generate and cache it - # If the sprite is larger than the cell, crop it to the correct - # size, maintaining its centre - # - if obj is Empty: - sprite = None - else: - try: - sprite = self._sprite_cache[obj] - except KeyError: - sprite = self._sprite_cache[obj] = callback(obj, (cell_w, cell_h)) - if sprite.width > cell_w or sprite.height > cell_h: - box_x = (sprite.width - cell_w) / 2 - box_y = (sprite.height - cell_h) / 2 - sprite = sprite.crop((box_x, box_y, cell_w, cell_h)) - - # - # Draw the cell and any sprite within it - # - cell_x = round(h_border + ((cell_w + h_border) * x)) - cell_y = round(v_border + ((cell_h + v_border) * y)) - draw.rectangle((cell_x, cell_y, cell_x + cell_w, cell_y + cell_h), fill=background_colour) - if sprite: - x_offset, y_offset = _centred_coord((cell_w, cell_h), sprite.size) - image.alpha_composite(sprite, (cell_x + x_offset, cell_y + y_offset)) - - # - # Return the whole image as PNG-encoded bytes - # - f = io.BytesIO() - image.save(f, "PNG") - return f.getvalue() - - def paint(self, filepath, callback=text_sprite(), size=(800, 800), background_colour="#ffffcc", use_borders=True): - with open(filepath, "wb") as f: - f.write(self.painted(callback, size, background_colour, use_borders)) - -def cornerposts(dimensions): - for d in dimensions: - yield 0 - if d.is_finite: - yield len(d) - -if __name__ == '__main__': - pass \ No newline at end of file diff --git a/scripts/tempSensor/lib/ez_setup.py b/scripts/tempSensor/lib/ez_setup.py deleted file mode 100644 index 1bcd3e94..00000000 --- a/scripts/tempSensor/lib/ez_setup.py +++ /dev/null @@ -1,332 +0,0 @@ -#!/usr/bin/env python -"""Bootstrap setuptools installation - -To use setuptools in your package's setup.py, include this -file in the same directory and add this to the top of your setup.py:: - - from ez_setup import use_setuptools - use_setuptools() - -To require a specific version of setuptools, set a download -mirror, or use an alternate download directory, simply supply -the appropriate options to ``use_setuptools()``. - -This file can also be run as a script to install or upgrade setuptools. -""" -import os -import shutil -import sys -import tempfile -import zipfile -import optparse -import subprocess -import platform -import textwrap -import contextlib - -from distutils import log - -try: - from urllib.request import urlopen -except ImportError: - from urllib2 import urlopen - -try: - from site import USER_SITE -except ImportError: - USER_SITE = None - -DEFAULT_VERSION = "4.0.1" -DEFAULT_URL = "https://pypi.python.org/packages/source/s/setuptools/" - -def _python_cmd(*args): - """ - Return True if the command succeeded. - """ - args = (sys.executable,) + args - return subprocess.call(args) == 0 - - -def _install(archive_filename, install_args=()): - with archive_context(archive_filename): - # installing - log.warn('Installing Setuptools') - if not _python_cmd('setup.py', 'install', *install_args): - log.warn('Something went wrong during the installation.') - log.warn('See the error message above.') - # exitcode will be 2 - return 2 - - -def _build_egg(egg, archive_filename, to_dir): - with archive_context(archive_filename): - # building an egg - log.warn('Building a Setuptools egg in %s', to_dir) - _python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir) - # returning the result - log.warn(egg) - if not os.path.exists(egg): - raise IOError('Could not build the egg.') - - -class ContextualZipFile(zipfile.ZipFile): - """ - Supplement ZipFile class to support context manager for Python 2.6 - """ - - def __enter__(self): - return self - - def __exit__(self, type, value, traceback): - self.close() - - def __new__(cls, *args, **kwargs): - """ - Construct a ZipFile or ContextualZipFile as appropriate - """ - if hasattr(zipfile.ZipFile, '__exit__'): - return zipfile.ZipFile(*args, **kwargs) - return super(ContextualZipFile, cls).__new__(cls) - - -@contextlib.contextmanager -def archive_context(filename): - # extracting the archive - tmpdir = tempfile.mkdtemp() - log.warn('Extracting in %s', tmpdir) - old_wd = os.getcwd() - try: - os.chdir(tmpdir) - with ContextualZipFile(filename) as archive: - archive.extractall() - - # going in the directory - subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) - os.chdir(subdir) - log.warn('Now working in %s', subdir) - yield - - finally: - os.chdir(old_wd) - shutil.rmtree(tmpdir) - - -def _do_download(version, download_base, to_dir, download_delay): - egg = os.path.join(to_dir, 'setuptools-%s-py%d.%d.egg' - % (version, sys.version_info[0], sys.version_info[1])) - if not os.path.exists(egg): - archive = download_setuptools(version, download_base, - to_dir, download_delay) - _build_egg(egg, archive, to_dir) - sys.path.insert(0, egg) - - # Remove previously-imported pkg_resources if present (see - # https://bitbucket.org/pypa/setuptools/pull-request/7/ for details). - if 'pkg_resources' in sys.modules: - del sys.modules['pkg_resources'] - - import setuptools - setuptools.bootstrap_install_from = egg - - -def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, - to_dir=os.curdir, download_delay=15): - to_dir = os.path.abspath(to_dir) - rep_modules = 'pkg_resources', 'setuptools' - imported = set(sys.modules).intersection(rep_modules) - try: - import pkg_resources - except ImportError: - return _do_download(version, download_base, to_dir, download_delay) - try: - pkg_resources.require("setuptools>=" + version) - return - except pkg_resources.DistributionNotFound: - return _do_download(version, download_base, to_dir, download_delay) - except pkg_resources.VersionConflict as VC_err: - if imported: - msg = textwrap.dedent(""" - The required version of setuptools (>={version}) is not available, - and can't be installed while this script is running. Please - install a more recent version first, using - 'easy_install -U setuptools'. - - (Currently using {VC_err.args[0]!r}) - """).format(VC_err=VC_err, version=version) - sys.stderr.write(msg) - sys.exit(2) - - # otherwise, reload ok - del pkg_resources, sys.modules['pkg_resources'] - return _do_download(version, download_base, to_dir, download_delay) - -def _clean_check(cmd, target): - """ - Run the command to download target. If the command fails, clean up before - re-raising the error. - """ - try: - subprocess.check_call(cmd) - except subprocess.CalledProcessError: - if os.access(target, os.F_OK): - os.unlink(target) - raise - -def download_file_powershell(url, target): - """ - Download the file at url to target using Powershell (which will validate - trust). Raise an exception if the command cannot complete. - """ - target = os.path.abspath(target) - ps_cmd = ( - "[System.Net.WebRequest]::DefaultWebProxy.Credentials = " - "[System.Net.CredentialCache]::DefaultCredentials; " - "(new-object System.Net.WebClient).DownloadFile(%(url)r, %(target)r)" - % vars() - ) - cmd = [ - 'powershell', - '-Command', - ps_cmd, - ] - _clean_check(cmd, target) - -def has_powershell(): - if platform.system() != 'Windows': - return False - cmd = ['powershell', '-Command', 'echo test'] - with open(os.path.devnull, 'wb') as devnull: - try: - subprocess.check_call(cmd, stdout=devnull, stderr=devnull) - except Exception: - return False - return True - -download_file_powershell.viable = has_powershell - -def download_file_curl(url, target): - cmd = ['curl', url, '--silent', '--output', target] - _clean_check(cmd, target) - -def has_curl(): - cmd = ['curl', '--version'] - with open(os.path.devnull, 'wb') as devnull: - try: - subprocess.check_call(cmd, stdout=devnull, stderr=devnull) - except Exception: - return False - return True - -download_file_curl.viable = has_curl - -def download_file_wget(url, target): - cmd = ['wget', url, '--quiet', '--output-document', target] - _clean_check(cmd, target) - -def has_wget(): - cmd = ['wget', '--version'] - with open(os.path.devnull, 'wb') as devnull: - try: - subprocess.check_call(cmd, stdout=devnull, stderr=devnull) - except Exception: - return False - return True - -download_file_wget.viable = has_wget - -def download_file_insecure(url, target): - """ - Use Python to download the file, even though it cannot authenticate the - connection. - """ - src = urlopen(url) - try: - # Read all the data in one block. - data = src.read() - finally: - src.close() - - # Write all the data in one block to avoid creating a partial file. - with open(target, "wb") as dst: - dst.write(data) - -download_file_insecure.viable = lambda: True - -def get_best_downloader(): - downloaders = ( - download_file_powershell, - download_file_curl, - download_file_wget, - download_file_insecure, - ) - viable_downloaders = (dl for dl in downloaders if dl.viable()) - return next(viable_downloaders, None) - -def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, - to_dir=os.curdir, delay=15, downloader_factory=get_best_downloader): - """ - Download setuptools from a specified location and return its filename - - `version` should be a valid setuptools version number that is available - as an egg for download under the `download_base` URL (which should end - with a '/'). `to_dir` is the directory where the egg will be downloaded. - `delay` is the number of seconds to pause before an actual download - attempt. - - ``downloader_factory`` should be a function taking no arguments and - returning a function for downloading a URL to a target. - """ - # making sure we use the absolute path - to_dir = os.path.abspath(to_dir) - zip_name = "setuptools-%s.zip" % version - url = download_base + zip_name - saveto = os.path.join(to_dir, zip_name) - if not os.path.exists(saveto): # Avoid repeated downloads - log.warn("Downloading %s", url) - downloader = downloader_factory() - downloader(url, saveto) - return os.path.realpath(saveto) - -def _build_install_args(options): - """ - Build the arguments to 'python setup.py install' on the setuptools package - """ - return ['--user'] if options.user_install else [] - -def _parse_args(): - """ - Parse the command line for options - """ - parser = optparse.OptionParser() - parser.add_option( - '--user', dest='user_install', action='store_true', default=False, - help='install in user site package (requires Python 2.6 or later)') - parser.add_option( - '--download-base', dest='download_base', metavar="URL", - default=DEFAULT_URL, - help='alternative URL from where to download the setuptools package') - parser.add_option( - '--insecure', dest='downloader_factory', action='store_const', - const=lambda: download_file_insecure, default=get_best_downloader, - help='Use internal, non-validating downloader' - ) - parser.add_option( - '--version', help="Specify which version to download", - default=DEFAULT_VERSION, - ) - options, args = parser.parse_args() - # positional arguments are ignored - return options - -def main(): - """Install or upgrade setuptools and EasyInstall""" - options = _parse_args() - archive = download_setuptools( - version=options.version, - download_base=options.download_base, - downloader_factory=options.downloader_factory, - ) - return _install(archive, _build_install_args(options)) - -if __name__ == '__main__': - sys.exit(main()) diff --git a/scripts/tempSensor/lib/functools-0.0.7.dist-info/METADATA b/scripts/tempSensor/lib/functools-0.0.7.dist-info/METADATA deleted file mode 100644 index 1cb319be..00000000 --- a/scripts/tempSensor/lib/functools-0.0.7.dist-info/METADATA +++ /dev/null @@ -1,6 +0,0 @@ -Metadata-Version: 2.1 -Name: functools -Version: 0.0.7 -Summary: -Author: -License: MIT diff --git a/scripts/tempSensor/lib/functools-0.0.7.dist-info/RECORD b/scripts/tempSensor/lib/functools-0.0.7.dist-info/RECORD deleted file mode 100644 index d07aa045..00000000 --- a/scripts/tempSensor/lib/functools-0.0.7.dist-info/RECORD +++ /dev/null @@ -1,3 +0,0 @@ -functools-0.0.7.dist-info/METADATA,, -functools.py,, -functools-0.0.7.dist-info/RECORD,, \ No newline at end of file diff --git a/scripts/tempSensor/lib/functools.py b/scripts/tempSensor/lib/functools.py deleted file mode 100644 index faa47a8b..00000000 --- a/scripts/tempSensor/lib/functools.py +++ /dev/null @@ -1,31 +0,0 @@ -def partial(func, *args, **kwargs): - def _partial(*more_args, **more_kwargs): - kw = kwargs.copy() - kw.update(more_kwargs) - return func(*(args + more_args), **kw) - - return _partial - - -def update_wrapper(wrapper, wrapped, assigned=None, updated=None): - # Dummy impl - return wrapper - - -def wraps(wrapped, assigned=None, updated=None): - # Dummy impl - return lambda x: x - - -def reduce(function, iterable, initializer=None): - it = iter(iterable) - if initializer is None: - value = next(it) - else: - value = initializer - for element in it: - value = function(value, element) - return value - - -__version__ = '0.0.7' diff --git a/scripts/tempSensor/lib/itertools-0.2.3.dist-info/METADATA b/scripts/tempSensor/lib/itertools-0.2.3.dist-info/METADATA deleted file mode 100644 index c5d05217..00000000 --- a/scripts/tempSensor/lib/itertools-0.2.3.dist-info/METADATA +++ /dev/null @@ -1,6 +0,0 @@ -Metadata-Version: 2.1 -Name: itertools -Version: 0.2.3 -Summary: -Author: -License: MIT diff --git a/scripts/tempSensor/lib/itertools-0.2.3.dist-info/RECORD b/scripts/tempSensor/lib/itertools-0.2.3.dist-info/RECORD deleted file mode 100644 index 957baf93..00000000 --- a/scripts/tempSensor/lib/itertools-0.2.3.dist-info/RECORD +++ /dev/null @@ -1,3 +0,0 @@ -itertools-0.2.3.dist-info/METADATA,, -itertools.py,, -itertools-0.2.3.dist-info/RECORD,, \ No newline at end of file diff --git a/scripts/tempSensor/lib/itertools.py b/scripts/tempSensor/lib/itertools.py deleted file mode 100644 index c715ca43..00000000 --- a/scripts/tempSensor/lib/itertools.py +++ /dev/null @@ -1,77 +0,0 @@ -def count(start=0, step=1): - while True: - yield start - start += step - - -def cycle(p): - try: - len(p) - except TypeError: - # len() is not defined for this type. Assume it is - # a finite iterable so we must cache the elements. - cache = [] - for i in p: - yield i - cache.append(i) - p = cache - while p: - yield from p - - -def repeat(el, n=None): - if n is None: - while True: - yield el - else: - for i in range(n): - yield el - - -def chain(*p): - for i in p: - yield from i - - -def islice(p, start, stop=(), step=1): - if stop == (): - stop = start - start = 0 - # TODO: optimizing or breaking semantics? - if start >= stop: - return - it = iter(p) - for i in range(start): - next(it) - - while True: - yield next(it) - for i in range(step - 1): - next(it) - start += step - if start >= stop: - return - - -def tee(iterable, n=2): - return [iter(iterable)] * n - - -def starmap(function, iterable): - for args in iterable: - yield function(*args) - - -def accumulate(iterable, func=lambda x, y: x + y): - it = iter(iterable) - try: - acc = next(it) - except StopIteration: - return - yield acc - for element in it: - acc = func(acc, element) - yield acc - - -__version__ = '0.2.3' diff --git a/scripts/tempSensor/lib/paho/__init__.py b/scripts/tempSensor/lib/paho/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/scripts/tempSensor/lib/paho/mqtt/__init__.py b/scripts/tempSensor/lib/paho/mqtt/__init__.py deleted file mode 100644 index 377cecc7..00000000 --- a/scripts/tempSensor/lib/paho/mqtt/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -__version__ = "2.1.0" - - -class MQTTException(Exception): - pass diff --git a/scripts/tempSensor/lib/paho/mqtt/client.py b/scripts/tempSensor/lib/paho/mqtt/client.py deleted file mode 100644 index 4ccc8696..00000000 --- a/scripts/tempSensor/lib/paho/mqtt/client.py +++ /dev/null @@ -1,5004 +0,0 @@ -# Copyright (c) 2012-2019 Roger Light and others -# -# All rights reserved. This program and the accompanying materials -# are made available under the terms of the Eclipse Public License v2.0 -# and Eclipse Distribution License v1.0 which accompany this distribution. -# -# The Eclipse Public License is available at -# http://www.eclipse.org/legal/epl-v20.html -# and the Eclipse Distribution License is available at -# http://www.eclipse.org/org/documents/edl-v10.php. -# -# Contributors: -# Roger Light - initial API and implementation -# Ian Craggs - MQTT V5 support -""" -This is an MQTT client module. MQTT is a lightweight pub/sub messaging -protocol that is easy to implement and suitable for low powered devices. -""" -from __future__ import annotations - -import base64 -import collections -import errno -import hashlib -import logging -import os -import platform -import select -import socket -import string -import struct -import threading -import time -import urllib.parse -import urllib.request -import uuid -import warnings -from typing import TYPE_CHECKING, Any, Callable, Dict, Iterator, List, NamedTuple, Sequence, Tuple, Union, cast - -from paho.mqtt.packettypes import PacketTypes - -from .enums import CallbackAPIVersion, ConnackCode, LogLevel, MessageState, MessageType, MQTTErrorCode, MQTTProtocolVersion, PahoClientMode, _ConnectionState -from .matcher import MQTTMatcher -from .properties import Properties -from .reasoncodes import ReasonCode, ReasonCodes -from .subscribeoptions import SubscribeOptions - -try: - from typing import Literal -except ImportError: - from typing_extensions import Literal # type: ignore - -if TYPE_CHECKING: - try: - from typing import TypedDict # type: ignore - except ImportError: - from typing_extensions import TypedDict - - try: - from typing import Protocol # type: ignore - except ImportError: - from typing_extensions import Protocol # type: ignore - - class _InPacket(TypedDict): - command: int - have_remaining: int - remaining_count: list[int] - remaining_mult: int - remaining_length: int - packet: bytearray - to_process: int - pos: int - - - class _OutPacket(TypedDict): - command: int - mid: int - qos: int - pos: int - to_process: int - packet: bytes - info: MQTTMessageInfo | None - - class SocketLike(Protocol): - def recv(self, buffer_size: int) -> bytes: - ... - def send(self, buffer: bytes) -> int: - ... - def close(self) -> None: - ... - def fileno(self) -> int: - ... - def setblocking(self, flag: bool) -> None: - ... - - -try: - import ssl -except ImportError: - ssl = None # type: ignore[assignment] - - -try: - import socks # type: ignore[import-untyped] -except ImportError: - socks = None # type: ignore[assignment] - - -try: - # Use monotonic clock if available - time_func = time.monotonic -except AttributeError: - time_func = time.time - -try: - import dns.resolver - - HAVE_DNS = True -except ImportError: - HAVE_DNS = False - - -if platform.system() == 'Windows': - EAGAIN = errno.WSAEWOULDBLOCK # type: ignore[attr-defined] -else: - EAGAIN = errno.EAGAIN - -# Avoid linter complain. We kept importing it as ReasonCodes (plural) for compatibility -_ = ReasonCodes - -# Keep copy of enums values for compatibility. -CONNECT = MessageType.CONNECT -CONNACK = MessageType.CONNACK -PUBLISH = MessageType.PUBLISH -PUBACK = MessageType.PUBACK -PUBREC = MessageType.PUBREC -PUBREL = MessageType.PUBREL -PUBCOMP = MessageType.PUBCOMP -SUBSCRIBE = MessageType.SUBSCRIBE -SUBACK = MessageType.SUBACK -UNSUBSCRIBE = MessageType.UNSUBSCRIBE -UNSUBACK = MessageType.UNSUBACK -PINGREQ = MessageType.PINGREQ -PINGRESP = MessageType.PINGRESP -DISCONNECT = MessageType.DISCONNECT -AUTH = MessageType.AUTH - -# Log levels -MQTT_LOG_INFO = LogLevel.MQTT_LOG_INFO -MQTT_LOG_NOTICE = LogLevel.MQTT_LOG_NOTICE -MQTT_LOG_WARNING = LogLevel.MQTT_LOG_WARNING -MQTT_LOG_ERR = LogLevel.MQTT_LOG_ERR -MQTT_LOG_DEBUG = LogLevel.MQTT_LOG_DEBUG -LOGGING_LEVEL = { - LogLevel.MQTT_LOG_DEBUG: logging.DEBUG, - LogLevel.MQTT_LOG_INFO: logging.INFO, - LogLevel.MQTT_LOG_NOTICE: logging.INFO, # This has no direct equivalent level - LogLevel.MQTT_LOG_WARNING: logging.WARNING, - LogLevel.MQTT_LOG_ERR: logging.ERROR, -} - -# CONNACK codes -CONNACK_ACCEPTED = ConnackCode.CONNACK_ACCEPTED -CONNACK_REFUSED_PROTOCOL_VERSION = ConnackCode.CONNACK_REFUSED_PROTOCOL_VERSION -CONNACK_REFUSED_IDENTIFIER_REJECTED = ConnackCode.CONNACK_REFUSED_IDENTIFIER_REJECTED -CONNACK_REFUSED_SERVER_UNAVAILABLE = ConnackCode.CONNACK_REFUSED_SERVER_UNAVAILABLE -CONNACK_REFUSED_BAD_USERNAME_PASSWORD = ConnackCode.CONNACK_REFUSED_BAD_USERNAME_PASSWORD -CONNACK_REFUSED_NOT_AUTHORIZED = ConnackCode.CONNACK_REFUSED_NOT_AUTHORIZED - -# Message state -mqtt_ms_invalid = MessageState.MQTT_MS_INVALID -mqtt_ms_publish = MessageState.MQTT_MS_PUBLISH -mqtt_ms_wait_for_puback = MessageState.MQTT_MS_WAIT_FOR_PUBACK -mqtt_ms_wait_for_pubrec = MessageState.MQTT_MS_WAIT_FOR_PUBREC -mqtt_ms_resend_pubrel = MessageState.MQTT_MS_RESEND_PUBREL -mqtt_ms_wait_for_pubrel = MessageState.MQTT_MS_WAIT_FOR_PUBREL -mqtt_ms_resend_pubcomp = MessageState.MQTT_MS_RESEND_PUBCOMP -mqtt_ms_wait_for_pubcomp = MessageState.MQTT_MS_WAIT_FOR_PUBCOMP -mqtt_ms_send_pubrec = MessageState.MQTT_MS_SEND_PUBREC -mqtt_ms_queued = MessageState.MQTT_MS_QUEUED - -MQTT_ERR_AGAIN = MQTTErrorCode.MQTT_ERR_AGAIN -MQTT_ERR_SUCCESS = MQTTErrorCode.MQTT_ERR_SUCCESS -MQTT_ERR_NOMEM = MQTTErrorCode.MQTT_ERR_NOMEM -MQTT_ERR_PROTOCOL = MQTTErrorCode.MQTT_ERR_PROTOCOL -MQTT_ERR_INVAL = MQTTErrorCode.MQTT_ERR_INVAL -MQTT_ERR_NO_CONN = MQTTErrorCode.MQTT_ERR_NO_CONN -MQTT_ERR_CONN_REFUSED = MQTTErrorCode.MQTT_ERR_CONN_REFUSED -MQTT_ERR_NOT_FOUND = MQTTErrorCode.MQTT_ERR_NOT_FOUND -MQTT_ERR_CONN_LOST = MQTTErrorCode.MQTT_ERR_CONN_LOST -MQTT_ERR_TLS = MQTTErrorCode.MQTT_ERR_TLS -MQTT_ERR_PAYLOAD_SIZE = MQTTErrorCode.MQTT_ERR_PAYLOAD_SIZE -MQTT_ERR_NOT_SUPPORTED = MQTTErrorCode.MQTT_ERR_NOT_SUPPORTED -MQTT_ERR_AUTH = MQTTErrorCode.MQTT_ERR_AUTH -MQTT_ERR_ACL_DENIED = MQTTErrorCode.MQTT_ERR_ACL_DENIED -MQTT_ERR_UNKNOWN = MQTTErrorCode.MQTT_ERR_UNKNOWN -MQTT_ERR_ERRNO = MQTTErrorCode.MQTT_ERR_ERRNO -MQTT_ERR_QUEUE_SIZE = MQTTErrorCode.MQTT_ERR_QUEUE_SIZE -MQTT_ERR_KEEPALIVE = MQTTErrorCode.MQTT_ERR_KEEPALIVE - -MQTTv31 = MQTTProtocolVersion.MQTTv31 -MQTTv311 = MQTTProtocolVersion.MQTTv311 -MQTTv5 = MQTTProtocolVersion.MQTTv5 - -MQTT_CLIENT = PahoClientMode.MQTT_CLIENT -MQTT_BRIDGE = PahoClientMode.MQTT_BRIDGE - -# For MQTT V5, use the clean start flag only on the first successful connect -MQTT_CLEAN_START_FIRST_ONLY: CleanStartOption = 3 - -sockpair_data = b"0" - -# Payload support all those type and will be converted to bytes: -# * str are utf8 encoded -# * int/float are converted to string and utf8 encoded (e.g. 1 is converted to b"1") -# * None is converted to a zero-length payload (i.e. b"") -PayloadType = Union[str, bytes, bytearray, int, float, None] - -HTTPHeader = Dict[str, str] -WebSocketHeaders = Union[Callable[[HTTPHeader], HTTPHeader], HTTPHeader] - -CleanStartOption = Union[bool, Literal[3]] - - -class ConnectFlags(NamedTuple): - """Contains additional information passed to `on_connect` callback""" - - session_present: bool - """ - this flag is useful for clients that are - using clean session set to False only (MQTTv3) or clean_start = False (MQTTv5). - In that case, if client that reconnects to a broker that it has previously - connected to, this flag indicates whether the broker still has the - session information for the client. If true, the session still exists. - """ - - -class DisconnectFlags(NamedTuple): - """Contains additional information passed to `on_disconnect` callback""" - - is_disconnect_packet_from_server: bool - """ - tells whether this on_disconnect call is the result - of receiving an DISCONNECT packet from the broker or if the on_disconnect is only - generated by the client library. - When true, the reason code is generated by the broker. - """ - - -CallbackOnConnect_v1_mqtt3 = Callable[["Client", Any, Dict[str, Any], MQTTErrorCode], None] -CallbackOnConnect_v1_mqtt5 = Callable[["Client", Any, Dict[str, Any], ReasonCode, Union[Properties, None]], None] -CallbackOnConnect_v1 = Union[CallbackOnConnect_v1_mqtt5, CallbackOnConnect_v1_mqtt3] -CallbackOnConnect_v2 = Callable[["Client", Any, ConnectFlags, ReasonCode, Union[Properties, None]], None] -CallbackOnConnect = Union[CallbackOnConnect_v1, CallbackOnConnect_v2] -CallbackOnConnectFail = Callable[["Client", Any], None] -CallbackOnDisconnect_v1_mqtt3 = Callable[["Client", Any, MQTTErrorCode], None] -CallbackOnDisconnect_v1_mqtt5 = Callable[["Client", Any, Union[ReasonCode, int, None], Union[Properties, None]], None] -CallbackOnDisconnect_v1 = Union[CallbackOnDisconnect_v1_mqtt3, CallbackOnDisconnect_v1_mqtt5] -CallbackOnDisconnect_v2 = Callable[["Client", Any, DisconnectFlags, ReasonCode, Union[Properties, None]], None] -CallbackOnDisconnect = Union[CallbackOnDisconnect_v1, CallbackOnDisconnect_v2] -CallbackOnLog = Callable[["Client", Any, int, str], None] -CallbackOnMessage = Callable[["Client", Any, "MQTTMessage"], None] -CallbackOnPreConnect = Callable[["Client", Any], None] -CallbackOnPublish_v1 = Callable[["Client", Any, int], None] -CallbackOnPublish_v2 = Callable[["Client", Any, int, ReasonCode, Properties], None] -CallbackOnPublish = Union[CallbackOnPublish_v1, CallbackOnPublish_v2] -CallbackOnSocket = Callable[["Client", Any, "SocketLike"], None] -CallbackOnSubscribe_v1_mqtt3 = Callable[["Client", Any, int, Tuple[int, ...]], None] -CallbackOnSubscribe_v1_mqtt5 = Callable[["Client", Any, int, List[ReasonCode], Properties], None] -CallbackOnSubscribe_v1 = Union[CallbackOnSubscribe_v1_mqtt3, CallbackOnSubscribe_v1_mqtt5] -CallbackOnSubscribe_v2 = Callable[["Client", Any, int, List[ReasonCode], Union[Properties, None]], None] -CallbackOnSubscribe = Union[CallbackOnSubscribe_v1, CallbackOnSubscribe_v2] -CallbackOnUnsubscribe_v1_mqtt3 = Callable[["Client", Any, int], None] -CallbackOnUnsubscribe_v1_mqtt5 = Callable[["Client", Any, int, Properties, Union[ReasonCode, List[ReasonCode]]], None] -CallbackOnUnsubscribe_v1 = Union[CallbackOnUnsubscribe_v1_mqtt3, CallbackOnUnsubscribe_v1_mqtt5] -CallbackOnUnsubscribe_v2 = Callable[["Client", Any, int, List[ReasonCode], Union[Properties, None]], None] -CallbackOnUnsubscribe = Union[CallbackOnUnsubscribe_v1, CallbackOnUnsubscribe_v2] - -# This is needed for typing because class Client redefined the name "socket" -_socket = socket - - -class WebsocketConnectionError(ConnectionError): - """ WebsocketConnectionError is a subclass of ConnectionError. - - It's raised when unable to perform the Websocket handshake. - """ - pass - - -def error_string(mqtt_errno: MQTTErrorCode | int) -> str: - """Return the error string associated with an mqtt error number.""" - if mqtt_errno == MQTT_ERR_SUCCESS: - return "No error." - elif mqtt_errno == MQTT_ERR_NOMEM: - return "Out of memory." - elif mqtt_errno == MQTT_ERR_PROTOCOL: - return "A network protocol error occurred when communicating with the broker." - elif mqtt_errno == MQTT_ERR_INVAL: - return "Invalid function arguments provided." - elif mqtt_errno == MQTT_ERR_NO_CONN: - return "The client is not currently connected." - elif mqtt_errno == MQTT_ERR_CONN_REFUSED: - return "The connection was refused." - elif mqtt_errno == MQTT_ERR_NOT_FOUND: - return "Message not found (internal error)." - elif mqtt_errno == MQTT_ERR_CONN_LOST: - return "The connection was lost." - elif mqtt_errno == MQTT_ERR_TLS: - return "A TLS error occurred." - elif mqtt_errno == MQTT_ERR_PAYLOAD_SIZE: - return "Payload too large." - elif mqtt_errno == MQTT_ERR_NOT_SUPPORTED: - return "This feature is not supported." - elif mqtt_errno == MQTT_ERR_AUTH: - return "Authorisation failed." - elif mqtt_errno == MQTT_ERR_ACL_DENIED: - return "Access denied by ACL." - elif mqtt_errno == MQTT_ERR_UNKNOWN: - return "Unknown error." - elif mqtt_errno == MQTT_ERR_ERRNO: - return "Error defined by errno." - elif mqtt_errno == MQTT_ERR_QUEUE_SIZE: - return "Message queue full." - elif mqtt_errno == MQTT_ERR_KEEPALIVE: - return "Client or broker did not communicate in the keepalive interval." - else: - return "Unknown error." - - -def connack_string(connack_code: int|ReasonCode) -> str: - """Return the string associated with a CONNACK result or CONNACK reason code.""" - if isinstance(connack_code, ReasonCode): - return str(connack_code) - - if connack_code == CONNACK_ACCEPTED: - return "Connection Accepted." - elif connack_code == CONNACK_REFUSED_PROTOCOL_VERSION: - return "Connection Refused: unacceptable protocol version." - elif connack_code == CONNACK_REFUSED_IDENTIFIER_REJECTED: - return "Connection Refused: identifier rejected." - elif connack_code == CONNACK_REFUSED_SERVER_UNAVAILABLE: - return "Connection Refused: broker unavailable." - elif connack_code == CONNACK_REFUSED_BAD_USERNAME_PASSWORD: - return "Connection Refused: bad user name or password." - elif connack_code == CONNACK_REFUSED_NOT_AUTHORIZED: - return "Connection Refused: not authorised." - else: - return "Connection Refused: unknown reason." - - -def convert_connack_rc_to_reason_code(connack_code: ConnackCode) -> ReasonCode: - """Convert a MQTTv3 / MQTTv3.1.1 connack result to `ReasonCode`. - - This is used in `on_connect` callback to have a consistent API. - - Be careful that the numeric value isn't the same, for example: - - >>> ConnackCode.CONNACK_REFUSED_SERVER_UNAVAILABLE == 3 - >>> convert_connack_rc_to_reason_code(ConnackCode.CONNACK_REFUSED_SERVER_UNAVAILABLE) == 136 - - It's recommended to compare by names - - >>> code_to_test = ReasonCode(PacketTypes.CONNACK, "Server unavailable") - >>> convert_connack_rc_to_reason_code(ConnackCode.CONNACK_REFUSED_SERVER_UNAVAILABLE) == code_to_test - """ - if connack_code == ConnackCode.CONNACK_ACCEPTED: - return ReasonCode(PacketTypes.CONNACK, "Success") - if connack_code == ConnackCode.CONNACK_REFUSED_PROTOCOL_VERSION: - return ReasonCode(PacketTypes.CONNACK, "Unsupported protocol version") - if connack_code == ConnackCode.CONNACK_REFUSED_IDENTIFIER_REJECTED: - return ReasonCode(PacketTypes.CONNACK, "Client identifier not valid") - if connack_code == ConnackCode.CONNACK_REFUSED_SERVER_UNAVAILABLE: - return ReasonCode(PacketTypes.CONNACK, "Server unavailable") - if connack_code == ConnackCode.CONNACK_REFUSED_BAD_USERNAME_PASSWORD: - return ReasonCode(PacketTypes.CONNACK, "Bad user name or password") - if connack_code == ConnackCode.CONNACK_REFUSED_NOT_AUTHORIZED: - return ReasonCode(PacketTypes.CONNACK, "Not authorized") - - return ReasonCode(PacketTypes.CONNACK, "Unspecified error") - - -def convert_disconnect_error_code_to_reason_code(rc: MQTTErrorCode) -> ReasonCode: - """Convert an MQTTErrorCode to Reason code. - - This is used in `on_disconnect` callback to have a consistent API. - - Be careful that the numeric value isn't the same, for example: - - >>> MQTTErrorCode.MQTT_ERR_PROTOCOL == 2 - >>> convert_disconnect_error_code_to_reason_code(MQTTErrorCode.MQTT_ERR_PROTOCOL) == 130 - - It's recommended to compare by names - - >>> code_to_test = ReasonCode(PacketTypes.DISCONNECT, "Protocol error") - >>> convert_disconnect_error_code_to_reason_code(MQTTErrorCode.MQTT_ERR_PROTOCOL) == code_to_test - """ - if rc == MQTTErrorCode.MQTT_ERR_SUCCESS: - return ReasonCode(PacketTypes.DISCONNECT, "Success") - if rc == MQTTErrorCode.MQTT_ERR_KEEPALIVE: - return ReasonCode(PacketTypes.DISCONNECT, "Keep alive timeout") - if rc == MQTTErrorCode.MQTT_ERR_CONN_LOST: - return ReasonCode(PacketTypes.DISCONNECT, "Unspecified error") - return ReasonCode(PacketTypes.DISCONNECT, "Unspecified error") - - -def _base62( - num: int, - base: str = string.digits + string.ascii_letters, - padding: int = 1, -) -> str: - """Convert a number to base-62 representation.""" - if num < 0: - raise ValueError("Number must be positive or zero") - digits = [] - while num: - num, rest = divmod(num, 62) - digits.append(base[rest]) - digits.extend(base[0] for _ in range(len(digits), padding)) - return ''.join(reversed(digits)) - - -def topic_matches_sub(sub: str, topic: str) -> bool: - """Check whether a topic matches a subscription. - - For example: - - * Topic "foo/bar" would match the subscription "foo/#" or "+/bar" - * Topic "non/matching" would not match the subscription "non/+/+" - """ - matcher = MQTTMatcher() - matcher[sub] = True - try: - next(matcher.iter_match(topic)) - return True - except StopIteration: - return False - - -def _socketpair_compat() -> tuple[socket.socket, socket.socket]: - """TCP/IP socketpair including Windows support""" - listensock = socket.socket( - socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_IP) - listensock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - listensock.bind(("127.0.0.1", 0)) - listensock.listen(1) - - iface, port = listensock.getsockname() - sock1 = socket.socket( - socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_IP) - sock1.setblocking(False) - try: - sock1.connect(("127.0.0.1", port)) - except BlockingIOError: - pass - sock2, address = listensock.accept() - sock2.setblocking(False) - listensock.close() - return (sock1, sock2) - - -def _force_bytes(s: str | bytes) -> bytes: - if isinstance(s, str): - return s.encode("utf-8") - return s - - -def _encode_payload(payload: str | bytes | bytearray | int | float | None) -> bytes|bytearray: - if isinstance(payload, str): - return payload.encode("utf-8") - - if isinstance(payload, (int, float)): - return str(payload).encode("ascii") - - if payload is None: - return b"" - - if not isinstance(payload, (bytes, bytearray)): - raise TypeError( - "payload must be a string, bytearray, int, float or None." - ) - - return payload - - -class MQTTMessageInfo: - """This is a class returned from `Client.publish()` and can be used to find - out the mid of the message that was published, and to determine whether the - message has been published, and/or wait until it is published. - """ - - __slots__ = 'mid', '_published', '_condition', 'rc', '_iterpos' - - def __init__(self, mid: int): - self.mid = mid - """ The message Id (int)""" - self._published = False - self._condition = threading.Condition() - self.rc: MQTTErrorCode = MQTTErrorCode.MQTT_ERR_SUCCESS - """ The `MQTTErrorCode` that give status for this message. - This value could change until the message `is_published`""" - self._iterpos = 0 - - def __str__(self) -> str: - return str((self.rc, self.mid)) - - def __iter__(self) -> Iterator[MQTTErrorCode | int]: - self._iterpos = 0 - return self - - def __next__(self) -> MQTTErrorCode | int: - return self.next() - - def next(self) -> MQTTErrorCode | int: - if self._iterpos == 0: - self._iterpos = 1 - return self.rc - elif self._iterpos == 1: - self._iterpos = 2 - return self.mid - else: - raise StopIteration - - def __getitem__(self, index: int) -> MQTTErrorCode | int: - if index == 0: - return self.rc - elif index == 1: - return self.mid - else: - raise IndexError("index out of range") - - def _set_as_published(self) -> None: - with self._condition: - self._published = True - self._condition.notify() - - def wait_for_publish(self, timeout: float | None = None) -> None: - """Block until the message associated with this object is published, or - until the timeout occurs. If timeout is None, this will never time out. - Set timeout to a positive number of seconds, e.g. 1.2, to enable the - timeout. - - :raises ValueError: if the message was not queued due to the outgoing - queue being full. - - :raises RuntimeError: if the message was not published for another - reason. - """ - if self.rc == MQTT_ERR_QUEUE_SIZE: - raise ValueError('Message is not queued due to ERR_QUEUE_SIZE') - elif self.rc == MQTT_ERR_AGAIN: - pass - elif self.rc > 0: - raise RuntimeError(f'Message publish failed: {error_string(self.rc)}') - - timeout_time = None if timeout is None else time_func() + timeout - timeout_tenth = None if timeout is None else timeout / 10. - def timed_out() -> bool: - return False if timeout_time is None else time_func() > timeout_time - - with self._condition: - while not self._published and not timed_out(): - self._condition.wait(timeout_tenth) - - if self.rc > 0: - raise RuntimeError(f'Message publish failed: {error_string(self.rc)}') - - def is_published(self) -> bool: - """Returns True if the message associated with this object has been - published, else returns False. - - To wait for this to become true, look at `wait_for_publish`. - """ - if self.rc == MQTTErrorCode.MQTT_ERR_QUEUE_SIZE: - raise ValueError('Message is not queued due to ERR_QUEUE_SIZE') - elif self.rc == MQTTErrorCode.MQTT_ERR_AGAIN: - pass - elif self.rc > 0: - raise RuntimeError(f'Message publish failed: {error_string(self.rc)}') - - with self._condition: - return self._published - - -class MQTTMessage: - """ This is a class that describes an incoming message. It is - passed to the `on_message` callback as the message parameter. - """ - __slots__ = 'timestamp', 'state', 'dup', 'mid', '_topic', 'payload', 'qos', 'retain', 'info', 'properties' - - def __init__(self, mid: int = 0, topic: bytes = b""): - self.timestamp = 0.0 - self.state = mqtt_ms_invalid - self.dup = False - self.mid = mid - """ The message id (int).""" - self._topic = topic - self.payload = b"" - """the message payload (bytes)""" - self.qos = 0 - """ The message Quality of Service (0, 1 or 2).""" - self.retain = False - """ If true, the message is a retained message and not fresh.""" - self.info = MQTTMessageInfo(mid) - self.properties: Properties | None = None - """ In MQTT v5.0, the properties associated with the message. (`Properties`)""" - - def __eq__(self, other: object) -> bool: - """Override the default Equals behavior""" - if isinstance(other, self.__class__): - return self.mid == other.mid - return False - - def __ne__(self, other: object) -> bool: - """Define a non-equality test""" - return not self.__eq__(other) - - @property - def topic(self) -> str: - """topic that the message was published on. - - This property is read-only. - """ - return self._topic.decode('utf-8') - - @topic.setter - def topic(self, value: bytes) -> None: - self._topic = value - - -class Client: - """MQTT version 3.1/3.1.1/5.0 client class. - - This is the main class for use communicating with an MQTT broker. - - General usage flow: - - * Use `connect()`, `connect_async()` or `connect_srv()` to connect to a broker - * Use `loop_start()` to set a thread running to call `loop()` for you. - * Or use `loop_forever()` to handle calling `loop()` for you in a blocking function. - * Or call `loop()` frequently to maintain network traffic flow with the broker - * Use `subscribe()` to subscribe to a topic and receive messages - * Use `publish()` to send messages - * Use `disconnect()` to disconnect from the broker - - Data returned from the broker is made available with the use of callback - functions as described below. - - :param CallbackAPIVersion callback_api_version: define the API version for user-callback (on_connect, on_publish,...). - This field is required and it's recommended to use the latest version (CallbackAPIVersion.API_VERSION2). - See each callback for description of API for each version. The file docs/migrations.rst contains details on - how to migrate between version. - - :param str client_id: the unique client id string used when connecting to the - broker. If client_id is zero length or None, then the behaviour is - defined by which protocol version is in use. If using MQTT v3.1.1, then - a zero length client id will be sent to the broker and the broker will - generate a random for the client. If using MQTT v3.1 then an id will be - randomly generated. In both cases, clean_session must be True. If this - is not the case a ValueError will be raised. - - :param bool clean_session: a boolean that determines the client type. If True, - the broker will remove all information about this client when it - disconnects. If False, the client is a persistent client and - subscription information and queued messages will be retained when the - client disconnects. - Note that a client will never discard its own outgoing messages on - disconnect. Calling connect() or reconnect() will cause the messages to - be resent. Use reinitialise() to reset a client to its original state. - The clean_session argument only applies to MQTT versions v3.1.1 and v3.1. - It is not accepted if the MQTT version is v5.0 - use the clean_start - argument on connect() instead. - - :param userdata: user defined data of any type that is passed as the "userdata" - parameter to callbacks. It may be updated at a later point with the - user_data_set() function. - - :param int protocol: allows explicit setting of the MQTT version to - use for this client. Can be paho.mqtt.client.MQTTv311 (v3.1.1), - paho.mqtt.client.MQTTv31 (v3.1) or paho.mqtt.client.MQTTv5 (v5.0), - with the default being v3.1.1. - - :param transport: use "websockets" to use WebSockets as the transport - mechanism. Set to "tcp" to use raw TCP, which is the default. - Use "unix" to use Unix sockets as the transport mechanism; note that - this option is only available on platforms that support Unix sockets, - and the "host" argument is interpreted as the path to the Unix socket - file in this case. - - :param bool manual_ack: normally, when a message is received, the library automatically - acknowledges after on_message callback returns. manual_ack=True allows the application to - acknowledge receipt after it has completed processing of a message - using a the ack() method. This addresses vulnerability to message loss - if applications fails while processing a message, or while it pending - locally. - - Callbacks - ========= - - A number of callback functions are available to receive data back from the - broker. To use a callback, define a function and then assign it to the - client:: - - def on_connect(client, userdata, flags, reason_code, properties): - print(f"Connected with result code {reason_code}") - - client.on_connect = on_connect - - Callbacks can also be attached using decorators:: - - mqttc = paho.mqtt.Client() - - @mqttc.connect_callback() - def on_connect(client, userdata, flags, reason_code, properties): - print(f"Connected with result code {reason_code}") - - All of the callbacks as described below have a "client" and an "userdata" - argument. "client" is the `Client` instance that is calling the callback. - userdata" is user data of any type and can be set when creating a new client - instance or with `user_data_set()`. - - If you wish to suppress exceptions within a callback, you should set - ``mqttc.suppress_exceptions = True`` - - The callbacks are listed below, documentation for each of them can be found - at the same function name: - - `on_connect`, `on_connect_fail`, `on_disconnect`, `on_message`, `on_publish`, - `on_subscribe`, `on_unsubscribe`, `on_log`, `on_socket_open`, `on_socket_close`, - `on_socket_register_write`, `on_socket_unregister_write` - """ - - def __init__( - self, - callback_api_version: CallbackAPIVersion = CallbackAPIVersion.VERSION1, - client_id: str | None = "", - clean_session: bool | None = None, - userdata: Any = None, - protocol: MQTTProtocolVersion = MQTTv311, - transport: Literal["tcp", "websockets", "unix"] = "tcp", - reconnect_on_failure: bool = True, - manual_ack: bool = False, - ) -> None: - transport = transport.lower() # type: ignore - if transport == "unix" and not hasattr(socket, "AF_UNIX"): - raise ValueError('"unix" transport not supported') - elif transport not in ("websockets", "tcp", "unix"): - raise ValueError( - f'transport must be "websockets", "tcp" or "unix", not {transport}') - - self._manual_ack = manual_ack - self._transport = transport - self._protocol = protocol - self._userdata = userdata - self._sock: SocketLike | None = None - self._sockpairR: socket.socket | None = None - self._sockpairW: socket.socket | None = None - self._keepalive = 60 - self._connect_timeout = 5.0 - self._client_mode = MQTT_CLIENT - self._callback_api_version = callback_api_version - - if self._callback_api_version == CallbackAPIVersion.VERSION1: - warnings.warn( - "Callback API version 1 is deprecated, update to latest version", - category=DeprecationWarning, - stacklevel=2, - ) - if isinstance(self._callback_api_version, str): - # Help user to migrate, it probably provided a client id - # as first arguments - raise ValueError( - "Unsupported callback API version: version 2.0 added a callback_api_version, see docs/migrations.rst for details" - ) - if self._callback_api_version not in CallbackAPIVersion: - raise ValueError("Unsupported callback API version") - - self._clean_start: int = MQTT_CLEAN_START_FIRST_ONLY - - if protocol == MQTTv5: - if clean_session is not None: - raise ValueError('Clean session is not used for MQTT 5.0') - else: - if clean_session is None: - clean_session = True - if not clean_session and (client_id == "" or client_id is None): - raise ValueError( - 'A client id must be provided if clean session is False.') - self._clean_session = clean_session - - # [MQTT-3.1.3-4] Client Id must be UTF-8 encoded string. - if client_id == "" or client_id is None: - if protocol == MQTTv31: - self._client_id = _base62(uuid.uuid4().int, padding=22).encode("utf8") - else: - self._client_id = b"" - else: - self._client_id = _force_bytes(client_id) - - self._username: bytes | None = None - self._password: bytes | None = None - self._in_packet: _InPacket = { - "command": 0, - "have_remaining": 0, - "remaining_count": [], - "remaining_mult": 1, - "remaining_length": 0, - "packet": bytearray(b""), - "to_process": 0, - "pos": 0, - } - self._out_packet: collections.deque[_OutPacket] = collections.deque() - self._last_msg_in = time_func() - self._last_msg_out = time_func() - self._reconnect_min_delay = 1 - self._reconnect_max_delay = 120 - self._reconnect_delay: int | None = None - self._reconnect_on_failure = reconnect_on_failure - self._ping_t = 0.0 - self._last_mid = 0 - self._state = _ConnectionState.MQTT_CS_NEW - self._out_messages: collections.OrderedDict[ - int, MQTTMessage - ] = collections.OrderedDict() - self._in_messages: collections.OrderedDict[ - int, MQTTMessage - ] = collections.OrderedDict() - self._max_inflight_messages = 20 - self._inflight_messages = 0 - self._max_queued_messages = 0 - self._connect_properties: Properties | None = None - self._will_properties: Properties | None = None - self._will = False - self._will_topic = b"" - self._will_payload = b"" - self._will_qos = 0 - self._will_retain = False - self._on_message_filtered = MQTTMatcher() - self._host = "" - self._port = 1883 - self._bind_address = "" - self._bind_port = 0 - self._proxy: Any = {} - self._in_callback_mutex = threading.Lock() - self._callback_mutex = threading.RLock() - self._msgtime_mutex = threading.Lock() - self._out_message_mutex = threading.RLock() - self._in_message_mutex = threading.Lock() - self._reconnect_delay_mutex = threading.Lock() - self._mid_generate_mutex = threading.Lock() - self._thread: threading.Thread | None = None - self._thread_terminate = False - self._ssl = False - self._ssl_context: ssl.SSLContext | None = None - # Only used when SSL context does not have check_hostname attribute - self._tls_insecure = False - self._logger: logging.Logger | None = None - self._registered_write = False - # No default callbacks - self._on_log: CallbackOnLog | None = None - self._on_pre_connect: CallbackOnPreConnect | None = None - self._on_connect: CallbackOnConnect | None = None - self._on_connect_fail: CallbackOnConnectFail | None = None - self._on_subscribe: CallbackOnSubscribe | None = None - self._on_message: CallbackOnMessage | None = None - self._on_publish: CallbackOnPublish | None = None - self._on_unsubscribe: CallbackOnUnsubscribe | None = None - self._on_disconnect: CallbackOnDisconnect | None = None - self._on_socket_open: CallbackOnSocket | None = None - self._on_socket_close: CallbackOnSocket | None = None - self._on_socket_register_write: CallbackOnSocket | None = None - self._on_socket_unregister_write: CallbackOnSocket | None = None - self._websocket_path = "/mqtt" - self._websocket_extra_headers: WebSocketHeaders | None = None - # for clean_start == MQTT_CLEAN_START_FIRST_ONLY - self._mqttv5_first_connect = True - self.suppress_exceptions = False # For callbacks - - def __del__(self) -> None: - self._reset_sockets() - - @property - def host(self) -> str: - """ - Host to connect to. If `connect()` hasn't been called yet, returns an empty string. - - This property may not be changed if the connection is already open. - """ - return self._host - - @host.setter - def host(self, value: str) -> None: - if not self._connection_closed(): - raise RuntimeError("updating host on established connection is not supported") - - if not value: - raise ValueError("Invalid host.") - self._host = value - - @property - def port(self) -> int: - """ - Broker TCP port to connect to. - - This property may not be changed if the connection is already open. - """ - return self._port - - @port.setter - def port(self, value: int) -> None: - if not self._connection_closed(): - raise RuntimeError("updating port on established connection is not supported") - - if value <= 0: - raise ValueError("Invalid port number.") - self._port = value - - @property - def keepalive(self) -> int: - """ - Client keepalive interval (in seconds). - - This property may not be changed if the connection is already open. - """ - return self._keepalive - - @keepalive.setter - def keepalive(self, value: int) -> None: - if not self._connection_closed(): - # The issue here is that the previous value of keepalive matter to possibly - # sent ping packet. - raise RuntimeError("updating keepalive on established connection is not supported") - - if value < 0: - raise ValueError("Keepalive must be >=0.") - - self._keepalive = value - - @property - def transport(self) -> Literal["tcp", "websockets", "unix"]: - """ - Transport method used for the connection ("tcp" or "websockets"). - - This property may not be changed if the connection is already open. - """ - return self._transport - - @transport.setter - def transport(self, value: Literal["tcp", "websockets"]) -> None: - if not self._connection_closed(): - raise RuntimeError("updating transport on established connection is not supported") - - self._transport = value - - @property - def protocol(self) -> MQTTProtocolVersion: - """ - Protocol version used (MQTT v3, MQTT v3.11, MQTTv5) - - This property is read-only. - """ - return self._protocol - - @property - def connect_timeout(self) -> float: - """ - Connection establishment timeout in seconds. - - This property may not be changed if the connection is already open. - """ - return self._connect_timeout - - @connect_timeout.setter - def connect_timeout(self, value: float) -> None: - if not self._connection_closed(): - raise RuntimeError("updating connect_timeout on established connection is not supported") - - if value <= 0.0: - raise ValueError("timeout must be a positive number") - - self._connect_timeout = value - - @property - def username(self) -> str | None: - """The username used to connect to the MQTT broker, or None if no username is used. - - This property may not be changed if the connection is already open. - """ - if self._username is None: - return None - return self._username.decode("utf-8") - - @username.setter - def username(self, value: str | None) -> None: - if not self._connection_closed(): - raise RuntimeError("updating username on established connection is not supported") - - if value is None: - self._username = None - else: - self._username = value.encode("utf-8") - - @property - def password(self) -> str | None: - """The password used to connect to the MQTT broker, or None if no password is used. - - This property may not be changed if the connection is already open. - """ - if self._password is None: - return None - return self._password.decode("utf-8") - - @password.setter - def password(self, value: str | None) -> None: - if not self._connection_closed(): - raise RuntimeError("updating password on established connection is not supported") - - if value is None: - self._password = None - else: - self._password = value.encode("utf-8") - - @property - def max_inflight_messages(self) -> int: - """ - Maximum number of messages with QoS > 0 that can be partway through the network flow at once - - This property may not be changed if the connection is already open. - """ - return self._max_inflight_messages - - @max_inflight_messages.setter - def max_inflight_messages(self, value: int) -> None: - if not self._connection_closed(): - # Not tested. Some doubt that everything is okay when max_inflight change between 0 - # and > 0 value because _update_inflight is skipped when _max_inflight_messages == 0 - raise RuntimeError("updating max_inflight_messages on established connection is not supported") - - if value < 0: - raise ValueError("Invalid inflight.") - - self._max_inflight_messages = value - - @property - def max_queued_messages(self) -> int: - """ - Maximum number of message in the outgoing message queue, 0 means unlimited - - This property may not be changed if the connection is already open. - """ - return self._max_queued_messages - - @max_queued_messages.setter - def max_queued_messages(self, value: int) -> None: - if not self._connection_closed(): - # Not tested. - raise RuntimeError("updating max_queued_messages on established connection is not supported") - - if value < 0: - raise ValueError("Invalid queue size.") - - self._max_queued_messages = value - - @property - def will_topic(self) -> str | None: - """ - The topic name a will message is sent to when disconnecting unexpectedly. None if a will shall not be sent. - - This property is read-only. Use `will_set()` to change its value. - """ - if self._will_topic is None: - return None - - return self._will_topic.decode("utf-8") - - @property - def will_payload(self) -> bytes | None: - """ - The payload for the will message that is sent when disconnecting unexpectedly. None if a will shall not be sent. - - This property is read-only. Use `will_set()` to change its value. - """ - return self._will_payload - - @property - def logger(self) -> logging.Logger | None: - return self._logger - - @logger.setter - def logger(self, value: logging.Logger | None) -> None: - self._logger = value - - def _sock_recv(self, bufsize: int) -> bytes: - if self._sock is None: - raise ConnectionError("self._sock is None") - try: - return self._sock.recv(bufsize) - except ssl.SSLWantReadError as err: - raise BlockingIOError() from err - except ssl.SSLWantWriteError as err: - self._call_socket_register_write() - raise BlockingIOError() from err - except AttributeError as err: - self._easy_log( - MQTT_LOG_DEBUG, "socket was None: %s", err) - raise ConnectionError() from err - - def _sock_send(self, buf: bytes) -> int: - if self._sock is None: - raise ConnectionError("self._sock is None") - - try: - return self._sock.send(buf) - except ssl.SSLWantReadError as err: - raise BlockingIOError() from err - except ssl.SSLWantWriteError as err: - self._call_socket_register_write() - raise BlockingIOError() from err - except BlockingIOError as err: - self._call_socket_register_write() - raise BlockingIOError() from err - - def _sock_close(self) -> None: - """Close the connection to the server.""" - if not self._sock: - return - - try: - sock = self._sock - self._sock = None - self._call_socket_unregister_write(sock) - self._call_socket_close(sock) - finally: - # In case a callback fails, still close the socket to avoid leaking the file descriptor. - sock.close() - - def _reset_sockets(self, sockpair_only: bool = False) -> None: - if not sockpair_only: - self._sock_close() - - if self._sockpairR: - self._sockpairR.close() - self._sockpairR = None - if self._sockpairW: - self._sockpairW.close() - self._sockpairW = None - - def reinitialise( - self, - client_id: str = "", - clean_session: bool = True, - userdata: Any = None, - ) -> None: - self._reset_sockets() - - self.__init__(client_id, clean_session, userdata) # type: ignore[misc] - - def ws_set_options( - self, - path: str = "/mqtt", - headers: WebSocketHeaders | None = None, - ) -> None: - """ Set the path and headers for a websocket connection - - :param str path: a string starting with / which should be the endpoint of the - mqtt connection on the remote server - - :param headers: can be either a dict or a callable object. If it is a dict then - the extra items in the dict are added to the websocket headers. If it is - a callable, then the default websocket headers are passed into this - function and the result is used as the new headers. - """ - self._websocket_path = path - - if headers is not None: - if isinstance(headers, dict) or callable(headers): - self._websocket_extra_headers = headers - else: - raise ValueError( - "'headers' option to ws_set_options has to be either a dictionary or callable") - - def tls_set_context( - self, - context: ssl.SSLContext | None = None, - ) -> None: - """Configure network encryption and authentication context. Enables SSL/TLS support. - - :param context: an ssl.SSLContext object. By default this is given by - ``ssl.create_default_context()``, if available. - - Must be called before `connect()`, `connect_async()` or `connect_srv()`.""" - if self._ssl_context is not None: - raise ValueError('SSL/TLS has already been configured.') - - if context is None: - context = ssl.create_default_context() - - self._ssl = True - self._ssl_context = context - - # Ensure _tls_insecure is consistent with check_hostname attribute - if hasattr(context, 'check_hostname'): - self._tls_insecure = not context.check_hostname - - def tls_set( - self, - ca_certs: str | None = None, - certfile: str | None = None, - keyfile: str | None = None, - cert_reqs: ssl.VerifyMode | None = None, - tls_version: int | None = None, - ciphers: str | None = None, - keyfile_password: str | None = None, - alpn_protocols: list[str] | None = None, - ) -> None: - """Configure network encryption and authentication options. Enables SSL/TLS support. - - :param str ca_certs: a string path to the Certificate Authority certificate files - that are to be treated as trusted by this client. If this is the only - option given then the client will operate in a similar manner to a web - browser. That is to say it will require the broker to have a - certificate signed by the Certificate Authorities in ca_certs and will - communicate using TLS v1,2, but will not attempt any form of - authentication. This provides basic network encryption but may not be - sufficient depending on how the broker is configured. - - By default, on Python 2.7.9+ or 3.4+, the default certification - authority of the system is used. On older Python version this parameter - is mandatory. - :param str certfile: PEM encoded client certificate filename. Used with - keyfile for client TLS based authentication. Support for this feature is - broker dependent. Note that if the files in encrypted and needs a password to - decrypt it, then this can be passed using the keyfile_password argument - you - should take precautions to ensure that your password is - not hard coded into your program by loading the password from a file - for example. If you do not provide keyfile_password, the password will - be requested to be typed in at a terminal window. - :param str keyfile: PEM encoded client private keys filename. Used with - certfile for client TLS based authentication. Support for this feature is - broker dependent. Note that if the files in encrypted and needs a password to - decrypt it, then this can be passed using the keyfile_password argument - you - should take precautions to ensure that your password is - not hard coded into your program by loading the password from a file - for example. If you do not provide keyfile_password, the password will - be requested to be typed in at a terminal window. - :param cert_reqs: the certificate requirements that the client imposes - on the broker to be changed. By default this is ssl.CERT_REQUIRED, - which means that the broker must provide a certificate. See the ssl - pydoc for more information on this parameter. - :param tls_version: the version of the SSL/TLS protocol used to be - specified. By default TLS v1.2 is used. Previous versions are allowed - but not recommended due to possible security problems. - :param str ciphers: encryption ciphers that are allowed - for this connection, or None to use the defaults. See the ssl pydoc for - more information. - - Must be called before `connect()`, `connect_async()` or `connect_srv()`.""" - if ssl is None: - raise ValueError('This platform has no SSL/TLS.') - - if not hasattr(ssl, 'SSLContext'): - # Require Python version that has SSL context support in standard library - raise ValueError( - 'Python 2.7.9 and 3.2 are the minimum supported versions for TLS.') - - if ca_certs is None and not hasattr(ssl.SSLContext, 'load_default_certs'): - raise ValueError('ca_certs must not be None.') - - # Create SSLContext object - if tls_version is None: - tls_version = ssl.PROTOCOL_TLSv1_2 - # If the python version supports it, use highest TLS version automatically - if hasattr(ssl, "PROTOCOL_TLS_CLIENT"): - # This also enables CERT_REQUIRED and check_hostname by default. - tls_version = ssl.PROTOCOL_TLS_CLIENT - elif hasattr(ssl, "PROTOCOL_TLS"): - tls_version = ssl.PROTOCOL_TLS - context = ssl.SSLContext(tls_version) - - # Configure context - if ciphers is not None: - context.set_ciphers(ciphers) - - if certfile is not None: - context.load_cert_chain(certfile, keyfile, keyfile_password) - - if cert_reqs == ssl.CERT_NONE and hasattr(context, 'check_hostname'): - context.check_hostname = False - - context.verify_mode = ssl.CERT_REQUIRED if cert_reqs is None else cert_reqs - - if ca_certs is not None: - context.load_verify_locations(ca_certs) - else: - context.load_default_certs() - - if alpn_protocols is not None: - if not getattr(ssl, "HAS_ALPN", None): - raise ValueError("SSL library has no support for ALPN") - context.set_alpn_protocols(alpn_protocols) - - self.tls_set_context(context) - - if cert_reqs != ssl.CERT_NONE: - # Default to secure, sets context.check_hostname attribute - # if available - self.tls_insecure_set(False) - else: - # But with ssl.CERT_NONE, we can not check_hostname - self.tls_insecure_set(True) - - def tls_insecure_set(self, value: bool) -> None: - """Configure verification of the server hostname in the server certificate. - - If value is set to true, it is impossible to guarantee that the host - you are connecting to is not impersonating your server. This can be - useful in initial server testing, but makes it possible for a malicious - third party to impersonate your server through DNS spoofing, for - example. - - Do not use this function in a real system. Setting value to true means - there is no point using encryption. - - Must be called before `connect()` and after either `tls_set()` or - `tls_set_context()`.""" - - if self._ssl_context is None: - raise ValueError( - 'Must configure SSL context before using tls_insecure_set.') - - self._tls_insecure = value - - # Ensure check_hostname is consistent with _tls_insecure attribute - if hasattr(self._ssl_context, 'check_hostname'): - # Rely on SSLContext to check host name - # If verify_mode is CERT_NONE then the host name will never be checked - self._ssl_context.check_hostname = not value - - def proxy_set(self, **proxy_args: Any) -> None: - """Configure proxying of MQTT connection. Enables support for SOCKS or - HTTP proxies. - - Proxying is done through the PySocks library. Brief descriptions of the - proxy_args parameters are below; see the PySocks docs for more info. - - (Required) - - :param proxy_type: One of {socks.HTTP, socks.SOCKS4, or socks.SOCKS5} - :param proxy_addr: IP address or DNS name of proxy server - - (Optional) - - :param proxy_port: (int) port number of the proxy server. If not provided, - the PySocks package default value will be utilized, which differs by proxy_type. - :param proxy_rdns: boolean indicating whether proxy lookup should be performed - remotely (True, default) or locally (False) - :param proxy_username: username for SOCKS5 proxy, or userid for SOCKS4 proxy - :param proxy_password: password for SOCKS5 proxy - - Example:: - - mqttc.proxy_set(proxy_type=socks.HTTP, proxy_addr='1.2.3.4', proxy_port=4231) - """ - if socks is None: - raise ValueError("PySocks must be installed for proxy support.") - elif not self._proxy_is_valid(proxy_args): - raise ValueError("proxy_type and/or proxy_addr are invalid.") - else: - self._proxy = proxy_args - - def enable_logger(self, logger: logging.Logger | None = None) -> None: - """ - Enables a logger to send log messages to - - :param logging.Logger logger: if specified, that ``logging.Logger`` object will be used, otherwise - one will be created automatically. - - See `disable_logger` to undo this action. - """ - if logger is None: - if self._logger is not None: - # Do not replace existing logger - return - logger = logging.getLogger(__name__) - self.logger = logger - - def disable_logger(self) -> None: - """ - Disable logging using standard python logging package. This has no effect on the `on_log` callback. - """ - self._logger = None - - def connect( - self, - host: str, - port: int = 1883, - keepalive: int = 60, - bind_address: str = "", - bind_port: int = 0, - clean_start: CleanStartOption = MQTT_CLEAN_START_FIRST_ONLY, - properties: Properties | None = None, - ) -> MQTTErrorCode: - """Connect to a remote broker. This is a blocking call that establishes - the underlying connection and transmits a CONNECT packet. - Note that the connection status will not be updated until a CONNACK is received and - processed (this requires a running network loop, see `loop_start`, `loop_forever`, `loop`...). - - :param str host: the hostname or IP address of the remote broker. - :param int port: the network port of the server host to connect to. Defaults to - 1883. Note that the default port for MQTT over SSL/TLS is 8883 so if you - are using `tls_set()` the port may need providing. - :param int keepalive: Maximum period in seconds between communications with the - broker. If no other messages are being exchanged, this controls the - rate at which the client will send ping messages to the broker. - :param bool clean_start: (MQTT v5.0 only) True, False or MQTT_CLEAN_START_FIRST_ONLY. - Sets the MQTT v5.0 clean_start flag always, never or on the first successful connect only, - respectively. MQTT session data (such as outstanding messages and subscriptions) - is cleared on successful connect when the clean_start flag is set. - For MQTT v3.1.1, the ``clean_session`` argument of `Client` should be used for similar - result. - :param Properties properties: (MQTT v5.0 only) the MQTT v5.0 properties to be sent in the - MQTT connect packet. - """ - - if self._protocol == MQTTv5: - self._mqttv5_first_connect = True - else: - if clean_start != MQTT_CLEAN_START_FIRST_ONLY: - raise ValueError("Clean start only applies to MQTT V5") - if properties: - raise ValueError("Properties only apply to MQTT V5") - - self.connect_async(host, port, keepalive, - bind_address, bind_port, clean_start, properties) - return self.reconnect() - - def connect_srv( - self, - domain: str | None = None, - keepalive: int = 60, - bind_address: str = "", - bind_port: int = 0, - clean_start: CleanStartOption = MQTT_CLEAN_START_FIRST_ONLY, - properties: Properties | None = None, - ) -> MQTTErrorCode: - """Connect to a remote broker. - - :param str domain: the DNS domain to search for SRV records; if None, - try to determine local domain name. - :param keepalive, bind_address, clean_start and properties: see `connect()` - """ - - if HAVE_DNS is False: - raise ValueError( - 'No DNS resolver library found, try "pip install dnspython".') - - if domain is None: - domain = socket.getfqdn() - domain = domain[domain.find('.') + 1:] - - try: - rr = f'_mqtt._tcp.{domain}' - if self._ssl: - # IANA specifies secure-mqtt (not mqtts) for port 8883 - rr = f'_secure-mqtt._tcp.{domain}' - answers = [] - for answer in dns.resolver.query(rr, dns.rdatatype.SRV): - addr = answer.target.to_text()[:-1] - answers.append( - (addr, answer.port, answer.priority, answer.weight)) - except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer, dns.resolver.NoNameservers) as err: - raise ValueError(f"No answer/NXDOMAIN for SRV in {domain}") from err - - # FIXME: doesn't account for weight - for answer in answers: - host, port, prio, weight = answer - - try: - return self.connect(host, port, keepalive, bind_address, bind_port, clean_start, properties) - except Exception: # noqa: S110 - pass - - raise ValueError("No SRV hosts responded") - - def connect_async( - self, - host: str, - port: int = 1883, - keepalive: int = 60, - bind_address: str = "", - bind_port: int = 0, - clean_start: CleanStartOption = MQTT_CLEAN_START_FIRST_ONLY, - properties: Properties | None = None, - ) -> None: - """Connect to a remote broker asynchronously. This is a non-blocking - connect call that can be used with `loop_start()` to provide very quick - start. - - Any already established connection will be terminated immediately. - - :param str host: the hostname or IP address of the remote broker. - :param int port: the network port of the server host to connect to. Defaults to - 1883. Note that the default port for MQTT over SSL/TLS is 8883 so if you - are using `tls_set()` the port may need providing. - :param int keepalive: Maximum period in seconds between communications with the - broker. If no other messages are being exchanged, this controls the - rate at which the client will send ping messages to the broker. - :param bool clean_start: (MQTT v5.0 only) True, False or MQTT_CLEAN_START_FIRST_ONLY. - Sets the MQTT v5.0 clean_start flag always, never or on the first successful connect only, - respectively. MQTT session data (such as outstanding messages and subscriptions) - is cleared on successful connect when the clean_start flag is set. - For MQTT v3.1.1, the ``clean_session`` argument of `Client` should be used for similar - result. - :param Properties properties: (MQTT v5.0 only) the MQTT v5.0 properties to be sent in the - MQTT connect packet. - """ - if bind_port < 0: - raise ValueError('Invalid bind port number.') - - # Switch to state NEW to allow update of host, port & co. - self._sock_close() - self._state = _ConnectionState.MQTT_CS_NEW - - self.host = host - self.port = port - self.keepalive = keepalive - self._bind_address = bind_address - self._bind_port = bind_port - self._clean_start = clean_start - self._connect_properties = properties - self._state = _ConnectionState.MQTT_CS_CONNECT_ASYNC - - def reconnect_delay_set(self, min_delay: int = 1, max_delay: int = 120) -> None: - """ Configure the exponential reconnect delay - - When connection is lost, wait initially min_delay seconds and - double this time every attempt. The wait is capped at max_delay. - Once the client is fully connected (e.g. not only TCP socket, but - received a success CONNACK), the wait timer is reset to min_delay. - """ - with self._reconnect_delay_mutex: - self._reconnect_min_delay = min_delay - self._reconnect_max_delay = max_delay - self._reconnect_delay = None - - def reconnect(self) -> MQTTErrorCode: - """Reconnect the client after a disconnect. Can only be called after - connect()/connect_async().""" - if len(self._host) == 0: - raise ValueError('Invalid host.') - if self._port <= 0: - raise ValueError('Invalid port number.') - - self._in_packet = { - "command": 0, - "have_remaining": 0, - "remaining_count": [], - "remaining_mult": 1, - "remaining_length": 0, - "packet": bytearray(b""), - "to_process": 0, - "pos": 0, - } - - self._ping_t = 0.0 - self._state = _ConnectionState.MQTT_CS_CONNECTING - - self._sock_close() - - # Mark all currently outgoing QoS = 0 packets as lost, - # or `wait_for_publish()` could hang forever - for pkt in self._out_packet: - if pkt["command"] & 0xF0 == PUBLISH and pkt["qos"] == 0 and pkt["info"] is not None: - pkt["info"].rc = MQTT_ERR_CONN_LOST - pkt["info"]._set_as_published() - - self._out_packet.clear() - - with self._msgtime_mutex: - self._last_msg_in = time_func() - self._last_msg_out = time_func() - - # Put messages in progress in a valid state. - self._messages_reconnect_reset() - - with self._callback_mutex: - on_pre_connect = self.on_pre_connect - - if on_pre_connect: - try: - on_pre_connect(self, self._userdata) - except Exception as err: - self._easy_log( - MQTT_LOG_ERR, 'Caught exception in on_pre_connect: %s', err) - if not self.suppress_exceptions: - raise - - self._sock = self._create_socket() - - self._sock.setblocking(False) # type: ignore[attr-defined] - self._registered_write = False - self._call_socket_open(self._sock) - - return self._send_connect(self._keepalive) - - def loop(self, timeout: float = 1.0) -> MQTTErrorCode: - """Process network events. - - It is strongly recommended that you use `loop_start()`, or - `loop_forever()`, or if you are using an external event loop using - `loop_read()`, `loop_write()`, and `loop_misc()`. Using loop() on it's own is - no longer recommended. - - This function must be called regularly to ensure communication with the - broker is carried out. It calls select() on the network socket to wait - for network events. If incoming data is present it will then be - processed. Outgoing commands, from e.g. `publish()`, are normally sent - immediately that their function is called, but this is not always - possible. loop() will also attempt to send any remaining outgoing - messages, which also includes commands that are part of the flow for - messages with QoS>0. - - :param int timeout: The time in seconds to wait for incoming/outgoing network - traffic before timing out and returning. - - Returns MQTT_ERR_SUCCESS on success. - Returns >0 on error. - - A ValueError will be raised if timeout < 0""" - - if self._sockpairR is None or self._sockpairW is None: - self._reset_sockets(sockpair_only=True) - self._sockpairR, self._sockpairW = _socketpair_compat() - - return self._loop(timeout) - - def _loop(self, timeout: float = 1.0) -> MQTTErrorCode: - if timeout < 0.0: - raise ValueError('Invalid timeout.') - - if self.want_write(): - wlist = [self._sock] - else: - wlist = [] - - # used to check if there are any bytes left in the (SSL) socket - pending_bytes = 0 - if hasattr(self._sock, 'pending'): - pending_bytes = self._sock.pending() # type: ignore[union-attr] - - # if bytes are pending do not wait in select - if pending_bytes > 0: - timeout = 0.0 - - # sockpairR is used to break out of select() before the timeout, on a - # call to publish() etc. - if self._sockpairR is None: - rlist = [self._sock] - else: - rlist = [self._sock, self._sockpairR] - - try: - socklist = select.select(rlist, wlist, [], timeout) - except TypeError: - # Socket isn't correct type, in likelihood connection is lost - # ... or we called disconnect(). In that case the socket will - # be closed but some loop (like loop_forever) will continue to - # call _loop(). We still want to break that loop by returning an - # rc != MQTT_ERR_SUCCESS and we don't want state to change from - # mqtt_cs_disconnecting. - if self._state not in (_ConnectionState.MQTT_CS_DISCONNECTING, _ConnectionState.MQTT_CS_DISCONNECTED): - self._state = _ConnectionState.MQTT_CS_CONNECTION_LOST - return MQTTErrorCode.MQTT_ERR_CONN_LOST - except ValueError: - # Can occur if we just reconnected but rlist/wlist contain a -1 for - # some reason. - if self._state not in (_ConnectionState.MQTT_CS_DISCONNECTING, _ConnectionState.MQTT_CS_DISCONNECTED): - self._state = _ConnectionState.MQTT_CS_CONNECTION_LOST - return MQTTErrorCode.MQTT_ERR_CONN_LOST - except Exception: - # Note that KeyboardInterrupt, etc. can still terminate since they - # are not derived from Exception - return MQTTErrorCode.MQTT_ERR_UNKNOWN - - if self._sock in socklist[0] or pending_bytes > 0: - rc = self.loop_read() - if rc or self._sock is None: - return rc - - if self._sockpairR and self._sockpairR in socklist[0]: - # Stimulate output write even though we didn't ask for it, because - # at that point the publish or other command wasn't present. - socklist[1].insert(0, self._sock) - # Clear sockpairR - only ever a single byte written. - try: - # Read many bytes at once - this allows up to 10000 calls to - # publish() inbetween calls to loop(). - self._sockpairR.recv(10000) - except BlockingIOError: - pass - - if self._sock in socklist[1]: - rc = self.loop_write() - if rc or self._sock is None: - return rc - - return self.loop_misc() - - def publish( - self, - topic: str, - payload: PayloadType = None, - qos: int = 0, - retain: bool = False, - properties: Properties | None = None, - ) -> MQTTMessageInfo: - """Publish a message on a topic. - - This causes a message to be sent to the broker and subsequently from - the broker to any clients subscribing to matching topics. - - :param str topic: The topic that the message should be published on. - :param payload: The actual message to send. If not given, or set to None a - zero length message will be used. Passing an int or float will result - in the payload being converted to a string representing that number. If - you wish to send a true int/float, use struct.pack() to create the - payload you require. - :param int qos: The quality of service level to use. - :param bool retain: If set to true, the message will be set as the "last known - good"/retained message for the topic. - :param Properties properties: (MQTT v5.0 only) the MQTT v5.0 properties to be included. - - Returns a `MQTTMessageInfo` class, which can be used to determine whether - the message has been delivered (using `is_published()`) or to block - waiting for the message to be delivered (`wait_for_publish()`). The - message ID and return code of the publish() call can be found at - :py:attr:`info.mid ` and :py:attr:`info.rc `. - - For backwards compatibility, the `MQTTMessageInfo` class is iterable so - the old construct of ``(rc, mid) = client.publish(...)`` is still valid. - - rc is MQTT_ERR_SUCCESS to indicate success or MQTT_ERR_NO_CONN if the - client is not currently connected. mid is the message ID for the - publish request. The mid value can be used to track the publish request - by checking against the mid argument in the on_publish() callback if it - is defined. - - :raises ValueError: if topic is None, has zero length or is - invalid (contains a wildcard), except if the MQTT version used is v5.0. - For v5.0, a zero length topic can be used when a Topic Alias has been set. - :raises ValueError: if qos is not one of 0, 1 or 2 - :raises ValueError: if the length of the payload is greater than 268435455 bytes. - """ - if self._protocol != MQTTv5: - if topic is None or len(topic) == 0: - raise ValueError('Invalid topic.') - - topic_bytes = topic.encode('utf-8') - - self._raise_for_invalid_topic(topic_bytes) - - if qos < 0 or qos > 2: - raise ValueError('Invalid QoS level.') - - local_payload = _encode_payload(payload) - - if len(local_payload) > 268435455: - raise ValueError('Payload too large.') - - local_mid = self._mid_generate() - - if qos == 0: - info = MQTTMessageInfo(local_mid) - rc = self._send_publish( - local_mid, topic_bytes, local_payload, qos, retain, False, info, properties) - info.rc = rc - return info - else: - message = MQTTMessage(local_mid, topic_bytes) - message.timestamp = time_func() - message.payload = local_payload - message.qos = qos - message.retain = retain - message.dup = False - message.properties = properties - - with self._out_message_mutex: - if self._max_queued_messages > 0 and len(self._out_messages) >= self._max_queued_messages: - message.info.rc = MQTTErrorCode.MQTT_ERR_QUEUE_SIZE - return message.info - - if local_mid in self._out_messages: - message.info.rc = MQTTErrorCode.MQTT_ERR_QUEUE_SIZE - return message.info - - self._out_messages[message.mid] = message - if self._max_inflight_messages == 0 or self._inflight_messages < self._max_inflight_messages: - self._inflight_messages += 1 - if qos == 1: - message.state = mqtt_ms_wait_for_puback - elif qos == 2: - message.state = mqtt_ms_wait_for_pubrec - - rc = self._send_publish(message.mid, topic_bytes, message.payload, message.qos, message.retain, - message.dup, message.info, message.properties) - - # remove from inflight messages so it will be send after a connection is made - if rc == MQTTErrorCode.MQTT_ERR_NO_CONN: - self._inflight_messages -= 1 - message.state = mqtt_ms_publish - - message.info.rc = rc - return message.info - else: - message.state = mqtt_ms_queued - message.info.rc = MQTTErrorCode.MQTT_ERR_SUCCESS - return message.info - - def username_pw_set( - self, username: str | None, password: str | None = None - ) -> None: - """Set a username and optionally a password for broker authentication. - - Must be called before connect() to have any effect. - Requires a broker that supports MQTT v3.1 or more. - - :param str username: The username to authenticate with. Need have no relationship to the client id. Must be str - [MQTT-3.1.3-11]. - Set to None to reset client back to not using username/password for broker authentication. - :param str password: The password to authenticate with. Optional, set to None if not required. If it is str, then it - will be encoded as UTF-8. - """ - - # [MQTT-3.1.3-11] User name must be UTF-8 encoded string - self._username = None if username is None else username.encode('utf-8') - if isinstance(password, str): - self._password = password.encode('utf-8') - else: - self._password = password - - def enable_bridge_mode(self) -> None: - """Sets the client in a bridge mode instead of client mode. - - Must be called before `connect()` to have any effect. - Requires brokers that support bridge mode. - - Under bridge mode, the broker will identify the client as a bridge and - not send it's own messages back to it. Hence a subsciption of # is - possible without message loops. This feature also correctly propagates - the retain flag on the messages. - - Currently Mosquitto and RSMB support this feature. This feature can - be used to create a bridge between multiple broker. - """ - self._client_mode = MQTT_BRIDGE - - def _connection_closed(self) -> bool: - """ - Return true if the connection is closed (and not trying to be opened). - """ - return ( - self._state == _ConnectionState.MQTT_CS_NEW - or (self._state in (_ConnectionState.MQTT_CS_DISCONNECTING, _ConnectionState.MQTT_CS_DISCONNECTED) and self._sock is None)) - - def is_connected(self) -> bool: - """Returns the current status of the connection - - True if connection exists - False if connection is closed - """ - return self._state == _ConnectionState.MQTT_CS_CONNECTED - - def disconnect( - self, - reasoncode: ReasonCode | None = None, - properties: Properties | None = None, - ) -> MQTTErrorCode: - """Disconnect a connected client from the broker. - - :param ReasonCode reasoncode: (MQTT v5.0 only) a ReasonCode instance setting the MQTT v5.0 - reasoncode to be sent with the disconnect packet. It is optional, the receiver - then assuming that 0 (success) is the value. - :param Properties properties: (MQTT v5.0 only) a Properties instance setting the MQTT v5.0 properties - to be included. Optional - if not set, no properties are sent. - """ - if self._sock is None: - self._state = _ConnectionState.MQTT_CS_DISCONNECTED - return MQTT_ERR_NO_CONN - else: - self._state = _ConnectionState.MQTT_CS_DISCONNECTING - - return self._send_disconnect(reasoncode, properties) - - def subscribe( - self, - topic: str | tuple[str, int] | tuple[str, SubscribeOptions] | list[tuple[str, int]] | list[tuple[str, SubscribeOptions]], - qos: int = 0, - options: SubscribeOptions | None = None, - properties: Properties | None = None, - ) -> tuple[MQTTErrorCode, int | None]: - """Subscribe the client to one or more topics. - - This function may be called in three different ways (and a further three for MQTT v5.0): - - Simple string and integer - ------------------------- - e.g. subscribe("my/topic", 2) - - :topic: A string specifying the subscription topic to subscribe to. - :qos: The desired quality of service level for the subscription. - Defaults to 0. - :options and properties: Not used. - - Simple string and subscribe options (MQTT v5.0 only) - ---------------------------------------------------- - e.g. subscribe("my/topic", options=SubscribeOptions(qos=2)) - - :topic: A string specifying the subscription topic to subscribe to. - :qos: Not used. - :options: The MQTT v5.0 subscribe options. - :properties: a Properties instance setting the MQTT v5.0 properties - to be included. Optional - if not set, no properties are sent. - - String and integer tuple - ------------------------ - e.g. subscribe(("my/topic", 1)) - - :topic: A tuple of (topic, qos). Both topic and qos must be present in - the tuple. - :qos and options: Not used. - :properties: Only used for MQTT v5.0. A Properties instance setting the - MQTT v5.0 properties. Optional - if not set, no properties are sent. - - String and subscribe options tuple (MQTT v5.0 only) - --------------------------------------------------- - e.g. subscribe(("my/topic", SubscribeOptions(qos=1))) - - :topic: A tuple of (topic, SubscribeOptions). Both topic and subscribe - options must be present in the tuple. - :qos and options: Not used. - :properties: a Properties instance setting the MQTT v5.0 properties - to be included. Optional - if not set, no properties are sent. - - List of string and integer tuples - --------------------------------- - e.g. subscribe([("my/topic", 0), ("another/topic", 2)]) - - This allows multiple topic subscriptions in a single SUBSCRIPTION - command, which is more efficient than using multiple calls to - subscribe(). - - :topic: A list of tuple of format (topic, qos). Both topic and qos must - be present in all of the tuples. - :qos, options and properties: Not used. - - List of string and subscribe option tuples (MQTT v5.0 only) - ----------------------------------------------------------- - e.g. subscribe([("my/topic", SubscribeOptions(qos=0), ("another/topic", SubscribeOptions(qos=2)]) - - This allows multiple topic subscriptions in a single SUBSCRIPTION - command, which is more efficient than using multiple calls to - subscribe(). - - :topic: A list of tuple of format (topic, SubscribeOptions). Both topic and subscribe - options must be present in all of the tuples. - :qos and options: Not used. - :properties: a Properties instance setting the MQTT v5.0 properties - to be included. Optional - if not set, no properties are sent. - - The function returns a tuple (result, mid), where result is - MQTT_ERR_SUCCESS to indicate success or (MQTT_ERR_NO_CONN, None) if the - client is not currently connected. mid is the message ID for the - subscribe request. The mid value can be used to track the subscribe - request by checking against the mid argument in the on_subscribe() - callback if it is defined. - - Raises a ValueError if qos is not 0, 1 or 2, or if topic is None or has - zero string length, or if topic is not a string, tuple or list. - """ - topic_qos_list = None - - if isinstance(topic, tuple): - if self._protocol == MQTTv5: - topic, options = topic # type: ignore - if not isinstance(options, SubscribeOptions): - raise ValueError( - 'Subscribe options must be instance of SubscribeOptions class.') - else: - topic, qos = topic # type: ignore - - if isinstance(topic, (bytes, str)): - if qos < 0 or qos > 2: - raise ValueError('Invalid QoS level.') - if self._protocol == MQTTv5: - if options is None: - # if no options are provided, use the QoS passed instead - options = SubscribeOptions(qos=qos) - elif qos != 0: - raise ValueError( - 'Subscribe options and qos parameters cannot be combined.') - if not isinstance(options, SubscribeOptions): - raise ValueError( - 'Subscribe options must be instance of SubscribeOptions class.') - topic_qos_list = [(topic.encode('utf-8'), options)] - else: - if topic is None or len(topic) == 0: - raise ValueError('Invalid topic.') - topic_qos_list = [(topic.encode('utf-8'), qos)] # type: ignore - elif isinstance(topic, list): - if len(topic) == 0: - raise ValueError('Empty topic list') - topic_qos_list = [] - if self._protocol == MQTTv5: - for t, o in topic: - if not isinstance(o, SubscribeOptions): - # then the second value should be QoS - if o < 0 or o > 2: - raise ValueError('Invalid QoS level.') - o = SubscribeOptions(qos=o) - topic_qos_list.append((t.encode('utf-8'), o)) - else: - for t, q in topic: - if isinstance(q, SubscribeOptions) or q < 0 or q > 2: - raise ValueError('Invalid QoS level.') - if t is None or len(t) == 0 or not isinstance(t, (bytes, str)): - raise ValueError('Invalid topic.') - topic_qos_list.append((t.encode('utf-8'), q)) # type: ignore - - if topic_qos_list is None: - raise ValueError("No topic specified, or incorrect topic type.") - - if any(self._filter_wildcard_len_check(topic) != MQTT_ERR_SUCCESS for topic, _ in topic_qos_list): - raise ValueError('Invalid subscription filter.') - - if self._sock is None: - return (MQTT_ERR_NO_CONN, None) - - return self._send_subscribe(False, topic_qos_list, properties) - - def unsubscribe( - self, topic: str | list[str], properties: Properties | None = None - ) -> tuple[MQTTErrorCode, int | None]: - """Unsubscribe the client from one or more topics. - - :param topic: A single string, or list of strings that are the subscription - topics to unsubscribe from. - :param properties: (MQTT v5.0 only) a Properties instance setting the MQTT v5.0 properties - to be included. Optional - if not set, no properties are sent. - - Returns a tuple (result, mid), where result is MQTT_ERR_SUCCESS - to indicate success or (MQTT_ERR_NO_CONN, None) if the client is not - currently connected. - mid is the message ID for the unsubscribe request. The mid value can be - used to track the unsubscribe request by checking against the mid - argument in the on_unsubscribe() callback if it is defined. - - :raises ValueError: if topic is None or has zero string length, or is - not a string or list. - """ - topic_list = None - if topic is None: - raise ValueError('Invalid topic.') - if isinstance(topic, (bytes, str)): - if len(topic) == 0: - raise ValueError('Invalid topic.') - topic_list = [topic.encode('utf-8')] - elif isinstance(topic, list): - topic_list = [] - for t in topic: - if len(t) == 0 or not isinstance(t, (bytes, str)): - raise ValueError('Invalid topic.') - topic_list.append(t.encode('utf-8')) - - if topic_list is None: - raise ValueError("No topic specified, or incorrect topic type.") - - if self._sock is None: - return (MQTTErrorCode.MQTT_ERR_NO_CONN, None) - - return self._send_unsubscribe(False, topic_list, properties) - - def loop_read(self, max_packets: int = 1) -> MQTTErrorCode: - """Process read network events. Use in place of calling `loop()` if you - wish to handle your client reads as part of your own application. - - Use `socket()` to obtain the client socket to call select() or equivalent - on. - - Do not use if you are using `loop_start()` or `loop_forever()`.""" - if self._sock is None: - return MQTTErrorCode.MQTT_ERR_NO_CONN - - max_packets = len(self._out_messages) + len(self._in_messages) - if max_packets < 1: - max_packets = 1 - - for _ in range(0, max_packets): - if self._sock is None: - return MQTTErrorCode.MQTT_ERR_NO_CONN - rc = self._packet_read() - if rc > 0: - return self._loop_rc_handle(rc) - elif rc == MQTTErrorCode.MQTT_ERR_AGAIN: - return MQTTErrorCode.MQTT_ERR_SUCCESS - return MQTTErrorCode.MQTT_ERR_SUCCESS - - def loop_write(self) -> MQTTErrorCode: - """Process write network events. Use in place of calling `loop()` if you - wish to handle your client writes as part of your own application. - - Use `socket()` to obtain the client socket to call select() or equivalent - on. - - Use `want_write()` to determine if there is data waiting to be written. - - Do not use if you are using `loop_start()` or `loop_forever()`.""" - if self._sock is None: - return MQTTErrorCode.MQTT_ERR_NO_CONN - - try: - rc = self._packet_write() - if rc == MQTTErrorCode.MQTT_ERR_AGAIN: - return MQTTErrorCode.MQTT_ERR_SUCCESS - elif rc > 0: - return self._loop_rc_handle(rc) - else: - return MQTTErrorCode.MQTT_ERR_SUCCESS - finally: - if self.want_write(): - self._call_socket_register_write() - else: - self._call_socket_unregister_write() - - def want_write(self) -> bool: - """Call to determine if there is network data waiting to be written. - Useful if you are calling select() yourself rather than using `loop()`, `loop_start()` or `loop_forever()`. - """ - return len(self._out_packet) > 0 - - def loop_misc(self) -> MQTTErrorCode: - """Process miscellaneous network events. Use in place of calling `loop()` if you - wish to call select() or equivalent on. - - Do not use if you are using `loop_start()` or `loop_forever()`.""" - if self._sock is None: - return MQTTErrorCode.MQTT_ERR_NO_CONN - - now = time_func() - self._check_keepalive() - - if self._ping_t > 0 and now - self._ping_t >= self._keepalive: - # client->ping_t != 0 means we are waiting for a pingresp. - # This hasn't happened in the keepalive time so we should disconnect. - self._sock_close() - - if self._state in (_ConnectionState.MQTT_CS_DISCONNECTING, _ConnectionState.MQTT_CS_DISCONNECTED): - self._state = _ConnectionState.MQTT_CS_DISCONNECTED - rc = MQTTErrorCode.MQTT_ERR_SUCCESS - else: - self._state = _ConnectionState.MQTT_CS_CONNECTION_LOST - rc = MQTTErrorCode.MQTT_ERR_KEEPALIVE - - self._do_on_disconnect( - packet_from_broker=False, - v1_rc=rc, - ) - - return MQTTErrorCode.MQTT_ERR_CONN_LOST - - return MQTTErrorCode.MQTT_ERR_SUCCESS - - def max_inflight_messages_set(self, inflight: int) -> None: - """Set the maximum number of messages with QoS>0 that can be part way - through their network flow at once. Defaults to 20.""" - self.max_inflight_messages = inflight - - def max_queued_messages_set(self, queue_size: int) -> Client: - """Set the maximum number of messages in the outgoing message queue. - 0 means unlimited.""" - if not isinstance(queue_size, int): - raise ValueError('Invalid type of queue size.') - self.max_queued_messages = queue_size - return self - - def user_data_set(self, userdata: Any) -> None: - """Set the user data variable passed to callbacks. May be any data type.""" - self._userdata = userdata - - def user_data_get(self) -> Any: - """Get the user data variable passed to callbacks. May be any data type.""" - return self._userdata - - def will_set( - self, - topic: str, - payload: PayloadType = None, - qos: int = 0, - retain: bool = False, - properties: Properties | None = None, - ) -> None: - """Set a Will to be sent by the broker in case the client disconnects unexpectedly. - - This must be called before connect() to have any effect. - - :param str topic: The topic that the will message should be published on. - :param payload: The message to send as a will. If not given, or set to None a - zero length message will be used as the will. Passing an int or float - will result in the payload being converted to a string representing - that number. If you wish to send a true int/float, use struct.pack() to - create the payload you require. - :param int qos: The quality of service level to use for the will. - :param bool retain: If set to true, the will message will be set as the "last known - good"/retained message for the topic. - :param Properties properties: (MQTT v5.0 only) the MQTT v5.0 properties - to be included with the will message. Optional - if not set, no properties are sent. - - :raises ValueError: if qos is not 0, 1 or 2, or if topic is None or has - zero string length. - - See `will_clear` to clear will. Note that will are NOT send if the client disconnect cleanly - for example by calling `disconnect()`. - """ - if topic is None or len(topic) == 0: - raise ValueError('Invalid topic.') - - if qos < 0 or qos > 2: - raise ValueError('Invalid QoS level.') - - if properties and not isinstance(properties, Properties): - raise ValueError( - "The properties argument must be an instance of the Properties class.") - - self._will_payload = _encode_payload(payload) - self._will = True - self._will_topic = topic.encode('utf-8') - self._will_qos = qos - self._will_retain = retain - self._will_properties = properties - - def will_clear(self) -> None: - """ Removes a will that was previously configured with `will_set()`. - - Must be called before connect() to have any effect.""" - self._will = False - self._will_topic = b"" - self._will_payload = b"" - self._will_qos = 0 - self._will_retain = False - - def socket(self) -> SocketLike | None: - """Return the socket or ssl object for this client.""" - return self._sock - - def loop_forever( - self, - timeout: float = 1.0, - retry_first_connection: bool = False, - ) -> MQTTErrorCode: - """This function calls the network loop functions for you in an - infinite blocking loop. It is useful for the case where you only want - to run the MQTT client loop in your program. - - loop_forever() will handle reconnecting for you if reconnect_on_failure is - true (this is the default behavior). If you call `disconnect()` in a callback - it will return. - - :param int timeout: The time in seconds to wait for incoming/outgoing network - traffic before timing out and returning. - :param bool retry_first_connection: Should the first connection attempt be retried on failure. - This is independent of the reconnect_on_failure setting. - - :raises OSError: if the first connection fail unless retry_first_connection=True - """ - - run = True - - while run: - if self._thread_terminate is True: - break - - if self._state == _ConnectionState.MQTT_CS_CONNECT_ASYNC: - try: - self.reconnect() - except OSError: - self._handle_on_connect_fail() - if not retry_first_connection: - raise - self._easy_log( - MQTT_LOG_DEBUG, "Connection failed, retrying") - self._reconnect_wait() - else: - break - - while run: - rc = MQTTErrorCode.MQTT_ERR_SUCCESS - while rc == MQTTErrorCode.MQTT_ERR_SUCCESS: - rc = self._loop(timeout) - # We don't need to worry about locking here, because we've - # either called loop_forever() when in single threaded mode, or - # in multi threaded mode when loop_stop() has been called and - # so no other threads can access _out_packet or _messages. - if (self._thread_terminate is True - and len(self._out_packet) == 0 - and len(self._out_messages) == 0): - rc = MQTTErrorCode.MQTT_ERR_NOMEM - run = False - - def should_exit() -> bool: - return ( - self._state in (_ConnectionState.MQTT_CS_DISCONNECTING, _ConnectionState.MQTT_CS_DISCONNECTED) or - run is False or # noqa: B023 (uses the run variable from the outer scope on purpose) - self._thread_terminate is True - ) - - if should_exit() or not self._reconnect_on_failure: - run = False - else: - self._reconnect_wait() - - if should_exit(): - run = False - else: - try: - self.reconnect() - except OSError: - self._handle_on_connect_fail() - self._easy_log( - MQTT_LOG_DEBUG, "Connection failed, retrying") - - return rc - - def loop_start(self) -> MQTTErrorCode: - """This is part of the threaded client interface. Call this once to - start a new thread to process network traffic. This provides an - alternative to repeatedly calling `loop()` yourself. - - Under the hood, this will call `loop_forever` in a thread, which means that - the thread will terminate if you call `disconnect()` - """ - if self._thread is not None: - return MQTTErrorCode.MQTT_ERR_INVAL - - self._sockpairR, self._sockpairW = _socketpair_compat() - self._thread_terminate = False - self._thread = threading.Thread(target=self._thread_main, name=f"paho-mqtt-client-{self._client_id.decode()}") - self._thread.daemon = True - self._thread.start() - - return MQTTErrorCode.MQTT_ERR_SUCCESS - - def loop_stop(self) -> MQTTErrorCode: - """This is part of the threaded client interface. Call this once to - stop the network thread previously created with `loop_start()`. This call - will block until the network thread finishes. - - This don't guarantee that publish packet are sent, use `wait_for_publish` or - `on_publish` to ensure `publish` are sent. - """ - if self._thread is None: - return MQTTErrorCode.MQTT_ERR_INVAL - - self._thread_terminate = True - if threading.current_thread() != self._thread: - self._thread.join() - - return MQTTErrorCode.MQTT_ERR_SUCCESS - - @property - def callback_api_version(self) -> CallbackAPIVersion: - """ - Return the callback API version used for user-callback. See docstring for - each user-callback (`on_connect`, `on_publish`, ...) for details. - - This property is read-only. - """ - return self._callback_api_version - - @property - def on_log(self) -> CallbackOnLog | None: - """The callback called when the client has log information. - Defined to allow debugging. - - Expected signature is:: - - log_callback(client, userdata, level, buf) - - :param Client client: the client instance for this callback - :param userdata: the private user data as set in Client() or user_data_set() - :param int level: gives the severity of the message and will be one of - MQTT_LOG_INFO, MQTT_LOG_NOTICE, MQTT_LOG_WARNING, - MQTT_LOG_ERR, and MQTT_LOG_DEBUG. - :param str buf: the message itself - - Decorator: @client.log_callback() (``client`` is the name of the - instance which this callback is being attached to) - """ - return self._on_log - - @on_log.setter - def on_log(self, func: CallbackOnLog | None) -> None: - self._on_log = func - - def log_callback(self) -> Callable[[CallbackOnLog], CallbackOnLog]: - def decorator(func: CallbackOnLog) -> CallbackOnLog: - self.on_log = func - return func - return decorator - - @property - def on_pre_connect(self) -> CallbackOnPreConnect | None: - """The callback called immediately prior to the connection is made - request. - - Expected signature (for all callback API version):: - - connect_callback(client, userdata) - - :parama Client client: the client instance for this callback - :parama userdata: the private user data as set in Client() or user_data_set() - - Decorator: @client.pre_connect_callback() (``client`` is the name of the - instance which this callback is being attached to) - - """ - return self._on_pre_connect - - @on_pre_connect.setter - def on_pre_connect(self, func: CallbackOnPreConnect | None) -> None: - with self._callback_mutex: - self._on_pre_connect = func - - def pre_connect_callback( - self, - ) -> Callable[[CallbackOnPreConnect], CallbackOnPreConnect]: - def decorator(func: CallbackOnPreConnect) -> CallbackOnPreConnect: - self.on_pre_connect = func - return func - return decorator - - @property - def on_connect(self) -> CallbackOnConnect | None: - """The callback called when the broker reponds to our connection request. - - Expected signature for callback API version 2:: - - connect_callback(client, userdata, connect_flags, reason_code, properties) - - Expected signature for callback API version 1 change with MQTT protocol version: - * For MQTT v3.1 and v3.1.1 it's:: - - connect_callback(client, userdata, flags, rc) - - * For MQTT v5.0 it's:: - - connect_callback(client, userdata, flags, reason_code, properties) - - - :param Client client: the client instance for this callback - :param userdata: the private user data as set in Client() or user_data_set() - :param ConnectFlags connect_flags: the flags for this connection - :param ReasonCode reason_code: the connection reason code received from the broken. - In MQTT v5.0 it's the reason code defined by the standard. - In MQTT v3, we convert return code to a reason code, see - `convert_connack_rc_to_reason_code()`. - `ReasonCode` may be compared to integer. - :param Properties properties: the MQTT v5.0 properties received from the broker. - For MQTT v3.1 and v3.1.1 properties is not provided and an empty Properties - object is always used. - :param dict flags: response flags sent by the broker - :param int rc: the connection result, should have a value of `ConnackCode` - - flags is a dict that contains response flags from the broker: - flags['session present'] - this flag is useful for clients that are - using clean session set to 0 only. If a client with clean - session=0, that reconnects to a broker that it has previously - connected to, this flag indicates whether the broker still has the - session information for the client. If 1, the session still exists. - - The value of rc indicates success or not: - - 0: Connection successful - - 1: Connection refused - incorrect protocol version - - 2: Connection refused - invalid client identifier - - 3: Connection refused - server unavailable - - 4: Connection refused - bad username or password - - 5: Connection refused - not authorised - - 6-255: Currently unused. - - Decorator: @client.connect_callback() (``client`` is the name of the - instance which this callback is being attached to) - """ - return self._on_connect - - @on_connect.setter - def on_connect(self, func: CallbackOnConnect | None) -> None: - with self._callback_mutex: - self._on_connect = func - - def connect_callback( - self, - ) -> Callable[[CallbackOnConnect], CallbackOnConnect]: - def decorator(func: CallbackOnConnect) -> CallbackOnConnect: - self.on_connect = func - return func - return decorator - - @property - def on_connect_fail(self) -> CallbackOnConnectFail | None: - """The callback called when the client failed to connect - to the broker. - - Expected signature is (for all callback_api_version):: - - connect_fail_callback(client, userdata) - - :param Client client: the client instance for this callback - :parama userdata: the private user data as set in Client() or user_data_set() - - Decorator: @client.connect_fail_callback() (``client`` is the name of the - instance which this callback is being attached to) - """ - return self._on_connect_fail - - @on_connect_fail.setter - def on_connect_fail(self, func: CallbackOnConnectFail | None) -> None: - with self._callback_mutex: - self._on_connect_fail = func - - def connect_fail_callback( - self, - ) -> Callable[[CallbackOnConnectFail], CallbackOnConnectFail]: - def decorator(func: CallbackOnConnectFail) -> CallbackOnConnectFail: - self.on_connect_fail = func - return func - return decorator - - @property - def on_subscribe(self) -> CallbackOnSubscribe | None: - """The callback called when the broker responds to a subscribe - request. - - Expected signature for callback API version 2:: - - subscribe_callback(client, userdata, mid, reason_code_list, properties) - - Expected signature for callback API version 1 change with MQTT protocol version: - * For MQTT v3.1 and v3.1.1 it's:: - - subscribe_callback(client, userdata, mid, granted_qos) - - * For MQTT v5.0 it's:: - - subscribe_callback(client, userdata, mid, reason_code_list, properties) - - :param Client client: the client instance for this callback - :param userdata: the private user data as set in Client() or user_data_set() - :param int mid: matches the mid variable returned from the corresponding - subscribe() call. - :param list[ReasonCode] reason_code_list: reason codes received from the broker for each subscription. - In MQTT v5.0 it's the reason code defined by the standard. - In MQTT v3, we convert granted QoS to a reason code. - It's a list of ReasonCode instances. - :param Properties properties: the MQTT v5.0 properties received from the broker. - For MQTT v3.1 and v3.1.1 properties is not provided and an empty Properties - object is always used. - :param list[int] granted_qos: list of integers that give the QoS level the broker has - granted for each of the different subscription requests. - - Decorator: @client.subscribe_callback() (``client`` is the name of the - instance which this callback is being attached to) - """ - return self._on_subscribe - - @on_subscribe.setter - def on_subscribe(self, func: CallbackOnSubscribe | None) -> None: - with self._callback_mutex: - self._on_subscribe = func - - def subscribe_callback( - self, - ) -> Callable[[CallbackOnSubscribe], CallbackOnSubscribe]: - def decorator(func: CallbackOnSubscribe) -> CallbackOnSubscribe: - self.on_subscribe = func - return func - return decorator - - @property - def on_message(self) -> CallbackOnMessage | None: - """The callback called when a message has been received on a topic - that the client subscribes to. - - This callback will be called for every message received unless a - `message_callback_add()` matched the message. - - Expected signature is (for all callback API version): - message_callback(client, userdata, message) - - :param Client client: the client instance for this callback - :param userdata: the private user data as set in Client() or user_data_set() - :param MQTTMessage message: the received message. - This is a class with members topic, payload, qos, retain. - - Decorator: @client.message_callback() (``client`` is the name of the - instance which this callback is being attached to) - """ - return self._on_message - - @on_message.setter - def on_message(self, func: CallbackOnMessage | None) -> None: - with self._callback_mutex: - self._on_message = func - - def message_callback( - self, - ) -> Callable[[CallbackOnMessage], CallbackOnMessage]: - def decorator(func: CallbackOnMessage) -> CallbackOnMessage: - self.on_message = func - return func - return decorator - - @property - def on_publish(self) -> CallbackOnPublish | None: - """The callback called when a message that was to be sent using the - `publish()` call has completed transmission to the broker. - - For messages with QoS levels 1 and 2, this means that the appropriate - handshakes have completed. For QoS 0, this simply means that the message - has left the client. - This callback is important because even if the `publish()` call returns - success, it does not always mean that the message has been sent. - - See also `wait_for_publish` which could be simpler to use. - - Expected signature for callback API version 2:: - - publish_callback(client, userdata, mid, reason_code, properties) - - Expected signature for callback API version 1:: - - publish_callback(client, userdata, mid) - - :param Client client: the client instance for this callback - :param userdata: the private user data as set in Client() or user_data_set() - :param int mid: matches the mid variable returned from the corresponding - `publish()` call, to allow outgoing messages to be tracked. - :param ReasonCode reason_code: the connection reason code received from the broken. - In MQTT v5.0 it's the reason code defined by the standard. - In MQTT v3 it's always the reason code Success - :parama Properties properties: the MQTT v5.0 properties received from the broker. - For MQTT v3.1 and v3.1.1 properties is not provided and an empty Properties - object is always used. - - Note: for QoS = 0, the reason_code and the properties don't really exist, it's the client - library that generate them. It's always an empty properties and a success reason code. - Because the (MQTTv5) standard don't have reason code for PUBLISH packet, the library create them - at PUBACK packet, as if the message was sent with QoS = 1. - - Decorator: @client.publish_callback() (``client`` is the name of the - instance which this callback is being attached to) - - """ - return self._on_publish - - @on_publish.setter - def on_publish(self, func: CallbackOnPublish | None) -> None: - with self._callback_mutex: - self._on_publish = func - - def publish_callback( - self, - ) -> Callable[[CallbackOnPublish], CallbackOnPublish]: - def decorator(func: CallbackOnPublish) -> CallbackOnPublish: - self.on_publish = func - return func - return decorator - - @property - def on_unsubscribe(self) -> CallbackOnUnsubscribe | None: - """The callback called when the broker responds to an unsubscribe - request. - - Expected signature for callback API version 2:: - - unsubscribe_callback(client, userdata, mid, reason_code_list, properties) - - Expected signature for callback API version 1 change with MQTT protocol version: - * For MQTT v3.1 and v3.1.1 it's:: - - unsubscribe_callback(client, userdata, mid) - - * For MQTT v5.0 it's:: - - unsubscribe_callback(client, userdata, mid, properties, v1_reason_codes) - - :param Client client: the client instance for this callback - :param userdata: the private user data as set in Client() or user_data_set() - :param mid: matches the mid variable returned from the corresponding - unsubscribe() call. - :param list[ReasonCode] reason_code_list: reason codes received from the broker for each unsubscription. - In MQTT v5.0 it's the reason code defined by the standard. - In MQTT v3, there is not equivalent from broken and empty list - is always used. - :param Properties properties: the MQTT v5.0 properties received from the broker. - For MQTT v3.1 and v3.1.1 properties is not provided and an empty Properties - object is always used. - :param v1_reason_codes: the MQTT v5.0 reason codes received from the broker for each - unsubscribe topic. A list of ReasonCode instances OR a single - ReasonCode when we unsubscribe from a single topic. - - Decorator: @client.unsubscribe_callback() (``client`` is the name of the - instance which this callback is being attached to) - """ - return self._on_unsubscribe - - @on_unsubscribe.setter - def on_unsubscribe(self, func: CallbackOnUnsubscribe | None) -> None: - with self._callback_mutex: - self._on_unsubscribe = func - - def unsubscribe_callback( - self, - ) -> Callable[[CallbackOnUnsubscribe], CallbackOnUnsubscribe]: - def decorator(func: CallbackOnUnsubscribe) -> CallbackOnUnsubscribe: - self.on_unsubscribe = func - return func - return decorator - - @property - def on_disconnect(self) -> CallbackOnDisconnect | None: - """The callback called when the client disconnects from the broker. - - Expected signature for callback API version 2:: - - disconnect_callback(client, userdata, disconnect_flags, reason_code, properties) - - Expected signature for callback API version 1 change with MQTT protocol version: - * For MQTT v3.1 and v3.1.1 it's:: - - disconnect_callback(client, userdata, rc) - - * For MQTT v5.0 it's:: - - disconnect_callback(client, userdata, reason_code, properties) - - :param Client client: the client instance for this callback - :param userdata: the private user data as set in Client() or user_data_set() - :param DisconnectFlag disconnect_flags: the flags for this disconnection. - :param ReasonCode reason_code: the disconnection reason code possibly received from the broker (see disconnect_flags). - In MQTT v5.0 it's the reason code defined by the standard. - In MQTT v3 it's never received from the broker, we convert an MQTTErrorCode, - see `convert_disconnect_error_code_to_reason_code()`. - `ReasonCode` may be compared to integer. - :param Properties properties: the MQTT v5.0 properties received from the broker. - For MQTT v3.1 and v3.1.1 properties is not provided and an empty Properties - object is always used. - :param int rc: the disconnection result - The rc parameter indicates the disconnection state. If - MQTT_ERR_SUCCESS (0), the callback was called in response to - a disconnect() call. If any other value the disconnection - was unexpected, such as might be caused by a network error. - - Decorator: @client.disconnect_callback() (``client`` is the name of the - instance which this callback is being attached to) - - """ - return self._on_disconnect - - @on_disconnect.setter - def on_disconnect(self, func: CallbackOnDisconnect | None) -> None: - with self._callback_mutex: - self._on_disconnect = func - - def disconnect_callback( - self, - ) -> Callable[[CallbackOnDisconnect], CallbackOnDisconnect]: - def decorator(func: CallbackOnDisconnect) -> CallbackOnDisconnect: - self.on_disconnect = func - return func - return decorator - - @property - def on_socket_open(self) -> CallbackOnSocket | None: - """The callback called just after the socket was opend. - - This should be used to register the socket to an external event loop for reading. - - Expected signature is (for all callback API version):: - - socket_open_callback(client, userdata, socket) - - :param Client client: the client instance for this callback - :param userdata: the private user data as set in Client() or user_data_set() - :param SocketLike sock: the socket which was just opened. - - Decorator: @client.socket_open_callback() (``client`` is the name of the - instance which this callback is being attached to) - """ - return self._on_socket_open - - @on_socket_open.setter - def on_socket_open(self, func: CallbackOnSocket | None) -> None: - with self._callback_mutex: - self._on_socket_open = func - - def socket_open_callback( - self, - ) -> Callable[[CallbackOnSocket], CallbackOnSocket]: - def decorator(func: CallbackOnSocket) -> CallbackOnSocket: - self.on_socket_open = func - return func - return decorator - - def _call_socket_open(self, sock: SocketLike) -> None: - """Call the socket_open callback with the just-opened socket""" - with self._callback_mutex: - on_socket_open = self.on_socket_open - - if on_socket_open: - with self._in_callback_mutex: - try: - on_socket_open(self, self._userdata, sock) - except Exception as err: - self._easy_log( - MQTT_LOG_ERR, 'Caught exception in on_socket_open: %s', err) - if not self.suppress_exceptions: - raise - - @property - def on_socket_close(self) -> CallbackOnSocket | None: - """The callback called just before the socket is closed. - - This should be used to unregister the socket from an external event loop for reading. - - Expected signature is (for all callback API version):: - - socket_close_callback(client, userdata, socket) - - :param Client client: the client instance for this callback - :param userdata: the private user data as set in Client() or user_data_set() - :param SocketLike sock: the socket which is about to be closed. - - Decorator: @client.socket_close_callback() (``client`` is the name of the - instance which this callback is being attached to) - """ - return self._on_socket_close - - @on_socket_close.setter - def on_socket_close(self, func: CallbackOnSocket | None) -> None: - with self._callback_mutex: - self._on_socket_close = func - - def socket_close_callback( - self, - ) -> Callable[[CallbackOnSocket], CallbackOnSocket]: - def decorator(func: CallbackOnSocket) -> CallbackOnSocket: - self.on_socket_close = func - return func - return decorator - - def _call_socket_close(self, sock: SocketLike) -> None: - """Call the socket_close callback with the about-to-be-closed socket""" - with self._callback_mutex: - on_socket_close = self.on_socket_close - - if on_socket_close: - with self._in_callback_mutex: - try: - on_socket_close(self, self._userdata, sock) - except Exception as err: - self._easy_log( - MQTT_LOG_ERR, 'Caught exception in on_socket_close: %s', err) - if not self.suppress_exceptions: - raise - - @property - def on_socket_register_write(self) -> CallbackOnSocket | None: - """The callback called when the socket needs writing but can't. - - This should be used to register the socket with an external event loop for writing. - - Expected signature is (for all callback API version):: - - socket_register_write_callback(client, userdata, socket) - - :param Client client: the client instance for this callback - :param userdata: the private user data as set in Client() or user_data_set() - :param SocketLike sock: the socket which should be registered for writing - - Decorator: @client.socket_register_write_callback() (``client`` is the name of the - instance which this callback is being attached to) - """ - return self._on_socket_register_write - - @on_socket_register_write.setter - def on_socket_register_write(self, func: CallbackOnSocket | None) -> None: - with self._callback_mutex: - self._on_socket_register_write = func - - def socket_register_write_callback( - self, - ) -> Callable[[CallbackOnSocket], CallbackOnSocket]: - def decorator(func: CallbackOnSocket) -> CallbackOnSocket: - self._on_socket_register_write = func - return func - return decorator - - def _call_socket_register_write(self) -> None: - """Call the socket_register_write callback with the unwritable socket""" - if not self._sock or self._registered_write: - return - self._registered_write = True - with self._callback_mutex: - on_socket_register_write = self.on_socket_register_write - - if on_socket_register_write: - try: - on_socket_register_write( - self, self._userdata, self._sock) - except Exception as err: - self._easy_log( - MQTT_LOG_ERR, 'Caught exception in on_socket_register_write: %s', err) - if not self.suppress_exceptions: - raise - - @property - def on_socket_unregister_write( - self, - ) -> CallbackOnSocket | None: - """The callback called when the socket doesn't need writing anymore. - - This should be used to unregister the socket from an external event loop for writing. - - Expected signature is (for all callback API version):: - - socket_unregister_write_callback(client, userdata, socket) - - :param Client client: the client instance for this callback - :param userdata: the private user data as set in Client() or user_data_set() - :param SocketLike sock: the socket which should be unregistered for writing - - Decorator: @client.socket_unregister_write_callback() (``client`` is the name of the - instance which this callback is being attached to) - """ - return self._on_socket_unregister_write - - @on_socket_unregister_write.setter - def on_socket_unregister_write( - self, func: CallbackOnSocket | None - ) -> None: - with self._callback_mutex: - self._on_socket_unregister_write = func - - def socket_unregister_write_callback( - self, - ) -> Callable[[CallbackOnSocket], CallbackOnSocket]: - def decorator( - func: CallbackOnSocket, - ) -> CallbackOnSocket: - self._on_socket_unregister_write = func - return func - return decorator - - def _call_socket_unregister_write( - self, sock: SocketLike | None = None - ) -> None: - """Call the socket_unregister_write callback with the writable socket""" - sock = sock or self._sock - if not sock or not self._registered_write: - return - self._registered_write = False - - with self._callback_mutex: - on_socket_unregister_write = self.on_socket_unregister_write - - if on_socket_unregister_write: - try: - on_socket_unregister_write(self, self._userdata, sock) - except Exception as err: - self._easy_log( - MQTT_LOG_ERR, 'Caught exception in on_socket_unregister_write: %s', err) - if not self.suppress_exceptions: - raise - - def message_callback_add(self, sub: str, callback: CallbackOnMessage) -> None: - """Register a message callback for a specific topic. - Messages that match 'sub' will be passed to 'callback'. Any - non-matching messages will be passed to the default `on_message` - callback. - - Call multiple times with different 'sub' to define multiple topic - specific callbacks. - - Topic specific callbacks may be removed with - `message_callback_remove()`. - - See `on_message` for the expected signature of the callback. - - Decorator: @client.topic_callback(sub) (``client`` is the name of the - instance which this callback is being attached to) - - Example:: - - @client.topic_callback("mytopic/#") - def handle_mytopic(client, userdata, message): - ... - """ - if callback is None or sub is None: - raise ValueError("sub and callback must both be defined.") - - with self._callback_mutex: - self._on_message_filtered[sub] = callback - - def topic_callback( - self, sub: str - ) -> Callable[[CallbackOnMessage], CallbackOnMessage]: - def decorator(func: CallbackOnMessage) -> CallbackOnMessage: - self.message_callback_add(sub, func) - return func - return decorator - - def message_callback_remove(self, sub: str) -> None: - """Remove a message callback previously registered with - `message_callback_add()`.""" - if sub is None: - raise ValueError("sub must defined.") - - with self._callback_mutex: - try: - del self._on_message_filtered[sub] - except KeyError: # no such subscription - pass - - # ============================================================ - # Private functions - # ============================================================ - - def _loop_rc_handle( - self, - rc: MQTTErrorCode, - ) -> MQTTErrorCode: - if rc: - self._sock_close() - - if self._state in (_ConnectionState.MQTT_CS_DISCONNECTING, _ConnectionState.MQTT_CS_DISCONNECTED): - self._state = _ConnectionState.MQTT_CS_DISCONNECTED - rc = MQTTErrorCode.MQTT_ERR_SUCCESS - - self._do_on_disconnect(packet_from_broker=False, v1_rc=rc) - - if rc == MQTT_ERR_CONN_LOST: - self._state = _ConnectionState.MQTT_CS_CONNECTION_LOST - - return rc - - def _packet_read(self) -> MQTTErrorCode: - # This gets called if pselect() indicates that there is network data - # available - ie. at least one byte. What we do depends on what data we - # already have. - # If we've not got a command, attempt to read one and save it. This should - # always work because it's only a single byte. - # Then try to read the remaining length. This may fail because it is may - # be more than one byte - will need to save data pending next read if it - # does fail. - # Then try to read the remaining payload, where 'payload' here means the - # combined variable header and actual payload. This is the most likely to - # fail due to longer length, so save current data and current position. - # After all data is read, send to _mqtt_handle_packet() to deal with. - # Finally, free the memory and reset everything to starting conditions. - if self._in_packet['command'] == 0: - try: - command = self._sock_recv(1) - except BlockingIOError: - return MQTTErrorCode.MQTT_ERR_AGAIN - except TimeoutError as err: - self._easy_log( - MQTT_LOG_ERR, 'timeout on socket: %s', err) - return MQTTErrorCode.MQTT_ERR_CONN_LOST - except OSError as err: - self._easy_log( - MQTT_LOG_ERR, 'failed to receive on socket: %s', err) - return MQTTErrorCode.MQTT_ERR_CONN_LOST - else: - if len(command) == 0: - return MQTTErrorCode.MQTT_ERR_CONN_LOST - self._in_packet['command'] = command[0] - - if self._in_packet['have_remaining'] == 0: - # Read remaining - # Algorithm for decoding taken from pseudo code at - # http://publib.boulder.ibm.com/infocenter/wmbhelp/v6r0m0/topic/com.ibm.etools.mft.doc/ac10870_.htm - while True: - try: - byte = self._sock_recv(1) - except BlockingIOError: - return MQTTErrorCode.MQTT_ERR_AGAIN - except OSError as err: - self._easy_log( - MQTT_LOG_ERR, 'failed to receive on socket: %s', err) - return MQTTErrorCode.MQTT_ERR_CONN_LOST - else: - if len(byte) == 0: - return MQTTErrorCode.MQTT_ERR_CONN_LOST - byte_value = byte[0] - self._in_packet['remaining_count'].append(byte_value) - # Max 4 bytes length for remaining length as defined by protocol. - # Anything more likely means a broken/malicious client. - if len(self._in_packet['remaining_count']) > 4: - return MQTTErrorCode.MQTT_ERR_PROTOCOL - - self._in_packet['remaining_length'] += ( - byte_value & 127) * self._in_packet['remaining_mult'] - self._in_packet['remaining_mult'] = self._in_packet['remaining_mult'] * 128 - - if (byte_value & 128) == 0: - break - - self._in_packet['have_remaining'] = 1 - self._in_packet['to_process'] = self._in_packet['remaining_length'] - - count = 100 # Don't get stuck in this loop if we have a huge message. - while self._in_packet['to_process'] > 0: - try: - data = self._sock_recv(self._in_packet['to_process']) - except BlockingIOError: - return MQTTErrorCode.MQTT_ERR_AGAIN - except OSError as err: - self._easy_log( - MQTT_LOG_ERR, 'failed to receive on socket: %s', err) - return MQTTErrorCode.MQTT_ERR_CONN_LOST - else: - if len(data) == 0: - return MQTTErrorCode.MQTT_ERR_CONN_LOST - self._in_packet['to_process'] -= len(data) - self._in_packet['packet'] += data - count -= 1 - if count == 0: - with self._msgtime_mutex: - self._last_msg_in = time_func() - return MQTTErrorCode.MQTT_ERR_AGAIN - - # All data for this packet is read. - self._in_packet['pos'] = 0 - rc = self._packet_handle() - - # Free data and reset values - self._in_packet = { - "command": 0, - "have_remaining": 0, - "remaining_count": [], - "remaining_mult": 1, - "remaining_length": 0, - "packet": bytearray(b""), - "to_process": 0, - "pos": 0, - } - - with self._msgtime_mutex: - self._last_msg_in = time_func() - return rc - - def _packet_write(self) -> MQTTErrorCode: - while True: - try: - packet = self._out_packet.popleft() - except IndexError: - return MQTTErrorCode.MQTT_ERR_SUCCESS - - try: - write_length = self._sock_send( - packet['packet'][packet['pos']:]) - except (AttributeError, ValueError): - self._out_packet.appendleft(packet) - return MQTTErrorCode.MQTT_ERR_SUCCESS - except BlockingIOError: - self._out_packet.appendleft(packet) - return MQTTErrorCode.MQTT_ERR_AGAIN - except OSError as err: - self._out_packet.appendleft(packet) - self._easy_log( - MQTT_LOG_ERR, 'failed to receive on socket: %s', err) - return MQTTErrorCode.MQTT_ERR_CONN_LOST - - if write_length > 0: - packet['to_process'] -= write_length - packet['pos'] += write_length - - if packet['to_process'] == 0: - if (packet['command'] & 0xF0) == PUBLISH and packet['qos'] == 0: - with self._callback_mutex: - on_publish = self.on_publish - - if on_publish: - with self._in_callback_mutex: - try: - if self._callback_api_version == CallbackAPIVersion.VERSION1: - on_publish = cast(CallbackOnPublish_v1, on_publish) - - on_publish(self, self._userdata, packet["mid"]) - elif self._callback_api_version == CallbackAPIVersion.VERSION2: - on_publish = cast(CallbackOnPublish_v2, on_publish) - - on_publish( - self, - self._userdata, - packet["mid"], - ReasonCode(PacketTypes.PUBACK), - Properties(PacketTypes.PUBACK), - ) - else: - raise RuntimeError("Unsupported callback API version") - except Exception as err: - self._easy_log( - MQTT_LOG_ERR, 'Caught exception in on_publish: %s', err) - if not self.suppress_exceptions: - raise - - # TODO: Something is odd here. I don't see why packet["info"] can't be None. - # A packet could be produced by _handle_connack with qos=0 and no info - # (around line 3645). Ignore the mypy check for now but I feel there is a bug - # somewhere. - packet['info']._set_as_published() # type: ignore - - if (packet['command'] & 0xF0) == DISCONNECT: - with self._msgtime_mutex: - self._last_msg_out = time_func() - - self._do_on_disconnect( - packet_from_broker=False, - v1_rc=MQTTErrorCode.MQTT_ERR_SUCCESS, - ) - self._sock_close() - # Only change to disconnected if the disconnection was wanted - # by the client (== state was disconnecting). If the broker disconnected - # use unilaterally don't change the state and client may reconnect. - if self._state == _ConnectionState.MQTT_CS_DISCONNECTING: - self._state = _ConnectionState.MQTT_CS_DISCONNECTED - return MQTTErrorCode.MQTT_ERR_SUCCESS - - else: - # We haven't finished with this packet - self._out_packet.appendleft(packet) - else: - break - - with self._msgtime_mutex: - self._last_msg_out = time_func() - - return MQTTErrorCode.MQTT_ERR_SUCCESS - - def _easy_log(self, level: LogLevel, fmt: str, *args: Any) -> None: - if self.on_log is not None: - buf = fmt % args - try: - self.on_log(self, self._userdata, level, buf) - except Exception: # noqa: S110 - # Can't _easy_log this, as we'll recurse until we break - pass # self._logger will pick this up, so we're fine - if self._logger is not None: - level_std = LOGGING_LEVEL[level] - self._logger.log(level_std, fmt, *args) - - def _check_keepalive(self) -> None: - if self._keepalive == 0: - return - - now = time_func() - - with self._msgtime_mutex: - last_msg_out = self._last_msg_out - last_msg_in = self._last_msg_in - - if self._sock is not None and (now - last_msg_out >= self._keepalive or now - last_msg_in >= self._keepalive): - if self._state == _ConnectionState.MQTT_CS_CONNECTED and self._ping_t == 0: - try: - self._send_pingreq() - except Exception: - self._sock_close() - self._do_on_disconnect( - packet_from_broker=False, - v1_rc=MQTTErrorCode.MQTT_ERR_CONN_LOST, - ) - else: - with self._msgtime_mutex: - self._last_msg_out = now - self._last_msg_in = now - else: - self._sock_close() - - if self._state in (_ConnectionState.MQTT_CS_DISCONNECTING, _ConnectionState.MQTT_CS_DISCONNECTED): - self._state = _ConnectionState.MQTT_CS_DISCONNECTED - rc = MQTTErrorCode.MQTT_ERR_SUCCESS - else: - rc = MQTTErrorCode.MQTT_ERR_KEEPALIVE - - self._do_on_disconnect( - packet_from_broker=False, - v1_rc=rc, - ) - - def _mid_generate(self) -> int: - with self._mid_generate_mutex: - self._last_mid += 1 - if self._last_mid == 65536: - self._last_mid = 1 - return self._last_mid - - @staticmethod - def _raise_for_invalid_topic(topic: bytes) -> None: - """ Check if the topic is a topic without wildcard and valid length. - - Raise ValueError if the topic isn't valid. - """ - if b'+' in topic or b'#' in topic: - raise ValueError('Publish topic cannot contain wildcards.') - if len(topic) > 65535: - raise ValueError('Publish topic is too long.') - - @staticmethod - def _filter_wildcard_len_check(sub: bytes) -> MQTTErrorCode: - if (len(sub) == 0 or len(sub) > 65535 - or any(b'+' in p or b'#' in p for p in sub.split(b'/') if len(p) > 1) - or b'#/' in sub): - return MQTTErrorCode.MQTT_ERR_INVAL - else: - return MQTTErrorCode.MQTT_ERR_SUCCESS - - def _send_pingreq(self) -> MQTTErrorCode: - self._easy_log(MQTT_LOG_DEBUG, "Sending PINGREQ") - rc = self._send_simple_command(PINGREQ) - if rc == MQTTErrorCode.MQTT_ERR_SUCCESS: - self._ping_t = time_func() - return rc - - def _send_pingresp(self) -> MQTTErrorCode: - self._easy_log(MQTT_LOG_DEBUG, "Sending PINGRESP") - return self._send_simple_command(PINGRESP) - - def _send_puback(self, mid: int) -> MQTTErrorCode: - self._easy_log(MQTT_LOG_DEBUG, "Sending PUBACK (Mid: %d)", mid) - return self._send_command_with_mid(PUBACK, mid, False) - - def _send_pubcomp(self, mid: int) -> MQTTErrorCode: - self._easy_log(MQTT_LOG_DEBUG, "Sending PUBCOMP (Mid: %d)", mid) - return self._send_command_with_mid(PUBCOMP, mid, False) - - def _pack_remaining_length( - self, packet: bytearray, remaining_length: int - ) -> bytearray: - remaining_bytes = [] - while True: - byte = remaining_length % 128 - remaining_length = remaining_length // 128 - # If there are more digits to encode, set the top bit of this digit - if remaining_length > 0: - byte |= 0x80 - - remaining_bytes.append(byte) - packet.append(byte) - if remaining_length == 0: - # FIXME - this doesn't deal with incorrectly large payloads - return packet - - def _pack_str16(self, packet: bytearray, data: bytes | str) -> None: - data = _force_bytes(data) - packet.extend(struct.pack("!H", len(data))) - packet.extend(data) - - def _send_publish( - self, - mid: int, - topic: bytes, - payload: bytes|bytearray = b"", - qos: int = 0, - retain: bool = False, - dup: bool = False, - info: MQTTMessageInfo | None = None, - properties: Properties | None = None, - ) -> MQTTErrorCode: - # we assume that topic and payload are already properly encoded - if not isinstance(topic, bytes): - raise TypeError('topic must be bytes, not str') - if payload and not isinstance(payload, (bytes, bytearray)): - raise TypeError('payload must be bytes if set') - - if self._sock is None: - return MQTTErrorCode.MQTT_ERR_NO_CONN - - command = PUBLISH | ((dup & 0x1) << 3) | (qos << 1) | retain - packet = bytearray() - packet.append(command) - - payloadlen = len(payload) - remaining_length = 2 + len(topic) + payloadlen - - if payloadlen == 0: - if self._protocol == MQTTv5: - self._easy_log( - MQTT_LOG_DEBUG, - "Sending PUBLISH (d%d, q%d, r%d, m%d), '%s', properties=%s (NULL payload)", - dup, qos, retain, mid, topic, properties - ) - else: - self._easy_log( - MQTT_LOG_DEBUG, - "Sending PUBLISH (d%d, q%d, r%d, m%d), '%s' (NULL payload)", - dup, qos, retain, mid, topic - ) - else: - if self._protocol == MQTTv5: - self._easy_log( - MQTT_LOG_DEBUG, - "Sending PUBLISH (d%d, q%d, r%d, m%d), '%s', properties=%s, ... (%d bytes)", - dup, qos, retain, mid, topic, properties, payloadlen - ) - else: - self._easy_log( - MQTT_LOG_DEBUG, - "Sending PUBLISH (d%d, q%d, r%d, m%d), '%s', ... (%d bytes)", - dup, qos, retain, mid, topic, payloadlen - ) - - if qos > 0: - # For message id - remaining_length += 2 - - if self._protocol == MQTTv5: - if properties is None: - packed_properties = b'\x00' - else: - packed_properties = properties.pack() - remaining_length += len(packed_properties) - - self._pack_remaining_length(packet, remaining_length) - self._pack_str16(packet, topic) - - if qos > 0: - # For message id - packet.extend(struct.pack("!H", mid)) - - if self._protocol == MQTTv5: - packet.extend(packed_properties) - - packet.extend(payload) - - return self._packet_queue(PUBLISH, packet, mid, qos, info) - - def _send_pubrec(self, mid: int) -> MQTTErrorCode: - self._easy_log(MQTT_LOG_DEBUG, "Sending PUBREC (Mid: %d)", mid) - return self._send_command_with_mid(PUBREC, mid, False) - - def _send_pubrel(self, mid: int) -> MQTTErrorCode: - self._easy_log(MQTT_LOG_DEBUG, "Sending PUBREL (Mid: %d)", mid) - return self._send_command_with_mid(PUBREL | 2, mid, False) - - def _send_command_with_mid(self, command: int, mid: int, dup: int) -> MQTTErrorCode: - # For PUBACK, PUBCOMP, PUBREC, and PUBREL - if dup: - command |= 0x8 - - remaining_length = 2 - packet = struct.pack('!BBH', command, remaining_length, mid) - return self._packet_queue(command, packet, mid, 1) - - def _send_simple_command(self, command: int) -> MQTTErrorCode: - # For DISCONNECT, PINGREQ and PINGRESP - remaining_length = 0 - packet = struct.pack('!BB', command, remaining_length) - return self._packet_queue(command, packet, 0, 0) - - def _send_connect(self, keepalive: int) -> MQTTErrorCode: - proto_ver = int(self._protocol) - # hard-coded UTF-8 encoded string - protocol = b"MQTT" if proto_ver >= MQTTv311 else b"MQIsdp" - - remaining_length = 2 + len(protocol) + 1 + \ - 1 + 2 + 2 + len(self._client_id) - - connect_flags = 0 - if self._protocol == MQTTv5: - if self._clean_start is True: - connect_flags |= 0x02 - elif self._clean_start == MQTT_CLEAN_START_FIRST_ONLY and self._mqttv5_first_connect: - connect_flags |= 0x02 - elif self._clean_session: - connect_flags |= 0x02 - - if self._will: - remaining_length += 2 + \ - len(self._will_topic) + 2 + len(self._will_payload) - connect_flags |= 0x04 | ((self._will_qos & 0x03) << 3) | ( - (self._will_retain & 0x01) << 5) - - if self._username is not None: - remaining_length += 2 + len(self._username) - connect_flags |= 0x80 - if self._password is not None: - connect_flags |= 0x40 - remaining_length += 2 + len(self._password) - - if self._protocol == MQTTv5: - if self._connect_properties is None: - packed_connect_properties = b'\x00' - else: - packed_connect_properties = self._connect_properties.pack() - remaining_length += len(packed_connect_properties) - if self._will: - if self._will_properties is None: - packed_will_properties = b'\x00' - else: - packed_will_properties = self._will_properties.pack() - remaining_length += len(packed_will_properties) - - command = CONNECT - packet = bytearray() - packet.append(command) - - # as per the mosquitto broker, if the MSB of this version is set - # to 1, then it treats the connection as a bridge - if self._client_mode == MQTT_BRIDGE: - proto_ver |= 0x80 - - self._pack_remaining_length(packet, remaining_length) - packet.extend(struct.pack( - f"!H{len(protocol)}sBBH", - len(protocol), protocol, proto_ver, connect_flags, keepalive, - )) - - if self._protocol == MQTTv5: - packet += packed_connect_properties - - self._pack_str16(packet, self._client_id) - - if self._will: - if self._protocol == MQTTv5: - packet += packed_will_properties - self._pack_str16(packet, self._will_topic) - self._pack_str16(packet, self._will_payload) - - if self._username is not None: - self._pack_str16(packet, self._username) - - if self._password is not None: - self._pack_str16(packet, self._password) - - self._keepalive = keepalive - if self._protocol == MQTTv5: - self._easy_log( - MQTT_LOG_DEBUG, - "Sending CONNECT (u%d, p%d, wr%d, wq%d, wf%d, c%d, k%d) client_id=%s properties=%s", - (connect_flags & 0x80) >> 7, - (connect_flags & 0x40) >> 6, - (connect_flags & 0x20) >> 5, - (connect_flags & 0x18) >> 3, - (connect_flags & 0x4) >> 2, - (connect_flags & 0x2) >> 1, - keepalive, - self._client_id, - self._connect_properties - ) - else: - self._easy_log( - MQTT_LOG_DEBUG, - "Sending CONNECT (u%d, p%d, wr%d, wq%d, wf%d, c%d, k%d) client_id=%s", - (connect_flags & 0x80) >> 7, - (connect_flags & 0x40) >> 6, - (connect_flags & 0x20) >> 5, - (connect_flags & 0x18) >> 3, - (connect_flags & 0x4) >> 2, - (connect_flags & 0x2) >> 1, - keepalive, - self._client_id - ) - return self._packet_queue(command, packet, 0, 0) - - def _send_disconnect( - self, - reasoncode: ReasonCode | None = None, - properties: Properties | None = None, - ) -> MQTTErrorCode: - if self._protocol == MQTTv5: - self._easy_log(MQTT_LOG_DEBUG, "Sending DISCONNECT reasonCode=%s properties=%s", - reasoncode, - properties - ) - else: - self._easy_log(MQTT_LOG_DEBUG, "Sending DISCONNECT") - - remaining_length = 0 - - command = DISCONNECT - packet = bytearray() - packet.append(command) - - if self._protocol == MQTTv5: - if properties is not None or reasoncode is not None: - if reasoncode is None: - reasoncode = ReasonCode(DISCONNECT >> 4, identifier=0) - remaining_length += 1 - if properties is not None: - packed_props = properties.pack() - remaining_length += len(packed_props) - - self._pack_remaining_length(packet, remaining_length) - - if self._protocol == MQTTv5: - if reasoncode is not None: - packet += reasoncode.pack() - if properties is not None: - packet += packed_props - - return self._packet_queue(command, packet, 0, 0) - - def _send_subscribe( - self, - dup: int, - topics: Sequence[tuple[bytes, SubscribeOptions | int]], - properties: Properties | None = None, - ) -> tuple[MQTTErrorCode, int]: - remaining_length = 2 - if self._protocol == MQTTv5: - if properties is None: - packed_subscribe_properties = b'\x00' - else: - packed_subscribe_properties = properties.pack() - remaining_length += len(packed_subscribe_properties) - for t, _ in topics: - remaining_length += 2 + len(t) + 1 - - command = SUBSCRIBE | (dup << 3) | 0x2 - packet = bytearray() - packet.append(command) - self._pack_remaining_length(packet, remaining_length) - local_mid = self._mid_generate() - packet.extend(struct.pack("!H", local_mid)) - - if self._protocol == MQTTv5: - packet += packed_subscribe_properties - - for t, q in topics: - self._pack_str16(packet, t) - if self._protocol == MQTTv5: - packet += q.pack() # type: ignore - else: - packet.append(q) # type: ignore - - self._easy_log( - MQTT_LOG_DEBUG, - "Sending SUBSCRIBE (d%d, m%d) %s", - dup, - local_mid, - topics, - ) - return (self._packet_queue(command, packet, local_mid, 1), local_mid) - - def _send_unsubscribe( - self, - dup: int, - topics: list[bytes], - properties: Properties | None = None, - ) -> tuple[MQTTErrorCode, int]: - remaining_length = 2 - if self._protocol == MQTTv5: - if properties is None: - packed_unsubscribe_properties = b'\x00' - else: - packed_unsubscribe_properties = properties.pack() - remaining_length += len(packed_unsubscribe_properties) - for t in topics: - remaining_length += 2 + len(t) - - command = UNSUBSCRIBE | (dup << 3) | 0x2 - packet = bytearray() - packet.append(command) - self._pack_remaining_length(packet, remaining_length) - local_mid = self._mid_generate() - packet.extend(struct.pack("!H", local_mid)) - - if self._protocol == MQTTv5: - packet += packed_unsubscribe_properties - - for t in topics: - self._pack_str16(packet, t) - - # topics_repr = ", ".join("'"+topic.decode('utf8')+"'" for topic in topics) - if self._protocol == MQTTv5: - self._easy_log( - MQTT_LOG_DEBUG, - "Sending UNSUBSCRIBE (d%d, m%d) %s %s", - dup, - local_mid, - properties, - topics, - ) - else: - self._easy_log( - MQTT_LOG_DEBUG, - "Sending UNSUBSCRIBE (d%d, m%d) %s", - dup, - local_mid, - topics, - ) - return (self._packet_queue(command, packet, local_mid, 1), local_mid) - - def _check_clean_session(self) -> bool: - if self._protocol == MQTTv5: - if self._clean_start == MQTT_CLEAN_START_FIRST_ONLY: - return self._mqttv5_first_connect - else: - return self._clean_start # type: ignore - else: - return self._clean_session - - def _messages_reconnect_reset_out(self) -> None: - with self._out_message_mutex: - self._inflight_messages = 0 - for m in self._out_messages.values(): - m.timestamp = 0 - if self._max_inflight_messages == 0 or self._inflight_messages < self._max_inflight_messages: - if m.qos == 0: - m.state = mqtt_ms_publish - elif m.qos == 1: - # self._inflight_messages = self._inflight_messages + 1 - if m.state == mqtt_ms_wait_for_puback: - m.dup = True - m.state = mqtt_ms_publish - elif m.qos == 2: - # self._inflight_messages = self._inflight_messages + 1 - if self._check_clean_session(): - if m.state != mqtt_ms_publish: - m.dup = True - m.state = mqtt_ms_publish - else: - if m.state == mqtt_ms_wait_for_pubcomp: - m.state = mqtt_ms_resend_pubrel - else: - if m.state == mqtt_ms_wait_for_pubrec: - m.dup = True - m.state = mqtt_ms_publish - else: - m.state = mqtt_ms_queued - - def _messages_reconnect_reset_in(self) -> None: - with self._in_message_mutex: - if self._check_clean_session(): - self._in_messages = collections.OrderedDict() - return - for m in self._in_messages.values(): - m.timestamp = 0 - if m.qos != 2: - self._in_messages.pop(m.mid) - else: - # Preserve current state - pass - - def _messages_reconnect_reset(self) -> None: - self._messages_reconnect_reset_out() - self._messages_reconnect_reset_in() - - def _packet_queue( - self, - command: int, - packet: bytes, - mid: int, - qos: int, - info: MQTTMessageInfo | None = None, - ) -> MQTTErrorCode: - mpkt: _OutPacket = { - "command": command, - "mid": mid, - "qos": qos, - "pos": 0, - "to_process": len(packet), - "packet": packet, - "info": info, - } - - self._out_packet.append(mpkt) - - # Write a single byte to sockpairW (connected to sockpairR) to break - # out of select() if in threaded mode. - if self._sockpairW is not None: - try: - self._sockpairW.send(sockpair_data) - except BlockingIOError: - pass - - # If we have an external event loop registered, use that instead - # of calling loop_write() directly. - if self._thread is None and self._on_socket_register_write is None: - if self._in_callback_mutex.acquire(False): - self._in_callback_mutex.release() - return self.loop_write() - - self._call_socket_register_write() - - return MQTTErrorCode.MQTT_ERR_SUCCESS - - def _packet_handle(self) -> MQTTErrorCode: - cmd = self._in_packet['command'] & 0xF0 - if cmd == PINGREQ: - return self._handle_pingreq() - elif cmd == PINGRESP: - return self._handle_pingresp() - elif cmd == PUBACK: - return self._handle_pubackcomp("PUBACK") - elif cmd == PUBCOMP: - return self._handle_pubackcomp("PUBCOMP") - elif cmd == PUBLISH: - return self._handle_publish() - elif cmd == PUBREC: - return self._handle_pubrec() - elif cmd == PUBREL: - return self._handle_pubrel() - elif cmd == CONNACK: - return self._handle_connack() - elif cmd == SUBACK: - self._handle_suback() - return MQTTErrorCode.MQTT_ERR_SUCCESS - elif cmd == UNSUBACK: - return self._handle_unsuback() - elif cmd == DISCONNECT and self._protocol == MQTTv5: # only allowed in MQTT 5.0 - self._handle_disconnect() - return MQTTErrorCode.MQTT_ERR_SUCCESS - else: - # If we don't recognise the command, return an error straight away. - self._easy_log(MQTT_LOG_ERR, "Error: Unrecognised command %s", cmd) - return MQTTErrorCode.MQTT_ERR_PROTOCOL - - def _handle_pingreq(self) -> MQTTErrorCode: - if self._in_packet['remaining_length'] != 0: - return MQTTErrorCode.MQTT_ERR_PROTOCOL - - self._easy_log(MQTT_LOG_DEBUG, "Received PINGREQ") - return self._send_pingresp() - - def _handle_pingresp(self) -> MQTTErrorCode: - if self._in_packet['remaining_length'] != 0: - return MQTTErrorCode.MQTT_ERR_PROTOCOL - - # No longer waiting for a PINGRESP. - self._ping_t = 0 - self._easy_log(MQTT_LOG_DEBUG, "Received PINGRESP") - return MQTTErrorCode.MQTT_ERR_SUCCESS - - def _handle_connack(self) -> MQTTErrorCode: - if self._protocol == MQTTv5: - if self._in_packet['remaining_length'] < 2: - return MQTTErrorCode.MQTT_ERR_PROTOCOL - elif self._in_packet['remaining_length'] != 2: - return MQTTErrorCode.MQTT_ERR_PROTOCOL - - if self._protocol == MQTTv5: - (flags, result) = struct.unpack( - "!BB", self._in_packet['packet'][:2]) - if result == 1: - # This is probably a failure from a broker that doesn't support - # MQTT v5. - reason = ReasonCode(CONNACK >> 4, aName="Unsupported protocol version") - properties = None - else: - reason = ReasonCode(CONNACK >> 4, identifier=result) - properties = Properties(CONNACK >> 4) - properties.unpack(self._in_packet['packet'][2:]) - else: - (flags, result) = struct.unpack("!BB", self._in_packet['packet']) - reason = convert_connack_rc_to_reason_code(result) - properties = None - if self._protocol == MQTTv311: - if result == CONNACK_REFUSED_PROTOCOL_VERSION: - if not self._reconnect_on_failure: - return MQTT_ERR_PROTOCOL - self._easy_log( - MQTT_LOG_DEBUG, - "Received CONNACK (%s, %s), attempting downgrade to MQTT v3.1.", - flags, result - ) - # Downgrade to MQTT v3.1 - self._protocol = MQTTv31 - return self.reconnect() - elif (result == CONNACK_REFUSED_IDENTIFIER_REJECTED - and self._client_id == b''): - if not self._reconnect_on_failure: - return MQTT_ERR_PROTOCOL - self._easy_log( - MQTT_LOG_DEBUG, - "Received CONNACK (%s, %s), attempting to use non-empty CID", - flags, result, - ) - self._client_id = _base62(uuid.uuid4().int, padding=22).encode("utf8") - return self.reconnect() - - if result == 0: - self._state = _ConnectionState.MQTT_CS_CONNECTED - self._reconnect_delay = None - - if self._protocol == MQTTv5: - self._easy_log( - MQTT_LOG_DEBUG, "Received CONNACK (%s, %s) properties=%s", flags, reason, properties) - else: - self._easy_log( - MQTT_LOG_DEBUG, "Received CONNACK (%s, %s)", flags, result) - - # it won't be the first successful connect any more - self._mqttv5_first_connect = False - - with self._callback_mutex: - on_connect = self.on_connect - - if on_connect: - flags_dict = {} - flags_dict['session present'] = flags & 0x01 - with self._in_callback_mutex: - try: - if self._callback_api_version == CallbackAPIVersion.VERSION1: - if self._protocol == MQTTv5: - on_connect = cast(CallbackOnConnect_v1_mqtt5, on_connect) - - on_connect(self, self._userdata, - flags_dict, reason, properties) - else: - on_connect = cast(CallbackOnConnect_v1_mqtt3, on_connect) - - on_connect( - self, self._userdata, flags_dict, result) - elif self._callback_api_version == CallbackAPIVersion.VERSION2: - on_connect = cast(CallbackOnConnect_v2, on_connect) - - connect_flags = ConnectFlags( - session_present=flags_dict['session present'] > 0 - ) - - if properties is None: - properties = Properties(PacketTypes.CONNACK) - - on_connect( - self, - self._userdata, - connect_flags, - reason, - properties, - ) - else: - raise RuntimeError("Unsupported callback API version") - except Exception as err: - self._easy_log( - MQTT_LOG_ERR, 'Caught exception in on_connect: %s', err) - if not self.suppress_exceptions: - raise - - if result == 0: - rc = MQTTErrorCode.MQTT_ERR_SUCCESS - with self._out_message_mutex: - for m in self._out_messages.values(): - m.timestamp = time_func() - if m.state == mqtt_ms_queued: - self.loop_write() # Process outgoing messages that have just been queued up - return MQTT_ERR_SUCCESS - - if m.qos == 0: - with self._in_callback_mutex: # Don't call loop_write after _send_publish() - rc = self._send_publish( - m.mid, - m.topic.encode('utf-8'), - m.payload, - m.qos, - m.retain, - m.dup, - properties=m.properties - ) - if rc != MQTTErrorCode.MQTT_ERR_SUCCESS: - return rc - elif m.qos == 1: - if m.state == mqtt_ms_publish: - self._inflight_messages += 1 - m.state = mqtt_ms_wait_for_puback - with self._in_callback_mutex: # Don't call loop_write after _send_publish() - rc = self._send_publish( - m.mid, - m.topic.encode('utf-8'), - m.payload, - m.qos, - m.retain, - m.dup, - properties=m.properties - ) - if rc != MQTTErrorCode.MQTT_ERR_SUCCESS: - return rc - elif m.qos == 2: - if m.state == mqtt_ms_publish: - self._inflight_messages += 1 - m.state = mqtt_ms_wait_for_pubrec - with self._in_callback_mutex: # Don't call loop_write after _send_publish() - rc = self._send_publish( - m.mid, - m.topic.encode('utf-8'), - m.payload, - m.qos, - m.retain, - m.dup, - properties=m.properties - ) - if rc != MQTTErrorCode.MQTT_ERR_SUCCESS: - return rc - elif m.state == mqtt_ms_resend_pubrel: - self._inflight_messages += 1 - m.state = mqtt_ms_wait_for_pubcomp - with self._in_callback_mutex: # Don't call loop_write after _send_publish() - rc = self._send_pubrel(m.mid) - if rc != MQTTErrorCode.MQTT_ERR_SUCCESS: - return rc - self.loop_write() # Process outgoing messages that have just been queued up - - return rc - elif result > 0 and result < 6: - return MQTTErrorCode.MQTT_ERR_CONN_REFUSED - else: - return MQTTErrorCode.MQTT_ERR_PROTOCOL - - def _handle_disconnect(self) -> None: - packet_type = DISCONNECT >> 4 - reasonCode = properties = None - if self._in_packet['remaining_length'] > 2: - reasonCode = ReasonCode(packet_type) - reasonCode.unpack(self._in_packet['packet']) - if self._in_packet['remaining_length'] > 3: - properties = Properties(packet_type) - props, props_len = properties.unpack( - self._in_packet['packet'][1:]) - self._easy_log(MQTT_LOG_DEBUG, "Received DISCONNECT %s %s", - reasonCode, - properties - ) - - self._sock_close() - self._do_on_disconnect( - packet_from_broker=True, - v1_rc=MQTTErrorCode.MQTT_ERR_SUCCESS, # If reason is absent (remaining length < 1), it means normal disconnection - reason=reasonCode, - properties=properties, - ) - - def _handle_suback(self) -> None: - self._easy_log(MQTT_LOG_DEBUG, "Received SUBACK") - pack_format = f"!H{len(self._in_packet['packet']) - 2}s" - (mid, packet) = struct.unpack(pack_format, self._in_packet['packet']) - - if self._protocol == MQTTv5: - properties = Properties(SUBACK >> 4) - props, props_len = properties.unpack(packet) - reasoncodes = [ReasonCode(SUBACK >> 4, identifier=c) for c in packet[props_len:]] - else: - pack_format = f"!{'B' * len(packet)}" - granted_qos = struct.unpack(pack_format, packet) - reasoncodes = [ReasonCode(SUBACK >> 4, identifier=c) for c in granted_qos] - properties = Properties(SUBACK >> 4) - - with self._callback_mutex: - on_subscribe = self.on_subscribe - - if on_subscribe: - with self._in_callback_mutex: # Don't call loop_write after _send_publish() - try: - if self._callback_api_version == CallbackAPIVersion.VERSION1: - if self._protocol == MQTTv5: - on_subscribe = cast(CallbackOnSubscribe_v1_mqtt5, on_subscribe) - - on_subscribe( - self, self._userdata, mid, reasoncodes, properties) - else: - on_subscribe = cast(CallbackOnSubscribe_v1_mqtt3, on_subscribe) - - on_subscribe( - self, self._userdata, mid, granted_qos) - elif self._callback_api_version == CallbackAPIVersion.VERSION2: - on_subscribe = cast(CallbackOnSubscribe_v2, on_subscribe) - - on_subscribe( - self, - self._userdata, - mid, - reasoncodes, - properties, - ) - else: - raise RuntimeError("Unsupported callback API version") - except Exception as err: - self._easy_log( - MQTT_LOG_ERR, 'Caught exception in on_subscribe: %s', err) - if not self.suppress_exceptions: - raise - - def _handle_publish(self) -> MQTTErrorCode: - header = self._in_packet['command'] - message = MQTTMessage() - message.dup = ((header & 0x08) >> 3) != 0 - message.qos = (header & 0x06) >> 1 - message.retain = (header & 0x01) != 0 - - pack_format = f"!H{len(self._in_packet['packet']) - 2}s" - (slen, packet) = struct.unpack(pack_format, self._in_packet['packet']) - pack_format = f"!{slen}s{len(packet) - slen}s" - (topic, packet) = struct.unpack(pack_format, packet) - - if self._protocol != MQTTv5 and len(topic) == 0: - return MQTTErrorCode.MQTT_ERR_PROTOCOL - - # Handle topics with invalid UTF-8 - # This replaces an invalid topic with a message and the hex - # representation of the topic for logging. When the user attempts to - # access message.topic in the callback, an exception will be raised. - try: - print_topic = topic.decode('utf-8') - except UnicodeDecodeError: - print_topic = f"TOPIC WITH INVALID UTF-8: {topic!r}" - - message.topic = topic - - if message.qos > 0: - pack_format = f"!H{len(packet) - 2}s" - (message.mid, packet) = struct.unpack(pack_format, packet) - - if self._protocol == MQTTv5: - message.properties = Properties(PUBLISH >> 4) - props, props_len = message.properties.unpack(packet) - packet = packet[props_len:] - - message.payload = packet - - if self._protocol == MQTTv5: - self._easy_log( - MQTT_LOG_DEBUG, - "Received PUBLISH (d%d, q%d, r%d, m%d), '%s', properties=%s, ... (%d bytes)", - message.dup, message.qos, message.retain, message.mid, - print_topic, message.properties, len(message.payload) - ) - else: - self._easy_log( - MQTT_LOG_DEBUG, - "Received PUBLISH (d%d, q%d, r%d, m%d), '%s', ... (%d bytes)", - message.dup, message.qos, message.retain, message.mid, - print_topic, len(message.payload) - ) - - message.timestamp = time_func() - if message.qos == 0: - self._handle_on_message(message) - return MQTTErrorCode.MQTT_ERR_SUCCESS - elif message.qos == 1: - self._handle_on_message(message) - if self._manual_ack: - return MQTTErrorCode.MQTT_ERR_SUCCESS - else: - return self._send_puback(message.mid) - elif message.qos == 2: - - rc = self._send_pubrec(message.mid) - - message.state = mqtt_ms_wait_for_pubrel - with self._in_message_mutex: - self._in_messages[message.mid] = message - - return rc - else: - return MQTTErrorCode.MQTT_ERR_PROTOCOL - - def ack(self, mid: int, qos: int) -> MQTTErrorCode: - """ - send an acknowledgement for a given message id (stored in :py:attr:`message.mid `). - only useful in QoS>=1 and ``manual_ack=True`` (option of `Client`) - """ - if self._manual_ack : - if qos == 1: - return self._send_puback(mid) - elif qos == 2: - return self._send_pubcomp(mid) - - return MQTTErrorCode.MQTT_ERR_SUCCESS - - def manual_ack_set(self, on: bool) -> None: - """ - The paho library normally acknowledges messages as soon as they are delivered to the caller. - If manual_ack is turned on, then the caller MUST manually acknowledge every message once - application processing is complete using `ack()` - """ - self._manual_ack = on - - - def _handle_pubrel(self) -> MQTTErrorCode: - if self._protocol == MQTTv5: - if self._in_packet['remaining_length'] < 2: - return MQTTErrorCode.MQTT_ERR_PROTOCOL - elif self._in_packet['remaining_length'] != 2: - return MQTTErrorCode.MQTT_ERR_PROTOCOL - - mid, = struct.unpack("!H", self._in_packet['packet'][:2]) - if self._protocol == MQTTv5: - if self._in_packet['remaining_length'] > 2: - reasonCode = ReasonCode(PUBREL >> 4) - reasonCode.unpack(self._in_packet['packet'][2:]) - if self._in_packet['remaining_length'] > 3: - properties = Properties(PUBREL >> 4) - props, props_len = properties.unpack( - self._in_packet['packet'][3:]) - self._easy_log(MQTT_LOG_DEBUG, "Received PUBREL (Mid: %d)", mid) - - with self._in_message_mutex: - if mid in self._in_messages: - # Only pass the message on if we have removed it from the queue - this - # prevents multiple callbacks for the same message. - message = self._in_messages.pop(mid) - self._handle_on_message(message) - self._inflight_messages -= 1 - if self._max_inflight_messages > 0: - with self._out_message_mutex: - rc = self._update_inflight() - if rc != MQTTErrorCode.MQTT_ERR_SUCCESS: - return rc - - # FIXME: this should only be done if the message is known - # If unknown it's a protocol error and we should close the connection. - # But since we don't have (on disk) persistence for the session, it - # is possible that we must known about this message. - # Choose to acknowledge this message (thus losing a message) but - # avoid hanging. See #284. - if self._manual_ack: - return MQTTErrorCode.MQTT_ERR_SUCCESS - else: - return self._send_pubcomp(mid) - - def _update_inflight(self) -> MQTTErrorCode: - # Dont lock message_mutex here - for m in self._out_messages.values(): - if self._inflight_messages < self._max_inflight_messages: - if m.qos > 0 and m.state == mqtt_ms_queued: - self._inflight_messages += 1 - if m.qos == 1: - m.state = mqtt_ms_wait_for_puback - elif m.qos == 2: - m.state = mqtt_ms_wait_for_pubrec - rc = self._send_publish( - m.mid, - m.topic.encode('utf-8'), - m.payload, - m.qos, - m.retain, - m.dup, - properties=m.properties, - ) - if rc != MQTTErrorCode.MQTT_ERR_SUCCESS: - return rc - else: - return MQTTErrorCode.MQTT_ERR_SUCCESS - return MQTTErrorCode.MQTT_ERR_SUCCESS - - def _handle_pubrec(self) -> MQTTErrorCode: - if self._protocol == MQTTv5: - if self._in_packet['remaining_length'] < 2: - return MQTTErrorCode.MQTT_ERR_PROTOCOL - elif self._in_packet['remaining_length'] != 2: - return MQTTErrorCode.MQTT_ERR_PROTOCOL - - mid, = struct.unpack("!H", self._in_packet['packet'][:2]) - if self._protocol == MQTTv5: - if self._in_packet['remaining_length'] > 2: - reasonCode = ReasonCode(PUBREC >> 4) - reasonCode.unpack(self._in_packet['packet'][2:]) - if self._in_packet['remaining_length'] > 3: - properties = Properties(PUBREC >> 4) - props, props_len = properties.unpack( - self._in_packet['packet'][3:]) - self._easy_log(MQTT_LOG_DEBUG, "Received PUBREC (Mid: %d)", mid) - - with self._out_message_mutex: - if mid in self._out_messages: - msg = self._out_messages[mid] - msg.state = mqtt_ms_wait_for_pubcomp - msg.timestamp = time_func() - return self._send_pubrel(mid) - - return MQTTErrorCode.MQTT_ERR_SUCCESS - - def _handle_unsuback(self) -> MQTTErrorCode: - if self._protocol == MQTTv5: - if self._in_packet['remaining_length'] < 4: - return MQTTErrorCode.MQTT_ERR_PROTOCOL - elif self._in_packet['remaining_length'] != 2: - return MQTTErrorCode.MQTT_ERR_PROTOCOL - - mid, = struct.unpack("!H", self._in_packet['packet'][:2]) - if self._protocol == MQTTv5: - packet = self._in_packet['packet'][2:] - properties = Properties(UNSUBACK >> 4) - props, props_len = properties.unpack(packet) - reasoncodes_list = [ - ReasonCode(UNSUBACK >> 4, identifier=c) - for c in packet[props_len:] - ] - else: - reasoncodes_list = [] - properties = Properties(UNSUBACK >> 4) - - self._easy_log(MQTT_LOG_DEBUG, "Received UNSUBACK (Mid: %d)", mid) - with self._callback_mutex: - on_unsubscribe = self.on_unsubscribe - - if on_unsubscribe: - with self._in_callback_mutex: - try: - if self._callback_api_version == CallbackAPIVersion.VERSION1: - if self._protocol == MQTTv5: - on_unsubscribe = cast(CallbackOnUnsubscribe_v1_mqtt5, on_unsubscribe) - - reasoncodes: ReasonCode | list[ReasonCode] = reasoncodes_list - if len(reasoncodes_list) == 1: - reasoncodes = reasoncodes_list[0] - - on_unsubscribe( - self, self._userdata, mid, properties, reasoncodes) - else: - on_unsubscribe = cast(CallbackOnUnsubscribe_v1_mqtt3, on_unsubscribe) - - on_unsubscribe(self, self._userdata, mid) - elif self._callback_api_version == CallbackAPIVersion.VERSION2: - on_unsubscribe = cast(CallbackOnUnsubscribe_v2, on_unsubscribe) - - if properties is None: - properties = Properties(PacketTypes.CONNACK) - - on_unsubscribe( - self, - self._userdata, - mid, - reasoncodes_list, - properties, - ) - else: - raise RuntimeError("Unsupported callback API version") - except Exception as err: - self._easy_log( - MQTT_LOG_ERR, 'Caught exception in on_unsubscribe: %s', err) - if not self.suppress_exceptions: - raise - - return MQTTErrorCode.MQTT_ERR_SUCCESS - - def _do_on_disconnect( - self, - packet_from_broker: bool, - v1_rc: MQTTErrorCode, - reason: ReasonCode | None = None, - properties: Properties | None = None, - ) -> None: - with self._callback_mutex: - on_disconnect = self.on_disconnect - - if on_disconnect: - with self._in_callback_mutex: - try: - if self._callback_api_version == CallbackAPIVersion.VERSION1: - if self._protocol == MQTTv5: - on_disconnect = cast(CallbackOnDisconnect_v1_mqtt5, on_disconnect) - - if packet_from_broker: - on_disconnect(self, self._userdata, reason, properties) - else: - on_disconnect(self, self._userdata, v1_rc, None) - else: - on_disconnect = cast(CallbackOnDisconnect_v1_mqtt3, on_disconnect) - - on_disconnect(self, self._userdata, v1_rc) - elif self._callback_api_version == CallbackAPIVersion.VERSION2: - on_disconnect = cast(CallbackOnDisconnect_v2, on_disconnect) - - disconnect_flags = DisconnectFlags( - is_disconnect_packet_from_server=packet_from_broker - ) - - if reason is None: - reason = convert_disconnect_error_code_to_reason_code(v1_rc) - - if properties is None: - properties = Properties(PacketTypes.DISCONNECT) - - on_disconnect( - self, - self._userdata, - disconnect_flags, - reason, - properties, - ) - else: - raise RuntimeError("Unsupported callback API version") - except Exception as err: - self._easy_log( - MQTT_LOG_ERR, 'Caught exception in on_disconnect: %s', err) - if not self.suppress_exceptions: - raise - - def _do_on_publish(self, mid: int, reason_code: ReasonCode, properties: Properties) -> MQTTErrorCode: - with self._callback_mutex: - on_publish = self.on_publish - - if on_publish: - with self._in_callback_mutex: - try: - if self._callback_api_version == CallbackAPIVersion.VERSION1: - on_publish = cast(CallbackOnPublish_v1, on_publish) - - on_publish(self, self._userdata, mid) - elif self._callback_api_version == CallbackAPIVersion.VERSION2: - on_publish = cast(CallbackOnPublish_v2, on_publish) - - on_publish( - self, - self._userdata, - mid, - reason_code, - properties, - ) - else: - raise RuntimeError("Unsupported callback API version") - except Exception as err: - self._easy_log( - MQTT_LOG_ERR, 'Caught exception in on_publish: %s', err) - if not self.suppress_exceptions: - raise - - msg = self._out_messages.pop(mid) - msg.info._set_as_published() - if msg.qos > 0: - self._inflight_messages -= 1 - if self._max_inflight_messages > 0: - rc = self._update_inflight() - if rc != MQTTErrorCode.MQTT_ERR_SUCCESS: - return rc - return MQTTErrorCode.MQTT_ERR_SUCCESS - - def _handle_pubackcomp( - self, cmd: Literal['PUBACK'] | Literal['PUBCOMP'] - ) -> MQTTErrorCode: - if self._protocol == MQTTv5: - if self._in_packet['remaining_length'] < 2: - return MQTTErrorCode.MQTT_ERR_PROTOCOL - elif self._in_packet['remaining_length'] != 2: - return MQTTErrorCode.MQTT_ERR_PROTOCOL - - packet_type_enum = PUBACK if cmd == "PUBACK" else PUBCOMP - packet_type = packet_type_enum.value >> 4 - mid, = struct.unpack("!H", self._in_packet['packet'][:2]) - reasonCode = ReasonCode(packet_type) - properties = Properties(packet_type) - if self._protocol == MQTTv5: - if self._in_packet['remaining_length'] > 2: - reasonCode.unpack(self._in_packet['packet'][2:]) - if self._in_packet['remaining_length'] > 3: - props, props_len = properties.unpack( - self._in_packet['packet'][3:]) - self._easy_log(MQTT_LOG_DEBUG, "Received %s (Mid: %d)", cmd, mid) - - with self._out_message_mutex: - if mid in self._out_messages: - # Only inform the client the message has been sent once. - rc = self._do_on_publish(mid, reasonCode, properties) - return rc - - return MQTTErrorCode.MQTT_ERR_SUCCESS - - def _handle_on_message(self, message: MQTTMessage) -> None: - - try: - topic = message.topic - except UnicodeDecodeError: - topic = None - - on_message_callbacks = [] - with self._callback_mutex: - if topic is not None: - on_message_callbacks = list(self._on_message_filtered.iter_match(message.topic)) - - if len(on_message_callbacks) == 0: - on_message = self.on_message - else: - on_message = None - - for callback in on_message_callbacks: - with self._in_callback_mutex: - try: - callback(self, self._userdata, message) - except Exception as err: - self._easy_log( - MQTT_LOG_ERR, - 'Caught exception in user defined callback function %s: %s', - callback.__name__, - err - ) - if not self.suppress_exceptions: - raise - - if on_message: - with self._in_callback_mutex: - try: - on_message(self, self._userdata, message) - except Exception as err: - self._easy_log( - MQTT_LOG_ERR, 'Caught exception in on_message: %s', err) - if not self.suppress_exceptions: - raise - - - def _handle_on_connect_fail(self) -> None: - with self._callback_mutex: - on_connect_fail = self.on_connect_fail - - if on_connect_fail: - with self._in_callback_mutex: - try: - on_connect_fail(self, self._userdata) - except Exception as err: - self._easy_log( - MQTT_LOG_ERR, 'Caught exception in on_connect_fail: %s', err) - - def _thread_main(self) -> None: - try: - self.loop_forever(retry_first_connection=True) - finally: - self._thread = None - - def _reconnect_wait(self) -> None: - # See reconnect_delay_set for details - now = time_func() - with self._reconnect_delay_mutex: - if self._reconnect_delay is None: - self._reconnect_delay = self._reconnect_min_delay - else: - self._reconnect_delay = min( - self._reconnect_delay * 2, - self._reconnect_max_delay, - ) - - target_time = now + self._reconnect_delay - - remaining = target_time - now - while (self._state not in (_ConnectionState.MQTT_CS_DISCONNECTING, _ConnectionState.MQTT_CS_DISCONNECTED) - and not self._thread_terminate - and remaining > 0): - - time.sleep(min(remaining, 1)) - remaining = target_time - time_func() - - @staticmethod - def _proxy_is_valid(p) -> bool: # type: ignore[no-untyped-def] - def check(t, a) -> bool: # type: ignore[no-untyped-def] - return (socks is not None and - t in {socks.HTTP, socks.SOCKS4, socks.SOCKS5} and a) - - if isinstance(p, dict): - return check(p.get("proxy_type"), p.get("proxy_addr")) - elif isinstance(p, (list, tuple)): - return len(p) == 6 and check(p[0], p[1]) - else: - return False - - def _get_proxy(self) -> dict[str, Any] | None: - if socks is None: - return None - - # First, check if the user explicitly passed us a proxy to use - if self._proxy_is_valid(self._proxy): - return self._proxy - - # Next, check for an mqtt_proxy environment variable as long as the host - # we're trying to connect to isn't listed under the no_proxy environment - # variable (matches built-in module urllib's behavior) - if not (hasattr(urllib.request, "proxy_bypass") and - urllib.request.proxy_bypass(self._host)): - env_proxies = urllib.request.getproxies() - if "mqtt" in env_proxies: - parts = urllib.parse.urlparse(env_proxies["mqtt"]) - if parts.scheme == "http": - proxy = { - "proxy_type": socks.HTTP, - "proxy_addr": parts.hostname, - "proxy_port": parts.port - } - return proxy - elif parts.scheme == "socks": - proxy = { - "proxy_type": socks.SOCKS5, - "proxy_addr": parts.hostname, - "proxy_port": parts.port - } - return proxy - - # Finally, check if the user has monkeypatched the PySocks library with - # a default proxy - socks_default = socks.get_default_proxy() - if self._proxy_is_valid(socks_default): - proxy_keys = ("proxy_type", "proxy_addr", "proxy_port", - "proxy_rdns", "proxy_username", "proxy_password") - return dict(zip(proxy_keys, socks_default)) - - # If we didn't find a proxy through any of the above methods, return - # None to indicate that the connection should be handled normally - return None - - def _create_socket(self) -> SocketLike: - if self._transport == "unix": - sock = self._create_unix_socket_connection() - else: - sock = self._create_socket_connection() - - if self._ssl: - sock = self._ssl_wrap_socket(sock) - - if self._transport == "websockets": - sock.settimeout(self._keepalive) - return _WebsocketWrapper( - socket=sock, - host=self._host, - port=self._port, - is_ssl=self._ssl, - path=self._websocket_path, - extra_headers=self._websocket_extra_headers, - ) - - return sock - - def _create_unix_socket_connection(self) -> _socket.socket: - unix_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - unix_socket.connect(self._host) - return unix_socket - - def _create_socket_connection(self) -> _socket.socket: - proxy = self._get_proxy() - addr = (self._host, self._port) - source = (self._bind_address, self._bind_port) - - if proxy: - return socks.create_connection(addr, timeout=self._connect_timeout, source_address=source, **proxy) - else: - return socket.create_connection(addr, timeout=self._connect_timeout, source_address=source) - - def _ssl_wrap_socket(self, tcp_sock: _socket.socket) -> ssl.SSLSocket: - if self._ssl_context is None: - raise ValueError( - "Impossible condition. _ssl_context should never be None if _ssl is True" - ) - - verify_host = not self._tls_insecure - try: - # Try with server_hostname, even it's not supported in certain scenarios - ssl_sock = self._ssl_context.wrap_socket( - tcp_sock, - server_hostname=self._host, - do_handshake_on_connect=False, - ) - except ssl.CertificateError: - # CertificateError is derived from ValueError - raise - except ValueError: - # Python version requires SNI in order to handle server_hostname, but SNI is not available - ssl_sock = self._ssl_context.wrap_socket( - tcp_sock, - do_handshake_on_connect=False, - ) - else: - # If SSL context has already checked hostname, then don't need to do it again - if getattr(self._ssl_context, 'check_hostname', False): # type: ignore - verify_host = False - - ssl_sock.settimeout(self._keepalive) - ssl_sock.do_handshake() - - if verify_host: - # TODO: this type error is a true error: - # error: Module has no attribute "match_hostname" [attr-defined] - # Python 3.12 no longer have this method. - ssl.match_hostname(ssl_sock.getpeercert(), self._host) # type: ignore - - return ssl_sock - -class _WebsocketWrapper: - OPCODE_CONTINUATION = 0x0 - OPCODE_TEXT = 0x1 - OPCODE_BINARY = 0x2 - OPCODE_CONNCLOSE = 0x8 - OPCODE_PING = 0x9 - OPCODE_PONG = 0xa - - def __init__( - self, - socket: socket.socket | ssl.SSLSocket, - host: str, - port: int, - is_ssl: bool, - path: str, - extra_headers: WebSocketHeaders | None, - ): - self.connected = False - - self._ssl = is_ssl - self._host = host - self._port = port - self._socket = socket - self._path = path - - self._sendbuffer = bytearray() - self._readbuffer = bytearray() - - self._requested_size = 0 - self._payload_head = 0 - self._readbuffer_head = 0 - - self._do_handshake(extra_headers) - - def __del__(self) -> None: - self._sendbuffer = bytearray() - self._readbuffer = bytearray() - - def _do_handshake(self, extra_headers: WebSocketHeaders | None) -> None: - - sec_websocket_key = uuid.uuid4().bytes - sec_websocket_key = base64.b64encode(sec_websocket_key) - - if self._ssl: - default_port = 443 - http_schema = "https" - else: - default_port = 80 - http_schema = "http" - - if default_port == self._port: - host_port = f"{self._host}" - else: - host_port = f"{self._host}:{self._port}" - - websocket_headers = { - "Host": host_port, - "Upgrade": "websocket", - "Connection": "Upgrade", - "Origin": f"{http_schema}://{host_port}", - "Sec-WebSocket-Key": sec_websocket_key.decode("utf8"), - "Sec-Websocket-Version": "13", - "Sec-Websocket-Protocol": "mqtt", - } - - # This is checked in ws_set_options so it will either be None, a - # dictionary, or a callable - if isinstance(extra_headers, dict): - websocket_headers.update(extra_headers) - elif callable(extra_headers): - websocket_headers = extra_headers(websocket_headers) - - header = "\r\n".join([ - f"GET {self._path} HTTP/1.1", - "\r\n".join(f"{i}: {j}" for i, j in websocket_headers.items()), - "\r\n", - ]).encode("utf8") - - self._socket.send(header) - - has_secret = False - has_upgrade = False - - while True: - # read HTTP response header as lines - try: - byte = self._socket.recv(1) - except ConnectionResetError: - byte = b"" - - self._readbuffer.extend(byte) - - # line end - if byte == b"\n": - if len(self._readbuffer) > 2: - # check upgrade - if b"connection" in str(self._readbuffer).lower().encode('utf-8'): - if b"upgrade" not in str(self._readbuffer).lower().encode('utf-8'): - raise WebsocketConnectionError( - "WebSocket handshake error, connection not upgraded") - else: - has_upgrade = True - - # check key hash - if b"sec-websocket-accept" in str(self._readbuffer).lower().encode('utf-8'): - GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" - - server_hash_str = self._readbuffer.decode( - 'utf-8').split(": ", 1)[1] - server_hash = server_hash_str.strip().encode('utf-8') - - client_hash_key = sec_websocket_key.decode('utf-8') + GUID - # Use of SHA-1 is OK here; it's according to the Websocket spec. - client_hash_digest = hashlib.sha1(client_hash_key.encode('utf-8')) # noqa: S324 - client_hash = base64.b64encode(client_hash_digest.digest()) - - if server_hash != client_hash: - raise WebsocketConnectionError( - "WebSocket handshake error, invalid secret key") - else: - has_secret = True - else: - # ending linebreak - break - - # reset linebuffer - self._readbuffer = bytearray() - - # connection reset - elif not byte: - raise WebsocketConnectionError("WebSocket handshake error") - - if not has_upgrade or not has_secret: - raise WebsocketConnectionError("WebSocket handshake error") - - self._readbuffer = bytearray() - self.connected = True - - def _create_frame( - self, opcode: int, data: bytearray, do_masking: int = 1 - ) -> bytearray: - header = bytearray() - length = len(data) - - mask_key = bytearray(os.urandom(4)) - mask_flag = do_masking - - # 1 << 7 is the final flag, we don't send continuated data - header.append(1 << 7 | opcode) - - if length < 126: - header.append(mask_flag << 7 | length) - - elif length < 65536: - header.append(mask_flag << 7 | 126) - header += struct.pack("!H", length) - - elif length < 0x8000000000000001: - header.append(mask_flag << 7 | 127) - header += struct.pack("!Q", length) - - else: - raise ValueError("Maximum payload size is 2^63") - - if mask_flag == 1: - for index in range(length): - data[index] ^= mask_key[index % 4] - data = mask_key + data - - return header + data - - def _buffered_read(self, length: int) -> bytearray: - - # try to recv and store needed bytes - wanted_bytes = length - (len(self._readbuffer) - self._readbuffer_head) - if wanted_bytes > 0: - - data = self._socket.recv(wanted_bytes) - - if not data: - raise ConnectionAbortedError - else: - self._readbuffer.extend(data) - - if len(data) < wanted_bytes: - raise BlockingIOError - - self._readbuffer_head += length - return self._readbuffer[self._readbuffer_head - length:self._readbuffer_head] - - def _recv_impl(self, length: int) -> bytes: - - # try to decode websocket payload part from data - try: - - self._readbuffer_head = 0 - - result = b"" - - chunk_startindex = self._payload_head - chunk_endindex = self._payload_head + length - - header1 = self._buffered_read(1) - header2 = self._buffered_read(1) - - opcode = (header1[0] & 0x0f) - maskbit = (header2[0] & 0x80) == 0x80 - lengthbits = (header2[0] & 0x7f) - payload_length = lengthbits - mask_key = None - - # read length - if lengthbits == 0x7e: - - value = self._buffered_read(2) - payload_length, = struct.unpack("!H", value) - - elif lengthbits == 0x7f: - - value = self._buffered_read(8) - payload_length, = struct.unpack("!Q", value) - - # read mask - if maskbit: - mask_key = self._buffered_read(4) - - # if frame payload is shorter than the requested data, read only the possible part - readindex = chunk_endindex - if payload_length < readindex: - readindex = payload_length - - if readindex > 0: - # get payload chunk - payload = self._buffered_read(readindex) - - # unmask only the needed part - if mask_key is not None: - for index in range(chunk_startindex, readindex): - payload[index] ^= mask_key[index % 4] - - result = payload[chunk_startindex:readindex] - self._payload_head = readindex - else: - payload = bytearray() - - # check if full frame arrived and reset readbuffer and payloadhead if needed - if readindex == payload_length: - self._readbuffer = bytearray() - self._payload_head = 0 - - # respond to non-binary opcodes, their arrival is not guaranteed because of non-blocking sockets - if opcode == _WebsocketWrapper.OPCODE_CONNCLOSE: - frame = self._create_frame( - _WebsocketWrapper.OPCODE_CONNCLOSE, payload, 0) - self._socket.send(frame) - - if opcode == _WebsocketWrapper.OPCODE_PING: - frame = self._create_frame( - _WebsocketWrapper.OPCODE_PONG, payload, 0) - self._socket.send(frame) - - # This isn't *proper* handling of continuation frames, but given - # that we only support binary frames, it is *probably* good enough. - if (opcode == _WebsocketWrapper.OPCODE_BINARY or opcode == _WebsocketWrapper.OPCODE_CONTINUATION) \ - and payload_length > 0: - return result - else: - raise BlockingIOError - - except ConnectionError: - self.connected = False - return b'' - - def _send_impl(self, data: bytes) -> int: - - # if previous frame was sent successfully - if len(self._sendbuffer) == 0: - # create websocket frame - frame = self._create_frame( - _WebsocketWrapper.OPCODE_BINARY, bytearray(data)) - self._sendbuffer.extend(frame) - self._requested_size = len(data) - - # try to write out as much as possible - length = self._socket.send(self._sendbuffer) - - self._sendbuffer = self._sendbuffer[length:] - - if len(self._sendbuffer) == 0: - # buffer sent out completely, return with payload's size - return self._requested_size - else: - # couldn't send whole data, request the same data again with 0 as sent length - return 0 - - def recv(self, length: int) -> bytes: - return self._recv_impl(length) - - def read(self, length: int) -> bytes: - return self._recv_impl(length) - - def send(self, data: bytes) -> int: - return self._send_impl(data) - - def write(self, data: bytes) -> int: - return self._send_impl(data) - - def close(self) -> None: - self._socket.close() - - def fileno(self) -> int: - return self._socket.fileno() - - def pending(self) -> int: - # Fix for bug #131: a SSL socket may still have data available - # for reading without select() being aware of it. - if self._ssl: - return self._socket.pending() # type: ignore[union-attr] - else: - # normal socket rely only on select() - return 0 - - def setblocking(self, flag: bool) -> None: - self._socket.setblocking(flag) diff --git a/scripts/tempSensor/lib/paho/mqtt/enums.py b/scripts/tempSensor/lib/paho/mqtt/enums.py deleted file mode 100644 index 5428769f..00000000 --- a/scripts/tempSensor/lib/paho/mqtt/enums.py +++ /dev/null @@ -1,113 +0,0 @@ -import enum - - -class MQTTErrorCode(enum.IntEnum): - MQTT_ERR_AGAIN = -1 - MQTT_ERR_SUCCESS = 0 - MQTT_ERR_NOMEM = 1 - MQTT_ERR_PROTOCOL = 2 - MQTT_ERR_INVAL = 3 - MQTT_ERR_NO_CONN = 4 - MQTT_ERR_CONN_REFUSED = 5 - MQTT_ERR_NOT_FOUND = 6 - MQTT_ERR_CONN_LOST = 7 - MQTT_ERR_TLS = 8 - MQTT_ERR_PAYLOAD_SIZE = 9 - MQTT_ERR_NOT_SUPPORTED = 10 - MQTT_ERR_AUTH = 11 - MQTT_ERR_ACL_DENIED = 12 - MQTT_ERR_UNKNOWN = 13 - MQTT_ERR_ERRNO = 14 - MQTT_ERR_QUEUE_SIZE = 15 - MQTT_ERR_KEEPALIVE = 16 - - -class MQTTProtocolVersion(enum.IntEnum): - MQTTv31 = 3 - MQTTv311 = 4 - MQTTv5 = 5 - - -class CallbackAPIVersion(enum.Enum): - """Defined the arguments passed to all user-callback. - - See each callbacks for details: `on_connect`, `on_connect_fail`, `on_disconnect`, `on_message`, `on_publish`, - `on_subscribe`, `on_unsubscribe`, `on_log`, `on_socket_open`, `on_socket_close`, - `on_socket_register_write`, `on_socket_unregister_write` - """ - VERSION1 = 1 - """The version used with paho-mqtt 1.x before introducing CallbackAPIVersion. - - This version had different arguments depending if MQTTv5 or MQTTv3 was used. `Properties` & `ReasonCode` were missing - on some callback (apply only to MQTTv5). - - This version is deprecated and will be removed in version 3.0. - """ - VERSION2 = 2 - """ This version fix some of the shortcoming of previous version. - - Callback have the same signature if using MQTTv5 or MQTTv3. `ReasonCode` are used in MQTTv3. - """ - - -class MessageType(enum.IntEnum): - CONNECT = 0x10 - CONNACK = 0x20 - PUBLISH = 0x30 - PUBACK = 0x40 - PUBREC = 0x50 - PUBREL = 0x60 - PUBCOMP = 0x70 - SUBSCRIBE = 0x80 - SUBACK = 0x90 - UNSUBSCRIBE = 0xA0 - UNSUBACK = 0xB0 - PINGREQ = 0xC0 - PINGRESP = 0xD0 - DISCONNECT = 0xE0 - AUTH = 0xF0 - - -class LogLevel(enum.IntEnum): - MQTT_LOG_INFO = 0x01 - MQTT_LOG_NOTICE = 0x02 - MQTT_LOG_WARNING = 0x04 - MQTT_LOG_ERR = 0x08 - MQTT_LOG_DEBUG = 0x10 - - -class ConnackCode(enum.IntEnum): - CONNACK_ACCEPTED = 0 - CONNACK_REFUSED_PROTOCOL_VERSION = 1 - CONNACK_REFUSED_IDENTIFIER_REJECTED = 2 - CONNACK_REFUSED_SERVER_UNAVAILABLE = 3 - CONNACK_REFUSED_BAD_USERNAME_PASSWORD = 4 - CONNACK_REFUSED_NOT_AUTHORIZED = 5 - - -class _ConnectionState(enum.Enum): - MQTT_CS_NEW = enum.auto() - MQTT_CS_CONNECT_ASYNC = enum.auto() - MQTT_CS_CONNECTING = enum.auto() - MQTT_CS_CONNECTED = enum.auto() - MQTT_CS_CONNECTION_LOST = enum.auto() - MQTT_CS_DISCONNECTING = enum.auto() - MQTT_CS_DISCONNECTED = enum.auto() - - -class MessageState(enum.IntEnum): - MQTT_MS_INVALID = 0 - MQTT_MS_PUBLISH = 1 - MQTT_MS_WAIT_FOR_PUBACK = 2 - MQTT_MS_WAIT_FOR_PUBREC = 3 - MQTT_MS_RESEND_PUBREL = 4 - MQTT_MS_WAIT_FOR_PUBREL = 5 - MQTT_MS_RESEND_PUBCOMP = 6 - MQTT_MS_WAIT_FOR_PUBCOMP = 7 - MQTT_MS_SEND_PUBREC = 8 - MQTT_MS_QUEUED = 9 - - -class PahoClientMode(enum.IntEnum): - MQTT_CLIENT = 0 - MQTT_BRIDGE = 1 diff --git a/scripts/tempSensor/lib/paho/mqtt/matcher.py b/scripts/tempSensor/lib/paho/mqtt/matcher.py deleted file mode 100644 index b73c13ac..00000000 --- a/scripts/tempSensor/lib/paho/mqtt/matcher.py +++ /dev/null @@ -1,78 +0,0 @@ -class MQTTMatcher: - """Intended to manage topic filters including wildcards. - - Internally, MQTTMatcher use a prefix tree (trie) to store - values associated with filters, and has an iter_match() - method to iterate efficiently over all filters that match - some topic name.""" - - class Node: - __slots__ = '_children', '_content' - - def __init__(self): - self._children = {} - self._content = None - - def __init__(self): - self._root = self.Node() - - def __setitem__(self, key, value): - """Add a topic filter :key to the prefix tree - and associate it to :value""" - node = self._root - for sym in key.split('/'): - node = node._children.setdefault(sym, self.Node()) - node._content = value - - def __getitem__(self, key): - """Retrieve the value associated with some topic filter :key""" - try: - node = self._root - for sym in key.split('/'): - node = node._children[sym] - if node._content is None: - raise KeyError(key) - return node._content - except KeyError as ke: - raise KeyError(key) from ke - - def __delitem__(self, key): - """Delete the value associated with some topic filter :key""" - lst = [] - try: - parent, node = None, self._root - for k in key.split('/'): - parent, node = node, node._children[k] - lst.append((parent, k, node)) - # TODO - node._content = None - except KeyError as ke: - raise KeyError(key) from ke - else: # cleanup - for parent, k, node in reversed(lst): - if node._children or node._content is not None: - break - del parent._children[k] - - def iter_match(self, topic): - """Return an iterator on all values associated with filters - that match the :topic""" - lst = topic.split('/') - normal = not topic.startswith('$') - def rec(node, i=0): - if i == len(lst): - if node._content is not None: - yield node._content - else: - part = lst[i] - if part in node._children: - for content in rec(node._children[part], i + 1): - yield content - if '+' in node._children and (normal or i > 0): - for content in rec(node._children['+'], i + 1): - yield content - if '#' in node._children and (normal or i > 0): - content = node._children['#']._content - if content is not None: - yield content - return rec(self._root) diff --git a/scripts/tempSensor/lib/paho/mqtt/packettypes.py b/scripts/tempSensor/lib/paho/mqtt/packettypes.py deleted file mode 100644 index d2051490..00000000 --- a/scripts/tempSensor/lib/paho/mqtt/packettypes.py +++ /dev/null @@ -1,43 +0,0 @@ -""" -******************************************************************* - Copyright (c) 2017, 2019 IBM Corp. - - All rights reserved. This program and the accompanying materials - are made available under the terms of the Eclipse Public License v2.0 - and Eclipse Distribution License v1.0 which accompany this distribution. - - The Eclipse Public License is available at - http://www.eclipse.org/legal/epl-v20.html - and the Eclipse Distribution License is available at - http://www.eclipse.org/org/documents/edl-v10.php. - - Contributors: - Ian Craggs - initial implementation and/or documentation -******************************************************************* -""" - - -class PacketTypes: - - """ - Packet types class. Includes the AUTH packet for MQTT v5.0. - - Holds constants for each packet type such as PacketTypes.PUBLISH - and packet name strings: PacketTypes.Names[PacketTypes.PUBLISH]. - - """ - - indexes = range(1, 16) - - # Packet types - CONNECT, CONNACK, PUBLISH, PUBACK, PUBREC, PUBREL, \ - PUBCOMP, SUBSCRIBE, SUBACK, UNSUBSCRIBE, UNSUBACK, \ - PINGREQ, PINGRESP, DISCONNECT, AUTH = indexes - - # Dummy packet type for properties use - will delay only applies to will - WILLMESSAGE = 99 - - Names = ( "reserved", \ - "Connect", "Connack", "Publish", "Puback", "Pubrec", "Pubrel", \ - "Pubcomp", "Subscribe", "Suback", "Unsubscribe", "Unsuback", \ - "Pingreq", "Pingresp", "Disconnect", "Auth") diff --git a/scripts/tempSensor/lib/paho/mqtt/properties.py b/scripts/tempSensor/lib/paho/mqtt/properties.py deleted file mode 100644 index f307b865..00000000 --- a/scripts/tempSensor/lib/paho/mqtt/properties.py +++ /dev/null @@ -1,421 +0,0 @@ -# ******************************************************************* -# Copyright (c) 2017, 2019 IBM Corp. -# -# All rights reserved. This program and the accompanying materials -# are made available under the terms of the Eclipse Public License v2.0 -# and Eclipse Distribution License v1.0 which accompany this distribution. -# -# The Eclipse Public License is available at -# http://www.eclipse.org/legal/epl-v20.html -# and the Eclipse Distribution License is available at -# http://www.eclipse.org/org/documents/edl-v10.php. -# -# Contributors: -# Ian Craggs - initial implementation and/or documentation -# ******************************************************************* - -import struct - -from .packettypes import PacketTypes - - -class MQTTException(Exception): - pass - - -class MalformedPacket(MQTTException): - pass - - -def writeInt16(length): - # serialize a 16 bit integer to network format - return bytearray(struct.pack("!H", length)) - - -def readInt16(buf): - # deserialize a 16 bit integer from network format - return struct.unpack("!H", buf[:2])[0] - - -def writeInt32(length): - # serialize a 32 bit integer to network format - return bytearray(struct.pack("!L", length)) - - -def readInt32(buf): - # deserialize a 32 bit integer from network format - return struct.unpack("!L", buf[:4])[0] - - -def writeUTF(data): - # data could be a string, or bytes. If string, encode into bytes with utf-8 - if not isinstance(data, bytes): - data = bytes(data, "utf-8") - return writeInt16(len(data)) + data - - -def readUTF(buffer, maxlen): - if maxlen >= 2: - length = readInt16(buffer) - else: - raise MalformedPacket("Not enough data to read string length") - maxlen -= 2 - if length > maxlen: - raise MalformedPacket("Length delimited string too long") - buf = buffer[2:2+length].decode("utf-8") - # look for chars which are invalid for MQTT - for c in buf: # look for D800-DFFF in the UTF string - ord_c = ord(c) - if ord_c >= 0xD800 and ord_c <= 0xDFFF: - raise MalformedPacket("[MQTT-1.5.4-1] D800-DFFF found in UTF-8 data") - if ord_c == 0x00: # look for null in the UTF string - raise MalformedPacket("[MQTT-1.5.4-2] Null found in UTF-8 data") - if ord_c == 0xFEFF: - raise MalformedPacket("[MQTT-1.5.4-3] U+FEFF in UTF-8 data") - return buf, length+2 - - -def writeBytes(buffer): - return writeInt16(len(buffer)) + buffer - - -def readBytes(buffer): - length = readInt16(buffer) - return buffer[2:2+length], length+2 - - -class VariableByteIntegers: # Variable Byte Integer - """ - MQTT variable byte integer helper class. Used - in several places in MQTT v5.0 properties. - - """ - - @staticmethod - def encode(x): - """ - Convert an integer 0 <= x <= 268435455 into multi-byte format. - Returns the buffer converted from the integer. - """ - if not 0 <= x <= 268435455: - raise ValueError(f"Value {x!r} must be in range 0-268435455") - buffer = b'' - while 1: - digit = x % 128 - x //= 128 - if x > 0: - digit |= 0x80 - buffer += bytes([digit]) - if x == 0: - break - return buffer - - @staticmethod - def decode(buffer): - """ - Get the value of a multi-byte integer from a buffer - Return the value, and the number of bytes used. - - [MQTT-1.5.5-1] the encoded value MUST use the minimum number of bytes necessary to represent the value - """ - multiplier = 1 - value = 0 - bytes = 0 - while 1: - bytes += 1 - digit = buffer[0] - buffer = buffer[1:] - value += (digit & 127) * multiplier - if digit & 128 == 0: - break - multiplier *= 128 - return (value, bytes) - - -class Properties: - """MQTT v5.0 properties class. - - See Properties.names for a list of accepted property names along with their numeric values. - - See Properties.properties for the data type of each property. - - Example of use:: - - publish_properties = Properties(PacketTypes.PUBLISH) - publish_properties.UserProperty = ("a", "2") - publish_properties.UserProperty = ("c", "3") - - First the object is created with packet type as argument, no properties will be present at - this point. Then properties are added as attributes, the name of which is the string property - name without the spaces. - - """ - - def __init__(self, packetType): - self.packetType = packetType - self.types = ["Byte", "Two Byte Integer", "Four Byte Integer", "Variable Byte Integer", - "Binary Data", "UTF-8 Encoded String", "UTF-8 String Pair"] - - self.names = { - "Payload Format Indicator": 1, - "Message Expiry Interval": 2, - "Content Type": 3, - "Response Topic": 8, - "Correlation Data": 9, - "Subscription Identifier": 11, - "Session Expiry Interval": 17, - "Assigned Client Identifier": 18, - "Server Keep Alive": 19, - "Authentication Method": 21, - "Authentication Data": 22, - "Request Problem Information": 23, - "Will Delay Interval": 24, - "Request Response Information": 25, - "Response Information": 26, - "Server Reference": 28, - "Reason String": 31, - "Receive Maximum": 33, - "Topic Alias Maximum": 34, - "Topic Alias": 35, - "Maximum QoS": 36, - "Retain Available": 37, - "User Property": 38, - "Maximum Packet Size": 39, - "Wildcard Subscription Available": 40, - "Subscription Identifier Available": 41, - "Shared Subscription Available": 42 - } - - self.properties = { - # id: type, packets - # payload format indicator - 1: (self.types.index("Byte"), [PacketTypes.PUBLISH, PacketTypes.WILLMESSAGE]), - 2: (self.types.index("Four Byte Integer"), [PacketTypes.PUBLISH, PacketTypes.WILLMESSAGE]), - 3: (self.types.index("UTF-8 Encoded String"), [PacketTypes.PUBLISH, PacketTypes.WILLMESSAGE]), - 8: (self.types.index("UTF-8 Encoded String"), [PacketTypes.PUBLISH, PacketTypes.WILLMESSAGE]), - 9: (self.types.index("Binary Data"), [PacketTypes.PUBLISH, PacketTypes.WILLMESSAGE]), - 11: (self.types.index("Variable Byte Integer"), - [PacketTypes.PUBLISH, PacketTypes.SUBSCRIBE]), - 17: (self.types.index("Four Byte Integer"), - [PacketTypes.CONNECT, PacketTypes.CONNACK, PacketTypes.DISCONNECT]), - 18: (self.types.index("UTF-8 Encoded String"), [PacketTypes.CONNACK]), - 19: (self.types.index("Two Byte Integer"), [PacketTypes.CONNACK]), - 21: (self.types.index("UTF-8 Encoded String"), - [PacketTypes.CONNECT, PacketTypes.CONNACK, PacketTypes.AUTH]), - 22: (self.types.index("Binary Data"), - [PacketTypes.CONNECT, PacketTypes.CONNACK, PacketTypes.AUTH]), - 23: (self.types.index("Byte"), - [PacketTypes.CONNECT]), - 24: (self.types.index("Four Byte Integer"), [PacketTypes.WILLMESSAGE]), - 25: (self.types.index("Byte"), [PacketTypes.CONNECT]), - 26: (self.types.index("UTF-8 Encoded String"), [PacketTypes.CONNACK]), - 28: (self.types.index("UTF-8 Encoded String"), - [PacketTypes.CONNACK, PacketTypes.DISCONNECT]), - 31: (self.types.index("UTF-8 Encoded String"), - [PacketTypes.CONNACK, PacketTypes.PUBACK, PacketTypes.PUBREC, - PacketTypes.PUBREL, PacketTypes.PUBCOMP, PacketTypes.SUBACK, - PacketTypes.UNSUBACK, PacketTypes.DISCONNECT, PacketTypes.AUTH]), - 33: (self.types.index("Two Byte Integer"), - [PacketTypes.CONNECT, PacketTypes.CONNACK]), - 34: (self.types.index("Two Byte Integer"), - [PacketTypes.CONNECT, PacketTypes.CONNACK]), - 35: (self.types.index("Two Byte Integer"), [PacketTypes.PUBLISH]), - 36: (self.types.index("Byte"), [PacketTypes.CONNACK]), - 37: (self.types.index("Byte"), [PacketTypes.CONNACK]), - 38: (self.types.index("UTF-8 String Pair"), - [PacketTypes.CONNECT, PacketTypes.CONNACK, - PacketTypes.PUBLISH, PacketTypes.PUBACK, - PacketTypes.PUBREC, PacketTypes.PUBREL, PacketTypes.PUBCOMP, - PacketTypes.SUBSCRIBE, PacketTypes.SUBACK, - PacketTypes.UNSUBSCRIBE, PacketTypes.UNSUBACK, - PacketTypes.DISCONNECT, PacketTypes.AUTH, PacketTypes.WILLMESSAGE]), - 39: (self.types.index("Four Byte Integer"), - [PacketTypes.CONNECT, PacketTypes.CONNACK]), - 40: (self.types.index("Byte"), [PacketTypes.CONNACK]), - 41: (self.types.index("Byte"), [PacketTypes.CONNACK]), - 42: (self.types.index("Byte"), [PacketTypes.CONNACK]), - } - - def allowsMultiple(self, compressedName): - return self.getIdentFromName(compressedName) in [11, 38] - - def getIdentFromName(self, compressedName): - # return the identifier corresponding to the property name - result = -1 - for name in self.names.keys(): - if compressedName == name.replace(' ', ''): - result = self.names[name] - break - return result - - def __setattr__(self, name, value): - name = name.replace(' ', '') - privateVars = ["packetType", "types", "names", "properties"] - if name in privateVars: - object.__setattr__(self, name, value) - else: - # the name could have spaces in, or not. Remove spaces before assignment - if name not in [aname.replace(' ', '') for aname in self.names.keys()]: - raise MQTTException( - f"Property name must be one of {self.names.keys()}") - # check that this attribute applies to the packet type - if self.packetType not in self.properties[self.getIdentFromName(name)][1]: - raise MQTTException(f"Property {name} does not apply to packet type {PacketTypes.Names[self.packetType]}") - - # Check for forbidden values - if not isinstance(value, list): - if name in ["ReceiveMaximum", "TopicAlias"] \ - and (value < 1 or value > 65535): - - raise MQTTException(f"{name} property value must be in the range 1-65535") - elif name in ["TopicAliasMaximum"] \ - and (value < 0 or value > 65535): - - raise MQTTException(f"{name} property value must be in the range 0-65535") - elif name in ["MaximumPacketSize", "SubscriptionIdentifier"] \ - and (value < 1 or value > 268435455): - - raise MQTTException(f"{name} property value must be in the range 1-268435455") - elif name in ["RequestResponseInformation", "RequestProblemInformation", "PayloadFormatIndicator"] \ - and (value != 0 and value != 1): - - raise MQTTException( - f"{name} property value must be 0 or 1") - - if self.allowsMultiple(name): - if not isinstance(value, list): - value = [value] - if hasattr(self, name): - value = object.__getattribute__(self, name) + value - object.__setattr__(self, name, value) - - def __str__(self): - buffer = "[" - first = True - for name in self.names.keys(): - compressedName = name.replace(' ', '') - if hasattr(self, compressedName): - if not first: - buffer += ", " - buffer += f"{compressedName} : {getattr(self, compressedName)}" - first = False - buffer += "]" - return buffer - - def json(self): - data = {} - for name in self.names.keys(): - compressedName = name.replace(' ', '') - if hasattr(self, compressedName): - val = getattr(self, compressedName) - if compressedName == 'CorrelationData' and isinstance(val, bytes): - data[compressedName] = val.hex() - else: - data[compressedName] = val - return data - - def isEmpty(self): - rc = True - for name in self.names.keys(): - compressedName = name.replace(' ', '') - if hasattr(self, compressedName): - rc = False - break - return rc - - def clear(self): - for name in self.names.keys(): - compressedName = name.replace(' ', '') - if hasattr(self, compressedName): - delattr(self, compressedName) - - def writeProperty(self, identifier, type, value): - buffer = b"" - buffer += VariableByteIntegers.encode(identifier) # identifier - if type == self.types.index("Byte"): # value - buffer += bytes([value]) - elif type == self.types.index("Two Byte Integer"): - buffer += writeInt16(value) - elif type == self.types.index("Four Byte Integer"): - buffer += writeInt32(value) - elif type == self.types.index("Variable Byte Integer"): - buffer += VariableByteIntegers.encode(value) - elif type == self.types.index("Binary Data"): - buffer += writeBytes(value) - elif type == self.types.index("UTF-8 Encoded String"): - buffer += writeUTF(value) - elif type == self.types.index("UTF-8 String Pair"): - buffer += writeUTF(value[0]) + writeUTF(value[1]) - return buffer - - def pack(self): - # serialize properties into buffer for sending over network - buffer = b"" - for name in self.names.keys(): - compressedName = name.replace(' ', '') - if hasattr(self, compressedName): - identifier = self.getIdentFromName(compressedName) - attr_type = self.properties[identifier][0] - if self.allowsMultiple(compressedName): - for prop in getattr(self, compressedName): - buffer += self.writeProperty(identifier, - attr_type, prop) - else: - buffer += self.writeProperty(identifier, attr_type, - getattr(self, compressedName)) - return VariableByteIntegers.encode(len(buffer)) + buffer - - def readProperty(self, buffer, type, propslen): - if type == self.types.index("Byte"): - value = buffer[0] - valuelen = 1 - elif type == self.types.index("Two Byte Integer"): - value = readInt16(buffer) - valuelen = 2 - elif type == self.types.index("Four Byte Integer"): - value = readInt32(buffer) - valuelen = 4 - elif type == self.types.index("Variable Byte Integer"): - value, valuelen = VariableByteIntegers.decode(buffer) - elif type == self.types.index("Binary Data"): - value, valuelen = readBytes(buffer) - elif type == self.types.index("UTF-8 Encoded String"): - value, valuelen = readUTF(buffer, propslen) - elif type == self.types.index("UTF-8 String Pair"): - value, valuelen = readUTF(buffer, propslen) - buffer = buffer[valuelen:] # strip the bytes used by the value - value1, valuelen1 = readUTF(buffer, propslen - valuelen) - value = (value, value1) - valuelen += valuelen1 - return value, valuelen - - def getNameFromIdent(self, identifier): - rc = None - for name in self.names: - if self.names[name] == identifier: - rc = name - return rc - - def unpack(self, buffer): - self.clear() - # deserialize properties into attributes from buffer received from network - propslen, VBIlen = VariableByteIntegers.decode(buffer) - buffer = buffer[VBIlen:] # strip the bytes used by the VBI - propslenleft = propslen - while propslenleft > 0: # properties length is 0 if there are none - identifier, VBIlen2 = VariableByteIntegers.decode( - buffer) # property identifier - buffer = buffer[VBIlen2:] # strip the bytes used by the VBI - propslenleft -= VBIlen2 - attr_type = self.properties[identifier][0] - value, valuelen = self.readProperty( - buffer, attr_type, propslenleft) - buffer = buffer[valuelen:] # strip the bytes used by the value - propslenleft -= valuelen - propname = self.getNameFromIdent(identifier) - compressedName = propname.replace(' ', '') - if not self.allowsMultiple(compressedName) and hasattr(self, compressedName): - raise MQTTException( - f"Property '{property}' must not exist more than once") - setattr(self, propname, value) - return self, propslen + VBIlen diff --git a/scripts/tempSensor/lib/paho/mqtt/publish.py b/scripts/tempSensor/lib/paho/mqtt/publish.py deleted file mode 100644 index 333c190a..00000000 --- a/scripts/tempSensor/lib/paho/mqtt/publish.py +++ /dev/null @@ -1,306 +0,0 @@ -# Copyright (c) 2014 Roger Light -# -# All rights reserved. This program and the accompanying materials -# are made available under the terms of the Eclipse Public License v2.0 -# and Eclipse Distribution License v1.0 which accompany this distribution. -# -# The Eclipse Public License is available at -# http://www.eclipse.org/legal/epl-v20.html -# and the Eclipse Distribution License is available at -# http://www.eclipse.org/org/documents/edl-v10.php. -# -# Contributors: -# Roger Light - initial API and implementation - -""" -This module provides some helper functions to allow straightforward publishing -of messages in a one-shot manner. In other words, they are useful for the -situation where you have a single/multiple messages you want to publish to a -broker, then disconnect and nothing else is required. -""" -from __future__ import annotations - -import collections -from collections.abc import Iterable -from typing import TYPE_CHECKING, Any, List, Tuple, Union - -from paho.mqtt.enums import CallbackAPIVersion, MQTTProtocolVersion -from paho.mqtt.properties import Properties -from paho.mqtt.reasoncodes import ReasonCode - -from .. import mqtt -from . import client as paho - -if TYPE_CHECKING: - try: - from typing import NotRequired, Required, TypedDict # type: ignore - except ImportError: - from typing_extensions import NotRequired, Required, TypedDict - - try: - from typing import Literal - except ImportError: - from typing_extensions import Literal # type: ignore - - - - class AuthParameter(TypedDict, total=False): - username: Required[str] - password: NotRequired[str] - - - class TLSParameter(TypedDict, total=False): - ca_certs: Required[str] - certfile: NotRequired[str] - keyfile: NotRequired[str] - tls_version: NotRequired[int] - ciphers: NotRequired[str] - insecure: NotRequired[bool] - - - class MessageDict(TypedDict, total=False): - topic: Required[str] - payload: NotRequired[paho.PayloadType] - qos: NotRequired[int] - retain: NotRequired[bool] - - MessageTuple = Tuple[str, paho.PayloadType, int, bool] - - MessagesList = List[Union[MessageDict, MessageTuple]] - - -def _do_publish(client: paho.Client): - """Internal function""" - - message = client._userdata.popleft() - - if isinstance(message, dict): - client.publish(**message) - elif isinstance(message, (tuple, list)): - client.publish(*message) - else: - raise TypeError('message must be a dict, tuple, or list') - - -def _on_connect(client: paho.Client, userdata: MessagesList, flags, reason_code, properties): - """Internal v5 callback""" - if reason_code == 0: - if len(userdata) > 0: - _do_publish(client) - else: - raise mqtt.MQTTException(paho.connack_string(reason_code)) - - -def _on_publish( - client: paho.Client, userdata: collections.deque[MessagesList], mid: int, reason_codes: ReasonCode, properties: Properties, -) -> None: - """Internal callback""" - #pylint: disable=unused-argument - - if len(userdata) == 0: - client.disconnect() - else: - _do_publish(client) - - -def multiple( - msgs: MessagesList, - hostname: str = "localhost", - port: int = 1883, - client_id: str = "", - keepalive: int = 60, - will: MessageDict | None = None, - auth: AuthParameter | None = None, - tls: TLSParameter | None = None, - protocol: MQTTProtocolVersion = paho.MQTTv311, - transport: Literal["tcp", "websockets"] = "tcp", - proxy_args: Any | None = None, -) -> None: - """Publish multiple messages to a broker, then disconnect cleanly. - - This function creates an MQTT client, connects to a broker and publishes a - list of messages. Once the messages have been delivered, it disconnects - cleanly from the broker. - - :param msgs: a list of messages to publish. Each message is either a dict or a - tuple. - - If a dict, only the topic must be present. Default values will be - used for any missing arguments. The dict must be of the form: - - msg = {'topic':"", 'payload':"", 'qos':, - 'retain':} - topic must be present and may not be empty. - If payload is "", None or not present then a zero length payload - will be published. - If qos is not present, the default of 0 is used. - If retain is not present, the default of False is used. - - If a tuple, then it must be of the form: - ("", "", qos, retain) - - :param str hostname: the address of the broker to connect to. - Defaults to localhost. - - :param int port: the port to connect to the broker on. Defaults to 1883. - - :param str client_id: the MQTT client id to use. If "" or None, the Paho library will - generate a client id automatically. - - :param int keepalive: the keepalive timeout value for the client. Defaults to 60 - seconds. - - :param will: a dict containing will parameters for the client: will = {'topic': - "", 'payload':", 'qos':, 'retain':}. - Topic is required, all other parameters are optional and will - default to None, 0 and False respectively. - Defaults to None, which indicates no will should be used. - - :param auth: a dict containing authentication parameters for the client: - auth = {'username':"", 'password':""} - Username is required, password is optional and will default to None - if not provided. - Defaults to None, which indicates no authentication is to be used. - - :param tls: a dict containing TLS configuration parameters for the client: - dict = {'ca_certs':"", 'certfile':"", - 'keyfile':"", 'tls_version':"", - 'ciphers':", 'insecure':""} - ca_certs is required, all other parameters are optional and will - default to None if not provided, which results in the client using - the default behaviour - see the paho.mqtt.client documentation. - Alternatively, tls input can be an SSLContext object, which will be - processed using the tls_set_context method. - Defaults to None, which indicates that TLS should not be used. - - :param str transport: set to "tcp" to use the default setting of transport which is - raw TCP. Set to "websockets" to use WebSockets as the transport. - - :param proxy_args: a dictionary that will be given to the client. - """ - - if not isinstance(msgs, Iterable): - raise TypeError('msgs must be an iterable') - if len(msgs) == 0: - raise ValueError('msgs is empty') - - client = paho.Client( - CallbackAPIVersion.VERSION2, - client_id=client_id, - userdata=collections.deque(msgs), - protocol=protocol, - transport=transport, - ) - - client.enable_logger() - client.on_publish = _on_publish - client.on_connect = _on_connect # type: ignore - - if proxy_args is not None: - client.proxy_set(**proxy_args) - - if auth: - username = auth.get('username') - if username: - password = auth.get('password') - client.username_pw_set(username, password) - else: - raise KeyError("The 'username' key was not found, this is " - "required for auth") - - if will is not None: - client.will_set(**will) - - if tls is not None: - if isinstance(tls, dict): - insecure = tls.pop('insecure', False) - # mypy don't get that tls no longer contains the key insecure - client.tls_set(**tls) # type: ignore[misc] - if insecure: - # Must be set *after* the `client.tls_set()` call since it sets - # up the SSL context that `client.tls_insecure_set` alters. - client.tls_insecure_set(insecure) - else: - # Assume input is SSLContext object - client.tls_set_context(tls) - - client.connect(hostname, port, keepalive) - client.loop_forever() - - -def single( - topic: str, - payload: paho.PayloadType = None, - qos: int = 0, - retain: bool = False, - hostname: str = "localhost", - port: int = 1883, - client_id: str = "", - keepalive: int = 60, - will: MessageDict | None = None, - auth: AuthParameter | None = None, - tls: TLSParameter | None = None, - protocol: MQTTProtocolVersion = paho.MQTTv311, - transport: Literal["tcp", "websockets"] = "tcp", - proxy_args: Any | None = None, -) -> None: - """Publish a single message to a broker, then disconnect cleanly. - - This function creates an MQTT client, connects to a broker and publishes a - single message. Once the message has been delivered, it disconnects cleanly - from the broker. - - :param str topic: the only required argument must be the topic string to which the - payload will be published. - - :param payload: the payload to be published. If "" or None, a zero length payload - will be published. - - :param int qos: the qos to use when publishing, default to 0. - - :param bool retain: set the message to be retained (True) or not (False). - - :param str hostname: the address of the broker to connect to. - Defaults to localhost. - - :param int port: the port to connect to the broker on. Defaults to 1883. - - :param str client_id: the MQTT client id to use. If "" or None, the Paho library will - generate a client id automatically. - - :param int keepalive: the keepalive timeout value for the client. Defaults to 60 - seconds. - - :param will: a dict containing will parameters for the client: will = {'topic': - "", 'payload':", 'qos':, 'retain':}. - Topic is required, all other parameters are optional and will - default to None, 0 and False respectively. - Defaults to None, which indicates no will should be used. - - :param auth: a dict containing authentication parameters for the client: - Username is required, password is optional and will default to None - auth = {'username':"", 'password':""} - if not provided. - Defaults to None, which indicates no authentication is to be used. - - :param tls: a dict containing TLS configuration parameters for the client: - dict = {'ca_certs':"", 'certfile':"", - 'keyfile':"", 'tls_version':"", - 'ciphers':", 'insecure':""} - ca_certs is required, all other parameters are optional and will - default to None if not provided, which results in the client using - the default behaviour - see the paho.mqtt.client documentation. - Defaults to None, which indicates that TLS should not be used. - Alternatively, tls input can be an SSLContext object, which will be - processed using the tls_set_context method. - - :param transport: set to "tcp" to use the default setting of transport which is - raw TCP. Set to "websockets" to use WebSockets as the transport. - - :param proxy_args: a dictionary that will be given to the client. - """ - - msg: MessageDict = {'topic':topic, 'payload':payload, 'qos':qos, 'retain':retain} - - multiple([msg], hostname, port, client_id, keepalive, will, auth, tls, - protocol, transport, proxy_args) diff --git a/scripts/tempSensor/lib/paho/mqtt/py.typed b/scripts/tempSensor/lib/paho/mqtt/py.typed deleted file mode 100644 index e69de29b..00000000 diff --git a/scripts/tempSensor/lib/paho/mqtt/reasoncodes.py b/scripts/tempSensor/lib/paho/mqtt/reasoncodes.py deleted file mode 100644 index 243ac96f..00000000 --- a/scripts/tempSensor/lib/paho/mqtt/reasoncodes.py +++ /dev/null @@ -1,223 +0,0 @@ -# ******************************************************************* -# Copyright (c) 2017, 2019 IBM Corp. -# -# All rights reserved. This program and the accompanying materials -# are made available under the terms of the Eclipse Public License v2.0 -# and Eclipse Distribution License v1.0 which accompany this distribution. -# -# The Eclipse Public License is available at -# http://www.eclipse.org/legal/epl-v20.html -# and the Eclipse Distribution License is available at -# http://www.eclipse.org/org/documents/edl-v10.php. -# -# Contributors: -# Ian Craggs - initial implementation and/or documentation -# ******************************************************************* - -import functools -import warnings -from typing import Any - -from .packettypes import PacketTypes - - -@functools.total_ordering -class ReasonCode: - """MQTT version 5.0 reason codes class. - - See ReasonCode.names for a list of possible numeric values along with their - names and the packets to which they apply. - - """ - - def __init__(self, packetType: int, aName: str ="Success", identifier: int =-1): - """ - packetType: the type of the packet, such as PacketTypes.CONNECT that - this reason code will be used with. Some reason codes have different - names for the same identifier when used a different packet type. - - aName: the String name of the reason code to be created. Ignored - if the identifier is set. - - identifier: an integer value of the reason code to be created. - - """ - - self.packetType = packetType - self.names = { - 0: {"Success": [PacketTypes.CONNACK, PacketTypes.PUBACK, - PacketTypes.PUBREC, PacketTypes.PUBREL, PacketTypes.PUBCOMP, - PacketTypes.UNSUBACK, PacketTypes.AUTH], - "Normal disconnection": [PacketTypes.DISCONNECT], - "Granted QoS 0": [PacketTypes.SUBACK]}, - 1: {"Granted QoS 1": [PacketTypes.SUBACK]}, - 2: {"Granted QoS 2": [PacketTypes.SUBACK]}, - 4: {"Disconnect with will message": [PacketTypes.DISCONNECT]}, - 16: {"No matching subscribers": - [PacketTypes.PUBACK, PacketTypes.PUBREC]}, - 17: {"No subscription found": [PacketTypes.UNSUBACK]}, - 24: {"Continue authentication": [PacketTypes.AUTH]}, - 25: {"Re-authenticate": [PacketTypes.AUTH]}, - 128: {"Unspecified error": [PacketTypes.CONNACK, PacketTypes.PUBACK, - PacketTypes.PUBREC, PacketTypes.SUBACK, PacketTypes.UNSUBACK, - PacketTypes.DISCONNECT], }, - 129: {"Malformed packet": - [PacketTypes.CONNACK, PacketTypes.DISCONNECT]}, - 130: {"Protocol error": - [PacketTypes.CONNACK, PacketTypes.DISCONNECT]}, - 131: {"Implementation specific error": [PacketTypes.CONNACK, - PacketTypes.PUBACK, PacketTypes.PUBREC, PacketTypes.SUBACK, - PacketTypes.UNSUBACK, PacketTypes.DISCONNECT], }, - 132: {"Unsupported protocol version": [PacketTypes.CONNACK]}, - 133: {"Client identifier not valid": [PacketTypes.CONNACK]}, - 134: {"Bad user name or password": [PacketTypes.CONNACK]}, - 135: {"Not authorized": [PacketTypes.CONNACK, PacketTypes.PUBACK, - PacketTypes.PUBREC, PacketTypes.SUBACK, PacketTypes.UNSUBACK, - PacketTypes.DISCONNECT], }, - 136: {"Server unavailable": [PacketTypes.CONNACK]}, - 137: {"Server busy": [PacketTypes.CONNACK, PacketTypes.DISCONNECT]}, - 138: {"Banned": [PacketTypes.CONNACK]}, - 139: {"Server shutting down": [PacketTypes.DISCONNECT]}, - 140: {"Bad authentication method": - [PacketTypes.CONNACK, PacketTypes.DISCONNECT]}, - 141: {"Keep alive timeout": [PacketTypes.DISCONNECT]}, - 142: {"Session taken over": [PacketTypes.DISCONNECT]}, - 143: {"Topic filter invalid": - [PacketTypes.SUBACK, PacketTypes.UNSUBACK, PacketTypes.DISCONNECT]}, - 144: {"Topic name invalid": - [PacketTypes.CONNACK, PacketTypes.PUBACK, - PacketTypes.PUBREC, PacketTypes.DISCONNECT]}, - 145: {"Packet identifier in use": - [PacketTypes.PUBACK, PacketTypes.PUBREC, - PacketTypes.SUBACK, PacketTypes.UNSUBACK]}, - 146: {"Packet identifier not found": - [PacketTypes.PUBREL, PacketTypes.PUBCOMP]}, - 147: {"Receive maximum exceeded": [PacketTypes.DISCONNECT]}, - 148: {"Topic alias invalid": [PacketTypes.DISCONNECT]}, - 149: {"Packet too large": [PacketTypes.CONNACK, PacketTypes.DISCONNECT]}, - 150: {"Message rate too high": [PacketTypes.DISCONNECT]}, - 151: {"Quota exceeded": [PacketTypes.CONNACK, PacketTypes.PUBACK, - PacketTypes.PUBREC, PacketTypes.SUBACK, PacketTypes.DISCONNECT], }, - 152: {"Administrative action": [PacketTypes.DISCONNECT]}, - 153: {"Payload format invalid": - [PacketTypes.PUBACK, PacketTypes.PUBREC, PacketTypes.DISCONNECT]}, - 154: {"Retain not supported": - [PacketTypes.CONNACK, PacketTypes.DISCONNECT]}, - 155: {"QoS not supported": - [PacketTypes.CONNACK, PacketTypes.DISCONNECT]}, - 156: {"Use another server": - [PacketTypes.CONNACK, PacketTypes.DISCONNECT]}, - 157: {"Server moved": - [PacketTypes.CONNACK, PacketTypes.DISCONNECT]}, - 158: {"Shared subscription not supported": - [PacketTypes.SUBACK, PacketTypes.DISCONNECT]}, - 159: {"Connection rate exceeded": - [PacketTypes.CONNACK, PacketTypes.DISCONNECT]}, - 160: {"Maximum connect time": - [PacketTypes.DISCONNECT]}, - 161: {"Subscription identifiers not supported": - [PacketTypes.SUBACK, PacketTypes.DISCONNECT]}, - 162: {"Wildcard subscription not supported": - [PacketTypes.SUBACK, PacketTypes.DISCONNECT]}, - } - if identifier == -1: - if packetType == PacketTypes.DISCONNECT and aName == "Success": - aName = "Normal disconnection" - self.set(aName) - else: - self.value = identifier - self.getName() # check it's good - - def __getName__(self, packetType, identifier): - """ - Get the reason code string name for a specific identifier. - The name can vary by packet type for the same identifier, which - is why the packet type is also required. - - Used when displaying the reason code. - """ - if identifier not in self.names: - raise KeyError(identifier) - names = self.names[identifier] - namelist = [name for name in names.keys() if packetType in names[name]] - if len(namelist) != 1: - raise ValueError(f"Expected exactly one name, found {namelist!r}") - return namelist[0] - - def getId(self, name): - """ - Get the numeric id corresponding to a reason code name. - - Used when setting the reason code for a packetType - check that only valid codes for the packet are set. - """ - for code in self.names.keys(): - if name in self.names[code].keys(): - if self.packetType in self.names[code][name]: - return code - raise KeyError(f"Reason code name not found: {name}") - - def set(self, name): - self.value = self.getId(name) - - def unpack(self, buffer): - c = buffer[0] - name = self.__getName__(self.packetType, c) - self.value = self.getId(name) - return 1 - - def getName(self): - """Returns the reason code name corresponding to the numeric value which is set. - """ - return self.__getName__(self.packetType, self.value) - - def __eq__(self, other): - if isinstance(other, int): - return self.value == other - if isinstance(other, str): - return other == str(self) - if isinstance(other, ReasonCode): - return self.value == other.value - return False - - def __lt__(self, other): - if isinstance(other, int): - return self.value < other - if isinstance(other, ReasonCode): - return self.value < other.value - return NotImplemented - - def __repr__(self): - try: - packet_name = PacketTypes.Names[self.packetType] - except IndexError: - packet_name = "Unknown" - - return f"ReasonCode({packet_name}, {self.getName()!r})" - - def __str__(self): - return self.getName() - - def json(self): - return self.getName() - - def pack(self): - return bytearray([self.value]) - - @property - def is_failure(self) -> bool: - return self.value >= 0x80 - - -class _CompatibilityIsInstance(type): - def __instancecheck__(self, other: Any) -> bool: - return isinstance(other, ReasonCode) - - -class ReasonCodes(ReasonCode, metaclass=_CompatibilityIsInstance): - def __init__(self, *args, **kwargs): - warnings.warn("ReasonCodes is deprecated, use ReasonCode (singular) instead", - category=DeprecationWarning, - stacklevel=2, - ) - super().__init__(*args, **kwargs) diff --git a/scripts/tempSensor/lib/paho/mqtt/subscribe.py b/scripts/tempSensor/lib/paho/mqtt/subscribe.py deleted file mode 100644 index b6c80f44..00000000 --- a/scripts/tempSensor/lib/paho/mqtt/subscribe.py +++ /dev/null @@ -1,281 +0,0 @@ -# Copyright (c) 2016 Roger Light -# -# All rights reserved. This program and the accompanying materials -# are made available under the terms of the Eclipse Public License v2.0 -# and Eclipse Distribution License v1.0 which accompany this distribution. -# -# The Eclipse Public License is available at -# http://www.eclipse.org/legal/epl-v20.html -# and the Eclipse Distribution License is available at -# http://www.eclipse.org/org/documents/edl-v10.php. -# -# Contributors: -# Roger Light - initial API and implementation - -""" -This module provides some helper functions to allow straightforward subscribing -to topics and retrieving messages. The two functions are simple(), which -returns one or messages matching a set of topics, and callback() which allows -you to pass a callback for processing of messages. -""" - -from .. import mqtt -from . import client as paho - - -def _on_connect(client, userdata, flags, reason_code, properties): - """Internal callback""" - if reason_code != 0: - raise mqtt.MQTTException(paho.connack_string(reason_code)) - - if isinstance(userdata['topics'], list): - for topic in userdata['topics']: - client.subscribe(topic, userdata['qos']) - else: - client.subscribe(userdata['topics'], userdata['qos']) - - -def _on_message_callback(client, userdata, message): - """Internal callback""" - userdata['callback'](client, userdata['userdata'], message) - - -def _on_message_simple(client, userdata, message): - """Internal callback""" - - if userdata['msg_count'] == 0: - return - - # Don't process stale retained messages if 'retained' was false - if message.retain and not userdata['retained']: - return - - userdata['msg_count'] = userdata['msg_count'] - 1 - - if userdata['messages'] is None and userdata['msg_count'] == 0: - userdata['messages'] = message - client.disconnect() - return - - userdata['messages'].append(message) - if userdata['msg_count'] == 0: - client.disconnect() - - -def callback(callback, topics, qos=0, userdata=None, hostname="localhost", - port=1883, client_id="", keepalive=60, will=None, auth=None, - tls=None, protocol=paho.MQTTv311, transport="tcp", - clean_session=True, proxy_args=None): - """Subscribe to a list of topics and process them in a callback function. - - This function creates an MQTT client, connects to a broker and subscribes - to a list of topics. Incoming messages are processed by the user provided - callback. This is a blocking function and will never return. - - :param callback: function with the same signature as `on_message` for - processing the messages received. - - :param topics: either a string containing a single topic to subscribe to, or a - list of topics to subscribe to. - - :param int qos: the qos to use when subscribing. This is applied to all topics. - - :param userdata: passed to the callback - - :param str hostname: the address of the broker to connect to. - Defaults to localhost. - - :param int port: the port to connect to the broker on. Defaults to 1883. - - :param str client_id: the MQTT client id to use. If "" or None, the Paho library will - generate a client id automatically. - - :param int keepalive: the keepalive timeout value for the client. Defaults to 60 - seconds. - - :param will: a dict containing will parameters for the client: will = {'topic': - "", 'payload':", 'qos':, 'retain':}. - Topic is required, all other parameters are optional and will - default to None, 0 and False respectively. - - Defaults to None, which indicates no will should be used. - - :param auth: a dict containing authentication parameters for the client: - auth = {'username':"", 'password':""} - Username is required, password is optional and will default to None - if not provided. - Defaults to None, which indicates no authentication is to be used. - - :param tls: a dict containing TLS configuration parameters for the client: - dict = {'ca_certs':"", 'certfile':"", - 'keyfile':"", 'tls_version':"", - 'ciphers':", 'insecure':""} - ca_certs is required, all other parameters are optional and will - default to None if not provided, which results in the client using - the default behaviour - see the paho.mqtt.client documentation. - Alternatively, tls input can be an SSLContext object, which will be - processed using the tls_set_context method. - Defaults to None, which indicates that TLS should not be used. - - :param str transport: set to "tcp" to use the default setting of transport which is - raw TCP. Set to "websockets" to use WebSockets as the transport. - - :param clean_session: a boolean that determines the client type. If True, - the broker will remove all information about this client - when it disconnects. If False, the client is a persistent - client and subscription information and queued messages - will be retained when the client disconnects. - Defaults to True. - - :param proxy_args: a dictionary that will be given to the client. - """ - - if qos < 0 or qos > 2: - raise ValueError('qos must be in the range 0-2') - - callback_userdata = { - 'callback':callback, - 'topics':topics, - 'qos':qos, - 'userdata':userdata} - - client = paho.Client( - paho.CallbackAPIVersion.VERSION2, - client_id=client_id, - userdata=callback_userdata, - protocol=protocol, - transport=transport, - clean_session=clean_session, - ) - client.enable_logger() - - client.on_message = _on_message_callback - client.on_connect = _on_connect - - if proxy_args is not None: - client.proxy_set(**proxy_args) - - if auth: - username = auth.get('username') - if username: - password = auth.get('password') - client.username_pw_set(username, password) - else: - raise KeyError("The 'username' key was not found, this is " - "required for auth") - - if will is not None: - client.will_set(**will) - - if tls is not None: - if isinstance(tls, dict): - insecure = tls.pop('insecure', False) - client.tls_set(**tls) - if insecure: - # Must be set *after* the `client.tls_set()` call since it sets - # up the SSL context that `client.tls_insecure_set` alters. - client.tls_insecure_set(insecure) - else: - # Assume input is SSLContext object - client.tls_set_context(tls) - - client.connect(hostname, port, keepalive) - client.loop_forever() - - -def simple(topics, qos=0, msg_count=1, retained=True, hostname="localhost", - port=1883, client_id="", keepalive=60, will=None, auth=None, - tls=None, protocol=paho.MQTTv311, transport="tcp", - clean_session=True, proxy_args=None): - """Subscribe to a list of topics and return msg_count messages. - - This function creates an MQTT client, connects to a broker and subscribes - to a list of topics. Once "msg_count" messages have been received, it - disconnects cleanly from the broker and returns the messages. - - :param topics: either a string containing a single topic to subscribe to, or a - list of topics to subscribe to. - - :param int qos: the qos to use when subscribing. This is applied to all topics. - - :param int msg_count: the number of messages to retrieve from the broker. - if msg_count == 1 then a single MQTTMessage will be returned. - if msg_count > 1 then a list of MQTTMessages will be returned. - - :param bool retained: If set to True, retained messages will be processed the same as - non-retained messages. If set to False, retained messages will - be ignored. This means that with retained=False and msg_count=1, - the function will return the first message received that does - not have the retained flag set. - - :param str hostname: the address of the broker to connect to. - Defaults to localhost. - - :param int port: the port to connect to the broker on. Defaults to 1883. - - :param str client_id: the MQTT client id to use. If "" or None, the Paho library will - generate a client id automatically. - - :param int keepalive: the keepalive timeout value for the client. Defaults to 60 - seconds. - - :param will: a dict containing will parameters for the client: will = {'topic': - "", 'payload':", 'qos':, 'retain':}. - Topic is required, all other parameters are optional and will - default to None, 0 and False respectively. - Defaults to None, which indicates no will should be used. - - :param auth: a dict containing authentication parameters for the client: - auth = {'username':"", 'password':""} - Username is required, password is optional and will default to None - if not provided. - Defaults to None, which indicates no authentication is to be used. - - :param tls: a dict containing TLS configuration parameters for the client: - dict = {'ca_certs':"", 'certfile':"", - 'keyfile':"", 'tls_version':"", - 'ciphers':", 'insecure':""} - ca_certs is required, all other parameters are optional and will - default to None if not provided, which results in the client using - the default behaviour - see the paho.mqtt.client documentation. - Alternatively, tls input can be an SSLContext object, which will be - processed using the tls_set_context method. - Defaults to None, which indicates that TLS should not be used. - - :param protocol: the MQTT protocol version to use. Defaults to MQTTv311. - - :param transport: set to "tcp" to use the default setting of transport which is - raw TCP. Set to "websockets" to use WebSockets as the transport. - - :param clean_session: a boolean that determines the client type. If True, - the broker will remove all information about this client - when it disconnects. If False, the client is a persistent - client and subscription information and queued messages - will be retained when the client disconnects. - Defaults to True. If protocol is MQTTv50, clean_session - is ignored. - - :param proxy_args: a dictionary that will be given to the client. - """ - - if msg_count < 1: - raise ValueError('msg_count must be > 0') - - # Set ourselves up to return a single message if msg_count == 1, or a list - # if > 1. - if msg_count == 1: - messages = None - else: - messages = [] - - # Ignore clean_session if protocol is MQTTv50, otherwise Client will raise - if protocol == paho.MQTTv5: - clean_session = None - - userdata = {'retained':retained, 'msg_count':msg_count, 'messages':messages} - - callback(_on_message_simple, topics, qos, userdata, hostname, port, - client_id, keepalive, will, auth, tls, protocol, transport, - clean_session, proxy_args) - - return userdata['messages'] diff --git a/scripts/tempSensor/lib/paho/mqtt/subscribeoptions.py b/scripts/tempSensor/lib/paho/mqtt/subscribeoptions.py deleted file mode 100644 index 7e0605de..00000000 --- a/scripts/tempSensor/lib/paho/mqtt/subscribeoptions.py +++ /dev/null @@ -1,113 +0,0 @@ -""" -******************************************************************* - Copyright (c) 2017, 2019 IBM Corp. - - All rights reserved. This program and the accompanying materials - are made available under the terms of the Eclipse Public License v2.0 - and Eclipse Distribution License v1.0 which accompany this distribution. - - The Eclipse Public License is available at - http://www.eclipse.org/legal/epl-v20.html - and the Eclipse Distribution License is available at - http://www.eclipse.org/org/documents/edl-v10.php. - - Contributors: - Ian Craggs - initial implementation and/or documentation -******************************************************************* -""" - - - -class MQTTException(Exception): - pass - - -class SubscribeOptions: - """The MQTT v5.0 subscribe options class. - - The options are: - qos: As in MQTT v3.1.1. - noLocal: True or False. If set to True, the subscriber will not receive its own publications. - retainAsPublished: True or False. If set to True, the retain flag on received publications will be as set - by the publisher. - retainHandling: RETAIN_SEND_ON_SUBSCRIBE, RETAIN_SEND_IF_NEW_SUB or RETAIN_DO_NOT_SEND - Controls when the broker should send retained messages: - - RETAIN_SEND_ON_SUBSCRIBE: on any successful subscribe request - - RETAIN_SEND_IF_NEW_SUB: only if the subscribe request is new - - RETAIN_DO_NOT_SEND: never send retained messages - """ - - # retain handling options - RETAIN_SEND_ON_SUBSCRIBE, RETAIN_SEND_IF_NEW_SUB, RETAIN_DO_NOT_SEND = range( - 0, 3) - - def __init__( - self, - qos: int = 0, - noLocal: bool = False, - retainAsPublished: bool = False, - retainHandling: int = RETAIN_SEND_ON_SUBSCRIBE, - ): - """ - qos: 0, 1 or 2. 0 is the default. - noLocal: True or False. False is the default and corresponds to MQTT v3.1.1 behavior. - retainAsPublished: True or False. False is the default and corresponds to MQTT v3.1.1 behavior. - retainHandling: RETAIN_SEND_ON_SUBSCRIBE, RETAIN_SEND_IF_NEW_SUB or RETAIN_DO_NOT_SEND - RETAIN_SEND_ON_SUBSCRIBE is the default and corresponds to MQTT v3.1.1 behavior. - """ - object.__setattr__(self, "names", - ["QoS", "noLocal", "retainAsPublished", "retainHandling"]) - self.QoS = qos # bits 0,1 - self.noLocal = noLocal # bit 2 - self.retainAsPublished = retainAsPublished # bit 3 - self.retainHandling = retainHandling # bits 4 and 5: 0, 1 or 2 - if self.retainHandling not in (0, 1, 2): - raise AssertionError(f"Retain handling should be 0, 1 or 2, not {self.retainHandling}") - if self.QoS not in (0, 1, 2): - raise AssertionError(f"QoS should be 0, 1 or 2, not {self.QoS}") - - def __setattr__(self, name, value): - if name not in self.names: - raise MQTTException( - f"{name} Attribute name must be one of {self.names}") - object.__setattr__(self, name, value) - - def pack(self): - if self.retainHandling not in (0, 1, 2): - raise AssertionError(f"Retain handling should be 0, 1 or 2, not {self.retainHandling}") - if self.QoS not in (0, 1, 2): - raise AssertionError(f"QoS should be 0, 1 or 2, not {self.QoS}") - noLocal = 1 if self.noLocal else 0 - retainAsPublished = 1 if self.retainAsPublished else 0 - data = [(self.retainHandling << 4) | (retainAsPublished << 3) | - (noLocal << 2) | self.QoS] - return bytes(data) - - def unpack(self, buffer): - b0 = buffer[0] - self.retainHandling = ((b0 >> 4) & 0x03) - self.retainAsPublished = True if ((b0 >> 3) & 0x01) == 1 else False - self.noLocal = True if ((b0 >> 2) & 0x01) == 1 else False - self.QoS = (b0 & 0x03) - if self.retainHandling not in (0, 1, 2): - raise AssertionError(f"Retain handling should be 0, 1 or 2, not {self.retainHandling}") - if self.QoS not in (0, 1, 2): - raise AssertionError(f"QoS should be 0, 1 or 2, not {self.QoS}") - return 1 - - def __repr__(self): - return str(self) - - def __str__(self): - return "{QoS="+str(self.QoS)+", noLocal="+str(self.noLocal) +\ - ", retainAsPublished="+str(self.retainAsPublished) +\ - ", retainHandling="+str(self.retainHandling)+"}" - - def json(self): - data = { - "QoS": self.QoS, - "noLocal": self.noLocal, - "retainAsPublished": self.retainAsPublished, - "retainHandling": self.retainHandling, - } - return data diff --git a/scripts/tempSensor/lib/paho_mqtt-2.1.0.dist-info/METADATA b/scripts/tempSensor/lib/paho_mqtt-2.1.0.dist-info/METADATA deleted file mode 100644 index bf1408ca..00000000 --- a/scripts/tempSensor/lib/paho_mqtt-2.1.0.dist-info/METADATA +++ /dev/null @@ -1,635 +0,0 @@ -Metadata-Version: 2.3 -Name: paho-mqtt -Version: 2.1.0 -Summary: MQTT version 5.0/3.1.1 client class -Project-URL: Homepage, http://eclipse.org/paho -Author-email: Roger Light -License: EPL-2.0 OR BSD-3-Clause -License-File: LICENSE.txt -Keywords: paho -Classifier: Development Status :: 4 - Beta -Classifier: Intended Audience :: Developers -Classifier: License :: OSI Approved -Classifier: Natural Language :: English -Classifier: Operating System :: MacOS :: MacOS X -Classifier: Operating System :: Microsoft :: Windows -Classifier: Operating System :: POSIX -Classifier: Programming Language :: Python -Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3.7 -Classifier: Programming Language :: Python :: 3.8 -Classifier: Programming Language :: Python :: 3.9 -Classifier: Programming Language :: Python :: 3.10 -Classifier: Topic :: Communications -Classifier: Topic :: Internet -Requires-Python: >=3.7 -Provides-Extra: proxy -Requires-Dist: pysocks; extra == 'proxy' -Description-Content-Type: text/x-rst - -Eclipse Paho™ MQTT Python Client -================================ - -The `full documentation is available here `_. - -**Warning breaking change** - Release 2.0 contains a breaking change; see the `release notes `_ and `migration details `_. - -This document describes the source code for the `Eclipse Paho `_ MQTT Python client library, which implements versions 5.0, 3.1.1, and 3.1 of the MQTT protocol. - -This code provides a client class which enables applications to connect to an `MQTT `_ broker to publish messages, and to subscribe to topics and receive published messages. It also provides some helper functions to make publishing one off messages to an MQTT server very straightforward. - -It supports Python 3.7+. - -The MQTT protocol is a machine-to-machine (M2M)/"Internet of Things" connectivity protocol. Designed as an extremely lightweight publish/subscribe messaging transport, it is useful for connections with remote locations where a small code footprint is required and/or network bandwidth is at a premium. - -Paho is an `Eclipse Foundation `_ project. - -Contents --------- - -* Installation_ -* `Known limitations`_ -* `Usage and API`_ - * `Getting Started`_ - * `Client`_ - * `Network loop`_ - * `Callbacks`_ - * `Logger`_ - * `External event loop support`_ - * `Global helper functions`_ - * `Publish`_ - * `Single`_ - * `Multiple`_ - * `Subscribe`_ - * `Simple`_ - * `Using Callback`_ -* `Reporting bugs`_ -* `More information`_ - - -Installation ------------- - -The latest stable version is available in the Python Package Index (PyPi) and can be installed using - -:: - - pip install paho-mqtt - -Or with ``virtualenv``: - -:: - - virtualenv paho-mqtt - source paho-mqtt/bin/activate - pip install paho-mqtt - -To obtain the full code, including examples and tests, you can clone the git repository: - -:: - - git clone https://github.com/eclipse/paho.mqtt.python - - -Once you have the code, it can be installed from your repository as well: - -:: - - cd paho.mqtt.python - pip install -e . - -To perform all tests (including MQTT v5 tests), you also need to clone paho.mqtt.testing in paho.mqtt.python folder:: - - git clone https://github.com/eclipse/paho.mqtt.testing.git - cd paho.mqtt.testing - git checkout a4dc694010217b291ee78ee13a6d1db812f9babd - -Known limitations ------------------ - -The following are the known unimplemented MQTT features. - -When ``clean_session`` is False, the session is only stored in memory and not persisted. This means that -when the client is restarted (not just reconnected, the object is recreated usually because the -program was restarted) the session is lost. This results in a possible message loss. - -The following part of the client session is lost: - -* QoS 2 messages which have been received from the server, but have not been completely acknowledged. - - Since the client will blindly acknowledge any PUBCOMP (last message of a QoS 2 transaction), it - won't hang but will lose this QoS 2 message. - -* QoS 1 and QoS 2 messages which have been sent to the server, but have not been completely acknowledged. - - This means that messages passed to ``publish()`` may be lost. This could be mitigated by taking care - that all messages passed to ``publish()`` have a corresponding ``on_publish()`` call or use `wait_for_publish`. - - It also means that the broker may have the QoS2 message in the session. Since the client starts - with an empty session it don't know it and will reuse the mid. This is not yet fixed. - -Also, when ``clean_session`` is True, this library will republish QoS > 0 message across network -reconnection. This means that QoS > 0 message won't be lost. But the standard says that -we should discard any message for which the publish packet was sent. Our choice means that -we are not compliant with the standard and it's possible for QoS 2 to be received twice. - -You should set ``clean_session = False`` if you need the QoS 2 guarantee of only one delivery. - -Usage and API -------------- - -Detailed API documentation `is available online `_ or could be built from ``docs/`` and samples are available in the `examples`_ directory. - -The package provides two modules, a full `Client` and few `helpers` for simple publishing or subscribing. - -Getting Started -*************** - -Here is a very simple example that subscribes to the broker $SYS topic tree and prints out the resulting messages: - -.. code:: python - - import paho.mqtt.client as mqtt - - # The callback for when the client receives a CONNACK response from the server. - def on_connect(client, userdata, flags, reason_code, properties): - print(f"Connected with result code {reason_code}") - # Subscribing in on_connect() means that if we lose the connection and - # reconnect then subscriptions will be renewed. - client.subscribe("$SYS/#") - - # The callback for when a PUBLISH message is received from the server. - def on_message(client, userdata, msg): - print(msg.topic+" "+str(msg.payload)) - - mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2) - mqttc.on_connect = on_connect - mqttc.on_message = on_message - - mqttc.connect("mqtt.eclipseprojects.io", 1883, 60) - - # Blocking call that processes network traffic, dispatches callbacks and - # handles reconnecting. - # Other loop*() functions are available that give a threaded interface and a - # manual interface. - mqttc.loop_forever() - -Client -****** - -You can use the client class as an instance, within a class or by subclassing. The general usage flow is as follows: - -* Create a client instance -* Connect to a broker using one of the ``connect*()`` functions -* Call one of the ``loop*()`` functions to maintain network traffic flow with the broker -* Use ``subscribe()`` to subscribe to a topic and receive messages -* Use ``publish()`` to publish messages to the broker -* Use ``disconnect()`` to disconnect from the broker - -Callbacks will be called to allow the application to process events as necessary. These callbacks are described below. - -Network loop -```````````` - -These functions are the driving force behind the client. If they are not -called, incoming network data will not be processed and outgoing network data -will not be sent. There are four options for managing the -network loop. Three are described here, the fourth in "External event loop -support" below. Do not mix the different loop functions. - -loop_start() / loop_stop() -'''''''''''''''''''''''''' - -.. code:: python - - mqttc.loop_start() - - while True: - temperature = sensor.blocking_read() - mqttc.publish("paho/temperature", temperature) - - mqttc.loop_stop() - -These functions implement a threaded interface to the network loop. Calling -`loop_start()` once, before or after ``connect*()``, runs a thread in the -background to call `loop()` automatically. This frees up the main thread for -other work that may be blocking. This call also handles reconnecting to the -broker. Call `loop_stop()` to stop the background thread. -The loop is also stopped if you call `disconnect()`. - -loop_forever() -'''''''''''''' - -.. code:: python - - mqttc.loop_forever(retry_first_connection=False) - -This is a blocking form of the network loop and will not return until the -client calls `disconnect()`. It automatically handles reconnecting. - -Except for the first connection attempt when using `connect_async`, use -``retry_first_connection=True`` to make it retry the first connection. - -*Warning*: This might lead to situations where the client keeps connecting to an -non existing host without failing. - -loop() -'''''' - -.. code:: python - - run = True - while run: - rc = mqttc.loop(timeout=1.0) - if rc != 0: - # need to handle error, possible reconnecting or stopping the application - -Call regularly to process network events. This call waits in ``select()`` until -the network socket is available for reading or writing, if appropriate, then -handles the incoming/outgoing data. This function blocks for up to ``timeout`` -seconds. ``timeout`` must not exceed the ``keepalive`` value for the client or -your client will be regularly disconnected by the broker. - -Using this kind of loop, require you to handle reconnection strategie. - - -Callbacks -````````` - -The interface to interact with paho-mqtt include various callback that are called by -the library when some events occur. - -The callbacks are functions defined in your code, to implement the require action on those events. This could -be simply printing received message or much more complex behaviour. - -Callbacks API is versioned, and the selected version is the `CallbackAPIVersion` you provided to `Client` -constructor. Currently two version are supported: - -* ``CallbackAPIVersion.VERSION1``: it's the historical version used in paho-mqtt before version 2.0. - It's the API used before the introduction of `CallbackAPIVersion`. - This version is deprecated and will be removed in paho-mqtt version 3.0. -* ``CallbackAPIVersion.VERSION2``: This version is more consistent between protocol MQTT 3.x and MQTT 5.x. It's also - much more usable with MQTT 5.x since reason code and properties are always provided when available. - It's recommended for all user to upgrade to this version. It's highly recommended for MQTT 5.x user. - -The following callbacks exists: - -* `on_connect()`: called when the CONNACK from the broker is received. The call could be for a refused connection, - check the reason_code to see if the connection is successful or rejected. -* `on_connect_fail()`: called by `loop_forever()` and `loop_start()` when the TCP connection failed to establish. - This callback is not called when using `connect()` or `reconnect()` directly. It's only called following - an automatic (re)connection made by `loop_start()` and `loop_forever()` -* `on_disconnect()`: called when the connection is closed. -* `on_message()`: called when a MQTT message is received from the broker. -* `on_publish()`: called when an MQTT message was sent to the broker. Depending on QoS level the callback is called - at different moment: - - * For QoS == 0, it's called as soon as the message is sent over the network. This could be before the corresponding ``publish()`` return. - * For QoS == 1, it's called when the corresponding PUBACK is received from the broker - * For QoS == 2, it's called when the corresponding PUBCOMP is received from the broker -* `on_subscribe()`: called when the SUBACK is received from the broker -* `on_unsubscribe()`: called when the UNSUBACK is received from the broker -* `on_log()`: called when the library log a message -* `on_socket_open`, `on_socket_close`, `on_socket_register_write`, `on_socket_unregister_write`: callbacks used for external loop support. See below for details. - -For the signature of each callback, see the `online documentation `_. - -Subscriber example -'''''''''''''''''' - -.. code:: python - - import paho.mqtt.client as mqtt - - def on_subscribe(client, userdata, mid, reason_code_list, properties): - # Since we subscribed only for a single channel, reason_code_list contains - # a single entry - if reason_code_list[0].is_failure: - print(f"Broker rejected you subscription: {reason_code_list[0]}") - else: - print(f"Broker granted the following QoS: {reason_code_list[0].value}") - - def on_unsubscribe(client, userdata, mid, reason_code_list, properties): - # Be careful, the reason_code_list is only present in MQTTv5. - # In MQTTv3 it will always be empty - if len(reason_code_list) == 0 or not reason_code_list[0].is_failure: - print("unsubscribe succeeded (if SUBACK is received in MQTTv3 it success)") - else: - print(f"Broker replied with failure: {reason_code_list[0]}") - client.disconnect() - - def on_message(client, userdata, message): - # userdata is the structure we choose to provide, here it's a list() - userdata.append(message.payload) - # We only want to process 10 messages - if len(userdata) >= 10: - client.unsubscribe("$SYS/#") - - def on_connect(client, userdata, flags, reason_code, properties): - if reason_code.is_failure: - print(f"Failed to connect: {reason_code}. loop_forever() will retry connection") - else: - # we should always subscribe from on_connect callback to be sure - # our subscribed is persisted across reconnections. - client.subscribe("$SYS/#") - - mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2) - mqttc.on_connect = on_connect - mqttc.on_message = on_message - mqttc.on_subscribe = on_subscribe - mqttc.on_unsubscribe = on_unsubscribe - - mqttc.user_data_set([]) - mqttc.connect("mqtt.eclipseprojects.io") - mqttc.loop_forever() - print(f"Received the following message: {mqttc.user_data_get()}") - -publisher example -''''''''''''''''' - -.. code:: python - - import time - import paho.mqtt.client as mqtt - - def on_publish(client, userdata, mid, reason_code, properties): - # reason_code and properties will only be present in MQTTv5. It's always unset in MQTTv3 - try: - userdata.remove(mid) - except KeyError: - print("on_publish() is called with a mid not present in unacked_publish") - print("This is due to an unavoidable race-condition:") - print("* publish() return the mid of the message sent.") - print("* mid from publish() is added to unacked_publish by the main thread") - print("* on_publish() is called by the loop_start thread") - print("While unlikely (because on_publish() will be called after a network round-trip),") - print(" this is a race-condition that COULD happen") - print("") - print("The best solution to avoid race-condition is using the msg_info from publish()") - print("We could also try using a list of acknowledged mid rather than removing from pending list,") - print("but remember that mid could be re-used !") - - unacked_publish = set() - mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2) - mqttc.on_publish = on_publish - - mqttc.user_data_set(unacked_publish) - mqttc.connect("mqtt.eclipseprojects.io") - mqttc.loop_start() - - # Our application produce some messages - msg_info = mqttc.publish("paho/test/topic", "my message", qos=1) - unacked_publish.add(msg_info.mid) - - msg_info2 = mqttc.publish("paho/test/topic", "my message2", qos=1) - unacked_publish.add(msg_info2.mid) - - # Wait for all message to be published - while len(unacked_publish): - time.sleep(0.1) - - # Due to race-condition described above, the following way to wait for all publish is safer - msg_info.wait_for_publish() - msg_info2.wait_for_publish() - - mqttc.disconnect() - mqttc.loop_stop() - - -Logger -`````` - -The Client emit some log message that could be useful during troubleshooting. The easiest way to -enable logs is the call `enable_logger()`. It's possible to provide a custom logger or let the -default logger being used. - -Example: - -.. code:: python - - import logging - import paho.mqtt.client as mqtt - - logging.basicConfig(level=logging.DEBUG) - - mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2) - mqttc.enable_logger() - - mqttc.connect("mqtt.eclipseprojects.io", 1883, 60) - mqttc.loop_start() - - # Do additional action needed, publish, subscribe, ... - [...] - -It's also possible to define a on_log callback that will receive a copy of all log messages. Example: - -.. code:: python - - import paho.mqtt.client as mqtt - - def on_log(client, userdata, paho_log_level, messages): - if paho_log_level == mqtt.LogLevel.MQTT_LOG_ERR: - print(message) - - mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2) - mqttc.on_log = on_log - - mqttc.connect("mqtt.eclipseprojects.io", 1883, 60) - mqttc.loop_start() - - # Do additional action needed, publish, subscribe, ... - [...] - - -The correspondence with Paho logging levels and standard ones is the following: - -==================== =============== -Paho logging -==================== =============== -``MQTT_LOG_ERR`` ``logging.ERROR`` -``MQTT_LOG_WARNING`` ``logging.WARNING`` -``MQTT_LOG_NOTICE`` ``logging.INFO`` *(no direct equivalent)* -``MQTT_LOG_INFO`` ``logging.INFO`` -``MQTT_LOG_DEBUG`` ``logging.DEBUG`` -==================== =============== - - -External event loop support -``````````````````````````` - -To support other network loop like asyncio (see examples_), the library expose some -method and callback to support those use-case. - -The following loop method exists: - -* `loop_read`: should be called when the socket is ready for reading. -* `loop_write`: should be called when the socket is ready for writing AND the library want to write data. -* `loop_misc`: should be called every few seconds to handle message retrying and pings. - -In pseudo code, it give the following: - -.. code:: python - - while run: - if need_read: - mqttc.loop_read() - if need_write: - mqttc.loop_write() - mqttc.loop_misc() - - if not need_read and not need_write: - # But don't wait more than few seconds, loop_misc() need to be called regularly - wait_for_change_in_need_read_or_write() - updated_need_read_and_write() - -The tricky part is implementing the update of need_read / need_write and wait for condition change. To support -this, the following method exists: - -* `socket()`: which return the socket object when the TCP connection is open. - This call is particularly useful for select_ based loops. See ``examples/loop_select.py``. -* `want_write()`: return true if there is data waiting to be written. This is close to the - ``need_writew`` of above pseudo-code, but you should also check whether the socket is ready for writing. -* callbacks ``on_socket_*``: - - * `on_socket_open`: called when the socket is opened. - * `on_socket_close`: called when the socket is about to be closed. - * `on_socket_register_write`: called when there is data the client want to write on the socket - * `on_socket_unregister_write`: called when there is no more data to write on the socket. - - Callbacks are particularly useful for event loops where you register or unregister a socket - for reading+writing. See ``examples/loop_asyncio.py`` for an example. - -.. _select: https://docs.python.org/3/library/select.html#select.select - -The callbacks are always called in this order: - -- `on_socket_open` -- Zero or more times: - - - `on_socket_register_write` - - `on_socket_unregister_write` - -- `on_socket_close` - -Global helper functions -``````````````````````` - -The client module also offers some global helper functions. - -``topic_matches_sub(sub, topic)`` can be used to check whether a ``topic`` -matches a ``subscription``. - -For example: - - the topic ``foo/bar`` would match the subscription ``foo/#`` or ``+/bar`` - - the topic ``non/matching`` would not match the subscription ``non/+/+`` - - -Publish -******* - -This module provides some helper functions to allow straightforward publishing -of messages in a one-shot manner. In other words, they are useful for the -situation where you have a single/multiple messages you want to publish to a -broker, then disconnect with nothing else required. - -The two functions provided are `single()` and `multiple()`. - -Both functions include support for MQTT v5.0, but do not currently let you -set any properties on connection or when sending messages. - -Single -`````` - -Publish a single message to a broker, then disconnect cleanly. - -Example: - -.. code:: python - - import paho.mqtt.publish as publish - - publish.single("paho/test/topic", "payload", hostname="mqtt.eclipseprojects.io") - -Multiple -```````` - -Publish multiple messages to a broker, then disconnect cleanly. - -Example: - -.. code:: python - - from paho.mqtt.enums import MQTTProtocolVersion - import paho.mqtt.publish as publish - - msgs = [{'topic':"paho/test/topic", 'payload':"multiple 1"}, - ("paho/test/topic", "multiple 2", 0, False)] - publish.multiple(msgs, hostname="mqtt.eclipseprojects.io", protocol=MQTTProtocolVersion.MQTTv5) - - -Subscribe -********* - -This module provides some helper functions to allow straightforward subscribing -and processing of messages. - -The two functions provided are `simple()` and `callback()`. - -Both functions include support for MQTT v5.0, but do not currently let you -set any properties on connection or when subscribing. - -Simple -`````` - -Subscribe to a set of topics and return the messages received. This is a -blocking function. - -Example: - -.. code:: python - - import paho.mqtt.subscribe as subscribe - - msg = subscribe.simple("paho/test/topic", hostname="mqtt.eclipseprojects.io") - print("%s %s" % (msg.topic, msg.payload)) - -Using Callback -`````````````` - -Subscribe to a set of topics and process the messages received using a user -provided callback. - -Example: - -.. code:: python - - import paho.mqtt.subscribe as subscribe - - def on_message_print(client, userdata, message): - print("%s %s" % (message.topic, message.payload)) - userdata["message_count"] += 1 - if userdata["message_count"] >= 5: - # it's possible to stop the program by disconnecting - client.disconnect() - - subscribe.callback(on_message_print, "paho/test/topic", hostname="mqtt.eclipseprojects.io", userdata={"message_count": 0}) - - -Reporting bugs --------------- - -Please report bugs in the issues tracker at https://github.com/eclipse/paho.mqtt.python/issues. - -More information ----------------- - -Discussion of the Paho clients takes place on the `Eclipse paho-dev mailing list `_. - -General questions about the MQTT protocol itself (not this library) are discussed in the `MQTT Google Group `_. - -There is much more information available via the `MQTT community site `_. - -.. _examples: https://github.com/eclipse/paho.mqtt.python/tree/master/examples -.. _documentation: https://eclipse.dev/paho/files/paho.mqtt.python/html/client.html diff --git a/scripts/tempSensor/lib/paho_mqtt-2.1.0.dist-info/RECORD b/scripts/tempSensor/lib/paho_mqtt-2.1.0.dist-info/RECORD deleted file mode 100644 index ee00eada..00000000 --- a/scripts/tempSensor/lib/paho_mqtt-2.1.0.dist-info/RECORD +++ /dev/null @@ -1,14 +0,0 @@ -paho/__init__.py,, -paho/mqtt/__init__.py,, -paho/mqtt/client.py,, -paho/mqtt/enums.py,, -paho/mqtt/matcher.py,, -paho/mqtt/packettypes.py,, -paho/mqtt/properties.py,, -paho/mqtt/publish.py,, -paho/mqtt/py.typed,, -paho/mqtt/reasoncodes.py,, -paho/mqtt/subscribe.py,, -paho/mqtt/subscribeoptions.py,, -paho_mqtt-2.1.0.dist-info/METADATA,, -paho_mqtt-2.1.0.dist-info/RECORD,, \ No newline at end of file diff --git a/scripts/tempSensor/lib/requests-0.10.0.dist-info/METADATA b/scripts/tempSensor/lib/requests-0.10.0.dist-info/METADATA deleted file mode 100644 index 5d4b463d..00000000 --- a/scripts/tempSensor/lib/requests-0.10.0.dist-info/METADATA +++ /dev/null @@ -1,6 +0,0 @@ -Metadata-Version: 2.1 -Name: requests -Version: 0.10.0 -Summary: -Author: -License: MIT diff --git a/scripts/tempSensor/lib/requests-0.10.0.dist-info/RECORD b/scripts/tempSensor/lib/requests-0.10.0.dist-info/RECORD deleted file mode 100644 index cc88f4b5..00000000 --- a/scripts/tempSensor/lib/requests-0.10.0.dist-info/RECORD +++ /dev/null @@ -1,3 +0,0 @@ -requests-0.10.0.dist-info/METADATA,, -requests/__init__.py,, -requests-0.10.0.dist-info/RECORD,, \ No newline at end of file diff --git a/scripts/tempSensor/lib/requests/__init__.py b/scripts/tempSensor/lib/requests/__init__.py deleted file mode 100644 index bf529b6d..00000000 --- a/scripts/tempSensor/lib/requests/__init__.py +++ /dev/null @@ -1,220 +0,0 @@ -import socket - - -class Response: - def __init__(self, f): - self.raw = f - self.encoding = "utf-8" - self._cached = None - - def close(self): - if self.raw: - self.raw.close() - self.raw = None - self._cached = None - - @property - def content(self): - if self._cached is None: - try: - self._cached = self.raw.read() - finally: - self.raw.close() - self.raw = None - return self._cached - - @property - def text(self): - return str(self.content, self.encoding) - - def json(self): - import json - - return json.loads(self.content) - - -def request( - method, - url, - data=None, - json=None, - headers=None, - stream=None, - auth=None, - timeout=None, - parse_headers=True, -): - if headers is None: - headers = {} - - redirect = None # redirection url, None means no redirection - chunked_data = data and getattr(data, "__next__", None) and not getattr(data, "__len__", None) - - if auth is not None: - import binascii - - username, password = auth - formated = b"{}:{}".format(username, password) - formated = str(binascii.b2a_base64(formated)[:-1], "ascii") - headers["Authorization"] = "Basic {}".format(formated) - - try: - proto, dummy, host, path = url.split("/", 3) - except ValueError: - proto, dummy, host = url.split("/", 2) - path = "" - if proto == "http:": - port = 80 - elif proto == "https:": - import tls - - port = 443 - else: - raise ValueError("Unsupported protocol: " + proto) - - if ":" in host: - host, port = host.split(":", 1) - port = int(port) - - ai = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM) - ai = ai[0] - - resp_d = None - if parse_headers is not False: - resp_d = {} - - s = socket.socket(ai[0], socket.SOCK_STREAM, ai[2]) - - if timeout is not None: - # Note: settimeout is not supported on all platforms, will raise - # an AttributeError if not available. - s.settimeout(timeout) - - try: - s.connect(ai[-1]) - if proto == "https:": - context = tls.SSLContext(tls.PROTOCOL_TLS_CLIENT) - context.verify_mode = tls.CERT_NONE - s = context.wrap_socket(s, server_hostname=host) - s.write(b"%s /%s HTTP/1.0\r\n" % (method, path)) - - if "Host" not in headers: - headers["Host"] = host - - if json is not None: - assert data is None - from json import dumps - - data = dumps(json) - - if "Content-Type" not in headers: - headers["Content-Type"] = "application/json" - - if data: - if chunked_data: - if "Transfer-Encoding" not in headers and "Content-Length" not in headers: - headers["Transfer-Encoding"] = "chunked" - elif "Content-Length" not in headers: - headers["Content-Length"] = str(len(data)) - - if "Connection" not in headers: - headers["Connection"] = "close" - - # Iterate over keys to avoid tuple alloc - for k in headers: - s.write(k) - s.write(b": ") - s.write(headers[k]) - s.write(b"\r\n") - - s.write(b"\r\n") - - if data: - if chunked_data: - if headers.get("Transfer-Encoding", None) == "chunked": - for chunk in data: - s.write(b"%x\r\n" % len(chunk)) - s.write(chunk) - s.write(b"\r\n") - s.write("0\r\n\r\n") - else: - for chunk in data: - s.write(chunk) - else: - s.write(data) - - l = s.readline() - # print(l) - l = l.split(None, 2) - if len(l) < 2: - # Invalid response - raise ValueError("HTTP error: BadStatusLine:\n%s" % l) - status = int(l[1]) - reason = "" - if len(l) > 2: - reason = l[2].rstrip() - while True: - l = s.readline() - if not l or l == b"\r\n": - break - # print(l) - if l.startswith(b"Transfer-Encoding:"): - if b"chunked" in l: - raise ValueError("Unsupported " + str(l, "utf-8")) - elif l.startswith(b"Location:") and not 200 <= status <= 299: - if status in [301, 302, 303, 307, 308]: - redirect = str(l[10:-2], "utf-8") - else: - raise NotImplementedError("Redirect %d not yet supported" % status) - if parse_headers is False: - pass - elif parse_headers is True: - l = str(l, "utf-8") - k, v = l.split(":", 1) - resp_d[k] = v.strip() - else: - parse_headers(l, resp_d) - except OSError: - s.close() - raise - - if redirect: - s.close() - if status in [301, 302, 303]: - return request("GET", redirect, None, None, headers, stream) - else: - return request(method, redirect, data, json, headers, stream) - else: - resp = Response(s) - resp.status_code = status - resp.reason = reason - if resp_d is not None: - resp.headers = resp_d - return resp - - -def head(url, **kw): - return request("HEAD", url, **kw) - - -def get(url, **kw): - return request("GET", url, **kw) - - -def post(url, **kw): - return request("POST", url, **kw) - - -def put(url, **kw): - return request("PUT", url, **kw) - - -def patch(url, **kw): - return request("PATCH", url, **kw) - - -def delete(url, **kw): - return request("DELETE", url, **kw) - - -__version__ = '0.10.0' diff --git a/scripts/tempSensor/lib/smbus2-0.4.3.dist-info/METADATA b/scripts/tempSensor/lib/smbus2-0.4.3.dist-info/METADATA deleted file mode 100644 index 3db43c8e..00000000 --- a/scripts/tempSensor/lib/smbus2-0.4.3.dist-info/METADATA +++ /dev/null @@ -1,237 +0,0 @@ -Metadata-Version: 2.1 -Name: smbus2 -Version: 0.4.3 -Summary: smbus2 is a drop-in replacement for smbus-cffi/smbus-python in pure Python -Home-page: https://github.com/kplindegaard/smbus2 -Author: Karl-Petter Lindegaard -Author-email: kp.lindegaard@gmail.com -License: MIT -Keywords: smbus,smbus2,python,i2c,raspberrypi,linux -Classifier: Development Status :: 4 - Beta -Classifier: Topic :: Utilities -Classifier: License :: OSI Approved :: MIT License -Classifier: Programming Language :: Python :: 2 -Classifier: Programming Language :: Python :: 2.7 -Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3.4 -Classifier: Programming Language :: Python :: 3.5 -Classifier: Programming Language :: Python :: 3.6 -Classifier: Programming Language :: Python :: 3.7 -Classifier: Programming Language :: Python :: 3.8 -Classifier: Programming Language :: Python :: 3.9 -Classifier: Programming Language :: Python :: 3.10 -Classifier: Programming Language :: Python :: 3.11 -Description-Content-Type: text/markdown -License-File: LICENSE -Provides-Extra: docs -Requires-Dist: sphinx >=1.5.3 ; extra == 'docs' -Provides-Extra: qa -Requires-Dist: flake8 ; extra == 'qa' -Provides-Extra: test -Requires-Dist: nose ; extra == 'test' -Requires-Dist: mock ; (python_version < "3.3") and extra == 'test' - -# smbus2 -A drop-in replacement for smbus-cffi/smbus-python in pure Python - -[![Build Status](https://github.com/kplindegaard/smbus2/actions/workflows/python-build-test.yml/badge.svg?branch=master)](https://github.com/kplindegaard/smbus2/actions/workflows/python-build-test.yml) -[![Documentation Status](https://readthedocs.org/projects/smbus2/badge/?version=latest)](http://smbus2.readthedocs.io/en/latest/?badge=latest) -![CodeQL](https://github.com/kplindegaard/smbus2/actions/workflows/codeql-analysis.yml/badge.svg?branch=master) -[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=kplindegaard_smbus2&metric=alert_status)](https://sonarcloud.io/dashboard?id=kplindegaard_smbus2) - -![Python Verions](https://img.shields.io/pypi/pyversions/smbus2.svg) -[![PyPi Version](https://img.shields.io/pypi/v/smbus2.svg)](https://pypi.org/project/smbus2/) -[![PyPI - Downloads](https://img.shields.io/pypi/dm/smbus2)](https://pypi.org/project/smbus2/) - -# Introduction - -smbus2 is (yet another) pure Python implementation of the [python-smbus](http://www.lm-sensors.org/browser/i2c-tools/trunk/py-smbus/) package. - -It was designed from the ground up with two goals in mind: - -1. It should be a drop-in replacement of smbus. The syntax shall be the same. -2. Use the inherent i2c structs and unions to a greater extent than other pure Python implementations like [pysmbus](https://github.com/bjornt/pysmbus) does. By doing so, it will be more feature complete and easier to extend. - -Currently supported features are: - -* Get i2c capabilities (I2C_FUNCS) -* SMBus Packet Error Checking (PEC) support -* read_byte -* write_byte -* read_byte_data -* write_byte_data -* read_word_data -* write_word_data -* read_i2c_block_data -* write_i2c_block_data -* write_quick -* process_call -* read_block_data -* write_block_data -* block_process_call -* i2c_rdwr - *combined write/read transactions with repeated start* - -It is developed on Python 2.7 but works without any modifications in Python 3.X too. - -More information about updates and general changes are recorded in the [change log](https://github.com/kplindegaard/smbus2/blob/master/CHANGELOG.md). - -# SMBus code examples - -smbus2 installs next to smbus as the package, so it's not really a 100% replacement. You must change the module name. - -## Example 1a: Read a byte - -```python -from smbus2 import SMBus - -# Open i2c bus 1 and read one byte from address 80, offset 0 -bus = SMBus(1) -b = bus.read_byte_data(80, 0) -print(b) -bus.close() -``` - -## Example 1b: Read a byte using 'with' - -This is the very same example but safer to use since the smbus will be closed automatically when exiting the with block. - -```python -from smbus2 import SMBus - -with SMBus(1) as bus: - b = bus.read_byte_data(80, 0) - print(b) -``` - -## Example 1c: Read a byte with PEC enabled - -Same example with Packet Error Checking enabled. - -```python -from smbus2 import SMBus - -with SMBus(1) as bus: - bus.pec = 1 # Enable PEC - b = bus.read_byte_data(80, 0) - print(b) -``` - -## Example 2: Read a block of data - -You can read up to 32 bytes at once. - -```python -from smbus2 import SMBus - -with SMBus(1) as bus: - # Read a block of 16 bytes from address 80, offset 0 - block = bus.read_i2c_block_data(80, 0, 16) - # Returned value is a list of 16 bytes - print(block) -``` - -## Example 3: Write a byte - -```python -from smbus2 import SMBus - -with SMBus(1) as bus: - # Write a byte to address 80, offset 0 - data = 45 - bus.write_byte_data(80, 0, data) -``` - -## Example 4: Write a block of data - -It is possible to write 32 bytes at the time, but I have found that error-prone. Write less and add a delay in between if you run into trouble. - -```python -from smbus2 import SMBus - -with SMBus(1) as bus: - # Write a block of 8 bytes to address 80 from offset 0 - data = [1, 2, 3, 4, 5, 6, 7, 8] - bus.write_i2c_block_data(80, 0, data) -``` - -# I2C - -Starting with v0.2, the smbus2 library also has support for combined read and write transactions. *i2c_rdwr* is not really a SMBus feature but comes in handy when the master needs to: - -1. read or write bulks of data larger than SMBus' 32 bytes limit. -1. write some data and then read from the slave with a repeated start and no stop bit between. - -Each operation is represented by a *i2c_msg* message object. - - -## Example 5: Single i2c_rdwr - -```python -from smbus2 import SMBus, i2c_msg - -with SMBus(1) as bus: - # Read 64 bytes from address 80 - msg = i2c_msg.read(80, 64) - bus.i2c_rdwr(msg) - - # Write a single byte to address 80 - msg = i2c_msg.write(80, [65]) - bus.i2c_rdwr(msg) - - # Write some bytes to address 80 - msg = i2c_msg.write(80, [65, 66, 67, 68]) - bus.i2c_rdwr(msg) -``` - -## Example 6: Dual i2c_rdwr - -To perform dual operations just add more i2c_msg instances to the bus call: - -```python -from smbus2 import SMBus, i2c_msg - -# Single transaction writing two bytes then read two at address 80 -write = i2c_msg.write(80, [40, 50]) -read = i2c_msg.read(80, 2) -with SMBus(1) as bus: - bus.i2c_rdwr(write, read) -``` - -## Example 7: Access i2c_msg data - -All data is contained in the i2c_msg instances. Here are some data access alternatives. - -```python -# 1: Convert message content to list -msg = i2c_msg.write(60, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) -data = list(msg) # data = [1, 2, 3, ...] -print(len(data)) # => 10 - -# 2: i2c_msg is iterable -for value in msg: - print(value) - -# 3: Through i2c_msg properties -for k in range(msg.len): - print(msg.buf[k]) -``` - -# Installation instructions - -From [PyPi](https://pypi.org/) with `pip`: - -``` -pip install smbus2 -``` - -From [conda-forge](https://anaconda.org/conda-forge) using `conda`: - -``` -conda install -c conda-forge smbus2 -``` - -Installation from source code is straight forward: - -``` -python setup.py install -``` diff --git a/scripts/tempSensor/lib/smbus2-0.4.3.dist-info/RECORD b/scripts/tempSensor/lib/smbus2-0.4.3.dist-info/RECORD deleted file mode 100644 index 7df5fae3..00000000 --- a/scripts/tempSensor/lib/smbus2-0.4.3.dist-info/RECORD +++ /dev/null @@ -1,6 +0,0 @@ -smbus2-0.4.3.dist-info/METADATA,, -smbus2/__init__.py,, -smbus2/py.typed,, -smbus2/smbus2.py,, -smbus2/smbus2.pyi,, -smbus2-0.4.3.dist-info/RECORD,, \ No newline at end of file diff --git a/scripts/tempSensor/lib/smbus2/__init__.py b/scripts/tempSensor/lib/smbus2/__init__.py deleted file mode 100644 index f52948f9..00000000 --- a/scripts/tempSensor/lib/smbus2/__init__.py +++ /dev/null @@ -1,26 +0,0 @@ -"""smbus2 - A drop-in replacement for smbus-cffi/smbus-python""" -# The MIT License (MIT) -# Copyright (c) 2020 Karl-Petter Lindegaard -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -from .smbus2 import SMBus, i2c_msg, I2cFunc # noqa: F401 - -__version__ = "0.4.3" -__all__ = ["SMBus", "i2c_msg", "I2cFunc"] diff --git a/scripts/tempSensor/lib/smbus2/py.typed b/scripts/tempSensor/lib/smbus2/py.typed deleted file mode 100644 index e69de29b..00000000 diff --git a/scripts/tempSensor/lib/smbus2/smbus2.py b/scripts/tempSensor/lib/smbus2/smbus2.py deleted file mode 100644 index e9d7477f..00000000 --- a/scripts/tempSensor/lib/smbus2/smbus2.py +++ /dev/null @@ -1,658 +0,0 @@ -"""smbus2 - A drop-in replacement for smbus-cffi/smbus-python""" -# The MIT License (MIT) -# Copyright (c) 2020 Karl-Petter Lindegaard -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -import os -import sys -from fcntl import ioctl -from ctypes import c_uint32, c_uint8, c_uint16, c_char, POINTER, Structure, Array, Union, create_string_buffer, string_at - - -# Commands from uapi/linux/i2c-dev.h -I2C_SLAVE = 0x0703 # Use this slave address -I2C_SLAVE_FORCE = 0x0706 # Use this slave address, even if it is already in use by a driver! -I2C_FUNCS = 0x0705 # Get the adapter functionality mask -I2C_RDWR = 0x0707 # Combined R/W transfer (one STOP only) -I2C_SMBUS = 0x0720 # SMBus transfer. Takes pointer to i2c_smbus_ioctl_data -I2C_PEC = 0x0708 # != 0 to use PEC with SMBus - -# SMBus transfer read or write markers from uapi/linux/i2c.h -I2C_SMBUS_WRITE = 0 -I2C_SMBUS_READ = 1 - -# Size identifiers uapi/linux/i2c.h -I2C_SMBUS_QUICK = 0 -I2C_SMBUS_BYTE = 1 -I2C_SMBUS_BYTE_DATA = 2 -I2C_SMBUS_WORD_DATA = 3 -I2C_SMBUS_PROC_CALL = 4 -I2C_SMBUS_BLOCK_DATA = 5 # This isn't supported by Pure-I2C drivers with SMBUS emulation, like those in RaspberryPi, OrangePi, etc :( -I2C_SMBUS_BLOCK_PROC_CALL = 7 # Like I2C_SMBUS_BLOCK_DATA, it isn't supported by Pure-I2C drivers either. -I2C_SMBUS_I2C_BLOCK_DATA = 8 -I2C_SMBUS_BLOCK_MAX = 32 - -# To determine what functionality is present (uapi/linux/i2c.h) -try: - from enum import IntFlag -except ImportError: - IntFlag = int - - -class I2cFunc(IntFlag): - """ - These flags identify the operations supported by an I2C/SMBus device. - - You can test these flags on your `smbus.funcs` - - On newer python versions, I2cFunc is an IntFlag enum, but it - falls back to class with a bunch of int constants on older releases. - """ - I2C = 0x00000001 - ADDR_10BIT = 0x00000002 - PROTOCOL_MANGLING = 0x00000004 # I2C_M_IGNORE_NAK etc. - SMBUS_PEC = 0x00000008 - NOSTART = 0x00000010 # I2C_M_NOSTART - SLAVE = 0x00000020 - SMBUS_BLOCK_PROC_CALL = 0x00008000 # SMBus 2.0 - SMBUS_QUICK = 0x00010000 - SMBUS_READ_BYTE = 0x00020000 - SMBUS_WRITE_BYTE = 0x00040000 - SMBUS_READ_BYTE_DATA = 0x00080000 - SMBUS_WRITE_BYTE_DATA = 0x00100000 - SMBUS_READ_WORD_DATA = 0x00200000 - SMBUS_WRITE_WORD_DATA = 0x00400000 - SMBUS_PROC_CALL = 0x00800000 - SMBUS_READ_BLOCK_DATA = 0x01000000 - SMBUS_WRITE_BLOCK_DATA = 0x02000000 - SMBUS_READ_I2C_BLOCK = 0x04000000 # I2C-like block xfer - SMBUS_WRITE_I2C_BLOCK = 0x08000000 # w/ 1-byte reg. addr. - SMBUS_HOST_NOTIFY = 0x10000000 - - SMBUS_BYTE = 0x00060000 - SMBUS_BYTE_DATA = 0x00180000 - SMBUS_WORD_DATA = 0x00600000 - SMBUS_BLOCK_DATA = 0x03000000 - SMBUS_I2C_BLOCK = 0x0c000000 - SMBUS_EMUL = 0x0eff0008 - - -# i2c_msg flags from uapi/linux/i2c.h -I2C_M_RD = 0x0001 - -# Pointer definitions -LP_c_uint8 = POINTER(c_uint8) -LP_c_uint16 = POINTER(c_uint16) -LP_c_uint32 = POINTER(c_uint32) - - -############################################################# -# Type definitions as in i2c.h - - -class i2c_smbus_data(Array): - """ - Adaptation of the i2c_smbus_data union in ``i2c.h``. - - Data for SMBus messages. - """ - _length_ = I2C_SMBUS_BLOCK_MAX + 2 - _type_ = c_uint8 - - -class union_i2c_smbus_data(Union): - _fields_ = [ - ("byte", c_uint8), - ("word", c_uint16), - ("block", i2c_smbus_data) - ] - - -union_pointer_type = POINTER(union_i2c_smbus_data) - - -class i2c_smbus_ioctl_data(Structure): - """ - As defined in ``i2c-dev.h``. - """ - _fields_ = [ - ('read_write', c_uint8), - ('command', c_uint8), - ('size', c_uint32), - ('data', union_pointer_type)] - __slots__ = [name for name, type in _fields_] - - @staticmethod - def create(read_write=I2C_SMBUS_READ, command=0, size=I2C_SMBUS_BYTE_DATA): - u = union_i2c_smbus_data() - return i2c_smbus_ioctl_data( - read_write=read_write, command=command, size=size, - data=union_pointer_type(u)) - - -############################################################# -# Type definitions for i2c_rdwr combined transactions - - -class i2c_msg(Structure): - """ - As defined in ``i2c.h``. - """ - _fields_ = [ - ('addr', c_uint16), - ('flags', c_uint16), - ('len', c_uint16), - ('buf', POINTER(c_char))] - - def __iter__(self): - """ Iterator / Generator - - :return: iterates over :py:attr:`buf` - :rtype: :py:class:`generator` which returns int values - """ - idx = 0 - while idx < self.len: - yield ord(self.buf[idx]) - idx += 1 - - def __len__(self): - return self.len - - def __bytes__(self): - return string_at(self.buf, self.len) - - def __repr__(self): - return 'i2c_msg(%d,%d,%r)' % (self.addr, self.flags, self.__bytes__()) - - def __str__(self): - s = self.__bytes__() - # Throw away non-decodable bytes - s = s.decode(errors="ignore") - return s - - @staticmethod - def read(address, length): - """ - Prepares an i2c read transaction. - - :param address: Slave address. - :type: address: int - :param length: Number of bytes to read. - :type: length: int - :return: New :py:class:`i2c_msg` instance for read operation. - :rtype: :py:class:`i2c_msg` - """ - arr = create_string_buffer(length) - return i2c_msg( - addr=address, flags=I2C_M_RD, len=length, - buf=arr) - - @staticmethod - def write(address, buf): - """ - Prepares an i2c write transaction. - - :param address: Slave address. - :type address: int - :param buf: Bytes to write. Either list of values or str. - :type buf: list - :return: New :py:class:`i2c_msg` instance for write operation. - :rtype: :py:class:`i2c_msg` - """ - if sys.version_info.major >= 3: - if type(buf) is str: - buf = bytes(map(ord, buf)) - else: - buf = bytes(buf) - else: - if type(buf) is not str: - buf = ''.join([chr(x) for x in buf]) - arr = create_string_buffer(buf, len(buf)) - return i2c_msg( - addr=address, flags=0, len=len(arr), - buf=arr) - - -class i2c_rdwr_ioctl_data(Structure): - """ - As defined in ``i2c-dev.h``. - """ - _fields_ = [ - ('msgs', POINTER(i2c_msg)), - ('nmsgs', c_uint32) - ] - __slots__ = [name for name, type in _fields_] - - @staticmethod - def create(*i2c_msg_instances): - """ - Factory method for creating a i2c_rdwr_ioctl_data struct that can - be called with ``ioctl(fd, I2C_RDWR, data)``. - - :param i2c_msg_instances: Up to 42 i2c_msg instances - :rtype: i2c_rdwr_ioctl_data - """ - n_msg = len(i2c_msg_instances) - msg_array = (i2c_msg * n_msg)(*i2c_msg_instances) - return i2c_rdwr_ioctl_data( - msgs=msg_array, - nmsgs=n_msg - ) - - -############################################################# - - -class SMBus(object): - - def __init__(self, bus=None, force=False): - """ - Initialize and (optionally) open an i2c bus connection. - - :param bus: i2c bus number (e.g. 0 or 1) - or an absolute file path (e.g. `/dev/i2c-42`). - If not given, a subsequent call to ``open()`` is required. - :type bus: int or str - :param force: force using the slave address even when driver is - already using it. - :type force: boolean - """ - self.fd = None - self.funcs = I2cFunc(0) - if bus is not None: - self.open(bus) - self.address = None - self.force = force - self._force_last = None - self._pec = 0 - - def __enter__(self): - """Enter handler.""" - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - """Exit handler.""" - self.close() - - def open(self, bus): - """ - Open a given i2c bus. - - :param bus: i2c bus number (e.g. 0 or 1) - or an absolute file path (e.g. '/dev/i2c-42'). - :type bus: int or str - :raise TypeError: if type(bus) is not in (int, str) - """ - if isinstance(bus, int): - filepath = "/dev/i2c-{}".format(bus) - elif isinstance(bus, str): - filepath = bus - else: - raise TypeError("Unexpected type(bus)={}".format(type(bus))) - - self.fd = os.open(filepath, os.O_RDWR) - self.funcs = self._get_funcs() - - def close(self): - """ - Close the i2c connection. - """ - if self.fd: - os.close(self.fd) - self.fd = None - self._pec = 0 - - def _get_pec(self): - return self._pec - - def enable_pec(self, enable=True): - """ - Enable/Disable PEC (Packet Error Checking) - SMBus 1.1 and later - - :param enable: - :type enable: Boolean - """ - if not (self.funcs & I2cFunc.SMBUS_PEC): - raise IOError('SMBUS_PEC is not a feature') - self._pec = int(enable) - ioctl(self.fd, I2C_PEC, self._pec) - - pec = property(_get_pec, enable_pec) # Drop-in replacement for smbus member "pec" - """Get and set SMBus PEC. 0 = disabled (default), 1 = enabled.""" - - def _set_address(self, address, force=None): - """ - Set i2c slave address to use for subsequent calls. - - :param address: - :type address: int - :param force: - :type force: Boolean - """ - force = force if force is not None else self.force - if self.address != address or self._force_last != force: - if force is True: - ioctl(self.fd, I2C_SLAVE_FORCE, address) - else: - ioctl(self.fd, I2C_SLAVE, address) - self.address = address - self._force_last = force - - def _get_funcs(self): - """ - Returns a 32-bit value stating supported I2C functions. - - :rtype: int - """ - f = c_uint32() - ioctl(self.fd, I2C_FUNCS, f) - return f.value - - def write_quick(self, i2c_addr, force=None): - """ - Perform quick transaction. Throws IOError if unsuccessful. - :param i2c_addr: i2c address - :type i2c_addr: int - :param force: - :type force: Boolean - """ - self._set_address(i2c_addr, force=force) - msg = i2c_smbus_ioctl_data.create( - read_write=I2C_SMBUS_WRITE, command=0, size=I2C_SMBUS_QUICK) - ioctl(self.fd, I2C_SMBUS, msg) - - def read_byte(self, i2c_addr, force=None): - """ - Read a single byte from a device. - - :rtype: int - :param i2c_addr: i2c address - :type i2c_addr: int - :param force: - :type force: Boolean - :return: Read byte value - """ - self._set_address(i2c_addr, force=force) - msg = i2c_smbus_ioctl_data.create( - read_write=I2C_SMBUS_READ, command=0, size=I2C_SMBUS_BYTE - ) - ioctl(self.fd, I2C_SMBUS, msg) - return msg.data.contents.byte - - def write_byte(self, i2c_addr, value, force=None): - """ - Write a single byte to a device. - - :param i2c_addr: i2c address - :type i2c_addr: int - :param value: value to write - :type value: int - :param force: - :type force: Boolean - """ - self._set_address(i2c_addr, force=force) - msg = i2c_smbus_ioctl_data.create( - read_write=I2C_SMBUS_WRITE, command=value, size=I2C_SMBUS_BYTE - ) - ioctl(self.fd, I2C_SMBUS, msg) - - def read_byte_data(self, i2c_addr, register, force=None): - """ - Read a single byte from a designated register. - - :param i2c_addr: i2c address - :type i2c_addr: int - :param register: Register to read - :type register: int - :param force: - :type force: Boolean - :return: Read byte value - :rtype: int - """ - self._set_address(i2c_addr, force=force) - msg = i2c_smbus_ioctl_data.create( - read_write=I2C_SMBUS_READ, command=register, size=I2C_SMBUS_BYTE_DATA - ) - ioctl(self.fd, I2C_SMBUS, msg) - return msg.data.contents.byte - - def write_byte_data(self, i2c_addr, register, value, force=None): - """ - Write a byte to a given register. - - :param i2c_addr: i2c address - :type i2c_addr: int - :param register: Register to write to - :type register: int - :param value: Byte value to transmit - :type value: int - :param force: - :type force: Boolean - :rtype: None - """ - self._set_address(i2c_addr, force=force) - msg = i2c_smbus_ioctl_data.create( - read_write=I2C_SMBUS_WRITE, command=register, size=I2C_SMBUS_BYTE_DATA - ) - msg.data.contents.byte = value - ioctl(self.fd, I2C_SMBUS, msg) - - def read_word_data(self, i2c_addr, register, force=None): - """ - Read a single word (2 bytes) from a given register. - - :param i2c_addr: i2c address - :type i2c_addr: int - :param register: Register to read - :type register: int - :param force: - :type force: Boolean - :return: 2-byte word - :rtype: int - """ - self._set_address(i2c_addr, force=force) - msg = i2c_smbus_ioctl_data.create( - read_write=I2C_SMBUS_READ, command=register, size=I2C_SMBUS_WORD_DATA - ) - ioctl(self.fd, I2C_SMBUS, msg) - return msg.data.contents.word - - def write_word_data(self, i2c_addr, register, value, force=None): - """ - Write a single word (2 bytes) to a given register. - - :param i2c_addr: i2c address - :type i2c_addr: int - :param register: Register to write to - :type register: int - :param value: Word value to transmit - :type value: int - :param force: - :type force: Boolean - :rtype: None - """ - self._set_address(i2c_addr, force=force) - msg = i2c_smbus_ioctl_data.create( - read_write=I2C_SMBUS_WRITE, command=register, size=I2C_SMBUS_WORD_DATA - ) - msg.data.contents.word = value - ioctl(self.fd, I2C_SMBUS, msg) - - def process_call(self, i2c_addr, register, value, force=None): - """ - Executes a SMBus Process Call, sending a 16-bit value and receiving a 16-bit response - - :param i2c_addr: i2c address - :type i2c_addr: int - :param register: Register to read/write to - :type register: int - :param value: Word value to transmit - :type value: int - :param force: - :type force: Boolean - :rtype: int - """ - self._set_address(i2c_addr, force=force) - msg = i2c_smbus_ioctl_data.create( - read_write=I2C_SMBUS_WRITE, command=register, size=I2C_SMBUS_PROC_CALL - ) - msg.data.contents.word = value - ioctl(self.fd, I2C_SMBUS, msg) - return msg.data.contents.word - - def read_block_data(self, i2c_addr, register, force=None): - """ - Read a block of up to 32-bytes from a given register. - - :param i2c_addr: i2c address - :type i2c_addr: int - :param register: Start register - :type register: int - :param force: - :type force: Boolean - :return: List of bytes - :rtype: list - """ - self._set_address(i2c_addr, force=force) - msg = i2c_smbus_ioctl_data.create( - read_write=I2C_SMBUS_READ, command=register, size=I2C_SMBUS_BLOCK_DATA - ) - ioctl(self.fd, I2C_SMBUS, msg) - length = msg.data.contents.block[0] - return msg.data.contents.block[1:length + 1] - - def write_block_data(self, i2c_addr, register, data, force=None): - """ - Write a block of byte data to a given register. - - :param i2c_addr: i2c address - :type i2c_addr: int - :param register: Start register - :type register: int - :param data: List of bytes - :type data: list - :param force: - :type force: Boolean - :rtype: None - """ - length = len(data) - if length > I2C_SMBUS_BLOCK_MAX: - raise ValueError("Data length cannot exceed %d bytes" % I2C_SMBUS_BLOCK_MAX) - self._set_address(i2c_addr, force=force) - msg = i2c_smbus_ioctl_data.create( - read_write=I2C_SMBUS_WRITE, command=register, size=I2C_SMBUS_BLOCK_DATA - ) - msg.data.contents.block[0] = length - msg.data.contents.block[1:length + 1] = data - ioctl(self.fd, I2C_SMBUS, msg) - - def block_process_call(self, i2c_addr, register, data, force=None): - """ - Executes a SMBus Block Process Call, sending a variable-size data - block and receiving another variable-size response - - :param i2c_addr: i2c address - :type i2c_addr: int - :param register: Register to read/write to - :type register: int - :param data: List of bytes - :type data: list - :param force: - :type force: Boolean - :return: List of bytes - :rtype: list - """ - length = len(data) - if length > I2C_SMBUS_BLOCK_MAX: - raise ValueError("Data length cannot exceed %d bytes" % I2C_SMBUS_BLOCK_MAX) - self._set_address(i2c_addr, force=force) - msg = i2c_smbus_ioctl_data.create( - read_write=I2C_SMBUS_WRITE, command=register, size=I2C_SMBUS_BLOCK_PROC_CALL - ) - msg.data.contents.block[0] = length - msg.data.contents.block[1:length + 1] = data - ioctl(self.fd, I2C_SMBUS, msg) - length = msg.data.contents.block[0] - return msg.data.contents.block[1:length + 1] - - def read_i2c_block_data(self, i2c_addr, register, length, force=None): - """ - Read a block of byte data from a given register. - - :param i2c_addr: i2c address - :type i2c_addr: int - :param register: Start register - :type register: int - :param length: Desired block length - :type length: int - :param force: - :type force: Boolean - :return: List of bytes - :rtype: list - """ - if length > I2C_SMBUS_BLOCK_MAX: - raise ValueError("Desired block length over %d bytes" % I2C_SMBUS_BLOCK_MAX) - self._set_address(i2c_addr, force=force) - msg = i2c_smbus_ioctl_data.create( - read_write=I2C_SMBUS_READ, command=register, size=I2C_SMBUS_I2C_BLOCK_DATA - ) - msg.data.contents.byte = length - ioctl(self.fd, I2C_SMBUS, msg) - return msg.data.contents.block[1:length + 1] - - def write_i2c_block_data(self, i2c_addr, register, data, force=None): - """ - Write a block of byte data to a given register. - - :param i2c_addr: i2c address - :type i2c_addr: int - :param register: Start register - :type register: int - :param data: List of bytes - :type data: list - :param force: - :type force: Boolean - :rtype: None - """ - length = len(data) - if length > I2C_SMBUS_BLOCK_MAX: - raise ValueError("Data length cannot exceed %d bytes" % I2C_SMBUS_BLOCK_MAX) - self._set_address(i2c_addr, force=force) - msg = i2c_smbus_ioctl_data.create( - read_write=I2C_SMBUS_WRITE, command=register, size=I2C_SMBUS_I2C_BLOCK_DATA - ) - msg.data.contents.block[0] = length - msg.data.contents.block[1:length + 1] = data - ioctl(self.fd, I2C_SMBUS, msg) - - def i2c_rdwr(self, *i2c_msgs): - """ - Combine a series of i2c read and write operations in a single - transaction (with repeated start bits but no stop bits in between). - - This method takes i2c_msg instances as input, which must be created - first with :py:meth:`i2c_msg.read` or :py:meth:`i2c_msg.write`. - - :param i2c_msgs: One or more i2c_msg class instances. - :type i2c_msgs: i2c_msg - :rtype: None - """ - ioctl_data = i2c_rdwr_ioctl_data.create(*i2c_msgs) - ioctl(self.fd, I2C_RDWR, ioctl_data) diff --git a/scripts/tempSensor/lib/smbus2/smbus2.pyi b/scripts/tempSensor/lib/smbus2/smbus2.pyi deleted file mode 100644 index f111ec26..00000000 --- a/scripts/tempSensor/lib/smbus2/smbus2.pyi +++ /dev/null @@ -1,148 +0,0 @@ -from enum import IntFlag -from typing import Optional, Sequence, List, Type, SupportsBytes, Iterable -from typing import Union as _UnionT -from types import TracebackType -from ctypes import c_uint32, c_uint8, c_uint16, pointer, Structure, Array, Union - -I2C_SLAVE: int -I2C_SLAVE_FORCE: int -I2C_FUNCS: int -I2C_RDWR: int -I2C_SMBUS: int -I2C_PEC: int -I2C_SMBUS_WRITE: int -I2C_SMBUS_READ: int -I2C_SMBUS_QUICK: int -I2C_SMBUS_BYTE: int -I2C_SMBUS_BYTE_DATA: int -I2C_SMBUS_WORD_DATA: int -I2C_SMBUS_PROC_CALL: int -I2C_SMBUS_BLOCK_DATA: int -I2C_SMBUS_BLOCK_PROC_CALL: int -I2C_SMBUS_I2C_BLOCK_DATA: int -I2C_SMBUS_BLOCK_MAX: int - -class I2cFunc(IntFlag): - I2C = ... - ADDR_10BIT = ... - PROTOCOL_MANGLING = ... - SMBUS_PEC = ... - NOSTART = ... - SLAVE = ... - SMBUS_BLOCK_PROC_CALL = ... - SMBUS_QUICK = ... - SMBUS_READ_BYTE = ... - SMBUS_WRITE_BYTE = ... - SMBUS_READ_BYTE_DATA = ... - SMBUS_WRITE_BYTE_DATA = ... - SMBUS_READ_WORD_DATA = ... - SMBUS_WRITE_WORD_DATA = ... - SMBUS_PROC_CALL = ... - SMBUS_READ_BLOCK_DATA = ... - SMBUS_WRITE_BLOCK_DATA = ... - SMBUS_READ_I2C_BLOCK = ... - SMBUS_WRITE_I2C_BLOCK = ... - SMBUS_HOST_NOTIFY = ... - SMBUS_BYTE = ... - SMBUS_BYTE_DATA = ... - SMBUS_WORD_DATA = ... - SMBUS_BLOCK_DATA = ... - SMBUS_I2C_BLOCK = ... - SMBUS_EMUL = ... - -I2C_M_RD: int -LP_c_uint8: Type[pointer[c_uint8]] -LP_c_uint16: Type[pointer[c_uint16]] -LP_c_uint32: Type[pointer[c_uint32]] - -class i2c_smbus_data(Array): ... -class union_i2c_smbus_data(Union): ... - -union_pointer_type: pointer[union_i2c_smbus_data] - -class i2c_smbus_ioctl_data(Structure): - @staticmethod - def create( - read_write: int = ..., command: int = ..., size: int = ... - ) -> "i2c_smbus_ioctl_data": ... - -class i2c_msg(Structure): - def __iter__(self) -> int: ... - def __len__(self) -> int: ... - def __bytes__(self) -> str: ... - @staticmethod - def read(address: int, length: int) -> "i2c_msg": ... - @staticmethod - def write(address: int, buf: _UnionT[str, Iterable[int], SupportsBytes]) -> "i2c_msg": ... - -class i2c_rdwr_ioctl_data(Structure): - @staticmethod - def create(*i2c_msg_instances: Sequence[i2c_msg]) -> "i2c_rdwr_ioctl_data": ... - -class SMBus: - fd: int = ... - funcs: I2cFunc = ... - address: Optional[int] = ... - force: Optional[bool] = ... - pec: int = ... - def __init__( - self, bus: _UnionT[None, int, str] = ..., force: bool = ... - ) -> None: ... - def __enter__(self) -> "SMBus": ... - def __exit__( - self, - exc_type: Optional[Type[BaseException]], - exc_val: Optional[BaseException], - exc_tb: Optional[TracebackType], - ) -> None: ... - def open(self, bus: _UnionT[int, str]) -> None: ... - def close(self) -> None: ... - def enable_pec(self, enable: bool) -> None: ... - def write_quick(self, i2c_addr: int, force: Optional[bool] = ...) -> None: ... - def read_byte(self, i2c_addr: int, force: Optional[bool] = ...) -> int: ... - def write_byte( - self, i2c_addr: int, value: int, force: Optional[bool] = ... - ) -> None: ... - def read_byte_data( - self, i2c_addr: int, register: int, force: Optional[bool] = ... - ) -> int: ... - def write_byte_data( - self, i2c_addr: int, register: int, value: int, force: Optional[bool] = ... - ) -> None: ... - def read_word_data( - self, i2c_addr: int, register: int, force: Optional[bool] = ... - ) -> int: ... - def write_word_data( - self, i2c_addr: int, register: int, value: int, force: Optional[bool] = ... - ) -> None: ... - def process_call( - self, i2c_addr: int, register: int, value: int, force: Optional[bool] = ... - ): ... - def read_block_data( - self, i2c_addr: int, register: int, force: Optional[bool] = ... - ) -> List[int]: ... - def write_block_data( - self, - i2c_addr: int, - register: int, - data: Sequence[int], - force: Optional[bool] = ..., - ) -> None: ... - def block_process_call( - self, - i2c_addr: int, - register: int, - data: Sequence[int], - force: Optional[bool] = ..., - ) -> List[int]: ... - def read_i2c_block_data( - self, i2c_addr: int, register: int, length: int, force: Optional[bool] = ... - ) -> List[int]: ... - def write_i2c_block_data( - self, - i2c_addr: int, - register: int, - data: Sequence[int], - force: Optional[bool] = ..., - ) -> None: ... - def i2c_rdwr(self, *i2c_msgs: i2c_msg) -> None: ... diff --git a/scripts/tempSensor/main.py b/scripts/tempSensor/main.py deleted file mode 100644 index b6d8f991..00000000 --- a/scripts/tempSensor/main.py +++ /dev/null @@ -1,17 +0,0 @@ -import time -import machine -import scripts.tempSensor.lib.adafruit_bme680 as adafruit_bme680 - -i2c = machine.I2C(0, scl=machine.Pin(5), sda=machine.Pin(4)) - -bme680 = adafruit_bme680.Adafruit_BME680_I2C(i2c) - -temperature_offset = -5 - -while True: - print("\nTemperature: %0.1f C" % (bme680.temperature + temperature_offset)) - print("Gas: %d ohm" % bme680.gas) - print("Humidity: %0.1f %%" % bme680.relative_humidity) - print("Pressure: %0.3f hPa" % bme680.pressure) - print("Altitude = %0.2f meters" % bme680.altitude) - time.sleep(1) diff --git a/scripts/tempSensor/tempSensor.py b/scripts/tempSensor/tempSensor.py new file mode 100644 index 00000000..ad6903b8 --- /dev/null +++ b/scripts/tempSensor/tempSensor.py @@ -0,0 +1,18 @@ +from bme680 import * +from machine import I2C, Pin +import time + +i2c = I2C(0, scl=Pin(5), sda=Pin(4)) + +bme = BME680_I2C(i2c) + +while True: + print("--------------------------------------------------") + print() + print("Temperature: {:.2f} °C".format(bme.temperature)) + print("Humidity: {:.2f} %".format(bme.humidity)) + print("Pressure: {:.2f} hPa".format(bme.pressure)) + print("Gas: {:.2f} ohms".format(bme.gas)) + print() + time.sleep(3) + From 91983eb9a121600807c63344c8898c15ed98c876 Mon Sep 17 00:00:00 2001 From: Luthira A <79228258+Luthiraa@users.noreply.github.com> Date: Wed, 27 Nov 2024 21:56:39 -0500 Subject: [PATCH 05/19] Add initial files for temperature sensor integration and unique ID generation --- .gitignore | 57 -- scripts/tempSensor/.gitignore | 1 + scripts/tempSensor/adafruit_bme680.mpy | Bin 0 -> 7402 bytes scripts/tempSensor/adafruit_bme680.py | 769 ++++++++++++++++ .../adafruit_bus_device/__init__.py | 0 .../adafruit_bus_device/i2c_device.py | 187 ++++ .../adafruit_bus_device/spi_device.py | 121 +++ scripts/tempSensor/bme680.py | 421 +++++++++ scripts/tempSensor/error.txt | 3 + scripts/tempSensor/hivemq-com-chain.der | Bin 0 -> 1289 bytes scripts/tempSensor/lib/CHANGELOG.md | 50 ++ scripts/tempSensor/lib/LICENSE | 21 + scripts/tempSensor/lib/README.md | 56 ++ .../adafruit_blinka-8.49.0.dist-info/METADATA | 8 + .../adafruit_blinka-8.49.0.dist-info/RECORD | 2 + scripts/tempSensor/lib/as7341.py | 608 +++++++++++++ scripts/tempSensor/lib/as7341_sensor.py | 149 ++++ scripts/tempSensor/lib/as7341_smux_select.py | 49 ++ .../lib/bme680-2.0.0.dist-info/METADATA | 156 ++++ .../lib/bme680-2.0.0.dist-info/RECORD | 7 + scripts/tempSensor/lib/bme680/__init__.py | 486 +++++++++++ scripts/tempSensor/lib/data_logging.py | 166 ++++ scripts/tempSensor/lib/functools.py | 28 + scripts/tempSensor/lib/mqtt_as.py | 824 ++++++++++++++++++ scripts/tempSensor/lib/netman.py | 73 ++ scripts/tempSensor/lib/sdcard/LICENSE | 21 + scripts/tempSensor/lib/sdcard/sdcard.py | 302 +++++++ scripts/tempSensor/lib/sdl_demo_utils.py | 276 ++++++ .../lib/smbus2-0.5.0.dist-info/METADATA | 234 +++++ .../lib/smbus2-0.5.0.dist-info/RECORD | 6 + scripts/tempSensor/lib/smbus2/__init__.py | 26 + scripts/tempSensor/lib/smbus2/py.typed | 0 scripts/tempSensor/lib/smbus2/smbus2.py | 660 ++++++++++++++ scripts/tempSensor/lib/smbus2/smbus2.pyi | 148 ++++ scripts/tempSensor/lib/ufastrsa/__init__.py | 0 scripts/tempSensor/lib/ufastrsa/genprime.py | 136 +++ scripts/tempSensor/lib/ufastrsa/rsa.py | 46 + scripts/tempSensor/lib/ufastrsa/srandom.py | 47 + scripts/tempSensor/lib/ufastrsa/util.py | 14 + scripts/tempSensor/lib/umqtt/robust.py | 44 + scripts/tempSensor/lib/umqtt/simple.py | 217 +++++ .../lib/unique_id-1.0.1.dist-info/METADATA | 11 + .../lib/unique_id-1.0.1.dist-info/RECORD | 5 + scripts/tempSensor/lib/unique_id/__init__.py | 1 + scripts/tempSensor/lib/unique_id/main.py | 58 ++ scripts/tempSensor/lib/unique_id/tests.py | 40 + scripts/tempSensor/lib/urequests_2.py | 203 +++++ scripts/tempSensor/pico_id.txt | 1 + scripts/tempSensor/rsa.json | 1 + scripts/tempSensor/secrets.py | 83 ++ scripts/tempSensor/temp.py | 19 + scripts/tempSensor/test.py | 65 ++ 52 files changed, 6849 insertions(+), 57 deletions(-) delete mode 100644 .gitignore create mode 100644 scripts/tempSensor/.gitignore create mode 100644 scripts/tempSensor/adafruit_bme680.mpy create mode 100644 scripts/tempSensor/adafruit_bme680.py create mode 100644 scripts/tempSensor/adafruit_bus_device/__init__.py create mode 100644 scripts/tempSensor/adafruit_bus_device/i2c_device.py create mode 100644 scripts/tempSensor/adafruit_bus_device/spi_device.py create mode 100644 scripts/tempSensor/bme680.py create mode 100644 scripts/tempSensor/error.txt create mode 100644 scripts/tempSensor/hivemq-com-chain.der create mode 100644 scripts/tempSensor/lib/CHANGELOG.md create mode 100644 scripts/tempSensor/lib/LICENSE create mode 100644 scripts/tempSensor/lib/README.md create mode 100644 scripts/tempSensor/lib/adafruit_blinka-8.49.0.dist-info/METADATA create mode 100644 scripts/tempSensor/lib/adafruit_blinka-8.49.0.dist-info/RECORD create mode 100644 scripts/tempSensor/lib/as7341.py create mode 100644 scripts/tempSensor/lib/as7341_sensor.py create mode 100644 scripts/tempSensor/lib/as7341_smux_select.py create mode 100644 scripts/tempSensor/lib/bme680-2.0.0.dist-info/METADATA create mode 100644 scripts/tempSensor/lib/bme680-2.0.0.dist-info/RECORD create mode 100644 scripts/tempSensor/lib/bme680/__init__.py create mode 100644 scripts/tempSensor/lib/data_logging.py create mode 100644 scripts/tempSensor/lib/functools.py create mode 100644 scripts/tempSensor/lib/mqtt_as.py create mode 100644 scripts/tempSensor/lib/netman.py create mode 100644 scripts/tempSensor/lib/sdcard/LICENSE create mode 100644 scripts/tempSensor/lib/sdcard/sdcard.py create mode 100644 scripts/tempSensor/lib/sdl_demo_utils.py create mode 100644 scripts/tempSensor/lib/smbus2-0.5.0.dist-info/METADATA create mode 100644 scripts/tempSensor/lib/smbus2-0.5.0.dist-info/RECORD create mode 100644 scripts/tempSensor/lib/smbus2/__init__.py create mode 100644 scripts/tempSensor/lib/smbus2/py.typed create mode 100644 scripts/tempSensor/lib/smbus2/smbus2.py create mode 100644 scripts/tempSensor/lib/smbus2/smbus2.pyi create mode 100644 scripts/tempSensor/lib/ufastrsa/__init__.py create mode 100644 scripts/tempSensor/lib/ufastrsa/genprime.py create mode 100644 scripts/tempSensor/lib/ufastrsa/rsa.py create mode 100644 scripts/tempSensor/lib/ufastrsa/srandom.py create mode 100644 scripts/tempSensor/lib/ufastrsa/util.py create mode 100644 scripts/tempSensor/lib/umqtt/robust.py create mode 100644 scripts/tempSensor/lib/umqtt/simple.py create mode 100644 scripts/tempSensor/lib/unique_id-1.0.1.dist-info/METADATA create mode 100644 scripts/tempSensor/lib/unique_id-1.0.1.dist-info/RECORD create mode 100644 scripts/tempSensor/lib/unique_id/__init__.py create mode 100644 scripts/tempSensor/lib/unique_id/main.py create mode 100644 scripts/tempSensor/lib/unique_id/tests.py create mode 100644 scripts/tempSensor/lib/urequests_2.py create mode 100644 scripts/tempSensor/pico_id.txt create mode 100644 scripts/tempSensor/rsa.json create mode 100644 scripts/tempSensor/secrets.py create mode 100644 scripts/tempSensor/temp.py create mode 100644 scripts/tempSensor/test.py diff --git a/.gitignore b/.gitignore deleted file mode 100644 index c80e709c..00000000 --- a/.gitignore +++ /dev/null @@ -1,57 +0,0 @@ -# Temporary and binary files -*~ -*.py[cod] -*.so -*.cfg -!.isort.cfg -!setup.cfg -*.orig -*.log -*.pot -__pycache__/* -.cache/* -.*.swp -*/.ipynb_checkpoints/* -.DS_Store - -# Project files -.ropeproject -.project -.pydevproject -.settings -.idea -.vscode -tags - -# Package files -*.egg -*.eggs/ -.installed.cfg -*.egg-info - -# Unittest and coverage -htmlcov/* -.coverage -.coverage.* -.tox -junit*.xml -coverage.xml -.pytest_cache/ - -# Build and docs folder/files -build/* -dist/* -sdist/* -docs/api/* -docs/_rst/* -docs/_build/* -cover/* -MANIFEST - -# Per-project virtualenvs -.venv*/ -.conda*/ -.python-version - -**/my_secrets.py -ac-training-lab.code-workspace diff --git a/scripts/tempSensor/.gitignore b/scripts/tempSensor/.gitignore new file mode 100644 index 00000000..b8cca463 --- /dev/null +++ b/scripts/tempSensor/.gitignore @@ -0,0 +1 @@ +constants.py \ No newline at end of file diff --git a/scripts/tempSensor/adafruit_bme680.mpy b/scripts/tempSensor/adafruit_bme680.mpy new file mode 100644 index 0000000000000000000000000000000000000000..0e0d14c46498b6128cdf05e935f856080d23fab7 GIT binary patch literal 7402 zcmbtZYfu|kmcA_^gm_9VA>@GLb~gf9U?ep{V2_=_GQ{AJ3D}T?Y?4Mu4JhHENW!u6 zShoa}!(QsbQ0;+CQ^Z-4fVNW@c*uzcSgKs+}ME%qF#|%A_h)UJ{`y&1}_f z?YS)>gdv+$4F>5x&OPVcbH95Y*C$nq1Js}NzY1v4CngSF>@BA6xJrL8xN@Ary{Hr$xkuXqOGUW+R7zt>Vhr+>G zZ&+@Zg#A-Iw6v(lO4OoI9qKWno?-T&vEIi|dMf}L z$BT-4*^}Dhv;DnRMQ!QmY{)-Tr!o@wZ&=XQo#A0Kqm#UQc5I9f0(*tu1g9&|6FSuJNmr3De40SdXiHmL+FOno zjU0E54}#!nj$@Ef%W*+IFvD>|Bs#wkw-y^4IQOI7U`X!liAVeUy3d>#>^>_*<}O|R zG5uXyxM(ferg*rwZ?OB!+1{tR&oTs~V3bS|qhg8~HB-W9m{O*UDQ7B}N~Vg@TD8Up zxxU99dt|7e8$99e>*gGcjv*O6V_>QoBU8hen0?FxOf6H#>}MWi4lva2B`ixDO>oRy zh==3$heIr5tS{_yfte6zXX}lXuq%&`bq=y-?Sm(_5R%=PQ(+_q*#>RHed8RQjZW!Q z8kK`=p+d@_4{{n_Koif@4x zW96*F+3xIUbvfHx?0-m)b=mD59S-|mTw3k$uf6@-NN9C*bhz4&WVy7u+MOvg-2t?+BBieFC8ez_ z+ty)sIf1Cu;j+uW(AI3r&}>s{w%KbOc}XR0auZgSuyPZ(9CEY8?mMV$Y8A{BB5j7! z&?L_XM25(?h~WV*fl8-m;dYO0^Lkn=BC9pR_eVOBDE($5nfOxLxaUPk8|241pC{}w z0#v6x0Nbh1^Z7O)wFFQ0HJ;C6Z5?frwwz*pDi=C!(2h^|s?MUa9 z!_ReIQ5XJQR$s1puJiH%_}0f-Vn&|g`IvT*iq*u*;jbF$P>m7%gVKl|INOXaYfV9Z zEXao@xS%J@kG3Ks(&@B;;7ouIhA*&$lQ1>}U_LY(zR z5~WVXDlzHA?3CZ<4`0|xs%DFFL@M}h4)asM-4ou$Qq30U=+y9S9P>|t050Tz3CmNn zCAmoro-AOZQ?jKn1X^eHgnk+H=^!}nu>Ty-ZD|D)<(RObUxE9c$*?~>>*Ha*68gpC zo)BAQq&U!WY$iA*J00-!tky`&ZO-eN^pC<0z}>PsqaiyudLhiSq_H&Xqx8md?!}-V zh786!?AU-QH^GCEgJ-P7+16@soOl=y1vv;SW316=l80sj+|-PZXKRe4+?OX!#yTw4 z3*57O*nLKVePC-8bAQp}4-a@z?WGQBK@d9KNY^SPJ8jQfEzdnVZhNd@#B4V6r{KeE z?lzw?BhgSrM@Hy~tBG(ahDkXD<}L=KfNLQdYUoJ6i&#t0g6E^IQh`J)qfGqHN${e9 zpd$enA(|fGyc7Nacg_>^d#1ypq1J_9VHqtvMGGOBF{TxKw7|$*B$~8baCREoeLyskoX6*t1>`%J zD|EE-46w_$vnvepi3L5=<2=k%a$#US%?o(Plb%pmK4kz!M9PRJ8q$Ml7|GrwoJzbU zi<}yjbr%iobhML>7{SZ~UL`_{J8s>W3huVOEyO(R@lWzTDm+7hC?Ay#EC;ERU|_1Z zi?W|LpC3~@?2gtJwyjU6-RojHg#QHpwpXwh_2waPUuZGnq)D|}_^qWMNu>Hey0ijG z7|_!aK25c|L_TG>)I9czXwjIx@UnXdk-gLB0Qx>^+Y?|t_BacTa1Zp*i2BAILQ~&P zsOD=VeU0nT2*1w?YuX{~fB!IHh5knqR~4GIAVU)MwnCxt(r+iU1bekm`IbF#FGB4z z6uLD{Tt_w9;Fj)kMEHv755>}S1f}RG%B)Io!7}pUF7%yX^Nvs*H&Knv%}s~x?R|}{ zP0bA*c4r$fRLcz1(F{G8Mw_eBdY5~AL^eGd!#Tx=*k57UyUC+3IJ(R6Ap zF`t3X+z>r4An0y!5q^pAcywqfonFAL$dHtdW>TPJTmW6^sDPvtj0;0)+=db1mjV*? z?VIb#1igeZoAeLb((6en6HTv+dga1Uav=l1X`n;!2^6bIS@tF1fXCL7*a~yM2d0ZK zv>>3&&7_n_En_jS4S|``sd(nI^<>^m$28jA;h`*UgK(|ShX>i9m$Waq2xZ&}M{)Rj z)WdxTyd(8Z4z|Jmu+;*ltdLDvQE(Qb{s^{V{!SFwP#B7*H?akypk<^dv$+OsFb{mr z;jQ>GQqI1(4FjDS+(^+80i|GHD6^hOrf&jyOy1P36-@qGb}8)e)-tBdWItv4+bh?= z_IH-Ar|@d}(u8cjyEmZq#a*%(yw(kn;8ftY=ll%;S~s#zqW%b(kBe3Dvz-tw^!woO z{>{n|q+CN8&`ThG4_}^v=%Lt5L*{}O$CM7pTZ-*1jwANg`SV9CIJj!!Pf?A%)Q$nn8oIz`@Ju84Xy*bg9_0lsPsNDLqWZAz)N`-xdmuL3ZG z(9F%{1YjB@k?7f^laOt8)%ikyMOA#Vvu*UusbK%1Cs?cHFwGpY+WH)hV}}kj^c`sE zwI6P_qg<>dr)H*S!ZXu;uM76LGp=lk`rT3GpSu*ofLAo+QVUv`74_eVewhoiN?Hhu z`d#5N_NA!bGqy&*yq8<4qJ>zh^1*2jMVk1knJLFJSxUB5Qmjl9lczVG{19r1u223KLGo7KuCO+)EG2c4JN(%W}Xz^P$z`V)?|F>>V{}?xLbD8 zUb(u9R+D@~riIv8h5aL)O>cI%iD-KB=CbS?WUqW{IhuiA`R?An2IJsA;^095w6{UE zm1`-i#8!%ujoZ1T;?kg}X?N1v7Jr*+e(V{G&DA>7#6DtgLWF2A(-F;EE=40@h7P26 zp14c#StCKBNwV5d4YwY-%|zsHavO3IjRdPOLxIppNR*QJOCiz(C4gwEkfaHBE=IT1hN!SeTuP$O|p_)8hsZz!N8fYZ$ky5l~Mu{Pq-OSF)^Gap> zoo|?b_Z#Ma^0o7WR;@-E7a`yoYNZIqz3jtqbqibIgI%?iN>zLrk;vvmkPG6>QB^+4 zPmhNuVD7j&etpNB8oe7GJXLpJsiTU&KQh8J4-Yk<9tV;JK0aA2BnCe2+6OoO{`yDi zCIlgYKeXliG=4DacSRM$ZVk{Xr3B>8r>oEtB(i_fRV)nmd-3zv8Cfr5wyu&dQoapI zc&zJ7)lrou{-OLt=(bp`)_VF-cQ;MW_n;PzljHZiNuJ<1P{+!1j7%^EU-m2)W~p z>&C*|HD}RXa*v0u_pYuul^>AG_h|e4b!QQ%wjZS4$T?Lgre1hYZf6grre@#Vv7?si zn)nwZ^eDxgu5YM+w7dgVY6v6EY)mk2g*n zN}a?oEI9e$MG2Nw!?F!&ZsQ&-#gQhvG^;~pu4Iii-z}=EEn6Z+s0NG0au_8eA9dl1 z!p^`80Nl3wYFMZ_ZsrFeoXaI_!bRN5R*=ivTlTHwwv20%nwqN6;ZvSak6cc7pZD?s zT#6s1^5rrGag~DF-3#RuuBBtmm$l7h6-yN(jg5^^{+@OmZ)j+;eGkHJyD*eZ!ywlL z#|qh1+b#HmGlgi9E8yAMN~HEk+MFDCb;q%>rL zkh0N{yO6S7&ZIxM@gC#}zj}XVK9%{9jwr9mxkl>mfyvK)4rDKFzLRzm{{Y=z&}<4X zbrQe+I~mm_I9|n^?`W-6RZI0F&CShL>*>>$CUZ-}cMu6lrcQvcw?-C4`W$JE{DatUaG*g`VV3|>8eszN>d5=_qqe`dJAO7t9 z>uFh2M)ULe`IS^A9f@3HO4HDIYZGhz3$F%FLpN6BfLE!pA}4-mRJXG2F8sy_IUl^m zwT-FqH{04aR*WAAVM5Lbm8~t#Y=~l`EHT*Q|dr2!2}Y|%1ML6twe>>YUe83=C#TFm?Y zoSP3-YU*D;YGc)-;55uTcH23<5~Ty%9h^R*1Bqk%25M#a2I&szXw!5 z{gGgw1@KhGs&>NXwJTH(KQ*JDvDBHU?2g! z%9j3VnsUfE!bEu-HP$Kb3|}{pAbJZlTJ5N3)`zXM3?UH%2v)2}rv2N{slK)YIxt`4 z4V4|z%=L{^wKYqhO!QbSHW@SUSP;Tq8G;%3-(dg^x`%-S^^gA4u`MulLUz74ko4aO zB<4_o>9?R`u2}kF+w|rgrl0S-q@@|v(tk}@8ZcgLfR}AGby+oqi=Tpux>L{Y0#T!k zo!Xt)!CTtG&!+|W(SJMq==a2ru43uGw>P2Nv580>Yp7B!{SR%S!GmG5;&Lqx;Zg?{ JFASGu{|AU;UgrP+ literal 0 HcmV?d00001 diff --git a/scripts/tempSensor/adafruit_bme680.py b/scripts/tempSensor/adafruit_bme680.py new file mode 100644 index 00000000..7c7d5d4b --- /dev/null +++ b/scripts/tempSensor/adafruit_bme680.py @@ -0,0 +1,769 @@ +# SPDX-FileCopyrightText: 2017 ladyada for Adafruit Industries +# +# SPDX-License-Identifier: MIT AND BSD-3-Clause + + +""" +`adafruit_bme680` +================================================================================ + +CircuitPython library for BME680 temperature, pressure and humidity sensor. + + +* Author(s): Limor Fried, William Garber, many others + + +Implementation Notes +-------------------- + +**Hardware:** + +* `Adafruit BME680 Temp, Humidity, Pressure and Gas Sensor `_ + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://github.com/adafruit/circuitpython/releases +* Adafruit's Bus Device library: https://github.com/adafruit/Adafruit_CircuitPython_BusDevice +""" + +import math +import struct +import time + +from micropython import const + + +def delay_microseconds(nusec): + """HELP must be same as dev->delay_us""" + time.sleep(nusec / 1000000.0) + + +try: + # Used only for type annotations. + + import typing + + from busio import I2C, SPI + from circuitpython_typing import ReadableBuffer + from digitalio import DigitalInOut + +except ImportError: + pass + +__version__ = "3.7.9" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_BME680.git" + + +# I2C ADDRESS/BITS/SETTINGS NEW +# ----------------------------------------------------------------------- +_BME68X_ENABLE_HEATER = const(0x00) +_BME68X_DISABLE_HEATER = const(0x01) +_BME68X_DISABLE_GAS_MEAS = const(0x00) +_BME68X_ENABLE_GAS_MEAS_L = const(0x01) +_BME68X_ENABLE_GAS_MEAS_H = const(0x02) +_BME68X_SLEEP_MODE = const(0) +_BME68X_FORCED_MODE = const(1) +_BME68X_VARIANT_GAS_LOW = const(0x00) +_BME68X_VARIANT_GAS_HIGH = const(0x01) +_BME68X_HCTRL_MSK = const(0x08) +_BME68X_HCTRL_POS = const(3) +_BME68X_NBCONV_MSK = const(0x0F) +_BME68X_RUN_GAS_MSK = const(0x30) +_BME68X_RUN_GAS_POS = const(4) +_BME68X_MODE_MSK = const(0x03) +_BME68X_PERIOD_POLL = const(10000) +_BME68X_REG_CTRL_GAS_0 = const(0x70) +_BME68X_REG_CTRL_GAS_1 = const(0x71) + +# I2C ADDRESS/BITS/SETTINGS +# ----------------------------------------------------------------------- +_BME680_CHIPID = const(0x61) + +_BME680_REG_CHIPID = const(0xD0) +_BME68X_REG_VARIANT = const(0xF0) +_BME680_BME680_COEFF_ADDR1 = const(0x89) +_BME680_BME680_COEFF_ADDR2 = const(0xE1) +_BME680_BME680_RES_HEAT_0 = const(0x5A) +_BME680_BME680_GAS_WAIT_0 = const(0x64) + +_BME680_REG_SOFTRESET = const(0xE0) +_BME680_REG_CTRL_GAS = const(0x71) +_BME680_REG_CTRL_HUM = const(0x72) +_BME680_REG_STATUS = const(0x73) +_BME680_REG_CTRL_MEAS = const(0x74) +_BME680_REG_CONFIG = const(0x75) + +_BME680_REG_MEAS_STATUS = const(0x1D) +_BME680_REG_PDATA = const(0x1F) +_BME680_REG_TDATA = const(0x22) +_BME680_REG_HDATA = const(0x25) + +_BME680_SAMPLERATES = (0, 1, 2, 4, 8, 16) +_BME680_FILTERSIZES = (0, 1, 3, 7, 15, 31, 63, 127) + +_BME680_RUNGAS = const(0x10) + +_LOOKUP_TABLE_1 = ( + 2147483647.0, + 2147483647.0, + 2147483647.0, + 2147483647.0, + 2147483647.0, + 2126008810.0, + 2147483647.0, + 2130303777.0, + 2147483647.0, + 2147483647.0, + 2143188679.0, + 2136746228.0, + 2147483647.0, + 2126008810.0, + 2147483647.0, + 2147483647.0, +) + +_LOOKUP_TABLE_2 = ( + 4096000000.0, + 2048000000.0, + 1024000000.0, + 512000000.0, + 255744255.0, + 127110228.0, + 64000000.0, + 32258064.0, + 16016016.0, + 8000000.0, + 4000000.0, + 2000000.0, + 1000000.0, + 500000.0, + 250000.0, + 125000.0, +) + + +def bme_set_bits(reg_data, bitname_msk, bitname_pos, data): + """ + Macro to set bits + data2 = data << bitname_pos + set masked bits from data2 in reg_data + """ + return (reg_data & ~bitname_msk) | ((data << bitname_pos) & bitname_msk) + + +def bme_set_bits_pos_0(reg_data, bitname_msk, data): + """ + Macro to set bits starting from position 0 + set masked bits from data in reg_data + """ + return (reg_data & ~bitname_msk) | (data & bitname_msk) + + +def _read24(arr: ReadableBuffer) -> float: + """Parse an unsigned 24-bit value as a floating point and return it.""" + ret = 0.0 + # print([hex(i) for i in arr]) + for b in arr: + ret *= 256.0 + ret += float(b & 0xFF) + return ret + + +class Adafruit_BME680: + """Driver from BME680 air quality sensor + + :param int refresh_rate: Maximum number of readings per second. Faster property reads + will be from the previous reading.""" + + def __init__(self, *, refresh_rate: int = 10) -> None: + """Check the BME680 was found, read the coefficients and enable the sensor for continuous + reads.""" + self._write(_BME680_REG_SOFTRESET, [0xB6]) + time.sleep(0.005) + + # Check device ID. + chip_id = self._read_byte(_BME680_REG_CHIPID) + if chip_id != _BME680_CHIPID: + raise RuntimeError("Failed to find BME680! Chip ID 0x%x" % chip_id) + + # Get variant + self._chip_variant = self._read_byte(_BME68X_REG_VARIANT) + + self._read_calibration() + + # set up heater + self._write(_BME680_BME680_RES_HEAT_0, [0x73]) + self._write(_BME680_BME680_GAS_WAIT_0, [0x65]) + + self.sea_level_pressure = 1013.25 + """Pressure in hectoPascals at sea level. Used to calibrate :attr:`altitude`.""" + + # Default oversampling and filter register values. + self._pressure_oversample = 0b011 + self._temp_oversample = 0b100 + self._humidity_oversample = 0b010 + self._filter = 0b010 + + # Gas measurements, as a mask applied to _BME680_RUNGAS + self._run_gas = 0xFF + + self._adc_pres = None + self._adc_temp = None + self._adc_hum = None + self._adc_gas = None + self._gas_range = None + self._t_fine = None + + self._last_reading = 0 + self._min_refresh_time = 1 / refresh_rate + + self._amb_temp = 25 # Copy required parameters from reference bme68x_dev struct + self.set_gas_heater(320, 150) # heater 320 deg C for 150 msec + + @property + def pressure_oversample(self) -> int: + """The oversampling for pressure sensor""" + return _BME680_SAMPLERATES[self._pressure_oversample] + + @pressure_oversample.setter + def pressure_oversample(self, sample_rate: int) -> None: + if sample_rate in _BME680_SAMPLERATES: + self._pressure_oversample = _BME680_SAMPLERATES.index(sample_rate) + else: + raise RuntimeError("Invalid oversample") + + @property + def humidity_oversample(self) -> int: + """The oversampling for humidity sensor""" + return _BME680_SAMPLERATES[self._humidity_oversample] + + @humidity_oversample.setter + def humidity_oversample(self, sample_rate: int) -> None: + if sample_rate in _BME680_SAMPLERATES: + self._humidity_oversample = _BME680_SAMPLERATES.index(sample_rate) + else: + raise RuntimeError("Invalid oversample") + + @property + def temperature_oversample(self) -> int: + """The oversampling for temperature sensor""" + return _BME680_SAMPLERATES[self._temp_oversample] + + @temperature_oversample.setter + def temperature_oversample(self, sample_rate: int) -> None: + if sample_rate in _BME680_SAMPLERATES: + self._temp_oversample = _BME680_SAMPLERATES.index(sample_rate) + else: + raise RuntimeError("Invalid oversample") + + @property + def filter_size(self) -> int: + """The filter size for the built in IIR filter""" + return _BME680_FILTERSIZES[self._filter] + + @filter_size.setter + def filter_size(self, size: int) -> None: + if size in _BME680_FILTERSIZES: + self._filter = _BME680_FILTERSIZES.index(size) + else: + raise RuntimeError("Invalid size") + + @property + def temperature(self) -> float: + """The compensated temperature in degrees Celsius.""" + self._perform_reading() + calc_temp = ((self._t_fine * 5) + 128) / 256 + return calc_temp / 100 + + @property + def pressure(self) -> float: + """The barometric pressure in hectoPascals""" + self._perform_reading() + var1 = (self._t_fine / 2) - 64000 + var2 = ((var1 / 4) * (var1 / 4)) / 2048 + var2 = (var2 * self._pressure_calibration[5]) / 4 + var2 = var2 + (var1 * self._pressure_calibration[4] * 2) + var2 = (var2 / 4) + (self._pressure_calibration[3] * 65536) + var1 = ((((var1 / 4) * (var1 / 4)) / 8192) * (self._pressure_calibration[2] * 32) / 8) + ( + (self._pressure_calibration[1] * var1) / 2 + ) + var1 = var1 / 262144 + var1 = ((32768 + var1) * self._pressure_calibration[0]) / 32768 + calc_pres = 1048576 - self._adc_pres + calc_pres = (calc_pres - (var2 / 4096)) * 3125 + calc_pres = (calc_pres / var1) * 2 + var1 = (self._pressure_calibration[8] * (((calc_pres / 8) * (calc_pres / 8)) / 8192)) / 4096 + var2 = ((calc_pres / 4) * self._pressure_calibration[7]) / 8192 + var3 = (((calc_pres / 256) ** 3) * self._pressure_calibration[9]) / 131072 + calc_pres += (var1 + var2 + var3 + (self._pressure_calibration[6] * 128)) / 16 + return calc_pres / 100 + + @property + def relative_humidity(self) -> float: + """The relative humidity in RH %""" + return self.humidity + + @property + def humidity(self) -> float: + """The relative humidity in RH %""" + self._perform_reading() + temp_scaled = ((self._t_fine * 5) + 128) / 256 + var1 = (self._adc_hum - (self._humidity_calibration[0] * 16)) - ( + (temp_scaled * self._humidity_calibration[2]) / 200 + ) + var2 = ( + self._humidity_calibration[1] + * ( + ((temp_scaled * self._humidity_calibration[3]) / 100) + + ( + ((temp_scaled * ((temp_scaled * self._humidity_calibration[4]) / 100)) / 64) + / 100 + ) + + 16384 + ) + ) / 1024 + var3 = var1 * var2 + var4 = self._humidity_calibration[5] * 128 + var4 = (var4 + ((temp_scaled * self._humidity_calibration[6]) / 100)) / 16 + var5 = ((var3 / 16384) * (var3 / 16384)) / 1024 + var6 = (var4 * var5) / 2 + calc_hum = (((var3 + var6) / 1024) * 1000) / 4096 + calc_hum /= 1000 # get back to RH + + calc_hum = min(calc_hum, 100) + calc_hum = max(calc_hum, 0) + return calc_hum + + @property + def altitude(self) -> float: + """The altitude based on current :attr:`pressure` vs the sea level pressure + (:attr:`sea_level_pressure`) - which you must enter ahead of time)""" + pressure = self.pressure # in Si units for hPascal + return 44330 * (1.0 - math.pow(pressure / self.sea_level_pressure, 0.1903)) + + @property + def gas(self) -> int: + """The gas resistance in ohms""" + self._perform_reading() + if self._chip_variant == 0x01: + # taken from https://github.com/BoschSensortec/BME68x-Sensor-API + var1 = 262144 >> self._gas_range + var2 = self._adc_gas - 512 + var2 *= 3 + var2 = 4096 + var2 + calc_gas_res = (10000 * var1) / var2 + calc_gas_res = calc_gas_res * 100 + else: + var1 = ((1340 + (5 * self._sw_err)) * (_LOOKUP_TABLE_1[self._gas_range])) / 65536 + var2 = ((self._adc_gas * 32768) - 16777216) + var1 + var3 = (_LOOKUP_TABLE_2[self._gas_range] * var1) / 512 + calc_gas_res = (var3 + (var2 / 2)) / var2 + return int(calc_gas_res) + + def _perform_reading(self) -> None: + """Perform a single-shot reading from the sensor and fill internal data structure for + calculations""" + if time.monotonic() - self._last_reading < self._min_refresh_time: + return + + # set filter + self._write(_BME680_REG_CONFIG, [self._filter << 2]) + # turn on temp oversample & pressure oversample + self._write( + _BME680_REG_CTRL_MEAS, + [(self._temp_oversample << 5) | (self._pressure_oversample << 2)], + ) + # turn on humidity oversample + self._write(_BME680_REG_CTRL_HUM, [self._humidity_oversample]) + # gas measurements enabled + if self._chip_variant == 0x01: + self._write(_BME680_REG_CTRL_GAS, [(self._run_gas & _BME680_RUNGAS) << 1]) + else: + self._write(_BME680_REG_CTRL_GAS, [(self._run_gas & _BME680_RUNGAS)]) + ctrl = self._read_byte(_BME680_REG_CTRL_MEAS) + ctrl = (ctrl & 0xFC) | 0x01 # enable single shot! + self._write(_BME680_REG_CTRL_MEAS, [ctrl]) + new_data = False + while not new_data: + data = self._read(_BME680_REG_MEAS_STATUS, 17) + new_data = data[0] & 0x80 != 0 + time.sleep(0.005) + self._last_reading = time.monotonic() + + self._adc_pres = _read24(data[2:5]) / 16 + self._adc_temp = _read24(data[5:8]) / 16 + self._adc_hum = struct.unpack(">H", bytes(data[8:10]))[0] + if self._chip_variant == 0x01: + self._adc_gas = int(struct.unpack(">H", bytes(data[15:17]))[0] / 64) + self._gas_range = data[16] & 0x0F + else: + self._adc_gas = int(struct.unpack(">H", bytes(data[13:15]))[0] / 64) + self._gas_range = data[14] & 0x0F + + var1 = (self._adc_temp / 8) - (self._temp_calibration[0] * 2) + var2 = (var1 * self._temp_calibration[1]) / 2048 + var3 = ((var1 / 2) * (var1 / 2)) / 4096 + var3 = (var3 * self._temp_calibration[2] * 16) / 16384 + self._t_fine = int(var2 + var3) + + def _read_calibration(self) -> None: + """Read & save the calibration coefficients""" + coeff = self._read(_BME680_BME680_COEFF_ADDR1, 25) + coeff += self._read(_BME680_BME680_COEFF_ADDR2, 16) + + coeff = list(struct.unpack(" int: + """Read a byte register value and return it""" + return self._read(register, 1)[0] + + def _read(self, register: int, length: int) -> bytearray: + raise NotImplementedError() + + def _write(self, register: int, values: bytearray) -> None: + raise NotImplementedError() + + def set_gas_heater(self, heater_temp: int, heater_time: int) -> bool: + """ + Enable and configure gas reading + heater (None disables) + :param heater_temp: Desired temperature in degrees Centigrade + :param heater_time: Time to keep heater on in milliseconds + :return: True on success, False on failure + """ + try: + if (heater_temp is None) or (heater_time is None): + self._set_heatr_conf(heater_temp or 0, heater_time or 0, enable=False) + else: + self._set_heatr_conf(heater_temp, heater_time) + except OSError: + return False + return True + + def _set_heatr_conf(self, heater_temp: int, heater_time: int, enable: bool = True) -> None: + # restrict to BME68X_FORCED_MODE + op_mode: int = _BME68X_FORCED_MODE + nb_conv: int = 0 + hctrl: int = _BME68X_ENABLE_HEATER + run_gas: int = 0 + ctrl_gas_data_0: int = 0 + ctrl_gas_data_1: int = 0 + + self._set_op_mode(_BME68X_SLEEP_MODE) + self._set_conf(heater_temp, heater_time, op_mode) + ctrl_gas_data_0 = self._read_byte(_BME68X_REG_CTRL_GAS_0) + ctrl_gas_data_1 = self._read_byte(_BME68X_REG_CTRL_GAS_1) + if enable: + hctrl = _BME68X_ENABLE_HEATER + if self._chip_variant == _BME68X_VARIANT_GAS_HIGH: + run_gas = _BME68X_ENABLE_GAS_MEAS_H + else: + run_gas = _BME68X_ENABLE_GAS_MEAS_L + else: + hctrl = _BME68X_DISABLE_HEATER + run_gas = _BME68X_DISABLE_GAS_MEAS + self._run_gas = ~(run_gas - 1) + + ctrl_gas_data_0 = bme_set_bits(ctrl_gas_data_0, _BME68X_HCTRL_MSK, _BME68X_HCTRL_POS, hctrl) + ctrl_gas_data_1 = bme_set_bits_pos_0(ctrl_gas_data_1, _BME68X_NBCONV_MSK, nb_conv) + ctrl_gas_data_1 = bme_set_bits( + ctrl_gas_data_1, _BME68X_RUN_GAS_MSK, _BME68X_RUN_GAS_POS, run_gas + ) + self._write(_BME68X_REG_CTRL_GAS_0, [ctrl_gas_data_0]) + self._write(_BME68X_REG_CTRL_GAS_1, [ctrl_gas_data_1]) + + def _set_op_mode(self, op_mode: int) -> None: + """ + * @brief This API is used to set the operation mode of the sensor + """ + tmp_pow_mode: int = 0 + pow_mode: int = _BME68X_FORCED_MODE + # Call until in sleep + + # was a do {} while() loop + while pow_mode != _BME68X_SLEEP_MODE: + tmp_pow_mode = self._read_byte(_BME680_REG_CTRL_MEAS) + # Put to sleep before changing mode + pow_mode = tmp_pow_mode & _BME68X_MODE_MSK + if pow_mode != _BME68X_SLEEP_MODE: + tmp_pow_mode &= ~_BME68X_MODE_MSK # Set to sleep + self._write(_BME680_REG_CTRL_MEAS, [tmp_pow_mode]) + # dev->delay_us(_BME68X_PERIOD_POLL, dev->intf_ptr) # HELP + delay_microseconds(_BME68X_PERIOD_POLL) + # Already in sleep + if op_mode != _BME68X_SLEEP_MODE: + tmp_pow_mode = (tmp_pow_mode & ~_BME68X_MODE_MSK) | (op_mode & _BME68X_MODE_MSK) + self._write(_BME680_REG_CTRL_MEAS, [tmp_pow_mode]) + + def _set_conf(self, heater_temp: int, heater_time: int, op_mode: int) -> None: + """ + This internal API is used to set heater configurations + """ + + if op_mode != _BME68X_FORCED_MODE: + raise OSError("GasHeaterException: _set_conf not forced mode") + rh_reg_data: int = self._calc_res_heat(heater_temp) + gw_reg_data: int = self._calc_gas_wait(heater_time) + self._write(_BME680_BME680_RES_HEAT_0, [rh_reg_data]) + self._write(_BME680_BME680_GAS_WAIT_0, [gw_reg_data]) + + def _calc_res_heat(self, temp: int) -> int: + """ + This internal API is used to calculate the heater resistance value using float + """ + gh1: int = self._gas_calibration[0] + gh2: int = self._gas_calibration[1] + gh3: int = self._gas_calibration[2] + htr: int = self._heat_range + htv: int = self._heat_val + amb: int = self._amb_temp + + temp = min(temp, 400) # Cap temperature + + var1: int = ((int(amb) * gh3) / 1000) * 256 + var2: int = (gh1 + 784) * (((((gh2 + 154009) * temp * 5) / 100) + 3276800) / 10) + var3: int = var1 + (var2 / 2) + var4: int = var3 / (htr + 4) + var5: int = (131 * htv) + 65536 + heatr_res_x100: int = int(((var4 / var5) - 250) * 34) + heatr_res: int = int((heatr_res_x100 + 50) / 100) + + return heatr_res + + def _calc_res_heat(self, temp: int) -> int: + """ + This internal API is used to calculate the heater resistance value + """ + gh1: float = float(self._gas_calibration[0]) + gh2: float = float(self._gas_calibration[1]) + gh3: float = float(self._gas_calibration[2]) + htr: float = float(self._heat_range) + htv: float = float(self._heat_val) + amb: float = float(self._amb_temp) + + temp = min(temp, 400) # Cap temperature + + var1: float = (gh1 / (16.0)) + 49.0 + var2: float = ((gh2 / (32768.0)) * (0.0005)) + 0.00235 + var3: float = gh3 / (1024.0) + var4: float = var1 * (1.0 + (var2 * float(temp))) + var5: float = var4 + (var3 * amb) + res_heat: int = int(3.4 * ((var5 * (4 / (4 + htr)) * (1 / (1 + (htv * 0.002)))) - 25)) + return res_heat + + def _calc_gas_wait(self, dur: int) -> int: + """ + This internal API is used to calculate the gas wait + """ + factor: int = 0 + durval: int = 0xFF # Max duration + + if dur >= 0xFC0: + return durval + while dur > 0x3F: + dur = dur / 4 + factor += 1 + durval = int(dur + (factor * 64)) + return durval + + +class Adafruit_BME680_I2C(Adafruit_BME680): + """Driver for I2C connected BME680. + + :param ~busio.I2C i2c: The I2C bus the BME680 is connected to. + :param int address: I2C device address. Defaults to :const:`0x77` + :param bool debug: Print debug statements when `True`. Defaults to `False` + :param int refresh_rate: Maximum number of readings per second. Faster property reads + will be from the previous reading. + + **Quickstart: Importing and using the BME680** + + Here is an example of using the :class:`BMP680_I2C` class. + First you will need to import the libraries to use the sensor + + .. code-block:: python + + import board + import adafruit_bme680 + + Once this is done you can define your ``board.I2C`` object and define your sensor object + + .. code-block:: python + + i2c = board.I2C() # uses board.SCL and board.SDA + bme680 = adafruit_bme680.Adafruit_BME680_I2C(i2c) + + You need to setup the pressure at sea level + + .. code-block:: python + + bme680.sea_level_pressure = 1013.25 + + Now you have access to the :attr:`temperature`, :attr:`gas`, :attr:`relative_humidity`, + :attr:`pressure` and :attr:`altitude` attributes + + .. code-block:: python + + temperature = bme680.temperature + gas = bme680.gas + relative_humidity = bme680.relative_humidity + pressure = bme680.pressure + altitude = bme680.altitude + + """ + + def __init__( + self, + i2c: I2C, + address: int = 0x77, + debug: bool = False, + *, + refresh_rate: int = 10, + ) -> None: + """Initialize the I2C device at the 'address' given""" + from adafruit_bus_device import ( + i2c_device, + ) + + self._i2c = i2c_device.I2CDevice(i2c, address) + self._debug = debug + super().__init__(refresh_rate=refresh_rate) + + def _read(self, register: int, length: int) -> bytearray: + """Returns an array of 'length' bytes from the 'register'""" + with self._i2c as i2c: + i2c.write(bytes([register & 0xFF])) + result = bytearray(length) + i2c.readinto(result) + if self._debug: + print(f"\t${register:02X} => {[hex(i) for i in result]}") + return result + + def _write(self, register: int, values: ReadableBuffer) -> None: + """Writes an array of 'length' bytes to the 'register'""" + with self._i2c as i2c: + buffer = bytearray(2 * len(values)) + for i, value in enumerate(values): + buffer[2 * i] = register + i + buffer[2 * i + 1] = value + i2c.write(buffer) + if self._debug: + print(f"\t${values[0]:02X} <= {[hex(i) for i in values[1:]]}") + + +class Adafruit_BME680_SPI(Adafruit_BME680): + """Driver for SPI connected BME680. + + :param ~busio.SPI spi: SPI device + :param ~digitalio.DigitalInOut cs: Chip Select + :param bool debug: Print debug statements when `True`. Defaults to `False` + :param int baudrate: Clock rate, default is :const:`100000` + :param int refresh_rate: Maximum number of readings per second. Faster property reads + will be from the previous reading. + + + **Quickstart: Importing and using the BME680** + + Here is an example of using the :class:`BMP680_SPI` class. + First you will need to import the libraries to use the sensor + + .. code-block:: python + + import board + from digitalio import DigitalInOut, Direction + import adafruit_bme680 + + Once this is done you can define your ``board.SPI`` object and define your sensor object + + .. code-block:: python + + cs = digitalio.DigitalInOut(board.D10) + spi = board.SPI() + bme680 = adafruit_bme680.Adafruit_BME680_SPI(spi, cs) + + You need to setup the pressure at sea level + + .. code-block:: python + + bme680.sea_level_pressure = 1013.25 + + Now you have access to the :attr:`temperature`, :attr:`gas`, :attr:`relative_humidity`, + :attr:`pressure` and :attr:`altitude` attributes + + .. code-block:: python + + temperature = bme680.temperature + gas = bme680.gas + relative_humidity = bme680.relative_humidity + pressure = bme680.pressure + altitude = bme680.altitude + + """ + + def __init__( # noqa: PLR0913 Too many arguments in function definition + self, + spi: SPI, + cs: DigitalInOut, + baudrate: int = 100000, + debug: bool = False, + *, + refresh_rate: int = 10, + ) -> None: + from adafruit_bus_device import ( + spi_device, + ) + + self._spi = spi_device.SPIDevice(spi, cs, baudrate=baudrate) + self._debug = debug + super().__init__(refresh_rate=refresh_rate) + + def _read(self, register: int, length: int) -> bytearray: + if register != _BME680_REG_STATUS: + # _BME680_REG_STATUS exists in both SPI memory pages + # For all other registers, we must set the correct memory page + self._set_spi_mem_page(register) + + register = (register | 0x80) & 0xFF # Read single, bit 7 high. + with self._spi as spi: + spi.write(bytearray([register])) + result = bytearray(length) + spi.readinto(result) + if self._debug: + print(f"\t${register:02X} => {[hex(i) for i in result]}") + return result + + def _write(self, register: int, values: ReadableBuffer) -> None: + if register != _BME680_REG_STATUS: + # _BME680_REG_STATUS exists in both SPI memory pages + # For all other registers, we must set the correct memory page + self._set_spi_mem_page(register) + register &= 0x7F # Write, bit 7 low. + with self._spi as spi: + buffer = bytearray(2 * len(values)) + for i, value in enumerate(values): + buffer[2 * i] = register + i + buffer[2 * i + 1] = value & 0xFF + spi.write(buffer) + if self._debug: + print(f"\t${values[0]:02X} <= {[hex(i) for i in values[1:]]}") + + def _set_spi_mem_page(self, register: int) -> None: + spi_mem_page = 0x00 + if register < 0x80: + spi_mem_page = 0x10 + self._write(_BME680_REG_STATUS, [spi_mem_page]) diff --git a/scripts/tempSensor/adafruit_bus_device/__init__.py b/scripts/tempSensor/adafruit_bus_device/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/scripts/tempSensor/adafruit_bus_device/i2c_device.py b/scripts/tempSensor/adafruit_bus_device/i2c_device.py new file mode 100644 index 00000000..c605290d --- /dev/null +++ b/scripts/tempSensor/adafruit_bus_device/i2c_device.py @@ -0,0 +1,187 @@ +# SPDX-FileCopyrightText: 2016 Scott Shawcroft for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +""" +`adafruit_bus_device.i2c_device` - I2C Bus Device +==================================================== +""" + +import time + +try: + from typing import Optional, Type + from types import TracebackType + from circuitpython_typing import ReadableBuffer, WriteableBuffer + + # Used only for type annotations. + from busio import I2C +except ImportError: + pass + + +__version__ = "5.2.10" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_BusDevice.git" + + +class I2CDevice: + """ + Represents a single I2C device and manages locking the bus and the device + address. + + :param ~busio.I2C i2c: The I2C bus the device is on + :param int device_address: The 7 bit device address + :param bool probe: Probe for the device upon object creation, default is true + + .. note:: This class is **NOT** built into CircuitPython. See + :ref:`here for install instructions `. + + Example: + + .. code-block:: python + + import busio + from board import * + from adafruit_bus_device.i2c_device import I2CDevice + + with busio.I2C(SCL, SDA) as i2c: + device = I2CDevice(i2c, 0x70) + bytes_read = bytearray(4) + with device: + device.readinto(bytes_read) + # A second transaction + with device: + device.write(bytes_read) + """ + + def __init__(self, i2c: I2C, device_address: int, probe: bool = True) -> None: + self.i2c = i2c + self.device_address = device_address + + if probe: + self.__probe_for_device() + + def readinto( + self, buf: WriteableBuffer, *, start: int = 0, end: Optional[int] = None + ) -> None: + """ + Read into ``buf`` from the device. The number of bytes read will be the + length of ``buf``. + + If ``start`` or ``end`` is provided, then the buffer will be sliced + as if ``buf[start:end]``. This will not cause an allocation like + ``buf[start:end]`` will so it saves memory. + + :param ~WriteableBuffer buffer: buffer to write into + :param int start: Index to start writing at + :param int end: Index to write up to but not include; if None, use ``len(buf)`` + """ + if end is None: + end = len(buf) + self.i2c.readfrom_into(self.device_address, buf, start=start, end=end) + + def write( + self, buf: ReadableBuffer, *, start: int = 0, end: Optional[int] = None + ) -> None: + """ + Write the bytes from ``buffer`` to the device, then transmit a stop + bit. + + If ``start`` or ``end`` is provided, then the buffer will be sliced + as if ``buffer[start:end]``. This will not cause an allocation like + ``buffer[start:end]`` will so it saves memory. + + :param ~ReadableBuffer buffer: buffer containing the bytes to write + :param int start: Index to start writing from + :param int end: Index to read up to but not include; if None, use ``len(buf)`` + """ + if end is None: + end = len(buf) + self.i2c.writeto(self.device_address, buf, start=start, end=end) + + # pylint: disable-msg=too-many-arguments + def write_then_readinto( + self, + out_buffer: ReadableBuffer, + in_buffer: WriteableBuffer, + *, + out_start: int = 0, + out_end: Optional[int] = None, + in_start: int = 0, + in_end: Optional[int] = None + ) -> None: + """ + Write the bytes from ``out_buffer`` to the device, then immediately + reads into ``in_buffer`` from the device. The number of bytes read + will be the length of ``in_buffer``. + + If ``out_start`` or ``out_end`` is provided, then the output buffer + will be sliced as if ``out_buffer[out_start:out_end]``. This will + not cause an allocation like ``buffer[out_start:out_end]`` will so + it saves memory. + + If ``in_start`` or ``in_end`` is provided, then the input buffer + will be sliced as if ``in_buffer[in_start:in_end]``. This will not + cause an allocation like ``in_buffer[in_start:in_end]`` will so + it saves memory. + + :param ~ReadableBuffer out_buffer: buffer containing the bytes to write + :param ~WriteableBuffer in_buffer: buffer containing the bytes to read into + :param int out_start: Index to start writing from + :param int out_end: Index to read up to but not include; if None, use ``len(out_buffer)`` + :param int in_start: Index to start writing at + :param int in_end: Index to write up to but not include; if None, use ``len(in_buffer)`` + """ + if out_end is None: + out_end = len(out_buffer) + if in_end is None: + in_end = len(in_buffer) + + self.i2c.writeto_then_readfrom( + self.device_address, + out_buffer, + in_buffer, + out_start=out_start, + out_end=out_end, + in_start=in_start, + in_end=in_end, + ) + + # pylint: enable-msg=too-many-arguments + + def __enter__(self) -> "I2CDevice": + while not self.i2c.try_lock(): + time.sleep(0) + return self + + def __exit__( + self, + exc_type: Optional[Type[type]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> bool: + self.i2c.unlock() + return False + + def __probe_for_device(self) -> None: + """ + Try to read a byte from an address, + if you get an OSError it means the device is not there + or that the device does not support these means of probing + """ + while not self.i2c.try_lock(): + time.sleep(0) + try: + self.i2c.writeto(self.device_address, b"") + except OSError: + # some OS's dont like writing an empty bytesting... + # Retry by reading a byte + try: + result = bytearray(1) + self.i2c.readfrom_into(self.device_address, result) + except OSError: + # pylint: disable=raise-missing-from + raise ValueError("No I2C device at address: 0x%x" % self.device_address) + # pylint: enable=raise-missing-from + finally: + self.i2c.unlock() diff --git a/scripts/tempSensor/adafruit_bus_device/spi_device.py b/scripts/tempSensor/adafruit_bus_device/spi_device.py new file mode 100644 index 00000000..60954e0c --- /dev/null +++ b/scripts/tempSensor/adafruit_bus_device/spi_device.py @@ -0,0 +1,121 @@ +# SPDX-FileCopyrightText: 2016 Scott Shawcroft for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +# pylint: disable=too-few-public-methods + +""" +`adafruit_bus_device.spi_device` - SPI Bus Device +==================================================== +""" + +import time + +try: + from typing import Optional, Type + from types import TracebackType + + # Used only for type annotations. + from busio import SPI + from digitalio import DigitalInOut +except ImportError: + pass + + +__version__ = "5.2.10" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_BusDevice.git" + + +class SPIDevice: + """ + Represents a single SPI device and manages locking the bus and the device + address. + + :param ~busio.SPI spi: The SPI bus the device is on + :param ~digitalio.DigitalInOut chip_select: The chip select pin object that implements the + DigitalInOut API. + :param bool cs_active_value: Set to True if your device requires CS to be active high. + Defaults to False. + :param int baudrate: The desired SCK clock rate in Hertz. The actual clock rate may be + higher or lower due to the granularity of available clock settings (MCU dependent). + :param int polarity: The base state of the SCK clock pin (0 or 1). + :param int phase: The edge of the clock that data is captured. First (0) or second (1). + Rising or falling depends on SCK clock polarity. + :param int extra_clocks: The minimum number of clock cycles to cycle the bus after CS is high. + (Used for SD cards.) + + .. note:: This class is **NOT** built into CircuitPython. See + :ref:`here for install instructions `. + + Example: + + .. code-block:: python + + import busio + import digitalio + from board import * + from adafruit_bus_device.spi_device import SPIDevice + + with busio.SPI(SCK, MOSI, MISO) as spi_bus: + cs = digitalio.DigitalInOut(D10) + device = SPIDevice(spi_bus, cs) + bytes_read = bytearray(4) + # The object assigned to spi in the with statements below + # is the original spi_bus object. We are using the busio.SPI + # operations busio.SPI.readinto() and busio.SPI.write(). + with device as spi: + spi.readinto(bytes_read) + # A second transaction + with device as spi: + spi.write(bytes_read) + """ + + def __init__( + self, + spi: SPI, + chip_select: Optional[DigitalInOut] = None, + *, + cs_active_value: bool = False, + baudrate: int = 100000, + polarity: int = 0, + phase: int = 0, + extra_clocks: int = 0 + ) -> None: + self.spi = spi + self.baudrate = baudrate + self.polarity = polarity + self.phase = phase + self.extra_clocks = extra_clocks + self.chip_select = chip_select + self.cs_active_value = cs_active_value + if self.chip_select: + self.chip_select.switch_to_output(value=not self.cs_active_value) + + def __enter__(self) -> SPI: + while not self.spi.try_lock(): + time.sleep(0) + self.spi.configure( + baudrate=self.baudrate, polarity=self.polarity, phase=self.phase + ) + if self.chip_select: + self.chip_select.value = self.cs_active_value + return self.spi + + def __exit__( + self, + exc_type: Optional[Type[type]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> bool: + if self.chip_select: + self.chip_select.value = not self.cs_active_value + if self.extra_clocks > 0: + buf = bytearray(1) + buf[0] = 0xFF + clocks = self.extra_clocks // 8 + if self.extra_clocks % 8 != 0: + clocks += 1 + for _ in range(clocks): + self.spi.write(buf) + self.spi.unlock() + return False diff --git a/scripts/tempSensor/bme680.py b/scripts/tempSensor/bme680.py new file mode 100644 index 00000000..bd2757ee --- /dev/null +++ b/scripts/tempSensor/bme680.py @@ -0,0 +1,421 @@ +# The MIT License (MIT) +# +# Copyright (c) 2017 ladyada for Adafruit Industries +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +# We have a lot of attributes for this complex sensor. +# pylint: disable=too-many-instance-attributes + +""" +`bme680` - BME680 - Temperature, Humidity, Pressure & Gas Sensor +================================================================ + +MicroPython driver from BME680 air quality sensor, based on Adafruit_bme680 + +* Author(s): Limor 'Ladyada' Fried of Adafruit + Jeff Raber (SPI support) + and many more contributors +""" + +import time +import math +from micropython import const +from ubinascii import hexlify as hex +try: + import struct +except ImportError: + import ustruct as struct + +# I2C ADDRESS/BITS/SETTINGS +# ----------------------------------------------------------------------- +_BME680_CHIPID = const(0x61) + +_BME680_REG_CHIPID = const(0xD0) +_BME680_BME680_COEFF_ADDR1 = const(0x89) +_BME680_BME680_COEFF_ADDR2 = const(0xE1) +_BME680_BME680_RES_HEAT_0 = const(0x5A) +_BME680_BME680_GAS_WAIT_0 = const(0x64) + +_BME680_REG_SOFTRESET = const(0xE0) +_BME680_REG_CTRL_GAS = const(0x71) +_BME680_REG_CTRL_HUM = const(0x72) +_BME280_REG_STATUS = const(0xF3) +_BME680_REG_CTRL_MEAS = const(0x74) +_BME680_REG_CONFIG = const(0x75) + +_BME680_REG_PAGE_SELECT = const(0x73) +_BME680_REG_MEAS_STATUS = const(0x1D) +_BME680_REG_PDATA = const(0x1F) +_BME680_REG_TDATA = const(0x22) +_BME680_REG_HDATA = const(0x25) + +_BME680_SAMPLERATES = (0, 1, 2, 4, 8, 16) +_BME680_FILTERSIZES = (0, 1, 3, 7, 15, 31, 63, 127) + +_BME680_RUNGAS = const(0x10) + +_LOOKUP_TABLE_1 = (2147483647.0, 2147483647.0, 2147483647.0, 2147483647.0, 2147483647.0, + 2126008810.0, 2147483647.0, 2130303777.0, 2147483647.0, 2147483647.0, + 2143188679.0, 2136746228.0, 2147483647.0, 2126008810.0, 2147483647.0, + 2147483647.0) + +_LOOKUP_TABLE_2 = (4096000000.0, 2048000000.0, 1024000000.0, 512000000.0, 255744255.0, 127110228.0, + 64000000.0, 32258064.0, 16016016.0, 8000000.0, 4000000.0, 2000000.0, 1000000.0, + 500000.0, 250000.0, 125000.0) + + +def _read24(arr): + """Parse an unsigned 24-bit value as a floating point and return it.""" + ret = 0.0 + #print([hex(i) for i in arr]) + for b in arr: + ret *= 256.0 + ret += float(b & 0xFF) + return ret + + +class Adafruit_BME680: + """Driver from BME680 air quality sensor + + :param int refresh_rate: Maximum number of readings per second. Faster property reads + will be from the previous reading.""" + def __init__(self, *, refresh_rate=10): + """Check the BME680 was found, read the coefficients and enable the sensor for continuous + reads.""" + self._write(_BME680_REG_SOFTRESET, [0xB6]) + time.sleep(0.005) + + # Check device ID. + chip_id = self._read_byte(_BME680_REG_CHIPID) + if chip_id != _BME680_CHIPID: + raise RuntimeError('Failed to find BME680! Chip ID 0x%x' % chip_id) + + self._read_calibration() + + # set up heater + self._write(_BME680_BME680_RES_HEAT_0, [0x73]) + self._write(_BME680_BME680_GAS_WAIT_0, [0x65]) + + self.sea_level_pressure = 1013.25 + """Pressure in hectoPascals at sea level. Used to calibrate ``altitude``.""" + + # Default oversampling and filter register values. + self._pressure_oversample = 0b011 + self._temp_oversample = 0b100 + self._humidity_oversample = 0b010 + self._filter = 0b010 + + self._adc_pres = None + self._adc_temp = None + self._adc_hum = None + self._adc_gas = None + self._gas_range = None + self._t_fine = None + + self._last_reading = time.ticks_ms() + self._min_refresh_time = 1000 // refresh_rate + + @property + def pressure_oversample(self): + """The oversampling for pressure sensor""" + return _BME680_SAMPLERATES[self._pressure_oversample] + + @pressure_oversample.setter + def pressure_oversample(self, sample_rate): + if sample_rate in _BME680_SAMPLERATES: + self._pressure_oversample = _BME680_SAMPLERATES.index(sample_rate) + else: + raise RuntimeError("Invalid oversample") + + @property + def humidity_oversample(self): + """The oversampling for humidity sensor""" + return _BME680_SAMPLERATES[self._humidity_oversample] + + @humidity_oversample.setter + def humidity_oversample(self, sample_rate): + if sample_rate in _BME680_SAMPLERATES: + self._humidity_oversample = _BME680_SAMPLERATES.index(sample_rate) + else: + raise RuntimeError("Invalid oversample") + + @property + def temperature_oversample(self): + """The oversampling for temperature sensor""" + return _BME680_SAMPLERATES[self._temp_oversample] + + @temperature_oversample.setter + def temperature_oversample(self, sample_rate): + if sample_rate in _BME680_SAMPLERATES: + self._temp_oversample = _BME680_SAMPLERATES.index(sample_rate) + else: + raise RuntimeError("Invalid oversample") + + @property + def filter_size(self): + """The filter size for the built in IIR filter""" + return _BME680_FILTERSIZES[self._filter] + + @filter_size.setter + def filter_size(self, size): + if size in _BME680_FILTERSIZES: + self._filter = _BME680_FILTERSIZES[size] + else: + raise RuntimeError("Invalid size") + + @property + def temperature(self): + """The compensated temperature in degrees celsius.""" + self._perform_reading() + calc_temp = (((self._t_fine * 5) + 128) / 256) + return calc_temp / 100 + + @property + def pressure(self): + """The barometric pressure in hectoPascals""" + self._perform_reading() + var1 = (self._t_fine / 2) - 64000 + var2 = ((var1 / 4) * (var1 / 4)) / 2048 + var2 = (var2 * self._pressure_calibration[5]) / 4 + var2 = var2 + (var1 * self._pressure_calibration[4] * 2) + var2 = (var2 / 4) + (self._pressure_calibration[3] * 65536) + var1 = (((((var1 / 4) * (var1 / 4)) / 8192) * + (self._pressure_calibration[2] * 32) / 8) + + ((self._pressure_calibration[1] * var1) / 2)) + var1 = var1 / 262144 + var1 = ((32768 + var1) * self._pressure_calibration[0]) / 32768 + calc_pres = 1048576 - self._adc_pres + calc_pres = (calc_pres - (var2 / 4096)) * 3125 + calc_pres = (calc_pres / var1) * 2 + var1 = (self._pressure_calibration[8] * (((calc_pres / 8) * (calc_pres / 8)) / 8192)) / 4096 + var2 = ((calc_pres / 4) * self._pressure_calibration[7]) / 8192 + var3 = (((calc_pres / 256) ** 3) * self._pressure_calibration[9]) / 131072 + calc_pres += ((var1 + var2 + var3 + (self._pressure_calibration[6] * 128)) / 16) + return calc_pres/100 + + @property + def humidity(self): + """The relative humidity in RH %""" + self._perform_reading() + temp_scaled = ((self._t_fine * 5) + 128) / 256 + var1 = ((self._adc_hum - (self._humidity_calibration[0] * 16)) - + ((temp_scaled * self._humidity_calibration[2]) / 200)) + var2 = (self._humidity_calibration[1] * + (((temp_scaled * self._humidity_calibration[3]) / 100) + + (((temp_scaled * ((temp_scaled * self._humidity_calibration[4]) / 100)) / + 64) / 100) + 16384)) / 1024 + var3 = var1 * var2 + var4 = self._humidity_calibration[5] * 128 + var4 = (var4 + ((temp_scaled * self._humidity_calibration[6]) / 100)) / 16 + var5 = ((var3 / 16384) * (var3 / 16384)) / 1024 + var6 = (var4 * var5) / 2 + calc_hum = (((var3 + var6) / 1024) * 1000) / 4096 + calc_hum /= 1000 # get back to RH + + if calc_hum > 100: + calc_hum = 100 + if calc_hum < 0: + calc_hum = 0 + return calc_hum + + @property + def altitude(self): + """The altitude based on current ``pressure`` vs the sea level pressure + (``sea_level_pressure``) - which you must enter ahead of time)""" + pressure = self.pressure # in Si units for hPascal + return 44330 * (1.0 - math.pow(pressure / self.sea_level_pressure, 0.1903)) + + @property + def gas(self): + """The gas resistance in ohms""" + self._perform_reading() + var1 = ((1340 + (5 * self._sw_err)) * (_LOOKUP_TABLE_1[self._gas_range])) / 65536 + var2 = ((self._adc_gas * 32768) - 16777216) + var1 + var3 = (_LOOKUP_TABLE_2[self._gas_range] * var1) / 512 + calc_gas_res = (var3 + (var2 / 2)) / var2 + return int(calc_gas_res) + + def _perform_reading(self): + """Perform a single-shot reading from the sensor and fill internal data structure for + calculations""" + expired = time.ticks_diff(self._last_reading, time.ticks_ms()) * time.ticks_diff(0, 1) + if 0 <= expired < self._min_refresh_time: + time.sleep_ms(self._min_refresh_time - expired) + + # set filter + self._write(_BME680_REG_CONFIG, [self._filter << 2]) + # turn on temp oversample & pressure oversample + self._write(_BME680_REG_CTRL_MEAS, + [(self._temp_oversample << 5)|(self._pressure_oversample << 2)]) + # turn on humidity oversample + self._write(_BME680_REG_CTRL_HUM, [self._humidity_oversample]) + # gas measurements enabled + self._write(_BME680_REG_CTRL_GAS, [_BME680_RUNGAS]) + + ctrl = self._read_byte(_BME680_REG_CTRL_MEAS) + ctrl = (ctrl & 0xFC) | 0x01 # enable single shot! + self._write(_BME680_REG_CTRL_MEAS, [ctrl]) + new_data = False + while not new_data: + data = self._read(_BME680_REG_MEAS_STATUS, 15) + new_data = data[0] & 0x80 != 0 + time.sleep(0.005) + self._last_reading = time.ticks_ms() + + self._adc_pres = _read24(data[2:5]) / 16 + self._adc_temp = _read24(data[5:8]) / 16 + self._adc_hum = struct.unpack('>H', bytes(data[8:10]))[0] + self._adc_gas = int(struct.unpack('>H', bytes(data[13:15]))[0] / 64) + self._gas_range = data[14] & 0x0F + + var1 = (self._adc_temp / 8) - (self._temp_calibration[0] * 2) + var2 = (var1 * self._temp_calibration[1]) / 2048 + var3 = ((var1 / 2) * (var1 / 2)) / 4096 + var3 = (var3 * self._temp_calibration[2] * 16) / 16384 + + self._t_fine = int(var2 + var3) + + def _read_calibration(self): + """Read & save the calibration coefficients""" + coeff = self._read(_BME680_BME680_COEFF_ADDR1, 25) + coeff += self._read(_BME680_BME680_COEFF_ADDR2, 16) + + coeff = list(struct.unpack('", line 90, in +RuntimeError: WiFi connection failed. Ensure you are using a 2.4 GHz WiFi network with WPA-2 authentication. See the additional prerequisites section from https://doi.org/10.1016/j.xpro.2023.102329 or the https://github.com/sparks-baird/self-driving-lab-demo/issues/76 for additional troubleshooting help. diff --git a/scripts/tempSensor/hivemq-com-chain.der b/scripts/tempSensor/hivemq-com-chain.der new file mode 100644 index 0000000000000000000000000000000000000000..ac22dcf9e8b888a98c10bd12f2108feb696b68c8 GIT binary patch literal 1289 zcmXqLVr4aGVtTuPnTe5!Nx*wW(B$v4^cNMgd9PsUExlmC%f_kI=F#?@mywa1mBGN@ zklTQhjX9KsO_(V(*ih3z9mL@hR`AR#NiE7tEl~(gO)f3UEU8ooN-a)JEK1H$a4*U) zEie={5C$n_7UuB`4surr%Fi!Rh%hve6X!KDF)%hXHUNVtAlDqoH8L_bwKTPi0y0r7 z6f+QkSjg*>TB2U8;F_0QR9R4B$Ya0-F`YTc(7>RHQ3=`MjI0dIO^o~uKyfanCPqev z^WjgQ9SIS7yT$$AN%e>_w*ZYES-Xz=taGn!mC)RKcWc!CU*FCx-nX7X(K{(B%7#(@ zXaw`O&i{Ra&aV${eIUX`tx0lmYk)j+Yhamh&ET#c)s`xyLPVB|J4zDrml1N z|MGPBk*(476}%FOd&HlI9NXQqqaiM3^$4$X<3;Tn^lSba?QosDo%8{^IJXBIN|d$-C@nmFsZm&_U06-A*R zHauI*{^sv;>9l$F9^Hj4g%KyMe!Y+T6z!sIQ}VojllwvTth2MW^G^FN)TkC1!u2Nc z+xM@JK6Ra#%G=At%*epFxbcTU<5vSdV7SThGcx{XVF4zAHUn7@UzG(E&>Y%qjI6Be z%uH|=qk#}eS{S69!+;G)F)=bgwD7Trv54&6bJ%_9lRYbs9bQtzBhb6Z^x=sY2J#>+ z$}AEFVhtjdTOyxTZ+rTLvABVw?!06V?FG|-3Cni}17GS&^aImqZH}WttF@h7LtUQZ?ft-QN0;vTO$hwn} zb)%+g<|d#oCT)D~X#To#ZsW7Fk5nI8DZHHEGIeod%=yEyfA=mZxVmwgh7$LsPa5ZW zo?m(9+@qo-d(dgpMb7o}y}z6&@K+Gq7)t&&=Y8J%?(W4` z&OToouy*hM$8YSC%nD{2E`QN`_f^s(p><#9T(Q;RS#@Df<-S+8F)n%243nd3uhsdh zEbwNnkITPw$X#6QV#HL#17UX*?xM+#GeNw@ieVHB%ztj0|M=yMFomo-$`pIuIoh+N3@0W84 zaeP$0wrh%Hh(yQIqkRvhW=^imek1eK&|$+FBZm9)TTuMdMC6sT9zcd^!(zYXZ`b3QaIzI*a>1kPyVS=_UK!7wXx`<#;@3Ozu)>5_fOHg k Accessories -> Terminal +on your Raspberry Pi desktop, as illustrated below: + +![Finding the terminal](http://get.pimoroni.com/resources/github-repo-terminal.png) + +In the new terminal window type the command exactly as it appears below (check for typos) and follow the on-screen instructions: + +```bash +git clone https://github.com/pimoroni/bme680-python +cd bme680-python +./install.sh +``` + +**Note** Libraries will be installed in the "pimoroni" virtual environment, you will need to activate it to run examples: + +``` +source ~/.virtualenvs/pimoroni/bin/activate +``` + +### Development: + +If you want to contribute, or like living on the edge of your seat by having the latest code, you can install the development version like so: + +```bash +git clone https://github.com/pimoroni/bme680-python +cd bme680-python +./install.sh --unstable +``` + +In all cases you will have to enable the i2c bus: + +``` +sudo raspi-config nonint do_i2c 0 +``` + +## Documentation & Support + +* Guides and tutorials - https://learn.pimoroni.com/bme680-breakout +* Get help - http://forums.pimoroni.com/c/support + diff --git a/scripts/tempSensor/lib/adafruit_blinka-8.49.0.dist-info/METADATA b/scripts/tempSensor/lib/adafruit_blinka-8.49.0.dist-info/METADATA new file mode 100644 index 00000000..8854faa0 --- /dev/null +++ b/scripts/tempSensor/lib/adafruit_blinka-8.49.0.dist-info/METADATA @@ -0,0 +1,8 @@ +Metadata-Version: 2.1 +Name: adafruit-blinka +Version: 8.49.0 +Summary: Dummy package for satisfying formal requirements +Home-page: ? +Author: ? + +? diff --git a/scripts/tempSensor/lib/adafruit_blinka-8.49.0.dist-info/RECORD b/scripts/tempSensor/lib/adafruit_blinka-8.49.0.dist-info/RECORD new file mode 100644 index 00000000..a105ee1c --- /dev/null +++ b/scripts/tempSensor/lib/adafruit_blinka-8.49.0.dist-info/RECORD @@ -0,0 +1,2 @@ +adafruit_blinka-8.49.0.dist-info/METADATA,, +adafruit_blinka-8.49.0.dist-info/RECORD,, \ No newline at end of file diff --git a/scripts/tempSensor/lib/as7341.py b/scripts/tempSensor/lib/as7341.py new file mode 100644 index 00000000..26dc8f89 --- /dev/null +++ b/scripts/tempSensor/lib/as7341.py @@ -0,0 +1,608 @@ +""" +This file licensed under the MIT License and incorporates work covered by +the following copyright and permission notice: + +The MIT License (MIT) + +Copyright (c) 2022-2022 Rob Hamerling + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +Rob Hamerling, Version 0.0, August 2022 + + Original by WaveShare for Raspberry Pi, part of: + https://www.waveshare.com/w/upload/b/b3/AS7341_Spectral_Color_Sensor_code.7z + + Converted to Micropython for use with MicroPython devices such as ESP32 + - pythonized (in stead of 'literal' translation of C code) + - instance of AS7341 requires specification of I2C interface + - added I2C read/write error detection + - added check for connected AS7341 incl. device ID + - some code optimization (esp. adding I2C word/block reads/writes) + - Replaced bit addressing like (1<<5) by symbolic name with bit mask + - moved SMUX settings for predefined channel mappings to a dictionary + and as a separate file to allow changes or additional configurations + by the user without changing the driver + - several changes of names of functions and constants + (incl. camel case -> word separators with underscores) + - added comments, doc-strings with explanation and/or argumentation + - several other improvements and some corrections + + Remarks: + - Automatic Gain Control (AGC) is not supported + - No provisions for SYND mode + +""" + +from time import sleep_ms + +from as7341_smux_select import * # predefined SMUX configurations + +AS7341_I2C_ADDRESS = const(0x39) # I2C address of AS7341 +AS7341_ID_VALUE = const(0x24) # AS7341 Part Number Identification +# (excl 2 low order bits) + +# Symbolic names for registers and some selected bit fields +# Note: ASTATUS, ITIME and CHx_DATA in address range 0x60--0x6F are not used +AS7341_CONFIG = const(0x70) +AS7341_CONFIG_INT_MODE_SPM = const(0x00) +AS7341_MODE_SPM = AS7341_CONFIG_INT_MODE_SPM # alias +AS7341_CONFIG_INT_MODE_SYNS = const(0x01) +AS7341_MODE_SYNS = AS7341_CONFIG_INT_MODE_SYNS # alias +AS7341_CONFIG_INT_MODE_SYND = const(0x03) +AS7341_MODE_SYND = AS7341_CONFIG_INT_MODE_SYND # alias +AS7341_CONFIG_INT_SEL = const(0x04) +AS7341_CONFIG_LED_SEL = const(0x08) +AS7341_STAT = const(0x71) +AS7341_STAT_READY = const(0x01) +AS7341_STAT_WAIT_SYNC = const(0x02) +AS7341_EDGE = const(0x72) +AS7341_GPIO = const(0x73) +AS7341_GPIO_PD_INT = const(0x01) +AS7341_GPIO_PD_GPIO = const(0x02) +AS7341_LED = const(0x74) +AS7341_LED_LED_ACT = const(0x80) +AS7341_ENABLE = const(0x80) +AS7341_ENABLE_PON = const(0x01) +AS7341_ENABLE_SP_EN = const(0x02) +AS7341_ENABLE_WEN = const(0x08) +AS7341_ENABLE_SMUXEN = const(0x10) +AS7341_ENABLE_FDEN = const(0x40) +AS7341_ATIME = const(0x81) +AS7341_WTIME = const(0x83) +AS7341_SP_TH_LOW = const(0x84) +AS7341_SP_TH_L_LSB = const(0x84) +AS7341_SP_TH_L_MSB = const(0x85) +AS7341_SP_TH_HIGH = const(0x86) +AS7341_SP_TH_H_LSB = const(0x86) +AS7341_SP_TH_H_MSB = const(0x87) +AS7341_AUXID = const(0x90) +AS7341_REVID = const(0x91) +AS7341_ID = const(0x92) +AS7341_STATUS = const(0x93) +AS7341_STATUS_ASAT = const(0x80) +AS7341_STATUS_AINT = const(0x08) +AS7341_STATUS_FINT = const(0x04) +AS7341_STATUS_C_INT = const(0x02) +AS7341_STATUS_SINT = const(0x01) +AS7341_ASTATUS = const(0x94) # start of bulk read (incl 6 counts) +AS7341_ASTATUS_ASAT_STATUS = const(0x80) +AS7341_ASTATUS_AGAIN_STATUS = const(0x0F) +AS7341_CH_DATA = const(0x95) # start of the 6 channel counts +AS7341_CH0_DATA_L = const(0x95) +AS7341_CH0_DATA_H = const(0x96) +AS7341_CH1_DATA_L = const(0x97) +AS7341_CH1_DATA_H = const(0x98) +AS7341_CH2_DATA_L = const(0x99) +AS7341_CH2_DATA_H = const(0x9A) +AS7341_CH3_DATA_L = const(0x9B) +AS7341_CH3_DATA_H = const(0x9C) +AS7341_CH4_DATA_L = const(0x9D) +AS7341_CH4_DATA_H = const(0x9E) +AS7341_CH5_DATA_L = const(0x9F) +AS7341_CH5_DATA_H = const(0xA0) +AS7341_STATUS_2 = const(0xA3) +AS7341_STATUS_2_AVALID = const(0x40) +AS7341_STATUS_3 = const(0xA4) +AS7341_STATUS_5 = const(0xA6) +AS7341_STATUS_6 = const(0xA7) +AS7341_CFG_0 = const(0xA9) +AS7341_CFG_0_WLONG = const(0x04) +AS7341_CFG_0_REG_BANK = const(0x10) # datasheet fig 82 (! fig 32) +AS7341_CFG_0_LOW_POWER = const(0x20) +AS7341_CFG_1 = const(0xAA) +AS7341_CFG_3 = const(0xAC) +AS7341_CFG_6 = const(0xAF) +AS7341_CFG_6_SMUX_CMD_ROM = const(0x00) +AS7341_CFG_6_SMUX_CMD_READ = const(0x08) +AS7341_CFG_6_SMUX_CMD_WRITE = const(0x10) +AS7341_CFG_8 = const(0xB1) +AS7341_CFG_9 = const(0xB2) +AS7341_CFG_10 = const(0xB3) +AS7341_CFG_12 = const(0xB5) +AS7341_PERS = const(0xBD) +AS7341_GPIO_2 = const(0xBE) +AS7341_GPIO_2_GPIO_IN = const(0x01) +AS7341_GPIO_2_GPIO_OUT = const(0x02) +AS7341_GPIO_2_GPIO_IN_EN = const(0x04) +AS7341_GPIO_2_GPIO_INV = const(0x08) +AS7341_ASTEP = const(0xCA) +AS7341_ASTEP_L = const(0xCA) +AS7341_ASTEP_H = const(0xCB) +AS7341_AGC_GAIN_MAX = const(0xCF) +AS7341_AZ_CONFIG = const(0xD6) +AS7341_FD_TIME_1 = const(0xD8) +AS7341_FD_TIME_2 = const(0xDA) +AS7341_FD_CFG0 = const(0xD7) +AS7341_FD_STATUS = const(0xDB) +AS7341_FD_STATUS_FD_100HZ = const(0x01) +AS7341_FD_STATUS_FD_120HZ = const(0x02) +AS7341_FD_STATUS_FD_100_VALID = const(0x04) +AS7341_FD_STATUS_FD_120_VALID = const(0x08) +AS7341_FD_STATUS_FD_SAT_DETECT = const(0x10) +AS7341_FD_STATUS_FD_MEAS_VALID = const(0x20) +AS7341_INTENAB = const(0xF9) +AS7341_INTENAB_SP_IEN = const(0x08) +AS7341_CONTROL = const(0xFA) +AS7341_FIFO_MAP = const(0xFC) +AS7341_FIFO_LVL = const(0xFD) +AS7341_FDATA = const(0xFE) +AS7341_FDATA_L = const(0xFE) +AS7341_FDATA_H = const(0xFF) + + +class AS7341: + """Class for AS7341: 11 Channel Multi-Spectral Digital Sensor""" + + def __init__(self, i2c, addr=AS7341_I2C_ADDRESS): + """specification of active I2C object is mandatory + specification of I2C address of AS7341 is optional + """ + self.__bus = i2c + self.__address = addr + self.__buffer1 = bytearray(1) # I2C I/O buffer for byte + self.__buffer2 = bytearray(2) # I2C I/O buffer for word + self.__buffer13 = bytearray(13) # I2C I/O buffer ASTATUS + 6 counts + self.__measuremode = AS7341_MODE_SPM # default measurement mode + self.__connected = self.reset() # recycle power, check AS7341 presence + + """ --------- 'private' functions ----------- """ + + def __read_byte(self, reg): + """read byte, return byte (integer) value""" + try: + self.__bus.readfrom_mem_into(self.__address, reg, self.__buffer1) + return self.__buffer1[0] # return integer value + except Exception as err: + print("I2C read_byte at 0x{:02X}, error".format(reg), err) + return -1 # indication 'no receive' + + def __read_word(self, reg): + """read 2 consecutive bytes, return integer value (little Endian)""" + try: + self.__bus.readfrom_mem_into(self.__address, reg, self.__buffer2) + return int.from_bytes(self.__buffer2, "little") # return word value + except Exception as err: + print("I2C read_word at 0x{:02X}, error".format(reg), err) + return -1 # indication 'no receive' + + def __read_all_channels(self): + """read ASTATUS register and all channels, return list of 6 integer values""" + try: + self.__bus.readfrom_mem_into( + self.__address, AS7341_ASTATUS, self.__buffer13 + ) + return [ + int.from_bytes(self.__buffer13[1 + 2 * i : 3 + 2 * i], "little") + for i in range(6) + ] + except Exception as err: + print( + "I2C read_all_channels at 0x{:02X}, error".format(AS7341_ASTATUS), err + ) + return [] # empty list + + def __write_byte(self, reg, value): + """write a single byte to the specified register""" + self.__buffer1[0] = value & 0xFF + try: + self.__bus.writeto_mem(self.__address, reg, self.__buffer1) + sleep_ms(10) + except Exception as err: + print("I2C write_byte at 0x{:02X}, error".format(reg), err) + return False + return True + + def __write_word(self, reg, value): + """write a word as 2 bytes (little endian encoding) + to adresses + 0 and + 1 + """ + self.__buffer2[0] = value & 0xFF # low byte + self.__buffer2[1] = (value >> 8) & 0xFF # high byte + try: + self.__bus.writeto_mem(self.__address, reg, self.__buffer2) + sleep_ms(20) + except Exception as err: + print("I2C write_word at 0x{:02X}, error".format(reg), err) + return False + return True + + def __write_burst(self, reg, value): + """write an array of bytes to consucutive addresses starting """ + try: + self.__bus.writeto_mem(self.__address, reg, value) + sleep_ms(100) + except Exception as err: + print("I2C write_burst at 0x{:02X}, error".format(reg), err) + return False + return True + + def __modify_reg(self, reg, mask, flag=True): + """modify register with + True means 'or' with : set the bit(s) + False means 'and' with inverted : reset the bit(s) + Notes: 1. Works only with '1' bits in + (in most cases contains a single 1-bit!) + 2. When is in region 0x60-0x74 + bank 1 is supposed be set by caller + """ + data = self.__read_byte(reg) # read + if flag: + data |= mask + else: + data &= ~mask + self.__write_byte(reg, data) # rewrite + + def __set_bank(self, bank=1): + """select registerbank + 1 for access to regs 0x60-0x74 + 0 for access to regs 0x80-0xFF + Note: It seems that reg CFG_0 (0x93) is accessible + even when REG_BANK bit is set for 0x60-0x74, + otherwise it wouldn't be possible to reset REG_BANK + Datasheet isn't clear about this. + """ + if bank in (0, 1): + self.__modify_reg(AS7341_CFG_0, AS7341_CFG_0_REG_BANK, bank == 1) + + """ ----------- 'public' functions ----------- """ + + def enable(self): + """enable device (only power on)""" + self.__write_byte(AS7341_ENABLE, AS7341_ENABLE_PON) + + def disable(self): + """disable all functions and power off""" + self.__set_bank(1) # CONFIG register is in bank 1 + self.__write_byte(AS7341_CONFIG, 0x00) # INT, LED off, SPM mode + self.__set_bank(0) + self.__write_byte(AS7341_ENABLE, 0x00) # power off + + def isconnected(self): + """determine if AS7341 is successfully initialized (True/False)""" + return self.__connected + + def reset(self): + """Cycle power and check if AS7341 is (re-)connected + When connected set (restore) measurement mode + """ + self.disable() # power-off ('reset') + sleep_ms(50) # quisce + self.enable() # (only) power-on + sleep_ms(50) # settle + id = self.__read_byte(AS7341_ID) # obtain Part Number ID + if id < 0: # read error + print( + "Failed to contact AS7341 at I2C address 0x{:02X}".format( + self.__address + ) + ) + return False + else: + if not (id & (~0x03)) == AS7341_ID_VALUE: # ID in bits 7..2 bits + print( + "No AS7341: ID = 0x{:02X}, expected 0x{:02X}".format( + id, AS7341_ID_VALUE + ) + ) + return False + self.set_measure_mode(self.__measuremode) # configure chip + return True + + def measurement_completed(self): + """check if measurement completed (return True) or otherwise return False""" + return bool(self.__read_byte(AS7341_STATUS_2) & AS7341_STATUS_2_AVALID) + + def set_spectral_measurement(self, flag=True): + """enable (flag == True) spectral measurement or otherwise disable it""" + self.__modify_reg(AS7341_ENABLE, AS7341_ENABLE_SP_EN, flag) + + def set_smux(self, flag=True): + """enable (flag == True) SMUX or otherwise disable it""" + self.__modify_reg(AS7341_ENABLE, AS7341_ENABLE_SMUXEN, flag) + + def set_measure_mode(self, mode=AS7341_CONFIG_INT_MODE_SPM): + """configure the AS7341 for a specific measurement mode + when interrupt needed it must be configured separately + """ + if mode in ( + AS7341_CONFIG_INT_MODE_SPM, # meas. started by SP_EN + AS7341_CONFIG_INT_MODE_SYNS, # meas. started by GPIO + AS7341_CONFIG_INT_MODE_SYND, + ): # meas. started by GPIO + EDGE + self.__measuremode = mode # store new measurement mode + self.__set_bank(1) # CONFIG register is in bank 1 + data = self.__read_byte(AS7341_CONFIG) & (~3) # discard 2 LSbs (mode) + data |= mode # insert new mode + self.__write_byte(AS7341_CONFIG, data) # modify measurement mode + self.__set_bank(0) + + def channel_select(self, selection): + """select one from a series of predefined SMUX configurations + should be a key in dictionary AS7341_SMUX_SELECT + 20 bytes of memory starting from address 0 will be overwritten. + """ + if selection in AS7341_SMUX_SELECT: + self.__write_burst(0x00, AS7341_SMUX_SELECT[selection]) + else: + print(selection, "is unknown in AS7341_SMUX_SELECT") + + def start_measure(self, selection): + """select SMUX configuration, prepare and start measurement""" + self.__modify_reg(AS7341_CFG_0, AS7341_CFG_0_LOW_POWER, False) # no low power + self.set_spectral_measurement(False) # quiesce + self.__write_byte(AS7341_CFG_6, AS7341_CFG_6_SMUX_CMD_WRITE) # write mode + if self.__measuremode == AS7341_CONFIG_INT_MODE_SPM: + self.channel_select(selection) + self.set_smux(True) + elif self.__measuremode == AS7341_CONFIG_INT_MODE_SYNS: + self.channel_select(selection) + self.set_smux(True) + self.set_gpio_mode(AS7341_GPIO_2_GPIO_IN_EN) + self.set_spectral_measurement(True) + if self.__measuremode == AS7341_CONFIG_INT_MODE_SPM: + while not self.measurement_completed(): + sleep_ms(50) + + def get_channel_data(self, channel): + """read count of a single channel (channel in range 0..5) + with or without measurement, just read count of one channel + contents depend on previous selection with 'start_measure' + auto-zero feature may result in value 0! + """ + data = 0 # default + if 0 <= channel <= 5: + data = self.__read_word(AS7341_CH_DATA + channel * 2) + return data # return integer value + + def get_spectral_data(self): + """obtain counts of all channels + return a tuple of 6 counts (integers) of the channels + contents depend on previous selection with 'start_measure' + """ + return self.__read_all_channels() # return a tuple! + + def set_flicker_detection(self, flag=True): + """enable (flag == True) flicker detection or otherwise disable it""" + self.__modify_reg(AS7341_ENABLE, AS7341_ENABLE_FDEN, flag) + + def get_flicker_frequency(self): + """Determine flicker frequency in Hz. Returns 100, 120 or 0 + Integration time and gain for flicker detection is the same as for + other channels, the dedicated FD_TIME and FD_GAIN are not supported + """ + self.__modify_reg(AS7341_CFG_0, AS7341_CFG_0_LOW_POWER, False) # no low power + self.set_spectral_measurement(False) + self.__write_byte(AS7341_CFG_6, AS7341_CFG_6_SMUX_CMD_WRITE) + self.channel_select("FD") # select flicker detection only + self.set_smux(True) + self.set_spectral_measurement(True) + self.set_flicker_detection(True) + for _ in range(10): # limited wait for completion + fd_status = self.__read_byte(AS7341_FD_STATUS) + if fd_status & AS7341_FD_STATUS_FD_MEAS_VALID: + break + # print("Flicker measurement not completed") + sleep_ms(100) + else: # timeout + print("Flicker measurement timed out") + return 0 + for _ in range(10): # limited wait for calculation + fd_status = self.__read_byte(AS7341_FD_STATUS) + if (fd_status & AS7341_FD_STATUS_FD_100_VALID) or ( + fd_status & AS7341_FD_STATUS_FD_120_VALID + ): + break + # print("Flicker calculation not completed") + sleep_ms(100) + else: # timeout + print("Flicker frequency calculation timed out") + return 0 + # print("FD_STATUS", "0x{:02X}".format(fd_status)) + self.set_flicker_detection(False) # disable + self.__write_byte(AS7341_FD_STATUS, 0x3C) # clear all FD STATUS bits + if (fd_status & AS7341_FD_STATUS_FD_100_VALID) and ( + fd_status & AS7341_FD_STATUS_FD_100HZ + ): + return 100 + elif (fd_status & AS7341_FD_STATUS_FD_120_VALID) and ( + fd_status & AS7341_FD_STATUS_FD_120HZ + ): + return 120 + return 0 + + def set_gpio_mode(self, mode): + """Configure mode of GPIO pin. + Allow only input-enable or output (with or without inverted) + specify 0x00 to reset the mode of the GPIO pin. + Notes: 1. It seems that GPIO_INV bit must be set + together with GPIO_IN_EN. + Proof: Use a pull-up resistor between GPIO and 3.3V: + - when program is ot started GPIO is high + - when program is started (GPIO_IN_EN=1) GPIO becomes low + - when also GPIO_INV=1 GPIO behaves normally + Maybe it is a quirk of the used test-board. + 2. GPIO output is not tested + (dataset lacks info how to set/reset GPIO) + """ + if mode in ( + 0x00, + AS7341_GPIO_2_GPIO_OUT, + AS7341_GPIO_2_GPIO_OUT | AS7341_GPIO_2_GPIO_INV, + AS7341_GPIO_2_GPIO_IN_EN, + AS7341_GPIO_2_GPIO_IN_EN | AS7341_GPIO_2_GPIO_INV, + ): + if mode == AS7341_GPIO_2_GPIO_IN_EN: # input mode + mode |= AS7341_GPIO_2_GPIO_INV # add 'inverted' + self.__write_byte(AS7341_GPIO_2, mode) + + def get_gpio_value(self): + """Determine GPIO value (when GPIO enabled for IN_EN) + returns 0 (low voltage) or 1 (high voltage) + """ + # print("GPIO_2 = 0x{:02X}".format(self.__read_byte(AS7341_GPIO_2))) + return self.__read_byte(AS7341_GPIO_2) & AS7341_GPIO_2_GPIO_IN + + def set_astep(self, value): + """set ASTEP size (range 0..65534 -> 2.78 usec .. 182 msec)""" + if 0 <= value <= 65534: + self.__write_word(AS7341_ASTEP, value) + + def set_atime(self, value): + """set number of integration steps (range 0..255 -> 1..256 ASTEPs)""" + self.__write_byte(AS7341_ATIME, value) + + def get_integration_time(self): + """return actual total integration time (atime * astep) + in milliseconds (valid with SPM and SYNS measurement mode) + """ + return ( + (self.__read_word(AS7341_ASTEP) + 1) + * (self.__read_byte(AS7341_ATIME) + 1) + * 2.78 + / 1000 + ) + + def set_again(self, code): + """set AGAIN (code in range 0..10 -> gain factor 0.5 .. 512) + value 0 1 2 3 4 5 6 7 8 9 10 + gain: *0.5 | *1 | *2 | *4 | *8 | *16 | *32 | *64 | *128 | *256 | *512 + """ + if 0 <= code <= 10: + self.__write_byte(AS7341_CFG_1, code) + + def get_again(self): + """obtain actual gain code (in range 0 .. 10)""" + return self.__read_byte(AS7341_CFG_1) + + def set_again_factor(self, factor): + """'inverse' of 'set_again': gain factor -> code 0 .. 10 + is rounded down to nearest power of 2 (in range 0.5 .. 512) + """ + code = 10 + gain = 512 + while gain > factor and code > 0: + gain /= 2 + code -= 1 + # print("factor", factor, "gain", gain, "code", code) + self.__write_byte(AS7341_CFG_1, code) + + def get_again_factor(self): + """obtain actual gain factor (in range 0.5 .. 512)""" + return 2 ** (self.__read_byte(AS7341_CFG_1) - 1) + + def set_wen(self, flag=True): + """enable (flag=True) or otherwise disable use of WTIME (auto re-start)""" + self.__modify_reg(AS7341_ENABLE, AS7341_ENABLE_WEN, flag) + + def set_wtime(self, wtime): + """set WTIME when auto-re-start is desired (in range 0 .. 0xFF) + 0 -> 2.78ms, 0xFF -> 711.7 ms + Note: The WEN bit in ENABLE should be set as well: set_wen() + """ + self.__write_byte(AS7341_WTIME, wtime) + + def set_led_current(self, current): + """Control current of onboard LED in milliamperes + LED-current is (here) limited to the range 4..20 mA + use only even numbers (4,6,8,... etc) + Specification outside this range results in LED OFF + """ + self.__set_bank(1) # CONFIG and LED registers in bank 1 + if 4 <= current <= 20: # within limits: 4..20 mA + self.__modify_reg(AS7341_CONFIG, AS7341_CONFIG_LED_SEL, True) + # print("Reg. CONFIG (0x70) now 0x{:02X}".format(self.__read_byte(0x70))) + data = AS7341_LED_LED_ACT + ((current - 4) // 2) # LED on with PWM + else: + self.__modify_reg(AS7341_CONFIG, AS7341_CONFIG_LED_SEL, False) + data = 0 # LED off, PWM 0 + self.__write_byte(AS7341_LED, data) + # print("reg 0x74 (LED) now 0x{:02X}".format(self.__read_byte(0x74))) + self.__set_bank(0) + sleep_ms(100) + + def check_interrupt(self): + """Check for Spectral or Flicker Detect saturation interrupt""" + data = self.__read_byte(AS7341_STATUS) + if data & AS7341_STATUS_ASAT: + print("Spectral interrupt generation!") + return True + return False + + def clear_interrupt(self): + """clear all interrupt signals""" + self.__write_byte(AS7341_STATUS, 0xFF) + + def set_spectral_interrupt(self, flag=True): + """enable (flag == True) or otherwise disable spectral interrupts""" + self.__modify_reg(AS7341_INTENAB, AS7341_INTENAB_SP_IEN, flag) + + def set_interrupt_persistence(self, value): + """configure interrupt persistance""" + if 0 <= value <= 15: + self.__write_byte(AS7341_PERS, value) + + def set_spectral_threshold_channel(self, value): + """select channel (0..4) for interrupts, persistence and AGC""" + if 0 <= value <= 4: + self.__write_byte(AS7341_CFG_12, value) + + def set_thresholds(self, lo, hi): + """Set thresholds (when lo < hi)""" + if lo < hi: + self.__write_word(AS7341_SP_TH_LOW, lo) + self.__write_word(AS7341_SP_TH_HIGH, hi) + sleep_ms(20) + + def get_thresholds(self): + """obtain and return tuple with low and high threshold values""" + lo = self.__read_word(AS7341_SP_TH_LOW) + hi = self.__read_word(AS7341_SP_TH_HIGH) + return (lo, hi) + + def set_syns_int(self): + """select SYNS mode and signal SYNS interrupt on Pin INT""" + self.__set_bank(1) # CONFIG register is in bank 1 + self.__write_byte( + AS7341_CONFIG, AS7341_CONFIG_INT_SEL | AS7341_CONFIG_INT_MODE_SYNS + ) + self.__set_bank(0) + + +# diff --git a/scripts/tempSensor/lib/as7341_sensor.py b/scripts/tempSensor/lib/as7341_sensor.py new file mode 100644 index 00000000..c9ef9473 --- /dev/null +++ b/scripts/tempSensor/lib/as7341_sensor.py @@ -0,0 +1,149 @@ +"""Sterling Baird: wrapper class for AS7341 sensor.""" + +from math import log + +from as7341 import AS7341, AS7341_MODE_SPM +from machine import I2C, Pin + + +class ExternalDeviceNotFound(OSError): + pass + + +class Sensor: + def __init__( + self, atime=100, astep=999, gain=8, i2c=I2C(1, scl=Pin(27), sda=Pin(26)) + ): + """Wrapper for Rob Hamerling's AS7341 implementation. + + Mimics the original CircuitPython class a bit more, specific to the needs of + SDL-Demo. + + Rob Hamerling's implementation: + - https://gitlab.com/robhamerling/micropython-as7341 + + Original Circuit Python repo: + - https://github.com/adafruit/Adafruit_CircuitPython_AS7341 + + Parameters + ---------- + atime : int, optional + The integration time step size in 2.78 microsecond increments, by default 100 + astep : int, optional + The integration time step count. Total integration time will be (ATIME + 1) + * (ASTEP + 1) * 2.78µS, by default 999, meaning 281 ms assuming atime=100 + gain : int, optional + The ADC gain multiplier, by default 128 + i2c : I2C, optional + The I2C bus, by default machine.I2C(1, scl=machine.Pin(27), + sda=machine.Pin(26)) + + Raises + ------ + ExternalDeviceNotFound + Couldn't connect to AS7341. + + Examples + -------- + >>> sensor = Sensor(atime=29, astep=599, again=4) + >>> channel_data = sensor.all_channels + """ + + # i2c = machine.SoftI2C(scl=Pin(27), sda=Pin(26)) + self.i2c = i2c + addrlist = " ".join(["0x{:02X}".format(x) for x in i2c.scan()]) # type: ignore + print("Detected devices at I2C-addresses:", addrlist) + + sensor = AS7341(i2c) + + if not sensor.isconnected(): + raise ExternalDeviceNotFound("Failed to contact AS7341, terminating") + + sensor.set_measure_mode(AS7341_MODE_SPM) + + sensor.set_atime(atime) + sensor.set_astep(astep) + sensor.set_again(gain) + + self.sensor = sensor + + self.__atime = atime + self.__astep = astep + self.__gain = gain + + @property + def _atime(self): + return self.__atime + + @_atime.setter + def _atime(self, value): + self.__atime = value + self.sensor.set_atime(value) + + @property + def _astep(self): + return self.__astep + + @_astep.setter + def _astep(self, value): + self.__atime = value + self.sensor.set_astep(value) + + @property + def _gain(self): + return self.__gain + + @_gain.setter + def _gain(self, gain): + """set AGAIN (code in range 0..10 -> gain factor 0.5 .. 512) + gain: *0.5 | *1 | *2 | *4 | *8 | *16 | *32 | *64 | *128 | *256 | *512 + code 0 1 2 3 4 5 6 7 8 9 10 + """ + self.__gain = gain + # gain == 0.5 * 2 ** code --> code == 1.4427 Ln[2 * gain] (via Mathematica) + code = int(round(1.4427 * log(2 * gain))) + self.sensor.set_again(code) + + @property + def all_channels(self): + self.sensor.start_measure("F1F4CN") + f1, f2, f3, f4, clr, nir = self.sensor.get_spectral_data() + + self.sensor.start_measure("F5F8CN") + f5, f6, f7, f8, clr, nir = self.sensor.get_spectral_data() + + clr, nir # to ignore "unused" linting warnings + + return [f1, f2, f3, f4, f5, f6, f7, f8] + + @property + def all_channels_clr_nir(self): + self.sensor.start_measure("F1F4CN") + f1, f2, f3, f4, clr, nir = self.sensor.get_spectral_data() + + self.sensor.start_measure("F5F8CN") + f5, f6, f7, f8, clr, nir = self.sensor.get_spectral_data() + + clr, nir # to ignore "unused" linting warnings + + return [f1, f2, f3, f4, f5, f6, f7, f8, clr, nir] + + def disable(self): + self.sensor.disable() + + +# %% Code Graveyard +# gain_to_code_lookup = { +# 0.5: 1, +# 1: 1, +# 2: 2, +# 4: 3, +# 8: 4, +# 16: 5, +# 32: 6, +# 64: 7, +# 128: 8, +# 256: 9, +# 512: 10, +# } +# code = gain_to_code_lookup[gain] diff --git a/scripts/tempSensor/lib/as7341_smux_select.py b/scripts/tempSensor/lib/as7341_smux_select.py new file mode 100644 index 00000000..edea2f40 --- /dev/null +++ b/scripts/tempSensor/lib/as7341_smux_select.py @@ -0,0 +1,49 @@ +""" +This file licensed under the MIT License and incorporates work covered by +the following copyright and permission notice: + +The MIT License (MIT) + +Copyright (c) 2022-2022 Rob Hamerling + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +""" Dictionary with specific SMUX configurations for AS7341 + See AMS Application Note AS7341_AN000666_1.01.pdf + for detailed instructions how to configure the channel mapping. + The Application Note can be found in one of the evaluation packages, e.g. + AS7341_EvalSW_Reflection_v1-26-3/Documents/application notes/SMUX/ + + This file should be imported by AS7341.py with: + from as7341_smux_select import * +""" +AS7341_SMUX_SELECT = { + # F1 through F4, CLEAR, NIR: + "F1F4CN": b"\x30\x01\x00\x00\x00\x42\x00\x00\x50\x00\x00\x00\x20\x04\x00\x30\x01\x50\x00\x06", + # F5 through F8, CLEAR, NIR: + "F5F8CN": b"\x00\x00\x00\x40\x02\x00\x10\x03\x50\x10\x03\x00\x00\x00\x24\x00\x00\x50\x00\x06", + # F2 through F7: + "F2F7": b"\x20\x00\x00\x00\x05\x31\x40\x06\x00\x40\x06\x00\x10\x03\x50\x20\x00\x00\x00\x00", + # Flicker Detection only: + "FD": b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x60", +} + +# diff --git a/scripts/tempSensor/lib/bme680-2.0.0.dist-info/METADATA b/scripts/tempSensor/lib/bme680-2.0.0.dist-info/METADATA new file mode 100644 index 00000000..52ebdfd8 --- /dev/null +++ b/scripts/tempSensor/lib/bme680-2.0.0.dist-info/METADATA @@ -0,0 +1,156 @@ +Metadata-Version: 2.3 +Name: bme680 +Version: 2.0.0 +Summary: Python library for the BME680 temperature, humidity and gas sensor +Project-URL: GitHub, https://www.github.com/pimoroni/bme680-python +Project-URL: Homepage, https://www.pimoroni.com +Author-email: Philip Howard +Maintainer-email: Philip Howard +License: MIT License + + Copyright (c) 2018 Pimoroni Ltd + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +License-File: LICENSE +Keywords: Pi,Raspberry +Classifier: Development Status :: 4 - Beta +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: POSIX :: Linux +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Topic :: Software Development +Classifier: Topic :: Software Development :: Libraries +Classifier: Topic :: System :: Hardware +Requires-Python: >=3.7 +Requires-Dist: smbus2 +Description-Content-Type: text/markdown + +# BME680 + +[![Build Status](https://img.shields.io/github/actions/workflow/status/pimoroni/bme680-python/test.yml?branch=main)](https://github.com/pimoroni/bme680-python/actions/workflows/test.yml) +[![Coverage Status](https://coveralls.io/repos/github/pimoroni/bme680-python/badge.svg?branch=main)](https://coveralls.io/github/pimoroni/bme680-python?branch=main) +[![PyPi Package](https://img.shields.io/pypi/v/bme680.svg)](https://pypi.python.org/pypi/bme680) +[![Python Versions](https://img.shields.io/pypi/pyversions/bme680.svg)](https://pypi.python.org/pypi/bme680) + +https://shop.pimoroni.com/products/bme680 + +The state-of-the-art BME680 breakout lets you measure temperature, pressure, humidity, and indoor air quality. + +## Installing + +### Full install (recommended): + +We've created an easy installation script that will install all pre-requisites and get your BME680 +up and running with minimal efforts. To run it, fire up Terminal which you'll find in Menu -> Accessories -> Terminal +on your Raspberry Pi desktop, as illustrated below: + +![Finding the terminal](http://get.pimoroni.com/resources/github-repo-terminal.png) + +In the new terminal window type the command exactly as it appears below (check for typos) and follow the on-screen instructions: + +```bash +git clone https://github.com/pimoroni/bme680-python +cd bme680-python +./install.sh +``` + +**Note** Libraries will be installed in the "pimoroni" virtual environment, you will need to activate it to run examples: + +``` +source ~/.virtualenvs/pimoroni/bin/activate +``` + +### Development: + +If you want to contribute, or like living on the edge of your seat by having the latest code, you can install the development version like so: + +```bash +git clone https://github.com/pimoroni/bme680-python +cd bme680-python +./install.sh --unstable +``` + +In all cases you will have to enable the i2c bus: + +``` +sudo raspi-config nonint do_i2c 0 +``` + +## Documentation & Support + +* Guides and tutorials - https://learn.pimoroni.com/bme680-breakout +* Get help - http://forums.pimoroni.com/c/support + + +2.0.0 +----- + +* Repackage to hatch/pyproject.toml +* Drop Python 2.7 support +* Switch from smbu2 to smbus2 + +1.1.1 +----- + +* New: constants to clarify heater on/off states + +1.1.0 +----- + +* New: support for BME688 "high" gas resistance variant +* New: set/get gas heater disable bit +* Enhancement: fail with descriptive RuntimeError when chip is not detected + +1.0.5 +----- + +* New: set_temp_offset to calibrate temperature offset in degrees C + +1.0.4 +----- + +* Fix to range_sw_err for extremely high gas readings +* Convert to unsigned int to fix negative gas readings + +1.0.3 +----- + +* Merged temperature compensation fix from Bosch's BME680_driver 3.5.3 + +1.0.2 +----- + +* Fixed set_gas_heater_temperature to avoid i2c TypeError + +1.0.1 +----- + +* Added Manifest to Python package + +1.0.0 +----- + +* Initial release + diff --git a/scripts/tempSensor/lib/bme680-2.0.0.dist-info/RECORD b/scripts/tempSensor/lib/bme680-2.0.0.dist-info/RECORD new file mode 100644 index 00000000..35d5cc6b --- /dev/null +++ b/scripts/tempSensor/lib/bme680-2.0.0.dist-info/RECORD @@ -0,0 +1,7 @@ +CHANGELOG.md,, +LICENSE,, +README.md,, +bme680-2.0.0.dist-info/METADATA,, +bme680/__init__.py,, +bme680/constants.py,, +bme680-2.0.0.dist-info/RECORD,, \ No newline at end of file diff --git a/scripts/tempSensor/lib/bme680/__init__.py b/scripts/tempSensor/lib/bme680/__init__.py new file mode 100644 index 00000000..56d547a1 --- /dev/null +++ b/scripts/tempSensor/lib/bme680/__init__.py @@ -0,0 +1,486 @@ +"""BME680 Temperature, Pressure, Humidity & Gas Sensor.""" +import math +import time + +from . import constants +from .constants import BME680Data, lookupTable1, lookupTable2 + +__version__ = '2.0.0' + + +# Export constants to global namespace +# so end-users can "from BME680 import NAME" +if hasattr(constants, '__dict__'): + for key in constants.__dict__: + value = constants.__dict__[key] + if key not in globals(): + globals()[key] = value + + +class BME680(BME680Data): + """BOSCH BME680. + + Gas, pressure, temperature and humidity sensor. + + :param i2c_addr: One of I2C_ADDR_PRIMARY (0x76) or I2C_ADDR_SECONDARY (0x77) + :param i2c_device: Optional smbus or compatible instance for facilitating i2c communications. + + """ + + def __init__(self, i2c_addr=constants.I2C_ADDR_PRIMARY, i2c_device=None): + """Initialise BME680 sensor instance and verify device presence. + + :param i2c_addr: i2c address of BME680 + :param i2c_device: Optional SMBus-compatible instance for i2c transport + + """ + BME680Data.__init__(self) + + self.i2c_addr = i2c_addr + self._i2c = i2c_device + if self._i2c is None: + import smbus2 + self._i2c = smbus2.SMBus(1) + + try: + self.chip_id = self._get_regs(constants.CHIP_ID_ADDR, 1) + if self.chip_id != constants.CHIP_ID: + raise RuntimeError('BME680 Not Found. Invalid CHIP ID: 0x{0:02x}'.format(self.chip_id)) + except IOError: + raise RuntimeError("Unable to identify BME680 at 0x{:02x} (IOError)".format(self.i2c_addr)) + + self._variant = self._get_regs(constants.CHIP_VARIANT_ADDR, 1) + + self.soft_reset() + self.set_power_mode(constants.SLEEP_MODE) + + self._get_calibration_data() + + self.set_humidity_oversample(constants.OS_2X) + self.set_pressure_oversample(constants.OS_4X) + self.set_temperature_oversample(constants.OS_8X) + self.set_filter(constants.FILTER_SIZE_3) + if self._variant == constants.VARIANT_HIGH: + self.set_gas_status(constants.ENABLE_GAS_MEAS_HIGH) + else: + self.set_gas_status(constants.ENABLE_GAS_MEAS_LOW) + self.set_temp_offset(0) + self.get_sensor_data() + + def _get_calibration_data(self): + """Retrieve the sensor calibration data and store it in .calibration_data.""" + calibration = self._get_regs(constants.COEFF_ADDR1, constants.COEFF_ADDR1_LEN) + calibration += self._get_regs(constants.COEFF_ADDR2, constants.COEFF_ADDR2_LEN) + + heat_range = self._get_regs(constants.ADDR_RES_HEAT_RANGE_ADDR, 1) + heat_value = constants.twos_comp(self._get_regs(constants.ADDR_RES_HEAT_VAL_ADDR, 1), bits=8) + sw_error = constants.twos_comp(self._get_regs(constants.ADDR_RANGE_SW_ERR_ADDR, 1), bits=8) + + self.calibration_data.set_from_array(calibration) + self.calibration_data.set_other(heat_range, heat_value, sw_error) + + def soft_reset(self): + """Trigger a soft reset.""" + self._set_regs(constants.SOFT_RESET_ADDR, constants.SOFT_RESET_CMD) + time.sleep(constants.RESET_PERIOD / 1000.0) + + def set_temp_offset(self, value): + """Set temperature offset in celsius. + + If set, the temperature t_fine will be increased by given value in celsius. + :param value: Temperature offset in Celsius, eg. 4, -8, 1.25 + + """ + if value == 0: + self.offset_temp_in_t_fine = 0 + else: + self.offset_temp_in_t_fine = int(math.copysign((((int(abs(value) * 100)) << 8) - 128) / 5, value)) + + def set_humidity_oversample(self, value): + """Set humidity oversampling. + + A higher oversampling value means more stable sensor readings, + with less noise and jitter. + + However each step of oversampling adds about 2ms to the latency, + causing a slower response time to fast transients. + + :param value: Oversampling value, one of: OS_NONE, OS_1X, OS_2X, OS_4X, OS_8X, OS_16X + + """ + self.tph_settings.os_hum = value + self._set_bits(constants.CONF_OS_H_ADDR, constants.OSH_MSK, constants.OSH_POS, value) + + def get_humidity_oversample(self): + """Get humidity oversampling.""" + return (self._get_regs(constants.CONF_OS_H_ADDR, 1) & constants.OSH_MSK) >> constants.OSH_POS + + def set_pressure_oversample(self, value): + """Set temperature oversampling. + + A higher oversampling value means more stable sensor readings, + with less noise and jitter. + + However each step of oversampling adds about 2ms to the latency, + causing a slower response time to fast transients. + + :param value: Oversampling value, one of: OS_NONE, OS_1X, OS_2X, OS_4X, OS_8X, OS_16X + + """ + self.tph_settings.os_pres = value + self._set_bits(constants.CONF_T_P_MODE_ADDR, constants.OSP_MSK, constants.OSP_POS, value) + + def get_pressure_oversample(self): + """Get pressure oversampling.""" + return (self._get_regs(constants.CONF_T_P_MODE_ADDR, 1) & constants.OSP_MSK) >> constants.OSP_POS + + def set_temperature_oversample(self, value): + """Set pressure oversampling. + + A higher oversampling value means more stable sensor readings, + with less noise and jitter. + + However each step of oversampling adds about 2ms to the latency, + causing a slower response time to fast transients. + + :param value: Oversampling value, one of: OS_NONE, OS_1X, OS_2X, OS_4X, OS_8X, OS_16X + + """ + self.tph_settings.os_temp = value + self._set_bits(constants.CONF_T_P_MODE_ADDR, constants.OST_MSK, constants.OST_POS, value) + + def get_temperature_oversample(self): + """Get temperature oversampling.""" + return (self._get_regs(constants.CONF_T_P_MODE_ADDR, 1) & constants.OST_MSK) >> constants.OST_POS + + def set_filter(self, value): + """Set IIR filter size. + + Optionally remove short term fluctuations from the temperature and pressure readings, + increasing their resolution but reducing their bandwidth. + + Enabling the IIR filter does not slow down the time a reading takes, but will slow + down the BME680s response to changes in temperature and pressure. + + When the IIR filter is enabled, the temperature and pressure resolution is effectively 20bit. + When it is disabled, it is 16bit + oversampling-1 bits. + + """ + self.tph_settings.filter = value + self._set_bits(constants.CONF_ODR_FILT_ADDR, constants.FILTER_MSK, constants.FILTER_POS, value) + + def get_filter(self): + """Get filter size.""" + return (self._get_regs(constants.CONF_ODR_FILT_ADDR, 1) & constants.FILTER_MSK) >> constants.FILTER_POS + + def select_gas_heater_profile(self, value): + """Set current gas sensor conversion profile. + + Select one of the 10 configured heating durations/set points. + + :param value: Profile index from 0 to 9 + + """ + if value > constants.NBCONV_MAX or value < constants.NBCONV_MIN: + raise ValueError("Profile '{}' should be between {} and {}".format(value, constants.NBCONV_MIN, constants.NBCONV_MAX)) + + self.gas_settings.nb_conv = value + self._set_bits(constants.CONF_ODR_RUN_GAS_NBC_ADDR, constants.NBCONV_MSK, constants.NBCONV_POS, value) + + def get_gas_heater_profile(self): + """Get gas sensor conversion profile: 0 to 9.""" + return self._get_regs(constants.CONF_ODR_RUN_GAS_NBC_ADDR, 1) & constants.NBCONV_MSK + + def set_gas_heater_status(self, value): + """Enable/disable gas heater.""" + self.gas_settings.heater = value + self._set_bits(constants.CONF_HEAT_CTRL_ADDR, constants.HCTRL_MSK, constants.HCTRL_POS, value) + + def get_gas_heater_status(self): + """Get current heater status.""" + return (self._get_regs(constants.CONF_HEAT_CTRL_ADDR, 1) & constants.HCTRL_MSK) >> constants.HCTRL_POS + + def set_gas_status(self, value): + """Enable/disable gas sensor.""" + if value == -1: + if self._variant == constants.VARIANT_HIGH: + value = constants.ENABLE_GAS_MEAS_HIGH + else: + value = constants.ENABLE_GAS_MEAS_LOW + self.gas_settings.run_gas = value + self._set_bits(constants.CONF_ODR_RUN_GAS_NBC_ADDR, constants.RUN_GAS_MSK, constants.RUN_GAS_POS, value) + + def get_gas_status(self): + """Get the current gas status.""" + return (self._get_regs(constants.CONF_ODR_RUN_GAS_NBC_ADDR, 1) & constants.RUN_GAS_MSK) >> constants.RUN_GAS_POS + + def set_gas_heater_profile(self, temperature, duration, nb_profile=0): + """Set temperature and duration of gas sensor heater. + + :param temperature: Target temperature in degrees celsius, between 200 and 400 + :param durarion: Target duration in milliseconds, between 1 and 4032 + :param nb_profile: Target profile, between 0 and 9 + + """ + self.set_gas_heater_temperature(temperature, nb_profile=nb_profile) + self.set_gas_heater_duration(duration, nb_profile=nb_profile) + + def set_gas_heater_temperature(self, value, nb_profile=0): + """Set gas sensor heater temperature. + + :param value: Target temperature in degrees celsius, between 200 and 400 + + When setting an nb_profile other than 0, + make sure to select it with select_gas_heater_profile. + + """ + if nb_profile > constants.NBCONV_MAX or value < constants.NBCONV_MIN: + raise ValueError('Profile "{}" should be between {} and {}'.format(nb_profile, constants.NBCONV_MIN, constants.NBCONV_MAX)) + + self.gas_settings.heatr_temp = value + temp = int(self._calc_heater_resistance(self.gas_settings.heatr_temp)) + self._set_regs(constants.RES_HEAT0_ADDR + nb_profile, temp) + + def set_gas_heater_duration(self, value, nb_profile=0): + """Set gas sensor heater duration. + + Heating durations between 1 ms and 4032 ms can be configured. + Approximately 20-30 ms are necessary for the heater to reach the intended target temperature. + + :param value: Heating duration in milliseconds. + + When setting an nb_profile other than 0, + make sure to select it with select_gas_heater_profile. + + """ + if nb_profile > constants.NBCONV_MAX or value < constants.NBCONV_MIN: + raise ValueError('Profile "{}" should be between {} and {}'.format(nb_profile, constants.NBCONV_MIN, constants.NBCONV_MAX)) + + self.gas_settings.heatr_dur = value + temp = self._calc_heater_duration(self.gas_settings.heatr_dur) + self._set_regs(constants.GAS_WAIT0_ADDR + nb_profile, temp) + + def set_power_mode(self, value, blocking=True): + """Set power mode.""" + if value not in (constants.SLEEP_MODE, constants.FORCED_MODE): + raise ValueError('Power mode should be one of SLEEP_MODE or FORCED_MODE') + + self.power_mode = value + + self._set_bits(constants.CONF_T_P_MODE_ADDR, constants.MODE_MSK, constants.MODE_POS, value) + + while blocking and self.get_power_mode() != self.power_mode: + time.sleep(constants.POLL_PERIOD_MS / 1000.0) + + def get_power_mode(self): + """Get power mode.""" + self.power_mode = self._get_regs(constants.CONF_T_P_MODE_ADDR, 1) + return self.power_mode + + def get_sensor_data(self): + """Get sensor data. + + Stores data in .data and returns True upon success. + + """ + self.set_power_mode(constants.FORCED_MODE) + + for attempt in range(10): + status = self._get_regs(constants.FIELD0_ADDR, 1) + + if (status & constants.NEW_DATA_MSK) == 0: + time.sleep(constants.POLL_PERIOD_MS / 1000.0) + continue + + regs = self._get_regs(constants.FIELD0_ADDR, constants.FIELD_LENGTH) + + self.data.status = regs[0] & constants.NEW_DATA_MSK + # Contains the nb_profile used to obtain the current measurement + self.data.gas_index = regs[0] & constants.GAS_INDEX_MSK + self.data.meas_index = regs[1] + + adc_pres = (regs[2] << 12) | (regs[3] << 4) | (regs[4] >> 4) + adc_temp = (regs[5] << 12) | (regs[6] << 4) | (regs[7] >> 4) + adc_hum = (regs[8] << 8) | regs[9] + adc_gas_res_low = (regs[13] << 2) | (regs[14] >> 6) + adc_gas_res_high = (regs[15] << 2) | (regs[16] >> 6) + gas_range_l = regs[14] & constants.GAS_RANGE_MSK + gas_range_h = regs[16] & constants.GAS_RANGE_MSK + + if self._variant == constants.VARIANT_HIGH: + self.data.status |= regs[16] & constants.GASM_VALID_MSK + self.data.status |= regs[16] & constants.HEAT_STAB_MSK + else: + self.data.status |= regs[14] & constants.GASM_VALID_MSK + self.data.status |= regs[14] & constants.HEAT_STAB_MSK + + self.data.heat_stable = (self.data.status & constants.HEAT_STAB_MSK) > 0 + + temperature = self._calc_temperature(adc_temp) + self.data.temperature = temperature / 100.0 + self.ambient_temperature = temperature # Saved for heater calc + + self.data.pressure = self._calc_pressure(adc_pres) / 100.0 + self.data.humidity = self._calc_humidity(adc_hum) / 1000.0 + + if self._variant == constants.VARIANT_HIGH: + self.data.gas_resistance = self._calc_gas_resistance_high(adc_gas_res_high, gas_range_h) + else: + self.data.gas_resistance = self._calc_gas_resistance_low(adc_gas_res_low, gas_range_l) + + return True + + return False + + def _set_bits(self, register, mask, position, value): + """Mask out and set one or more bits in a register.""" + temp = self._get_regs(register, 1) + temp &= ~mask + temp |= value << position + self._set_regs(register, temp) + + def _set_regs(self, register, value): + """Set one or more registers.""" + if isinstance(value, int): + self._i2c.write_byte_data(self.i2c_addr, register, value) + else: + self._i2c.write_i2c_block_data(self.i2c_addr, register, value) + + def _get_regs(self, register, length): + """Get one or more registers.""" + if length == 1: + return self._i2c.read_byte_data(self.i2c_addr, register) + else: + return self._i2c.read_i2c_block_data(self.i2c_addr, register, length) + + def _calc_temperature(self, temperature_adc): + """Convert the raw temperature to degrees C using calibration_data.""" + var1 = (temperature_adc >> 3) - (self.calibration_data.par_t1 << 1) + var2 = (var1 * self.calibration_data.par_t2) >> 11 + var3 = ((var1 >> 1) * (var1 >> 1)) >> 12 + var3 = ((var3) * (self.calibration_data.par_t3 << 4)) >> 14 + + # Save teperature data for pressure calculations + self.calibration_data.t_fine = (var2 + var3) + self.offset_temp_in_t_fine + calc_temp = (((self.calibration_data.t_fine * 5) + 128) >> 8) + + return calc_temp + + def _calc_pressure(self, pressure_adc): + """Convert the raw pressure using calibration data.""" + var1 = ((self.calibration_data.t_fine) >> 1) - 64000 + var2 = ((((var1 >> 2) * (var1 >> 2)) >> 11) * + self.calibration_data.par_p6) >> 2 + var2 = var2 + ((var1 * self.calibration_data.par_p5) << 1) + var2 = (var2 >> 2) + (self.calibration_data.par_p4 << 16) + var1 = (((((var1 >> 2) * (var1 >> 2)) >> 13) * + ((self.calibration_data.par_p3 << 5)) >> 3) + + ((self.calibration_data.par_p2 * var1) >> 1)) + var1 = var1 >> 18 + + var1 = ((32768 + var1) * self.calibration_data.par_p1) >> 15 + calc_pressure = 1048576 - pressure_adc + calc_pressure = ((calc_pressure - (var2 >> 12)) * (3125)) + + if calc_pressure >= (1 << 31): + calc_pressure = ((calc_pressure // var1) << 1) + else: + calc_pressure = ((calc_pressure << 1) // var1) + + var1 = (self.calibration_data.par_p9 * (((calc_pressure >> 3) * + (calc_pressure >> 3)) >> 13)) >> 12 + var2 = ((calc_pressure >> 2) * + self.calibration_data.par_p8) >> 13 + var3 = ((calc_pressure >> 8) * (calc_pressure >> 8) * + (calc_pressure >> 8) * + self.calibration_data.par_p10) >> 17 + + calc_pressure = (calc_pressure) + ((var1 + var2 + var3 + + (self.calibration_data.par_p7 << 7)) >> 4) + + return calc_pressure + + def _calc_humidity(self, humidity_adc): + """Convert the raw humidity using calibration data.""" + temp_scaled = ((self.calibration_data.t_fine * 5) + 128) >> 8 + var1 = (humidity_adc - ((self.calibration_data.par_h1 * 16))) -\ + (((temp_scaled * self.calibration_data.par_h3) // (100)) >> 1) + var2 = (self.calibration_data.par_h2 * + (((temp_scaled * self.calibration_data.par_h4) // (100)) + + (((temp_scaled * ((temp_scaled * self.calibration_data.par_h5) // (100))) >> 6) // + (100)) + (1 * 16384))) >> 10 + var3 = var1 * var2 + var4 = self.calibration_data.par_h6 << 7 + var4 = ((var4) + ((temp_scaled * self.calibration_data.par_h7) // (100))) >> 4 + var5 = ((var3 >> 14) * (var3 >> 14)) >> 10 + var6 = (var4 * var5) >> 1 + calc_hum = (((var3 + var6) >> 10) * (1000)) >> 12 + + return min(max(calc_hum, 0), 100000) + + def _calc_gas_resistance(self, gas_res_adc, gas_range): + """Convert the raw gas resistance using calibration data.""" + if self._variant == constants.VARIANT_HIGH: + return self._calc_gas_resistance_high(gas_res_adc, gas_range) + else: + return self._calc_gas_resistance_low(gas_res_adc, gas_range) + + def _calc_gas_resistance_high(self, gas_res_adc, gas_range): + """Convert the raw gas resistance using calibration data. + + Applies to Variant ID == 0x01 only. + + """ + var1 = 262144 >> gas_range + var2 = gas_res_adc - 512 + + var2 *= 3 + var2 = 4096 + var2 + + calc_gas_res = (10000 * var1) / var2 + calc_gas_res *= 100 + + return calc_gas_res + + def _calc_gas_resistance_low(self, gas_res_adc, gas_range): + """Convert the raw gas resistance using calibration data. + + Applies to Variant ID == 0x00 only. + + """ + var1 = ((1340 + (5 * self.calibration_data.range_sw_err)) * (lookupTable1[gas_range])) >> 16 + var2 = (((gas_res_adc << 15) - (16777216)) + var1) + var3 = ((lookupTable2[gas_range] * var1) >> 9) + calc_gas_res = ((var3 + (var2 >> 1)) / var2) + + if calc_gas_res < 0: + calc_gas_res = (1 << 32) + calc_gas_res + + return calc_gas_res + + def _calc_heater_resistance(self, temperature): + """Convert raw heater resistance using calibration data.""" + temperature = min(max(temperature, 200), 400) + + var1 = ((self.ambient_temperature * self.calibration_data.par_gh3) / 1000) * 256 + var2 = (self.calibration_data.par_gh1 + 784) * (((((self.calibration_data.par_gh2 + 154009) * temperature * 5) / 100) + 3276800) / 10) + var3 = var1 + (var2 / 2) + var4 = (var3 / (self.calibration_data.res_heat_range + 4)) + var5 = (131 * self.calibration_data.res_heat_val) + 65536 + heatr_res_x100 = (((var4 / var5) - 250) * 34) + heatr_res = ((heatr_res_x100 + 50) / 100) + + return heatr_res + + def _calc_heater_duration(self, duration): + """Calculate correct value for heater duration setting from milliseconds.""" + if duration < 0xfc0: + factor = 0 + + while duration > 0x3f: + duration /= 4 + factor += 1 + + return int(duration + (factor * 64)) + + return 0xff diff --git a/scripts/tempSensor/lib/data_logging.py b/scripts/tempSensor/lib/data_logging.py new file mode 100644 index 00000000..f391682d --- /dev/null +++ b/scripts/tempSensor/lib/data_logging.py @@ -0,0 +1,166 @@ +import json +import sys +from time import gmtime, localtime, time + +import machine +import ntptime +import uos +import urequests +from machine import SPI, Pin +from sdcard import sdcard +from uio import StringIO + +# # uses a more robust ntptime +# from lib.ntptime import ntptime + + +def get_traceback(err): + try: + with StringIO() as f: # type: ignore + sys.print_exception(err, f) + return f.getvalue() + except Exception as err2: + print(err2) + return f"Failed to extract file and line number due to {err2}.\nOriginal error: {err}" # noqa: E501 + + +def initialize_sdcard( + spi_id=1, + cs_pin=15, + sck_pin=10, + mosi_pin=11, + miso_pin=12, + baudrate=1000000, + polarity=0, + phase=0, + bits=8, + firstbit=SPI.MSB, + verbose=True, +): + try: + cs = Pin(cs_pin, Pin.OUT) + + spi = SPI( + spi_id, + baudrate=baudrate, + polarity=polarity, + phase=phase, + bits=bits, + firstbit=firstbit, + sck=Pin(sck_pin), + mosi=Pin(mosi_pin), + miso=Pin(miso_pin), + ) + + # Initialize SD card + sd = sdcard.SDCard(spi, cs) + + vfs = uos.VfsFat(sd) + uos.mount(vfs, "/sd") # type: ignore + if verbose: + print("SD Card initialized successfully") + return True + except Exception as e: + if verbose: + print(get_traceback(e)) + print("SD Card failed to initialize") + return False + + +def write_payload_backup(payload_data: str, fpath: str = "/sd/experiments.txt"): + payload = json.dumps(payload_data) + with open(fpath, "a") as file: + # line = ",".join([str(payload[key]) for key in payload.keys()]) + file.write(f"{payload}\r\n") + + +def log_to_mongodb( + document: dict, + api_key: str, + url: str, + cluster_name: str, + database_name: str, + collection_name: str, + verbose: bool = True, + retries: int = 2, +): + # based on https://medium.com/@johnlpage/introduction-to-microcontrollers-and-the-pi-pico-w-f7a2d9ad1394 + headers = {"api-key": api_key} + + insertPayload = { + "dataSource": cluster_name, + "database": database_name, + "collection": collection_name, + "document": document, + } + + if verbose: + print(f"sending document to {cluster_name}:{database_name}:{collection_name}") + + for _ in range(retries): + response = None + if _ > 0: + print(f"retrying... ({_} of {retries})") + + try: + response = urequests.post(url, headers=headers, json=insertPayload) + txt = str(response.text) + status_code = response.status_code + + if verbose: + print(f"Response: ({status_code}), msg = {txt}") + if response.status_code == 201: + print("Added Successfully") + break + else: + print("Error") + + # Always close response objects so we don't leak memory + response.close() + except Exception as e: + if response is not None: + response.close() + if _ == retries - 1: + raise e + else: + print(e) + + +def get_timestamp(timeout=2, return_str=False): + ntptime.timeout = timeout # type: ignore + time_int = ntptime.time() + utc_tuple = gmtime(time_int) + year, month, mday, hour, minute, second, weekday, yearday = utc_tuple + + time_str = f"{year}-{month}-{mday} {hour:02}:{minute:02}:{second:02}" + + if return_str: + return time_int, time_str + + return time_int + + +def get_local_timestamp(return_str=False): + t = time() + year, month, mday, hour, minute, second, _, _ = localtime(t) + time_str = f"{year}-{month}-{mday} {hour:02}:{minute:02}:{second:02}" + + if return_str: + return t, time_str + + return t + + +def get_onboard_temperature(unit="K"): + sensor_temp = machine.ADC(4) + conversion_factor = 3.3 / (65535) + reading = sensor_temp.read_u16() * conversion_factor + celsius_degrees = 27 - (reading - 0.706) / 0.001721 + if unit == "C": + return celsius_degrees + elif unit == "K": + return celsius_degrees + 273.15 + elif unit == "F": + return celsius_degrees * 9 / 5 + 32 + else: + raise ValueError("Invalid unit. Must be one of 'C', 'K', or 'F") diff --git a/scripts/tempSensor/lib/functools.py b/scripts/tempSensor/lib/functools.py new file mode 100644 index 00000000..510a3406 --- /dev/null +++ b/scripts/tempSensor/lib/functools.py @@ -0,0 +1,28 @@ +def partial(func, *args, **kwargs): + def _partial(*more_args, **more_kwargs): + kw = kwargs.copy() + kw.update(more_kwargs) + func(*(args + more_args), **kw) + + return _partial + + +def update_wrapper(wrapper, wrapped): + # Dummy impl + return wrapper + + +def wraps(wrapped): + # Dummy impl + return lambda x: x + + +def reduce(function, iterable, initializer=None): + it = iter(iterable) + if initializer is None: + value = next(it) + else: + value = initializer + for element in it: + value = function(value, element) + return value diff --git a/scripts/tempSensor/lib/mqtt_as.py b/scripts/tempSensor/lib/mqtt_as.py new file mode 100644 index 00000000..2be2cb50 --- /dev/null +++ b/scripts/tempSensor/lib/mqtt_as.py @@ -0,0 +1,824 @@ +# mqtt_as.py Asynchronous version of umqtt.robust +# (C) Copyright Peter Hinch 2017-2023. +# Released under the MIT licence. + +# Pyboard D support added also RP2/default +# Various improvements contributed by Kevin Köck. + +import gc + +import usocket as socket +import ustruct as struct + +gc.collect() +import uasyncio as asyncio +from ubinascii import hexlify + +gc.collect() +from uerrno import EINPROGRESS, ETIMEDOUT +from utime import ticks_diff, ticks_ms + +gc.collect() +import network +from machine import unique_id +from micropython import const + +gc.collect() +from sys import platform + +VERSION = (0, 7, 1) + +# Default short delay for good SynCom throughput (avoid sleep(0) with SynCom). +_DEFAULT_MS = const(20) +_SOCKET_POLL_DELAY = const(5) # 100ms added greatly to publish latency + +# Legitimate errors while waiting on a socket. See uasyncio __init__.py open_connection(). +ESP32 = platform == "esp32" +RP2 = platform == "rp2" +if ESP32: + # https://forum.micropython.org/viewtopic.php?f=16&t=3608&p=20942#p20942 + BUSY_ERRORS = [EINPROGRESS, ETIMEDOUT, 118, 119] # Add in weird ESP32 errors +elif RP2: + BUSY_ERRORS = [EINPROGRESS, ETIMEDOUT, -110] +else: + BUSY_ERRORS = [EINPROGRESS, ETIMEDOUT] + +ESP8266 = platform == "esp8266" +PYBOARD = platform == "pyboard" + + +# Default "do little" coro for optional user replacement +async def eliza(*_): # e.g. via set_wifi_handler(coro): see test program + await asyncio.sleep_ms(_DEFAULT_MS) + + +class MsgQueue: + def __init__(self, size): + self._q = [0 for _ in range(max(size, 4))] + self._size = size + self._wi = 0 + self._ri = 0 + self._evt = asyncio.Event() + self.discards = 0 + + def put(self, *v): + self._q[self._wi] = v + self._evt.set() + self._wi = (self._wi + 1) % self._size + if self._wi == self._ri: # Would indicate empty + self._ri = (self._ri + 1) % self._size # Discard a message + self.discards += 1 + + def __aiter__(self): + return self + + async def __anext__(self): + if self._ri == self._wi: # Empty + self._evt.clear() + await self._evt.wait() + r = self._q[self._ri] + self._ri = (self._ri + 1) % self._size + return r + + +config = { + "client_id": hexlify(unique_id()), + "server": None, + "port": 0, + "user": "", + "password": "", + "keepalive": 60, + "ping_interval": 0, + "ssl": False, + "ssl_params": {}, + "response_time": 10, + "clean_init": True, + "clean": True, + "max_repubs": 4, + "will": None, + "subs_cb": lambda *_: None, + "wifi_coro": eliza, + "connect_coro": eliza, + "ssid": None, + "wifi_pw": None, + "queue_len": 0, + "gateway": False, +} + + +class MQTTException(Exception): + pass + + +def pid_gen(): + pid = 0 + while True: + pid = pid + 1 if pid < 65535 else 1 + yield pid + + +def qos_check(qos): + if not (qos == 0 or qos == 1): + raise ValueError("Only qos 0 and 1 are supported.") + + +# MQTT_base class. Handles MQTT protocol on the basis of a good connection. +# Exceptions from connectivity failures are handled by MQTTClient subclass. +class MQTT_base: + REPUB_COUNT = 0 # TEST + DEBUG = False + + def __init__(self, config): + self._events = config["queue_len"] > 0 + # MQTT config + self._client_id = config["client_id"] + self._user = config["user"] + self._pswd = config["password"] + self._keepalive = config["keepalive"] + if self._keepalive >= 65536: + raise ValueError("invalid keepalive time") + self._response_time = ( + config["response_time"] * 1000 + ) # Repub if no PUBACK received (ms). + self._max_repubs = config["max_repubs"] + self._clean_init = config[ + "clean_init" + ] # clean_session state on first connection + self._clean = config["clean"] # clean_session state on reconnect + will = config["will"] + if will is None: + self._lw_topic = False + else: + self._set_last_will(*will) + # WiFi config + self._ssid = config["ssid"] # Required for ESP32 / Pyboard D. Optional ESP8266 + self._wifi_pw = config["wifi_pw"] + self._ssl = config["ssl"] + self._ssl_params = config["ssl_params"] + # Callbacks and coros + if self._events: + self.up = asyncio.Event() + self.down = asyncio.Event() + self.queue = MsgQueue(config["queue_len"]) + else: # Callbacks + self._cb = config["subs_cb"] + self._wifi_handler = config["wifi_coro"] + self._connect_handler = config["connect_coro"] + # Network + self.port = config["port"] + if self.port == 0: + self.port = 8883 if self._ssl else 1883 + self.server = config["server"] + if self.server is None: + raise ValueError("no server specified.") + self._sock = None + self._sta_if = network.WLAN(network.STA_IF) + self._sta_if.active(True) + if config["gateway"]: # Called from gateway (hence ESP32). + import aioespnow # Set up ESPNOW + + while not (sta := self._sta_if).active(): + time.sleep(0.1) + sta.config(pm=sta.PM_NONE) # No power management + sta.active(True) + self._espnow = ( + aioespnow.AIOESPNow() + ) # Returns AIOESPNow enhanced with async support + self._espnow.active(True) + + self.newpid = pid_gen() + self.rcv_pids = set() # PUBACK and SUBACK pids awaiting ACK response + self.last_rx = ticks_ms() # Time of last communication from broker + self.lock = asyncio.Lock() + + def _set_last_will(self, topic, msg, retain=False, qos=0): + qos_check(qos) + if not topic: + raise ValueError("Empty topic.") + self._lw_topic = topic + self._lw_msg = msg + self._lw_qos = qos + self._lw_retain = retain + + def dprint(self, msg, *args): + if self.DEBUG: + print(msg % args) + + def _timeout(self, t): + return ticks_diff(ticks_ms(), t) > self._response_time + + async def _as_read(self, n, sock=None): # OSError caught by superclass + if sock is None: + sock = self._sock + # Declare a byte array of size n. That space is needed anyway, better + # to just 'allocate' it in one go instead of appending to an + # existing object, this prevents reallocation and fragmentation. + data = bytearray(n) + buffer = memoryview(data) + size = 0 + t = ticks_ms() + while size < n: + if self._timeout(t) or not self.isconnected(): + raise OSError(-1, "Timeout on socket read") + try: + msg_size = sock.readinto(buffer[size:], n - size) + except OSError as e: # ESP32 issues weird 119 errors here + msg_size = None + if e.args[0] not in BUSY_ERRORS: + raise + if msg_size == 0: # Connection closed by host + raise OSError(-1, "Connection closed by host") + if msg_size is not None: # data received + size += msg_size + t = ticks_ms() + self.last_rx = ticks_ms() + await asyncio.sleep_ms(_SOCKET_POLL_DELAY) + return data + + async def _as_write(self, bytes_wr, length=0, sock=None): + if sock is None: + sock = self._sock + + # Wrap bytes in memoryview to avoid copying during slicing + bytes_wr = memoryview(bytes_wr) + if length: + bytes_wr = bytes_wr[:length] + t = ticks_ms() + while bytes_wr: + if self._timeout(t) or not self.isconnected(): + raise OSError(-1, "Timeout on socket write") + try: + n = sock.write(bytes_wr) + except OSError as e: # ESP32 issues weird 119 errors here + n = 0 + if e.args[0] not in BUSY_ERRORS: + raise + if n: + t = ticks_ms() + bytes_wr = bytes_wr[n:] + await asyncio.sleep_ms(_SOCKET_POLL_DELAY) + + async def _send_str(self, s): + await self._as_write(struct.pack("!H", len(s))) + await self._as_write(s) + + async def _recv_len(self): + n = 0 + sh = 0 + while 1: + res = await self._as_read(1) + b = res[0] + n |= (b & 0x7F) << sh + if not b & 0x80: + return n + sh += 7 + + async def _connect(self, clean): + self._sock = socket.socket() + self._sock.setblocking(False) + try: + self._sock.connect(self._addr) + except OSError as e: + if e.args[0] not in BUSY_ERRORS: + raise + await asyncio.sleep_ms(_DEFAULT_MS) + self.dprint("Connecting to broker.") + if self._ssl: + import ssl + + self._sock = ssl.wrap_socket(self._sock, **self._ssl_params) + premsg = bytearray(b"\x10\0\0\0\0\0") + msg = bytearray(b"\x04MQTT\x04\0\0\0") # Protocol 3.1.1 + + sz = 10 + 2 + len(self._client_id) + msg[6] = clean << 1 + if self._user: + sz += 2 + len(self._user) + 2 + len(self._pswd) + msg[6] |= 0xC0 + if self._keepalive: + msg[7] |= self._keepalive >> 8 + msg[8] |= self._keepalive & 0x00FF + if self._lw_topic: + sz += 2 + len(self._lw_topic) + 2 + len(self._lw_msg) + msg[6] |= 0x4 | (self._lw_qos & 0x1) << 3 | (self._lw_qos & 0x2) << 3 + msg[6] |= self._lw_retain << 5 + + i = 1 + while sz > 0x7F: + premsg[i] = (sz & 0x7F) | 0x80 + sz >>= 7 + i += 1 + premsg[i] = sz + await self._as_write(premsg, i + 2) + await self._as_write(msg) + await self._send_str(self._client_id) + if self._lw_topic: + await self._send_str(self._lw_topic) + await self._send_str(self._lw_msg) + if self._user: + await self._send_str(self._user) + await self._send_str(self._pswd) + # Await CONNACK + # read causes ECONNABORTED if broker is out; triggers a reconnect. + resp = await self._as_read(4) + self.dprint("Connected to broker.") # Got CONNACK + if ( + resp[3] != 0 or resp[0] != 0x20 or resp[1] != 0x02 + ): # Bad CONNACK e.g. authentication fail. + raise OSError( + -1, + f"Connect fail: 0x{(resp[0] << 8) + resp[1]:04x} {resp[3]} (README 7)", + ) + + async def _ping(self): + async with self.lock: + await self._as_write(b"\xc0\0") + + # Check internet connectivity by sending DNS lookup to Google's 8.8.8.8 + async def wan_ok( + self, + packet=b"$\x1a\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x03www\x06google\x03com\x00\x00\x01\x00\x01", + ): + if not self.isconnected(): # WiFi is down + return False + length = 32 # DNS query and response packet size + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.setblocking(False) + s.connect(("8.8.8.8", 53)) + await asyncio.sleep(1) + try: + await self._as_write(packet, sock=s) + await asyncio.sleep(2) + res = await self._as_read(length, s) + if len(res) == length: + return True # DNS response size OK + except OSError: # Timeout on read: no connectivity. + return False + finally: + s.close() + return False + + async def broker_up(self): # Test broker connectivity + if not self.isconnected(): + return False + tlast = self.last_rx + if ticks_diff(ticks_ms(), tlast) < 1000: + return True + try: + await self._ping() + except OSError: + return False + t = ticks_ms() + while not self._timeout(t): + await asyncio.sleep_ms(100) + if ticks_diff(self.last_rx, tlast) > 0: # Response received + return True + return False + + async def disconnect(self): + if self._sock is not None: + await self._kill_tasks(False) # Keep socket open + try: + async with self.lock: + self._sock.write(b"\xe0\0") # Close broker connection + await asyncio.sleep_ms(100) + except OSError: + pass + self._close() + self._has_connected = False + + def _close(self): + if self._sock is not None: + self._sock.close() + + def close( + self, + ): # API. See https://github.com/peterhinch/micropython-mqtt/issues/60 + self._close() + try: + self._sta_if.disconnect() # Disconnect Wi-Fi to avoid errors + except OSError: + self.dprint("Wi-Fi not started, unable to disconnect interface") + self._sta_if.active(False) + + async def _await_pid(self, pid): + t = ticks_ms() + while pid in self.rcv_pids: # local copy + if self._timeout(t) or not self.isconnected(): + break # Must repub or bail out + await asyncio.sleep_ms(100) + else: + return True # PID received. All done. + return False + + # qos == 1: coro blocks until wait_msg gets correct PID. + # If WiFi fails completely subclass re-publishes with new PID. + async def publish(self, topic, msg, retain, qos): + pid = next(self.newpid) + if qos: + self.rcv_pids.add(pid) + async with self.lock: + await self._publish(topic, msg, retain, qos, 0, pid) + if qos == 0: + return + + count = 0 + while 1: # Await PUBACK, republish on timeout + if await self._await_pid(pid): + return + # No match + if count >= self._max_repubs or not self.isconnected(): + raise OSError(-1) # Subclass to re-publish with new PID + async with self.lock: + await self._publish(topic, msg, retain, qos, dup=1, pid=pid) # Add pid + count += 1 + self.REPUB_COUNT += 1 + + async def _publish(self, topic, msg, retain, qos, dup, pid): + pkt = bytearray(b"\x30\0\0\0") + pkt[0] |= qos << 1 | retain | dup << 3 + sz = 2 + len(topic) + len(msg) + if qos > 0: + sz += 2 + if sz >= 2097152: + raise MQTTException("Strings too long.") + i = 1 + while sz > 0x7F: + pkt[i] = (sz & 0x7F) | 0x80 + sz >>= 7 + i += 1 + pkt[i] = sz + await self._as_write(pkt, i + 1) + await self._send_str(topic) + if qos > 0: + struct.pack_into("!H", pkt, 0, pid) + await self._as_write(pkt, 2) + await self._as_write(msg) + + # Can raise OSError if WiFi fails. Subclass traps. + async def subscribe(self, topic, qos): + pkt = bytearray(b"\x82\0\0\0") + pid = next(self.newpid) + self.rcv_pids.add(pid) + struct.pack_into("!BH", pkt, 1, 2 + 2 + len(topic) + 1, pid) + async with self.lock: + await self._as_write(pkt) + await self._send_str(topic) + await self._as_write(qos.to_bytes(1, "little")) + + if not await self._await_pid(pid): + raise OSError(-1) + + # Can raise OSError if WiFi fails. Subclass traps. + async def unsubscribe(self, topic): + pkt = bytearray(b"\xa2\0\0\0") + pid = next(self.newpid) + self.rcv_pids.add(pid) + struct.pack_into("!BH", pkt, 1, 2 + 2 + len(topic), pid) + async with self.lock: + await self._as_write(pkt) + await self._send_str(topic) + + if not await self._await_pid(pid): + raise OSError(-1) + + # Wait for a single incoming MQTT message and process it. + # Subscribed messages are delivered to a callback previously + # set by .setup() method. Other (internal) MQTT + # messages processed internally. + # Immediate return if no data available. Called from ._handle_msg(). + async def wait_msg(self): + try: + res = self._sock.read(1) # Throws OSError on WiFi fail + except OSError as e: + if e.args[0] in BUSY_ERRORS: # Needed by RP2 + await asyncio.sleep_ms(0) + return + raise + if res is None: + return + if res == b"": + raise OSError(-1, "Empty response") + + if res == b"\xd0": # PINGRESP + await self._as_read(1) # Update .last_rx time + return + op = res[0] + + if op == 0x40: # PUBACK: save pid + sz = await self._as_read(1) + if sz != b"\x02": + raise OSError(-1, "Invalid PUBACK packet") + rcv_pid = await self._as_read(2) + pid = rcv_pid[0] << 8 | rcv_pid[1] + if pid in self.rcv_pids: + self.rcv_pids.discard(pid) + else: + raise OSError(-1, "Invalid pid in PUBACK packet") + + if op == 0x90: # SUBACK + resp = await self._as_read(4) + if resp[3] == 0x80: + raise OSError(-1, "Invalid SUBACK packet") + pid = resp[2] | (resp[1] << 8) + if pid in self.rcv_pids: + self.rcv_pids.discard(pid) + else: + raise OSError(-1, "Invalid pid in SUBACK packet") + + if op == 0xB0: # UNSUBACK + resp = await self._as_read(3) + pid = resp[2] | (resp[1] << 8) + if pid in self.rcv_pids: + self.rcv_pids.discard(pid) + else: + raise OSError(-1) + + if op & 0xF0 != 0x30: + return + sz = await self._recv_len() + topic_len = await self._as_read(2) + topic_len = (topic_len[0] << 8) | topic_len[1] + topic = await self._as_read(topic_len) + sz -= topic_len + 2 + if op & 6: + pid = await self._as_read(2) + pid = pid[0] << 8 | pid[1] + sz -= 2 + msg = await self._as_read(sz) + retained = op & 0x01 + if self._events: + self.queue.put(topic, msg, bool(retained)) + else: + self._cb(topic, msg, bool(retained)) + if op & 6 == 2: # qos 1 + pkt = bytearray(b"\x40\x02\0\0") # Send PUBACK + struct.pack_into("!H", pkt, 2, pid) + await self._as_write(pkt) + elif op & 6 == 4: # qos 2 not supported + raise OSError(-1, "QoS 2 not supported") + + +# MQTTClient class. Handles issues relating to connectivity. + + +class MQTTClient(MQTT_base): + def __init__(self, config): + super().__init__(config) + self._isconnected = False # Current connection state + keepalive = 1000 * self._keepalive # ms + self._ping_interval = keepalive // 4 if keepalive else 20000 + p_i = ( + config["ping_interval"] * 1000 + ) # Can specify shorter e.g. for subscribe-only + if p_i and p_i < self._ping_interval: + self._ping_interval = p_i + self._in_connect = False + self._has_connected = False # Define 'Clean Session' value to use. + self._tasks = [] + if ESP8266: + import esp + + esp.sleep_type( + 0 + ) # Improve connection integrity at cost of power consumption. + + async def wifi_connect(self, quick=False): + s = self._sta_if + if ESP8266: + if s.isconnected(): # 1st attempt, already connected. + return + s.active(True) + s.connect() # ESP8266 remembers connection. + for _ in range(60): + if ( + s.status() != network.STAT_CONNECTING + ): # Break out on fail or success. Check once per sec. + break + await asyncio.sleep(1) + if ( + s.status() == network.STAT_CONNECTING + ): # might hang forever awaiting dhcp lease renewal or something else + s.disconnect() + await asyncio.sleep(1) + if ( + not s.isconnected() + and self._ssid is not None + and self._wifi_pw is not None + ): + s.connect(self._ssid, self._wifi_pw) + while ( + s.status() == network.STAT_CONNECTING + ): # Break out on fail or success. Check once per sec. + await asyncio.sleep(1) + else: + s.active(True) + if RP2: # Disable auto-sleep. + # https://datasheets.raspberrypi.com/picow/connecting-to-the-internet-with-pico-w.pdf + # para 3.6.3 + s.config(pm=0xA11140) + s.connect(self._ssid, self._wifi_pw) + for _ in range(60): # Break out on fail or success. Check once per sec. + await asyncio.sleep(1) + # Loop while connecting or no IP + if s.isconnected(): + break + if ESP32: + if s.status() != network.STAT_CONNECTING: # 1001 + break + elif PYBOARD: # No symbolic constants in network + if not 1 <= s.status() <= 2: + break + elif RP2: # 1 is STAT_CONNECTING. 2 reported by user (No IP?) + if not 1 <= s.status() <= 2: + break + else: # Timeout: still in connecting state + s.disconnect() + await asyncio.sleep(1) + + if not s.isconnected(): # Timed out + raise OSError("Wi-Fi connect timed out") + if not quick: # Skip on first connection only if power saving + # Ensure connection stays up for a few secs. + self.dprint("Checking WiFi integrity.") + for _ in range(5): + if not s.isconnected(): + raise OSError("Connection Unstable") # in 1st 5 secs + await asyncio.sleep(1) + self.dprint("Got reliable connection") + + async def connect( + self, *, quick=False + ): # Quick initial connect option for battery apps + if not self._has_connected: + await self.wifi_connect(quick) # On 1st call, caller handles error + # Note this blocks if DNS lookup occurs. Do it once to prevent + # blocking during later internet outage: + self._addr = socket.getaddrinfo(self.server, self.port)[0][-1] + self._in_connect = True # Disable low level ._isconnected check + try: + if not self._has_connected and self._clean_init and not self._clean: + # Power up. Clear previous session data but subsequently save it. + # Issue #40 + await self._connect(True) # Connect with clean session + try: + async with self.lock: + self._sock.write( + b"\xe0\0" + ) # Force disconnect but keep socket open + except OSError: + pass + self.dprint("Waiting for disconnect") + await asyncio.sleep(2) # Wait for broker to disconnect + self.dprint("About to reconnect with unclean session.") + await self._connect(self._clean) + except Exception: + self._close() + self._in_connect = False # Caller may run .isconnected() + raise + self.rcv_pids.clear() + # If we get here without error broker/LAN must be up. + self._isconnected = True + self._in_connect = False # Low level code can now check connectivity. + if not self._events: + asyncio.create_task(self._wifi_handler(True)) # User handler. + if not self._has_connected: + self._has_connected = True # Use normal clean flag on reconnect. + asyncio.create_task(self._keep_connected()) + # Runs forever unless user issues .disconnect() + + asyncio.create_task(self._handle_msg()) # Task quits on connection fail. + self._tasks.append(asyncio.create_task(self._keep_alive())) + if self.DEBUG: + self._tasks.append(asyncio.create_task(self._memory())) + if self._events: + self.up.set() # Connectivity is up + else: + asyncio.create_task(self._connect_handler(self)) # User handler. + + # Launched by .connect(). Runs until connectivity fails. Checks for and + # handles incoming messages. + async def _handle_msg(self): + try: + while self.isconnected(): + async with self.lock: + await self.wait_msg() # Immediate return if no message + await asyncio.sleep_ms(_DEFAULT_MS) # Let other tasks get lock + + except OSError: + pass + self._reconnect() # Broker or WiFi fail. + + # Keep broker alive MQTT spec 3.1.2.10 Keep Alive. + # Runs until ping failure or no response in keepalive period. + async def _keep_alive(self): + while self.isconnected(): + pings_due = ticks_diff(ticks_ms(), self.last_rx) // self._ping_interval + if pings_due >= 4: + self.dprint("Reconnect: broker fail.") + break + await asyncio.sleep_ms(self._ping_interval) + try: + await self._ping() + except OSError: + break + self._reconnect() # Broker or WiFi fail. + + async def _kill_tasks(self, kill_skt): # Cancel running tasks + for task in self._tasks: + task.cancel() + self._tasks.clear() + await asyncio.sleep_ms(0) # Ensure cancellation complete + if kill_skt: # Close socket + self._close() + + # DEBUG: show RAM messages. + async def _memory(self): + while True: + await asyncio.sleep(20) + gc.collect() + self.dprint("RAM free %d alloc %d", gc.mem_free(), gc.mem_alloc()) + + def isconnected(self): + if self._in_connect: # Disable low-level check during .connect() + return True + if self._isconnected and not self._sta_if.isconnected(): # It's going down. + self._reconnect() + return self._isconnected + + def _reconnect(self): # Schedule a reconnection if not underway. + if self._isconnected: + self._isconnected = False + asyncio.create_task(self._kill_tasks(True)) # Shut down tasks and socket + if self._events: # Signal an outage + self.down.set() + else: + asyncio.create_task(self._wifi_handler(False)) # User handler. + + # Await broker connection. + async def _connection(self): + while not self._isconnected: + await asyncio.sleep(1) + + # Scheduled on 1st successful connection. Runs forever maintaining wifi and + # broker connection. Must handle conditions at edge of WiFi range. + async def _keep_connected(self): + while self._has_connected: + if self.isconnected(): # Pause for 1 second + await asyncio.sleep(1) + gc.collect() + else: # Link is down, socket is closed, tasks are killed + try: + self._sta_if.disconnect() + except OSError: + self.dprint("Wi-Fi not started, unable to disconnect interface") + await asyncio.sleep(1) + try: + await self.wifi_connect() + except OSError: + continue + if ( + not self._has_connected + ): # User has issued the terminal .disconnect() + self.dprint("Disconnected, exiting _keep_connected") + break + try: + await self.connect() + # Now has set ._isconnected and scheduled _connect_handler(). + self.dprint("Reconnect OK!") + except OSError as e: + self.dprint("Error in reconnect. %s", e) + # Can get ECONNABORTED or -1. The latter signifies no or bad CONNACK received. + self._close() # Disconnect and try again. + self._in_connect = False + self._isconnected = False + self.dprint("Disconnected, exited _keep_connected") + + async def subscribe(self, topic, qos=0): + qos_check(qos) + while 1: + await self._connection() + try: + return await super().subscribe(topic, qos) + except OSError: + pass + self._reconnect() # Broker or WiFi fail. + + async def unsubscribe(self, topic): + while 1: + await self._connection() + try: + return await super().unsubscribe(topic) + except OSError: + pass + self._reconnect() # Broker or WiFi fail. + + async def publish(self, topic, msg, retain=False, qos=0): + qos_check(qos) + while 1: + await self._connection() + try: + return await super().publish(topic, msg, retain, qos) + except OSError: + pass + self._reconnect() # Broker or WiFi fail. diff --git a/scripts/tempSensor/lib/netman.py b/scripts/tempSensor/lib/netman.py new file mode 100644 index 00000000..4c4e420f --- /dev/null +++ b/scripts/tempSensor/lib/netman.py @@ -0,0 +1,73 @@ +# .';:cc;. +# .,',;lol::c. +# ;';lddddlclo +# lcloxxoddodxdool:,. +# cxdddxdodxdkOkkkkkkkd:. +# .ldxkkOOOOkkOO000Okkxkkkkx:. +# .lddxkkOkOOO0OOO0000Okxxxxkkkk: +# 'ooddkkkxxkO0000KK00Okxdoodxkkkko +# .ooodxkkxxxOO000kkkO0KOxolooxkkxxkl +# lolodxkkxxkOx,. .lkdolodkkxxxO. +# doloodxkkkOk .... .,cxO; +# ddoodddxkkkk: ,oxxxkOdc'..o' +# :kdddxxxxd, ,lolccldxxxkkOOOkkkko, +# lOkxkkk; :xkkkkkkkkOOO000OOkkOOk. +# ;00Ok' 'O000OO0000000000OOOO0Od. +# .l0l.;OOO000000OOOOOO000000x, +# .'OKKKK00000000000000kc. +# .:ox0KKKKKKK0kdc,. +# ... +# +# Author: peppe8o +# Date: Jul 24th, 2022 +# Version: 1.0 +# https://peppe8o.com + +# modified by @sgbaird from source: +# https://peppe8o.com/getting-started-with-wifi-on-raspberry-pi-pico-w-and-micropython/ + +import time + +import network +import rp2 +from ubinascii import hexlify + + +def connectWiFi(ssid, password, country=None, wifi_energy_saver=False, retries=3): + for _ in range(retries): + try: + if country is not None: + # https://www.google.com/search?q=wifi+country+codes + rp2.country(country) + wlan = network.WLAN(network.STA_IF) + if not wifi_energy_saver: + wlan.config(pm=0xA11140) # avoid the energy-saving WiFi mode + wlan.active(True) + + mac = hexlify(network.WLAN().config("mac"), ":").decode() + print(f"MAC address: {mac}") + + wlan.connect(ssid, password) + # Wait for connect or fail + max_wait = 10 + while max_wait > 0: + if wlan.status() < 0 or wlan.status() >= 3: + break + max_wait -= 1 + print("waiting for connection...") + time.sleep(1) + + # Handle connection error + if wlan.status() != 3: + raise RuntimeError("network connection failed") + else: + print("connected") + status = wlan.ifconfig() + print("ip = " + status[0]) + return status + except RuntimeError as e: + print(f"Attempt failed with error: {e}. Retrying...") + raise RuntimeError( + "All attempts to connect to the network failed. Ensure you are using a 2.4 GHz WiFi network with WPA-2 authentication. See the additional prerequisites section from https://doi.org/10.1016/j.xpro.2023.102329 or the https://github.com/sparks-baird/self-driving-lab-demo/issues/76 for additional troubleshooting help." + ) + diff --git a/scripts/tempSensor/lib/sdcard/LICENSE b/scripts/tempSensor/lib/sdcard/LICENSE new file mode 100644 index 00000000..e3474e33 --- /dev/null +++ b/scripts/tempSensor/lib/sdcard/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2013, 2014 Damien P. George + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/scripts/tempSensor/lib/sdcard/sdcard.py b/scripts/tempSensor/lib/sdcard/sdcard.py new file mode 100644 index 00000000..2b6356b4 --- /dev/null +++ b/scripts/tempSensor/lib/sdcard/sdcard.py @@ -0,0 +1,302 @@ +""" +MicroPython driver for SD cards using SPI bus. + +Requires an SPI bus and a CS pin. Provides readblocks and writeblocks +methods so the device can be mounted as a filesystem. + +Example usage on pyboard: + + import pyb, sdcard, os + sd = sdcard.SDCard(pyb.SPI(1), pyb.Pin.board.X5) + pyb.mount(sd, '/sd2') + os.listdir('/') + +Example usage on ESP8266: + + import machine, sdcard, os + sd = sdcard.SDCard(machine.SPI(1), machine.Pin(15)) + os.mount(sd, '/sd') + os.listdir('/') + +Copied from source: https://raw.githubusercontent.com/micropython/micropython-lib/master/micropython/drivers/storage/sdcard/sdcard.py + +""" + +import time + +from micropython import const + +_CMD_TIMEOUT = const(100) + +_R1_IDLE_STATE = const(1 << 0) +# R1_ERASE_RESET = const(1 << 1) +_R1_ILLEGAL_COMMAND = const(1 << 2) +# R1_COM_CRC_ERROR = const(1 << 3) +# R1_ERASE_SEQUENCE_ERROR = const(1 << 4) +# R1_ADDRESS_ERROR = const(1 << 5) +# R1_PARAMETER_ERROR = const(1 << 6) +_TOKEN_CMD25 = const(0xFC) +_TOKEN_STOP_TRAN = const(0xFD) +_TOKEN_DATA = const(0xFE) + + +class SDCard: + def __init__(self, spi, cs, baudrate=1320000): + self.spi = spi + self.cs = cs + + self.cmdbuf = bytearray(6) + self.dummybuf = bytearray(512) + self.tokenbuf = bytearray(1) + for i in range(512): + self.dummybuf[i] = 0xFF + self.dummybuf_memoryview = memoryview(self.dummybuf) + + # initialise the card + self.init_card(baudrate) + + def init_spi(self, baudrate): + try: + master = self.spi.MASTER + except AttributeError: + # on ESP8266 + self.spi.init(baudrate=baudrate, phase=0, polarity=0) + else: + # on pyboard + self.spi.init(master, baudrate=baudrate, phase=0, polarity=0) + + def init_card(self, baudrate): + # init CS pin + self.cs.init(self.cs.OUT, value=1) + + # init SPI bus; use low data rate for initialisation + self.init_spi(100000) + + # clock card at least 100 cycles with cs high + for i in range(16): + self.spi.write(b"\xff") + + # CMD0: init card; should return _R1_IDLE_STATE (allow 5 attempts) + for _ in range(5): + if self.cmd(0, 0, 0x95) == _R1_IDLE_STATE: + break + else: + raise OSError("no SD card") + + # CMD8: determine card version + r = self.cmd(8, 0x01AA, 0x87, 4) + if r == _R1_IDLE_STATE: + self.init_card_v2() + elif r == (_R1_IDLE_STATE | _R1_ILLEGAL_COMMAND): + self.init_card_v1() + else: + raise OSError("couldn't determine SD card version") + + # get the number of sectors + # CMD9: response R2 (R1 byte + 16-byte block read) + if self.cmd(9, 0, 0, 0, False) != 0: + raise OSError("no response from SD card") + csd = bytearray(16) + self.readinto(csd) + if csd[0] & 0xC0 == 0x40: # CSD version 2.0 + self.sectors = ((csd[8] << 8 | csd[9]) + 1) * 1024 + elif csd[0] & 0xC0 == 0x00: # CSD version 1.0 (old, <=2GB) + c_size = (csd[6] & 0b11) << 10 | csd[7] << 2 | csd[8] >> 6 + c_size_mult = (csd[9] & 0b11) << 1 | csd[10] >> 7 + read_bl_len = csd[5] & 0b1111 + capacity = (c_size + 1) * (2 ** (c_size_mult + 2)) * (2**read_bl_len) + self.sectors = capacity // 512 + else: + raise OSError("SD card CSD format not supported") + # print('sectors', self.sectors) + + # CMD16: set block length to 512 bytes + if self.cmd(16, 512, 0) != 0: + raise OSError("can't set 512 block size") + + # set to high data rate now that it's initialised + self.init_spi(baudrate) + + def init_card_v1(self): + for i in range(_CMD_TIMEOUT): + time.sleep_ms(50) + self.cmd(55, 0, 0) + if self.cmd(41, 0, 0) == 0: + # SDSC card, uses byte addressing in read/write/erase commands + self.cdv = 512 + # print("[SDCard] v1 card") + return + raise OSError("timeout waiting for v1 card") + + def init_card_v2(self): + for i in range(_CMD_TIMEOUT): + time.sleep_ms(50) + self.cmd(58, 0, 0, 4) + self.cmd(55, 0, 0) + if self.cmd(41, 0x40000000, 0) == 0: + self.cmd( + 58, 0, 0, -4 + ) # 4-byte response, negative means keep the first byte + ocr = self.tokenbuf[0] # get first byte of response, which is OCR + if not ocr & 0x40: + # SDSC card, uses byte addressing in read/write/erase commands + self.cdv = 512 + else: + # SDHC/SDXC card, uses block addressing in read/write/erase commands + self.cdv = 1 + # print("[SDCard] v2 card") + return + raise OSError("timeout waiting for v2 card") + + def cmd(self, cmd, arg, crc, final=0, release=True, skip1=False): + self.cs(0) + + # create and send the command + buf = self.cmdbuf + buf[0] = 0x40 | cmd + buf[1] = arg >> 24 + buf[2] = arg >> 16 + buf[3] = arg >> 8 + buf[4] = arg + buf[5] = crc + self.spi.write(buf) + + if skip1: + self.spi.readinto(self.tokenbuf, 0xFF) + + # wait for the response (response[7] == 0) + for i in range(_CMD_TIMEOUT): + self.spi.readinto(self.tokenbuf, 0xFF) + response = self.tokenbuf[0] + if not (response & 0x80): + # this could be a big-endian integer that we are getting here + # if final<0 then store the first byte to tokenbuf and discard the rest + if final < 0: + self.spi.readinto(self.tokenbuf, 0xFF) + final = -1 - final + for j in range(final): + self.spi.write(b"\xff") + if release: + self.cs(1) + self.spi.write(b"\xff") + return response + + # timeout + self.cs(1) + self.spi.write(b"\xff") + return -1 + + def readinto(self, buf): + self.cs(0) + + # read until start byte (0xff) + for i in range(_CMD_TIMEOUT): + self.spi.readinto(self.tokenbuf, 0xFF) + if self.tokenbuf[0] == _TOKEN_DATA: + break + time.sleep_ms(1) + else: + self.cs(1) + raise OSError("timeout waiting for response") + + # read data + mv = self.dummybuf_memoryview + if len(buf) != len(mv): + mv = mv[: len(buf)] + self.spi.write_readinto(mv, buf) + + # read checksum + self.spi.write(b"\xff") + self.spi.write(b"\xff") + + self.cs(1) + self.spi.write(b"\xff") + + def write(self, token, buf): + self.cs(0) + + # send: start of block, data, checksum + self.spi.read(1, token) + self.spi.write(buf) + self.spi.write(b"\xff") + self.spi.write(b"\xff") + + # check the response + if (self.spi.read(1, 0xFF)[0] & 0x1F) != 0x05: + self.cs(1) + self.spi.write(b"\xff") + return + + # wait for write to finish + while self.spi.read(1, 0xFF)[0] == 0: + pass + + self.cs(1) + self.spi.write(b"\xff") + + def write_token(self, token): + self.cs(0) + self.spi.read(1, token) + self.spi.write(b"\xff") + # wait for write to finish + while self.spi.read(1, 0xFF)[0] == 0x00: + pass + + self.cs(1) + self.spi.write(b"\xff") + + def readblocks(self, block_num, buf): + nblocks = len(buf) // 512 + assert nblocks and not len(buf) % 512, "Buffer length is invalid" + if nblocks == 1: + # CMD17: set read address for single block + if self.cmd(17, block_num * self.cdv, 0, release=False) != 0: + # release the card + self.cs(1) + raise OSError(5) # EIO + # receive the data and release card + self.readinto(buf) + else: + # CMD18: set read address for multiple blocks + if self.cmd(18, block_num * self.cdv, 0, release=False) != 0: + # release the card + self.cs(1) + raise OSError(5) # EIO + offset = 0 + mv = memoryview(buf) + while nblocks: + # receive the data and release card + self.readinto(mv[offset : offset + 512]) + offset += 512 + nblocks -= 1 + if self.cmd(12, 0, 0xFF, skip1=True): + raise OSError(5) # EIO + + def writeblocks(self, block_num, buf): + nblocks, err = divmod(len(buf), 512) + assert nblocks and not err, "Buffer length is invalid" + if nblocks == 1: + # CMD24: set write address for single block + if self.cmd(24, block_num * self.cdv, 0) != 0: + raise OSError(5) # EIO + + # send the data + self.write(_TOKEN_DATA, buf) + else: + # CMD25: set write address for first block + if self.cmd(25, block_num * self.cdv, 0) != 0: + raise OSError(5) # EIO + # send the data + offset = 0 + mv = memoryview(buf) + while nblocks: + self.write(_TOKEN_CMD25, mv[offset : offset + 512]) + offset += 512 + nblocks -= 1 + self.write_token(_TOKEN_STOP_TRAN) + + def ioctl(self, op, arg): + if op == 4: # get number of blocks + return self.sectors + if op == 5: # get block size in bytes + return 512 diff --git a/scripts/tempSensor/lib/sdl_demo_utils.py b/scripts/tempSensor/lib/sdl_demo_utils.py new file mode 100644 index 00000000..009bbeed --- /dev/null +++ b/scripts/tempSensor/lib/sdl_demo_utils.py @@ -0,0 +1,276 @@ +import json +import sys +from time import localtime, sleep, ticks_diff, ticks_ms # type: ignore + +import uos +from data_logging import ( + get_local_timestamp, + get_onboard_temperature, + write_payload_backup, +) +from machine import PWM, Pin +from ufastrsa.genprime import genrsa +from ufastrsa.rsa import RSA +from uio import StringIO + + +def beep(buzzer, power=0.005): + buzzer.freq(300) + buzzer.duty_u16(round(65535 * power)) + sleep(0.15) + buzzer.duty_u16(0) + + +def get_traceback(err): + try: + with StringIO() as f: # type: ignore + sys.print_exception(err, f) + return f.getvalue() + except Exception as err2: + print(err2) + return f"Failed to extract file and line number due to {err2}.\nOriginal error: {err}" # noqa: E501 + + +def merge_two_dicts(x, y): + z = x.copy() # start with keys and values of x + z.update(y) # modifies z with keys and values of y + return z + + +def path_exists(path): + # Check if path exists. + # Works for relative and absolute path. + parent = "" # parent folder name + name = path # name of file/folder + + # Check if file/folder has a parent folder + index = path.rstrip("/").rfind("/") + if index >= 0: + index += 1 + parent = path[: index - 1] + name = path[index:] + + # Searching with iterator is more efficient if the parent contains lost of files/folders + # return name in uos.listdir(parent) + return any((name == x[0]) for x in uos.ilistdir(parent)) + + +def encrypt_id(my_id, verbose=False): + rsa_path = "rsa.json" + # if path_exists(rsa_path): + try: + with open(rsa_path, "r") as f: + cipher_data = json.load(f) + cipher = RSA( + cipher_data["bits"], + n=cipher_data["n"], + e=cipher_data["e"], + d=cipher_data["d"], + ) + except (KeyError, OSError) as e: + print(e) + print("Generating new RSA parameters...") + bits = 256 + bits, n, e, d = genrsa(bits, e=65537) # type: ignore + cipher = RSA(bits, n=n, e=e, d=d) + with open("rsa.json", "w") as f: + json.dump(dict(bits=bits, n=n, e=e, d=d), f) + + if verbose: + with open(rsa_path, "r") as f: + cipher_data = json.load(f) + print("RSA parameters (keep private):") + print(cipher_data) + + my_id = int.from_bytes(cipher.pkcs_encrypt(my_id), "big") + return my_id + + +def decrypt_id(my_id): + rsa_path = "rsa.json" + if path_exists(rsa_path): + with open(rsa_path, "r") as f: + cipher_data = json.load(f) + cipher = RSA( + cipher_data["bits"], + n=cipher_data["n"], + e=cipher_data["e"], + d=cipher_data["d"], + ) + else: + bits = 256 + bits, n, e, d = genrsa(bits, e=65537) # type: ignore + cipher = RSA(bits, n=n, e=e, d=d) + with open("rsa.json", "w") as f: + json.dump(dict(bits=bits, n=n, e=e, d=d), f) + + my_id = int.from_bytes(cipher.pkcs_decrypt(my_id), "big") + return my_id + + +def get_onboard_led(): + try: + onboard_led = Pin("LED", Pin.OUT) # only works for Pico W + except Exception as e: + print(e) + onboard_led = Pin(25, Pin.OUT) + return onboard_led + + +class Experiment(object): + def __init__( + self, + run_experiment_fn, + devices, + reset_experiment_fn=None, + validate_inputs_fn=None, + emergency_shutdown_fn=None, + buzzer=None, + sdcard_ready=False, + ) -> None: + self.validate_inputs_fn = validate_inputs_fn + self.run_experiment_fn = run_experiment_fn + self.reset_experiment_fn = reset_experiment_fn + self.devices = devices + self.emergency_shutdown_fn = emergency_shutdown_fn + self.buzzer = buzzer + self.sdcard_ready = sdcard_ready + + if self.reset_experiment_fn is None: + + def do_nothing(*args, **kwargs): + pass + + self.reset_experiment_fn = do_nothing + + if self.emergency_shutdown_fn is None: + self.emergency_shutdown_fn = self.reset_experiment_fn + + if self.validate_inputs_fn is None: + + def no_input_validation(*args, **kwargs): + return True + + self.validate_inputs_fn = no_input_validation + + if self.buzzer is None: + self.buzzer = PWM(Pin(18)) + + def try_experiment(self, msg): + payload_data = {} + # # pin numbers not used here, but can help with organization for complex tasks + # p = int(t[5:]) # pin number + + print(msg) + + # careful not to throw an unrecoverable error due to bad request + # Perform the experiment and record the results + try: + parameters = json.loads(msg) + payload_data["_input_message"] = parameters + + # don't allow access to hardware if any input values are out of bounds + self.validate_inputs_fn(parameters) # type: ignore + + beep(self.buzzer) + sensor_data = self.run_experiment_fn(parameters, self.devices) + payload_data = merge_two_dicts(payload_data, sensor_data) + + except Exception as err: + print(err) + if "_input_message" not in payload_data.keys(): + payload_data["_input_message"] = msg + payload_data["error"] = get_traceback(err) + + try: + payload_data["onboard_temperature_K"] = get_onboard_temperature(unit="K") + payload_data["sd_card_ready"] = self.sdcard_ready + stamp, time_str = get_local_timestamp(return_str=True) # type: ignore + payload_data["utc_timestamp"] = stamp + payload_data["utc_time_str"] = time_str + except OverflowError as e: + print(get_traceback(e)) + except Exception as e: + print(get_traceback(e)) + + try: + parameters = json.loads(msg) + self.reset_experiment_fn(parameters, devices=self.devices) # type: ignore + except Exception as e: + try: + self.emergency_shutdown_fn(devices=self.devices) # type: ignore + payload_data["reset_error"] = get_traceback(e) + except Exception as e: + payload_data["emergency_error"] = get_traceback(e) + + return payload_data + + def write_to_sd_card(self, payload_data, fpath="/sd/experiments.txt"): + try: + write_payload_backup(payload_data, fpath=fpath) + except Exception as e: + w = f"Failed to write to SD card: {get_traceback(e)}" + print(w) + payload_data["warning"] = w + + return payload_data + + # def log_to_mongodb( + # self, + # payload_data, + # api_key: str, + # url: str, + # cluster_name: str, + # database_name: str, + # collection_name: str, + # verbose: bool = True, + # retries: int = 2, + # ): + # try: + # log_to_mongodb( + # payload_data, + # url=url, + # api_key=api_key, + # cluster_name=cluster_name, + # database_name=database_name, + # collection_name=collection_name, + # verbose=verbose, + # retries=retries, + # ) + # except Exception as e: + # print(f"Failed to log to MongoDB backend: {get_traceback(e)}") + + +def heartbeat(client, first, ping_interval_ms=15000): + global lastping + if first: + client.ping() + lastping = ticks_ms() + if ticks_diff(ticks_ms(), lastping) >= ping_interval_ms: + client.ping() + lastping = ticks_ms() + return + + +def sign_of_life(led, first, blink_interval_ms=5000): + global last_blink + if first: + led.on() + last_blink = ticks_ms() + time_since = ticks_diff(ticks_ms(), last_blink) + if led.value() == 0 and time_since >= blink_interval_ms: + led.toggle() + last_blink = ticks_ms() + elif led.value() == 1 and time_since >= 500: + led.toggle() + last_blink = ticks_ms() + + +class DummyMotor: + def __init__(self): + pass + + +class DummySensor: + def __init__(self): + pass diff --git a/scripts/tempSensor/lib/smbus2-0.5.0.dist-info/METADATA b/scripts/tempSensor/lib/smbus2-0.5.0.dist-info/METADATA new file mode 100644 index 00000000..2226ef2e --- /dev/null +++ b/scripts/tempSensor/lib/smbus2-0.5.0.dist-info/METADATA @@ -0,0 +1,234 @@ +Metadata-Version: 2.1 +Name: smbus2 +Version: 0.5.0 +Summary: smbus2 is a drop-in replacement for smbus-cffi/smbus-python in pure Python +Home-page: https://github.com/kplindegaard/smbus2 +Author: Karl-Petter Lindegaard +Author-email: kp.lindegaard@gmail.com +License: MIT +Keywords: smbus,smbus2,python,i2c,raspberrypi,linux +Classifier: Development Status :: 4 - Beta +Classifier: Topic :: Utilities +Classifier: License :: OSI Approved :: MIT License +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Description-Content-Type: text/markdown +License-File: LICENSE +Provides-Extra: docs +Requires-Dist: sphinx>=1.5.3; extra == "docs" +Provides-Extra: qa +Requires-Dist: flake8; extra == "qa" + +# smbus2 +A drop-in replacement for smbus-cffi/smbus-python in pure Python + +[![Build Status](https://github.com/kplindegaard/smbus2/actions/workflows/python-build-test.yml/badge.svg?branch=master)](https://github.com/kplindegaard/smbus2/actions/workflows/python-build-test.yml) +[![Documentation Status](https://readthedocs.org/projects/smbus2/badge/?version=latest)](http://smbus2.readthedocs.io/en/latest/?badge=latest) +![CodeQL](https://github.com/kplindegaard/smbus2/actions/workflows/codeql-analysis.yml/badge.svg?branch=master) +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=kplindegaard_smbus2&metric=alert_status)](https://sonarcloud.io/dashboard?id=kplindegaard_smbus2) + +![Python Verions](https://img.shields.io/pypi/pyversions/smbus2.svg) +[![PyPi Version](https://img.shields.io/pypi/v/smbus2.svg)](https://pypi.org/project/smbus2/) +[![PyPI - Downloads](https://img.shields.io/pypi/dm/smbus2)](https://pypi.org/project/smbus2/) + +# Introduction + +smbus2 is (yet another) pure Python implementation of the [python-smbus](http://www.lm-sensors.org/browser/i2c-tools/trunk/py-smbus/) package. + +It was designed from the ground up with two goals in mind: + +1. It should be a drop-in replacement of smbus. The syntax shall be the same. +2. Use the inherent i2c structs and unions to a greater extent than other pure Python implementations like [pysmbus](https://github.com/bjornt/pysmbus) does. By doing so, it will be more feature complete and easier to extend. + +Currently supported features are: + +* Get i2c capabilities (I2C_FUNCS) +* SMBus Packet Error Checking (PEC) support +* read_byte +* write_byte +* read_byte_data +* write_byte_data +* read_word_data +* write_word_data +* read_i2c_block_data +* write_i2c_block_data +* write_quick +* process_call +* read_block_data +* write_block_data +* block_process_call +* i2c_rdwr - *combined write/read transactions with repeated start* + +It is developed on Python 2.7 but works without any modifications in Python 3.X too. + +More information about updates and general changes are recorded in the [change log](https://github.com/kplindegaard/smbus2/blob/master/CHANGELOG.md). + +# SMBus code examples + +smbus2 installs next to smbus as the package, so it's not really a 100% replacement. You must change the module name. + +## Example 1a: Read a byte + +```python +from smbus2 import SMBus + +# Open i2c bus 1 and read one byte from address 80, offset 0 +bus = SMBus(1) +b = bus.read_byte_data(80, 0) +print(b) +bus.close() +``` + +## Example 1b: Read a byte using 'with' + +This is the very same example but safer to use since the smbus will be closed automatically when exiting the with block. + +```python +from smbus2 import SMBus + +with SMBus(1) as bus: + b = bus.read_byte_data(80, 0) + print(b) +``` + +## Example 1c: Read a byte with PEC enabled + +Same example with Packet Error Checking enabled. + +```python +from smbus2 import SMBus + +with SMBus(1) as bus: + bus.pec = 1 # Enable PEC + b = bus.read_byte_data(80, 0) + print(b) +``` + +## Example 2: Read a block of data + +You can read up to 32 bytes at once. + +```python +from smbus2 import SMBus + +with SMBus(1) as bus: + # Read a block of 16 bytes from address 80, offset 0 + block = bus.read_i2c_block_data(80, 0, 16) + # Returned value is a list of 16 bytes + print(block) +``` + +## Example 3: Write a byte + +```python +from smbus2 import SMBus + +with SMBus(1) as bus: + # Write a byte to address 80, offset 0 + data = 45 + bus.write_byte_data(80, 0, data) +``` + +## Example 4: Write a block of data + +It is possible to write 32 bytes at the time, but I have found that error-prone. Write less and add a delay in between if you run into trouble. + +```python +from smbus2 import SMBus + +with SMBus(1) as bus: + # Write a block of 8 bytes to address 80 from offset 0 + data = [1, 2, 3, 4, 5, 6, 7, 8] + bus.write_i2c_block_data(80, 0, data) +``` + +# I2C + +Starting with v0.2, the smbus2 library also has support for combined read and write transactions. *i2c_rdwr* is not really a SMBus feature but comes in handy when the master needs to: + +1. read or write bulks of data larger than SMBus' 32 bytes limit. +1. write some data and then read from the slave with a repeated start and no stop bit between. + +Each operation is represented by a *i2c_msg* message object. + + +## Example 5: Single i2c_rdwr + +```python +from smbus2 import SMBus, i2c_msg + +with SMBus(1) as bus: + # Read 64 bytes from address 80 + msg = i2c_msg.read(80, 64) + bus.i2c_rdwr(msg) + + # Write a single byte to address 80 + msg = i2c_msg.write(80, [65]) + bus.i2c_rdwr(msg) + + # Write some bytes to address 80 + msg = i2c_msg.write(80, [65, 66, 67, 68]) + bus.i2c_rdwr(msg) +``` + +## Example 6: Dual i2c_rdwr + +To perform dual operations just add more i2c_msg instances to the bus call: + +```python +from smbus2 import SMBus, i2c_msg + +# Single transaction writing two bytes then read two at address 80 +write = i2c_msg.write(80, [40, 50]) +read = i2c_msg.read(80, 2) +with SMBus(1) as bus: + bus.i2c_rdwr(write, read) +``` + +## Example 7: Access i2c_msg data + +All data is contained in the i2c_msg instances. Here are some data access alternatives. + +```python +# 1: Convert message content to list +msg = i2c_msg.write(60, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) +data = list(msg) # data = [1, 2, 3, ...] +print(len(data)) # => 10 + +# 2: i2c_msg is iterable +for value in msg: + print(value) + +# 3: Through i2c_msg properties +for k in range(msg.len): + print(msg.buf[k]) +``` + +# Installation instructions + +From [PyPi](https://pypi.org/) with `pip`: + +``` +pip install smbus2 +``` + +From [conda-forge](https://anaconda.org/conda-forge) using `conda`: + +``` +conda install -c conda-forge smbus2 +``` + +Installation from source code is straight forward: + +``` +python setup.py install +``` diff --git a/scripts/tempSensor/lib/smbus2-0.5.0.dist-info/RECORD b/scripts/tempSensor/lib/smbus2-0.5.0.dist-info/RECORD new file mode 100644 index 00000000..7a53a382 --- /dev/null +++ b/scripts/tempSensor/lib/smbus2-0.5.0.dist-info/RECORD @@ -0,0 +1,6 @@ +smbus2-0.5.0.dist-info/METADATA,, +smbus2/__init__.py,, +smbus2/py.typed,, +smbus2/smbus2.py,, +smbus2/smbus2.pyi,, +smbus2-0.5.0.dist-info/RECORD,, \ No newline at end of file diff --git a/scripts/tempSensor/lib/smbus2/__init__.py b/scripts/tempSensor/lib/smbus2/__init__.py new file mode 100644 index 00000000..f7f465eb --- /dev/null +++ b/scripts/tempSensor/lib/smbus2/__init__.py @@ -0,0 +1,26 @@ +"""smbus2 - A drop-in replacement for smbus-cffi/smbus-python""" +# The MIT License (MIT) +# Copyright (c) 2020 Karl-Petter Lindegaard +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from .smbus2 import SMBus, i2c_msg, I2cFunc # noqa: F401 + +__version__ = "0.5.0" +__all__ = ["SMBus", "i2c_msg", "I2cFunc"] diff --git a/scripts/tempSensor/lib/smbus2/py.typed b/scripts/tempSensor/lib/smbus2/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/scripts/tempSensor/lib/smbus2/smbus2.py b/scripts/tempSensor/lib/smbus2/smbus2.py new file mode 100644 index 00000000..a35868f5 --- /dev/null +++ b/scripts/tempSensor/lib/smbus2/smbus2.py @@ -0,0 +1,660 @@ +"""smbus2 - A drop-in replacement for smbus-cffi/smbus-python""" +# The MIT License (MIT) +# Copyright (c) 2020 Karl-Petter Lindegaard +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import os +import sys +from fcntl import ioctl +from ctypes import c_uint32, c_uint8, c_uint16, c_char, POINTER, Structure, Array, Union, create_string_buffer, string_at + + +# Commands from uapi/linux/i2c-dev.h +I2C_SLAVE = 0x0703 # Use this slave address +I2C_SLAVE_FORCE = 0x0706 # Use this slave address, even if it is already in use by a driver! +I2C_FUNCS = 0x0705 # Get the adapter functionality mask +I2C_RDWR = 0x0707 # Combined R/W transfer (one STOP only) +I2C_SMBUS = 0x0720 # SMBus transfer. Takes pointer to i2c_smbus_ioctl_data +I2C_PEC = 0x0708 # != 0 to use PEC with SMBus + +# SMBus transfer read or write markers from uapi/linux/i2c.h +I2C_SMBUS_WRITE = 0 +I2C_SMBUS_READ = 1 + +# Size identifiers uapi/linux/i2c.h +I2C_SMBUS_QUICK = 0 +I2C_SMBUS_BYTE = 1 +I2C_SMBUS_BYTE_DATA = 2 +I2C_SMBUS_WORD_DATA = 3 +I2C_SMBUS_PROC_CALL = 4 +I2C_SMBUS_BLOCK_DATA = 5 # This isn't supported by Pure-I2C drivers with SMBUS emulation, like those in RaspberryPi, OrangePi, etc :( +I2C_SMBUS_BLOCK_PROC_CALL = 7 # Like I2C_SMBUS_BLOCK_DATA, it isn't supported by Pure-I2C drivers either. +I2C_SMBUS_I2C_BLOCK_DATA = 8 +I2C_SMBUS_BLOCK_MAX = 32 + +# To determine what functionality is present (uapi/linux/i2c.h) +try: + from enum import IntFlag +except ImportError: + IntFlag = int + + +class I2cFunc(IntFlag): + """ + These flags identify the operations supported by an I2C/SMBus device. + + You can test these flags on your `smbus.funcs` + + On newer python versions, I2cFunc is an IntFlag enum, but it + falls back to class with a bunch of int constants on older releases. + """ + I2C = 0x00000001 + ADDR_10BIT = 0x00000002 + PROTOCOL_MANGLING = 0x00000004 # I2C_M_IGNORE_NAK etc. + SMBUS_PEC = 0x00000008 + NOSTART = 0x00000010 # I2C_M_NOSTART + SLAVE = 0x00000020 + SMBUS_BLOCK_PROC_CALL = 0x00008000 # SMBus 2.0 + SMBUS_QUICK = 0x00010000 + SMBUS_READ_BYTE = 0x00020000 + SMBUS_WRITE_BYTE = 0x00040000 + SMBUS_READ_BYTE_DATA = 0x00080000 + SMBUS_WRITE_BYTE_DATA = 0x00100000 + SMBUS_READ_WORD_DATA = 0x00200000 + SMBUS_WRITE_WORD_DATA = 0x00400000 + SMBUS_PROC_CALL = 0x00800000 + SMBUS_READ_BLOCK_DATA = 0x01000000 + SMBUS_WRITE_BLOCK_DATA = 0x02000000 + SMBUS_READ_I2C_BLOCK = 0x04000000 # I2C-like block xfer + SMBUS_WRITE_I2C_BLOCK = 0x08000000 # w/ 1-byte reg. addr. + SMBUS_HOST_NOTIFY = 0x10000000 + + SMBUS_BYTE = 0x00060000 + SMBUS_BYTE_DATA = 0x00180000 + SMBUS_WORD_DATA = 0x00600000 + SMBUS_BLOCK_DATA = 0x03000000 + SMBUS_I2C_BLOCK = 0x0c000000 + SMBUS_EMUL = 0x0eff0008 + + +# i2c_msg flags from uapi/linux/i2c.h +I2C_M_RD = 0x0001 + +# Pointer definitions +LP_c_uint8 = POINTER(c_uint8) +LP_c_uint16 = POINTER(c_uint16) +LP_c_uint32 = POINTER(c_uint32) + + +############################################################# +# Type definitions as in i2c.h + + +class i2c_smbus_data(Array): + """ + Adaptation of the i2c_smbus_data union in ``i2c.h``. + + Data for SMBus messages. + """ + _length_ = I2C_SMBUS_BLOCK_MAX + 2 + _type_ = c_uint8 + + +class union_i2c_smbus_data(Union): + _fields_ = [ + ("byte", c_uint8), + ("word", c_uint16), + ("block", i2c_smbus_data) + ] + + +union_pointer_type = POINTER(union_i2c_smbus_data) + + +class i2c_smbus_ioctl_data(Structure): + """ + As defined in ``i2c-dev.h``. + """ + _fields_ = [ + ('read_write', c_uint8), + ('command', c_uint8), + ('size', c_uint32), + ('data', union_pointer_type)] + __slots__ = [name for name, type in _fields_] + + @staticmethod + def create(read_write=I2C_SMBUS_READ, command=0, size=I2C_SMBUS_BYTE_DATA): + u = union_i2c_smbus_data() + return i2c_smbus_ioctl_data( + read_write=read_write, command=command, size=size, + data=union_pointer_type(u)) + + +############################################################# +# Type definitions for i2c_rdwr combined transactions + + +class i2c_msg(Structure): + """ + As defined in ``i2c.h``. + """ + _fields_ = [ + ('addr', c_uint16), + ('flags', c_uint16), + ('len', c_uint16), + ('buf', POINTER(c_char))] + + def __iter__(self): + """ Iterator / Generator + + :return: iterates over :py:attr:`buf` + :rtype: :py:class:`generator` which returns int values + """ + idx = 0 + while idx < self.len: + yield ord(self.buf[idx]) + idx += 1 + + def __len__(self): + return self.len + + def __bytes__(self): + return string_at(self.buf, self.len) + + def __repr__(self): + return 'i2c_msg(%d,%d,%r)' % (self.addr, self.flags, self.__bytes__()) + + def __str__(self): + s = self.__bytes__() + # Throw away non-decodable bytes + s = s.decode(errors="ignore") + return s + + @staticmethod + def read(address, length): + """ + Prepares an i2c read transaction. + + :param address: Slave address. + :type address: int + :param length: Number of bytes to read. + :type length: int + :return: New :py:class:`i2c_msg` instance for read operation. + :rtype: :py:class:`i2c_msg` + """ + arr = create_string_buffer(length) + return i2c_msg( + addr=address, flags=I2C_M_RD, len=length, + buf=arr) + + @staticmethod + def write(address, buf): + """ + Prepares an i2c write transaction. + + :param address: Slave address. + :type address: int + :param buf: Bytes to write. Either list of values or str. + :type buf: list + :return: New :py:class:`i2c_msg` instance for write operation. + :rtype: :py:class:`i2c_msg` + """ + if sys.version_info.major >= 3: + if type(buf) is str: + buf = bytes(map(ord, buf)) + else: + buf = bytes(buf) + else: + if type(buf) is not str: + buf = ''.join([chr(x) for x in buf]) + arr = create_string_buffer(buf, len(buf)) + return i2c_msg( + addr=address, flags=0, len=len(arr), + buf=arr) + + +class i2c_rdwr_ioctl_data(Structure): + """ + As defined in ``i2c-dev.h``. + """ + _fields_ = [ + ('msgs', POINTER(i2c_msg)), + ('nmsgs', c_uint32) + ] + __slots__ = [name for name, type in _fields_] + + @staticmethod + def create(*i2c_msg_instances): + """ + Factory method for creating a i2c_rdwr_ioctl_data struct that can + be called with ``ioctl(fd, I2C_RDWR, data)``. + + :param i2c_msg_instances: Up to 42 i2c_msg instances + :rtype: i2c_rdwr_ioctl_data + """ + n_msg = len(i2c_msg_instances) + msg_array = (i2c_msg * n_msg)(*i2c_msg_instances) + return i2c_rdwr_ioctl_data( + msgs=msg_array, + nmsgs=n_msg + ) + + +############################################################# + + +class SMBus(object): + + def __init__(self, bus=None, force=False): + """ + Initialize and (optionally) open an i2c bus connection. + + :param bus: i2c bus number (e.g. 0 or 1) + or an absolute file path (e.g. `/dev/i2c-42`). + If not given, a subsequent call to ``open()`` is required. + :type bus: int or str + :param force: force using the slave address even when driver is + already using it. + :type force: boolean + """ + self.fd = None + self.funcs = I2cFunc(0) + if bus is not None: + self.open(bus) + self.address = None + self.force = force + self._force_last = None + self._pec = 0 + + def __enter__(self): + """Enter handler.""" + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """Exit handler.""" + self.close() + + def open(self, bus): + """ + Open a given i2c bus. + + :param bus: i2c bus number (e.g. 0 or 1) + or an absolute file path (e.g. '/dev/i2c-42'). + :type bus: int or str + :raise TypeError: if type(bus) is not in (int, str) + """ + if isinstance(bus, int): + filepath = "/dev/i2c-{}".format(bus) + elif isinstance(bus, str): + filepath = bus + else: + raise TypeError("Unexpected type(bus)={}".format(type(bus))) + + self.fd = os.open(filepath, os.O_RDWR) + self.funcs = self._get_funcs() + + def close(self): + """ + Close the i2c connection. + """ + if self.fd: + os.close(self.fd) + self.fd = None + self._pec = 0 + self.address = None + self._force_last = None + + def _get_pec(self): + return self._pec + + def enable_pec(self, enable=True): + """ + Enable/Disable PEC (Packet Error Checking) - SMBus 1.1 and later + + :param enable: + :type enable: Boolean + """ + if not (self.funcs & I2cFunc.SMBUS_PEC): + raise IOError('SMBUS_PEC is not a feature') + self._pec = int(enable) + ioctl(self.fd, I2C_PEC, self._pec) + + pec = property(_get_pec, enable_pec) # Drop-in replacement for smbus member "pec" + """Get and set SMBus PEC. 0 = disabled (default), 1 = enabled.""" + + def _set_address(self, address, force=None): + """ + Set i2c slave address to use for subsequent calls. + + :param address: + :type address: int + :param force: + :type force: Boolean + """ + force = force if force is not None else self.force + if self.address != address or self._force_last != force: + if force is True: + ioctl(self.fd, I2C_SLAVE_FORCE, address) + else: + ioctl(self.fd, I2C_SLAVE, address) + self.address = address + self._force_last = force + + def _get_funcs(self): + """ + Returns a 32-bit value stating supported I2C functions. + + :rtype: int + """ + f = c_uint32() + ioctl(self.fd, I2C_FUNCS, f) + return f.value + + def write_quick(self, i2c_addr, force=None): + """ + Perform quick transaction. Throws IOError if unsuccessful. + :param i2c_addr: i2c address + :type i2c_addr: int + :param force: + :type force: Boolean + """ + self._set_address(i2c_addr, force=force) + msg = i2c_smbus_ioctl_data.create( + read_write=I2C_SMBUS_WRITE, command=0, size=I2C_SMBUS_QUICK) + ioctl(self.fd, I2C_SMBUS, msg) + + def read_byte(self, i2c_addr, force=None): + """ + Read a single byte from a device. + + :rtype: int + :param i2c_addr: i2c address + :type i2c_addr: int + :param force: + :type force: Boolean + :return: Read byte value + """ + self._set_address(i2c_addr, force=force) + msg = i2c_smbus_ioctl_data.create( + read_write=I2C_SMBUS_READ, command=0, size=I2C_SMBUS_BYTE + ) + ioctl(self.fd, I2C_SMBUS, msg) + return msg.data.contents.byte + + def write_byte(self, i2c_addr, value, force=None): + """ + Write a single byte to a device. + + :param i2c_addr: i2c address + :type i2c_addr: int + :param value: value to write + :type value: int + :param force: + :type force: Boolean + """ + self._set_address(i2c_addr, force=force) + msg = i2c_smbus_ioctl_data.create( + read_write=I2C_SMBUS_WRITE, command=value, size=I2C_SMBUS_BYTE + ) + ioctl(self.fd, I2C_SMBUS, msg) + + def read_byte_data(self, i2c_addr, register, force=None): + """ + Read a single byte from a designated register. + + :param i2c_addr: i2c address + :type i2c_addr: int + :param register: Register to read + :type register: int + :param force: + :type force: Boolean + :return: Read byte value + :rtype: int + """ + self._set_address(i2c_addr, force=force) + msg = i2c_smbus_ioctl_data.create( + read_write=I2C_SMBUS_READ, command=register, size=I2C_SMBUS_BYTE_DATA + ) + ioctl(self.fd, I2C_SMBUS, msg) + return msg.data.contents.byte + + def write_byte_data(self, i2c_addr, register, value, force=None): + """ + Write a byte to a given register. + + :param i2c_addr: i2c address + :type i2c_addr: int + :param register: Register to write to + :type register: int + :param value: Byte value to transmit + :type value: int + :param force: + :type force: Boolean + :rtype: None + """ + self._set_address(i2c_addr, force=force) + msg = i2c_smbus_ioctl_data.create( + read_write=I2C_SMBUS_WRITE, command=register, size=I2C_SMBUS_BYTE_DATA + ) + msg.data.contents.byte = value + ioctl(self.fd, I2C_SMBUS, msg) + + def read_word_data(self, i2c_addr, register, force=None): + """ + Read a single word (2 bytes) from a given register. + + :param i2c_addr: i2c address + :type i2c_addr: int + :param register: Register to read + :type register: int + :param force: + :type force: Boolean + :return: 2-byte word + :rtype: int + """ + self._set_address(i2c_addr, force=force) + msg = i2c_smbus_ioctl_data.create( + read_write=I2C_SMBUS_READ, command=register, size=I2C_SMBUS_WORD_DATA + ) + ioctl(self.fd, I2C_SMBUS, msg) + return msg.data.contents.word + + def write_word_data(self, i2c_addr, register, value, force=None): + """ + Write a single word (2 bytes) to a given register. + + :param i2c_addr: i2c address + :type i2c_addr: int + :param register: Register to write to + :type register: int + :param value: Word value to transmit + :type value: int + :param force: + :type force: Boolean + :rtype: None + """ + self._set_address(i2c_addr, force=force) + msg = i2c_smbus_ioctl_data.create( + read_write=I2C_SMBUS_WRITE, command=register, size=I2C_SMBUS_WORD_DATA + ) + msg.data.contents.word = value + ioctl(self.fd, I2C_SMBUS, msg) + + def process_call(self, i2c_addr, register, value, force=None): + """ + Executes a SMBus Process Call, sending a 16-bit value and receiving a 16-bit response + + :param i2c_addr: i2c address + :type i2c_addr: int + :param register: Register to read/write to + :type register: int + :param value: Word value to transmit + :type value: int + :param force: + :type force: Boolean + :rtype: int + """ + self._set_address(i2c_addr, force=force) + msg = i2c_smbus_ioctl_data.create( + read_write=I2C_SMBUS_WRITE, command=register, size=I2C_SMBUS_PROC_CALL + ) + msg.data.contents.word = value + ioctl(self.fd, I2C_SMBUS, msg) + return msg.data.contents.word + + def read_block_data(self, i2c_addr, register, force=None): + """ + Read a block of up to 32-bytes from a given register. + + :param i2c_addr: i2c address + :type i2c_addr: int + :param register: Start register + :type register: int + :param force: + :type force: Boolean + :return: List of bytes + :rtype: list + """ + self._set_address(i2c_addr, force=force) + msg = i2c_smbus_ioctl_data.create( + read_write=I2C_SMBUS_READ, command=register, size=I2C_SMBUS_BLOCK_DATA + ) + ioctl(self.fd, I2C_SMBUS, msg) + length = msg.data.contents.block[0] + return msg.data.contents.block[1:length + 1] + + def write_block_data(self, i2c_addr, register, data, force=None): + """ + Write a block of byte data to a given register. + + :param i2c_addr: i2c address + :type i2c_addr: int + :param register: Start register + :type register: int + :param data: List of bytes + :type data: list + :param force: + :type force: Boolean + :rtype: None + """ + length = len(data) + if length > I2C_SMBUS_BLOCK_MAX: + raise ValueError("Data length cannot exceed %d bytes" % I2C_SMBUS_BLOCK_MAX) + self._set_address(i2c_addr, force=force) + msg = i2c_smbus_ioctl_data.create( + read_write=I2C_SMBUS_WRITE, command=register, size=I2C_SMBUS_BLOCK_DATA + ) + msg.data.contents.block[0] = length + msg.data.contents.block[1:length + 1] = data + ioctl(self.fd, I2C_SMBUS, msg) + + def block_process_call(self, i2c_addr, register, data, force=None): + """ + Executes a SMBus Block Process Call, sending a variable-size data + block and receiving another variable-size response + + :param i2c_addr: i2c address + :type i2c_addr: int + :param register: Register to read/write to + :type register: int + :param data: List of bytes + :type data: list + :param force: + :type force: Boolean + :return: List of bytes + :rtype: list + """ + length = len(data) + if length > I2C_SMBUS_BLOCK_MAX: + raise ValueError("Data length cannot exceed %d bytes" % I2C_SMBUS_BLOCK_MAX) + self._set_address(i2c_addr, force=force) + msg = i2c_smbus_ioctl_data.create( + read_write=I2C_SMBUS_WRITE, command=register, size=I2C_SMBUS_BLOCK_PROC_CALL + ) + msg.data.contents.block[0] = length + msg.data.contents.block[1:length + 1] = data + ioctl(self.fd, I2C_SMBUS, msg) + length = msg.data.contents.block[0] + return msg.data.contents.block[1:length + 1] + + def read_i2c_block_data(self, i2c_addr, register, length, force=None): + """ + Read a block of byte data from a given register. + + :param i2c_addr: i2c address + :type i2c_addr: int + :param register: Start register + :type register: int + :param length: Desired block length + :type length: int + :param force: + :type force: Boolean + :return: List of bytes + :rtype: list + """ + if length > I2C_SMBUS_BLOCK_MAX: + raise ValueError("Desired block length over %d bytes" % I2C_SMBUS_BLOCK_MAX) + self._set_address(i2c_addr, force=force) + msg = i2c_smbus_ioctl_data.create( + read_write=I2C_SMBUS_READ, command=register, size=I2C_SMBUS_I2C_BLOCK_DATA + ) + msg.data.contents.byte = length + ioctl(self.fd, I2C_SMBUS, msg) + return msg.data.contents.block[1:length + 1] + + def write_i2c_block_data(self, i2c_addr, register, data, force=None): + """ + Write a block of byte data to a given register. + + :param i2c_addr: i2c address + :type i2c_addr: int + :param register: Start register + :type register: int + :param data: List of bytes + :type data: list + :param force: + :type force: Boolean + :rtype: None + """ + length = len(data) + if length > I2C_SMBUS_BLOCK_MAX: + raise ValueError("Data length cannot exceed %d bytes" % I2C_SMBUS_BLOCK_MAX) + self._set_address(i2c_addr, force=force) + msg = i2c_smbus_ioctl_data.create( + read_write=I2C_SMBUS_WRITE, command=register, size=I2C_SMBUS_I2C_BLOCK_DATA + ) + msg.data.contents.block[0] = length + msg.data.contents.block[1:length + 1] = data + ioctl(self.fd, I2C_SMBUS, msg) + + def i2c_rdwr(self, *i2c_msgs): + """ + Combine a series of i2c read and write operations in a single + transaction (with repeated start bits but no stop bits in between). + + This method takes i2c_msg instances as input, which must be created + first with :py:meth:`i2c_msg.read` or :py:meth:`i2c_msg.write`. + + :param i2c_msgs: One or more i2c_msg class instances. + :type i2c_msgs: i2c_msg + :rtype: None + """ + ioctl_data = i2c_rdwr_ioctl_data.create(*i2c_msgs) + ioctl(self.fd, I2C_RDWR, ioctl_data) diff --git a/scripts/tempSensor/lib/smbus2/smbus2.pyi b/scripts/tempSensor/lib/smbus2/smbus2.pyi new file mode 100644 index 00000000..0861e538 --- /dev/null +++ b/scripts/tempSensor/lib/smbus2/smbus2.pyi @@ -0,0 +1,148 @@ +from enum import IntFlag +from typing import Optional, Sequence, List, Type, SupportsBytes, Iterable +from typing import Union as _UnionT +from types import TracebackType +from ctypes import c_uint32, c_uint8, c_uint16, pointer, Structure, Array, Union + +I2C_SLAVE: int +I2C_SLAVE_FORCE: int +I2C_FUNCS: int +I2C_RDWR: int +I2C_SMBUS: int +I2C_PEC: int +I2C_SMBUS_WRITE: int +I2C_SMBUS_READ: int +I2C_SMBUS_QUICK: int +I2C_SMBUS_BYTE: int +I2C_SMBUS_BYTE_DATA: int +I2C_SMBUS_WORD_DATA: int +I2C_SMBUS_PROC_CALL: int +I2C_SMBUS_BLOCK_DATA: int +I2C_SMBUS_BLOCK_PROC_CALL: int +I2C_SMBUS_I2C_BLOCK_DATA: int +I2C_SMBUS_BLOCK_MAX: int + +class I2cFunc(IntFlag): + I2C = ... + ADDR_10BIT = ... + PROTOCOL_MANGLING = ... + SMBUS_PEC = ... + NOSTART = ... + SLAVE = ... + SMBUS_BLOCK_PROC_CALL = ... + SMBUS_QUICK = ... + SMBUS_READ_BYTE = ... + SMBUS_WRITE_BYTE = ... + SMBUS_READ_BYTE_DATA = ... + SMBUS_WRITE_BYTE_DATA = ... + SMBUS_READ_WORD_DATA = ... + SMBUS_WRITE_WORD_DATA = ... + SMBUS_PROC_CALL = ... + SMBUS_READ_BLOCK_DATA = ... + SMBUS_WRITE_BLOCK_DATA = ... + SMBUS_READ_I2C_BLOCK = ... + SMBUS_WRITE_I2C_BLOCK = ... + SMBUS_HOST_NOTIFY = ... + SMBUS_BYTE = ... + SMBUS_BYTE_DATA = ... + SMBUS_WORD_DATA = ... + SMBUS_BLOCK_DATA = ... + SMBUS_I2C_BLOCK = ... + SMBUS_EMUL = ... + +I2C_M_RD: int +LP_c_uint8: Type[pointer[c_uint8]] +LP_c_uint16: Type[pointer[c_uint16]] +LP_c_uint32: Type[pointer[c_uint32]] + +class i2c_smbus_data(Array): ... +class union_i2c_smbus_data(Union): ... + +union_pointer_type: pointer[union_i2c_smbus_data] + +class i2c_smbus_ioctl_data(Structure): + @staticmethod + def create( + read_write: int = ..., command: int = ..., size: int = ... + ) -> "i2c_smbus_ioctl_data": ... + +class i2c_msg(Structure): + def __iter__(self) -> int: ... + def __len__(self) -> int: ... + def __bytes__(self) -> str: ... + @staticmethod + def read(address: int, length: int) -> "i2c_msg": ... + @staticmethod + def write(address: int, buf: _UnionT[str, Iterable[int], SupportsBytes]) -> "i2c_msg": ... + +class i2c_rdwr_ioctl_data(Structure): + @staticmethod + def create(*i2c_msg_instances: Sequence[i2c_msg]) -> "i2c_rdwr_ioctl_data": ... + +class SMBus: + fd: Optional[int] = ... + funcs: I2cFunc = ... + address: Optional[int] = ... + force: bool = ... + pec: int = ... + def __init__( + self, bus: _UnionT[None, int, str] = ..., force: bool = ... + ) -> None: ... + def __enter__(self) -> "SMBus": ... + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> None: ... + def open(self, bus: _UnionT[int, str]) -> None: ... + def close(self) -> None: ... + def enable_pec(self, enable: bool = ...) -> None: ... + def write_quick(self, i2c_addr: int, force: Optional[bool] = ...) -> None: ... + def read_byte(self, i2c_addr: int, force: Optional[bool] = ...) -> int: ... + def write_byte( + self, i2c_addr: int, value: int, force: Optional[bool] = ... + ) -> None: ... + def read_byte_data( + self, i2c_addr: int, register: int, force: Optional[bool] = ... + ) -> int: ... + def write_byte_data( + self, i2c_addr: int, register: int, value: int, force: Optional[bool] = ... + ) -> None: ... + def read_word_data( + self, i2c_addr: int, register: int, force: Optional[bool] = ... + ) -> int: ... + def write_word_data( + self, i2c_addr: int, register: int, value: int, force: Optional[bool] = ... + ) -> None: ... + def process_call( + self, i2c_addr: int, register: int, value: int, force: Optional[bool] = ... + ): ... + def read_block_data( + self, i2c_addr: int, register: int, force: Optional[bool] = ... + ) -> List[int]: ... + def write_block_data( + self, + i2c_addr: int, + register: int, + data: Sequence[int], + force: Optional[bool] = ..., + ) -> None: ... + def block_process_call( + self, + i2c_addr: int, + register: int, + data: Sequence[int], + force: Optional[bool] = ..., + ) -> List[int]: ... + def read_i2c_block_data( + self, i2c_addr: int, register: int, length: int, force: Optional[bool] = ... + ) -> List[int]: ... + def write_i2c_block_data( + self, + i2c_addr: int, + register: int, + data: Sequence[int], + force: Optional[bool] = ..., + ) -> None: ... + def i2c_rdwr(self, *i2c_msgs: i2c_msg) -> None: ... diff --git a/scripts/tempSensor/lib/ufastrsa/__init__.py b/scripts/tempSensor/lib/ufastrsa/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/scripts/tempSensor/lib/ufastrsa/genprime.py b/scripts/tempSensor/lib/ufastrsa/genprime.py new file mode 100644 index 00000000..4478394b --- /dev/null +++ b/scripts/tempSensor/lib/ufastrsa/genprime.py @@ -0,0 +1,136 @@ +from ufastrsa import srandom + +try: + from _crypto import NUMBER as tomsfastmath + + pow3_ = tomsfastmath.exptmod + invmod_ = tomsfastmath.invmod + generate_prime_ = tomsfastmath.generate_prime + + def genprime(num=1024, test=25, safe=False): + return generate_prime_(num, test, safe) + +except ImportError: + pow3_ = pow + + def invmod_(a, b): + c, d, e, f, g = 1, 0, 0, 1, b + while b: + q = a // b + a, c, d, b, e, f = b, e, f, a - q * b, c - q * e, d - q * f + assert a >= 0 and c % g >= 0 + return a == 1 and c % g or 0 + + def miller_rabin_pass(a, n): + n_minus_one = n - 1 + s, d = get_lowest_set_bit(n_minus_one) + a_to_power = pow3(a, d, n) + if a_to_power == 1: + return True + for i in range(s): + if a_to_power == n_minus_one: + return True + a_to_power = pow3(a_to_power, 2, n) + if a_to_power == n_minus_one: + return True + return False + + class MillerRabinTest: + def __init__(self, randint, repeat): + self.randint = randint + self.repeat = repeat + + def __call__(self, n): + randint = self.randint + n_minus_one = n - 1 + for repeat in range(self.repeat): + a = randint(1, n_minus_one) + if not miller_rabin_pass(a, n): + return False + return True + + class GenPrime: + def __init__(self, getrandbits, testfn): + self.getrandbits = getrandbits + self.testfn = testfn + + def __call__(self, bits): + getrandbits = self.getrandbits + testfn = self.testfn + while True: + p = (1 << (bits - 1)) | getrandbits(bits - 1) | 1 + if p % 3 != 0 and p % 5 != 0 and p % 7 != 0 and testfn(p): + break + return p + + miller_rabin_test = MillerRabinTest(srandom.randint, 25) + genprime = GenPrime(srandom.getrandbits, miller_rabin_test) + + +def pow3(x, y, z): + return pow3_(x, y, z) + + +def invmod(a, b): + return invmod_(a, b) + + +def get_lowest_set_bit(n): + i = 0 + while n: + if n & 1: + return i, n + n >>= 1 + i += 1 + raise "Error" + + +def gcd(a, b): + while b: + a, b = b, a % b + return a + + +def get_bit_length(n): + return srandom.get_bit_length(n) + + +class GenRSA: + def __init__(self, genprime): + self.genprime = genprime + + def __call__(self, bits, e=None, with_crt=False): + pbits = (bits + 1) >> 1 + qbits = bits - pbits + if e is None: + e = 65537 + elif e < 0: + e = self.genprime(-e) + while True: + p = self.genprime(pbits) + if gcd(e, p - 1) == 1: + break + while True: + while True: + q = self.genprime(qbits) + if gcd(e, q - 1) == 1 and p != q: + break + n = p * q + if get_bit_length(n) == bits: + break + p = max(p, q) + p_minus_1 = p - 1 + q_minus_1 = q - 1 + phi = p_minus_1 * q_minus_1 + d = invmod(e, phi) + if with_crt: + dp = d % p_minus_1 + dq = d % q_minus_1 + qinv = invmod(q, p) + assert qinv < p + return bits, n, e, d, p, q, dp, dq, qinv + else: + return bits, n, e, d + + +genrsa = GenRSA(genprime) diff --git a/scripts/tempSensor/lib/ufastrsa/rsa.py b/scripts/tempSensor/lib/ufastrsa/rsa.py new file mode 100644 index 00000000..14fd2f83 --- /dev/null +++ b/scripts/tempSensor/lib/ufastrsa/rsa.py @@ -0,0 +1,46 @@ +from ufastrsa.genprime import pow3 +from ufastrsa.srandom import rndsrcnz + + +class RSA: + def __init__(self, bits, n=None, e=None, d=None): + self.bits = bits + self.bytes = (bits + 7) >> 3 + self.n = n + self.e = e + self.d = d + self.rndsrcnz = rndsrcnz + + def pkcs_sign(self, value): + len_padding = self.bytes - 3 - len(value) + assert len_padding >= 0, len_padding + base = int.from_bytes( + b"\x00\x01" + len_padding * b"\xff" + b"\x00" + value, "big" + ) + return int.to_bytes(pow3(base, self.d, self.n), self.bytes, "big") + + def pkcs_verify(self, value): + assert len(value) == self.bytes + signed = int.to_bytes( + pow3(int.from_bytes(value, "big"), self.e, self.n), self.bytes, "big" + ) + idx = signed.find(b"\0", 1) + assert idx != -1 and signed[:idx] == b"\x00\x01" + (idx - 2) * b"\xff" + return signed[idx + 1 :] + + def pkcs_encrypt(self, value): + len_padding = self.bytes - 3 - len(value) + assert len_padding >= 0 + base = int.from_bytes( + b"\x00\x02" + self.rndsrcnz(len_padding) + b"\x00" + value, "big" + ) + return int.to_bytes(pow3(base, self.e, self.n), self.bytes, "big") + + def pkcs_decrypt(self, value): + assert len(value) == self.bytes + decrypted = int.to_bytes( + pow3(int.from_bytes(value, "big"), self.d, self.n), self.bytes, "big" + ) + idx = decrypted.find(b"\0", 2) + assert idx != -1 and decrypted[:2] == b"\x00\x02" + return decrypted[idx + 1 :] diff --git a/scripts/tempSensor/lib/ufastrsa/srandom.py b/scripts/tempSensor/lib/ufastrsa/srandom.py new file mode 100644 index 00000000..30bbc666 --- /dev/null +++ b/scripts/tempSensor/lib/ufastrsa/srandom.py @@ -0,0 +1,47 @@ +from functools import reduce +from os import urandom + +from ufastrsa.util import get_bit_length + + +class Random: + def __init__(self, seed=None, rndsrc=None): + if rndsrc is None: + rndsrc = urandom + self.rndsrc = rndsrc + + def getrandbits(self, k): + if not k >= 0: + raise ValueError("number of bits must be >= 0") + return reduce( + lambda x, y: x << 8 | y, + self.rndsrc(k >> 3), + int.from_bytes(self.rndsrc(1), "little") & ((1 << (k & 7)) - 1), + ) + + def randint(self, a, b): + if a > b: + raise ValueError("empty range for randint(): %d, %d" % (a, b)) + c = 1 + b - a + k = get_bit_length(c - 1) + while True: + r = self.getrandbits(k) + if r <= c: + break + return a + r + + def rndsrcnz(self, size): + rv = self.rndsrc(size).replace(b"\x00", b"") + mv = size - len(rv) + while mv > 0: + rv += self.rndsrc(mv).replace(b"\x00", b"") + mv = size - len(rv) + assert len(rv) == size + return rv + + +basernd = Random() +rndsrc = basernd.rndsrc +getrandbits = basernd.getrandbits +randint = basernd.randint +rndsrcnz = basernd.rndsrcnz diff --git a/scripts/tempSensor/lib/ufastrsa/util.py b/scripts/tempSensor/lib/ufastrsa/util.py new file mode 100644 index 00000000..f74b3d0a --- /dev/null +++ b/scripts/tempSensor/lib/ufastrsa/util.py @@ -0,0 +1,14 @@ +try: + int.bit_length(0) + + def get_bit_length(n): + return n.bit_length() + +except: + # Work around + def get_bit_length(n): + i = 0 + while n: + n >>= 1 + i += 1 + return i diff --git a/scripts/tempSensor/lib/umqtt/robust.py b/scripts/tempSensor/lib/umqtt/robust.py new file mode 100644 index 00000000..2a2b5629 --- /dev/null +++ b/scripts/tempSensor/lib/umqtt/robust.py @@ -0,0 +1,44 @@ +import utime + +from . import simple + + +class MQTTClient(simple.MQTTClient): + DELAY = 2 + DEBUG = False + + def delay(self, i): + utime.sleep(self.DELAY) + + def log(self, in_reconnect, e): + if self.DEBUG: + if in_reconnect: + print("mqtt reconnect: %r" % e) + else: + print("mqtt: %r" % e) + + def reconnect(self): + i = 0 + while 1: + try: + return super().connect(False) + except OSError as e: + self.log(True, e) + i += 1 + self.delay(i) + + def publish(self, topic, msg, retain=False, qos=0): + while 1: + try: + return super().publish(topic, msg, retain, qos) + except OSError as e: + self.log(False, e) + self.reconnect() + + def wait_msg(self): + while 1: + try: + return super().wait_msg() + except OSError as e: + self.log(False, e) + self.reconnect() diff --git a/scripts/tempSensor/lib/umqtt/simple.py b/scripts/tempSensor/lib/umqtt/simple.py new file mode 100644 index 00000000..5d09230c --- /dev/null +++ b/scripts/tempSensor/lib/umqtt/simple.py @@ -0,0 +1,217 @@ +import usocket as socket +import ustruct as struct + + +class MQTTException(Exception): + pass + + +class MQTTClient: + def __init__( + self, + client_id, + server, + port=0, + user=None, + password=None, + keepalive=0, + ssl=False, + ssl_params={}, + ): + if port == 0: + port = 8883 if ssl else 1883 + self.client_id = client_id + self.sock = None + self.server = server + self.port = port + self.ssl = ssl + self.ssl_params = ssl_params + self.pid = 0 + self.cb = None + self.user = user + self.pswd = password + self.keepalive = keepalive + self.lw_topic = None + self.lw_msg = None + self.lw_qos = 0 + self.lw_retain = False + + def _send_str(self, s): + self.sock.write(struct.pack("!H", len(s))) + self.sock.write(s) + + def _recv_len(self): + n = 0 + sh = 0 + while 1: + b = self.sock.read(1)[0] + n |= (b & 0x7F) << sh + if not b & 0x80: + return n + sh += 7 + + def set_callback(self, f): + self.cb = f + + def set_last_will(self, topic, msg, retain=False, qos=0): + assert 0 <= qos <= 2 + assert topic + self.lw_topic = topic + self.lw_msg = msg + self.lw_qos = qos + self.lw_retain = retain + + def connect(self, clean_session=True): + self.sock = socket.socket() + addr = socket.getaddrinfo(self.server, self.port)[0][-1] + self.sock.connect(addr) + if self.ssl: + # replaced ussl with ssl due to deprecation in MicroPython 1.23.0 + # (not PR'd on source repo, but I'm using mqtt_as in my workflows + # instead, anyway) + import ssl + + self.sock = ssl.wrap_socket(self.sock, **self.ssl_params) + premsg = bytearray(b"\x10\0\0\0\0\0") + msg = bytearray(b"\x04MQTT\x04\x02\0\0") + + sz = 10 + 2 + len(self.client_id) + msg[6] = clean_session << 1 + if self.user is not None: + sz += 2 + len(self.user) + 2 + len(self.pswd) + msg[6] |= 0xC0 + if self.keepalive: + assert self.keepalive < 65536 + msg[7] |= self.keepalive >> 8 + msg[8] |= self.keepalive & 0x00FF + if self.lw_topic: + sz += 2 + len(self.lw_topic) + 2 + len(self.lw_msg) + msg[6] |= 0x4 | (self.lw_qos & 0x1) << 3 | (self.lw_qos & 0x2) << 3 + msg[6] |= self.lw_retain << 5 + + i = 1 + while sz > 0x7F: + premsg[i] = (sz & 0x7F) | 0x80 + sz >>= 7 + i += 1 + premsg[i] = sz + + self.sock.write(premsg, i + 2) + self.sock.write(msg) + # print(hex(len(msg)), hexlify(msg, ":")) + self._send_str(self.client_id) + if self.lw_topic: + self._send_str(self.lw_topic) + self._send_str(self.lw_msg) + if self.user is not None: + self._send_str(self.user) + self._send_str(self.pswd) + resp = self.sock.read(4) + assert resp[0] == 0x20 and resp[1] == 0x02 + if resp[3] != 0: + raise MQTTException(resp[3]) + return resp[2] & 1 + + def disconnect(self): + self.sock.write(b"\xe0\0") + self.sock.close() + + def ping(self): + self.sock.write(b"\xc0\0") + + def publish(self, topic, msg, retain=False, qos=0): + pkt = bytearray(b"\x30\0\0\0") + pkt[0] |= qos << 1 | retain + sz = 2 + len(topic) + len(msg) + if qos > 0: + sz += 2 + assert sz < 2097152 + i = 1 + while sz > 0x7F: + pkt[i] = (sz & 0x7F) | 0x80 + sz >>= 7 + i += 1 + pkt[i] = sz + # print(hex(len(pkt)), hexlify(pkt, ":")) + self.sock.write(pkt, i + 1) + self._send_str(topic) + if qos > 0: + self.pid += 1 + pid = self.pid + struct.pack_into("!H", pkt, 0, pid) + self.sock.write(pkt, 2) + self.sock.write(msg) + if qos == 1: + while 1: + op = self.wait_msg() + if op == 0x40: + sz = self.sock.read(1) + assert sz == b"\x02" + rcv_pid = self.sock.read(2) + rcv_pid = rcv_pid[0] << 8 | rcv_pid[1] + if pid == rcv_pid: + return + elif qos == 2: + assert 0 + + def subscribe(self, topic, qos=0): + assert self.cb is not None, "Subscribe callback is not set" + pkt = bytearray(b"\x82\0\0\0") + self.pid += 1 + struct.pack_into("!BH", pkt, 1, 2 + 2 + len(topic) + 1, self.pid) + # print(hex(len(pkt)), hexlify(pkt, ":")) + self.sock.write(pkt) + self._send_str(topic) + self.sock.write(qos.to_bytes(1, "little")) + while 1: + op = self.wait_msg() + if op == 0x90: + resp = self.sock.read(4) + # print(resp) + assert resp[1] == pkt[2] and resp[2] == pkt[3] + if resp[3] == 0x80: + raise MQTTException(resp[3]) + return + + # Wait for a single incoming MQTT message and process it. + # Subscribed messages are delivered to a callback previously + # set by .set_callback() method. Other (internal) MQTT + # messages processed internally. + def wait_msg(self): + res = self.sock.read(1) + self.sock.setblocking(True) + if res is None: + return None + if res == b"": + raise OSError(-1) + if res == b"\xd0": # PINGRESP + sz = self.sock.read(1)[0] + assert sz == 0 + return None + op = res[0] + if op & 0xF0 != 0x30: + return op + sz = self._recv_len() + topic_len = self.sock.read(2) + topic_len = (topic_len[0] << 8) | topic_len[1] + topic = self.sock.read(topic_len) + sz -= topic_len + 2 + if op & 6: + pid = self.sock.read(2) + pid = pid[0] << 8 | pid[1] + sz -= 2 + msg = self.sock.read(sz) + self.cb(topic, msg) + if op & 6 == 2: + pkt = bytearray(b"\x40\x02\0\0") + struct.pack_into("!H", pkt, 2, pid) + self.sock.write(pkt) + elif op & 6 == 4: + assert 0 + + # Checks whether a pending message from server is available. + # If not, returns immediately with None. Otherwise, does + # the same processing as wait_msg. + def check_msg(self): + self.sock.setblocking(False) + return self.wait_msg() diff --git a/scripts/tempSensor/lib/unique_id-1.0.1.dist-info/METADATA b/scripts/tempSensor/lib/unique_id-1.0.1.dist-info/METADATA new file mode 100644 index 00000000..51ea95a7 --- /dev/null +++ b/scripts/tempSensor/lib/unique_id-1.0.1.dist-info/METADATA @@ -0,0 +1,11 @@ +Metadata-Version: 2.1 +Name: unique-id +Version: 1.0.1 +Summary: Unique-ID is a small lib to generate unique ids - string values. +Home-page: +Download-URL: https://github.com/slawek87/unique-id +Author: Sławomir Kabik +Author-email: slawek@redsoftware.pl +Keywords: Python Unique ID,Python ID,Python Unique string +Requires-Dist: setuptools + diff --git a/scripts/tempSensor/lib/unique_id-1.0.1.dist-info/RECORD b/scripts/tempSensor/lib/unique_id-1.0.1.dist-info/RECORD new file mode 100644 index 00000000..4e105c50 --- /dev/null +++ b/scripts/tempSensor/lib/unique_id-1.0.1.dist-info/RECORD @@ -0,0 +1,5 @@ +unique_id-1.0.1.dist-info/METADATA,, +unique_id/__init__.py,, +unique_id/main.py,, +unique_id/tests.py,, +unique_id-1.0.1.dist-info/RECORD,, \ No newline at end of file diff --git a/scripts/tempSensor/lib/unique_id/__init__.py b/scripts/tempSensor/lib/unique_id/__init__.py new file mode 100644 index 00000000..6138a1f8 --- /dev/null +++ b/scripts/tempSensor/lib/unique_id/__init__.py @@ -0,0 +1 @@ +from unique_id.main import get_unique_id \ No newline at end of file diff --git a/scripts/tempSensor/lib/unique_id/main.py b/scripts/tempSensor/lib/unique_id/main.py new file mode 100644 index 00000000..87825a53 --- /dev/null +++ b/scripts/tempSensor/lib/unique_id/main.py @@ -0,0 +1,58 @@ +import random + + +class UniqueID(object): + """ + Generates Unique ID. + """ + DEFAULT_ID_LENGTH = 14 + DEFAULT_EXCLUDED_CHARS = ":*^`\",.~;%+-'" + + def __init__(self, length=DEFAULT_ID_LENGTH, excluded_chars=DEFAULT_EXCLUDED_CHARS): + """ + `length` - defines length of unique ID. + `excluded_chars` - defines chars excluded during generate process of unique ID. + """ + self.id_length = length + self.excluded_chars = excluded_chars + + def get_random_bits(self): + """ + Method returns random number included in max 8 bits. + """ + return random.getrandbits(8) + + def is_approved_ascii(self, ascii_number): + return 126 >= ascii_number >= 33 + + def is_excluded_char(self, current_char): + """ + Method checks if given char is not in excluded chars list. + """ + return current_char in self.excluded_chars + + def generate_id(self): + """ + Method generates unique ID. + """ + unique_id = "" + + while len(unique_id) < self.id_length: + ascii_number = self.get_random_bits() + + if self.is_approved_ascii(ascii_number): + random_char = chr(ascii_number) + + if not self.is_excluded_char(random_char): + unique_id += chr(ascii_number) + + return unique_id + + +def get_unique_id(length=UniqueID.DEFAULT_ID_LENGTH, excluded_chars=UniqueID.DEFAULT_EXCLUDED_CHARS): + """ + Function returns unique ID. + """ + unique_id = UniqueID(length=length, excluded_chars=excluded_chars) + return unique_id.generate_id() + diff --git a/scripts/tempSensor/lib/unique_id/tests.py b/scripts/tempSensor/lib/unique_id/tests.py new file mode 100644 index 00000000..6438c2cc --- /dev/null +++ b/scripts/tempSensor/lib/unique_id/tests.py @@ -0,0 +1,40 @@ +from random import randint +import unittest + +from unique_id import get_unique_id + + +class TestStringMethods(unittest.TestCase): + def test_unique_id(self): + unique_ids = list() + + for item in range(1000): + unique_id = get_unique_id() + + is_duplicated = unique_id in unique_ids + self.assertFalse(is_duplicated) + + unique_ids.append(unique_id) + + def test_max_length(self): + for item in range(1000): + id_length = randint(1, 128) + unique_id = get_unique_id(length=id_length) + + is_over_length = len(unique_id) != id_length + self.assertFalse(is_over_length) + + def test_excluded_chars(self): + id_length = 256 + excluded_chars = [1, 'f', 'm', 'a', 4, 5, 'Z', 'w', '_'] + + for item in range(1000): + unique_id = get_unique_id(length=id_length, excluded_chars=excluded_chars) + + for seed in unique_id: + is_excluded_char = seed in excluded_chars + self.assertFalse(is_excluded_char) + + +if __name__ == '__main__': + unittest.main() diff --git a/scripts/tempSensor/lib/urequests_2.py b/scripts/tempSensor/lib/urequests_2.py new file mode 100644 index 00000000..f43f88eb --- /dev/null +++ b/scripts/tempSensor/lib/urequests_2.py @@ -0,0 +1,203 @@ +# Workaround for the `urequests` module to support HTTP/1.1 +# Based on https://github.com/micropython/micropython-lib/blob/e025c843b60e93689f0f991d753010bb5bd6a722/python-ecosys/requests/requests/__init__.py +# See https://github.com/micropython/micropython-lib/pull/861 and https://github.com/orgs/micropython/discussions/15112 +# `1.0` replaced with `1.1, i.e.: +# `s.write(b"%s /%s HTTP/1.0\r\n" % (method, path))` changed to `s.write(b"%s /%s HTTP/1.1\r\n" % (method, path))` +import usocket + + +class Response: + def __init__(self, f): + self.raw = f + self.encoding = "utf-8" + self._cached = None + + def close(self): + if self.raw: + self.raw.close() + self.raw = None + self._cached = None + + @property + def content(self): + if self._cached is None: + try: + self._cached = self.raw.read() + finally: + self.raw.close() + self.raw = None + return self._cached + + @property + def text(self): + return str(self.content, self.encoding) + + def json(self): + import ujson + + return ujson.loads(self.content) + + +def request( + method, + url, + data=None, + json=None, + headers={}, + stream=None, + auth=None, + timeout=None, + parse_headers=True, +): + redirect = None # redirection url, None means no redirection + chunked_data = ( + data and getattr(data, "__next__", None) and not getattr(data, "__len__", None) + ) + + if auth is not None: + import ubinascii + + username, password = auth + formated = b"{}:{}".format(username, password) + formated = str(ubinascii.b2a_base64(formated)[:-1], "ascii") + headers["Authorization"] = "Basic {}".format(formated) + + try: + proto, dummy, host, path = url.split("/", 3) + except ValueError: + proto, dummy, host = url.split("/", 2) + path = "" + if proto == "http:": + port = 80 + elif proto == "https:": + import ussl + + port = 443 + else: + raise ValueError("Unsupported protocol: " + proto) + + if ":" in host: + host, port = host.split(":", 1) + port = int(port) + + ai = usocket.getaddrinfo(host, port, 0, usocket.SOCK_STREAM) + ai = ai[0] + + resp_d = None + if parse_headers is not False: + resp_d = {} + + s = usocket.socket(ai[0], usocket.SOCK_STREAM, ai[2]) + + if timeout is not None: + # Note: settimeout is not supported on all platforms, will raise + # an AttributeError if not available. + s.settimeout(timeout) + + try: + s.connect(ai[-1]) + if proto == "https:": + s = ussl.wrap_socket(s, server_hostname=host) + s.write(b"%s /%s HTTP/1.1\r\n" % (method, path)) + if "Host" not in headers: + s.write(b"Host: %s\r\n" % host) + # Iterate over keys to avoid tuple alloc + for k in headers: + s.write(k) + s.write(b": ") + s.write(headers[k]) + s.write(b"\r\n") + if json is not None: + assert data is None + import ujson + + data = ujson.dumps(json) + s.write(b"Content-Type: application/json\r\n") + if data: + if chunked_data: + s.write(b"Transfer-Encoding: chunked\r\n") + else: + s.write(b"Content-Length: %d\r\n" % len(data)) + s.write(b"Connection: close\r\n\r\n") + if data: + if chunked_data: + for chunk in data: + s.write(b"%x\r\n" % len(chunk)) + s.write(chunk) + s.write(b"\r\n") + s.write("0\r\n\r\n") + else: + s.write(data) + + l = s.readline() + # print(l) + l = l.split(None, 2) + if len(l) < 2: + # Invalid response + raise ValueError("HTTP error: BadStatusLine:\n%s" % l) + status = int(l[1]) + reason = "" + if len(l) > 2: + reason = l[2].rstrip() + while True: + l = s.readline() + if not l or l == b"\r\n": + break + # print(l) + if l.startswith(b"Transfer-Encoding:"): + if b"chunked" in l: + raise ValueError("Unsupported " + str(l, "utf-8")) + elif l.startswith(b"Location:") and not 200 <= status <= 299: + if status in [301, 302, 303, 307, 308]: + redirect = str(l[10:-2], "utf-8") + else: + raise NotImplementedError("Redirect %d not yet supported" % status) + if parse_headers is False: + pass + elif parse_headers is True: + l = str(l, "utf-8") + k, v = l.split(":", 1) + resp_d[k] = v.strip() + else: + parse_headers(l, resp_d) + except OSError: + s.close() + raise + + if redirect: + s.close() + if status in [301, 302, 303]: + return request("GET", redirect, None, None, headers, stream) + else: + return request(method, redirect, data, json, headers, stream) + else: + resp = Response(s) + resp.status_code = status + resp.reason = reason + if resp_d is not None: + resp.headers = resp_d + return resp + + +def head(url, **kw): + return request("HEAD", url, **kw) + + +def get(url, **kw): + return request("GET", url, **kw) + + +def post(url, **kw): + return request("POST", url, **kw) + + +def put(url, **kw): + return request("PUT", url, **kw) + + +def patch(url, **kw): + return request("PATCH", url, **kw) + + +def delete(url, **kw): + return request("DELETE", url, **kw) diff --git a/scripts/tempSensor/pico_id.txt b/scripts/tempSensor/pico_id.txt new file mode 100644 index 00000000..3bb8d27d --- /dev/null +++ b/scripts/tempSensor/pico_id.txt @@ -0,0 +1 @@ +e66130100f594628 \ No newline at end of file diff --git a/scripts/tempSensor/rsa.json b/scripts/tempSensor/rsa.json new file mode 100644 index 00000000..2b6faab5 --- /dev/null +++ b/scripts/tempSensor/rsa.json @@ -0,0 +1 @@ +{"e": 65537, "bits": 256, "d": 61449807040989742694108195033472778816813477767129784887758545872352461876233, "n": 93521805862369252866652008218584959020573609838284713191171615421986659700917} \ No newline at end of file diff --git a/scripts/tempSensor/secrets.py b/scripts/tempSensor/secrets.py new file mode 100644 index 00000000..729d7eb8 --- /dev/null +++ b/scripts/tempSensor/secrets.py @@ -0,0 +1,83 @@ +from bme680 import BME680_I2C # Ensure you have the right import for the BME680 class +from machine import I2C, Pin +from netman import connectWiFi +from umqtt.simple import MQTTClient +import time + +# Wi-Fi and MQTT configuration +SSID = 'Pixel 8' # Replace with your Wi-Fi SSID +PASSWORD = '123456789' # Replace with your Wi-Fi password +MQTT_BROKER = 'b6bdb89571144b3d8e5ca4bbe666ddb5.s1.eu.hivemq.cloud' # HiveMQ Cloud broker URL +MQTT_PORT = 8883 # Port for TLS +MQTT_TOPIC = "sensors/bme680/data" # Replace with your desired MQTT topic + +MQTT_USER = 'Luthiraa' +MQTT_PASS = 'theboss1010' + +def connect_to_internet(): + try: + status = connectWiFi(SSID, PASSWORD, country='US', retries=3) + print("Connected to Wi-Fi successfully!") + print("IP Address:", status[0]) + except RuntimeError as e: + print(f"Failed to connect to Wi-Fi: {e}") + raise + +# Initialize I2C and BME680 +i2c = I2C(1, scl=Pin(27), sda=Pin(26)) +bme = BME680_I2C(i2c) + +# MQTT setup with authentication and TLS +client = MQTTClient( + client_id=b"kudzai_raspberrypi_picow", + server=MQTT_BROKER, + port=MQTT_PORT, + user=MQTT_USER, + password=MQTT_PASS, + keepalive=60, # Set to a shorter interval + ssl=True, + ssl_params={'server_hostname': MQTT_BROKER} +) +# Connect to MQTT broker +def connect_to_mqtt(): + try: + client.connect() + print("Connected to MQTT broker") + print("Client ID:", client.client_id) # Print client ID + except Exception as e: + print(f"Failed to connect to MQTT broker: {e}") + raise + +# Connect to Wi-Fi and MQTT +connect_to_internet() +connect_to_mqtt() + +while True: + # Read sensor data + temperature = bme.temperature + humidity = bme.humidity + pressure = bme.pressure + gas = bme.gas + + # Prepare data payload + payload = ( + f"Temperature: {temperature:.2f} °C, " + f"Humidity: {humidity:.2f} %, " + f"Pressure: {pressure:.2f} hPa, " + f"Gas: {gas:.2f} ohms" + ) + + # Print data to console + print("--------------------------------------------------") + print(payload) + print("--------------------------------------------------") + + # Publish data to MQTT broker + try: + client.publish(MQTT_TOPIC, payload) + print("Data published to MQTT topic:", MQTT_TOPIC) + except Exception as e: + print(f"Failed to publish data: {e}") + client.connect() + + time.sleep(2) diff --git a/scripts/tempSensor/temp.py b/scripts/tempSensor/temp.py new file mode 100644 index 00000000..74e84b3f --- /dev/null +++ b/scripts/tempSensor/temp.py @@ -0,0 +1,19 @@ +from bme680 import * # Ensure you have the right import for the BME680 class +from machine import I2C, Pin +import time + +# Initialize I2C on GPIO pins 27 (SCL) and 26 (SDA) for the Raspberry Pi Pico W +i2c = I2C(1, scl=Pin(27), sda=Pin(26)) + +# Initialize the BME680 sensor over I2C +bme = BME680_I2C(i2c) + +while True: + print("--------------------------------------------------") + print() + print("Temperature: {:.2f} °C".format(bme.temperature)) + print("Humidity: {:.2f} %".format(bme.humidity)) + print("Pressure: {:.2f} hPa".format(bme.pressure)) + print("Gas: {:.2f} ohms".format(bme.gas)) + print() + time.sleep(3) diff --git a/scripts/tempSensor/test.py b/scripts/tempSensor/test.py new file mode 100644 index 00000000..595de131 --- /dev/null +++ b/scripts/tempSensor/test.py @@ -0,0 +1,65 @@ +from bme680 import BME680_I2C +from machine import I2C, Pin +from netman import connectWiFi +from umqtt.simple import MQTTClient +import json +import time +from constants import WIFI_SSID, WIFI_PASSWORD, MQTT_BROKER, MQTT_PORT, MQTT_TOPIC, MQTT_USER, MQTT_PASSWORD + + +def connect_to_internet(): + try: + status = connectWiFi(WIFI_SSID, WIFI_PASSWORD, country='US', retries=3) + print("Connected to Wi-Fi successfully!") + print("IP Address:", status[0]) + except RuntimeError as e: + print(f"Failed to connect to Wi-Fi: {e}") + raise + +i2c = I2C(1, scl=Pin(27), sda=Pin(26)) +bme = BME680_I2C(i2c) + +client = MQTTClient( + client_id=b"kudzai_raspberrypi_picow", + server=MQTT_BROKER, + port=MQTT_PORT, + user=MQTT_USER, + password=MQTT_PASSWORD, + keepalive=60, # Set to a shorter interval + ssl=True, + ssl_params={'server_hostname': MQTT_BROKER} +) + +def connect_to_mqtt(): + try: + client.connect() + print("Connected to MQTT broker") + print("Client ID:", client.client_id) + except Exception as e: + print(f"Failed to connect to MQTT broker: {e}") + raise + +connect_to_internet() +connect_to_mqtt() + +while True: + temperature = bme.temperature + humidity = bme.humidity + pressure = bme.pressure + gas = bme.gas + payload = json.dumps({ + "temperature": temperature, + "humidity": humidity, + "pressure": pressure, + "gas": gas + }) + print("oayload:", payload) + + try: + client.publish(MQTT_TOPIC, payload) + print("Data published to MQTT topic:", MQTT_TOPIC) + except Exception as e: + print(f"Failed to publish data: {e}") + client.connect() + + time.sleep(2) From 374b4edc8ab723854ee5ec34257390f557e7ed41 Mon Sep 17 00:00:00 2001 From: Luthira A <79228258+Luthiraa@users.noreply.github.com> Date: Sun, 8 Dec 2024 10:50:42 -0500 Subject: [PATCH 06/19] Add BME680 sensor constants and configuration files --- scripts/tempSensor/.gitignore | 1 - scripts/tempSensor/constants.py | 12 + scripts/tempSensor/lib/bme680/constants.py | 413 +++++++++++++++++++++ 3 files changed, 425 insertions(+), 1 deletion(-) delete mode 100644 scripts/tempSensor/.gitignore create mode 100644 scripts/tempSensor/constants.py create mode 100644 scripts/tempSensor/lib/bme680/constants.py diff --git a/scripts/tempSensor/.gitignore b/scripts/tempSensor/.gitignore deleted file mode 100644 index b8cca463..00000000 --- a/scripts/tempSensor/.gitignore +++ /dev/null @@ -1 +0,0 @@ -constants.py \ No newline at end of file diff --git a/scripts/tempSensor/constants.py b/scripts/tempSensor/constants.py new file mode 100644 index 00000000..2930feb7 --- /dev/null +++ b/scripts/tempSensor/constants.py @@ -0,0 +1,12 @@ +# constants.py + +# Wi-Fi Configuration +WIFI_SSID = 'Pixel 8' +WIFI_PASSWORD = '123456789' + +# MQTT Configuration +MQTT_BROKER = 'b6bdb89571144b3d8e5ca4bbe666ddb5.s1.eu.hivemq.cloud' +MQTT_PORT = 8883 +MQTT_TOPIC = 'sensors/bme680/data' +MQTT_USER = 'LuthiraMQ' +MQTT_PASSWORD = 'Password1118.' diff --git a/scripts/tempSensor/lib/bme680/constants.py b/scripts/tempSensor/lib/bme680/constants.py new file mode 100644 index 00000000..d77415d3 --- /dev/null +++ b/scripts/tempSensor/lib/bme680/constants.py @@ -0,0 +1,413 @@ +"""BME680 constants, structures and utilities.""" + +# BME680 General config +POLL_PERIOD_MS = 10 + +# BME680 I2C addresses +I2C_ADDR_PRIMARY = 0x76 +I2C_ADDR_SECONDARY = 0x77 + +# BME680 unique chip identifier +CHIP_ID = 0x61 + +# BME680 coefficients related defines +COEFF_SIZE = 41 +COEFF_ADDR1_LEN = 25 +COEFF_ADDR2_LEN = 16 + +# BME680 field_x related defines +FIELD_LENGTH = 17 +FIELD_ADDR_OFFSET = 17 + +# Soft reset command +SOFT_RESET_CMD = 0xb6 + +# Error code definitions +OK = 0 +# Errors +E_NULL_PTR = -1 +E_COM_FAIL = -2 +E_DEV_NOT_FOUND = -3 +E_INVALID_LENGTH = -4 + +# Warnings +W_DEFINE_PWR_MODE = 1 +W_NO_NEW_DATA = 2 + +# Info's +I_MIN_CORRECTION = 1 +I_MAX_CORRECTION = 2 + +# Register map +# Other coefficient's address +ADDR_RES_HEAT_VAL_ADDR = 0x00 +ADDR_RES_HEAT_RANGE_ADDR = 0x02 +ADDR_RANGE_SW_ERR_ADDR = 0x04 +ADDR_SENS_CONF_START = 0x5A +ADDR_GAS_CONF_START = 0x64 + +# Field settings +FIELD0_ADDR = 0x1d + +# Heater settings +RES_HEAT0_ADDR = 0x5a +GAS_WAIT0_ADDR = 0x64 + +# Sensor configuration registers +CONF_HEAT_CTRL_ADDR = 0x70 +CONF_ODR_RUN_GAS_NBC_ADDR = 0x71 +CONF_OS_H_ADDR = 0x72 +MEM_PAGE_ADDR = 0xf3 +CONF_T_P_MODE_ADDR = 0x74 +CONF_ODR_FILT_ADDR = 0x75 + +# Coefficient's address +COEFF_ADDR1 = 0x89 +COEFF_ADDR2 = 0xe1 + +# Chip identifier +CHIP_ID_ADDR = 0xd0 +CHIP_VARIANT_ADDR = 0xf0 + +VARIANT_LOW = 0x00 +VARIANT_HIGH = 0x01 + +# Soft reset register +SOFT_RESET_ADDR = 0xe0 + +# Heater control settings +ENABLE_HEATER = 0x00 +DISABLE_HEATER = 0x08 + +# Gas measurement settings +DISABLE_GAS_MEAS = 0x00 +ENABLE_GAS_MEAS = -1 # Now used as auto-select +ENABLE_GAS_MEAS_LOW = 0x01 +ENABLE_GAS_MEAS_HIGH = 0x02 + +# Over-sampling settings +OS_NONE = 0 +OS_1X = 1 +OS_2X = 2 +OS_4X = 3 +OS_8X = 4 +OS_16X = 5 + +# IIR filter settings +FILTER_SIZE_0 = 0 +FILTER_SIZE_1 = 1 +FILTER_SIZE_3 = 2 +FILTER_SIZE_7 = 3 +FILTER_SIZE_15 = 4 +FILTER_SIZE_31 = 5 +FILTER_SIZE_63 = 6 +FILTER_SIZE_127 = 7 + +# Power mode settings +SLEEP_MODE = 0 +FORCED_MODE = 1 + +# Delay related macro declaration +RESET_PERIOD = 10 + +# SPI memory page settings +MEM_PAGE0 = 0x10 +MEM_PAGE1 = 0x00 + +# Ambient humidity shift value for compensation +HUM_REG_SHIFT_VAL = 4 + +# Run gas enable and disable settings +RUN_GAS_DISABLE = 0 +RUN_GAS_ENABLE = 1 + +# Gas heater enable and disable settings +GAS_HEAT_ENABLE = 0 +GAS_HEAT_DISABLE = 1 + +# Buffer length macro declaration +TMP_BUFFER_LENGTH = 40 +REG_BUFFER_LENGTH = 6 +FIELD_DATA_LENGTH = 3 +GAS_REG_BUF_LENGTH = 20 +GAS_HEATER_PROF_LEN_MAX = 10 + +# Settings selector +OST_SEL = 1 +OSP_SEL = 2 +OSH_SEL = 4 +GAS_MEAS_SEL = 8 +FILTER_SEL = 16 +HCNTRL_SEL = 32 +RUN_GAS_SEL = 64 +NBCONV_SEL = 128 +GAS_SENSOR_SEL = GAS_MEAS_SEL | RUN_GAS_SEL | NBCONV_SEL + +# Number of conversion settings +NBCONV_MIN = 0 +NBCONV_MAX = 9 # Was 10, but there are only 10 settings: 0 1 2 ... 8 9 + +# Mask definitions +GAS_MEAS_MSK = 0x30 +NBCONV_MSK = 0X0F +FILTER_MSK = 0X1C +OST_MSK = 0XE0 +OSP_MSK = 0X1C +OSH_MSK = 0X07 +HCTRL_MSK = 0x08 +RUN_GAS_MSK = 0x30 +MODE_MSK = 0x03 +RHRANGE_MSK = 0x30 +RSERROR_MSK = 0xf0 +NEW_DATA_MSK = 0x80 +GAS_INDEX_MSK = 0x0f +GAS_RANGE_MSK = 0x0f +GASM_VALID_MSK = 0x20 +HEAT_STAB_MSK = 0x10 +MEM_PAGE_MSK = 0x10 +SPI_RD_MSK = 0x80 +SPI_WR_MSK = 0x7f +BIT_H1_DATA_MSK = 0x0F + +# Bit position definitions for sensor settings +GAS_MEAS_POS = 4 +FILTER_POS = 2 +OST_POS = 5 +OSP_POS = 2 +OSH_POS = 0 +HCTRL_POS = 3 +RUN_GAS_POS = 4 +MODE_POS = 0 +NBCONV_POS = 0 + +# Array Index to Field data mapping for Calibration Data +T2_LSB_REG = 1 +T2_MSB_REG = 2 +T3_REG = 3 +P1_LSB_REG = 5 +P1_MSB_REG = 6 +P2_LSB_REG = 7 +P2_MSB_REG = 8 +P3_REG = 9 +P4_LSB_REG = 11 +P4_MSB_REG = 12 +P5_LSB_REG = 13 +P5_MSB_REG = 14 +P7_REG = 15 +P6_REG = 16 +P8_LSB_REG = 19 +P8_MSB_REG = 20 +P9_LSB_REG = 21 +P9_MSB_REG = 22 +P10_REG = 23 +H2_MSB_REG = 25 +H2_LSB_REG = 26 +H1_LSB_REG = 26 +H1_MSB_REG = 27 +H3_REG = 28 +H4_REG = 29 +H5_REG = 30 +H6_REG = 31 +H7_REG = 32 +T1_LSB_REG = 33 +T1_MSB_REG = 34 +GH2_LSB_REG = 35 +GH2_MSB_REG = 36 +GH1_REG = 37 +GH3_REG = 38 + +# BME680 register buffer index settings +REG_FILTER_INDEX = 5 +REG_TEMP_INDEX = 4 +REG_PRES_INDEX = 4 +REG_HUM_INDEX = 2 +REG_NBCONV_INDEX = 1 +REG_RUN_GAS_INDEX = 1 +REG_HCTRL_INDEX = 0 + +# Look up tables for the possible gas range values +lookupTable1 = [2147483647, 2147483647, 2147483647, 2147483647, + 2147483647, 2126008810, 2147483647, 2130303777, 2147483647, + 2147483647, 2143188679, 2136746228, 2147483647, 2126008810, + 2147483647, 2147483647] + +lookupTable2 = [4096000000, 2048000000, 1024000000, 512000000, + 255744255, 127110228, 64000000, 32258064, + 16016016, 8000000, 4000000, 2000000, + 1000000, 500000, 250000, 125000] + + +def bytes_to_word(msb, lsb, bits=16, signed=False): + """Convert a most and least significant byte into a word.""" + # TODO: Reimplement with struct + word = (msb << 8) | lsb + if signed: + word = twos_comp(word, bits) + return word + + +def twos_comp(val, bits=16): + """Convert two bytes into a two's compliment signed word.""" + # TODO: Reimplement with struct + if val & (1 << (bits - 1)) != 0: + val = val - (1 << bits) + return val + + +class FieldData: + """Structure for storing BME680 sensor data.""" + + def __init__(self): # noqa D107 + # Contains new_data, gasm_valid & heat_stab + self.status = None + self.heat_stable = False + # The index of the heater profile used + self.gas_index = None + # Measurement index to track order + self.meas_index = None + # Temperature in degree celsius x100 + self.temperature = None + # Pressure in Pascal + self.pressure = None + # Humidity in % relative humidity x1000 + self.humidity = None + # Gas resistance in Ohms + self.gas_resistance = None + + +class CalibrationData: + """Structure for storing BME680 calibration data.""" + + def __init__(self): # noqa D107 + self.par_h1 = None + self.par_h2 = None + self.par_h3 = None + self.par_h4 = None + self.par_h5 = None + self.par_h6 = None + self.par_h7 = None + self.par_gh1 = None + self.par_gh2 = None + self.par_gh3 = None + self.par_t1 = None + self.par_t2 = None + self.par_t3 = None + self.par_p1 = None + self.par_p2 = None + self.par_p3 = None + self.par_p4 = None + self.par_p5 = None + self.par_p6 = None + self.par_p7 = None + self.par_p8 = None + self.par_p9 = None + self.par_p10 = None + # Variable to store t_fine size + self.t_fine = None + # Variable to store heater resistance range + self.res_heat_range = None + # Variable to store heater resistance value + self.res_heat_val = None + # Variable to store error range + self.range_sw_err = None + + def set_from_array(self, calibration): + """Set parameters from an array of bytes.""" + # Temperature related coefficients + self.par_t1 = bytes_to_word(calibration[T1_MSB_REG], calibration[T1_LSB_REG]) + self.par_t2 = bytes_to_word(calibration[T2_MSB_REG], calibration[T2_LSB_REG], bits=16, signed=True) + self.par_t3 = twos_comp(calibration[T3_REG], bits=8) + + # Pressure related coefficients + self.par_p1 = bytes_to_word(calibration[P1_MSB_REG], calibration[P1_LSB_REG]) + self.par_p2 = bytes_to_word(calibration[P2_MSB_REG], calibration[P2_LSB_REG], bits=16, signed=True) + self.par_p3 = twos_comp(calibration[P3_REG], bits=8) + self.par_p4 = bytes_to_word(calibration[P4_MSB_REG], calibration[P4_LSB_REG], bits=16, signed=True) + self.par_p5 = bytes_to_word(calibration[P5_MSB_REG], calibration[P5_LSB_REG], bits=16, signed=True) + self.par_p6 = twos_comp(calibration[P6_REG], bits=8) + self.par_p7 = twos_comp(calibration[P7_REG], bits=8) + self.par_p8 = bytes_to_word(calibration[P8_MSB_REG], calibration[P8_LSB_REG], bits=16, signed=True) + self.par_p9 = bytes_to_word(calibration[P9_MSB_REG], calibration[P9_LSB_REG], bits=16, signed=True) + self.par_p10 = calibration[P10_REG] + + # Humidity related coefficients + self.par_h1 = (calibration[H1_MSB_REG] << HUM_REG_SHIFT_VAL) | (calibration[H1_LSB_REG] & BIT_H1_DATA_MSK) + self.par_h2 = (calibration[H2_MSB_REG] << HUM_REG_SHIFT_VAL) | (calibration[H2_LSB_REG] >> HUM_REG_SHIFT_VAL) + self.par_h3 = twos_comp(calibration[H3_REG], bits=8) + self.par_h4 = twos_comp(calibration[H4_REG], bits=8) + self.par_h5 = twos_comp(calibration[H5_REG], bits=8) + self.par_h6 = calibration[H6_REG] + self.par_h7 = twos_comp(calibration[H7_REG], bits=8) + + # Gas heater related coefficients + self.par_gh1 = twos_comp(calibration[GH1_REG], bits=8) + self.par_gh2 = bytes_to_word(calibration[GH2_MSB_REG], calibration[GH2_LSB_REG], bits=16, signed=True) + self.par_gh3 = twos_comp(calibration[GH3_REG], bits=8) + + def set_other(self, heat_range, heat_value, sw_error): + """Set other values.""" + self.res_heat_range = (heat_range & RHRANGE_MSK) // 16 + self.res_heat_val = heat_value + self.range_sw_err = (sw_error & RSERROR_MSK) // 16 + + +class TPHSettings: + """Structure for storing BME680 sensor settings. + + Comprises of output data rate, over-sampling and filter settings. + + """ + + def __init__(self): # noqa D107 + # Humidity oversampling + self.os_hum = None + # Temperature oversampling + self.os_temp = None + # Pressure oversampling + self.os_pres = None + # Filter coefficient + self.filter = None + + +class GasSettings: + """Structure for storing BME680 gas settings and status.""" + + def __init__(self): # noqa D107 + # Variable to store nb conversion + self.nb_conv = None + # Variable to store heater control + self.heatr_ctrl = None + # Run gas enable value + self.run_gas = None + # Pointer to store heater temperature + self.heatr_temp = None + # Pointer to store duration profile + self.heatr_dur = None + + +class BME680Data: + """Structure to represent BME680 device.""" + + def __init__(self): # noqa D107 + # Chip Id + self.chip_id = None + # Device Id + self.dev_id = None + # SPI/I2C interface + self.intf = None + # Memory page used + self.mem_page = None + # Ambient temperature in Degree C + self.ambient_temperature = None + # Field Data + self.data = FieldData() + # Sensor calibration data + self.calibration_data = CalibrationData() + # Sensor settings + self.tph_settings = TPHSettings() + # Gas Sensor settings + self.gas_settings = GasSettings() + # Sensor power modes + self.power_mode = None + # New sensor fields + self.new_fields = None From da5d4acf6e48f875a66a36a879159d760f6fb376 Mon Sep 17 00:00:00 2001 From: Luthira A <79228258+Luthiraa@users.noreply.github.com> Date: Tue, 11 Mar 2025 23:35:43 -0400 Subject: [PATCH 07/19] Remove deprecated sensor libraries and example scripts --- scripts/tempSensor/adafruit_bme680.mpy | Bin 7402 -> 0 bytes scripts/tempSensor/adafruit_bme680.py | 769 ------------------ .../adafruit_bus_device/__init__.py | 0 .../adafruit_bus_device/i2c_device.py | 187 ----- .../adafruit_bus_device/spi_device.py | 121 --- scripts/tempSensor/bme60.py | 421 ---------- scripts/tempSensor/bme680i.py | 418 ---------- scripts/tempSensor/constants.py | 12 - scripts/tempSensor/error.txt | 3 - scripts/tempSensor/lib/CHANGELOG.md | 50 -- scripts/tempSensor/lib/README.md | 56 -- .../adafruit_blinka-8.49.0.dist-info/METADATA | 8 - .../adafruit_blinka-8.49.0.dist-info/RECORD | 2 - scripts/tempSensor/lib/as7341.py | 608 -------------- scripts/tempSensor/lib/as7341_sensor.py | 149 ---- scripts/tempSensor/lib/as7341_smux_select.py | 49 -- .../lib/bme680-2.0.0.dist-info/METADATA | 156 ---- .../lib/bme680-2.0.0.dist-info/RECORD | 7 - scripts/tempSensor/{ => lib}/bme680.py | 0 scripts/tempSensor/lib/bme680/__init__.py | 486 ----------- scripts/tempSensor/lib/bme680/constants.py | 413 ---------- scripts/tempSensor/lib/data_logging.py | 166 ---- scripts/tempSensor/lib/functools.py | 28 - scripts/tempSensor/lib/sdcard/LICENSE | 21 - scripts/tempSensor/lib/sdcard/sdcard.py | 302 ------- scripts/tempSensor/lib/sdl_demo_utils.py | 276 ------- .../lib/smbus2-0.5.0.dist-info/METADATA | 234 ------ .../lib/smbus2-0.5.0.dist-info/RECORD | 6 - scripts/tempSensor/lib/smbus2/__init__.py | 26 - scripts/tempSensor/lib/smbus2/py.typed | 0 scripts/tempSensor/lib/smbus2/smbus2.py | 660 --------------- scripts/tempSensor/lib/smbus2/smbus2.pyi | 148 ---- scripts/tempSensor/lib/ufastrsa/__init__.py | 0 scripts/tempSensor/lib/ufastrsa/genprime.py | 136 ---- scripts/tempSensor/lib/ufastrsa/rsa.py | 46 -- scripts/tempSensor/lib/ufastrsa/srandom.py | 47 -- scripts/tempSensor/lib/ufastrsa/util.py | 14 - .../lib/unique_id-1.0.1.dist-info/METADATA | 11 - .../lib/unique_id-1.0.1.dist-info/RECORD | 5 - scripts/tempSensor/lib/unique_id/__init__.py | 1 - scripts/tempSensor/lib/unique_id/main.py | 58 -- scripts/tempSensor/lib/unique_id/tests.py | 40 - scripts/tempSensor/lib/urequests_2.py | 203 ----- scripts/tempSensor/main.py | 77 ++ scripts/tempSensor/rsa.json | 1 - scripts/tempSensor/secrets.py | 83 -- scripts/tempSensor/temp.py | 19 - scripts/tempSensor/tempSensor.py | 18 - scripts/tempSensor/test.py | 65 -- scripts/tempSensor/{lib => }/umqtt/robust.py | 0 scripts/tempSensor/{lib => }/umqtt/simple.py | 0 51 files changed, 77 insertions(+), 6529 deletions(-) delete mode 100644 scripts/tempSensor/adafruit_bme680.mpy delete mode 100644 scripts/tempSensor/adafruit_bme680.py delete mode 100644 scripts/tempSensor/adafruit_bus_device/__init__.py delete mode 100644 scripts/tempSensor/adafruit_bus_device/i2c_device.py delete mode 100644 scripts/tempSensor/adafruit_bus_device/spi_device.py delete mode 100644 scripts/tempSensor/bme60.py delete mode 100644 scripts/tempSensor/bme680i.py delete mode 100644 scripts/tempSensor/constants.py delete mode 100644 scripts/tempSensor/error.txt delete mode 100644 scripts/tempSensor/lib/CHANGELOG.md delete mode 100644 scripts/tempSensor/lib/README.md delete mode 100644 scripts/tempSensor/lib/adafruit_blinka-8.49.0.dist-info/METADATA delete mode 100644 scripts/tempSensor/lib/adafruit_blinka-8.49.0.dist-info/RECORD delete mode 100644 scripts/tempSensor/lib/as7341.py delete mode 100644 scripts/tempSensor/lib/as7341_sensor.py delete mode 100644 scripts/tempSensor/lib/as7341_smux_select.py delete mode 100644 scripts/tempSensor/lib/bme680-2.0.0.dist-info/METADATA delete mode 100644 scripts/tempSensor/lib/bme680-2.0.0.dist-info/RECORD rename scripts/tempSensor/{ => lib}/bme680.py (100%) delete mode 100644 scripts/tempSensor/lib/bme680/__init__.py delete mode 100644 scripts/tempSensor/lib/bme680/constants.py delete mode 100644 scripts/tempSensor/lib/data_logging.py delete mode 100644 scripts/tempSensor/lib/functools.py delete mode 100644 scripts/tempSensor/lib/sdcard/LICENSE delete mode 100644 scripts/tempSensor/lib/sdcard/sdcard.py delete mode 100644 scripts/tempSensor/lib/sdl_demo_utils.py delete mode 100644 scripts/tempSensor/lib/smbus2-0.5.0.dist-info/METADATA delete mode 100644 scripts/tempSensor/lib/smbus2-0.5.0.dist-info/RECORD delete mode 100644 scripts/tempSensor/lib/smbus2/__init__.py delete mode 100644 scripts/tempSensor/lib/smbus2/py.typed delete mode 100644 scripts/tempSensor/lib/smbus2/smbus2.py delete mode 100644 scripts/tempSensor/lib/smbus2/smbus2.pyi delete mode 100644 scripts/tempSensor/lib/ufastrsa/__init__.py delete mode 100644 scripts/tempSensor/lib/ufastrsa/genprime.py delete mode 100644 scripts/tempSensor/lib/ufastrsa/rsa.py delete mode 100644 scripts/tempSensor/lib/ufastrsa/srandom.py delete mode 100644 scripts/tempSensor/lib/ufastrsa/util.py delete mode 100644 scripts/tempSensor/lib/unique_id-1.0.1.dist-info/METADATA delete mode 100644 scripts/tempSensor/lib/unique_id-1.0.1.dist-info/RECORD delete mode 100644 scripts/tempSensor/lib/unique_id/__init__.py delete mode 100644 scripts/tempSensor/lib/unique_id/main.py delete mode 100644 scripts/tempSensor/lib/unique_id/tests.py delete mode 100644 scripts/tempSensor/lib/urequests_2.py create mode 100644 scripts/tempSensor/main.py delete mode 100644 scripts/tempSensor/rsa.json delete mode 100644 scripts/tempSensor/secrets.py delete mode 100644 scripts/tempSensor/temp.py delete mode 100644 scripts/tempSensor/tempSensor.py delete mode 100644 scripts/tempSensor/test.py rename scripts/tempSensor/{lib => }/umqtt/robust.py (100%) rename scripts/tempSensor/{lib => }/umqtt/simple.py (100%) diff --git a/scripts/tempSensor/adafruit_bme680.mpy b/scripts/tempSensor/adafruit_bme680.mpy deleted file mode 100644 index 0e0d14c46498b6128cdf05e935f856080d23fab7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7402 zcmbtZYfu|kmcA_^gm_9VA>@GLb~gf9U?ep{V2_=_GQ{AJ3D}T?Y?4Mu4JhHENW!u6 zShoa}!(QsbQ0;+CQ^Z-4fVNW@c*uzcSgKs+}ME%qF#|%A_h)UJ{`y&1}_f z?YS)>gdv+$4F>5x&OPVcbH95Y*C$nq1Js}NzY1v4CngSF>@BA6xJrL8xN@Ary{Hr$xkuXqOGUW+R7zt>Vhr+>G zZ&+@Zg#A-Iw6v(lO4OoI9qKWno?-T&vEIi|dMf}L z$BT-4*^}Dhv;DnRMQ!QmY{)-Tr!o@wZ&=XQo#A0Kqm#UQc5I9f0(*tu1g9&|6FSuJNmr3De40SdXiHmL+FOno zjU0E54}#!nj$@Ef%W*+IFvD>|Bs#wkw-y^4IQOI7U`X!liAVeUy3d>#>^>_*<}O|R zG5uXyxM(ferg*rwZ?OB!+1{tR&oTs~V3bS|qhg8~HB-W9m{O*UDQ7B}N~Vg@TD8Up zxxU99dt|7e8$99e>*gGcjv*O6V_>QoBU8hen0?FxOf6H#>}MWi4lva2B`ixDO>oRy zh==3$heIr5tS{_yfte6zXX}lXuq%&`bq=y-?Sm(_5R%=PQ(+_q*#>RHed8RQjZW!Q z8kK`=p+d@_4{{n_Koif@4x zW96*F+3xIUbvfHx?0-m)b=mD59S-|mTw3k$uf6@-NN9C*bhz4&WVy7u+MOvg-2t?+BBieFC8ez_ z+ty)sIf1Cu;j+uW(AI3r&}>s{w%KbOc}XR0auZgSuyPZ(9CEY8?mMV$Y8A{BB5j7! z&?L_XM25(?h~WV*fl8-m;dYO0^Lkn=BC9pR_eVOBDE($5nfOxLxaUPk8|241pC{}w z0#v6x0Nbh1^Z7O)wFFQ0HJ;C6Z5?frwwz*pDi=C!(2h^|s?MUa9 z!_ReIQ5XJQR$s1puJiH%_}0f-Vn&|g`IvT*iq*u*;jbF$P>m7%gVKl|INOXaYfV9Z zEXao@xS%J@kG3Ks(&@B;;7ouIhA*&$lQ1>}U_LY(zR z5~WVXDlzHA?3CZ<4`0|xs%DFFL@M}h4)asM-4ou$Qq30U=+y9S9P>|t050Tz3CmNn zCAmoro-AOZQ?jKn1X^eHgnk+H=^!}nu>Ty-ZD|D)<(RObUxE9c$*?~>>*Ha*68gpC zo)BAQq&U!WY$iA*J00-!tky`&ZO-eN^pC<0z}>PsqaiyudLhiSq_H&Xqx8md?!}-V zh786!?AU-QH^GCEgJ-P7+16@soOl=y1vv;SW316=l80sj+|-PZXKRe4+?OX!#yTw4 z3*57O*nLKVePC-8bAQp}4-a@z?WGQBK@d9KNY^SPJ8jQfEzdnVZhNd@#B4V6r{KeE z?lzw?BhgSrM@Hy~tBG(ahDkXD<}L=KfNLQdYUoJ6i&#t0g6E^IQh`J)qfGqHN${e9 zpd$enA(|fGyc7Nacg_>^d#1ypq1J_9VHqtvMGGOBF{TxKw7|$*B$~8baCREoeLyskoX6*t1>`%J zD|EE-46w_$vnvepi3L5=<2=k%a$#US%?o(Plb%pmK4kz!M9PRJ8q$Ml7|GrwoJzbU zi<}yjbr%iobhML>7{SZ~UL`_{J8s>W3huVOEyO(R@lWzTDm+7hC?Ay#EC;ERU|_1Z zi?W|LpC3~@?2gtJwyjU6-RojHg#QHpwpXwh_2waPUuZGnq)D|}_^qWMNu>Hey0ijG z7|_!aK25c|L_TG>)I9czXwjIx@UnXdk-gLB0Qx>^+Y?|t_BacTa1Zp*i2BAILQ~&P zsOD=VeU0nT2*1w?YuX{~fB!IHh5knqR~4GIAVU)MwnCxt(r+iU1bekm`IbF#FGB4z z6uLD{Tt_w9;Fj)kMEHv755>}S1f}RG%B)Io!7}pUF7%yX^Nvs*H&Knv%}s~x?R|}{ zP0bA*c4r$fRLcz1(F{G8Mw_eBdY5~AL^eGd!#Tx=*k57UyUC+3IJ(R6Ap zF`t3X+z>r4An0y!5q^pAcywqfonFAL$dHtdW>TPJTmW6^sDPvtj0;0)+=db1mjV*? z?VIb#1igeZoAeLb((6en6HTv+dga1Uav=l1X`n;!2^6bIS@tF1fXCL7*a~yM2d0ZK zv>>3&&7_n_En_jS4S|``sd(nI^<>^m$28jA;h`*UgK(|ShX>i9m$Waq2xZ&}M{)Rj z)WdxTyd(8Z4z|Jmu+;*ltdLDvQE(Qb{s^{V{!SFwP#B7*H?akypk<^dv$+OsFb{mr z;jQ>GQqI1(4FjDS+(^+80i|GHD6^hOrf&jyOy1P36-@qGb}8)e)-tBdWItv4+bh?= z_IH-Ar|@d}(u8cjyEmZq#a*%(yw(kn;8ftY=ll%;S~s#zqW%b(kBe3Dvz-tw^!woO z{>{n|q+CN8&`ThG4_}^v=%Lt5L*{}O$CM7pTZ-*1jwANg`SV9CIJj!!Pf?A%)Q$nn8oIz`@Ju84Xy*bg9_0lsPsNDLqWZAz)N`-xdmuL3ZG z(9F%{1YjB@k?7f^laOt8)%ikyMOA#Vvu*UusbK%1Cs?cHFwGpY+WH)hV}}kj^c`sE zwI6P_qg<>dr)H*S!ZXu;uM76LGp=lk`rT3GpSu*ofLAo+QVUv`74_eVewhoiN?Hhu z`d#5N_NA!bGqy&*yq8<4qJ>zh^1*2jMVk1knJLFJSxUB5Qmjl9lczVG{19r1u223KLGo7KuCO+)EG2c4JN(%W}Xz^P$z`V)?|F>>V{}?xLbD8 zUb(u9R+D@~riIv8h5aL)O>cI%iD-KB=CbS?WUqW{IhuiA`R?An2IJsA;^095w6{UE zm1`-i#8!%ujoZ1T;?kg}X?N1v7Jr*+e(V{G&DA>7#6DtgLWF2A(-F;EE=40@h7P26 zp14c#StCKBNwV5d4YwY-%|zsHavO3IjRdPOLxIppNR*QJOCiz(C4gwEkfaHBE=IT1hN!SeTuP$O|p_)8hsZz!N8fYZ$ky5l~Mu{Pq-OSF)^Gap> zoo|?b_Z#Ma^0o7WR;@-E7a`yoYNZIqz3jtqbqibIgI%?iN>zLrk;vvmkPG6>QB^+4 zPmhNuVD7j&etpNB8oe7GJXLpJsiTU&KQh8J4-Yk<9tV;JK0aA2BnCe2+6OoO{`yDi zCIlgYKeXliG=4DacSRM$ZVk{Xr3B>8r>oEtB(i_fRV)nmd-3zv8Cfr5wyu&dQoapI zc&zJ7)lrou{-OLt=(bp`)_VF-cQ;MW_n;PzljHZiNuJ<1P{+!1j7%^EU-m2)W~p z>&C*|HD}RXa*v0u_pYuul^>AG_h|e4b!QQ%wjZS4$T?Lgre1hYZf6grre@#Vv7?si zn)nwZ^eDxgu5YM+w7dgVY6v6EY)mk2g*n zN}a?oEI9e$MG2Nw!?F!&ZsQ&-#gQhvG^;~pu4Iii-z}=EEn6Z+s0NG0au_8eA9dl1 z!p^`80Nl3wYFMZ_ZsrFeoXaI_!bRN5R*=ivTlTHwwv20%nwqN6;ZvSak6cc7pZD?s zT#6s1^5rrGag~DF-3#RuuBBtmm$l7h6-yN(jg5^^{+@OmZ)j+;eGkHJyD*eZ!ywlL z#|qh1+b#HmGlgi9E8yAMN~HEk+MFDCb;q%>rL zkh0N{yO6S7&ZIxM@gC#}zj}XVK9%{9jwr9mxkl>mfyvK)4rDKFzLRzm{{Y=z&}<4X zbrQe+I~mm_I9|n^?`W-6RZI0F&CShL>*>>$CUZ-}cMu6lrcQvcw?-C4`W$JE{DatUaG*g`VV3|>8eszN>d5=_qqe`dJAO7t9 z>uFh2M)ULe`IS^A9f@3HO4HDIYZGhz3$F%FLpN6BfLE!pA}4-mRJXG2F8sy_IUl^m zwT-FqH{04aR*WAAVM5Lbm8~t#Y=~l`EHT*Q|dr2!2}Y|%1ML6twe>>YUe83=C#TFm?Y zoSP3-YU*D;YGc)-;55uTcH23<5~Ty%9h^R*1Bqk%25M#a2I&szXw!5 z{gGgw1@KhGs&>NXwJTH(KQ*JDvDBHU?2g! z%9j3VnsUfE!bEu-HP$Kb3|}{pAbJZlTJ5N3)`zXM3?UH%2v)2}rv2N{slK)YIxt`4 z4V4|z%=L{^wKYqhO!QbSHW@SUSP;Tq8G;%3-(dg^x`%-S^^gA4u`MulLUz74ko4aO zB<4_o>9?R`u2}kF+w|rgrl0S-q@@|v(tk}@8ZcgLfR}AGby+oqi=Tpux>L{Y0#T!k zo!Xt)!CTtG&!+|W(SJMq==a2ru43uGw>P2Nv580>Yp7B!{SR%S!GmG5;&Lqx;Zg?{ JFASGu{|AU;UgrP+ diff --git a/scripts/tempSensor/adafruit_bme680.py b/scripts/tempSensor/adafruit_bme680.py deleted file mode 100644 index 7c7d5d4b..00000000 --- a/scripts/tempSensor/adafruit_bme680.py +++ /dev/null @@ -1,769 +0,0 @@ -# SPDX-FileCopyrightText: 2017 ladyada for Adafruit Industries -# -# SPDX-License-Identifier: MIT AND BSD-3-Clause - - -""" -`adafruit_bme680` -================================================================================ - -CircuitPython library for BME680 temperature, pressure and humidity sensor. - - -* Author(s): Limor Fried, William Garber, many others - - -Implementation Notes --------------------- - -**Hardware:** - -* `Adafruit BME680 Temp, Humidity, Pressure and Gas Sensor `_ - -**Software and Dependencies:** - -* Adafruit CircuitPython firmware for the supported boards: - https://github.com/adafruit/circuitpython/releases -* Adafruit's Bus Device library: https://github.com/adafruit/Adafruit_CircuitPython_BusDevice -""" - -import math -import struct -import time - -from micropython import const - - -def delay_microseconds(nusec): - """HELP must be same as dev->delay_us""" - time.sleep(nusec / 1000000.0) - - -try: - # Used only for type annotations. - - import typing - - from busio import I2C, SPI - from circuitpython_typing import ReadableBuffer - from digitalio import DigitalInOut - -except ImportError: - pass - -__version__ = "3.7.9" -__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_BME680.git" - - -# I2C ADDRESS/BITS/SETTINGS NEW -# ----------------------------------------------------------------------- -_BME68X_ENABLE_HEATER = const(0x00) -_BME68X_DISABLE_HEATER = const(0x01) -_BME68X_DISABLE_GAS_MEAS = const(0x00) -_BME68X_ENABLE_GAS_MEAS_L = const(0x01) -_BME68X_ENABLE_GAS_MEAS_H = const(0x02) -_BME68X_SLEEP_MODE = const(0) -_BME68X_FORCED_MODE = const(1) -_BME68X_VARIANT_GAS_LOW = const(0x00) -_BME68X_VARIANT_GAS_HIGH = const(0x01) -_BME68X_HCTRL_MSK = const(0x08) -_BME68X_HCTRL_POS = const(3) -_BME68X_NBCONV_MSK = const(0x0F) -_BME68X_RUN_GAS_MSK = const(0x30) -_BME68X_RUN_GAS_POS = const(4) -_BME68X_MODE_MSK = const(0x03) -_BME68X_PERIOD_POLL = const(10000) -_BME68X_REG_CTRL_GAS_0 = const(0x70) -_BME68X_REG_CTRL_GAS_1 = const(0x71) - -# I2C ADDRESS/BITS/SETTINGS -# ----------------------------------------------------------------------- -_BME680_CHIPID = const(0x61) - -_BME680_REG_CHIPID = const(0xD0) -_BME68X_REG_VARIANT = const(0xF0) -_BME680_BME680_COEFF_ADDR1 = const(0x89) -_BME680_BME680_COEFF_ADDR2 = const(0xE1) -_BME680_BME680_RES_HEAT_0 = const(0x5A) -_BME680_BME680_GAS_WAIT_0 = const(0x64) - -_BME680_REG_SOFTRESET = const(0xE0) -_BME680_REG_CTRL_GAS = const(0x71) -_BME680_REG_CTRL_HUM = const(0x72) -_BME680_REG_STATUS = const(0x73) -_BME680_REG_CTRL_MEAS = const(0x74) -_BME680_REG_CONFIG = const(0x75) - -_BME680_REG_MEAS_STATUS = const(0x1D) -_BME680_REG_PDATA = const(0x1F) -_BME680_REG_TDATA = const(0x22) -_BME680_REG_HDATA = const(0x25) - -_BME680_SAMPLERATES = (0, 1, 2, 4, 8, 16) -_BME680_FILTERSIZES = (0, 1, 3, 7, 15, 31, 63, 127) - -_BME680_RUNGAS = const(0x10) - -_LOOKUP_TABLE_1 = ( - 2147483647.0, - 2147483647.0, - 2147483647.0, - 2147483647.0, - 2147483647.0, - 2126008810.0, - 2147483647.0, - 2130303777.0, - 2147483647.0, - 2147483647.0, - 2143188679.0, - 2136746228.0, - 2147483647.0, - 2126008810.0, - 2147483647.0, - 2147483647.0, -) - -_LOOKUP_TABLE_2 = ( - 4096000000.0, - 2048000000.0, - 1024000000.0, - 512000000.0, - 255744255.0, - 127110228.0, - 64000000.0, - 32258064.0, - 16016016.0, - 8000000.0, - 4000000.0, - 2000000.0, - 1000000.0, - 500000.0, - 250000.0, - 125000.0, -) - - -def bme_set_bits(reg_data, bitname_msk, bitname_pos, data): - """ - Macro to set bits - data2 = data << bitname_pos - set masked bits from data2 in reg_data - """ - return (reg_data & ~bitname_msk) | ((data << bitname_pos) & bitname_msk) - - -def bme_set_bits_pos_0(reg_data, bitname_msk, data): - """ - Macro to set bits starting from position 0 - set masked bits from data in reg_data - """ - return (reg_data & ~bitname_msk) | (data & bitname_msk) - - -def _read24(arr: ReadableBuffer) -> float: - """Parse an unsigned 24-bit value as a floating point and return it.""" - ret = 0.0 - # print([hex(i) for i in arr]) - for b in arr: - ret *= 256.0 - ret += float(b & 0xFF) - return ret - - -class Adafruit_BME680: - """Driver from BME680 air quality sensor - - :param int refresh_rate: Maximum number of readings per second. Faster property reads - will be from the previous reading.""" - - def __init__(self, *, refresh_rate: int = 10) -> None: - """Check the BME680 was found, read the coefficients and enable the sensor for continuous - reads.""" - self._write(_BME680_REG_SOFTRESET, [0xB6]) - time.sleep(0.005) - - # Check device ID. - chip_id = self._read_byte(_BME680_REG_CHIPID) - if chip_id != _BME680_CHIPID: - raise RuntimeError("Failed to find BME680! Chip ID 0x%x" % chip_id) - - # Get variant - self._chip_variant = self._read_byte(_BME68X_REG_VARIANT) - - self._read_calibration() - - # set up heater - self._write(_BME680_BME680_RES_HEAT_0, [0x73]) - self._write(_BME680_BME680_GAS_WAIT_0, [0x65]) - - self.sea_level_pressure = 1013.25 - """Pressure in hectoPascals at sea level. Used to calibrate :attr:`altitude`.""" - - # Default oversampling and filter register values. - self._pressure_oversample = 0b011 - self._temp_oversample = 0b100 - self._humidity_oversample = 0b010 - self._filter = 0b010 - - # Gas measurements, as a mask applied to _BME680_RUNGAS - self._run_gas = 0xFF - - self._adc_pres = None - self._adc_temp = None - self._adc_hum = None - self._adc_gas = None - self._gas_range = None - self._t_fine = None - - self._last_reading = 0 - self._min_refresh_time = 1 / refresh_rate - - self._amb_temp = 25 # Copy required parameters from reference bme68x_dev struct - self.set_gas_heater(320, 150) # heater 320 deg C for 150 msec - - @property - def pressure_oversample(self) -> int: - """The oversampling for pressure sensor""" - return _BME680_SAMPLERATES[self._pressure_oversample] - - @pressure_oversample.setter - def pressure_oversample(self, sample_rate: int) -> None: - if sample_rate in _BME680_SAMPLERATES: - self._pressure_oversample = _BME680_SAMPLERATES.index(sample_rate) - else: - raise RuntimeError("Invalid oversample") - - @property - def humidity_oversample(self) -> int: - """The oversampling for humidity sensor""" - return _BME680_SAMPLERATES[self._humidity_oversample] - - @humidity_oversample.setter - def humidity_oversample(self, sample_rate: int) -> None: - if sample_rate in _BME680_SAMPLERATES: - self._humidity_oversample = _BME680_SAMPLERATES.index(sample_rate) - else: - raise RuntimeError("Invalid oversample") - - @property - def temperature_oversample(self) -> int: - """The oversampling for temperature sensor""" - return _BME680_SAMPLERATES[self._temp_oversample] - - @temperature_oversample.setter - def temperature_oversample(self, sample_rate: int) -> None: - if sample_rate in _BME680_SAMPLERATES: - self._temp_oversample = _BME680_SAMPLERATES.index(sample_rate) - else: - raise RuntimeError("Invalid oversample") - - @property - def filter_size(self) -> int: - """The filter size for the built in IIR filter""" - return _BME680_FILTERSIZES[self._filter] - - @filter_size.setter - def filter_size(self, size: int) -> None: - if size in _BME680_FILTERSIZES: - self._filter = _BME680_FILTERSIZES.index(size) - else: - raise RuntimeError("Invalid size") - - @property - def temperature(self) -> float: - """The compensated temperature in degrees Celsius.""" - self._perform_reading() - calc_temp = ((self._t_fine * 5) + 128) / 256 - return calc_temp / 100 - - @property - def pressure(self) -> float: - """The barometric pressure in hectoPascals""" - self._perform_reading() - var1 = (self._t_fine / 2) - 64000 - var2 = ((var1 / 4) * (var1 / 4)) / 2048 - var2 = (var2 * self._pressure_calibration[5]) / 4 - var2 = var2 + (var1 * self._pressure_calibration[4] * 2) - var2 = (var2 / 4) + (self._pressure_calibration[3] * 65536) - var1 = ((((var1 / 4) * (var1 / 4)) / 8192) * (self._pressure_calibration[2] * 32) / 8) + ( - (self._pressure_calibration[1] * var1) / 2 - ) - var1 = var1 / 262144 - var1 = ((32768 + var1) * self._pressure_calibration[0]) / 32768 - calc_pres = 1048576 - self._adc_pres - calc_pres = (calc_pres - (var2 / 4096)) * 3125 - calc_pres = (calc_pres / var1) * 2 - var1 = (self._pressure_calibration[8] * (((calc_pres / 8) * (calc_pres / 8)) / 8192)) / 4096 - var2 = ((calc_pres / 4) * self._pressure_calibration[7]) / 8192 - var3 = (((calc_pres / 256) ** 3) * self._pressure_calibration[9]) / 131072 - calc_pres += (var1 + var2 + var3 + (self._pressure_calibration[6] * 128)) / 16 - return calc_pres / 100 - - @property - def relative_humidity(self) -> float: - """The relative humidity in RH %""" - return self.humidity - - @property - def humidity(self) -> float: - """The relative humidity in RH %""" - self._perform_reading() - temp_scaled = ((self._t_fine * 5) + 128) / 256 - var1 = (self._adc_hum - (self._humidity_calibration[0] * 16)) - ( - (temp_scaled * self._humidity_calibration[2]) / 200 - ) - var2 = ( - self._humidity_calibration[1] - * ( - ((temp_scaled * self._humidity_calibration[3]) / 100) - + ( - ((temp_scaled * ((temp_scaled * self._humidity_calibration[4]) / 100)) / 64) - / 100 - ) - + 16384 - ) - ) / 1024 - var3 = var1 * var2 - var4 = self._humidity_calibration[5] * 128 - var4 = (var4 + ((temp_scaled * self._humidity_calibration[6]) / 100)) / 16 - var5 = ((var3 / 16384) * (var3 / 16384)) / 1024 - var6 = (var4 * var5) / 2 - calc_hum = (((var3 + var6) / 1024) * 1000) / 4096 - calc_hum /= 1000 # get back to RH - - calc_hum = min(calc_hum, 100) - calc_hum = max(calc_hum, 0) - return calc_hum - - @property - def altitude(self) -> float: - """The altitude based on current :attr:`pressure` vs the sea level pressure - (:attr:`sea_level_pressure`) - which you must enter ahead of time)""" - pressure = self.pressure # in Si units for hPascal - return 44330 * (1.0 - math.pow(pressure / self.sea_level_pressure, 0.1903)) - - @property - def gas(self) -> int: - """The gas resistance in ohms""" - self._perform_reading() - if self._chip_variant == 0x01: - # taken from https://github.com/BoschSensortec/BME68x-Sensor-API - var1 = 262144 >> self._gas_range - var2 = self._adc_gas - 512 - var2 *= 3 - var2 = 4096 + var2 - calc_gas_res = (10000 * var1) / var2 - calc_gas_res = calc_gas_res * 100 - else: - var1 = ((1340 + (5 * self._sw_err)) * (_LOOKUP_TABLE_1[self._gas_range])) / 65536 - var2 = ((self._adc_gas * 32768) - 16777216) + var1 - var3 = (_LOOKUP_TABLE_2[self._gas_range] * var1) / 512 - calc_gas_res = (var3 + (var2 / 2)) / var2 - return int(calc_gas_res) - - def _perform_reading(self) -> None: - """Perform a single-shot reading from the sensor and fill internal data structure for - calculations""" - if time.monotonic() - self._last_reading < self._min_refresh_time: - return - - # set filter - self._write(_BME680_REG_CONFIG, [self._filter << 2]) - # turn on temp oversample & pressure oversample - self._write( - _BME680_REG_CTRL_MEAS, - [(self._temp_oversample << 5) | (self._pressure_oversample << 2)], - ) - # turn on humidity oversample - self._write(_BME680_REG_CTRL_HUM, [self._humidity_oversample]) - # gas measurements enabled - if self._chip_variant == 0x01: - self._write(_BME680_REG_CTRL_GAS, [(self._run_gas & _BME680_RUNGAS) << 1]) - else: - self._write(_BME680_REG_CTRL_GAS, [(self._run_gas & _BME680_RUNGAS)]) - ctrl = self._read_byte(_BME680_REG_CTRL_MEAS) - ctrl = (ctrl & 0xFC) | 0x01 # enable single shot! - self._write(_BME680_REG_CTRL_MEAS, [ctrl]) - new_data = False - while not new_data: - data = self._read(_BME680_REG_MEAS_STATUS, 17) - new_data = data[0] & 0x80 != 0 - time.sleep(0.005) - self._last_reading = time.monotonic() - - self._adc_pres = _read24(data[2:5]) / 16 - self._adc_temp = _read24(data[5:8]) / 16 - self._adc_hum = struct.unpack(">H", bytes(data[8:10]))[0] - if self._chip_variant == 0x01: - self._adc_gas = int(struct.unpack(">H", bytes(data[15:17]))[0] / 64) - self._gas_range = data[16] & 0x0F - else: - self._adc_gas = int(struct.unpack(">H", bytes(data[13:15]))[0] / 64) - self._gas_range = data[14] & 0x0F - - var1 = (self._adc_temp / 8) - (self._temp_calibration[0] * 2) - var2 = (var1 * self._temp_calibration[1]) / 2048 - var3 = ((var1 / 2) * (var1 / 2)) / 4096 - var3 = (var3 * self._temp_calibration[2] * 16) / 16384 - self._t_fine = int(var2 + var3) - - def _read_calibration(self) -> None: - """Read & save the calibration coefficients""" - coeff = self._read(_BME680_BME680_COEFF_ADDR1, 25) - coeff += self._read(_BME680_BME680_COEFF_ADDR2, 16) - - coeff = list(struct.unpack(" int: - """Read a byte register value and return it""" - return self._read(register, 1)[0] - - def _read(self, register: int, length: int) -> bytearray: - raise NotImplementedError() - - def _write(self, register: int, values: bytearray) -> None: - raise NotImplementedError() - - def set_gas_heater(self, heater_temp: int, heater_time: int) -> bool: - """ - Enable and configure gas reading + heater (None disables) - :param heater_temp: Desired temperature in degrees Centigrade - :param heater_time: Time to keep heater on in milliseconds - :return: True on success, False on failure - """ - try: - if (heater_temp is None) or (heater_time is None): - self._set_heatr_conf(heater_temp or 0, heater_time or 0, enable=False) - else: - self._set_heatr_conf(heater_temp, heater_time) - except OSError: - return False - return True - - def _set_heatr_conf(self, heater_temp: int, heater_time: int, enable: bool = True) -> None: - # restrict to BME68X_FORCED_MODE - op_mode: int = _BME68X_FORCED_MODE - nb_conv: int = 0 - hctrl: int = _BME68X_ENABLE_HEATER - run_gas: int = 0 - ctrl_gas_data_0: int = 0 - ctrl_gas_data_1: int = 0 - - self._set_op_mode(_BME68X_SLEEP_MODE) - self._set_conf(heater_temp, heater_time, op_mode) - ctrl_gas_data_0 = self._read_byte(_BME68X_REG_CTRL_GAS_0) - ctrl_gas_data_1 = self._read_byte(_BME68X_REG_CTRL_GAS_1) - if enable: - hctrl = _BME68X_ENABLE_HEATER - if self._chip_variant == _BME68X_VARIANT_GAS_HIGH: - run_gas = _BME68X_ENABLE_GAS_MEAS_H - else: - run_gas = _BME68X_ENABLE_GAS_MEAS_L - else: - hctrl = _BME68X_DISABLE_HEATER - run_gas = _BME68X_DISABLE_GAS_MEAS - self._run_gas = ~(run_gas - 1) - - ctrl_gas_data_0 = bme_set_bits(ctrl_gas_data_0, _BME68X_HCTRL_MSK, _BME68X_HCTRL_POS, hctrl) - ctrl_gas_data_1 = bme_set_bits_pos_0(ctrl_gas_data_1, _BME68X_NBCONV_MSK, nb_conv) - ctrl_gas_data_1 = bme_set_bits( - ctrl_gas_data_1, _BME68X_RUN_GAS_MSK, _BME68X_RUN_GAS_POS, run_gas - ) - self._write(_BME68X_REG_CTRL_GAS_0, [ctrl_gas_data_0]) - self._write(_BME68X_REG_CTRL_GAS_1, [ctrl_gas_data_1]) - - def _set_op_mode(self, op_mode: int) -> None: - """ - * @brief This API is used to set the operation mode of the sensor - """ - tmp_pow_mode: int = 0 - pow_mode: int = _BME68X_FORCED_MODE - # Call until in sleep - - # was a do {} while() loop - while pow_mode != _BME68X_SLEEP_MODE: - tmp_pow_mode = self._read_byte(_BME680_REG_CTRL_MEAS) - # Put to sleep before changing mode - pow_mode = tmp_pow_mode & _BME68X_MODE_MSK - if pow_mode != _BME68X_SLEEP_MODE: - tmp_pow_mode &= ~_BME68X_MODE_MSK # Set to sleep - self._write(_BME680_REG_CTRL_MEAS, [tmp_pow_mode]) - # dev->delay_us(_BME68X_PERIOD_POLL, dev->intf_ptr) # HELP - delay_microseconds(_BME68X_PERIOD_POLL) - # Already in sleep - if op_mode != _BME68X_SLEEP_MODE: - tmp_pow_mode = (tmp_pow_mode & ~_BME68X_MODE_MSK) | (op_mode & _BME68X_MODE_MSK) - self._write(_BME680_REG_CTRL_MEAS, [tmp_pow_mode]) - - def _set_conf(self, heater_temp: int, heater_time: int, op_mode: int) -> None: - """ - This internal API is used to set heater configurations - """ - - if op_mode != _BME68X_FORCED_MODE: - raise OSError("GasHeaterException: _set_conf not forced mode") - rh_reg_data: int = self._calc_res_heat(heater_temp) - gw_reg_data: int = self._calc_gas_wait(heater_time) - self._write(_BME680_BME680_RES_HEAT_0, [rh_reg_data]) - self._write(_BME680_BME680_GAS_WAIT_0, [gw_reg_data]) - - def _calc_res_heat(self, temp: int) -> int: - """ - This internal API is used to calculate the heater resistance value using float - """ - gh1: int = self._gas_calibration[0] - gh2: int = self._gas_calibration[1] - gh3: int = self._gas_calibration[2] - htr: int = self._heat_range - htv: int = self._heat_val - amb: int = self._amb_temp - - temp = min(temp, 400) # Cap temperature - - var1: int = ((int(amb) * gh3) / 1000) * 256 - var2: int = (gh1 + 784) * (((((gh2 + 154009) * temp * 5) / 100) + 3276800) / 10) - var3: int = var1 + (var2 / 2) - var4: int = var3 / (htr + 4) - var5: int = (131 * htv) + 65536 - heatr_res_x100: int = int(((var4 / var5) - 250) * 34) - heatr_res: int = int((heatr_res_x100 + 50) / 100) - - return heatr_res - - def _calc_res_heat(self, temp: int) -> int: - """ - This internal API is used to calculate the heater resistance value - """ - gh1: float = float(self._gas_calibration[0]) - gh2: float = float(self._gas_calibration[1]) - gh3: float = float(self._gas_calibration[2]) - htr: float = float(self._heat_range) - htv: float = float(self._heat_val) - amb: float = float(self._amb_temp) - - temp = min(temp, 400) # Cap temperature - - var1: float = (gh1 / (16.0)) + 49.0 - var2: float = ((gh2 / (32768.0)) * (0.0005)) + 0.00235 - var3: float = gh3 / (1024.0) - var4: float = var1 * (1.0 + (var2 * float(temp))) - var5: float = var4 + (var3 * amb) - res_heat: int = int(3.4 * ((var5 * (4 / (4 + htr)) * (1 / (1 + (htv * 0.002)))) - 25)) - return res_heat - - def _calc_gas_wait(self, dur: int) -> int: - """ - This internal API is used to calculate the gas wait - """ - factor: int = 0 - durval: int = 0xFF # Max duration - - if dur >= 0xFC0: - return durval - while dur > 0x3F: - dur = dur / 4 - factor += 1 - durval = int(dur + (factor * 64)) - return durval - - -class Adafruit_BME680_I2C(Adafruit_BME680): - """Driver for I2C connected BME680. - - :param ~busio.I2C i2c: The I2C bus the BME680 is connected to. - :param int address: I2C device address. Defaults to :const:`0x77` - :param bool debug: Print debug statements when `True`. Defaults to `False` - :param int refresh_rate: Maximum number of readings per second. Faster property reads - will be from the previous reading. - - **Quickstart: Importing and using the BME680** - - Here is an example of using the :class:`BMP680_I2C` class. - First you will need to import the libraries to use the sensor - - .. code-block:: python - - import board - import adafruit_bme680 - - Once this is done you can define your ``board.I2C`` object and define your sensor object - - .. code-block:: python - - i2c = board.I2C() # uses board.SCL and board.SDA - bme680 = adafruit_bme680.Adafruit_BME680_I2C(i2c) - - You need to setup the pressure at sea level - - .. code-block:: python - - bme680.sea_level_pressure = 1013.25 - - Now you have access to the :attr:`temperature`, :attr:`gas`, :attr:`relative_humidity`, - :attr:`pressure` and :attr:`altitude` attributes - - .. code-block:: python - - temperature = bme680.temperature - gas = bme680.gas - relative_humidity = bme680.relative_humidity - pressure = bme680.pressure - altitude = bme680.altitude - - """ - - def __init__( - self, - i2c: I2C, - address: int = 0x77, - debug: bool = False, - *, - refresh_rate: int = 10, - ) -> None: - """Initialize the I2C device at the 'address' given""" - from adafruit_bus_device import ( - i2c_device, - ) - - self._i2c = i2c_device.I2CDevice(i2c, address) - self._debug = debug - super().__init__(refresh_rate=refresh_rate) - - def _read(self, register: int, length: int) -> bytearray: - """Returns an array of 'length' bytes from the 'register'""" - with self._i2c as i2c: - i2c.write(bytes([register & 0xFF])) - result = bytearray(length) - i2c.readinto(result) - if self._debug: - print(f"\t${register:02X} => {[hex(i) for i in result]}") - return result - - def _write(self, register: int, values: ReadableBuffer) -> None: - """Writes an array of 'length' bytes to the 'register'""" - with self._i2c as i2c: - buffer = bytearray(2 * len(values)) - for i, value in enumerate(values): - buffer[2 * i] = register + i - buffer[2 * i + 1] = value - i2c.write(buffer) - if self._debug: - print(f"\t${values[0]:02X} <= {[hex(i) for i in values[1:]]}") - - -class Adafruit_BME680_SPI(Adafruit_BME680): - """Driver for SPI connected BME680. - - :param ~busio.SPI spi: SPI device - :param ~digitalio.DigitalInOut cs: Chip Select - :param bool debug: Print debug statements when `True`. Defaults to `False` - :param int baudrate: Clock rate, default is :const:`100000` - :param int refresh_rate: Maximum number of readings per second. Faster property reads - will be from the previous reading. - - - **Quickstart: Importing and using the BME680** - - Here is an example of using the :class:`BMP680_SPI` class. - First you will need to import the libraries to use the sensor - - .. code-block:: python - - import board - from digitalio import DigitalInOut, Direction - import adafruit_bme680 - - Once this is done you can define your ``board.SPI`` object and define your sensor object - - .. code-block:: python - - cs = digitalio.DigitalInOut(board.D10) - spi = board.SPI() - bme680 = adafruit_bme680.Adafruit_BME680_SPI(spi, cs) - - You need to setup the pressure at sea level - - .. code-block:: python - - bme680.sea_level_pressure = 1013.25 - - Now you have access to the :attr:`temperature`, :attr:`gas`, :attr:`relative_humidity`, - :attr:`pressure` and :attr:`altitude` attributes - - .. code-block:: python - - temperature = bme680.temperature - gas = bme680.gas - relative_humidity = bme680.relative_humidity - pressure = bme680.pressure - altitude = bme680.altitude - - """ - - def __init__( # noqa: PLR0913 Too many arguments in function definition - self, - spi: SPI, - cs: DigitalInOut, - baudrate: int = 100000, - debug: bool = False, - *, - refresh_rate: int = 10, - ) -> None: - from adafruit_bus_device import ( - spi_device, - ) - - self._spi = spi_device.SPIDevice(spi, cs, baudrate=baudrate) - self._debug = debug - super().__init__(refresh_rate=refresh_rate) - - def _read(self, register: int, length: int) -> bytearray: - if register != _BME680_REG_STATUS: - # _BME680_REG_STATUS exists in both SPI memory pages - # For all other registers, we must set the correct memory page - self._set_spi_mem_page(register) - - register = (register | 0x80) & 0xFF # Read single, bit 7 high. - with self._spi as spi: - spi.write(bytearray([register])) - result = bytearray(length) - spi.readinto(result) - if self._debug: - print(f"\t${register:02X} => {[hex(i) for i in result]}") - return result - - def _write(self, register: int, values: ReadableBuffer) -> None: - if register != _BME680_REG_STATUS: - # _BME680_REG_STATUS exists in both SPI memory pages - # For all other registers, we must set the correct memory page - self._set_spi_mem_page(register) - register &= 0x7F # Write, bit 7 low. - with self._spi as spi: - buffer = bytearray(2 * len(values)) - for i, value in enumerate(values): - buffer[2 * i] = register + i - buffer[2 * i + 1] = value & 0xFF - spi.write(buffer) - if self._debug: - print(f"\t${values[0]:02X} <= {[hex(i) for i in values[1:]]}") - - def _set_spi_mem_page(self, register: int) -> None: - spi_mem_page = 0x00 - if register < 0x80: - spi_mem_page = 0x10 - self._write(_BME680_REG_STATUS, [spi_mem_page]) diff --git a/scripts/tempSensor/adafruit_bus_device/__init__.py b/scripts/tempSensor/adafruit_bus_device/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/scripts/tempSensor/adafruit_bus_device/i2c_device.py b/scripts/tempSensor/adafruit_bus_device/i2c_device.py deleted file mode 100644 index c605290d..00000000 --- a/scripts/tempSensor/adafruit_bus_device/i2c_device.py +++ /dev/null @@ -1,187 +0,0 @@ -# SPDX-FileCopyrightText: 2016 Scott Shawcroft for Adafruit Industries -# -# SPDX-License-Identifier: MIT - -""" -`adafruit_bus_device.i2c_device` - I2C Bus Device -==================================================== -""" - -import time - -try: - from typing import Optional, Type - from types import TracebackType - from circuitpython_typing import ReadableBuffer, WriteableBuffer - - # Used only for type annotations. - from busio import I2C -except ImportError: - pass - - -__version__ = "5.2.10" -__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_BusDevice.git" - - -class I2CDevice: - """ - Represents a single I2C device and manages locking the bus and the device - address. - - :param ~busio.I2C i2c: The I2C bus the device is on - :param int device_address: The 7 bit device address - :param bool probe: Probe for the device upon object creation, default is true - - .. note:: This class is **NOT** built into CircuitPython. See - :ref:`here for install instructions `. - - Example: - - .. code-block:: python - - import busio - from board import * - from adafruit_bus_device.i2c_device import I2CDevice - - with busio.I2C(SCL, SDA) as i2c: - device = I2CDevice(i2c, 0x70) - bytes_read = bytearray(4) - with device: - device.readinto(bytes_read) - # A second transaction - with device: - device.write(bytes_read) - """ - - def __init__(self, i2c: I2C, device_address: int, probe: bool = True) -> None: - self.i2c = i2c - self.device_address = device_address - - if probe: - self.__probe_for_device() - - def readinto( - self, buf: WriteableBuffer, *, start: int = 0, end: Optional[int] = None - ) -> None: - """ - Read into ``buf`` from the device. The number of bytes read will be the - length of ``buf``. - - If ``start`` or ``end`` is provided, then the buffer will be sliced - as if ``buf[start:end]``. This will not cause an allocation like - ``buf[start:end]`` will so it saves memory. - - :param ~WriteableBuffer buffer: buffer to write into - :param int start: Index to start writing at - :param int end: Index to write up to but not include; if None, use ``len(buf)`` - """ - if end is None: - end = len(buf) - self.i2c.readfrom_into(self.device_address, buf, start=start, end=end) - - def write( - self, buf: ReadableBuffer, *, start: int = 0, end: Optional[int] = None - ) -> None: - """ - Write the bytes from ``buffer`` to the device, then transmit a stop - bit. - - If ``start`` or ``end`` is provided, then the buffer will be sliced - as if ``buffer[start:end]``. This will not cause an allocation like - ``buffer[start:end]`` will so it saves memory. - - :param ~ReadableBuffer buffer: buffer containing the bytes to write - :param int start: Index to start writing from - :param int end: Index to read up to but not include; if None, use ``len(buf)`` - """ - if end is None: - end = len(buf) - self.i2c.writeto(self.device_address, buf, start=start, end=end) - - # pylint: disable-msg=too-many-arguments - def write_then_readinto( - self, - out_buffer: ReadableBuffer, - in_buffer: WriteableBuffer, - *, - out_start: int = 0, - out_end: Optional[int] = None, - in_start: int = 0, - in_end: Optional[int] = None - ) -> None: - """ - Write the bytes from ``out_buffer`` to the device, then immediately - reads into ``in_buffer`` from the device. The number of bytes read - will be the length of ``in_buffer``. - - If ``out_start`` or ``out_end`` is provided, then the output buffer - will be sliced as if ``out_buffer[out_start:out_end]``. This will - not cause an allocation like ``buffer[out_start:out_end]`` will so - it saves memory. - - If ``in_start`` or ``in_end`` is provided, then the input buffer - will be sliced as if ``in_buffer[in_start:in_end]``. This will not - cause an allocation like ``in_buffer[in_start:in_end]`` will so - it saves memory. - - :param ~ReadableBuffer out_buffer: buffer containing the bytes to write - :param ~WriteableBuffer in_buffer: buffer containing the bytes to read into - :param int out_start: Index to start writing from - :param int out_end: Index to read up to but not include; if None, use ``len(out_buffer)`` - :param int in_start: Index to start writing at - :param int in_end: Index to write up to but not include; if None, use ``len(in_buffer)`` - """ - if out_end is None: - out_end = len(out_buffer) - if in_end is None: - in_end = len(in_buffer) - - self.i2c.writeto_then_readfrom( - self.device_address, - out_buffer, - in_buffer, - out_start=out_start, - out_end=out_end, - in_start=in_start, - in_end=in_end, - ) - - # pylint: enable-msg=too-many-arguments - - def __enter__(self) -> "I2CDevice": - while not self.i2c.try_lock(): - time.sleep(0) - return self - - def __exit__( - self, - exc_type: Optional[Type[type]], - exc_val: Optional[BaseException], - exc_tb: Optional[TracebackType], - ) -> bool: - self.i2c.unlock() - return False - - def __probe_for_device(self) -> None: - """ - Try to read a byte from an address, - if you get an OSError it means the device is not there - or that the device does not support these means of probing - """ - while not self.i2c.try_lock(): - time.sleep(0) - try: - self.i2c.writeto(self.device_address, b"") - except OSError: - # some OS's dont like writing an empty bytesting... - # Retry by reading a byte - try: - result = bytearray(1) - self.i2c.readfrom_into(self.device_address, result) - except OSError: - # pylint: disable=raise-missing-from - raise ValueError("No I2C device at address: 0x%x" % self.device_address) - # pylint: enable=raise-missing-from - finally: - self.i2c.unlock() diff --git a/scripts/tempSensor/adafruit_bus_device/spi_device.py b/scripts/tempSensor/adafruit_bus_device/spi_device.py deleted file mode 100644 index 60954e0c..00000000 --- a/scripts/tempSensor/adafruit_bus_device/spi_device.py +++ /dev/null @@ -1,121 +0,0 @@ -# SPDX-FileCopyrightText: 2016 Scott Shawcroft for Adafruit Industries -# -# SPDX-License-Identifier: MIT - -# pylint: disable=too-few-public-methods - -""" -`adafruit_bus_device.spi_device` - SPI Bus Device -==================================================== -""" - -import time - -try: - from typing import Optional, Type - from types import TracebackType - - # Used only for type annotations. - from busio import SPI - from digitalio import DigitalInOut -except ImportError: - pass - - -__version__ = "5.2.10" -__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_BusDevice.git" - - -class SPIDevice: - """ - Represents a single SPI device and manages locking the bus and the device - address. - - :param ~busio.SPI spi: The SPI bus the device is on - :param ~digitalio.DigitalInOut chip_select: The chip select pin object that implements the - DigitalInOut API. - :param bool cs_active_value: Set to True if your device requires CS to be active high. - Defaults to False. - :param int baudrate: The desired SCK clock rate in Hertz. The actual clock rate may be - higher or lower due to the granularity of available clock settings (MCU dependent). - :param int polarity: The base state of the SCK clock pin (0 or 1). - :param int phase: The edge of the clock that data is captured. First (0) or second (1). - Rising or falling depends on SCK clock polarity. - :param int extra_clocks: The minimum number of clock cycles to cycle the bus after CS is high. - (Used for SD cards.) - - .. note:: This class is **NOT** built into CircuitPython. See - :ref:`here for install instructions `. - - Example: - - .. code-block:: python - - import busio - import digitalio - from board import * - from adafruit_bus_device.spi_device import SPIDevice - - with busio.SPI(SCK, MOSI, MISO) as spi_bus: - cs = digitalio.DigitalInOut(D10) - device = SPIDevice(spi_bus, cs) - bytes_read = bytearray(4) - # The object assigned to spi in the with statements below - # is the original spi_bus object. We are using the busio.SPI - # operations busio.SPI.readinto() and busio.SPI.write(). - with device as spi: - spi.readinto(bytes_read) - # A second transaction - with device as spi: - spi.write(bytes_read) - """ - - def __init__( - self, - spi: SPI, - chip_select: Optional[DigitalInOut] = None, - *, - cs_active_value: bool = False, - baudrate: int = 100000, - polarity: int = 0, - phase: int = 0, - extra_clocks: int = 0 - ) -> None: - self.spi = spi - self.baudrate = baudrate - self.polarity = polarity - self.phase = phase - self.extra_clocks = extra_clocks - self.chip_select = chip_select - self.cs_active_value = cs_active_value - if self.chip_select: - self.chip_select.switch_to_output(value=not self.cs_active_value) - - def __enter__(self) -> SPI: - while not self.spi.try_lock(): - time.sleep(0) - self.spi.configure( - baudrate=self.baudrate, polarity=self.polarity, phase=self.phase - ) - if self.chip_select: - self.chip_select.value = self.cs_active_value - return self.spi - - def __exit__( - self, - exc_type: Optional[Type[type]], - exc_val: Optional[BaseException], - exc_tb: Optional[TracebackType], - ) -> bool: - if self.chip_select: - self.chip_select.value = not self.cs_active_value - if self.extra_clocks > 0: - buf = bytearray(1) - buf[0] = 0xFF - clocks = self.extra_clocks // 8 - if self.extra_clocks % 8 != 0: - clocks += 1 - for _ in range(clocks): - self.spi.write(buf) - self.spi.unlock() - return False diff --git a/scripts/tempSensor/bme60.py b/scripts/tempSensor/bme60.py deleted file mode 100644 index bd2757ee..00000000 --- a/scripts/tempSensor/bme60.py +++ /dev/null @@ -1,421 +0,0 @@ -# The MIT License (MIT) -# -# Copyright (c) 2017 ladyada for Adafruit Industries -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -# We have a lot of attributes for this complex sensor. -# pylint: disable=too-many-instance-attributes - -""" -`bme680` - BME680 - Temperature, Humidity, Pressure & Gas Sensor -================================================================ - -MicroPython driver from BME680 air quality sensor, based on Adafruit_bme680 - -* Author(s): Limor 'Ladyada' Fried of Adafruit - Jeff Raber (SPI support) - and many more contributors -""" - -import time -import math -from micropython import const -from ubinascii import hexlify as hex -try: - import struct -except ImportError: - import ustruct as struct - -# I2C ADDRESS/BITS/SETTINGS -# ----------------------------------------------------------------------- -_BME680_CHIPID = const(0x61) - -_BME680_REG_CHIPID = const(0xD0) -_BME680_BME680_COEFF_ADDR1 = const(0x89) -_BME680_BME680_COEFF_ADDR2 = const(0xE1) -_BME680_BME680_RES_HEAT_0 = const(0x5A) -_BME680_BME680_GAS_WAIT_0 = const(0x64) - -_BME680_REG_SOFTRESET = const(0xE0) -_BME680_REG_CTRL_GAS = const(0x71) -_BME680_REG_CTRL_HUM = const(0x72) -_BME280_REG_STATUS = const(0xF3) -_BME680_REG_CTRL_MEAS = const(0x74) -_BME680_REG_CONFIG = const(0x75) - -_BME680_REG_PAGE_SELECT = const(0x73) -_BME680_REG_MEAS_STATUS = const(0x1D) -_BME680_REG_PDATA = const(0x1F) -_BME680_REG_TDATA = const(0x22) -_BME680_REG_HDATA = const(0x25) - -_BME680_SAMPLERATES = (0, 1, 2, 4, 8, 16) -_BME680_FILTERSIZES = (0, 1, 3, 7, 15, 31, 63, 127) - -_BME680_RUNGAS = const(0x10) - -_LOOKUP_TABLE_1 = (2147483647.0, 2147483647.0, 2147483647.0, 2147483647.0, 2147483647.0, - 2126008810.0, 2147483647.0, 2130303777.0, 2147483647.0, 2147483647.0, - 2143188679.0, 2136746228.0, 2147483647.0, 2126008810.0, 2147483647.0, - 2147483647.0) - -_LOOKUP_TABLE_2 = (4096000000.0, 2048000000.0, 1024000000.0, 512000000.0, 255744255.0, 127110228.0, - 64000000.0, 32258064.0, 16016016.0, 8000000.0, 4000000.0, 2000000.0, 1000000.0, - 500000.0, 250000.0, 125000.0) - - -def _read24(arr): - """Parse an unsigned 24-bit value as a floating point and return it.""" - ret = 0.0 - #print([hex(i) for i in arr]) - for b in arr: - ret *= 256.0 - ret += float(b & 0xFF) - return ret - - -class Adafruit_BME680: - """Driver from BME680 air quality sensor - - :param int refresh_rate: Maximum number of readings per second. Faster property reads - will be from the previous reading.""" - def __init__(self, *, refresh_rate=10): - """Check the BME680 was found, read the coefficients and enable the sensor for continuous - reads.""" - self._write(_BME680_REG_SOFTRESET, [0xB6]) - time.sleep(0.005) - - # Check device ID. - chip_id = self._read_byte(_BME680_REG_CHIPID) - if chip_id != _BME680_CHIPID: - raise RuntimeError('Failed to find BME680! Chip ID 0x%x' % chip_id) - - self._read_calibration() - - # set up heater - self._write(_BME680_BME680_RES_HEAT_0, [0x73]) - self._write(_BME680_BME680_GAS_WAIT_0, [0x65]) - - self.sea_level_pressure = 1013.25 - """Pressure in hectoPascals at sea level. Used to calibrate ``altitude``.""" - - # Default oversampling and filter register values. - self._pressure_oversample = 0b011 - self._temp_oversample = 0b100 - self._humidity_oversample = 0b010 - self._filter = 0b010 - - self._adc_pres = None - self._adc_temp = None - self._adc_hum = None - self._adc_gas = None - self._gas_range = None - self._t_fine = None - - self._last_reading = time.ticks_ms() - self._min_refresh_time = 1000 // refresh_rate - - @property - def pressure_oversample(self): - """The oversampling for pressure sensor""" - return _BME680_SAMPLERATES[self._pressure_oversample] - - @pressure_oversample.setter - def pressure_oversample(self, sample_rate): - if sample_rate in _BME680_SAMPLERATES: - self._pressure_oversample = _BME680_SAMPLERATES.index(sample_rate) - else: - raise RuntimeError("Invalid oversample") - - @property - def humidity_oversample(self): - """The oversampling for humidity sensor""" - return _BME680_SAMPLERATES[self._humidity_oversample] - - @humidity_oversample.setter - def humidity_oversample(self, sample_rate): - if sample_rate in _BME680_SAMPLERATES: - self._humidity_oversample = _BME680_SAMPLERATES.index(sample_rate) - else: - raise RuntimeError("Invalid oversample") - - @property - def temperature_oversample(self): - """The oversampling for temperature sensor""" - return _BME680_SAMPLERATES[self._temp_oversample] - - @temperature_oversample.setter - def temperature_oversample(self, sample_rate): - if sample_rate in _BME680_SAMPLERATES: - self._temp_oversample = _BME680_SAMPLERATES.index(sample_rate) - else: - raise RuntimeError("Invalid oversample") - - @property - def filter_size(self): - """The filter size for the built in IIR filter""" - return _BME680_FILTERSIZES[self._filter] - - @filter_size.setter - def filter_size(self, size): - if size in _BME680_FILTERSIZES: - self._filter = _BME680_FILTERSIZES[size] - else: - raise RuntimeError("Invalid size") - - @property - def temperature(self): - """The compensated temperature in degrees celsius.""" - self._perform_reading() - calc_temp = (((self._t_fine * 5) + 128) / 256) - return calc_temp / 100 - - @property - def pressure(self): - """The barometric pressure in hectoPascals""" - self._perform_reading() - var1 = (self._t_fine / 2) - 64000 - var2 = ((var1 / 4) * (var1 / 4)) / 2048 - var2 = (var2 * self._pressure_calibration[5]) / 4 - var2 = var2 + (var1 * self._pressure_calibration[4] * 2) - var2 = (var2 / 4) + (self._pressure_calibration[3] * 65536) - var1 = (((((var1 / 4) * (var1 / 4)) / 8192) * - (self._pressure_calibration[2] * 32) / 8) + - ((self._pressure_calibration[1] * var1) / 2)) - var1 = var1 / 262144 - var1 = ((32768 + var1) * self._pressure_calibration[0]) / 32768 - calc_pres = 1048576 - self._adc_pres - calc_pres = (calc_pres - (var2 / 4096)) * 3125 - calc_pres = (calc_pres / var1) * 2 - var1 = (self._pressure_calibration[8] * (((calc_pres / 8) * (calc_pres / 8)) / 8192)) / 4096 - var2 = ((calc_pres / 4) * self._pressure_calibration[7]) / 8192 - var3 = (((calc_pres / 256) ** 3) * self._pressure_calibration[9]) / 131072 - calc_pres += ((var1 + var2 + var3 + (self._pressure_calibration[6] * 128)) / 16) - return calc_pres/100 - - @property - def humidity(self): - """The relative humidity in RH %""" - self._perform_reading() - temp_scaled = ((self._t_fine * 5) + 128) / 256 - var1 = ((self._adc_hum - (self._humidity_calibration[0] * 16)) - - ((temp_scaled * self._humidity_calibration[2]) / 200)) - var2 = (self._humidity_calibration[1] * - (((temp_scaled * self._humidity_calibration[3]) / 100) + - (((temp_scaled * ((temp_scaled * self._humidity_calibration[4]) / 100)) / - 64) / 100) + 16384)) / 1024 - var3 = var1 * var2 - var4 = self._humidity_calibration[5] * 128 - var4 = (var4 + ((temp_scaled * self._humidity_calibration[6]) / 100)) / 16 - var5 = ((var3 / 16384) * (var3 / 16384)) / 1024 - var6 = (var4 * var5) / 2 - calc_hum = (((var3 + var6) / 1024) * 1000) / 4096 - calc_hum /= 1000 # get back to RH - - if calc_hum > 100: - calc_hum = 100 - if calc_hum < 0: - calc_hum = 0 - return calc_hum - - @property - def altitude(self): - """The altitude based on current ``pressure`` vs the sea level pressure - (``sea_level_pressure``) - which you must enter ahead of time)""" - pressure = self.pressure # in Si units for hPascal - return 44330 * (1.0 - math.pow(pressure / self.sea_level_pressure, 0.1903)) - - @property - def gas(self): - """The gas resistance in ohms""" - self._perform_reading() - var1 = ((1340 + (5 * self._sw_err)) * (_LOOKUP_TABLE_1[self._gas_range])) / 65536 - var2 = ((self._adc_gas * 32768) - 16777216) + var1 - var3 = (_LOOKUP_TABLE_2[self._gas_range] * var1) / 512 - calc_gas_res = (var3 + (var2 / 2)) / var2 - return int(calc_gas_res) - - def _perform_reading(self): - """Perform a single-shot reading from the sensor and fill internal data structure for - calculations""" - expired = time.ticks_diff(self._last_reading, time.ticks_ms()) * time.ticks_diff(0, 1) - if 0 <= expired < self._min_refresh_time: - time.sleep_ms(self._min_refresh_time - expired) - - # set filter - self._write(_BME680_REG_CONFIG, [self._filter << 2]) - # turn on temp oversample & pressure oversample - self._write(_BME680_REG_CTRL_MEAS, - [(self._temp_oversample << 5)|(self._pressure_oversample << 2)]) - # turn on humidity oversample - self._write(_BME680_REG_CTRL_HUM, [self._humidity_oversample]) - # gas measurements enabled - self._write(_BME680_REG_CTRL_GAS, [_BME680_RUNGAS]) - - ctrl = self._read_byte(_BME680_REG_CTRL_MEAS) - ctrl = (ctrl & 0xFC) | 0x01 # enable single shot! - self._write(_BME680_REG_CTRL_MEAS, [ctrl]) - new_data = False - while not new_data: - data = self._read(_BME680_REG_MEAS_STATUS, 15) - new_data = data[0] & 0x80 != 0 - time.sleep(0.005) - self._last_reading = time.ticks_ms() - - self._adc_pres = _read24(data[2:5]) / 16 - self._adc_temp = _read24(data[5:8]) / 16 - self._adc_hum = struct.unpack('>H', bytes(data[8:10]))[0] - self._adc_gas = int(struct.unpack('>H', bytes(data[13:15]))[0] / 64) - self._gas_range = data[14] & 0x0F - - var1 = (self._adc_temp / 8) - (self._temp_calibration[0] * 2) - var2 = (var1 * self._temp_calibration[1]) / 2048 - var3 = ((var1 / 2) * (var1 / 2)) / 4096 - var3 = (var3 * self._temp_calibration[2] * 16) / 16384 - - self._t_fine = int(var2 + var3) - - def _read_calibration(self): - """Read & save the calibration coefficients""" - coeff = self._read(_BME680_BME680_COEFF_ADDR1, 25) - coeff += self._read(_BME680_BME680_COEFF_ADDR2, 16) - - coeff = list(struct.unpack('> 1) - 64000 - var2 = ((var1 >> 2) * (var1 >> 2 )) >> 11 - var2 = (var2 * self._pressure_calibration[5]) >> 2 - var2 = var2 + ((var1 * self._pressure_calibration[4]) << 1) - var2 = (var2 >> 2) + (self._pressure_calibration[3] << 16) - var1 = (((((var1 >> 2) * (var1 >> 2)) >> 13) * - (self._pressure_calibration[2] << 5) >> 3) + - ((self._pressure_calibration[1] * var1) >> 1)) - var1 = var1 >> 18 - var1 = ((32768 + var1) * self._pressure_calibration[0]) >> 15 - calc_pres = 1048576 - int(self._adc_pres) - calc_pres = (calc_pres - (var2 >> 12)) * 3125 - calc_pres = (calc_pres << 1) // var1 - var1 = (self._pressure_calibration[8] * (((calc_pres >> 3) * (calc_pres >> 3)) >> 13)) >> 12 - var2 = ((calc_pres >> 2) * self._pressure_calibration[7]) >> 13 - var3 = (((calc_pres >> 8) * (calc_pres >> 8) * (calc_pres >> 8)) * self._pressure_calibration[9]) >> 17 - calc_pres += ((var1 + var2 + var3 + (self._pressure_calibration[6] << 7)) >> 4) - return calc_pres / 100 - - @property - def humidity(self): - """The relative humidity in RH %""" - self._perform_reading() - - temp_scaled = ((self._t_fine * 5) + 128) >> 8 - var1 = ((self._adc_hum - (self._humidity_calibration[0] * 16)) - - (((temp_scaled * self._humidity_calibration[2]) // 100) >> 1)) - var2 = (self._humidity_calibration[1] * - (((temp_scaled * self._humidity_calibration[3]) // 100) + - (((temp_scaled * ((temp_scaled * self._humidity_calibration[4]) - // 100)) >> 6) // 100) + 16384)) >> 10 - var3 = var1 * var2 - var4 = self._humidity_calibration[5] << 7 - var4 = (var4 + ((temp_scaled * self._humidity_calibration[6]) // 100)) >> 4 - var5 = ((var3 >> 14) * (var3 >> 14)) >> 10 - var6 = (var4 * var5) >> 1 - calc_hum = ((var3 + var6) >> 10) / 4096 - - if calc_hum > 10000: - calc_hum = 10000 - if calc_hum < 0: - calc_hum = 0 - return calc_hum - - @property - def altitude(self): - """The altitude based on current ``pressure`` vs the sea level pressure - (``sea_level_pressure``) - which you must enter ahead of time)""" - pressure = self.pressure # in Si units for hPascal - return 44330 * (1.0 - math.pow(pressure / self.sea_level_pressure, 0.1903)) - - @property - def gas(self): - """The gas resistance in ohms""" - self._perform_reading() - var1 = ((1340 + (5 * self._sw_err)) * (_LOOKUP_TABLE_1[self._gas_range])) >> 16 - var2 = ((self._adc_gas << 15) - 16777216) + var1 - var3 = (_LOOKUP_TABLE_2[self._gas_range] * var1) >> 9 - calc_gas_res = (var3 + (var2 >> 1)) // var2 - return int(calc_gas_res) - - def _perform_reading(self): - """Perform a single-shot reading from the sensor and fill internal data structure for - calculations""" - expired = time.ticks_diff(self._last_reading, time.ticks_ms()) * time.ticks_diff(0, 1) - if 0 <= expired < self._min_refresh_time: - time.sleep_ms(self._min_refresh_time - expired) - - # set filter - self._write(_BME680_REG_CONFIG, [self._filter << 2]) - # turn on temp oversample & pressure oversample - self._write(_BME680_REG_CTRL_MEAS, - [(self._temp_oversample << 5)|(self._pressure_oversample << 2)]) - # turn on humidity oversample - self._write(_BME680_REG_CTRL_HUM, [self._humidity_oversample]) - # gas measurements enabled - self._write(_BME680_REG_CTRL_GAS, [_BME680_RUNGAS]) - - ctrl = self._read_byte(_BME680_REG_CTRL_MEAS) - ctrl = (ctrl & 0xFC) | 0x01 # enable single shot! - self._write(_BME680_REG_CTRL_MEAS, [ctrl]) - new_data = False - while not new_data: - data = self._read(_BME680_REG_MEAS_STATUS, 15) - new_data = data[0] & 0x80 != 0 - time.sleep(0.005) - self._last_reading = time.ticks_ms() - - self._adc_pres = _read24(data[2:5]) / 16 - self._adc_temp = _read24(data[5:8]) / 16 - self._adc_hum = struct.unpack('>H', bytes(data[8:10]))[0] - self._adc_gas = int(struct.unpack('>H', bytes(data[13:15]))[0] / 64) - self._gas_range = data[14] & 0x0F - - var1 = (int(self._adc_temp) >> 3) - (self._temp_calibration[0] << 1) - var2 = (var1 * self._temp_calibration[1]) >> 11 - var3 = ((var1 >> 1) * (var1 >> 1)) >> 12 - var3 = (var3 * self._temp_calibration[2] << 4) >> 14 - self._t_fine = int(var2 + var3) - - def _read_calibration(self): - """Read & save the calibration coefficients""" - coeff = self._read(_BME680_BME680_COEFF_ADDR1, 25) - coeff += self._read(_BME680_BME680_COEFF_ADDR2, 16) - - coeff = list(struct.unpack('>= 4 - - self._heat_range = (self._read_byte(0x02) & 0x30) >> 4 - self._heat_val = self._read_byte(0x00) - self._sw_err = (self._read_byte(0x04) & 0xF0) >> 4 - - def _read_byte(self, register): - """Read a byte register value and return it""" - return self._read(register, 1)[0] - - def _read(self, register, length): - raise NotImplementedError() - - def _write(self, register, values): - raise NotImplementedError() - -class BME680_I2C(Adafruit_BME680): - """Driver for I2C connected BME680. - - :param i2c: I2C device object - :param int address: I2C device address - :param bool debug: Print debug statements when True. - :param int refresh_rate: Maximum number of readings per second. Faster property reads - will be from the previous reading.""" - def __init__(self, i2c, address=0x77, debug=False, *, refresh_rate=10): - """Initialize the I2C device at the 'address' given""" - self._i2c = i2c - self._address = address - self._debug = debug - super().__init__(refresh_rate=refresh_rate) - - def _read(self, register, length): - """Returns an array of 'length' bytes from the 'register'""" - result = bytearray(length) - self._i2c.readfrom_mem_into(self._address, register & 0xff, result) - if self._debug: - print("\t${:x} read ".format(register), " ".join(["{:02x}".format(i) for i in result])) - return result - - def _write(self, register, values): - """Writes an array of 'length' bytes to the 'register'""" - if self._debug: - print("\t${:x} write".format(register), " ".join(["{:02x}".format(i) for i in values])) - for value in values: - self._i2c.writeto_mem(self._address, register, bytearray([value & 0xFF])) - register += 1 - - -class BME680_SPI(Adafruit_BME680): - """Driver for SPI connected BME680. - - :param spi: SPI device object, configured - :param cs: Chip Select Pin object, configured to OUT mode - :param bool debug: Print debug statements when True. - :param int refresh_rate: Maximum number of readings per second. Faster property reads - will be from the previous reading. - """ - - def __init__(self, spi, cs, debug=False, *, refresh_rate=10): - self._spi = spi - self._cs = cs - self._debug = debug - self._cs(1) - super().__init__(refresh_rate=refresh_rate) - - def _read(self, register, length): - if register != _BME680_REG_PAGE_SELECT: - # _BME680_REG_PAGE_SELECT exists in both SPI memory pages - # For all other registers, we must set the correct memory page - self._set_spi_mem_page(register) - register = (register | 0x80) & 0xFF # Read single, bit 7 high. - - try: - self._cs(0) - self._spi.write(bytearray([register])) # pylint: disable=no-member - result = bytearray(length) - self._spi.readinto(result) # pylint: disable=no-member - if self._debug: - print("\t${:x} read ".format(register), " ".join(["{:02x}".format(i) for i in result])) - except Exception as e: - print (e) - result = None - finally: - self._cs(1) - return result - - def _write(self, register, values): - if register != _BME680_REG_PAGE_SELECT: - # _BME680_REG_PAGE_SELECT exists in both SPI memory pages - # For all other registers, we must set the correct memory page - self._set_spi_mem_page(register) - register &= 0x7F # Write, bit 7 low. - try: - self._cs(0) - buffer = bytearray(2 * len(values)) - for i, value in enumerate(values): - buffer[2 * i] = register + i - buffer[2 * i + 1] = value & 0xFF - self._spi.write(buffer) # pylint: disable=no-member - if self._debug: - print("\t${:x} write".format(register), " ".join(["{:02x}".format(i) for i in values])) - except Exception as e: - print (e) - finally: - self._cs(1) - - def _set_spi_mem_page(self, register): - spi_mem_page = 0x00 - if register < 0x80: - spi_mem_page = 0x10 - self._write(_BME680_REG_PAGE_SELECT, [spi_mem_page]) diff --git a/scripts/tempSensor/constants.py b/scripts/tempSensor/constants.py deleted file mode 100644 index 2930feb7..00000000 --- a/scripts/tempSensor/constants.py +++ /dev/null @@ -1,12 +0,0 @@ -# constants.py - -# Wi-Fi Configuration -WIFI_SSID = 'Pixel 8' -WIFI_PASSWORD = '123456789' - -# MQTT Configuration -MQTT_BROKER = 'b6bdb89571144b3d8e5ca4bbe666ddb5.s1.eu.hivemq.cloud' -MQTT_PORT = 8883 -MQTT_TOPIC = 'sensors/bme680/data' -MQTT_USER = 'LuthiraMQ' -MQTT_PASSWORD = 'Password1118.' diff --git a/scripts/tempSensor/error.txt b/scripts/tempSensor/error.txt deleted file mode 100644 index 339b9ea0..00000000 --- a/scripts/tempSensor/error.txt +++ /dev/null @@ -1,3 +0,0 @@ -Traceback (most recent call last): - File "", line 90, in -RuntimeError: WiFi connection failed. Ensure you are using a 2.4 GHz WiFi network with WPA-2 authentication. See the additional prerequisites section from https://doi.org/10.1016/j.xpro.2023.102329 or the https://github.com/sparks-baird/self-driving-lab-demo/issues/76 for additional troubleshooting help. diff --git a/scripts/tempSensor/lib/CHANGELOG.md b/scripts/tempSensor/lib/CHANGELOG.md deleted file mode 100644 index 78fec497..00000000 --- a/scripts/tempSensor/lib/CHANGELOG.md +++ /dev/null @@ -1,50 +0,0 @@ -2.0.0 ------ - -* Repackage to hatch/pyproject.toml -* Drop Python 2.7 support -* Switch from smbu2 to smbus2 - -1.1.1 ------ - -* New: constants to clarify heater on/off states - -1.1.0 ------ - -* New: support for BME688 "high" gas resistance variant -* New: set/get gas heater disable bit -* Enhancement: fail with descriptive RuntimeError when chip is not detected - -1.0.5 ------ - -* New: set_temp_offset to calibrate temperature offset in degrees C - -1.0.4 ------ - -* Fix to range_sw_err for extremely high gas readings -* Convert to unsigned int to fix negative gas readings - -1.0.3 ------ - -* Merged temperature compensation fix from Bosch's BME680_driver 3.5.3 - -1.0.2 ------ - -* Fixed set_gas_heater_temperature to avoid i2c TypeError - -1.0.1 ------ - -* Added Manifest to Python package - -1.0.0 ------ - -* Initial release - diff --git a/scripts/tempSensor/lib/README.md b/scripts/tempSensor/lib/README.md deleted file mode 100644 index 0c71b7f7..00000000 --- a/scripts/tempSensor/lib/README.md +++ /dev/null @@ -1,56 +0,0 @@ -# BME680 - -[![Build Status](https://img.shields.io/github/actions/workflow/status/pimoroni/bme680-python/test.yml?branch=main)](https://github.com/pimoroni/bme680-python/actions/workflows/test.yml) -[![Coverage Status](https://coveralls.io/repos/github/pimoroni/bme680-python/badge.svg?branch=main)](https://coveralls.io/github/pimoroni/bme680-python?branch=main) -[![PyPi Package](https://img.shields.io/pypi/v/bme680.svg)](https://pypi.python.org/pypi/bme680) -[![Python Versions](https://img.shields.io/pypi/pyversions/bme680.svg)](https://pypi.python.org/pypi/bme680) - -https://shop.pimoroni.com/products/bme680 - -The state-of-the-art BME680 breakout lets you measure temperature, pressure, humidity, and indoor air quality. - -## Installing - -### Full install (recommended): - -We've created an easy installation script that will install all pre-requisites and get your BME680 -up and running with minimal efforts. To run it, fire up Terminal which you'll find in Menu -> Accessories -> Terminal -on your Raspberry Pi desktop, as illustrated below: - -![Finding the terminal](http://get.pimoroni.com/resources/github-repo-terminal.png) - -In the new terminal window type the command exactly as it appears below (check for typos) and follow the on-screen instructions: - -```bash -git clone https://github.com/pimoroni/bme680-python -cd bme680-python -./install.sh -``` - -**Note** Libraries will be installed in the "pimoroni" virtual environment, you will need to activate it to run examples: - -``` -source ~/.virtualenvs/pimoroni/bin/activate -``` - -### Development: - -If you want to contribute, or like living on the edge of your seat by having the latest code, you can install the development version like so: - -```bash -git clone https://github.com/pimoroni/bme680-python -cd bme680-python -./install.sh --unstable -``` - -In all cases you will have to enable the i2c bus: - -``` -sudo raspi-config nonint do_i2c 0 -``` - -## Documentation & Support - -* Guides and tutorials - https://learn.pimoroni.com/bme680-breakout -* Get help - http://forums.pimoroni.com/c/support - diff --git a/scripts/tempSensor/lib/adafruit_blinka-8.49.0.dist-info/METADATA b/scripts/tempSensor/lib/adafruit_blinka-8.49.0.dist-info/METADATA deleted file mode 100644 index 8854faa0..00000000 --- a/scripts/tempSensor/lib/adafruit_blinka-8.49.0.dist-info/METADATA +++ /dev/null @@ -1,8 +0,0 @@ -Metadata-Version: 2.1 -Name: adafruit-blinka -Version: 8.49.0 -Summary: Dummy package for satisfying formal requirements -Home-page: ? -Author: ? - -? diff --git a/scripts/tempSensor/lib/adafruit_blinka-8.49.0.dist-info/RECORD b/scripts/tempSensor/lib/adafruit_blinka-8.49.0.dist-info/RECORD deleted file mode 100644 index a105ee1c..00000000 --- a/scripts/tempSensor/lib/adafruit_blinka-8.49.0.dist-info/RECORD +++ /dev/null @@ -1,2 +0,0 @@ -adafruit_blinka-8.49.0.dist-info/METADATA,, -adafruit_blinka-8.49.0.dist-info/RECORD,, \ No newline at end of file diff --git a/scripts/tempSensor/lib/as7341.py b/scripts/tempSensor/lib/as7341.py deleted file mode 100644 index 26dc8f89..00000000 --- a/scripts/tempSensor/lib/as7341.py +++ /dev/null @@ -1,608 +0,0 @@ -""" -This file licensed under the MIT License and incorporates work covered by -the following copyright and permission notice: - -The MIT License (MIT) - -Copyright (c) 2022-2022 Rob Hamerling - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -Rob Hamerling, Version 0.0, August 2022 - - Original by WaveShare for Raspberry Pi, part of: - https://www.waveshare.com/w/upload/b/b3/AS7341_Spectral_Color_Sensor_code.7z - - Converted to Micropython for use with MicroPython devices such as ESP32 - - pythonized (in stead of 'literal' translation of C code) - - instance of AS7341 requires specification of I2C interface - - added I2C read/write error detection - - added check for connected AS7341 incl. device ID - - some code optimization (esp. adding I2C word/block reads/writes) - - Replaced bit addressing like (1<<5) by symbolic name with bit mask - - moved SMUX settings for predefined channel mappings to a dictionary - and as a separate file to allow changes or additional configurations - by the user without changing the driver - - several changes of names of functions and constants - (incl. camel case -> word separators with underscores) - - added comments, doc-strings with explanation and/or argumentation - - several other improvements and some corrections - - Remarks: - - Automatic Gain Control (AGC) is not supported - - No provisions for SYND mode - -""" - -from time import sleep_ms - -from as7341_smux_select import * # predefined SMUX configurations - -AS7341_I2C_ADDRESS = const(0x39) # I2C address of AS7341 -AS7341_ID_VALUE = const(0x24) # AS7341 Part Number Identification -# (excl 2 low order bits) - -# Symbolic names for registers and some selected bit fields -# Note: ASTATUS, ITIME and CHx_DATA in address range 0x60--0x6F are not used -AS7341_CONFIG = const(0x70) -AS7341_CONFIG_INT_MODE_SPM = const(0x00) -AS7341_MODE_SPM = AS7341_CONFIG_INT_MODE_SPM # alias -AS7341_CONFIG_INT_MODE_SYNS = const(0x01) -AS7341_MODE_SYNS = AS7341_CONFIG_INT_MODE_SYNS # alias -AS7341_CONFIG_INT_MODE_SYND = const(0x03) -AS7341_MODE_SYND = AS7341_CONFIG_INT_MODE_SYND # alias -AS7341_CONFIG_INT_SEL = const(0x04) -AS7341_CONFIG_LED_SEL = const(0x08) -AS7341_STAT = const(0x71) -AS7341_STAT_READY = const(0x01) -AS7341_STAT_WAIT_SYNC = const(0x02) -AS7341_EDGE = const(0x72) -AS7341_GPIO = const(0x73) -AS7341_GPIO_PD_INT = const(0x01) -AS7341_GPIO_PD_GPIO = const(0x02) -AS7341_LED = const(0x74) -AS7341_LED_LED_ACT = const(0x80) -AS7341_ENABLE = const(0x80) -AS7341_ENABLE_PON = const(0x01) -AS7341_ENABLE_SP_EN = const(0x02) -AS7341_ENABLE_WEN = const(0x08) -AS7341_ENABLE_SMUXEN = const(0x10) -AS7341_ENABLE_FDEN = const(0x40) -AS7341_ATIME = const(0x81) -AS7341_WTIME = const(0x83) -AS7341_SP_TH_LOW = const(0x84) -AS7341_SP_TH_L_LSB = const(0x84) -AS7341_SP_TH_L_MSB = const(0x85) -AS7341_SP_TH_HIGH = const(0x86) -AS7341_SP_TH_H_LSB = const(0x86) -AS7341_SP_TH_H_MSB = const(0x87) -AS7341_AUXID = const(0x90) -AS7341_REVID = const(0x91) -AS7341_ID = const(0x92) -AS7341_STATUS = const(0x93) -AS7341_STATUS_ASAT = const(0x80) -AS7341_STATUS_AINT = const(0x08) -AS7341_STATUS_FINT = const(0x04) -AS7341_STATUS_C_INT = const(0x02) -AS7341_STATUS_SINT = const(0x01) -AS7341_ASTATUS = const(0x94) # start of bulk read (incl 6 counts) -AS7341_ASTATUS_ASAT_STATUS = const(0x80) -AS7341_ASTATUS_AGAIN_STATUS = const(0x0F) -AS7341_CH_DATA = const(0x95) # start of the 6 channel counts -AS7341_CH0_DATA_L = const(0x95) -AS7341_CH0_DATA_H = const(0x96) -AS7341_CH1_DATA_L = const(0x97) -AS7341_CH1_DATA_H = const(0x98) -AS7341_CH2_DATA_L = const(0x99) -AS7341_CH2_DATA_H = const(0x9A) -AS7341_CH3_DATA_L = const(0x9B) -AS7341_CH3_DATA_H = const(0x9C) -AS7341_CH4_DATA_L = const(0x9D) -AS7341_CH4_DATA_H = const(0x9E) -AS7341_CH5_DATA_L = const(0x9F) -AS7341_CH5_DATA_H = const(0xA0) -AS7341_STATUS_2 = const(0xA3) -AS7341_STATUS_2_AVALID = const(0x40) -AS7341_STATUS_3 = const(0xA4) -AS7341_STATUS_5 = const(0xA6) -AS7341_STATUS_6 = const(0xA7) -AS7341_CFG_0 = const(0xA9) -AS7341_CFG_0_WLONG = const(0x04) -AS7341_CFG_0_REG_BANK = const(0x10) # datasheet fig 82 (! fig 32) -AS7341_CFG_0_LOW_POWER = const(0x20) -AS7341_CFG_1 = const(0xAA) -AS7341_CFG_3 = const(0xAC) -AS7341_CFG_6 = const(0xAF) -AS7341_CFG_6_SMUX_CMD_ROM = const(0x00) -AS7341_CFG_6_SMUX_CMD_READ = const(0x08) -AS7341_CFG_6_SMUX_CMD_WRITE = const(0x10) -AS7341_CFG_8 = const(0xB1) -AS7341_CFG_9 = const(0xB2) -AS7341_CFG_10 = const(0xB3) -AS7341_CFG_12 = const(0xB5) -AS7341_PERS = const(0xBD) -AS7341_GPIO_2 = const(0xBE) -AS7341_GPIO_2_GPIO_IN = const(0x01) -AS7341_GPIO_2_GPIO_OUT = const(0x02) -AS7341_GPIO_2_GPIO_IN_EN = const(0x04) -AS7341_GPIO_2_GPIO_INV = const(0x08) -AS7341_ASTEP = const(0xCA) -AS7341_ASTEP_L = const(0xCA) -AS7341_ASTEP_H = const(0xCB) -AS7341_AGC_GAIN_MAX = const(0xCF) -AS7341_AZ_CONFIG = const(0xD6) -AS7341_FD_TIME_1 = const(0xD8) -AS7341_FD_TIME_2 = const(0xDA) -AS7341_FD_CFG0 = const(0xD7) -AS7341_FD_STATUS = const(0xDB) -AS7341_FD_STATUS_FD_100HZ = const(0x01) -AS7341_FD_STATUS_FD_120HZ = const(0x02) -AS7341_FD_STATUS_FD_100_VALID = const(0x04) -AS7341_FD_STATUS_FD_120_VALID = const(0x08) -AS7341_FD_STATUS_FD_SAT_DETECT = const(0x10) -AS7341_FD_STATUS_FD_MEAS_VALID = const(0x20) -AS7341_INTENAB = const(0xF9) -AS7341_INTENAB_SP_IEN = const(0x08) -AS7341_CONTROL = const(0xFA) -AS7341_FIFO_MAP = const(0xFC) -AS7341_FIFO_LVL = const(0xFD) -AS7341_FDATA = const(0xFE) -AS7341_FDATA_L = const(0xFE) -AS7341_FDATA_H = const(0xFF) - - -class AS7341: - """Class for AS7341: 11 Channel Multi-Spectral Digital Sensor""" - - def __init__(self, i2c, addr=AS7341_I2C_ADDRESS): - """specification of active I2C object is mandatory - specification of I2C address of AS7341 is optional - """ - self.__bus = i2c - self.__address = addr - self.__buffer1 = bytearray(1) # I2C I/O buffer for byte - self.__buffer2 = bytearray(2) # I2C I/O buffer for word - self.__buffer13 = bytearray(13) # I2C I/O buffer ASTATUS + 6 counts - self.__measuremode = AS7341_MODE_SPM # default measurement mode - self.__connected = self.reset() # recycle power, check AS7341 presence - - """ --------- 'private' functions ----------- """ - - def __read_byte(self, reg): - """read byte, return byte (integer) value""" - try: - self.__bus.readfrom_mem_into(self.__address, reg, self.__buffer1) - return self.__buffer1[0] # return integer value - except Exception as err: - print("I2C read_byte at 0x{:02X}, error".format(reg), err) - return -1 # indication 'no receive' - - def __read_word(self, reg): - """read 2 consecutive bytes, return integer value (little Endian)""" - try: - self.__bus.readfrom_mem_into(self.__address, reg, self.__buffer2) - return int.from_bytes(self.__buffer2, "little") # return word value - except Exception as err: - print("I2C read_word at 0x{:02X}, error".format(reg), err) - return -1 # indication 'no receive' - - def __read_all_channels(self): - """read ASTATUS register and all channels, return list of 6 integer values""" - try: - self.__bus.readfrom_mem_into( - self.__address, AS7341_ASTATUS, self.__buffer13 - ) - return [ - int.from_bytes(self.__buffer13[1 + 2 * i : 3 + 2 * i], "little") - for i in range(6) - ] - except Exception as err: - print( - "I2C read_all_channels at 0x{:02X}, error".format(AS7341_ASTATUS), err - ) - return [] # empty list - - def __write_byte(self, reg, value): - """write a single byte to the specified register""" - self.__buffer1[0] = value & 0xFF - try: - self.__bus.writeto_mem(self.__address, reg, self.__buffer1) - sleep_ms(10) - except Exception as err: - print("I2C write_byte at 0x{:02X}, error".format(reg), err) - return False - return True - - def __write_word(self, reg, value): - """write a word as 2 bytes (little endian encoding) - to adresses + 0 and + 1 - """ - self.__buffer2[0] = value & 0xFF # low byte - self.__buffer2[1] = (value >> 8) & 0xFF # high byte - try: - self.__bus.writeto_mem(self.__address, reg, self.__buffer2) - sleep_ms(20) - except Exception as err: - print("I2C write_word at 0x{:02X}, error".format(reg), err) - return False - return True - - def __write_burst(self, reg, value): - """write an array of bytes to consucutive addresses starting """ - try: - self.__bus.writeto_mem(self.__address, reg, value) - sleep_ms(100) - except Exception as err: - print("I2C write_burst at 0x{:02X}, error".format(reg), err) - return False - return True - - def __modify_reg(self, reg, mask, flag=True): - """modify register with - True means 'or' with : set the bit(s) - False means 'and' with inverted : reset the bit(s) - Notes: 1. Works only with '1' bits in - (in most cases contains a single 1-bit!) - 2. When is in region 0x60-0x74 - bank 1 is supposed be set by caller - """ - data = self.__read_byte(reg) # read - if flag: - data |= mask - else: - data &= ~mask - self.__write_byte(reg, data) # rewrite - - def __set_bank(self, bank=1): - """select registerbank - 1 for access to regs 0x60-0x74 - 0 for access to regs 0x80-0xFF - Note: It seems that reg CFG_0 (0x93) is accessible - even when REG_BANK bit is set for 0x60-0x74, - otherwise it wouldn't be possible to reset REG_BANK - Datasheet isn't clear about this. - """ - if bank in (0, 1): - self.__modify_reg(AS7341_CFG_0, AS7341_CFG_0_REG_BANK, bank == 1) - - """ ----------- 'public' functions ----------- """ - - def enable(self): - """enable device (only power on)""" - self.__write_byte(AS7341_ENABLE, AS7341_ENABLE_PON) - - def disable(self): - """disable all functions and power off""" - self.__set_bank(1) # CONFIG register is in bank 1 - self.__write_byte(AS7341_CONFIG, 0x00) # INT, LED off, SPM mode - self.__set_bank(0) - self.__write_byte(AS7341_ENABLE, 0x00) # power off - - def isconnected(self): - """determine if AS7341 is successfully initialized (True/False)""" - return self.__connected - - def reset(self): - """Cycle power and check if AS7341 is (re-)connected - When connected set (restore) measurement mode - """ - self.disable() # power-off ('reset') - sleep_ms(50) # quisce - self.enable() # (only) power-on - sleep_ms(50) # settle - id = self.__read_byte(AS7341_ID) # obtain Part Number ID - if id < 0: # read error - print( - "Failed to contact AS7341 at I2C address 0x{:02X}".format( - self.__address - ) - ) - return False - else: - if not (id & (~0x03)) == AS7341_ID_VALUE: # ID in bits 7..2 bits - print( - "No AS7341: ID = 0x{:02X}, expected 0x{:02X}".format( - id, AS7341_ID_VALUE - ) - ) - return False - self.set_measure_mode(self.__measuremode) # configure chip - return True - - def measurement_completed(self): - """check if measurement completed (return True) or otherwise return False""" - return bool(self.__read_byte(AS7341_STATUS_2) & AS7341_STATUS_2_AVALID) - - def set_spectral_measurement(self, flag=True): - """enable (flag == True) spectral measurement or otherwise disable it""" - self.__modify_reg(AS7341_ENABLE, AS7341_ENABLE_SP_EN, flag) - - def set_smux(self, flag=True): - """enable (flag == True) SMUX or otherwise disable it""" - self.__modify_reg(AS7341_ENABLE, AS7341_ENABLE_SMUXEN, flag) - - def set_measure_mode(self, mode=AS7341_CONFIG_INT_MODE_SPM): - """configure the AS7341 for a specific measurement mode - when interrupt needed it must be configured separately - """ - if mode in ( - AS7341_CONFIG_INT_MODE_SPM, # meas. started by SP_EN - AS7341_CONFIG_INT_MODE_SYNS, # meas. started by GPIO - AS7341_CONFIG_INT_MODE_SYND, - ): # meas. started by GPIO + EDGE - self.__measuremode = mode # store new measurement mode - self.__set_bank(1) # CONFIG register is in bank 1 - data = self.__read_byte(AS7341_CONFIG) & (~3) # discard 2 LSbs (mode) - data |= mode # insert new mode - self.__write_byte(AS7341_CONFIG, data) # modify measurement mode - self.__set_bank(0) - - def channel_select(self, selection): - """select one from a series of predefined SMUX configurations - should be a key in dictionary AS7341_SMUX_SELECT - 20 bytes of memory starting from address 0 will be overwritten. - """ - if selection in AS7341_SMUX_SELECT: - self.__write_burst(0x00, AS7341_SMUX_SELECT[selection]) - else: - print(selection, "is unknown in AS7341_SMUX_SELECT") - - def start_measure(self, selection): - """select SMUX configuration, prepare and start measurement""" - self.__modify_reg(AS7341_CFG_0, AS7341_CFG_0_LOW_POWER, False) # no low power - self.set_spectral_measurement(False) # quiesce - self.__write_byte(AS7341_CFG_6, AS7341_CFG_6_SMUX_CMD_WRITE) # write mode - if self.__measuremode == AS7341_CONFIG_INT_MODE_SPM: - self.channel_select(selection) - self.set_smux(True) - elif self.__measuremode == AS7341_CONFIG_INT_MODE_SYNS: - self.channel_select(selection) - self.set_smux(True) - self.set_gpio_mode(AS7341_GPIO_2_GPIO_IN_EN) - self.set_spectral_measurement(True) - if self.__measuremode == AS7341_CONFIG_INT_MODE_SPM: - while not self.measurement_completed(): - sleep_ms(50) - - def get_channel_data(self, channel): - """read count of a single channel (channel in range 0..5) - with or without measurement, just read count of one channel - contents depend on previous selection with 'start_measure' - auto-zero feature may result in value 0! - """ - data = 0 # default - if 0 <= channel <= 5: - data = self.__read_word(AS7341_CH_DATA + channel * 2) - return data # return integer value - - def get_spectral_data(self): - """obtain counts of all channels - return a tuple of 6 counts (integers) of the channels - contents depend on previous selection with 'start_measure' - """ - return self.__read_all_channels() # return a tuple! - - def set_flicker_detection(self, flag=True): - """enable (flag == True) flicker detection or otherwise disable it""" - self.__modify_reg(AS7341_ENABLE, AS7341_ENABLE_FDEN, flag) - - def get_flicker_frequency(self): - """Determine flicker frequency in Hz. Returns 100, 120 or 0 - Integration time and gain for flicker detection is the same as for - other channels, the dedicated FD_TIME and FD_GAIN are not supported - """ - self.__modify_reg(AS7341_CFG_0, AS7341_CFG_0_LOW_POWER, False) # no low power - self.set_spectral_measurement(False) - self.__write_byte(AS7341_CFG_6, AS7341_CFG_6_SMUX_CMD_WRITE) - self.channel_select("FD") # select flicker detection only - self.set_smux(True) - self.set_spectral_measurement(True) - self.set_flicker_detection(True) - for _ in range(10): # limited wait for completion - fd_status = self.__read_byte(AS7341_FD_STATUS) - if fd_status & AS7341_FD_STATUS_FD_MEAS_VALID: - break - # print("Flicker measurement not completed") - sleep_ms(100) - else: # timeout - print("Flicker measurement timed out") - return 0 - for _ in range(10): # limited wait for calculation - fd_status = self.__read_byte(AS7341_FD_STATUS) - if (fd_status & AS7341_FD_STATUS_FD_100_VALID) or ( - fd_status & AS7341_FD_STATUS_FD_120_VALID - ): - break - # print("Flicker calculation not completed") - sleep_ms(100) - else: # timeout - print("Flicker frequency calculation timed out") - return 0 - # print("FD_STATUS", "0x{:02X}".format(fd_status)) - self.set_flicker_detection(False) # disable - self.__write_byte(AS7341_FD_STATUS, 0x3C) # clear all FD STATUS bits - if (fd_status & AS7341_FD_STATUS_FD_100_VALID) and ( - fd_status & AS7341_FD_STATUS_FD_100HZ - ): - return 100 - elif (fd_status & AS7341_FD_STATUS_FD_120_VALID) and ( - fd_status & AS7341_FD_STATUS_FD_120HZ - ): - return 120 - return 0 - - def set_gpio_mode(self, mode): - """Configure mode of GPIO pin. - Allow only input-enable or output (with or without inverted) - specify 0x00 to reset the mode of the GPIO pin. - Notes: 1. It seems that GPIO_INV bit must be set - together with GPIO_IN_EN. - Proof: Use a pull-up resistor between GPIO and 3.3V: - - when program is ot started GPIO is high - - when program is started (GPIO_IN_EN=1) GPIO becomes low - - when also GPIO_INV=1 GPIO behaves normally - Maybe it is a quirk of the used test-board. - 2. GPIO output is not tested - (dataset lacks info how to set/reset GPIO) - """ - if mode in ( - 0x00, - AS7341_GPIO_2_GPIO_OUT, - AS7341_GPIO_2_GPIO_OUT | AS7341_GPIO_2_GPIO_INV, - AS7341_GPIO_2_GPIO_IN_EN, - AS7341_GPIO_2_GPIO_IN_EN | AS7341_GPIO_2_GPIO_INV, - ): - if mode == AS7341_GPIO_2_GPIO_IN_EN: # input mode - mode |= AS7341_GPIO_2_GPIO_INV # add 'inverted' - self.__write_byte(AS7341_GPIO_2, mode) - - def get_gpio_value(self): - """Determine GPIO value (when GPIO enabled for IN_EN) - returns 0 (low voltage) or 1 (high voltage) - """ - # print("GPIO_2 = 0x{:02X}".format(self.__read_byte(AS7341_GPIO_2))) - return self.__read_byte(AS7341_GPIO_2) & AS7341_GPIO_2_GPIO_IN - - def set_astep(self, value): - """set ASTEP size (range 0..65534 -> 2.78 usec .. 182 msec)""" - if 0 <= value <= 65534: - self.__write_word(AS7341_ASTEP, value) - - def set_atime(self, value): - """set number of integration steps (range 0..255 -> 1..256 ASTEPs)""" - self.__write_byte(AS7341_ATIME, value) - - def get_integration_time(self): - """return actual total integration time (atime * astep) - in milliseconds (valid with SPM and SYNS measurement mode) - """ - return ( - (self.__read_word(AS7341_ASTEP) + 1) - * (self.__read_byte(AS7341_ATIME) + 1) - * 2.78 - / 1000 - ) - - def set_again(self, code): - """set AGAIN (code in range 0..10 -> gain factor 0.5 .. 512) - value 0 1 2 3 4 5 6 7 8 9 10 - gain: *0.5 | *1 | *2 | *4 | *8 | *16 | *32 | *64 | *128 | *256 | *512 - """ - if 0 <= code <= 10: - self.__write_byte(AS7341_CFG_1, code) - - def get_again(self): - """obtain actual gain code (in range 0 .. 10)""" - return self.__read_byte(AS7341_CFG_1) - - def set_again_factor(self, factor): - """'inverse' of 'set_again': gain factor -> code 0 .. 10 - is rounded down to nearest power of 2 (in range 0.5 .. 512) - """ - code = 10 - gain = 512 - while gain > factor and code > 0: - gain /= 2 - code -= 1 - # print("factor", factor, "gain", gain, "code", code) - self.__write_byte(AS7341_CFG_1, code) - - def get_again_factor(self): - """obtain actual gain factor (in range 0.5 .. 512)""" - return 2 ** (self.__read_byte(AS7341_CFG_1) - 1) - - def set_wen(self, flag=True): - """enable (flag=True) or otherwise disable use of WTIME (auto re-start)""" - self.__modify_reg(AS7341_ENABLE, AS7341_ENABLE_WEN, flag) - - def set_wtime(self, wtime): - """set WTIME when auto-re-start is desired (in range 0 .. 0xFF) - 0 -> 2.78ms, 0xFF -> 711.7 ms - Note: The WEN bit in ENABLE should be set as well: set_wen() - """ - self.__write_byte(AS7341_WTIME, wtime) - - def set_led_current(self, current): - """Control current of onboard LED in milliamperes - LED-current is (here) limited to the range 4..20 mA - use only even numbers (4,6,8,... etc) - Specification outside this range results in LED OFF - """ - self.__set_bank(1) # CONFIG and LED registers in bank 1 - if 4 <= current <= 20: # within limits: 4..20 mA - self.__modify_reg(AS7341_CONFIG, AS7341_CONFIG_LED_SEL, True) - # print("Reg. CONFIG (0x70) now 0x{:02X}".format(self.__read_byte(0x70))) - data = AS7341_LED_LED_ACT + ((current - 4) // 2) # LED on with PWM - else: - self.__modify_reg(AS7341_CONFIG, AS7341_CONFIG_LED_SEL, False) - data = 0 # LED off, PWM 0 - self.__write_byte(AS7341_LED, data) - # print("reg 0x74 (LED) now 0x{:02X}".format(self.__read_byte(0x74))) - self.__set_bank(0) - sleep_ms(100) - - def check_interrupt(self): - """Check for Spectral or Flicker Detect saturation interrupt""" - data = self.__read_byte(AS7341_STATUS) - if data & AS7341_STATUS_ASAT: - print("Spectral interrupt generation!") - return True - return False - - def clear_interrupt(self): - """clear all interrupt signals""" - self.__write_byte(AS7341_STATUS, 0xFF) - - def set_spectral_interrupt(self, flag=True): - """enable (flag == True) or otherwise disable spectral interrupts""" - self.__modify_reg(AS7341_INTENAB, AS7341_INTENAB_SP_IEN, flag) - - def set_interrupt_persistence(self, value): - """configure interrupt persistance""" - if 0 <= value <= 15: - self.__write_byte(AS7341_PERS, value) - - def set_spectral_threshold_channel(self, value): - """select channel (0..4) for interrupts, persistence and AGC""" - if 0 <= value <= 4: - self.__write_byte(AS7341_CFG_12, value) - - def set_thresholds(self, lo, hi): - """Set thresholds (when lo < hi)""" - if lo < hi: - self.__write_word(AS7341_SP_TH_LOW, lo) - self.__write_word(AS7341_SP_TH_HIGH, hi) - sleep_ms(20) - - def get_thresholds(self): - """obtain and return tuple with low and high threshold values""" - lo = self.__read_word(AS7341_SP_TH_LOW) - hi = self.__read_word(AS7341_SP_TH_HIGH) - return (lo, hi) - - def set_syns_int(self): - """select SYNS mode and signal SYNS interrupt on Pin INT""" - self.__set_bank(1) # CONFIG register is in bank 1 - self.__write_byte( - AS7341_CONFIG, AS7341_CONFIG_INT_SEL | AS7341_CONFIG_INT_MODE_SYNS - ) - self.__set_bank(0) - - -# diff --git a/scripts/tempSensor/lib/as7341_sensor.py b/scripts/tempSensor/lib/as7341_sensor.py deleted file mode 100644 index c9ef9473..00000000 --- a/scripts/tempSensor/lib/as7341_sensor.py +++ /dev/null @@ -1,149 +0,0 @@ -"""Sterling Baird: wrapper class for AS7341 sensor.""" - -from math import log - -from as7341 import AS7341, AS7341_MODE_SPM -from machine import I2C, Pin - - -class ExternalDeviceNotFound(OSError): - pass - - -class Sensor: - def __init__( - self, atime=100, astep=999, gain=8, i2c=I2C(1, scl=Pin(27), sda=Pin(26)) - ): - """Wrapper for Rob Hamerling's AS7341 implementation. - - Mimics the original CircuitPython class a bit more, specific to the needs of - SDL-Demo. - - Rob Hamerling's implementation: - - https://gitlab.com/robhamerling/micropython-as7341 - - Original Circuit Python repo: - - https://github.com/adafruit/Adafruit_CircuitPython_AS7341 - - Parameters - ---------- - atime : int, optional - The integration time step size in 2.78 microsecond increments, by default 100 - astep : int, optional - The integration time step count. Total integration time will be (ATIME + 1) - * (ASTEP + 1) * 2.78µS, by default 999, meaning 281 ms assuming atime=100 - gain : int, optional - The ADC gain multiplier, by default 128 - i2c : I2C, optional - The I2C bus, by default machine.I2C(1, scl=machine.Pin(27), - sda=machine.Pin(26)) - - Raises - ------ - ExternalDeviceNotFound - Couldn't connect to AS7341. - - Examples - -------- - >>> sensor = Sensor(atime=29, astep=599, again=4) - >>> channel_data = sensor.all_channels - """ - - # i2c = machine.SoftI2C(scl=Pin(27), sda=Pin(26)) - self.i2c = i2c - addrlist = " ".join(["0x{:02X}".format(x) for x in i2c.scan()]) # type: ignore - print("Detected devices at I2C-addresses:", addrlist) - - sensor = AS7341(i2c) - - if not sensor.isconnected(): - raise ExternalDeviceNotFound("Failed to contact AS7341, terminating") - - sensor.set_measure_mode(AS7341_MODE_SPM) - - sensor.set_atime(atime) - sensor.set_astep(astep) - sensor.set_again(gain) - - self.sensor = sensor - - self.__atime = atime - self.__astep = astep - self.__gain = gain - - @property - def _atime(self): - return self.__atime - - @_atime.setter - def _atime(self, value): - self.__atime = value - self.sensor.set_atime(value) - - @property - def _astep(self): - return self.__astep - - @_astep.setter - def _astep(self, value): - self.__atime = value - self.sensor.set_astep(value) - - @property - def _gain(self): - return self.__gain - - @_gain.setter - def _gain(self, gain): - """set AGAIN (code in range 0..10 -> gain factor 0.5 .. 512) - gain: *0.5 | *1 | *2 | *4 | *8 | *16 | *32 | *64 | *128 | *256 | *512 - code 0 1 2 3 4 5 6 7 8 9 10 - """ - self.__gain = gain - # gain == 0.5 * 2 ** code --> code == 1.4427 Ln[2 * gain] (via Mathematica) - code = int(round(1.4427 * log(2 * gain))) - self.sensor.set_again(code) - - @property - def all_channels(self): - self.sensor.start_measure("F1F4CN") - f1, f2, f3, f4, clr, nir = self.sensor.get_spectral_data() - - self.sensor.start_measure("F5F8CN") - f5, f6, f7, f8, clr, nir = self.sensor.get_spectral_data() - - clr, nir # to ignore "unused" linting warnings - - return [f1, f2, f3, f4, f5, f6, f7, f8] - - @property - def all_channels_clr_nir(self): - self.sensor.start_measure("F1F4CN") - f1, f2, f3, f4, clr, nir = self.sensor.get_spectral_data() - - self.sensor.start_measure("F5F8CN") - f5, f6, f7, f8, clr, nir = self.sensor.get_spectral_data() - - clr, nir # to ignore "unused" linting warnings - - return [f1, f2, f3, f4, f5, f6, f7, f8, clr, nir] - - def disable(self): - self.sensor.disable() - - -# %% Code Graveyard -# gain_to_code_lookup = { -# 0.5: 1, -# 1: 1, -# 2: 2, -# 4: 3, -# 8: 4, -# 16: 5, -# 32: 6, -# 64: 7, -# 128: 8, -# 256: 9, -# 512: 10, -# } -# code = gain_to_code_lookup[gain] diff --git a/scripts/tempSensor/lib/as7341_smux_select.py b/scripts/tempSensor/lib/as7341_smux_select.py deleted file mode 100644 index edea2f40..00000000 --- a/scripts/tempSensor/lib/as7341_smux_select.py +++ /dev/null @@ -1,49 +0,0 @@ -""" -This file licensed under the MIT License and incorporates work covered by -the following copyright and permission notice: - -The MIT License (MIT) - -Copyright (c) 2022-2022 Rob Hamerling - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -""" Dictionary with specific SMUX configurations for AS7341 - See AMS Application Note AS7341_AN000666_1.01.pdf - for detailed instructions how to configure the channel mapping. - The Application Note can be found in one of the evaluation packages, e.g. - AS7341_EvalSW_Reflection_v1-26-3/Documents/application notes/SMUX/ - - This file should be imported by AS7341.py with: - from as7341_smux_select import * -""" -AS7341_SMUX_SELECT = { - # F1 through F4, CLEAR, NIR: - "F1F4CN": b"\x30\x01\x00\x00\x00\x42\x00\x00\x50\x00\x00\x00\x20\x04\x00\x30\x01\x50\x00\x06", - # F5 through F8, CLEAR, NIR: - "F5F8CN": b"\x00\x00\x00\x40\x02\x00\x10\x03\x50\x10\x03\x00\x00\x00\x24\x00\x00\x50\x00\x06", - # F2 through F7: - "F2F7": b"\x20\x00\x00\x00\x05\x31\x40\x06\x00\x40\x06\x00\x10\x03\x50\x20\x00\x00\x00\x00", - # Flicker Detection only: - "FD": b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x60", -} - -# diff --git a/scripts/tempSensor/lib/bme680-2.0.0.dist-info/METADATA b/scripts/tempSensor/lib/bme680-2.0.0.dist-info/METADATA deleted file mode 100644 index 52ebdfd8..00000000 --- a/scripts/tempSensor/lib/bme680-2.0.0.dist-info/METADATA +++ /dev/null @@ -1,156 +0,0 @@ -Metadata-Version: 2.3 -Name: bme680 -Version: 2.0.0 -Summary: Python library for the BME680 temperature, humidity and gas sensor -Project-URL: GitHub, https://www.github.com/pimoroni/bme680-python -Project-URL: Homepage, https://www.pimoroni.com -Author-email: Philip Howard -Maintainer-email: Philip Howard -License: MIT License - - Copyright (c) 2018 Pimoroni Ltd - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. -License-File: LICENSE -Keywords: Pi,Raspberry -Classifier: Development Status :: 4 - Beta -Classifier: Intended Audience :: Developers -Classifier: License :: OSI Approved :: MIT License -Classifier: Operating System :: POSIX :: Linux -Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3 :: Only -Classifier: Programming Language :: Python :: 3.7 -Classifier: Programming Language :: Python :: 3.8 -Classifier: Programming Language :: Python :: 3.9 -Classifier: Programming Language :: Python :: 3.10 -Classifier: Programming Language :: Python :: 3.11 -Classifier: Topic :: Software Development -Classifier: Topic :: Software Development :: Libraries -Classifier: Topic :: System :: Hardware -Requires-Python: >=3.7 -Requires-Dist: smbus2 -Description-Content-Type: text/markdown - -# BME680 - -[![Build Status](https://img.shields.io/github/actions/workflow/status/pimoroni/bme680-python/test.yml?branch=main)](https://github.com/pimoroni/bme680-python/actions/workflows/test.yml) -[![Coverage Status](https://coveralls.io/repos/github/pimoroni/bme680-python/badge.svg?branch=main)](https://coveralls.io/github/pimoroni/bme680-python?branch=main) -[![PyPi Package](https://img.shields.io/pypi/v/bme680.svg)](https://pypi.python.org/pypi/bme680) -[![Python Versions](https://img.shields.io/pypi/pyversions/bme680.svg)](https://pypi.python.org/pypi/bme680) - -https://shop.pimoroni.com/products/bme680 - -The state-of-the-art BME680 breakout lets you measure temperature, pressure, humidity, and indoor air quality. - -## Installing - -### Full install (recommended): - -We've created an easy installation script that will install all pre-requisites and get your BME680 -up and running with minimal efforts. To run it, fire up Terminal which you'll find in Menu -> Accessories -> Terminal -on your Raspberry Pi desktop, as illustrated below: - -![Finding the terminal](http://get.pimoroni.com/resources/github-repo-terminal.png) - -In the new terminal window type the command exactly as it appears below (check for typos) and follow the on-screen instructions: - -```bash -git clone https://github.com/pimoroni/bme680-python -cd bme680-python -./install.sh -``` - -**Note** Libraries will be installed in the "pimoroni" virtual environment, you will need to activate it to run examples: - -``` -source ~/.virtualenvs/pimoroni/bin/activate -``` - -### Development: - -If you want to contribute, or like living on the edge of your seat by having the latest code, you can install the development version like so: - -```bash -git clone https://github.com/pimoroni/bme680-python -cd bme680-python -./install.sh --unstable -``` - -In all cases you will have to enable the i2c bus: - -``` -sudo raspi-config nonint do_i2c 0 -``` - -## Documentation & Support - -* Guides and tutorials - https://learn.pimoroni.com/bme680-breakout -* Get help - http://forums.pimoroni.com/c/support - - -2.0.0 ------ - -* Repackage to hatch/pyproject.toml -* Drop Python 2.7 support -* Switch from smbu2 to smbus2 - -1.1.1 ------ - -* New: constants to clarify heater on/off states - -1.1.0 ------ - -* New: support for BME688 "high" gas resistance variant -* New: set/get gas heater disable bit -* Enhancement: fail with descriptive RuntimeError when chip is not detected - -1.0.5 ------ - -* New: set_temp_offset to calibrate temperature offset in degrees C - -1.0.4 ------ - -* Fix to range_sw_err for extremely high gas readings -* Convert to unsigned int to fix negative gas readings - -1.0.3 ------ - -* Merged temperature compensation fix from Bosch's BME680_driver 3.5.3 - -1.0.2 ------ - -* Fixed set_gas_heater_temperature to avoid i2c TypeError - -1.0.1 ------ - -* Added Manifest to Python package - -1.0.0 ------ - -* Initial release - diff --git a/scripts/tempSensor/lib/bme680-2.0.0.dist-info/RECORD b/scripts/tempSensor/lib/bme680-2.0.0.dist-info/RECORD deleted file mode 100644 index 35d5cc6b..00000000 --- a/scripts/tempSensor/lib/bme680-2.0.0.dist-info/RECORD +++ /dev/null @@ -1,7 +0,0 @@ -CHANGELOG.md,, -LICENSE,, -README.md,, -bme680-2.0.0.dist-info/METADATA,, -bme680/__init__.py,, -bme680/constants.py,, -bme680-2.0.0.dist-info/RECORD,, \ No newline at end of file diff --git a/scripts/tempSensor/bme680.py b/scripts/tempSensor/lib/bme680.py similarity index 100% rename from scripts/tempSensor/bme680.py rename to scripts/tempSensor/lib/bme680.py diff --git a/scripts/tempSensor/lib/bme680/__init__.py b/scripts/tempSensor/lib/bme680/__init__.py deleted file mode 100644 index 56d547a1..00000000 --- a/scripts/tempSensor/lib/bme680/__init__.py +++ /dev/null @@ -1,486 +0,0 @@ -"""BME680 Temperature, Pressure, Humidity & Gas Sensor.""" -import math -import time - -from . import constants -from .constants import BME680Data, lookupTable1, lookupTable2 - -__version__ = '2.0.0' - - -# Export constants to global namespace -# so end-users can "from BME680 import NAME" -if hasattr(constants, '__dict__'): - for key in constants.__dict__: - value = constants.__dict__[key] - if key not in globals(): - globals()[key] = value - - -class BME680(BME680Data): - """BOSCH BME680. - - Gas, pressure, temperature and humidity sensor. - - :param i2c_addr: One of I2C_ADDR_PRIMARY (0x76) or I2C_ADDR_SECONDARY (0x77) - :param i2c_device: Optional smbus or compatible instance for facilitating i2c communications. - - """ - - def __init__(self, i2c_addr=constants.I2C_ADDR_PRIMARY, i2c_device=None): - """Initialise BME680 sensor instance and verify device presence. - - :param i2c_addr: i2c address of BME680 - :param i2c_device: Optional SMBus-compatible instance for i2c transport - - """ - BME680Data.__init__(self) - - self.i2c_addr = i2c_addr - self._i2c = i2c_device - if self._i2c is None: - import smbus2 - self._i2c = smbus2.SMBus(1) - - try: - self.chip_id = self._get_regs(constants.CHIP_ID_ADDR, 1) - if self.chip_id != constants.CHIP_ID: - raise RuntimeError('BME680 Not Found. Invalid CHIP ID: 0x{0:02x}'.format(self.chip_id)) - except IOError: - raise RuntimeError("Unable to identify BME680 at 0x{:02x} (IOError)".format(self.i2c_addr)) - - self._variant = self._get_regs(constants.CHIP_VARIANT_ADDR, 1) - - self.soft_reset() - self.set_power_mode(constants.SLEEP_MODE) - - self._get_calibration_data() - - self.set_humidity_oversample(constants.OS_2X) - self.set_pressure_oversample(constants.OS_4X) - self.set_temperature_oversample(constants.OS_8X) - self.set_filter(constants.FILTER_SIZE_3) - if self._variant == constants.VARIANT_HIGH: - self.set_gas_status(constants.ENABLE_GAS_MEAS_HIGH) - else: - self.set_gas_status(constants.ENABLE_GAS_MEAS_LOW) - self.set_temp_offset(0) - self.get_sensor_data() - - def _get_calibration_data(self): - """Retrieve the sensor calibration data and store it in .calibration_data.""" - calibration = self._get_regs(constants.COEFF_ADDR1, constants.COEFF_ADDR1_LEN) - calibration += self._get_regs(constants.COEFF_ADDR2, constants.COEFF_ADDR2_LEN) - - heat_range = self._get_regs(constants.ADDR_RES_HEAT_RANGE_ADDR, 1) - heat_value = constants.twos_comp(self._get_regs(constants.ADDR_RES_HEAT_VAL_ADDR, 1), bits=8) - sw_error = constants.twos_comp(self._get_regs(constants.ADDR_RANGE_SW_ERR_ADDR, 1), bits=8) - - self.calibration_data.set_from_array(calibration) - self.calibration_data.set_other(heat_range, heat_value, sw_error) - - def soft_reset(self): - """Trigger a soft reset.""" - self._set_regs(constants.SOFT_RESET_ADDR, constants.SOFT_RESET_CMD) - time.sleep(constants.RESET_PERIOD / 1000.0) - - def set_temp_offset(self, value): - """Set temperature offset in celsius. - - If set, the temperature t_fine will be increased by given value in celsius. - :param value: Temperature offset in Celsius, eg. 4, -8, 1.25 - - """ - if value == 0: - self.offset_temp_in_t_fine = 0 - else: - self.offset_temp_in_t_fine = int(math.copysign((((int(abs(value) * 100)) << 8) - 128) / 5, value)) - - def set_humidity_oversample(self, value): - """Set humidity oversampling. - - A higher oversampling value means more stable sensor readings, - with less noise and jitter. - - However each step of oversampling adds about 2ms to the latency, - causing a slower response time to fast transients. - - :param value: Oversampling value, one of: OS_NONE, OS_1X, OS_2X, OS_4X, OS_8X, OS_16X - - """ - self.tph_settings.os_hum = value - self._set_bits(constants.CONF_OS_H_ADDR, constants.OSH_MSK, constants.OSH_POS, value) - - def get_humidity_oversample(self): - """Get humidity oversampling.""" - return (self._get_regs(constants.CONF_OS_H_ADDR, 1) & constants.OSH_MSK) >> constants.OSH_POS - - def set_pressure_oversample(self, value): - """Set temperature oversampling. - - A higher oversampling value means more stable sensor readings, - with less noise and jitter. - - However each step of oversampling adds about 2ms to the latency, - causing a slower response time to fast transients. - - :param value: Oversampling value, one of: OS_NONE, OS_1X, OS_2X, OS_4X, OS_8X, OS_16X - - """ - self.tph_settings.os_pres = value - self._set_bits(constants.CONF_T_P_MODE_ADDR, constants.OSP_MSK, constants.OSP_POS, value) - - def get_pressure_oversample(self): - """Get pressure oversampling.""" - return (self._get_regs(constants.CONF_T_P_MODE_ADDR, 1) & constants.OSP_MSK) >> constants.OSP_POS - - def set_temperature_oversample(self, value): - """Set pressure oversampling. - - A higher oversampling value means more stable sensor readings, - with less noise and jitter. - - However each step of oversampling adds about 2ms to the latency, - causing a slower response time to fast transients. - - :param value: Oversampling value, one of: OS_NONE, OS_1X, OS_2X, OS_4X, OS_8X, OS_16X - - """ - self.tph_settings.os_temp = value - self._set_bits(constants.CONF_T_P_MODE_ADDR, constants.OST_MSK, constants.OST_POS, value) - - def get_temperature_oversample(self): - """Get temperature oversampling.""" - return (self._get_regs(constants.CONF_T_P_MODE_ADDR, 1) & constants.OST_MSK) >> constants.OST_POS - - def set_filter(self, value): - """Set IIR filter size. - - Optionally remove short term fluctuations from the temperature and pressure readings, - increasing their resolution but reducing their bandwidth. - - Enabling the IIR filter does not slow down the time a reading takes, but will slow - down the BME680s response to changes in temperature and pressure. - - When the IIR filter is enabled, the temperature and pressure resolution is effectively 20bit. - When it is disabled, it is 16bit + oversampling-1 bits. - - """ - self.tph_settings.filter = value - self._set_bits(constants.CONF_ODR_FILT_ADDR, constants.FILTER_MSK, constants.FILTER_POS, value) - - def get_filter(self): - """Get filter size.""" - return (self._get_regs(constants.CONF_ODR_FILT_ADDR, 1) & constants.FILTER_MSK) >> constants.FILTER_POS - - def select_gas_heater_profile(self, value): - """Set current gas sensor conversion profile. - - Select one of the 10 configured heating durations/set points. - - :param value: Profile index from 0 to 9 - - """ - if value > constants.NBCONV_MAX or value < constants.NBCONV_MIN: - raise ValueError("Profile '{}' should be between {} and {}".format(value, constants.NBCONV_MIN, constants.NBCONV_MAX)) - - self.gas_settings.nb_conv = value - self._set_bits(constants.CONF_ODR_RUN_GAS_NBC_ADDR, constants.NBCONV_MSK, constants.NBCONV_POS, value) - - def get_gas_heater_profile(self): - """Get gas sensor conversion profile: 0 to 9.""" - return self._get_regs(constants.CONF_ODR_RUN_GAS_NBC_ADDR, 1) & constants.NBCONV_MSK - - def set_gas_heater_status(self, value): - """Enable/disable gas heater.""" - self.gas_settings.heater = value - self._set_bits(constants.CONF_HEAT_CTRL_ADDR, constants.HCTRL_MSK, constants.HCTRL_POS, value) - - def get_gas_heater_status(self): - """Get current heater status.""" - return (self._get_regs(constants.CONF_HEAT_CTRL_ADDR, 1) & constants.HCTRL_MSK) >> constants.HCTRL_POS - - def set_gas_status(self, value): - """Enable/disable gas sensor.""" - if value == -1: - if self._variant == constants.VARIANT_HIGH: - value = constants.ENABLE_GAS_MEAS_HIGH - else: - value = constants.ENABLE_GAS_MEAS_LOW - self.gas_settings.run_gas = value - self._set_bits(constants.CONF_ODR_RUN_GAS_NBC_ADDR, constants.RUN_GAS_MSK, constants.RUN_GAS_POS, value) - - def get_gas_status(self): - """Get the current gas status.""" - return (self._get_regs(constants.CONF_ODR_RUN_GAS_NBC_ADDR, 1) & constants.RUN_GAS_MSK) >> constants.RUN_GAS_POS - - def set_gas_heater_profile(self, temperature, duration, nb_profile=0): - """Set temperature and duration of gas sensor heater. - - :param temperature: Target temperature in degrees celsius, between 200 and 400 - :param durarion: Target duration in milliseconds, between 1 and 4032 - :param nb_profile: Target profile, between 0 and 9 - - """ - self.set_gas_heater_temperature(temperature, nb_profile=nb_profile) - self.set_gas_heater_duration(duration, nb_profile=nb_profile) - - def set_gas_heater_temperature(self, value, nb_profile=0): - """Set gas sensor heater temperature. - - :param value: Target temperature in degrees celsius, between 200 and 400 - - When setting an nb_profile other than 0, - make sure to select it with select_gas_heater_profile. - - """ - if nb_profile > constants.NBCONV_MAX or value < constants.NBCONV_MIN: - raise ValueError('Profile "{}" should be between {} and {}'.format(nb_profile, constants.NBCONV_MIN, constants.NBCONV_MAX)) - - self.gas_settings.heatr_temp = value - temp = int(self._calc_heater_resistance(self.gas_settings.heatr_temp)) - self._set_regs(constants.RES_HEAT0_ADDR + nb_profile, temp) - - def set_gas_heater_duration(self, value, nb_profile=0): - """Set gas sensor heater duration. - - Heating durations between 1 ms and 4032 ms can be configured. - Approximately 20-30 ms are necessary for the heater to reach the intended target temperature. - - :param value: Heating duration in milliseconds. - - When setting an nb_profile other than 0, - make sure to select it with select_gas_heater_profile. - - """ - if nb_profile > constants.NBCONV_MAX or value < constants.NBCONV_MIN: - raise ValueError('Profile "{}" should be between {} and {}'.format(nb_profile, constants.NBCONV_MIN, constants.NBCONV_MAX)) - - self.gas_settings.heatr_dur = value - temp = self._calc_heater_duration(self.gas_settings.heatr_dur) - self._set_regs(constants.GAS_WAIT0_ADDR + nb_profile, temp) - - def set_power_mode(self, value, blocking=True): - """Set power mode.""" - if value not in (constants.SLEEP_MODE, constants.FORCED_MODE): - raise ValueError('Power mode should be one of SLEEP_MODE or FORCED_MODE') - - self.power_mode = value - - self._set_bits(constants.CONF_T_P_MODE_ADDR, constants.MODE_MSK, constants.MODE_POS, value) - - while blocking and self.get_power_mode() != self.power_mode: - time.sleep(constants.POLL_PERIOD_MS / 1000.0) - - def get_power_mode(self): - """Get power mode.""" - self.power_mode = self._get_regs(constants.CONF_T_P_MODE_ADDR, 1) - return self.power_mode - - def get_sensor_data(self): - """Get sensor data. - - Stores data in .data and returns True upon success. - - """ - self.set_power_mode(constants.FORCED_MODE) - - for attempt in range(10): - status = self._get_regs(constants.FIELD0_ADDR, 1) - - if (status & constants.NEW_DATA_MSK) == 0: - time.sleep(constants.POLL_PERIOD_MS / 1000.0) - continue - - regs = self._get_regs(constants.FIELD0_ADDR, constants.FIELD_LENGTH) - - self.data.status = regs[0] & constants.NEW_DATA_MSK - # Contains the nb_profile used to obtain the current measurement - self.data.gas_index = regs[0] & constants.GAS_INDEX_MSK - self.data.meas_index = regs[1] - - adc_pres = (regs[2] << 12) | (regs[3] << 4) | (regs[4] >> 4) - adc_temp = (regs[5] << 12) | (regs[6] << 4) | (regs[7] >> 4) - adc_hum = (regs[8] << 8) | regs[9] - adc_gas_res_low = (regs[13] << 2) | (regs[14] >> 6) - adc_gas_res_high = (regs[15] << 2) | (regs[16] >> 6) - gas_range_l = regs[14] & constants.GAS_RANGE_MSK - gas_range_h = regs[16] & constants.GAS_RANGE_MSK - - if self._variant == constants.VARIANT_HIGH: - self.data.status |= regs[16] & constants.GASM_VALID_MSK - self.data.status |= regs[16] & constants.HEAT_STAB_MSK - else: - self.data.status |= regs[14] & constants.GASM_VALID_MSK - self.data.status |= regs[14] & constants.HEAT_STAB_MSK - - self.data.heat_stable = (self.data.status & constants.HEAT_STAB_MSK) > 0 - - temperature = self._calc_temperature(adc_temp) - self.data.temperature = temperature / 100.0 - self.ambient_temperature = temperature # Saved for heater calc - - self.data.pressure = self._calc_pressure(adc_pres) / 100.0 - self.data.humidity = self._calc_humidity(adc_hum) / 1000.0 - - if self._variant == constants.VARIANT_HIGH: - self.data.gas_resistance = self._calc_gas_resistance_high(adc_gas_res_high, gas_range_h) - else: - self.data.gas_resistance = self._calc_gas_resistance_low(adc_gas_res_low, gas_range_l) - - return True - - return False - - def _set_bits(self, register, mask, position, value): - """Mask out and set one or more bits in a register.""" - temp = self._get_regs(register, 1) - temp &= ~mask - temp |= value << position - self._set_regs(register, temp) - - def _set_regs(self, register, value): - """Set one or more registers.""" - if isinstance(value, int): - self._i2c.write_byte_data(self.i2c_addr, register, value) - else: - self._i2c.write_i2c_block_data(self.i2c_addr, register, value) - - def _get_regs(self, register, length): - """Get one or more registers.""" - if length == 1: - return self._i2c.read_byte_data(self.i2c_addr, register) - else: - return self._i2c.read_i2c_block_data(self.i2c_addr, register, length) - - def _calc_temperature(self, temperature_adc): - """Convert the raw temperature to degrees C using calibration_data.""" - var1 = (temperature_adc >> 3) - (self.calibration_data.par_t1 << 1) - var2 = (var1 * self.calibration_data.par_t2) >> 11 - var3 = ((var1 >> 1) * (var1 >> 1)) >> 12 - var3 = ((var3) * (self.calibration_data.par_t3 << 4)) >> 14 - - # Save teperature data for pressure calculations - self.calibration_data.t_fine = (var2 + var3) + self.offset_temp_in_t_fine - calc_temp = (((self.calibration_data.t_fine * 5) + 128) >> 8) - - return calc_temp - - def _calc_pressure(self, pressure_adc): - """Convert the raw pressure using calibration data.""" - var1 = ((self.calibration_data.t_fine) >> 1) - 64000 - var2 = ((((var1 >> 2) * (var1 >> 2)) >> 11) * - self.calibration_data.par_p6) >> 2 - var2 = var2 + ((var1 * self.calibration_data.par_p5) << 1) - var2 = (var2 >> 2) + (self.calibration_data.par_p4 << 16) - var1 = (((((var1 >> 2) * (var1 >> 2)) >> 13) * - ((self.calibration_data.par_p3 << 5)) >> 3) + - ((self.calibration_data.par_p2 * var1) >> 1)) - var1 = var1 >> 18 - - var1 = ((32768 + var1) * self.calibration_data.par_p1) >> 15 - calc_pressure = 1048576 - pressure_adc - calc_pressure = ((calc_pressure - (var2 >> 12)) * (3125)) - - if calc_pressure >= (1 << 31): - calc_pressure = ((calc_pressure // var1) << 1) - else: - calc_pressure = ((calc_pressure << 1) // var1) - - var1 = (self.calibration_data.par_p9 * (((calc_pressure >> 3) * - (calc_pressure >> 3)) >> 13)) >> 12 - var2 = ((calc_pressure >> 2) * - self.calibration_data.par_p8) >> 13 - var3 = ((calc_pressure >> 8) * (calc_pressure >> 8) * - (calc_pressure >> 8) * - self.calibration_data.par_p10) >> 17 - - calc_pressure = (calc_pressure) + ((var1 + var2 + var3 + - (self.calibration_data.par_p7 << 7)) >> 4) - - return calc_pressure - - def _calc_humidity(self, humidity_adc): - """Convert the raw humidity using calibration data.""" - temp_scaled = ((self.calibration_data.t_fine * 5) + 128) >> 8 - var1 = (humidity_adc - ((self.calibration_data.par_h1 * 16))) -\ - (((temp_scaled * self.calibration_data.par_h3) // (100)) >> 1) - var2 = (self.calibration_data.par_h2 * - (((temp_scaled * self.calibration_data.par_h4) // (100)) + - (((temp_scaled * ((temp_scaled * self.calibration_data.par_h5) // (100))) >> 6) // - (100)) + (1 * 16384))) >> 10 - var3 = var1 * var2 - var4 = self.calibration_data.par_h6 << 7 - var4 = ((var4) + ((temp_scaled * self.calibration_data.par_h7) // (100))) >> 4 - var5 = ((var3 >> 14) * (var3 >> 14)) >> 10 - var6 = (var4 * var5) >> 1 - calc_hum = (((var3 + var6) >> 10) * (1000)) >> 12 - - return min(max(calc_hum, 0), 100000) - - def _calc_gas_resistance(self, gas_res_adc, gas_range): - """Convert the raw gas resistance using calibration data.""" - if self._variant == constants.VARIANT_HIGH: - return self._calc_gas_resistance_high(gas_res_adc, gas_range) - else: - return self._calc_gas_resistance_low(gas_res_adc, gas_range) - - def _calc_gas_resistance_high(self, gas_res_adc, gas_range): - """Convert the raw gas resistance using calibration data. - - Applies to Variant ID == 0x01 only. - - """ - var1 = 262144 >> gas_range - var2 = gas_res_adc - 512 - - var2 *= 3 - var2 = 4096 + var2 - - calc_gas_res = (10000 * var1) / var2 - calc_gas_res *= 100 - - return calc_gas_res - - def _calc_gas_resistance_low(self, gas_res_adc, gas_range): - """Convert the raw gas resistance using calibration data. - - Applies to Variant ID == 0x00 only. - - """ - var1 = ((1340 + (5 * self.calibration_data.range_sw_err)) * (lookupTable1[gas_range])) >> 16 - var2 = (((gas_res_adc << 15) - (16777216)) + var1) - var3 = ((lookupTable2[gas_range] * var1) >> 9) - calc_gas_res = ((var3 + (var2 >> 1)) / var2) - - if calc_gas_res < 0: - calc_gas_res = (1 << 32) + calc_gas_res - - return calc_gas_res - - def _calc_heater_resistance(self, temperature): - """Convert raw heater resistance using calibration data.""" - temperature = min(max(temperature, 200), 400) - - var1 = ((self.ambient_temperature * self.calibration_data.par_gh3) / 1000) * 256 - var2 = (self.calibration_data.par_gh1 + 784) * (((((self.calibration_data.par_gh2 + 154009) * temperature * 5) / 100) + 3276800) / 10) - var3 = var1 + (var2 / 2) - var4 = (var3 / (self.calibration_data.res_heat_range + 4)) - var5 = (131 * self.calibration_data.res_heat_val) + 65536 - heatr_res_x100 = (((var4 / var5) - 250) * 34) - heatr_res = ((heatr_res_x100 + 50) / 100) - - return heatr_res - - def _calc_heater_duration(self, duration): - """Calculate correct value for heater duration setting from milliseconds.""" - if duration < 0xfc0: - factor = 0 - - while duration > 0x3f: - duration /= 4 - factor += 1 - - return int(duration + (factor * 64)) - - return 0xff diff --git a/scripts/tempSensor/lib/bme680/constants.py b/scripts/tempSensor/lib/bme680/constants.py deleted file mode 100644 index d77415d3..00000000 --- a/scripts/tempSensor/lib/bme680/constants.py +++ /dev/null @@ -1,413 +0,0 @@ -"""BME680 constants, structures and utilities.""" - -# BME680 General config -POLL_PERIOD_MS = 10 - -# BME680 I2C addresses -I2C_ADDR_PRIMARY = 0x76 -I2C_ADDR_SECONDARY = 0x77 - -# BME680 unique chip identifier -CHIP_ID = 0x61 - -# BME680 coefficients related defines -COEFF_SIZE = 41 -COEFF_ADDR1_LEN = 25 -COEFF_ADDR2_LEN = 16 - -# BME680 field_x related defines -FIELD_LENGTH = 17 -FIELD_ADDR_OFFSET = 17 - -# Soft reset command -SOFT_RESET_CMD = 0xb6 - -# Error code definitions -OK = 0 -# Errors -E_NULL_PTR = -1 -E_COM_FAIL = -2 -E_DEV_NOT_FOUND = -3 -E_INVALID_LENGTH = -4 - -# Warnings -W_DEFINE_PWR_MODE = 1 -W_NO_NEW_DATA = 2 - -# Info's -I_MIN_CORRECTION = 1 -I_MAX_CORRECTION = 2 - -# Register map -# Other coefficient's address -ADDR_RES_HEAT_VAL_ADDR = 0x00 -ADDR_RES_HEAT_RANGE_ADDR = 0x02 -ADDR_RANGE_SW_ERR_ADDR = 0x04 -ADDR_SENS_CONF_START = 0x5A -ADDR_GAS_CONF_START = 0x64 - -# Field settings -FIELD0_ADDR = 0x1d - -# Heater settings -RES_HEAT0_ADDR = 0x5a -GAS_WAIT0_ADDR = 0x64 - -# Sensor configuration registers -CONF_HEAT_CTRL_ADDR = 0x70 -CONF_ODR_RUN_GAS_NBC_ADDR = 0x71 -CONF_OS_H_ADDR = 0x72 -MEM_PAGE_ADDR = 0xf3 -CONF_T_P_MODE_ADDR = 0x74 -CONF_ODR_FILT_ADDR = 0x75 - -# Coefficient's address -COEFF_ADDR1 = 0x89 -COEFF_ADDR2 = 0xe1 - -# Chip identifier -CHIP_ID_ADDR = 0xd0 -CHIP_VARIANT_ADDR = 0xf0 - -VARIANT_LOW = 0x00 -VARIANT_HIGH = 0x01 - -# Soft reset register -SOFT_RESET_ADDR = 0xe0 - -# Heater control settings -ENABLE_HEATER = 0x00 -DISABLE_HEATER = 0x08 - -# Gas measurement settings -DISABLE_GAS_MEAS = 0x00 -ENABLE_GAS_MEAS = -1 # Now used as auto-select -ENABLE_GAS_MEAS_LOW = 0x01 -ENABLE_GAS_MEAS_HIGH = 0x02 - -# Over-sampling settings -OS_NONE = 0 -OS_1X = 1 -OS_2X = 2 -OS_4X = 3 -OS_8X = 4 -OS_16X = 5 - -# IIR filter settings -FILTER_SIZE_0 = 0 -FILTER_SIZE_1 = 1 -FILTER_SIZE_3 = 2 -FILTER_SIZE_7 = 3 -FILTER_SIZE_15 = 4 -FILTER_SIZE_31 = 5 -FILTER_SIZE_63 = 6 -FILTER_SIZE_127 = 7 - -# Power mode settings -SLEEP_MODE = 0 -FORCED_MODE = 1 - -# Delay related macro declaration -RESET_PERIOD = 10 - -# SPI memory page settings -MEM_PAGE0 = 0x10 -MEM_PAGE1 = 0x00 - -# Ambient humidity shift value for compensation -HUM_REG_SHIFT_VAL = 4 - -# Run gas enable and disable settings -RUN_GAS_DISABLE = 0 -RUN_GAS_ENABLE = 1 - -# Gas heater enable and disable settings -GAS_HEAT_ENABLE = 0 -GAS_HEAT_DISABLE = 1 - -# Buffer length macro declaration -TMP_BUFFER_LENGTH = 40 -REG_BUFFER_LENGTH = 6 -FIELD_DATA_LENGTH = 3 -GAS_REG_BUF_LENGTH = 20 -GAS_HEATER_PROF_LEN_MAX = 10 - -# Settings selector -OST_SEL = 1 -OSP_SEL = 2 -OSH_SEL = 4 -GAS_MEAS_SEL = 8 -FILTER_SEL = 16 -HCNTRL_SEL = 32 -RUN_GAS_SEL = 64 -NBCONV_SEL = 128 -GAS_SENSOR_SEL = GAS_MEAS_SEL | RUN_GAS_SEL | NBCONV_SEL - -# Number of conversion settings -NBCONV_MIN = 0 -NBCONV_MAX = 9 # Was 10, but there are only 10 settings: 0 1 2 ... 8 9 - -# Mask definitions -GAS_MEAS_MSK = 0x30 -NBCONV_MSK = 0X0F -FILTER_MSK = 0X1C -OST_MSK = 0XE0 -OSP_MSK = 0X1C -OSH_MSK = 0X07 -HCTRL_MSK = 0x08 -RUN_GAS_MSK = 0x30 -MODE_MSK = 0x03 -RHRANGE_MSK = 0x30 -RSERROR_MSK = 0xf0 -NEW_DATA_MSK = 0x80 -GAS_INDEX_MSK = 0x0f -GAS_RANGE_MSK = 0x0f -GASM_VALID_MSK = 0x20 -HEAT_STAB_MSK = 0x10 -MEM_PAGE_MSK = 0x10 -SPI_RD_MSK = 0x80 -SPI_WR_MSK = 0x7f -BIT_H1_DATA_MSK = 0x0F - -# Bit position definitions for sensor settings -GAS_MEAS_POS = 4 -FILTER_POS = 2 -OST_POS = 5 -OSP_POS = 2 -OSH_POS = 0 -HCTRL_POS = 3 -RUN_GAS_POS = 4 -MODE_POS = 0 -NBCONV_POS = 0 - -# Array Index to Field data mapping for Calibration Data -T2_LSB_REG = 1 -T2_MSB_REG = 2 -T3_REG = 3 -P1_LSB_REG = 5 -P1_MSB_REG = 6 -P2_LSB_REG = 7 -P2_MSB_REG = 8 -P3_REG = 9 -P4_LSB_REG = 11 -P4_MSB_REG = 12 -P5_LSB_REG = 13 -P5_MSB_REG = 14 -P7_REG = 15 -P6_REG = 16 -P8_LSB_REG = 19 -P8_MSB_REG = 20 -P9_LSB_REG = 21 -P9_MSB_REG = 22 -P10_REG = 23 -H2_MSB_REG = 25 -H2_LSB_REG = 26 -H1_LSB_REG = 26 -H1_MSB_REG = 27 -H3_REG = 28 -H4_REG = 29 -H5_REG = 30 -H6_REG = 31 -H7_REG = 32 -T1_LSB_REG = 33 -T1_MSB_REG = 34 -GH2_LSB_REG = 35 -GH2_MSB_REG = 36 -GH1_REG = 37 -GH3_REG = 38 - -# BME680 register buffer index settings -REG_FILTER_INDEX = 5 -REG_TEMP_INDEX = 4 -REG_PRES_INDEX = 4 -REG_HUM_INDEX = 2 -REG_NBCONV_INDEX = 1 -REG_RUN_GAS_INDEX = 1 -REG_HCTRL_INDEX = 0 - -# Look up tables for the possible gas range values -lookupTable1 = [2147483647, 2147483647, 2147483647, 2147483647, - 2147483647, 2126008810, 2147483647, 2130303777, 2147483647, - 2147483647, 2143188679, 2136746228, 2147483647, 2126008810, - 2147483647, 2147483647] - -lookupTable2 = [4096000000, 2048000000, 1024000000, 512000000, - 255744255, 127110228, 64000000, 32258064, - 16016016, 8000000, 4000000, 2000000, - 1000000, 500000, 250000, 125000] - - -def bytes_to_word(msb, lsb, bits=16, signed=False): - """Convert a most and least significant byte into a word.""" - # TODO: Reimplement with struct - word = (msb << 8) | lsb - if signed: - word = twos_comp(word, bits) - return word - - -def twos_comp(val, bits=16): - """Convert two bytes into a two's compliment signed word.""" - # TODO: Reimplement with struct - if val & (1 << (bits - 1)) != 0: - val = val - (1 << bits) - return val - - -class FieldData: - """Structure for storing BME680 sensor data.""" - - def __init__(self): # noqa D107 - # Contains new_data, gasm_valid & heat_stab - self.status = None - self.heat_stable = False - # The index of the heater profile used - self.gas_index = None - # Measurement index to track order - self.meas_index = None - # Temperature in degree celsius x100 - self.temperature = None - # Pressure in Pascal - self.pressure = None - # Humidity in % relative humidity x1000 - self.humidity = None - # Gas resistance in Ohms - self.gas_resistance = None - - -class CalibrationData: - """Structure for storing BME680 calibration data.""" - - def __init__(self): # noqa D107 - self.par_h1 = None - self.par_h2 = None - self.par_h3 = None - self.par_h4 = None - self.par_h5 = None - self.par_h6 = None - self.par_h7 = None - self.par_gh1 = None - self.par_gh2 = None - self.par_gh3 = None - self.par_t1 = None - self.par_t2 = None - self.par_t3 = None - self.par_p1 = None - self.par_p2 = None - self.par_p3 = None - self.par_p4 = None - self.par_p5 = None - self.par_p6 = None - self.par_p7 = None - self.par_p8 = None - self.par_p9 = None - self.par_p10 = None - # Variable to store t_fine size - self.t_fine = None - # Variable to store heater resistance range - self.res_heat_range = None - # Variable to store heater resistance value - self.res_heat_val = None - # Variable to store error range - self.range_sw_err = None - - def set_from_array(self, calibration): - """Set parameters from an array of bytes.""" - # Temperature related coefficients - self.par_t1 = bytes_to_word(calibration[T1_MSB_REG], calibration[T1_LSB_REG]) - self.par_t2 = bytes_to_word(calibration[T2_MSB_REG], calibration[T2_LSB_REG], bits=16, signed=True) - self.par_t3 = twos_comp(calibration[T3_REG], bits=8) - - # Pressure related coefficients - self.par_p1 = bytes_to_word(calibration[P1_MSB_REG], calibration[P1_LSB_REG]) - self.par_p2 = bytes_to_word(calibration[P2_MSB_REG], calibration[P2_LSB_REG], bits=16, signed=True) - self.par_p3 = twos_comp(calibration[P3_REG], bits=8) - self.par_p4 = bytes_to_word(calibration[P4_MSB_REG], calibration[P4_LSB_REG], bits=16, signed=True) - self.par_p5 = bytes_to_word(calibration[P5_MSB_REG], calibration[P5_LSB_REG], bits=16, signed=True) - self.par_p6 = twos_comp(calibration[P6_REG], bits=8) - self.par_p7 = twos_comp(calibration[P7_REG], bits=8) - self.par_p8 = bytes_to_word(calibration[P8_MSB_REG], calibration[P8_LSB_REG], bits=16, signed=True) - self.par_p9 = bytes_to_word(calibration[P9_MSB_REG], calibration[P9_LSB_REG], bits=16, signed=True) - self.par_p10 = calibration[P10_REG] - - # Humidity related coefficients - self.par_h1 = (calibration[H1_MSB_REG] << HUM_REG_SHIFT_VAL) | (calibration[H1_LSB_REG] & BIT_H1_DATA_MSK) - self.par_h2 = (calibration[H2_MSB_REG] << HUM_REG_SHIFT_VAL) | (calibration[H2_LSB_REG] >> HUM_REG_SHIFT_VAL) - self.par_h3 = twos_comp(calibration[H3_REG], bits=8) - self.par_h4 = twos_comp(calibration[H4_REG], bits=8) - self.par_h5 = twos_comp(calibration[H5_REG], bits=8) - self.par_h6 = calibration[H6_REG] - self.par_h7 = twos_comp(calibration[H7_REG], bits=8) - - # Gas heater related coefficients - self.par_gh1 = twos_comp(calibration[GH1_REG], bits=8) - self.par_gh2 = bytes_to_word(calibration[GH2_MSB_REG], calibration[GH2_LSB_REG], bits=16, signed=True) - self.par_gh3 = twos_comp(calibration[GH3_REG], bits=8) - - def set_other(self, heat_range, heat_value, sw_error): - """Set other values.""" - self.res_heat_range = (heat_range & RHRANGE_MSK) // 16 - self.res_heat_val = heat_value - self.range_sw_err = (sw_error & RSERROR_MSK) // 16 - - -class TPHSettings: - """Structure for storing BME680 sensor settings. - - Comprises of output data rate, over-sampling and filter settings. - - """ - - def __init__(self): # noqa D107 - # Humidity oversampling - self.os_hum = None - # Temperature oversampling - self.os_temp = None - # Pressure oversampling - self.os_pres = None - # Filter coefficient - self.filter = None - - -class GasSettings: - """Structure for storing BME680 gas settings and status.""" - - def __init__(self): # noqa D107 - # Variable to store nb conversion - self.nb_conv = None - # Variable to store heater control - self.heatr_ctrl = None - # Run gas enable value - self.run_gas = None - # Pointer to store heater temperature - self.heatr_temp = None - # Pointer to store duration profile - self.heatr_dur = None - - -class BME680Data: - """Structure to represent BME680 device.""" - - def __init__(self): # noqa D107 - # Chip Id - self.chip_id = None - # Device Id - self.dev_id = None - # SPI/I2C interface - self.intf = None - # Memory page used - self.mem_page = None - # Ambient temperature in Degree C - self.ambient_temperature = None - # Field Data - self.data = FieldData() - # Sensor calibration data - self.calibration_data = CalibrationData() - # Sensor settings - self.tph_settings = TPHSettings() - # Gas Sensor settings - self.gas_settings = GasSettings() - # Sensor power modes - self.power_mode = None - # New sensor fields - self.new_fields = None diff --git a/scripts/tempSensor/lib/data_logging.py b/scripts/tempSensor/lib/data_logging.py deleted file mode 100644 index f391682d..00000000 --- a/scripts/tempSensor/lib/data_logging.py +++ /dev/null @@ -1,166 +0,0 @@ -import json -import sys -from time import gmtime, localtime, time - -import machine -import ntptime -import uos -import urequests -from machine import SPI, Pin -from sdcard import sdcard -from uio import StringIO - -# # uses a more robust ntptime -# from lib.ntptime import ntptime - - -def get_traceback(err): - try: - with StringIO() as f: # type: ignore - sys.print_exception(err, f) - return f.getvalue() - except Exception as err2: - print(err2) - return f"Failed to extract file and line number due to {err2}.\nOriginal error: {err}" # noqa: E501 - - -def initialize_sdcard( - spi_id=1, - cs_pin=15, - sck_pin=10, - mosi_pin=11, - miso_pin=12, - baudrate=1000000, - polarity=0, - phase=0, - bits=8, - firstbit=SPI.MSB, - verbose=True, -): - try: - cs = Pin(cs_pin, Pin.OUT) - - spi = SPI( - spi_id, - baudrate=baudrate, - polarity=polarity, - phase=phase, - bits=bits, - firstbit=firstbit, - sck=Pin(sck_pin), - mosi=Pin(mosi_pin), - miso=Pin(miso_pin), - ) - - # Initialize SD card - sd = sdcard.SDCard(spi, cs) - - vfs = uos.VfsFat(sd) - uos.mount(vfs, "/sd") # type: ignore - if verbose: - print("SD Card initialized successfully") - return True - except Exception as e: - if verbose: - print(get_traceback(e)) - print("SD Card failed to initialize") - return False - - -def write_payload_backup(payload_data: str, fpath: str = "/sd/experiments.txt"): - payload = json.dumps(payload_data) - with open(fpath, "a") as file: - # line = ",".join([str(payload[key]) for key in payload.keys()]) - file.write(f"{payload}\r\n") - - -def log_to_mongodb( - document: dict, - api_key: str, - url: str, - cluster_name: str, - database_name: str, - collection_name: str, - verbose: bool = True, - retries: int = 2, -): - # based on https://medium.com/@johnlpage/introduction-to-microcontrollers-and-the-pi-pico-w-f7a2d9ad1394 - headers = {"api-key": api_key} - - insertPayload = { - "dataSource": cluster_name, - "database": database_name, - "collection": collection_name, - "document": document, - } - - if verbose: - print(f"sending document to {cluster_name}:{database_name}:{collection_name}") - - for _ in range(retries): - response = None - if _ > 0: - print(f"retrying... ({_} of {retries})") - - try: - response = urequests.post(url, headers=headers, json=insertPayload) - txt = str(response.text) - status_code = response.status_code - - if verbose: - print(f"Response: ({status_code}), msg = {txt}") - if response.status_code == 201: - print("Added Successfully") - break - else: - print("Error") - - # Always close response objects so we don't leak memory - response.close() - except Exception as e: - if response is not None: - response.close() - if _ == retries - 1: - raise e - else: - print(e) - - -def get_timestamp(timeout=2, return_str=False): - ntptime.timeout = timeout # type: ignore - time_int = ntptime.time() - utc_tuple = gmtime(time_int) - year, month, mday, hour, minute, second, weekday, yearday = utc_tuple - - time_str = f"{year}-{month}-{mday} {hour:02}:{minute:02}:{second:02}" - - if return_str: - return time_int, time_str - - return time_int - - -def get_local_timestamp(return_str=False): - t = time() - year, month, mday, hour, minute, second, _, _ = localtime(t) - time_str = f"{year}-{month}-{mday} {hour:02}:{minute:02}:{second:02}" - - if return_str: - return t, time_str - - return t - - -def get_onboard_temperature(unit="K"): - sensor_temp = machine.ADC(4) - conversion_factor = 3.3 / (65535) - reading = sensor_temp.read_u16() * conversion_factor - celsius_degrees = 27 - (reading - 0.706) / 0.001721 - if unit == "C": - return celsius_degrees - elif unit == "K": - return celsius_degrees + 273.15 - elif unit == "F": - return celsius_degrees * 9 / 5 + 32 - else: - raise ValueError("Invalid unit. Must be one of 'C', 'K', or 'F") diff --git a/scripts/tempSensor/lib/functools.py b/scripts/tempSensor/lib/functools.py deleted file mode 100644 index 510a3406..00000000 --- a/scripts/tempSensor/lib/functools.py +++ /dev/null @@ -1,28 +0,0 @@ -def partial(func, *args, **kwargs): - def _partial(*more_args, **more_kwargs): - kw = kwargs.copy() - kw.update(more_kwargs) - func(*(args + more_args), **kw) - - return _partial - - -def update_wrapper(wrapper, wrapped): - # Dummy impl - return wrapper - - -def wraps(wrapped): - # Dummy impl - return lambda x: x - - -def reduce(function, iterable, initializer=None): - it = iter(iterable) - if initializer is None: - value = next(it) - else: - value = initializer - for element in it: - value = function(value, element) - return value diff --git a/scripts/tempSensor/lib/sdcard/LICENSE b/scripts/tempSensor/lib/sdcard/LICENSE deleted file mode 100644 index e3474e33..00000000 --- a/scripts/tempSensor/lib/sdcard/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2013, 2014 Damien P. George - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/scripts/tempSensor/lib/sdcard/sdcard.py b/scripts/tempSensor/lib/sdcard/sdcard.py deleted file mode 100644 index 2b6356b4..00000000 --- a/scripts/tempSensor/lib/sdcard/sdcard.py +++ /dev/null @@ -1,302 +0,0 @@ -""" -MicroPython driver for SD cards using SPI bus. - -Requires an SPI bus and a CS pin. Provides readblocks and writeblocks -methods so the device can be mounted as a filesystem. - -Example usage on pyboard: - - import pyb, sdcard, os - sd = sdcard.SDCard(pyb.SPI(1), pyb.Pin.board.X5) - pyb.mount(sd, '/sd2') - os.listdir('/') - -Example usage on ESP8266: - - import machine, sdcard, os - sd = sdcard.SDCard(machine.SPI(1), machine.Pin(15)) - os.mount(sd, '/sd') - os.listdir('/') - -Copied from source: https://raw.githubusercontent.com/micropython/micropython-lib/master/micropython/drivers/storage/sdcard/sdcard.py - -""" - -import time - -from micropython import const - -_CMD_TIMEOUT = const(100) - -_R1_IDLE_STATE = const(1 << 0) -# R1_ERASE_RESET = const(1 << 1) -_R1_ILLEGAL_COMMAND = const(1 << 2) -# R1_COM_CRC_ERROR = const(1 << 3) -# R1_ERASE_SEQUENCE_ERROR = const(1 << 4) -# R1_ADDRESS_ERROR = const(1 << 5) -# R1_PARAMETER_ERROR = const(1 << 6) -_TOKEN_CMD25 = const(0xFC) -_TOKEN_STOP_TRAN = const(0xFD) -_TOKEN_DATA = const(0xFE) - - -class SDCard: - def __init__(self, spi, cs, baudrate=1320000): - self.spi = spi - self.cs = cs - - self.cmdbuf = bytearray(6) - self.dummybuf = bytearray(512) - self.tokenbuf = bytearray(1) - for i in range(512): - self.dummybuf[i] = 0xFF - self.dummybuf_memoryview = memoryview(self.dummybuf) - - # initialise the card - self.init_card(baudrate) - - def init_spi(self, baudrate): - try: - master = self.spi.MASTER - except AttributeError: - # on ESP8266 - self.spi.init(baudrate=baudrate, phase=0, polarity=0) - else: - # on pyboard - self.spi.init(master, baudrate=baudrate, phase=0, polarity=0) - - def init_card(self, baudrate): - # init CS pin - self.cs.init(self.cs.OUT, value=1) - - # init SPI bus; use low data rate for initialisation - self.init_spi(100000) - - # clock card at least 100 cycles with cs high - for i in range(16): - self.spi.write(b"\xff") - - # CMD0: init card; should return _R1_IDLE_STATE (allow 5 attempts) - for _ in range(5): - if self.cmd(0, 0, 0x95) == _R1_IDLE_STATE: - break - else: - raise OSError("no SD card") - - # CMD8: determine card version - r = self.cmd(8, 0x01AA, 0x87, 4) - if r == _R1_IDLE_STATE: - self.init_card_v2() - elif r == (_R1_IDLE_STATE | _R1_ILLEGAL_COMMAND): - self.init_card_v1() - else: - raise OSError("couldn't determine SD card version") - - # get the number of sectors - # CMD9: response R2 (R1 byte + 16-byte block read) - if self.cmd(9, 0, 0, 0, False) != 0: - raise OSError("no response from SD card") - csd = bytearray(16) - self.readinto(csd) - if csd[0] & 0xC0 == 0x40: # CSD version 2.0 - self.sectors = ((csd[8] << 8 | csd[9]) + 1) * 1024 - elif csd[0] & 0xC0 == 0x00: # CSD version 1.0 (old, <=2GB) - c_size = (csd[6] & 0b11) << 10 | csd[7] << 2 | csd[8] >> 6 - c_size_mult = (csd[9] & 0b11) << 1 | csd[10] >> 7 - read_bl_len = csd[5] & 0b1111 - capacity = (c_size + 1) * (2 ** (c_size_mult + 2)) * (2**read_bl_len) - self.sectors = capacity // 512 - else: - raise OSError("SD card CSD format not supported") - # print('sectors', self.sectors) - - # CMD16: set block length to 512 bytes - if self.cmd(16, 512, 0) != 0: - raise OSError("can't set 512 block size") - - # set to high data rate now that it's initialised - self.init_spi(baudrate) - - def init_card_v1(self): - for i in range(_CMD_TIMEOUT): - time.sleep_ms(50) - self.cmd(55, 0, 0) - if self.cmd(41, 0, 0) == 0: - # SDSC card, uses byte addressing in read/write/erase commands - self.cdv = 512 - # print("[SDCard] v1 card") - return - raise OSError("timeout waiting for v1 card") - - def init_card_v2(self): - for i in range(_CMD_TIMEOUT): - time.sleep_ms(50) - self.cmd(58, 0, 0, 4) - self.cmd(55, 0, 0) - if self.cmd(41, 0x40000000, 0) == 0: - self.cmd( - 58, 0, 0, -4 - ) # 4-byte response, negative means keep the first byte - ocr = self.tokenbuf[0] # get first byte of response, which is OCR - if not ocr & 0x40: - # SDSC card, uses byte addressing in read/write/erase commands - self.cdv = 512 - else: - # SDHC/SDXC card, uses block addressing in read/write/erase commands - self.cdv = 1 - # print("[SDCard] v2 card") - return - raise OSError("timeout waiting for v2 card") - - def cmd(self, cmd, arg, crc, final=0, release=True, skip1=False): - self.cs(0) - - # create and send the command - buf = self.cmdbuf - buf[0] = 0x40 | cmd - buf[1] = arg >> 24 - buf[2] = arg >> 16 - buf[3] = arg >> 8 - buf[4] = arg - buf[5] = crc - self.spi.write(buf) - - if skip1: - self.spi.readinto(self.tokenbuf, 0xFF) - - # wait for the response (response[7] == 0) - for i in range(_CMD_TIMEOUT): - self.spi.readinto(self.tokenbuf, 0xFF) - response = self.tokenbuf[0] - if not (response & 0x80): - # this could be a big-endian integer that we are getting here - # if final<0 then store the first byte to tokenbuf and discard the rest - if final < 0: - self.spi.readinto(self.tokenbuf, 0xFF) - final = -1 - final - for j in range(final): - self.spi.write(b"\xff") - if release: - self.cs(1) - self.spi.write(b"\xff") - return response - - # timeout - self.cs(1) - self.spi.write(b"\xff") - return -1 - - def readinto(self, buf): - self.cs(0) - - # read until start byte (0xff) - for i in range(_CMD_TIMEOUT): - self.spi.readinto(self.tokenbuf, 0xFF) - if self.tokenbuf[0] == _TOKEN_DATA: - break - time.sleep_ms(1) - else: - self.cs(1) - raise OSError("timeout waiting for response") - - # read data - mv = self.dummybuf_memoryview - if len(buf) != len(mv): - mv = mv[: len(buf)] - self.spi.write_readinto(mv, buf) - - # read checksum - self.spi.write(b"\xff") - self.spi.write(b"\xff") - - self.cs(1) - self.spi.write(b"\xff") - - def write(self, token, buf): - self.cs(0) - - # send: start of block, data, checksum - self.spi.read(1, token) - self.spi.write(buf) - self.spi.write(b"\xff") - self.spi.write(b"\xff") - - # check the response - if (self.spi.read(1, 0xFF)[0] & 0x1F) != 0x05: - self.cs(1) - self.spi.write(b"\xff") - return - - # wait for write to finish - while self.spi.read(1, 0xFF)[0] == 0: - pass - - self.cs(1) - self.spi.write(b"\xff") - - def write_token(self, token): - self.cs(0) - self.spi.read(1, token) - self.spi.write(b"\xff") - # wait for write to finish - while self.spi.read(1, 0xFF)[0] == 0x00: - pass - - self.cs(1) - self.spi.write(b"\xff") - - def readblocks(self, block_num, buf): - nblocks = len(buf) // 512 - assert nblocks and not len(buf) % 512, "Buffer length is invalid" - if nblocks == 1: - # CMD17: set read address for single block - if self.cmd(17, block_num * self.cdv, 0, release=False) != 0: - # release the card - self.cs(1) - raise OSError(5) # EIO - # receive the data and release card - self.readinto(buf) - else: - # CMD18: set read address for multiple blocks - if self.cmd(18, block_num * self.cdv, 0, release=False) != 0: - # release the card - self.cs(1) - raise OSError(5) # EIO - offset = 0 - mv = memoryview(buf) - while nblocks: - # receive the data and release card - self.readinto(mv[offset : offset + 512]) - offset += 512 - nblocks -= 1 - if self.cmd(12, 0, 0xFF, skip1=True): - raise OSError(5) # EIO - - def writeblocks(self, block_num, buf): - nblocks, err = divmod(len(buf), 512) - assert nblocks and not err, "Buffer length is invalid" - if nblocks == 1: - # CMD24: set write address for single block - if self.cmd(24, block_num * self.cdv, 0) != 0: - raise OSError(5) # EIO - - # send the data - self.write(_TOKEN_DATA, buf) - else: - # CMD25: set write address for first block - if self.cmd(25, block_num * self.cdv, 0) != 0: - raise OSError(5) # EIO - # send the data - offset = 0 - mv = memoryview(buf) - while nblocks: - self.write(_TOKEN_CMD25, mv[offset : offset + 512]) - offset += 512 - nblocks -= 1 - self.write_token(_TOKEN_STOP_TRAN) - - def ioctl(self, op, arg): - if op == 4: # get number of blocks - return self.sectors - if op == 5: # get block size in bytes - return 512 diff --git a/scripts/tempSensor/lib/sdl_demo_utils.py b/scripts/tempSensor/lib/sdl_demo_utils.py deleted file mode 100644 index 009bbeed..00000000 --- a/scripts/tempSensor/lib/sdl_demo_utils.py +++ /dev/null @@ -1,276 +0,0 @@ -import json -import sys -from time import localtime, sleep, ticks_diff, ticks_ms # type: ignore - -import uos -from data_logging import ( - get_local_timestamp, - get_onboard_temperature, - write_payload_backup, -) -from machine import PWM, Pin -from ufastrsa.genprime import genrsa -from ufastrsa.rsa import RSA -from uio import StringIO - - -def beep(buzzer, power=0.005): - buzzer.freq(300) - buzzer.duty_u16(round(65535 * power)) - sleep(0.15) - buzzer.duty_u16(0) - - -def get_traceback(err): - try: - with StringIO() as f: # type: ignore - sys.print_exception(err, f) - return f.getvalue() - except Exception as err2: - print(err2) - return f"Failed to extract file and line number due to {err2}.\nOriginal error: {err}" # noqa: E501 - - -def merge_two_dicts(x, y): - z = x.copy() # start with keys and values of x - z.update(y) # modifies z with keys and values of y - return z - - -def path_exists(path): - # Check if path exists. - # Works for relative and absolute path. - parent = "" # parent folder name - name = path # name of file/folder - - # Check if file/folder has a parent folder - index = path.rstrip("/").rfind("/") - if index >= 0: - index += 1 - parent = path[: index - 1] - name = path[index:] - - # Searching with iterator is more efficient if the parent contains lost of files/folders - # return name in uos.listdir(parent) - return any((name == x[0]) for x in uos.ilistdir(parent)) - - -def encrypt_id(my_id, verbose=False): - rsa_path = "rsa.json" - # if path_exists(rsa_path): - try: - with open(rsa_path, "r") as f: - cipher_data = json.load(f) - cipher = RSA( - cipher_data["bits"], - n=cipher_data["n"], - e=cipher_data["e"], - d=cipher_data["d"], - ) - except (KeyError, OSError) as e: - print(e) - print("Generating new RSA parameters...") - bits = 256 - bits, n, e, d = genrsa(bits, e=65537) # type: ignore - cipher = RSA(bits, n=n, e=e, d=d) - with open("rsa.json", "w") as f: - json.dump(dict(bits=bits, n=n, e=e, d=d), f) - - if verbose: - with open(rsa_path, "r") as f: - cipher_data = json.load(f) - print("RSA parameters (keep private):") - print(cipher_data) - - my_id = int.from_bytes(cipher.pkcs_encrypt(my_id), "big") - return my_id - - -def decrypt_id(my_id): - rsa_path = "rsa.json" - if path_exists(rsa_path): - with open(rsa_path, "r") as f: - cipher_data = json.load(f) - cipher = RSA( - cipher_data["bits"], - n=cipher_data["n"], - e=cipher_data["e"], - d=cipher_data["d"], - ) - else: - bits = 256 - bits, n, e, d = genrsa(bits, e=65537) # type: ignore - cipher = RSA(bits, n=n, e=e, d=d) - with open("rsa.json", "w") as f: - json.dump(dict(bits=bits, n=n, e=e, d=d), f) - - my_id = int.from_bytes(cipher.pkcs_decrypt(my_id), "big") - return my_id - - -def get_onboard_led(): - try: - onboard_led = Pin("LED", Pin.OUT) # only works for Pico W - except Exception as e: - print(e) - onboard_led = Pin(25, Pin.OUT) - return onboard_led - - -class Experiment(object): - def __init__( - self, - run_experiment_fn, - devices, - reset_experiment_fn=None, - validate_inputs_fn=None, - emergency_shutdown_fn=None, - buzzer=None, - sdcard_ready=False, - ) -> None: - self.validate_inputs_fn = validate_inputs_fn - self.run_experiment_fn = run_experiment_fn - self.reset_experiment_fn = reset_experiment_fn - self.devices = devices - self.emergency_shutdown_fn = emergency_shutdown_fn - self.buzzer = buzzer - self.sdcard_ready = sdcard_ready - - if self.reset_experiment_fn is None: - - def do_nothing(*args, **kwargs): - pass - - self.reset_experiment_fn = do_nothing - - if self.emergency_shutdown_fn is None: - self.emergency_shutdown_fn = self.reset_experiment_fn - - if self.validate_inputs_fn is None: - - def no_input_validation(*args, **kwargs): - return True - - self.validate_inputs_fn = no_input_validation - - if self.buzzer is None: - self.buzzer = PWM(Pin(18)) - - def try_experiment(self, msg): - payload_data = {} - # # pin numbers not used here, but can help with organization for complex tasks - # p = int(t[5:]) # pin number - - print(msg) - - # careful not to throw an unrecoverable error due to bad request - # Perform the experiment and record the results - try: - parameters = json.loads(msg) - payload_data["_input_message"] = parameters - - # don't allow access to hardware if any input values are out of bounds - self.validate_inputs_fn(parameters) # type: ignore - - beep(self.buzzer) - sensor_data = self.run_experiment_fn(parameters, self.devices) - payload_data = merge_two_dicts(payload_data, sensor_data) - - except Exception as err: - print(err) - if "_input_message" not in payload_data.keys(): - payload_data["_input_message"] = msg - payload_data["error"] = get_traceback(err) - - try: - payload_data["onboard_temperature_K"] = get_onboard_temperature(unit="K") - payload_data["sd_card_ready"] = self.sdcard_ready - stamp, time_str = get_local_timestamp(return_str=True) # type: ignore - payload_data["utc_timestamp"] = stamp - payload_data["utc_time_str"] = time_str - except OverflowError as e: - print(get_traceback(e)) - except Exception as e: - print(get_traceback(e)) - - try: - parameters = json.loads(msg) - self.reset_experiment_fn(parameters, devices=self.devices) # type: ignore - except Exception as e: - try: - self.emergency_shutdown_fn(devices=self.devices) # type: ignore - payload_data["reset_error"] = get_traceback(e) - except Exception as e: - payload_data["emergency_error"] = get_traceback(e) - - return payload_data - - def write_to_sd_card(self, payload_data, fpath="/sd/experiments.txt"): - try: - write_payload_backup(payload_data, fpath=fpath) - except Exception as e: - w = f"Failed to write to SD card: {get_traceback(e)}" - print(w) - payload_data["warning"] = w - - return payload_data - - # def log_to_mongodb( - # self, - # payload_data, - # api_key: str, - # url: str, - # cluster_name: str, - # database_name: str, - # collection_name: str, - # verbose: bool = True, - # retries: int = 2, - # ): - # try: - # log_to_mongodb( - # payload_data, - # url=url, - # api_key=api_key, - # cluster_name=cluster_name, - # database_name=database_name, - # collection_name=collection_name, - # verbose=verbose, - # retries=retries, - # ) - # except Exception as e: - # print(f"Failed to log to MongoDB backend: {get_traceback(e)}") - - -def heartbeat(client, first, ping_interval_ms=15000): - global lastping - if first: - client.ping() - lastping = ticks_ms() - if ticks_diff(ticks_ms(), lastping) >= ping_interval_ms: - client.ping() - lastping = ticks_ms() - return - - -def sign_of_life(led, first, blink_interval_ms=5000): - global last_blink - if first: - led.on() - last_blink = ticks_ms() - time_since = ticks_diff(ticks_ms(), last_blink) - if led.value() == 0 and time_since >= blink_interval_ms: - led.toggle() - last_blink = ticks_ms() - elif led.value() == 1 and time_since >= 500: - led.toggle() - last_blink = ticks_ms() - - -class DummyMotor: - def __init__(self): - pass - - -class DummySensor: - def __init__(self): - pass diff --git a/scripts/tempSensor/lib/smbus2-0.5.0.dist-info/METADATA b/scripts/tempSensor/lib/smbus2-0.5.0.dist-info/METADATA deleted file mode 100644 index 2226ef2e..00000000 --- a/scripts/tempSensor/lib/smbus2-0.5.0.dist-info/METADATA +++ /dev/null @@ -1,234 +0,0 @@ -Metadata-Version: 2.1 -Name: smbus2 -Version: 0.5.0 -Summary: smbus2 is a drop-in replacement for smbus-cffi/smbus-python in pure Python -Home-page: https://github.com/kplindegaard/smbus2 -Author: Karl-Petter Lindegaard -Author-email: kp.lindegaard@gmail.com -License: MIT -Keywords: smbus,smbus2,python,i2c,raspberrypi,linux -Classifier: Development Status :: 4 - Beta -Classifier: Topic :: Utilities -Classifier: License :: OSI Approved :: MIT License -Classifier: Programming Language :: Python :: 2 -Classifier: Programming Language :: Python :: 2.7 -Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3.6 -Classifier: Programming Language :: Python :: 3.7 -Classifier: Programming Language :: Python :: 3.8 -Classifier: Programming Language :: Python :: 3.9 -Classifier: Programming Language :: Python :: 3.10 -Classifier: Programming Language :: Python :: 3.11 -Classifier: Programming Language :: Python :: 3.12 -Classifier: Programming Language :: Python :: 3.13 -Description-Content-Type: text/markdown -License-File: LICENSE -Provides-Extra: docs -Requires-Dist: sphinx>=1.5.3; extra == "docs" -Provides-Extra: qa -Requires-Dist: flake8; extra == "qa" - -# smbus2 -A drop-in replacement for smbus-cffi/smbus-python in pure Python - -[![Build Status](https://github.com/kplindegaard/smbus2/actions/workflows/python-build-test.yml/badge.svg?branch=master)](https://github.com/kplindegaard/smbus2/actions/workflows/python-build-test.yml) -[![Documentation Status](https://readthedocs.org/projects/smbus2/badge/?version=latest)](http://smbus2.readthedocs.io/en/latest/?badge=latest) -![CodeQL](https://github.com/kplindegaard/smbus2/actions/workflows/codeql-analysis.yml/badge.svg?branch=master) -[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=kplindegaard_smbus2&metric=alert_status)](https://sonarcloud.io/dashboard?id=kplindegaard_smbus2) - -![Python Verions](https://img.shields.io/pypi/pyversions/smbus2.svg) -[![PyPi Version](https://img.shields.io/pypi/v/smbus2.svg)](https://pypi.org/project/smbus2/) -[![PyPI - Downloads](https://img.shields.io/pypi/dm/smbus2)](https://pypi.org/project/smbus2/) - -# Introduction - -smbus2 is (yet another) pure Python implementation of the [python-smbus](http://www.lm-sensors.org/browser/i2c-tools/trunk/py-smbus/) package. - -It was designed from the ground up with two goals in mind: - -1. It should be a drop-in replacement of smbus. The syntax shall be the same. -2. Use the inherent i2c structs and unions to a greater extent than other pure Python implementations like [pysmbus](https://github.com/bjornt/pysmbus) does. By doing so, it will be more feature complete and easier to extend. - -Currently supported features are: - -* Get i2c capabilities (I2C_FUNCS) -* SMBus Packet Error Checking (PEC) support -* read_byte -* write_byte -* read_byte_data -* write_byte_data -* read_word_data -* write_word_data -* read_i2c_block_data -* write_i2c_block_data -* write_quick -* process_call -* read_block_data -* write_block_data -* block_process_call -* i2c_rdwr - *combined write/read transactions with repeated start* - -It is developed on Python 2.7 but works without any modifications in Python 3.X too. - -More information about updates and general changes are recorded in the [change log](https://github.com/kplindegaard/smbus2/blob/master/CHANGELOG.md). - -# SMBus code examples - -smbus2 installs next to smbus as the package, so it's not really a 100% replacement. You must change the module name. - -## Example 1a: Read a byte - -```python -from smbus2 import SMBus - -# Open i2c bus 1 and read one byte from address 80, offset 0 -bus = SMBus(1) -b = bus.read_byte_data(80, 0) -print(b) -bus.close() -``` - -## Example 1b: Read a byte using 'with' - -This is the very same example but safer to use since the smbus will be closed automatically when exiting the with block. - -```python -from smbus2 import SMBus - -with SMBus(1) as bus: - b = bus.read_byte_data(80, 0) - print(b) -``` - -## Example 1c: Read a byte with PEC enabled - -Same example with Packet Error Checking enabled. - -```python -from smbus2 import SMBus - -with SMBus(1) as bus: - bus.pec = 1 # Enable PEC - b = bus.read_byte_data(80, 0) - print(b) -``` - -## Example 2: Read a block of data - -You can read up to 32 bytes at once. - -```python -from smbus2 import SMBus - -with SMBus(1) as bus: - # Read a block of 16 bytes from address 80, offset 0 - block = bus.read_i2c_block_data(80, 0, 16) - # Returned value is a list of 16 bytes - print(block) -``` - -## Example 3: Write a byte - -```python -from smbus2 import SMBus - -with SMBus(1) as bus: - # Write a byte to address 80, offset 0 - data = 45 - bus.write_byte_data(80, 0, data) -``` - -## Example 4: Write a block of data - -It is possible to write 32 bytes at the time, but I have found that error-prone. Write less and add a delay in between if you run into trouble. - -```python -from smbus2 import SMBus - -with SMBus(1) as bus: - # Write a block of 8 bytes to address 80 from offset 0 - data = [1, 2, 3, 4, 5, 6, 7, 8] - bus.write_i2c_block_data(80, 0, data) -``` - -# I2C - -Starting with v0.2, the smbus2 library also has support for combined read and write transactions. *i2c_rdwr* is not really a SMBus feature but comes in handy when the master needs to: - -1. read or write bulks of data larger than SMBus' 32 bytes limit. -1. write some data and then read from the slave with a repeated start and no stop bit between. - -Each operation is represented by a *i2c_msg* message object. - - -## Example 5: Single i2c_rdwr - -```python -from smbus2 import SMBus, i2c_msg - -with SMBus(1) as bus: - # Read 64 bytes from address 80 - msg = i2c_msg.read(80, 64) - bus.i2c_rdwr(msg) - - # Write a single byte to address 80 - msg = i2c_msg.write(80, [65]) - bus.i2c_rdwr(msg) - - # Write some bytes to address 80 - msg = i2c_msg.write(80, [65, 66, 67, 68]) - bus.i2c_rdwr(msg) -``` - -## Example 6: Dual i2c_rdwr - -To perform dual operations just add more i2c_msg instances to the bus call: - -```python -from smbus2 import SMBus, i2c_msg - -# Single transaction writing two bytes then read two at address 80 -write = i2c_msg.write(80, [40, 50]) -read = i2c_msg.read(80, 2) -with SMBus(1) as bus: - bus.i2c_rdwr(write, read) -``` - -## Example 7: Access i2c_msg data - -All data is contained in the i2c_msg instances. Here are some data access alternatives. - -```python -# 1: Convert message content to list -msg = i2c_msg.write(60, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) -data = list(msg) # data = [1, 2, 3, ...] -print(len(data)) # => 10 - -# 2: i2c_msg is iterable -for value in msg: - print(value) - -# 3: Through i2c_msg properties -for k in range(msg.len): - print(msg.buf[k]) -``` - -# Installation instructions - -From [PyPi](https://pypi.org/) with `pip`: - -``` -pip install smbus2 -``` - -From [conda-forge](https://anaconda.org/conda-forge) using `conda`: - -``` -conda install -c conda-forge smbus2 -``` - -Installation from source code is straight forward: - -``` -python setup.py install -``` diff --git a/scripts/tempSensor/lib/smbus2-0.5.0.dist-info/RECORD b/scripts/tempSensor/lib/smbus2-0.5.0.dist-info/RECORD deleted file mode 100644 index 7a53a382..00000000 --- a/scripts/tempSensor/lib/smbus2-0.5.0.dist-info/RECORD +++ /dev/null @@ -1,6 +0,0 @@ -smbus2-0.5.0.dist-info/METADATA,, -smbus2/__init__.py,, -smbus2/py.typed,, -smbus2/smbus2.py,, -smbus2/smbus2.pyi,, -smbus2-0.5.0.dist-info/RECORD,, \ No newline at end of file diff --git a/scripts/tempSensor/lib/smbus2/__init__.py b/scripts/tempSensor/lib/smbus2/__init__.py deleted file mode 100644 index f7f465eb..00000000 --- a/scripts/tempSensor/lib/smbus2/__init__.py +++ /dev/null @@ -1,26 +0,0 @@ -"""smbus2 - A drop-in replacement for smbus-cffi/smbus-python""" -# The MIT License (MIT) -# Copyright (c) 2020 Karl-Petter Lindegaard -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -from .smbus2 import SMBus, i2c_msg, I2cFunc # noqa: F401 - -__version__ = "0.5.0" -__all__ = ["SMBus", "i2c_msg", "I2cFunc"] diff --git a/scripts/tempSensor/lib/smbus2/py.typed b/scripts/tempSensor/lib/smbus2/py.typed deleted file mode 100644 index e69de29b..00000000 diff --git a/scripts/tempSensor/lib/smbus2/smbus2.py b/scripts/tempSensor/lib/smbus2/smbus2.py deleted file mode 100644 index a35868f5..00000000 --- a/scripts/tempSensor/lib/smbus2/smbus2.py +++ /dev/null @@ -1,660 +0,0 @@ -"""smbus2 - A drop-in replacement for smbus-cffi/smbus-python""" -# The MIT License (MIT) -# Copyright (c) 2020 Karl-Petter Lindegaard -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -import os -import sys -from fcntl import ioctl -from ctypes import c_uint32, c_uint8, c_uint16, c_char, POINTER, Structure, Array, Union, create_string_buffer, string_at - - -# Commands from uapi/linux/i2c-dev.h -I2C_SLAVE = 0x0703 # Use this slave address -I2C_SLAVE_FORCE = 0x0706 # Use this slave address, even if it is already in use by a driver! -I2C_FUNCS = 0x0705 # Get the adapter functionality mask -I2C_RDWR = 0x0707 # Combined R/W transfer (one STOP only) -I2C_SMBUS = 0x0720 # SMBus transfer. Takes pointer to i2c_smbus_ioctl_data -I2C_PEC = 0x0708 # != 0 to use PEC with SMBus - -# SMBus transfer read or write markers from uapi/linux/i2c.h -I2C_SMBUS_WRITE = 0 -I2C_SMBUS_READ = 1 - -# Size identifiers uapi/linux/i2c.h -I2C_SMBUS_QUICK = 0 -I2C_SMBUS_BYTE = 1 -I2C_SMBUS_BYTE_DATA = 2 -I2C_SMBUS_WORD_DATA = 3 -I2C_SMBUS_PROC_CALL = 4 -I2C_SMBUS_BLOCK_DATA = 5 # This isn't supported by Pure-I2C drivers with SMBUS emulation, like those in RaspberryPi, OrangePi, etc :( -I2C_SMBUS_BLOCK_PROC_CALL = 7 # Like I2C_SMBUS_BLOCK_DATA, it isn't supported by Pure-I2C drivers either. -I2C_SMBUS_I2C_BLOCK_DATA = 8 -I2C_SMBUS_BLOCK_MAX = 32 - -# To determine what functionality is present (uapi/linux/i2c.h) -try: - from enum import IntFlag -except ImportError: - IntFlag = int - - -class I2cFunc(IntFlag): - """ - These flags identify the operations supported by an I2C/SMBus device. - - You can test these flags on your `smbus.funcs` - - On newer python versions, I2cFunc is an IntFlag enum, but it - falls back to class with a bunch of int constants on older releases. - """ - I2C = 0x00000001 - ADDR_10BIT = 0x00000002 - PROTOCOL_MANGLING = 0x00000004 # I2C_M_IGNORE_NAK etc. - SMBUS_PEC = 0x00000008 - NOSTART = 0x00000010 # I2C_M_NOSTART - SLAVE = 0x00000020 - SMBUS_BLOCK_PROC_CALL = 0x00008000 # SMBus 2.0 - SMBUS_QUICK = 0x00010000 - SMBUS_READ_BYTE = 0x00020000 - SMBUS_WRITE_BYTE = 0x00040000 - SMBUS_READ_BYTE_DATA = 0x00080000 - SMBUS_WRITE_BYTE_DATA = 0x00100000 - SMBUS_READ_WORD_DATA = 0x00200000 - SMBUS_WRITE_WORD_DATA = 0x00400000 - SMBUS_PROC_CALL = 0x00800000 - SMBUS_READ_BLOCK_DATA = 0x01000000 - SMBUS_WRITE_BLOCK_DATA = 0x02000000 - SMBUS_READ_I2C_BLOCK = 0x04000000 # I2C-like block xfer - SMBUS_WRITE_I2C_BLOCK = 0x08000000 # w/ 1-byte reg. addr. - SMBUS_HOST_NOTIFY = 0x10000000 - - SMBUS_BYTE = 0x00060000 - SMBUS_BYTE_DATA = 0x00180000 - SMBUS_WORD_DATA = 0x00600000 - SMBUS_BLOCK_DATA = 0x03000000 - SMBUS_I2C_BLOCK = 0x0c000000 - SMBUS_EMUL = 0x0eff0008 - - -# i2c_msg flags from uapi/linux/i2c.h -I2C_M_RD = 0x0001 - -# Pointer definitions -LP_c_uint8 = POINTER(c_uint8) -LP_c_uint16 = POINTER(c_uint16) -LP_c_uint32 = POINTER(c_uint32) - - -############################################################# -# Type definitions as in i2c.h - - -class i2c_smbus_data(Array): - """ - Adaptation of the i2c_smbus_data union in ``i2c.h``. - - Data for SMBus messages. - """ - _length_ = I2C_SMBUS_BLOCK_MAX + 2 - _type_ = c_uint8 - - -class union_i2c_smbus_data(Union): - _fields_ = [ - ("byte", c_uint8), - ("word", c_uint16), - ("block", i2c_smbus_data) - ] - - -union_pointer_type = POINTER(union_i2c_smbus_data) - - -class i2c_smbus_ioctl_data(Structure): - """ - As defined in ``i2c-dev.h``. - """ - _fields_ = [ - ('read_write', c_uint8), - ('command', c_uint8), - ('size', c_uint32), - ('data', union_pointer_type)] - __slots__ = [name for name, type in _fields_] - - @staticmethod - def create(read_write=I2C_SMBUS_READ, command=0, size=I2C_SMBUS_BYTE_DATA): - u = union_i2c_smbus_data() - return i2c_smbus_ioctl_data( - read_write=read_write, command=command, size=size, - data=union_pointer_type(u)) - - -############################################################# -# Type definitions for i2c_rdwr combined transactions - - -class i2c_msg(Structure): - """ - As defined in ``i2c.h``. - """ - _fields_ = [ - ('addr', c_uint16), - ('flags', c_uint16), - ('len', c_uint16), - ('buf', POINTER(c_char))] - - def __iter__(self): - """ Iterator / Generator - - :return: iterates over :py:attr:`buf` - :rtype: :py:class:`generator` which returns int values - """ - idx = 0 - while idx < self.len: - yield ord(self.buf[idx]) - idx += 1 - - def __len__(self): - return self.len - - def __bytes__(self): - return string_at(self.buf, self.len) - - def __repr__(self): - return 'i2c_msg(%d,%d,%r)' % (self.addr, self.flags, self.__bytes__()) - - def __str__(self): - s = self.__bytes__() - # Throw away non-decodable bytes - s = s.decode(errors="ignore") - return s - - @staticmethod - def read(address, length): - """ - Prepares an i2c read transaction. - - :param address: Slave address. - :type address: int - :param length: Number of bytes to read. - :type length: int - :return: New :py:class:`i2c_msg` instance for read operation. - :rtype: :py:class:`i2c_msg` - """ - arr = create_string_buffer(length) - return i2c_msg( - addr=address, flags=I2C_M_RD, len=length, - buf=arr) - - @staticmethod - def write(address, buf): - """ - Prepares an i2c write transaction. - - :param address: Slave address. - :type address: int - :param buf: Bytes to write. Either list of values or str. - :type buf: list - :return: New :py:class:`i2c_msg` instance for write operation. - :rtype: :py:class:`i2c_msg` - """ - if sys.version_info.major >= 3: - if type(buf) is str: - buf = bytes(map(ord, buf)) - else: - buf = bytes(buf) - else: - if type(buf) is not str: - buf = ''.join([chr(x) for x in buf]) - arr = create_string_buffer(buf, len(buf)) - return i2c_msg( - addr=address, flags=0, len=len(arr), - buf=arr) - - -class i2c_rdwr_ioctl_data(Structure): - """ - As defined in ``i2c-dev.h``. - """ - _fields_ = [ - ('msgs', POINTER(i2c_msg)), - ('nmsgs', c_uint32) - ] - __slots__ = [name for name, type in _fields_] - - @staticmethod - def create(*i2c_msg_instances): - """ - Factory method for creating a i2c_rdwr_ioctl_data struct that can - be called with ``ioctl(fd, I2C_RDWR, data)``. - - :param i2c_msg_instances: Up to 42 i2c_msg instances - :rtype: i2c_rdwr_ioctl_data - """ - n_msg = len(i2c_msg_instances) - msg_array = (i2c_msg * n_msg)(*i2c_msg_instances) - return i2c_rdwr_ioctl_data( - msgs=msg_array, - nmsgs=n_msg - ) - - -############################################################# - - -class SMBus(object): - - def __init__(self, bus=None, force=False): - """ - Initialize and (optionally) open an i2c bus connection. - - :param bus: i2c bus number (e.g. 0 or 1) - or an absolute file path (e.g. `/dev/i2c-42`). - If not given, a subsequent call to ``open()`` is required. - :type bus: int or str - :param force: force using the slave address even when driver is - already using it. - :type force: boolean - """ - self.fd = None - self.funcs = I2cFunc(0) - if bus is not None: - self.open(bus) - self.address = None - self.force = force - self._force_last = None - self._pec = 0 - - def __enter__(self): - """Enter handler.""" - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - """Exit handler.""" - self.close() - - def open(self, bus): - """ - Open a given i2c bus. - - :param bus: i2c bus number (e.g. 0 or 1) - or an absolute file path (e.g. '/dev/i2c-42'). - :type bus: int or str - :raise TypeError: if type(bus) is not in (int, str) - """ - if isinstance(bus, int): - filepath = "/dev/i2c-{}".format(bus) - elif isinstance(bus, str): - filepath = bus - else: - raise TypeError("Unexpected type(bus)={}".format(type(bus))) - - self.fd = os.open(filepath, os.O_RDWR) - self.funcs = self._get_funcs() - - def close(self): - """ - Close the i2c connection. - """ - if self.fd: - os.close(self.fd) - self.fd = None - self._pec = 0 - self.address = None - self._force_last = None - - def _get_pec(self): - return self._pec - - def enable_pec(self, enable=True): - """ - Enable/Disable PEC (Packet Error Checking) - SMBus 1.1 and later - - :param enable: - :type enable: Boolean - """ - if not (self.funcs & I2cFunc.SMBUS_PEC): - raise IOError('SMBUS_PEC is not a feature') - self._pec = int(enable) - ioctl(self.fd, I2C_PEC, self._pec) - - pec = property(_get_pec, enable_pec) # Drop-in replacement for smbus member "pec" - """Get and set SMBus PEC. 0 = disabled (default), 1 = enabled.""" - - def _set_address(self, address, force=None): - """ - Set i2c slave address to use for subsequent calls. - - :param address: - :type address: int - :param force: - :type force: Boolean - """ - force = force if force is not None else self.force - if self.address != address or self._force_last != force: - if force is True: - ioctl(self.fd, I2C_SLAVE_FORCE, address) - else: - ioctl(self.fd, I2C_SLAVE, address) - self.address = address - self._force_last = force - - def _get_funcs(self): - """ - Returns a 32-bit value stating supported I2C functions. - - :rtype: int - """ - f = c_uint32() - ioctl(self.fd, I2C_FUNCS, f) - return f.value - - def write_quick(self, i2c_addr, force=None): - """ - Perform quick transaction. Throws IOError if unsuccessful. - :param i2c_addr: i2c address - :type i2c_addr: int - :param force: - :type force: Boolean - """ - self._set_address(i2c_addr, force=force) - msg = i2c_smbus_ioctl_data.create( - read_write=I2C_SMBUS_WRITE, command=0, size=I2C_SMBUS_QUICK) - ioctl(self.fd, I2C_SMBUS, msg) - - def read_byte(self, i2c_addr, force=None): - """ - Read a single byte from a device. - - :rtype: int - :param i2c_addr: i2c address - :type i2c_addr: int - :param force: - :type force: Boolean - :return: Read byte value - """ - self._set_address(i2c_addr, force=force) - msg = i2c_smbus_ioctl_data.create( - read_write=I2C_SMBUS_READ, command=0, size=I2C_SMBUS_BYTE - ) - ioctl(self.fd, I2C_SMBUS, msg) - return msg.data.contents.byte - - def write_byte(self, i2c_addr, value, force=None): - """ - Write a single byte to a device. - - :param i2c_addr: i2c address - :type i2c_addr: int - :param value: value to write - :type value: int - :param force: - :type force: Boolean - """ - self._set_address(i2c_addr, force=force) - msg = i2c_smbus_ioctl_data.create( - read_write=I2C_SMBUS_WRITE, command=value, size=I2C_SMBUS_BYTE - ) - ioctl(self.fd, I2C_SMBUS, msg) - - def read_byte_data(self, i2c_addr, register, force=None): - """ - Read a single byte from a designated register. - - :param i2c_addr: i2c address - :type i2c_addr: int - :param register: Register to read - :type register: int - :param force: - :type force: Boolean - :return: Read byte value - :rtype: int - """ - self._set_address(i2c_addr, force=force) - msg = i2c_smbus_ioctl_data.create( - read_write=I2C_SMBUS_READ, command=register, size=I2C_SMBUS_BYTE_DATA - ) - ioctl(self.fd, I2C_SMBUS, msg) - return msg.data.contents.byte - - def write_byte_data(self, i2c_addr, register, value, force=None): - """ - Write a byte to a given register. - - :param i2c_addr: i2c address - :type i2c_addr: int - :param register: Register to write to - :type register: int - :param value: Byte value to transmit - :type value: int - :param force: - :type force: Boolean - :rtype: None - """ - self._set_address(i2c_addr, force=force) - msg = i2c_smbus_ioctl_data.create( - read_write=I2C_SMBUS_WRITE, command=register, size=I2C_SMBUS_BYTE_DATA - ) - msg.data.contents.byte = value - ioctl(self.fd, I2C_SMBUS, msg) - - def read_word_data(self, i2c_addr, register, force=None): - """ - Read a single word (2 bytes) from a given register. - - :param i2c_addr: i2c address - :type i2c_addr: int - :param register: Register to read - :type register: int - :param force: - :type force: Boolean - :return: 2-byte word - :rtype: int - """ - self._set_address(i2c_addr, force=force) - msg = i2c_smbus_ioctl_data.create( - read_write=I2C_SMBUS_READ, command=register, size=I2C_SMBUS_WORD_DATA - ) - ioctl(self.fd, I2C_SMBUS, msg) - return msg.data.contents.word - - def write_word_data(self, i2c_addr, register, value, force=None): - """ - Write a single word (2 bytes) to a given register. - - :param i2c_addr: i2c address - :type i2c_addr: int - :param register: Register to write to - :type register: int - :param value: Word value to transmit - :type value: int - :param force: - :type force: Boolean - :rtype: None - """ - self._set_address(i2c_addr, force=force) - msg = i2c_smbus_ioctl_data.create( - read_write=I2C_SMBUS_WRITE, command=register, size=I2C_SMBUS_WORD_DATA - ) - msg.data.contents.word = value - ioctl(self.fd, I2C_SMBUS, msg) - - def process_call(self, i2c_addr, register, value, force=None): - """ - Executes a SMBus Process Call, sending a 16-bit value and receiving a 16-bit response - - :param i2c_addr: i2c address - :type i2c_addr: int - :param register: Register to read/write to - :type register: int - :param value: Word value to transmit - :type value: int - :param force: - :type force: Boolean - :rtype: int - """ - self._set_address(i2c_addr, force=force) - msg = i2c_smbus_ioctl_data.create( - read_write=I2C_SMBUS_WRITE, command=register, size=I2C_SMBUS_PROC_CALL - ) - msg.data.contents.word = value - ioctl(self.fd, I2C_SMBUS, msg) - return msg.data.contents.word - - def read_block_data(self, i2c_addr, register, force=None): - """ - Read a block of up to 32-bytes from a given register. - - :param i2c_addr: i2c address - :type i2c_addr: int - :param register: Start register - :type register: int - :param force: - :type force: Boolean - :return: List of bytes - :rtype: list - """ - self._set_address(i2c_addr, force=force) - msg = i2c_smbus_ioctl_data.create( - read_write=I2C_SMBUS_READ, command=register, size=I2C_SMBUS_BLOCK_DATA - ) - ioctl(self.fd, I2C_SMBUS, msg) - length = msg.data.contents.block[0] - return msg.data.contents.block[1:length + 1] - - def write_block_data(self, i2c_addr, register, data, force=None): - """ - Write a block of byte data to a given register. - - :param i2c_addr: i2c address - :type i2c_addr: int - :param register: Start register - :type register: int - :param data: List of bytes - :type data: list - :param force: - :type force: Boolean - :rtype: None - """ - length = len(data) - if length > I2C_SMBUS_BLOCK_MAX: - raise ValueError("Data length cannot exceed %d bytes" % I2C_SMBUS_BLOCK_MAX) - self._set_address(i2c_addr, force=force) - msg = i2c_smbus_ioctl_data.create( - read_write=I2C_SMBUS_WRITE, command=register, size=I2C_SMBUS_BLOCK_DATA - ) - msg.data.contents.block[0] = length - msg.data.contents.block[1:length + 1] = data - ioctl(self.fd, I2C_SMBUS, msg) - - def block_process_call(self, i2c_addr, register, data, force=None): - """ - Executes a SMBus Block Process Call, sending a variable-size data - block and receiving another variable-size response - - :param i2c_addr: i2c address - :type i2c_addr: int - :param register: Register to read/write to - :type register: int - :param data: List of bytes - :type data: list - :param force: - :type force: Boolean - :return: List of bytes - :rtype: list - """ - length = len(data) - if length > I2C_SMBUS_BLOCK_MAX: - raise ValueError("Data length cannot exceed %d bytes" % I2C_SMBUS_BLOCK_MAX) - self._set_address(i2c_addr, force=force) - msg = i2c_smbus_ioctl_data.create( - read_write=I2C_SMBUS_WRITE, command=register, size=I2C_SMBUS_BLOCK_PROC_CALL - ) - msg.data.contents.block[0] = length - msg.data.contents.block[1:length + 1] = data - ioctl(self.fd, I2C_SMBUS, msg) - length = msg.data.contents.block[0] - return msg.data.contents.block[1:length + 1] - - def read_i2c_block_data(self, i2c_addr, register, length, force=None): - """ - Read a block of byte data from a given register. - - :param i2c_addr: i2c address - :type i2c_addr: int - :param register: Start register - :type register: int - :param length: Desired block length - :type length: int - :param force: - :type force: Boolean - :return: List of bytes - :rtype: list - """ - if length > I2C_SMBUS_BLOCK_MAX: - raise ValueError("Desired block length over %d bytes" % I2C_SMBUS_BLOCK_MAX) - self._set_address(i2c_addr, force=force) - msg = i2c_smbus_ioctl_data.create( - read_write=I2C_SMBUS_READ, command=register, size=I2C_SMBUS_I2C_BLOCK_DATA - ) - msg.data.contents.byte = length - ioctl(self.fd, I2C_SMBUS, msg) - return msg.data.contents.block[1:length + 1] - - def write_i2c_block_data(self, i2c_addr, register, data, force=None): - """ - Write a block of byte data to a given register. - - :param i2c_addr: i2c address - :type i2c_addr: int - :param register: Start register - :type register: int - :param data: List of bytes - :type data: list - :param force: - :type force: Boolean - :rtype: None - """ - length = len(data) - if length > I2C_SMBUS_BLOCK_MAX: - raise ValueError("Data length cannot exceed %d bytes" % I2C_SMBUS_BLOCK_MAX) - self._set_address(i2c_addr, force=force) - msg = i2c_smbus_ioctl_data.create( - read_write=I2C_SMBUS_WRITE, command=register, size=I2C_SMBUS_I2C_BLOCK_DATA - ) - msg.data.contents.block[0] = length - msg.data.contents.block[1:length + 1] = data - ioctl(self.fd, I2C_SMBUS, msg) - - def i2c_rdwr(self, *i2c_msgs): - """ - Combine a series of i2c read and write operations in a single - transaction (with repeated start bits but no stop bits in between). - - This method takes i2c_msg instances as input, which must be created - first with :py:meth:`i2c_msg.read` or :py:meth:`i2c_msg.write`. - - :param i2c_msgs: One or more i2c_msg class instances. - :type i2c_msgs: i2c_msg - :rtype: None - """ - ioctl_data = i2c_rdwr_ioctl_data.create(*i2c_msgs) - ioctl(self.fd, I2C_RDWR, ioctl_data) diff --git a/scripts/tempSensor/lib/smbus2/smbus2.pyi b/scripts/tempSensor/lib/smbus2/smbus2.pyi deleted file mode 100644 index 0861e538..00000000 --- a/scripts/tempSensor/lib/smbus2/smbus2.pyi +++ /dev/null @@ -1,148 +0,0 @@ -from enum import IntFlag -from typing import Optional, Sequence, List, Type, SupportsBytes, Iterable -from typing import Union as _UnionT -from types import TracebackType -from ctypes import c_uint32, c_uint8, c_uint16, pointer, Structure, Array, Union - -I2C_SLAVE: int -I2C_SLAVE_FORCE: int -I2C_FUNCS: int -I2C_RDWR: int -I2C_SMBUS: int -I2C_PEC: int -I2C_SMBUS_WRITE: int -I2C_SMBUS_READ: int -I2C_SMBUS_QUICK: int -I2C_SMBUS_BYTE: int -I2C_SMBUS_BYTE_DATA: int -I2C_SMBUS_WORD_DATA: int -I2C_SMBUS_PROC_CALL: int -I2C_SMBUS_BLOCK_DATA: int -I2C_SMBUS_BLOCK_PROC_CALL: int -I2C_SMBUS_I2C_BLOCK_DATA: int -I2C_SMBUS_BLOCK_MAX: int - -class I2cFunc(IntFlag): - I2C = ... - ADDR_10BIT = ... - PROTOCOL_MANGLING = ... - SMBUS_PEC = ... - NOSTART = ... - SLAVE = ... - SMBUS_BLOCK_PROC_CALL = ... - SMBUS_QUICK = ... - SMBUS_READ_BYTE = ... - SMBUS_WRITE_BYTE = ... - SMBUS_READ_BYTE_DATA = ... - SMBUS_WRITE_BYTE_DATA = ... - SMBUS_READ_WORD_DATA = ... - SMBUS_WRITE_WORD_DATA = ... - SMBUS_PROC_CALL = ... - SMBUS_READ_BLOCK_DATA = ... - SMBUS_WRITE_BLOCK_DATA = ... - SMBUS_READ_I2C_BLOCK = ... - SMBUS_WRITE_I2C_BLOCK = ... - SMBUS_HOST_NOTIFY = ... - SMBUS_BYTE = ... - SMBUS_BYTE_DATA = ... - SMBUS_WORD_DATA = ... - SMBUS_BLOCK_DATA = ... - SMBUS_I2C_BLOCK = ... - SMBUS_EMUL = ... - -I2C_M_RD: int -LP_c_uint8: Type[pointer[c_uint8]] -LP_c_uint16: Type[pointer[c_uint16]] -LP_c_uint32: Type[pointer[c_uint32]] - -class i2c_smbus_data(Array): ... -class union_i2c_smbus_data(Union): ... - -union_pointer_type: pointer[union_i2c_smbus_data] - -class i2c_smbus_ioctl_data(Structure): - @staticmethod - def create( - read_write: int = ..., command: int = ..., size: int = ... - ) -> "i2c_smbus_ioctl_data": ... - -class i2c_msg(Structure): - def __iter__(self) -> int: ... - def __len__(self) -> int: ... - def __bytes__(self) -> str: ... - @staticmethod - def read(address: int, length: int) -> "i2c_msg": ... - @staticmethod - def write(address: int, buf: _UnionT[str, Iterable[int], SupportsBytes]) -> "i2c_msg": ... - -class i2c_rdwr_ioctl_data(Structure): - @staticmethod - def create(*i2c_msg_instances: Sequence[i2c_msg]) -> "i2c_rdwr_ioctl_data": ... - -class SMBus: - fd: Optional[int] = ... - funcs: I2cFunc = ... - address: Optional[int] = ... - force: bool = ... - pec: int = ... - def __init__( - self, bus: _UnionT[None, int, str] = ..., force: bool = ... - ) -> None: ... - def __enter__(self) -> "SMBus": ... - def __exit__( - self, - exc_type: Optional[Type[BaseException]], - exc_val: Optional[BaseException], - exc_tb: Optional[TracebackType], - ) -> None: ... - def open(self, bus: _UnionT[int, str]) -> None: ... - def close(self) -> None: ... - def enable_pec(self, enable: bool = ...) -> None: ... - def write_quick(self, i2c_addr: int, force: Optional[bool] = ...) -> None: ... - def read_byte(self, i2c_addr: int, force: Optional[bool] = ...) -> int: ... - def write_byte( - self, i2c_addr: int, value: int, force: Optional[bool] = ... - ) -> None: ... - def read_byte_data( - self, i2c_addr: int, register: int, force: Optional[bool] = ... - ) -> int: ... - def write_byte_data( - self, i2c_addr: int, register: int, value: int, force: Optional[bool] = ... - ) -> None: ... - def read_word_data( - self, i2c_addr: int, register: int, force: Optional[bool] = ... - ) -> int: ... - def write_word_data( - self, i2c_addr: int, register: int, value: int, force: Optional[bool] = ... - ) -> None: ... - def process_call( - self, i2c_addr: int, register: int, value: int, force: Optional[bool] = ... - ): ... - def read_block_data( - self, i2c_addr: int, register: int, force: Optional[bool] = ... - ) -> List[int]: ... - def write_block_data( - self, - i2c_addr: int, - register: int, - data: Sequence[int], - force: Optional[bool] = ..., - ) -> None: ... - def block_process_call( - self, - i2c_addr: int, - register: int, - data: Sequence[int], - force: Optional[bool] = ..., - ) -> List[int]: ... - def read_i2c_block_data( - self, i2c_addr: int, register: int, length: int, force: Optional[bool] = ... - ) -> List[int]: ... - def write_i2c_block_data( - self, - i2c_addr: int, - register: int, - data: Sequence[int], - force: Optional[bool] = ..., - ) -> None: ... - def i2c_rdwr(self, *i2c_msgs: i2c_msg) -> None: ... diff --git a/scripts/tempSensor/lib/ufastrsa/__init__.py b/scripts/tempSensor/lib/ufastrsa/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/scripts/tempSensor/lib/ufastrsa/genprime.py b/scripts/tempSensor/lib/ufastrsa/genprime.py deleted file mode 100644 index 4478394b..00000000 --- a/scripts/tempSensor/lib/ufastrsa/genprime.py +++ /dev/null @@ -1,136 +0,0 @@ -from ufastrsa import srandom - -try: - from _crypto import NUMBER as tomsfastmath - - pow3_ = tomsfastmath.exptmod - invmod_ = tomsfastmath.invmod - generate_prime_ = tomsfastmath.generate_prime - - def genprime(num=1024, test=25, safe=False): - return generate_prime_(num, test, safe) - -except ImportError: - pow3_ = pow - - def invmod_(a, b): - c, d, e, f, g = 1, 0, 0, 1, b - while b: - q = a // b - a, c, d, b, e, f = b, e, f, a - q * b, c - q * e, d - q * f - assert a >= 0 and c % g >= 0 - return a == 1 and c % g or 0 - - def miller_rabin_pass(a, n): - n_minus_one = n - 1 - s, d = get_lowest_set_bit(n_minus_one) - a_to_power = pow3(a, d, n) - if a_to_power == 1: - return True - for i in range(s): - if a_to_power == n_minus_one: - return True - a_to_power = pow3(a_to_power, 2, n) - if a_to_power == n_minus_one: - return True - return False - - class MillerRabinTest: - def __init__(self, randint, repeat): - self.randint = randint - self.repeat = repeat - - def __call__(self, n): - randint = self.randint - n_minus_one = n - 1 - for repeat in range(self.repeat): - a = randint(1, n_minus_one) - if not miller_rabin_pass(a, n): - return False - return True - - class GenPrime: - def __init__(self, getrandbits, testfn): - self.getrandbits = getrandbits - self.testfn = testfn - - def __call__(self, bits): - getrandbits = self.getrandbits - testfn = self.testfn - while True: - p = (1 << (bits - 1)) | getrandbits(bits - 1) | 1 - if p % 3 != 0 and p % 5 != 0 and p % 7 != 0 and testfn(p): - break - return p - - miller_rabin_test = MillerRabinTest(srandom.randint, 25) - genprime = GenPrime(srandom.getrandbits, miller_rabin_test) - - -def pow3(x, y, z): - return pow3_(x, y, z) - - -def invmod(a, b): - return invmod_(a, b) - - -def get_lowest_set_bit(n): - i = 0 - while n: - if n & 1: - return i, n - n >>= 1 - i += 1 - raise "Error" - - -def gcd(a, b): - while b: - a, b = b, a % b - return a - - -def get_bit_length(n): - return srandom.get_bit_length(n) - - -class GenRSA: - def __init__(self, genprime): - self.genprime = genprime - - def __call__(self, bits, e=None, with_crt=False): - pbits = (bits + 1) >> 1 - qbits = bits - pbits - if e is None: - e = 65537 - elif e < 0: - e = self.genprime(-e) - while True: - p = self.genprime(pbits) - if gcd(e, p - 1) == 1: - break - while True: - while True: - q = self.genprime(qbits) - if gcd(e, q - 1) == 1 and p != q: - break - n = p * q - if get_bit_length(n) == bits: - break - p = max(p, q) - p_minus_1 = p - 1 - q_minus_1 = q - 1 - phi = p_minus_1 * q_minus_1 - d = invmod(e, phi) - if with_crt: - dp = d % p_minus_1 - dq = d % q_minus_1 - qinv = invmod(q, p) - assert qinv < p - return bits, n, e, d, p, q, dp, dq, qinv - else: - return bits, n, e, d - - -genrsa = GenRSA(genprime) diff --git a/scripts/tempSensor/lib/ufastrsa/rsa.py b/scripts/tempSensor/lib/ufastrsa/rsa.py deleted file mode 100644 index 14fd2f83..00000000 --- a/scripts/tempSensor/lib/ufastrsa/rsa.py +++ /dev/null @@ -1,46 +0,0 @@ -from ufastrsa.genprime import pow3 -from ufastrsa.srandom import rndsrcnz - - -class RSA: - def __init__(self, bits, n=None, e=None, d=None): - self.bits = bits - self.bytes = (bits + 7) >> 3 - self.n = n - self.e = e - self.d = d - self.rndsrcnz = rndsrcnz - - def pkcs_sign(self, value): - len_padding = self.bytes - 3 - len(value) - assert len_padding >= 0, len_padding - base = int.from_bytes( - b"\x00\x01" + len_padding * b"\xff" + b"\x00" + value, "big" - ) - return int.to_bytes(pow3(base, self.d, self.n), self.bytes, "big") - - def pkcs_verify(self, value): - assert len(value) == self.bytes - signed = int.to_bytes( - pow3(int.from_bytes(value, "big"), self.e, self.n), self.bytes, "big" - ) - idx = signed.find(b"\0", 1) - assert idx != -1 and signed[:idx] == b"\x00\x01" + (idx - 2) * b"\xff" - return signed[idx + 1 :] - - def pkcs_encrypt(self, value): - len_padding = self.bytes - 3 - len(value) - assert len_padding >= 0 - base = int.from_bytes( - b"\x00\x02" + self.rndsrcnz(len_padding) + b"\x00" + value, "big" - ) - return int.to_bytes(pow3(base, self.e, self.n), self.bytes, "big") - - def pkcs_decrypt(self, value): - assert len(value) == self.bytes - decrypted = int.to_bytes( - pow3(int.from_bytes(value, "big"), self.d, self.n), self.bytes, "big" - ) - idx = decrypted.find(b"\0", 2) - assert idx != -1 and decrypted[:2] == b"\x00\x02" - return decrypted[idx + 1 :] diff --git a/scripts/tempSensor/lib/ufastrsa/srandom.py b/scripts/tempSensor/lib/ufastrsa/srandom.py deleted file mode 100644 index 30bbc666..00000000 --- a/scripts/tempSensor/lib/ufastrsa/srandom.py +++ /dev/null @@ -1,47 +0,0 @@ -from functools import reduce -from os import urandom - -from ufastrsa.util import get_bit_length - - -class Random: - def __init__(self, seed=None, rndsrc=None): - if rndsrc is None: - rndsrc = urandom - self.rndsrc = rndsrc - - def getrandbits(self, k): - if not k >= 0: - raise ValueError("number of bits must be >= 0") - return reduce( - lambda x, y: x << 8 | y, - self.rndsrc(k >> 3), - int.from_bytes(self.rndsrc(1), "little") & ((1 << (k & 7)) - 1), - ) - - def randint(self, a, b): - if a > b: - raise ValueError("empty range for randint(): %d, %d" % (a, b)) - c = 1 + b - a - k = get_bit_length(c - 1) - while True: - r = self.getrandbits(k) - if r <= c: - break - return a + r - - def rndsrcnz(self, size): - rv = self.rndsrc(size).replace(b"\x00", b"") - mv = size - len(rv) - while mv > 0: - rv += self.rndsrc(mv).replace(b"\x00", b"") - mv = size - len(rv) - assert len(rv) == size - return rv - - -basernd = Random() -rndsrc = basernd.rndsrc -getrandbits = basernd.getrandbits -randint = basernd.randint -rndsrcnz = basernd.rndsrcnz diff --git a/scripts/tempSensor/lib/ufastrsa/util.py b/scripts/tempSensor/lib/ufastrsa/util.py deleted file mode 100644 index f74b3d0a..00000000 --- a/scripts/tempSensor/lib/ufastrsa/util.py +++ /dev/null @@ -1,14 +0,0 @@ -try: - int.bit_length(0) - - def get_bit_length(n): - return n.bit_length() - -except: - # Work around - def get_bit_length(n): - i = 0 - while n: - n >>= 1 - i += 1 - return i diff --git a/scripts/tempSensor/lib/unique_id-1.0.1.dist-info/METADATA b/scripts/tempSensor/lib/unique_id-1.0.1.dist-info/METADATA deleted file mode 100644 index 51ea95a7..00000000 --- a/scripts/tempSensor/lib/unique_id-1.0.1.dist-info/METADATA +++ /dev/null @@ -1,11 +0,0 @@ -Metadata-Version: 2.1 -Name: unique-id -Version: 1.0.1 -Summary: Unique-ID is a small lib to generate unique ids - string values. -Home-page: -Download-URL: https://github.com/slawek87/unique-id -Author: Sławomir Kabik -Author-email: slawek@redsoftware.pl -Keywords: Python Unique ID,Python ID,Python Unique string -Requires-Dist: setuptools - diff --git a/scripts/tempSensor/lib/unique_id-1.0.1.dist-info/RECORD b/scripts/tempSensor/lib/unique_id-1.0.1.dist-info/RECORD deleted file mode 100644 index 4e105c50..00000000 --- a/scripts/tempSensor/lib/unique_id-1.0.1.dist-info/RECORD +++ /dev/null @@ -1,5 +0,0 @@ -unique_id-1.0.1.dist-info/METADATA,, -unique_id/__init__.py,, -unique_id/main.py,, -unique_id/tests.py,, -unique_id-1.0.1.dist-info/RECORD,, \ No newline at end of file diff --git a/scripts/tempSensor/lib/unique_id/__init__.py b/scripts/tempSensor/lib/unique_id/__init__.py deleted file mode 100644 index 6138a1f8..00000000 --- a/scripts/tempSensor/lib/unique_id/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from unique_id.main import get_unique_id \ No newline at end of file diff --git a/scripts/tempSensor/lib/unique_id/main.py b/scripts/tempSensor/lib/unique_id/main.py deleted file mode 100644 index 87825a53..00000000 --- a/scripts/tempSensor/lib/unique_id/main.py +++ /dev/null @@ -1,58 +0,0 @@ -import random - - -class UniqueID(object): - """ - Generates Unique ID. - """ - DEFAULT_ID_LENGTH = 14 - DEFAULT_EXCLUDED_CHARS = ":*^`\",.~;%+-'" - - def __init__(self, length=DEFAULT_ID_LENGTH, excluded_chars=DEFAULT_EXCLUDED_CHARS): - """ - `length` - defines length of unique ID. - `excluded_chars` - defines chars excluded during generate process of unique ID. - """ - self.id_length = length - self.excluded_chars = excluded_chars - - def get_random_bits(self): - """ - Method returns random number included in max 8 bits. - """ - return random.getrandbits(8) - - def is_approved_ascii(self, ascii_number): - return 126 >= ascii_number >= 33 - - def is_excluded_char(self, current_char): - """ - Method checks if given char is not in excluded chars list. - """ - return current_char in self.excluded_chars - - def generate_id(self): - """ - Method generates unique ID. - """ - unique_id = "" - - while len(unique_id) < self.id_length: - ascii_number = self.get_random_bits() - - if self.is_approved_ascii(ascii_number): - random_char = chr(ascii_number) - - if not self.is_excluded_char(random_char): - unique_id += chr(ascii_number) - - return unique_id - - -def get_unique_id(length=UniqueID.DEFAULT_ID_LENGTH, excluded_chars=UniqueID.DEFAULT_EXCLUDED_CHARS): - """ - Function returns unique ID. - """ - unique_id = UniqueID(length=length, excluded_chars=excluded_chars) - return unique_id.generate_id() - diff --git a/scripts/tempSensor/lib/unique_id/tests.py b/scripts/tempSensor/lib/unique_id/tests.py deleted file mode 100644 index 6438c2cc..00000000 --- a/scripts/tempSensor/lib/unique_id/tests.py +++ /dev/null @@ -1,40 +0,0 @@ -from random import randint -import unittest - -from unique_id import get_unique_id - - -class TestStringMethods(unittest.TestCase): - def test_unique_id(self): - unique_ids = list() - - for item in range(1000): - unique_id = get_unique_id() - - is_duplicated = unique_id in unique_ids - self.assertFalse(is_duplicated) - - unique_ids.append(unique_id) - - def test_max_length(self): - for item in range(1000): - id_length = randint(1, 128) - unique_id = get_unique_id(length=id_length) - - is_over_length = len(unique_id) != id_length - self.assertFalse(is_over_length) - - def test_excluded_chars(self): - id_length = 256 - excluded_chars = [1, 'f', 'm', 'a', 4, 5, 'Z', 'w', '_'] - - for item in range(1000): - unique_id = get_unique_id(length=id_length, excluded_chars=excluded_chars) - - for seed in unique_id: - is_excluded_char = seed in excluded_chars - self.assertFalse(is_excluded_char) - - -if __name__ == '__main__': - unittest.main() diff --git a/scripts/tempSensor/lib/urequests_2.py b/scripts/tempSensor/lib/urequests_2.py deleted file mode 100644 index f43f88eb..00000000 --- a/scripts/tempSensor/lib/urequests_2.py +++ /dev/null @@ -1,203 +0,0 @@ -# Workaround for the `urequests` module to support HTTP/1.1 -# Based on https://github.com/micropython/micropython-lib/blob/e025c843b60e93689f0f991d753010bb5bd6a722/python-ecosys/requests/requests/__init__.py -# See https://github.com/micropython/micropython-lib/pull/861 and https://github.com/orgs/micropython/discussions/15112 -# `1.0` replaced with `1.1, i.e.: -# `s.write(b"%s /%s HTTP/1.0\r\n" % (method, path))` changed to `s.write(b"%s /%s HTTP/1.1\r\n" % (method, path))` -import usocket - - -class Response: - def __init__(self, f): - self.raw = f - self.encoding = "utf-8" - self._cached = None - - def close(self): - if self.raw: - self.raw.close() - self.raw = None - self._cached = None - - @property - def content(self): - if self._cached is None: - try: - self._cached = self.raw.read() - finally: - self.raw.close() - self.raw = None - return self._cached - - @property - def text(self): - return str(self.content, self.encoding) - - def json(self): - import ujson - - return ujson.loads(self.content) - - -def request( - method, - url, - data=None, - json=None, - headers={}, - stream=None, - auth=None, - timeout=None, - parse_headers=True, -): - redirect = None # redirection url, None means no redirection - chunked_data = ( - data and getattr(data, "__next__", None) and not getattr(data, "__len__", None) - ) - - if auth is not None: - import ubinascii - - username, password = auth - formated = b"{}:{}".format(username, password) - formated = str(ubinascii.b2a_base64(formated)[:-1], "ascii") - headers["Authorization"] = "Basic {}".format(formated) - - try: - proto, dummy, host, path = url.split("/", 3) - except ValueError: - proto, dummy, host = url.split("/", 2) - path = "" - if proto == "http:": - port = 80 - elif proto == "https:": - import ussl - - port = 443 - else: - raise ValueError("Unsupported protocol: " + proto) - - if ":" in host: - host, port = host.split(":", 1) - port = int(port) - - ai = usocket.getaddrinfo(host, port, 0, usocket.SOCK_STREAM) - ai = ai[0] - - resp_d = None - if parse_headers is not False: - resp_d = {} - - s = usocket.socket(ai[0], usocket.SOCK_STREAM, ai[2]) - - if timeout is not None: - # Note: settimeout is not supported on all platforms, will raise - # an AttributeError if not available. - s.settimeout(timeout) - - try: - s.connect(ai[-1]) - if proto == "https:": - s = ussl.wrap_socket(s, server_hostname=host) - s.write(b"%s /%s HTTP/1.1\r\n" % (method, path)) - if "Host" not in headers: - s.write(b"Host: %s\r\n" % host) - # Iterate over keys to avoid tuple alloc - for k in headers: - s.write(k) - s.write(b": ") - s.write(headers[k]) - s.write(b"\r\n") - if json is not None: - assert data is None - import ujson - - data = ujson.dumps(json) - s.write(b"Content-Type: application/json\r\n") - if data: - if chunked_data: - s.write(b"Transfer-Encoding: chunked\r\n") - else: - s.write(b"Content-Length: %d\r\n" % len(data)) - s.write(b"Connection: close\r\n\r\n") - if data: - if chunked_data: - for chunk in data: - s.write(b"%x\r\n" % len(chunk)) - s.write(chunk) - s.write(b"\r\n") - s.write("0\r\n\r\n") - else: - s.write(data) - - l = s.readline() - # print(l) - l = l.split(None, 2) - if len(l) < 2: - # Invalid response - raise ValueError("HTTP error: BadStatusLine:\n%s" % l) - status = int(l[1]) - reason = "" - if len(l) > 2: - reason = l[2].rstrip() - while True: - l = s.readline() - if not l or l == b"\r\n": - break - # print(l) - if l.startswith(b"Transfer-Encoding:"): - if b"chunked" in l: - raise ValueError("Unsupported " + str(l, "utf-8")) - elif l.startswith(b"Location:") and not 200 <= status <= 299: - if status in [301, 302, 303, 307, 308]: - redirect = str(l[10:-2], "utf-8") - else: - raise NotImplementedError("Redirect %d not yet supported" % status) - if parse_headers is False: - pass - elif parse_headers is True: - l = str(l, "utf-8") - k, v = l.split(":", 1) - resp_d[k] = v.strip() - else: - parse_headers(l, resp_d) - except OSError: - s.close() - raise - - if redirect: - s.close() - if status in [301, 302, 303]: - return request("GET", redirect, None, None, headers, stream) - else: - return request(method, redirect, data, json, headers, stream) - else: - resp = Response(s) - resp.status_code = status - resp.reason = reason - if resp_d is not None: - resp.headers = resp_d - return resp - - -def head(url, **kw): - return request("HEAD", url, **kw) - - -def get(url, **kw): - return request("GET", url, **kw) - - -def post(url, **kw): - return request("POST", url, **kw) - - -def put(url, **kw): - return request("PUT", url, **kw) - - -def patch(url, **kw): - return request("PATCH", url, **kw) - - -def delete(url, **kw): - return request("DELETE", url, **kw) diff --git a/scripts/tempSensor/main.py b/scripts/tempSensor/main.py new file mode 100644 index 00000000..16b4b18c --- /dev/null +++ b/scripts/tempSensor/main.py @@ -0,0 +1,77 @@ +from machine import Pin, I2C, reset +from bme680 import BME680_I2C +from netman import connectWiFi +from umqtt.simple import MQTTClient +import time +import json + +# Configuration +SSID = 'Pixel 8' +PASSWORD = '123456789' +MQTT_BROKER = b'b6bdb89571144b3d8e5ca4bbe666ddb5.s1.eu.hivemq.cloud' +MQTT_PORT = 8883 +MQTT_TOPIC = b"sensors/bme680/data" +MQTT_USER = b'Luthiraa' +MQTT_PASS = b'theboss1010' + +# Initialize I2C and Sensor +i2c = I2C(0, scl=Pin(5), sda=Pin(4)) +bme = BME680_I2C(i2c) + +def connect_to_wifi(): + """Connects to Wi-Fi, restarts on failure.""" + try: + status = connectWiFi(SSID, PASSWORD, country='US', retries=5) + print("Wi-Fi connected! IP:", status[0]) + except Exception as e: + print(f"Wi-Fi error: {e}") + time.sleep(5) + reset() + +def connect_to_mqtt(): + """Connects to MQTT broker, restarts on failure.""" + client = MQTTClient( + client_id=b"pico_w", + server=MQTT_BROKER, + port=MQTT_PORT, + user=MQTT_USER, + password=MQTT_PASS, + keepalive=60, + ssl=True, + ssl_params={'server_hostname': MQTT_BROKER} + ) + try: + client.connect() + print("Connected to MQTT broker!") + return client + except Exception as e: + print(f"MQTT error: {e}") + time.sleep(5) + reset() + +def main(): + connect_to_wifi() + mqtt_client = connect_to_mqtt() + + while True: + try: + # Create JSON payload + payload = json.dumps({ + "temperature": round(bme.temperature, 2), + "humidity": round(bme.humidity, 2), + "pressure": round(bme.pressure, 2), + "gas": round(bme.gas, 2) + }) + + mqtt_client.publish(MQTT_TOPIC, payload) + print("Published:", payload) + time.sleep(2) + + except Exception as e: + print(f"Runtime error: {e}") + time.sleep(5) + reset() + +if __name__ == "__main__": + main() + diff --git a/scripts/tempSensor/rsa.json b/scripts/tempSensor/rsa.json deleted file mode 100644 index 2b6faab5..00000000 --- a/scripts/tempSensor/rsa.json +++ /dev/null @@ -1 +0,0 @@ -{"e": 65537, "bits": 256, "d": 61449807040989742694108195033472778816813477767129784887758545872352461876233, "n": 93521805862369252866652008218584959020573609838284713191171615421986659700917} \ No newline at end of file diff --git a/scripts/tempSensor/secrets.py b/scripts/tempSensor/secrets.py deleted file mode 100644 index 729d7eb8..00000000 --- a/scripts/tempSensor/secrets.py +++ /dev/null @@ -1,83 +0,0 @@ -from bme680 import BME680_I2C # Ensure you have the right import for the BME680 class -from machine import I2C, Pin -from netman import connectWiFi -from umqtt.simple import MQTTClient -import time - -# Wi-Fi and MQTT configuration -SSID = 'Pixel 8' # Replace with your Wi-Fi SSID -PASSWORD = '123456789' # Replace with your Wi-Fi password -MQTT_BROKER = 'b6bdb89571144b3d8e5ca4bbe666ddb5.s1.eu.hivemq.cloud' # HiveMQ Cloud broker URL -MQTT_PORT = 8883 # Port for TLS -MQTT_TOPIC = "sensors/bme680/data" # Replace with your desired MQTT topic - -MQTT_USER = 'Luthiraa' -MQTT_PASS = 'theboss1010' - -def connect_to_internet(): - try: - status = connectWiFi(SSID, PASSWORD, country='US', retries=3) - print("Connected to Wi-Fi successfully!") - print("IP Address:", status[0]) - except RuntimeError as e: - print(f"Failed to connect to Wi-Fi: {e}") - raise - -# Initialize I2C and BME680 -i2c = I2C(1, scl=Pin(27), sda=Pin(26)) -bme = BME680_I2C(i2c) - -# MQTT setup with authentication and TLS -client = MQTTClient( - client_id=b"kudzai_raspberrypi_picow", - server=MQTT_BROKER, - port=MQTT_PORT, - user=MQTT_USER, - password=MQTT_PASS, - keepalive=60, # Set to a shorter interval - ssl=True, - ssl_params={'server_hostname': MQTT_BROKER} -) -# Connect to MQTT broker -def connect_to_mqtt(): - try: - client.connect() - print("Connected to MQTT broker") - print("Client ID:", client.client_id) # Print client ID - except Exception as e: - print(f"Failed to connect to MQTT broker: {e}") - raise - -# Connect to Wi-Fi and MQTT -connect_to_internet() -connect_to_mqtt() - -while True: - # Read sensor data - temperature = bme.temperature - humidity = bme.humidity - pressure = bme.pressure - gas = bme.gas - - # Prepare data payload - payload = ( - f"Temperature: {temperature:.2f} °C, " - f"Humidity: {humidity:.2f} %, " - f"Pressure: {pressure:.2f} hPa, " - f"Gas: {gas:.2f} ohms" - ) - - # Print data to console - print("--------------------------------------------------") - print(payload) - print("--------------------------------------------------") - - # Publish data to MQTT broker - try: - client.publish(MQTT_TOPIC, payload) - print("Data published to MQTT topic:", MQTT_TOPIC) - except Exception as e: - print(f"Failed to publish data: {e}") - client.connect() - - time.sleep(2) diff --git a/scripts/tempSensor/temp.py b/scripts/tempSensor/temp.py deleted file mode 100644 index 74e84b3f..00000000 --- a/scripts/tempSensor/temp.py +++ /dev/null @@ -1,19 +0,0 @@ -from bme680 import * # Ensure you have the right import for the BME680 class -from machine import I2C, Pin -import time - -# Initialize I2C on GPIO pins 27 (SCL) and 26 (SDA) for the Raspberry Pi Pico W -i2c = I2C(1, scl=Pin(27), sda=Pin(26)) - -# Initialize the BME680 sensor over I2C -bme = BME680_I2C(i2c) - -while True: - print("--------------------------------------------------") - print() - print("Temperature: {:.2f} °C".format(bme.temperature)) - print("Humidity: {:.2f} %".format(bme.humidity)) - print("Pressure: {:.2f} hPa".format(bme.pressure)) - print("Gas: {:.2f} ohms".format(bme.gas)) - print() - time.sleep(3) diff --git a/scripts/tempSensor/tempSensor.py b/scripts/tempSensor/tempSensor.py deleted file mode 100644 index ad6903b8..00000000 --- a/scripts/tempSensor/tempSensor.py +++ /dev/null @@ -1,18 +0,0 @@ -from bme680 import * -from machine import I2C, Pin -import time - -i2c = I2C(0, scl=Pin(5), sda=Pin(4)) - -bme = BME680_I2C(i2c) - -while True: - print("--------------------------------------------------") - print() - print("Temperature: {:.2f} °C".format(bme.temperature)) - print("Humidity: {:.2f} %".format(bme.humidity)) - print("Pressure: {:.2f} hPa".format(bme.pressure)) - print("Gas: {:.2f} ohms".format(bme.gas)) - print() - time.sleep(3) - diff --git a/scripts/tempSensor/test.py b/scripts/tempSensor/test.py deleted file mode 100644 index 595de131..00000000 --- a/scripts/tempSensor/test.py +++ /dev/null @@ -1,65 +0,0 @@ -from bme680 import BME680_I2C -from machine import I2C, Pin -from netman import connectWiFi -from umqtt.simple import MQTTClient -import json -import time -from constants import WIFI_SSID, WIFI_PASSWORD, MQTT_BROKER, MQTT_PORT, MQTT_TOPIC, MQTT_USER, MQTT_PASSWORD - - -def connect_to_internet(): - try: - status = connectWiFi(WIFI_SSID, WIFI_PASSWORD, country='US', retries=3) - print("Connected to Wi-Fi successfully!") - print("IP Address:", status[0]) - except RuntimeError as e: - print(f"Failed to connect to Wi-Fi: {e}") - raise - -i2c = I2C(1, scl=Pin(27), sda=Pin(26)) -bme = BME680_I2C(i2c) - -client = MQTTClient( - client_id=b"kudzai_raspberrypi_picow", - server=MQTT_BROKER, - port=MQTT_PORT, - user=MQTT_USER, - password=MQTT_PASSWORD, - keepalive=60, # Set to a shorter interval - ssl=True, - ssl_params={'server_hostname': MQTT_BROKER} -) - -def connect_to_mqtt(): - try: - client.connect() - print("Connected to MQTT broker") - print("Client ID:", client.client_id) - except Exception as e: - print(f"Failed to connect to MQTT broker: {e}") - raise - -connect_to_internet() -connect_to_mqtt() - -while True: - temperature = bme.temperature - humidity = bme.humidity - pressure = bme.pressure - gas = bme.gas - payload = json.dumps({ - "temperature": temperature, - "humidity": humidity, - "pressure": pressure, - "gas": gas - }) - print("oayload:", payload) - - try: - client.publish(MQTT_TOPIC, payload) - print("Data published to MQTT topic:", MQTT_TOPIC) - except Exception as e: - print(f"Failed to publish data: {e}") - client.connect() - - time.sleep(2) diff --git a/scripts/tempSensor/lib/umqtt/robust.py b/scripts/tempSensor/umqtt/robust.py similarity index 100% rename from scripts/tempSensor/lib/umqtt/robust.py rename to scripts/tempSensor/umqtt/robust.py diff --git a/scripts/tempSensor/lib/umqtt/simple.py b/scripts/tempSensor/umqtt/simple.py similarity index 100% rename from scripts/tempSensor/lib/umqtt/simple.py rename to scripts/tempSensor/umqtt/simple.py From b9e1da4a79a1ded841811b6ad7a4ba2fabe99bd6 Mon Sep 17 00:00:00 2001 From: Luthira A <79228258+Luthiraa@users.noreply.github.com> Date: Tue, 11 Mar 2025 23:38:28 -0400 Subject: [PATCH 08/19] Add pre-commit configuration for code quality checks --- scripts/tempSensor/.pre-commit-config.yaml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 scripts/tempSensor/.pre-commit-config.yaml diff --git a/scripts/tempSensor/.pre-commit-config.yaml b/scripts/tempSensor/.pre-commit-config.yaml new file mode 100644 index 00000000..d275aca9 --- /dev/null +++ b/scripts/tempSensor/.pre-commit-config.yaml @@ -0,0 +1,18 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v3.4.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - repo: https://github.com/ambv/black + rev: 21.12b0 + hooks: + - id: black + - repo: https://github.com/PyCQA/isort + rev: 5.9.3 + hooks: + - id: isort + - repo: https://gitlab.com/pycqa/flake8 + rev: 3.9.2 + hooks: + - id: flake8 \ No newline at end of file From a8c6a8e7d2d58babc4c0511d1acf71f814666245 Mon Sep 17 00:00:00 2001 From: Luthira A <79228258+Luthiraa@users.noreply.github.com> Date: Tue, 11 Mar 2025 23:54:00 -0400 Subject: [PATCH 09/19] Refactor code by removing unnecessary blank lines and improving formatting for better readability --- docs/conf.py | 2 +- scripts/tempSensor/lib/bme680.py | 184 ++++++++++++------ scripts/tempSensor/lib/netman.py | 1 - scripts/tempSensor/main.py | 40 ++-- src/ac_training_lab/cobot280pi/client.py | 1 - src/ac_training_lab/cobot280pi/dummy_cobot.py | 1 - .../openflexure/huggingface/key_request.py | 3 - .../_scripts/streamlit_webapp.py | 1 - .../_scripts/time-sync-micro.py | 1 - .../picow/digital-pipette/time-sync-micro.py | 1 - 10 files changed, 152 insertions(+), 83 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 5c619b03..4ac02d7c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -8,8 +8,8 @@ # serve to show the default. import os -import sys import shutil +import sys # -- Path setup -------------------------------------------------------------- diff --git a/scripts/tempSensor/lib/bme680.py b/scripts/tempSensor/lib/bme680.py index bd2757ee..aec33c73 100644 --- a/scripts/tempSensor/lib/bme680.py +++ b/scripts/tempSensor/lib/bme680.py @@ -34,10 +34,12 @@ and many more contributors """ -import time import math +import time + from micropython import const from ubinascii import hexlify as hex + try: import struct except ImportError: @@ -71,20 +73,49 @@ _BME680_RUNGAS = const(0x10) -_LOOKUP_TABLE_1 = (2147483647.0, 2147483647.0, 2147483647.0, 2147483647.0, 2147483647.0, - 2126008810.0, 2147483647.0, 2130303777.0, 2147483647.0, 2147483647.0, - 2143188679.0, 2136746228.0, 2147483647.0, 2126008810.0, 2147483647.0, - 2147483647.0) - -_LOOKUP_TABLE_2 = (4096000000.0, 2048000000.0, 1024000000.0, 512000000.0, 255744255.0, 127110228.0, - 64000000.0, 32258064.0, 16016016.0, 8000000.0, 4000000.0, 2000000.0, 1000000.0, - 500000.0, 250000.0, 125000.0) +_LOOKUP_TABLE_1 = ( + 2147483647.0, + 2147483647.0, + 2147483647.0, + 2147483647.0, + 2147483647.0, + 2126008810.0, + 2147483647.0, + 2130303777.0, + 2147483647.0, + 2147483647.0, + 2143188679.0, + 2136746228.0, + 2147483647.0, + 2126008810.0, + 2147483647.0, + 2147483647.0, +) + +_LOOKUP_TABLE_2 = ( + 4096000000.0, + 2048000000.0, + 1024000000.0, + 512000000.0, + 255744255.0, + 127110228.0, + 64000000.0, + 32258064.0, + 16016016.0, + 8000000.0, + 4000000.0, + 2000000.0, + 1000000.0, + 500000.0, + 250000.0, + 125000.0, +) def _read24(arr): """Parse an unsigned 24-bit value as a floating point and return it.""" ret = 0.0 - #print([hex(i) for i in arr]) + # print([hex(i) for i in arr]) for b in arr: ret *= 256.0 ret += float(b & 0xFF) @@ -94,18 +125,19 @@ def _read24(arr): class Adafruit_BME680: """Driver from BME680 air quality sensor - :param int refresh_rate: Maximum number of readings per second. Faster property reads - will be from the previous reading.""" + :param int refresh_rate: Maximum number of readings per second. Faster property reads + will be from the previous reading.""" + def __init__(self, *, refresh_rate=10): """Check the BME680 was found, read the coefficients and enable the sensor for continuous - reads.""" + reads.""" self._write(_BME680_REG_SOFTRESET, [0xB6]) time.sleep(0.005) # Check device ID. chip_id = self._read_byte(_BME680_REG_CHIPID) if chip_id != _BME680_CHIPID: - raise RuntimeError('Failed to find BME680! Chip ID 0x%x' % chip_id) + raise RuntimeError("Failed to find BME680! Chip ID 0x%x" % chip_id) self._read_calibration() @@ -184,7 +216,7 @@ def filter_size(self, size): def temperature(self): """The compensated temperature in degrees celsius.""" self._perform_reading() - calc_temp = (((self._t_fine * 5) + 128) / 256) + calc_temp = ((self._t_fine * 5) + 128) / 256 return calc_temp / 100 @property @@ -196,31 +228,49 @@ def pressure(self): var2 = (var2 * self._pressure_calibration[5]) / 4 var2 = var2 + (var1 * self._pressure_calibration[4] * 2) var2 = (var2 / 4) + (self._pressure_calibration[3] * 65536) - var1 = (((((var1 / 4) * (var1 / 4)) / 8192) * - (self._pressure_calibration[2] * 32) / 8) + - ((self._pressure_calibration[1] * var1) / 2)) + var1 = ( + (((var1 / 4) * (var1 / 4)) / 8192) + * (self._pressure_calibration[2] * 32) + / 8 + ) + ((self._pressure_calibration[1] * var1) / 2) var1 = var1 / 262144 var1 = ((32768 + var1) * self._pressure_calibration[0]) / 32768 calc_pres = 1048576 - self._adc_pres calc_pres = (calc_pres - (var2 / 4096)) * 3125 calc_pres = (calc_pres / var1) * 2 - var1 = (self._pressure_calibration[8] * (((calc_pres / 8) * (calc_pres / 8)) / 8192)) / 4096 + var1 = ( + self._pressure_calibration[8] * (((calc_pres / 8) * (calc_pres / 8)) / 8192) + ) / 4096 var2 = ((calc_pres / 4) * self._pressure_calibration[7]) / 8192 var3 = (((calc_pres / 256) ** 3) * self._pressure_calibration[9]) / 131072 - calc_pres += ((var1 + var2 + var3 + (self._pressure_calibration[6] * 128)) / 16) - return calc_pres/100 + calc_pres += (var1 + var2 + var3 + (self._pressure_calibration[6] * 128)) / 16 + return calc_pres / 100 @property def humidity(self): """The relative humidity in RH %""" self._perform_reading() temp_scaled = ((self._t_fine * 5) + 128) / 256 - var1 = ((self._adc_hum - (self._humidity_calibration[0] * 16)) - - ((temp_scaled * self._humidity_calibration[2]) / 200)) - var2 = (self._humidity_calibration[1] * - (((temp_scaled * self._humidity_calibration[3]) / 100) + - (((temp_scaled * ((temp_scaled * self._humidity_calibration[4]) / 100)) / - 64) / 100) + 16384)) / 1024 + var1 = (self._adc_hum - (self._humidity_calibration[0] * 16)) - ( + (temp_scaled * self._humidity_calibration[2]) / 200 + ) + var2 = ( + self._humidity_calibration[1] + * ( + ((temp_scaled * self._humidity_calibration[3]) / 100) + + ( + ( + ( + temp_scaled + * ((temp_scaled * self._humidity_calibration[4]) / 100) + ) + / 64 + ) + / 100 + ) + + 16384 + ) + ) / 1024 var3 = var1 * var2 var4 = self._humidity_calibration[5] * 128 var4 = (var4 + ((temp_scaled * self._humidity_calibration[6]) / 100)) / 16 @@ -238,15 +288,17 @@ def humidity(self): @property def altitude(self): """The altitude based on current ``pressure`` vs the sea level pressure - (``sea_level_pressure``) - which you must enter ahead of time)""" - pressure = self.pressure # in Si units for hPascal + (``sea_level_pressure``) - which you must enter ahead of time)""" + pressure = self.pressure # in Si units for hPascal return 44330 * (1.0 - math.pow(pressure / self.sea_level_pressure, 0.1903)) @property def gas(self): """The gas resistance in ohms""" self._perform_reading() - var1 = ((1340 + (5 * self._sw_err)) * (_LOOKUP_TABLE_1[self._gas_range])) / 65536 + var1 = ( + (1340 + (5 * self._sw_err)) * (_LOOKUP_TABLE_1[self._gas_range]) + ) / 65536 var2 = ((self._adc_gas * 32768) - 16777216) + var1 var3 = (_LOOKUP_TABLE_2[self._gas_range] * var1) / 512 calc_gas_res = (var3 + (var2 / 2)) / var2 @@ -254,16 +306,20 @@ def gas(self): def _perform_reading(self): """Perform a single-shot reading from the sensor and fill internal data structure for - calculations""" - expired = time.ticks_diff(self._last_reading, time.ticks_ms()) * time.ticks_diff(0, 1) + calculations""" + expired = time.ticks_diff( + self._last_reading, time.ticks_ms() + ) * time.ticks_diff(0, 1) if 0 <= expired < self._min_refresh_time: time.sleep_ms(self._min_refresh_time - expired) # set filter self._write(_BME680_REG_CONFIG, [self._filter << 2]) # turn on temp oversample & pressure oversample - self._write(_BME680_REG_CTRL_MEAS, - [(self._temp_oversample << 5)|(self._pressure_oversample << 2)]) + self._write( + _BME680_REG_CTRL_MEAS, + [(self._temp_oversample << 5) | (self._pressure_oversample << 2)], + ) # turn on humidity oversample self._write(_BME680_REG_CTRL_HUM, [self._humidity_oversample]) # gas measurements enabled @@ -281,8 +337,8 @@ def _perform_reading(self): self._adc_pres = _read24(data[2:5]) / 16 self._adc_temp = _read24(data[5:8]) / 16 - self._adc_hum = struct.unpack('>H', bytes(data[8:10]))[0] - self._adc_gas = int(struct.unpack('>H', bytes(data[13:15]))[0] / 64) + self._adc_hum = struct.unpack(">H", bytes(data[8:10]))[0] + self._adc_gas = int(struct.unpack(">H", bytes(data[13:15]))[0] / 64) self._gas_range = data[14] & 0x0F var1 = (self._adc_temp / 8) - (self._temp_calibration[0] * 2) @@ -297,11 +353,13 @@ def _read_calibration(self): coeff = self._read(_BME680_BME680_COEFF_ADDR1, 25) coeff += self._read(_BME680_BME680_COEFF_ADDR2, 16) - coeff = list(struct.unpack('= var + access_time: - access_key = "Microscope" + str(random.randint(10000000, 99999999)) delete_user(microscope + "clientuser") create_user(microscope + "clientuser", access_key) diff --git a/src/ac_training_lab/picow/digital-pipette/_scripts/streamlit_webapp.py b/src/ac_training_lab/picow/digital-pipette/_scripts/streamlit_webapp.py index 9190456f..c4c19bb4 100644 --- a/src/ac_training_lab/picow/digital-pipette/_scripts/streamlit_webapp.py +++ b/src/ac_training_lab/picow/digital-pipette/_scripts/streamlit_webapp.py @@ -32,7 +32,6 @@ # singleton: https://docs.streamlit.io/develop/api-reference/caching-and-state/st.cache_resource @st.cache_resource def get_paho_client(hostname, username, password=None, port=8883, tls=True): - client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, protocol=mqtt.MQTTv5) # The callback for when the client receives a CONNACK response from the server. diff --git a/src/ac_training_lab/picow/digital-pipette/_scripts/time-sync-micro.py b/src/ac_training_lab/picow/digital-pipette/_scripts/time-sync-micro.py index 89800d93..8452fe9c 100644 --- a/src/ac_training_lab/picow/digital-pipette/_scripts/time-sync-micro.py +++ b/src/ac_training_lab/picow/digital-pipette/_scripts/time-sync-micro.py @@ -127,7 +127,6 @@ async def messages(client): print((topic, msg, retained)) if topic == command_topic: - data = json.loads(msg) time = data["ntp_time"] received_time = "True" diff --git a/src/ac_training_lab/picow/digital-pipette/time-sync-micro.py b/src/ac_training_lab/picow/digital-pipette/time-sync-micro.py index 36037fd3..6a0401e3 100644 --- a/src/ac_training_lab/picow/digital-pipette/time-sync-micro.py +++ b/src/ac_training_lab/picow/digital-pipette/time-sync-micro.py @@ -127,7 +127,6 @@ async def messages(client): print((topic, msg, retained)) if topic == command_topic: - data = json.loads(msg) time = data["ntp_time"] received_time = "True" From e2569c3be3f8e69f66f7083c390ee26b7af3458f Mon Sep 17 00:00:00 2001 From: Luthira A <79228258+Luthiraa@users.noreply.github.com> Date: Tue, 11 Mar 2025 23:59:03 -0400 Subject: [PATCH 10/19] Fix end-of-file issues --- scripts/tempSensor/.pre-commit-config.yaml | 2 +- scripts/tempSensor/pico_id.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/tempSensor/.pre-commit-config.yaml b/scripts/tempSensor/.pre-commit-config.yaml index d275aca9..4bb444e3 100644 --- a/scripts/tempSensor/.pre-commit-config.yaml +++ b/scripts/tempSensor/.pre-commit-config.yaml @@ -15,4 +15,4 @@ repos: - repo: https://gitlab.com/pycqa/flake8 rev: 3.9.2 hooks: - - id: flake8 \ No newline at end of file + - id: flake8 diff --git a/scripts/tempSensor/pico_id.txt b/scripts/tempSensor/pico_id.txt index 3bb8d27d..ad449ae1 100644 --- a/scripts/tempSensor/pico_id.txt +++ b/scripts/tempSensor/pico_id.txt @@ -1 +1 @@ -e66130100f594628 \ No newline at end of file +e66130100f594628 From c40470a6b7c90140374f2406f978bc5357965da0 Mon Sep 17 00:00:00 2001 From: Luthira A <79228258+Luthiraa@users.noreply.github.com> Date: Wed, 12 Mar 2025 00:05:48 -0400 Subject: [PATCH 11/19] Fix trailing whitespace issues --- scripts/tempSensor/lib/bme680.py | 50 ++++++++++--------------------- scripts/tempSensor/lib/mqtt_as.py | 32 +++++++++++++------- 2 files changed, 37 insertions(+), 45 deletions(-) diff --git a/scripts/tempSensor/lib/bme680.py b/scripts/tempSensor/lib/bme680.py index aec33c73..1fd6d44d 100644 --- a/scripts/tempSensor/lib/bme680.py +++ b/scripts/tempSensor/lib/bme680.py @@ -1,27 +1,3 @@ -# The MIT License (MIT) -# -# Copyright (c) 2017 ladyada for Adafruit Industries -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -# We have a lot of attributes for this complex sensor. -# pylint: disable=too-many-instance-attributes """ `bme680` - BME680 - Temperature, Humidity, Pressure & Gas Sensor @@ -38,7 +14,6 @@ import time from micropython import const -from ubinascii import hexlify as hex try: import struct @@ -125,11 +100,13 @@ def _read24(arr): class Adafruit_BME680: """Driver from BME680 air quality sensor - :param int refresh_rate: Maximum number of readings per second. Faster property reads + :param int refresh_rate: Maximum number of readings per second. + Faster property reads will be from the previous reading.""" def __init__(self, *, refresh_rate=10): - """Check the BME680 was found, read the coefficients and enable the sensor for continuous + """Check the BME680 was found, read the coefficients and enable the sensor for + continuous reads.""" self._write(_BME680_REG_SOFTRESET, [0xB6]) time.sleep(0.005) @@ -239,7 +216,7 @@ def pressure(self): calc_pres = (calc_pres - (var2 / 4096)) * 3125 calc_pres = (calc_pres / var1) * 2 var1 = ( - self._pressure_calibration[8] * (((calc_pres / 8) * (calc_pres / 8)) / 8192) + self._pressure_calibration[8] * (((calc_pres / 8) * (calc_pres / 8))/8192) ) / 4096 var2 = ((calc_pres / 4) * self._pressure_calibration[7]) / 8192 var3 = (((calc_pres / 256) ** 3) * self._pressure_calibration[9]) / 131072 @@ -287,8 +264,10 @@ def humidity(self): @property def altitude(self): - """The altitude based on current ``pressure`` vs the sea level pressure - (``sea_level_pressure``) - which you must enter ahead of time)""" + """The altitude based on current + ``pressure`` vs the sea level pressure + (``sea_level_pressure``) - + which you must enter ahead of time)""" pressure = self.pressure # in Si units for hPascal return 44330 * (1.0 - math.pow(pressure / self.sea_level_pressure, 0.1903)) @@ -305,7 +284,8 @@ def gas(self): return int(calc_gas_res) def _perform_reading(self): - """Perform a single-shot reading from the sensor and fill internal data structure for + """Perform a single-shot reading from the sensor + and fill internal data structure for calculations""" expired = time.ticks_diff( self._last_reading, time.ticks_ms() @@ -353,7 +333,7 @@ def _read_calibration(self): coeff = self._read(_BME680_BME680_COEFF_ADDR1, 25) coeff += self._read(_BME680_BME680_COEFF_ADDR2, 16) - coeff = list(struct.unpack(" Date: Wed, 12 Mar 2025 00:06:13 -0400 Subject: [PATCH 12/19] Apply black formatting --- scripts/tempSensor/lib/bme680.py | 5 ++--- scripts/tempSensor/lib/mqtt_as.py | 11 ++++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/scripts/tempSensor/lib/bme680.py b/scripts/tempSensor/lib/bme680.py index 1fd6d44d..b316ec02 100644 --- a/scripts/tempSensor/lib/bme680.py +++ b/scripts/tempSensor/lib/bme680.py @@ -1,4 +1,3 @@ - """ `bme680` - BME680 - Temperature, Humidity, Pressure & Gas Sensor ================================================================ @@ -216,7 +215,7 @@ def pressure(self): calc_pres = (calc_pres - (var2 / 4096)) * 3125 calc_pres = (calc_pres / var1) * 2 var1 = ( - self._pressure_calibration[8] * (((calc_pres / 8) * (calc_pres / 8))/8192) + self._pressure_calibration[8] * (((calc_pres / 8) * (calc_pres / 8)) / 8192) ) / 4096 var2 = ((calc_pres / 4) * self._pressure_calibration[7]) / 8192 var3 = (((calc_pres / 256) ** 3) * self._pressure_calibration[9]) / 131072 @@ -333,7 +332,7 @@ def _read_calibration(self): coeff = self._read(_BME680_BME680_COEFF_ADDR1, 25) coeff += self._read(_BME680_BME680_COEFF_ADDR2, 16) - coeff =list(struct.unpack(" Date: Wed, 12 Mar 2025 00:20:43 -0400 Subject: [PATCH 13/19] Fix flake8 and isort errors in mqtt_as.py and netman.py --- scripts/tempSensor/lib/mqtt_as.py | 91 ++++++++++++++----------------- scripts/tempSensor/lib/netman.py | 13 ++++- 2 files changed, 50 insertions(+), 54 deletions(-) diff --git a/scripts/tempSensor/lib/mqtt_as.py b/scripts/tempSensor/lib/mqtt_as.py index 415a5ea6..ef5f5bd6 100644 --- a/scripts/tempSensor/lib/mqtt_as.py +++ b/scripts/tempSensor/lib/mqtt_as.py @@ -6,25 +6,20 @@ # Various improvements contributed by Kevin Köck. import gc +import time +from sys import platform +import network +import uasyncio as asyncio import usocket as socket import ustruct as struct - -gc.collect() -import uasyncio as asyncio +from machine import unique_id +from micropython import const from ubinascii import hexlify - -gc.collect() from uerrno import EINPROGRESS, ETIMEDOUT from utime import ticks_diff, ticks_ms gc.collect() -import network -from machine import unique_id -from micropython import const - -gc.collect() -from sys import platform VERSION = (0, 7, 1) @@ -33,11 +28,18 @@ _DEFAULT_MS = const(20) _SOCKET_POLL_DELAY = const(5) # 100ms added greatly to publish latency -# Legitimate errors while waiting on a socket. See uasyncio __init__.py open_connection(). +# Legitimate errors while waiting on a socket. +# See uasyncio __init__.py open_connection(). ESP32 = platform == "esp32" RP2 = platform == "rp2" + if ESP32: - BUSY_ERRORS = [EINPROGRESS, ETIMEDOUT, 118, 119] # Add in weird ESP32 errors + BUSY_ERRORS = [ + EINPROGRESS, + ETIMEDOUT, + 118, # Add in weird ESP32 errors + 119, + ] elif RP2: BUSY_ERRORS = [EINPROGRESS, ETIMEDOUT, -110] else: @@ -47,8 +49,9 @@ PYBOARD = platform == "pyboard" -# Default "do little" coro for optional user replacement -async def eliza(*_): # e.g. via set_wifi_handler(coro): see test program +# Default "do little" coroutine for optional user replacement +async def eliza(*_): + """Example: set_wifi_handler(coro) - see test program""" await asyncio.sleep_ms(_DEFAULT_MS) @@ -118,76 +121,69 @@ def pid_gen(): def qos_check(qos): - if not (qos == 0 or qos == 1): + if qos not in {0, 1}: raise ValueError("Only qos 0 and 1 are supported.") -# MQTT_base class. Handles MQTT protocol on -# the basis of -# a good connection. -# Exceptions from connectivity failures -# are handled by -# MQTTClient subclass. +# MQTT_base class - Handles MQTT protocol class MQTT_base: REPUB_COUNT = 0 # TEST DEBUG = False def __init__(self, config): self._events = config["queue_len"] > 0 - # MQTT config self._client_id = config["client_id"] self._user = config["user"] self._pswd = config["password"] self._keepalive = config["keepalive"] if self._keepalive >= 65536: - raise ValueError("invalid keepalive time") - self._response_time = ( - config["response_time"] * 1000 - ) # Repub if no PUBACK received (ms). + raise ValueError("Invalid keepalive time") + self._response_time = config["response_time"] * 1000 self._max_repubs = config["max_repubs"] - self._clean_init = config[ - "clean_init" - ] # clean_session state on first connection - self._clean = config["clean"] # clean_session state on reconnect + self._clean_init = config["clean_init"] + self._clean = config["clean"] + will = config["will"] if will is None: self._lw_topic = False else: self._set_last_will(*will) + # WiFi config - self._ssid = config["ssid"] # Required for ESP32 / Pyboard D. Optional ESP8266 + self._ssid = config["ssid"] self._wifi_pw = config["wifi_pw"] self._ssl = config["ssl"] self._ssl_params = config["ssl_params"] - # Callbacks and coros + + # Callbacks and coroutines if self._events: self.up = asyncio.Event() self.down = asyncio.Event() self.queue = MsgQueue(config["queue_len"]) - else: # Callbacks + else: self._cb = config["subs_cb"] self._wifi_handler = config["wifi_coro"] self._connect_handler = config["connect_coro"] - # Network - self.port = config["port"] - if self.port == 0: - self.port = 8883 if self._ssl else 1883 + + # Network settings + self.port = config["port"] or (8883 if self._ssl else 1883) self.server = config["server"] + if self.server is None: - raise ValueError("no server specified.") + raise ValueError("No server specified.") + self._sock = None self._sta_if = network.WLAN(network.STA_IF) self._sta_if.active(True) - if config["gateway"]: # Called from gateway (hence ESP32). + + if config["gateway"]: # Called from gateway (hence ESP32) import aioespnow # Set up ESPNOW while not (sta := self._sta_if).active(): time.sleep(0.1) sta.config(pm=sta.PM_NONE) # No power management sta.active(True) - self._espnow = ( - aioespnow.AIOESPNow() - ) # Returns AIOESPNow enhanced with async support + self._espnow = aioespnow.AIOESPNow() self._espnow.active(True) self.newpid = pid_gen() @@ -211,15 +207,9 @@ def dprint(self, msg, *args): def _timeout(self, t): return ticks_diff(ticks_ms(), t) > self._response_time - async def _as_read(self, n, sock=None): # OSError caught by superclass + async def _as_read(self, n, sock=None): if sock is None: sock = self._sock - # Declare a byte array of size n. - # That space is needed anyway, better - # to just 'allocate' it in one go - # instead of appending to an - # existing object, this prevents - # reallocation and fragmentation. data = bytearray(n) buffer = memoryview(data) size = 0 @@ -309,7 +299,6 @@ async def _connect(self, clean): sz += 2 + len(self._lw_topic) + 2 + len(self._lw_msg) msg[6] |= 0x4 | (self._lw_qos & 0x1) << 3 | (self._lw_qos & 0x2) << 3 msg[6] |= self._lw_retain << 5 - i = 1 while sz > 0x7F: premsg[i] = (sz & 0x7F) | 0x80 diff --git a/scripts/tempSensor/lib/netman.py b/scripts/tempSensor/lib/netman.py index 8f1250d1..90e39d26 100644 --- a/scripts/tempSensor/lib/netman.py +++ b/scripts/tempSensor/lib/netman.py @@ -67,6 +67,13 @@ def connectWiFi(ssid, password, country=None, wifi_energy_saver=False, retries=3 return status except RuntimeError as e: print(f"Attempt failed with error: {e}. Retrying...") - raise RuntimeError( - "All attempts to connect to the network failed. Ensure you are using a 2.4 GHz WiFi network with WPA-2 authentication. See the additional prerequisites section from https://doi.org/10.1016/j.xpro.2023.102329 or the https://github.com/sparks-baird/self-driving-lab-demo/issues/76 for additional troubleshooting help." - ) + raise RuntimeError( + "All attempts to connect to the network failed." + "Ensure you are using a 2.4 " + "GHz WiFi network with WPA-2 authentication." + "See the additional prerequisites " + "section from https://doi.org/10.1016/j.xpro.2023.102329 or the " + "https://github.com/sparks-baird/self-driving-lab-demo/issues/76" + "for additional " + "troubleshooting help." + ) From af6fd509fe1385cd33d5eba4083d8dbaeb531ecd Mon Sep 17 00:00:00 2001 From: Luthira A <79228258+Luthiraa@users.noreply.github.com> Date: Wed, 12 Mar 2025 00:27:06 -0400 Subject: [PATCH 14/19] Remove unused files and configurations from tempSensor directory --- scripts/tempSensor/.pre-commit-config.yaml | 18 - scripts/tempSensor/hivemq-com-chain.der | Bin 1289 -> 0 bytes scripts/tempSensor/lib/LICENSE | 21 - scripts/tempSensor/lib/bme680.py | 474 ------------ scripts/tempSensor/lib/mqtt_as.py | 824 --------------------- scripts/tempSensor/lib/netman.py | 79 -- scripts/tempSensor/pico_id.txt | 1 - scripts/tempSensor/umqtt/robust.py | 44 -- scripts/tempSensor/umqtt/simple.py | 217 ------ 9 files changed, 1678 deletions(-) delete mode 100644 scripts/tempSensor/.pre-commit-config.yaml delete mode 100644 scripts/tempSensor/hivemq-com-chain.der delete mode 100644 scripts/tempSensor/lib/LICENSE delete mode 100644 scripts/tempSensor/lib/bme680.py delete mode 100644 scripts/tempSensor/lib/mqtt_as.py delete mode 100644 scripts/tempSensor/lib/netman.py delete mode 100644 scripts/tempSensor/pico_id.txt delete mode 100644 scripts/tempSensor/umqtt/robust.py delete mode 100644 scripts/tempSensor/umqtt/simple.py diff --git a/scripts/tempSensor/.pre-commit-config.yaml b/scripts/tempSensor/.pre-commit-config.yaml deleted file mode 100644 index 4bb444e3..00000000 --- a/scripts/tempSensor/.pre-commit-config.yaml +++ /dev/null @@ -1,18 +0,0 @@ -repos: - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.4.0 - hooks: - - id: trailing-whitespace - - id: end-of-file-fixer - - repo: https://github.com/ambv/black - rev: 21.12b0 - hooks: - - id: black - - repo: https://github.com/PyCQA/isort - rev: 5.9.3 - hooks: - - id: isort - - repo: https://gitlab.com/pycqa/flake8 - rev: 3.9.2 - hooks: - - id: flake8 diff --git a/scripts/tempSensor/hivemq-com-chain.der b/scripts/tempSensor/hivemq-com-chain.der deleted file mode 100644 index ac22dcf9e8b888a98c10bd12f2108feb696b68c8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1289 zcmXqLVr4aGVtTuPnTe5!Nx*wW(B$v4^cNMgd9PsUExlmC%f_kI=F#?@mywa1mBGN@ zklTQhjX9KsO_(V(*ih3z9mL@hR`AR#NiE7tEl~(gO)f3UEU8ooN-a)JEK1H$a4*U) zEie={5C$n_7UuB`4surr%Fi!Rh%hve6X!KDF)%hXHUNVtAlDqoH8L_bwKTPi0y0r7 z6f+QkSjg*>TB2U8;F_0QR9R4B$Ya0-F`YTc(7>RHQ3=`MjI0dIO^o~uKyfanCPqev z^WjgQ9SIS7yT$$AN%e>_w*ZYES-Xz=taGn!mC)RKcWc!CU*FCx-nX7X(K{(B%7#(@ zXaw`O&i{Ra&aV${eIUX`tx0lmYk)j+Yhamh&ET#c)s`xyLPVB|J4zDrml1N z|MGPBk*(476}%FOd&HlI9NXQqqaiM3^$4$X<3;Tn^lSba?QosDo%8{^IJXBIN|d$-C@nmFsZm&_U06-A*R zHauI*{^sv;>9l$F9^Hj4g%KyMe!Y+T6z!sIQ}VojllwvTth2MW^G^FN)TkC1!u2Nc z+xM@JK6Ra#%G=At%*epFxbcTU<5vSdV7SThGcx{XVF4zAHUn7@UzG(E&>Y%qjI6Be z%uH|=qk#}eS{S69!+;G)F)=bgwD7Trv54&6bJ%_9lRYbs9bQtzBhb6Z^x=sY2J#>+ z$}AEFVhtjdTOyxTZ+rTLvABVw?!06V?FG|-3Cni}17GS&^aImqZH}WttF@h7LtUQZ?ft-QN0;vTO$hwn} zb)%+g<|d#oCT)D~X#To#ZsW7Fk5nI8DZHHEGIeod%=yEyfA=mZxVmwgh7$LsPa5ZW zo?m(9+@qo-d(dgpMb7o}y}z6&@K+Gq7)t&&=Y8J%?(W4` z&OToouy*hM$8YSC%nD{2E`QN`_f^s(p><#9T(Q;RS#@Df<-S+8F)n%243nd3uhsdh zEbwNnkITPw$X#6QV#HL#17UX*?xM+#GeNw@ieVHB%ztj0|M=yMFomo-$`pIuIoh+N3@0W84 zaeP$0wrh%Hh(yQIqkRvhW=^imek1eK&|$+FBZm9)TTuMdMC6sT9zcd^!(zYXZ`b3QaIzI*a>1kPyVS=_UK!7wXx`<#;@3Ozu)>5_fOHg k 100: - calc_hum = 100 - if calc_hum < 0: - calc_hum = 0 - return calc_hum - - @property - def altitude(self): - """The altitude based on current - ``pressure`` vs the sea level pressure - (``sea_level_pressure``) - - which you must enter ahead of time)""" - pressure = self.pressure # in Si units for hPascal - return 44330 * (1.0 - math.pow(pressure / self.sea_level_pressure, 0.1903)) - - @property - def gas(self): - """The gas resistance in ohms""" - self._perform_reading() - var1 = ( - (1340 + (5 * self._sw_err)) * (_LOOKUP_TABLE_1[self._gas_range]) - ) / 65536 - var2 = ((self._adc_gas * 32768) - 16777216) + var1 - var3 = (_LOOKUP_TABLE_2[self._gas_range] * var1) / 512 - calc_gas_res = (var3 + (var2 / 2)) / var2 - return int(calc_gas_res) - - def _perform_reading(self): - """Perform a single-shot reading from the sensor - and fill internal data structure for - calculations""" - expired = time.ticks_diff( - self._last_reading, time.ticks_ms() - ) * time.ticks_diff(0, 1) - if 0 <= expired < self._min_refresh_time: - time.sleep_ms(self._min_refresh_time - expired) - - # set filter - self._write(_BME680_REG_CONFIG, [self._filter << 2]) - # turn on temp oversample & pressure oversample - self._write( - _BME680_REG_CTRL_MEAS, - [(self._temp_oversample << 5) | (self._pressure_oversample << 2)], - ) - # turn on humidity oversample - self._write(_BME680_REG_CTRL_HUM, [self._humidity_oversample]) - # gas measurements enabled - self._write(_BME680_REG_CTRL_GAS, [_BME680_RUNGAS]) - - ctrl = self._read_byte(_BME680_REG_CTRL_MEAS) - ctrl = (ctrl & 0xFC) | 0x01 # enable single shot! - self._write(_BME680_REG_CTRL_MEAS, [ctrl]) - new_data = False - while not new_data: - data = self._read(_BME680_REG_MEAS_STATUS, 15) - new_data = data[0] & 0x80 != 0 - time.sleep(0.005) - self._last_reading = time.ticks_ms() - - self._adc_pres = _read24(data[2:5]) / 16 - self._adc_temp = _read24(data[5:8]) / 16 - self._adc_hum = struct.unpack(">H", bytes(data[8:10]))[0] - self._adc_gas = int(struct.unpack(">H", bytes(data[13:15]))[0] / 64) - self._gas_range = data[14] & 0x0F - - var1 = (self._adc_temp / 8) - (self._temp_calibration[0] * 2) - var2 = (var1 * self._temp_calibration[1]) / 2048 - var3 = ((var1 / 2) * (var1 / 2)) / 4096 - var3 = (var3 * self._temp_calibration[2] * 16) / 16384 - - self._t_fine = int(var2 + var3) - - def _read_calibration(self): - """Read & save the calibration coefficients""" - coeff = self._read(_BME680_BME680_COEFF_ADDR1, 25) - coeff += self._read(_BME680_BME680_COEFF_ADDR2, 16) - - coeff = list(struct.unpack(" 0 - self._client_id = config["client_id"] - self._user = config["user"] - self._pswd = config["password"] - self._keepalive = config["keepalive"] - if self._keepalive >= 65536: - raise ValueError("Invalid keepalive time") - self._response_time = config["response_time"] * 1000 - self._max_repubs = config["max_repubs"] - self._clean_init = config["clean_init"] - self._clean = config["clean"] - - will = config["will"] - if will is None: - self._lw_topic = False - else: - self._set_last_will(*will) - - # WiFi config - self._ssid = config["ssid"] - self._wifi_pw = config["wifi_pw"] - self._ssl = config["ssl"] - self._ssl_params = config["ssl_params"] - - # Callbacks and coroutines - if self._events: - self.up = asyncio.Event() - self.down = asyncio.Event() - self.queue = MsgQueue(config["queue_len"]) - else: - self._cb = config["subs_cb"] - self._wifi_handler = config["wifi_coro"] - self._connect_handler = config["connect_coro"] - - # Network settings - self.port = config["port"] or (8883 if self._ssl else 1883) - self.server = config["server"] - - if self.server is None: - raise ValueError("No server specified.") - - self._sock = None - self._sta_if = network.WLAN(network.STA_IF) - self._sta_if.active(True) - - if config["gateway"]: # Called from gateway (hence ESP32) - import aioespnow # Set up ESPNOW - - while not (sta := self._sta_if).active(): - time.sleep(0.1) - sta.config(pm=sta.PM_NONE) # No power management - sta.active(True) - self._espnow = aioespnow.AIOESPNow() - self._espnow.active(True) - - self.newpid = pid_gen() - self.rcv_pids = set() # PUBACK and SUBACK pids awaiting ACK response - self.last_rx = ticks_ms() # Time of last communication from broker - self.lock = asyncio.Lock() - - def _set_last_will(self, topic, msg, retain=False, qos=0): - qos_check(qos) - if not topic: - raise ValueError("Empty topic.") - self._lw_topic = topic - self._lw_msg = msg - self._lw_qos = qos - self._lw_retain = retain - - def dprint(self, msg, *args): - if self.DEBUG: - print(msg % args) - - def _timeout(self, t): - return ticks_diff(ticks_ms(), t) > self._response_time - - async def _as_read(self, n, sock=None): - if sock is None: - sock = self._sock - data = bytearray(n) - buffer = memoryview(data) - size = 0 - t = ticks_ms() - while size < n: - if self._timeout(t) or not self.isconnected(): - raise OSError(-1, "Timeout on socket read") - try: - msg_size = sock.readinto(buffer[size:], n - size) - except OSError as e: # ESP32 issues weird 119 errors here - msg_size = None - if e.args[0] not in BUSY_ERRORS: - raise - if msg_size == 0: # Connection closed by host - raise OSError(-1, "Connection closed by host") - if msg_size is not None: # data received - size += msg_size - t = ticks_ms() - self.last_rx = ticks_ms() - await asyncio.sleep_ms(_SOCKET_POLL_DELAY) - return data - - async def _as_write(self, bytes_wr, length=0, sock=None): - if sock is None: - sock = self._sock - - # Wrap bytes in memoryview to avoid copying during slicing - bytes_wr = memoryview(bytes_wr) - if length: - bytes_wr = bytes_wr[:length] - t = ticks_ms() - while bytes_wr: - if self._timeout(t) or not self.isconnected(): - raise OSError(-1, "Timeout on socket write") - try: - n = sock.write(bytes_wr) - except OSError as e: # ESP32 issues weird 119 errors here - n = 0 - if e.args[0] not in BUSY_ERRORS: - raise - if n: - t = ticks_ms() - bytes_wr = bytes_wr[n:] - await asyncio.sleep_ms(_SOCKET_POLL_DELAY) - - async def _send_str(self, s): - await self._as_write(struct.pack("!H", len(s))) - await self._as_write(s) - - async def _recv_len(self): - n = 0 - sh = 0 - while 1: - res = await self._as_read(1) - b = res[0] - n |= (b & 0x7F) << sh - if not b & 0x80: - return n - sh += 7 - - async def _connect(self, clean): - self._sock = socket.socket() - self._sock.setblocking(False) - try: - self._sock.connect(self._addr) - except OSError as e: - if e.args[0] not in BUSY_ERRORS: - raise - await asyncio.sleep_ms(_DEFAULT_MS) - self.dprint("Connecting to broker.") - if self._ssl: - import ssl - - self._sock = ssl.wrap_socket(self._sock, **self._ssl_params) - premsg = bytearray(b"\x10\0\0\0\0\0") - msg = bytearray(b"\x04MQTT\x04\0\0\0") # Protocol 3.1.1 - - sz = 10 + 2 + len(self._client_id) - msg[6] = clean << 1 - if self._user: - sz += 2 + len(self._user) + 2 + len(self._pswd) - msg[6] |= 0xC0 - if self._keepalive: - msg[7] |= self._keepalive >> 8 - msg[8] |= self._keepalive & 0x00FF - if self._lw_topic: - sz += 2 + len(self._lw_topic) + 2 + len(self._lw_msg) - msg[6] |= 0x4 | (self._lw_qos & 0x1) << 3 | (self._lw_qos & 0x2) << 3 - msg[6] |= self._lw_retain << 5 - i = 1 - while sz > 0x7F: - premsg[i] = (sz & 0x7F) | 0x80 - sz >>= 7 - i += 1 - premsg[i] = sz - await self._as_write(premsg, i + 2) - await self._as_write(msg) - await self._send_str(self._client_id) - if self._lw_topic: - await self._send_str(self._lw_topic) - await self._send_str(self._lw_msg) - if self._user: - await self._send_str(self._user) - await self._send_str(self._pswd) - # Await CONNACK - # read causes ECONNABORTED if broker is out; triggers a reconnect. - resp = await self._as_read(4) - self.dprint("Connected to broker.") # Got CONNACK - if ( - resp[3] != 0 or resp[0] != 0x20 or resp[1] != 0x02 - ): # Bad CONNACK e.g. authentication fail. - raise OSError( - -1, - f"Connect fail: 0x{(resp[0] << 8) + resp[1]:04x} {resp[3]} (README 7)", - ) - - async def _ping(self): - async with self.lock: - await self._as_write(b"\xc0\0") - - # Check internet connectivity by sending DNS lookup to Google's 8.8.8.8 - async def wan_ok( - self, - packet=( - b"$\x1a\x01\x00\x00\x01\x00\x00\x00" - b"\x00\x00\x00\x03www\x06google\x03com\x00" - b"\x00\x01\x00\x01" - ), - ): - if not self.isconnected(): # WiFi is down - return False - length = 32 # DNS query and response packet size - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - s.setblocking(False) - s.connect(("8.8.8.8", 53)) - await asyncio.sleep(1) - try: - await self._as_write(packet, sock=s) - await asyncio.sleep(2) - res = await self._as_read(length, s) - if len(res) == length: - return True # DNS response size OK - except OSError: # Timeout on read: no connectivity. - return False - finally: - s.close() - return False - - async def broker_up(self): # Test broker connectivity - if not self.isconnected(): - return False - tlast = self.last_rx - if ticks_diff(ticks_ms(), tlast) < 1000: - return True - try: - await self._ping() - except OSError: - return False - t = ticks_ms() - while not self._timeout(t): - await asyncio.sleep_ms(100) - if ticks_diff(self.last_rx, tlast) > 0: # Response received - return True - return False - - async def disconnect(self): - if self._sock is not None: - await self._kill_tasks(False) # Keep socket open - try: - async with self.lock: - self._sock.write(b"\xe0\0") # Close broker connection - await asyncio.sleep_ms(100) - except OSError: - pass - self._close() - self._has_connected = False - - def _close(self): - if self._sock is not None: - self._sock.close() - - def close( - self, - ): # API. See https://github.com/peterhinch/micropython-mqtt/issues/60 - self._close() - try: - self._sta_if.disconnect() # Disconnect Wi-Fi to avoid errors - except OSError: - self.dprint("Wi-Fi not started, unable to disconnect interface") - self._sta_if.active(False) - - async def _await_pid(self, pid): - t = ticks_ms() - while pid in self.rcv_pids: # local copy - if self._timeout(t) or not self.isconnected(): - break # Must repub or bail out - await asyncio.sleep_ms(100) - else: - return True # PID received. All done. - return False - - # qos == 1: coro blocks until wait_msg gets correct PID. - # If WiFi fails completely subclass re-publishes with new PID. - async def publish(self, topic, msg, retain, qos): - pid = next(self.newpid) - if qos: - self.rcv_pids.add(pid) - async with self.lock: - await self._publish(topic, msg, retain, qos, 0, pid) - if qos == 0: - return - - count = 0 - while 1: # Await PUBACK, republish on timeout - if await self._await_pid(pid): - return - # No match - if count >= self._max_repubs or not self.isconnected(): - raise OSError(-1) # Subclass to re-publish with new PID - async with self.lock: - await self._publish(topic, msg, retain, qos, dup=1, pid=pid) # Add pid - count += 1 - self.REPUB_COUNT += 1 - - async def _publish(self, topic, msg, retain, qos, dup, pid): - pkt = bytearray(b"\x30\0\0\0") - pkt[0] |= qos << 1 | retain | dup << 3 - sz = 2 + len(topic) + len(msg) - if qos > 0: - sz += 2 - if sz >= 2097152: - raise MQTTException("Strings too long.") - i = 1 - while sz > 0x7F: - pkt[i] = (sz & 0x7F) | 0x80 - sz >>= 7 - i += 1 - pkt[i] = sz - await self._as_write(pkt, i + 1) - await self._send_str(topic) - if qos > 0: - struct.pack_into("!H", pkt, 0, pid) - await self._as_write(pkt, 2) - await self._as_write(msg) - - # Can raise OSError if WiFi fails. Subclass traps. - async def subscribe(self, topic, qos): - pkt = bytearray(b"\x82\0\0\0") - pid = next(self.newpid) - self.rcv_pids.add(pid) - struct.pack_into("!BH", pkt, 1, 2 + 2 + len(topic) + 1, pid) - async with self.lock: - await self._as_write(pkt) - await self._send_str(topic) - await self._as_write(qos.to_bytes(1, "little")) - - if not await self._await_pid(pid): - raise OSError(-1) - - # Can raise OSError if WiFi fails. Subclass traps. - async def unsubscribe(self, topic): - pkt = bytearray(b"\xa2\0\0\0") - pid = next(self.newpid) - self.rcv_pids.add(pid) - struct.pack_into("!BH", pkt, 1, 2 + 2 + len(topic), pid) - async with self.lock: - await self._as_write(pkt) - await self._send_str(topic) - - if not await self._await_pid(pid): - raise OSError(-1) - - # Wait for a single incoming MQTT message and process it. - # Subscribed messages are delivered to a callback previously - # set by .setup() method. Other (internal) MQTT - # messages processed internally. - # Immediate return if no data available. Called from ._handle_msg(). - async def wait_msg(self): - try: - res = self._sock.read(1) # Throws OSError on WiFi fail - except OSError as e: - if e.args[0] in BUSY_ERRORS: # Needed by RP2 - await asyncio.sleep_ms(0) - return - raise - if res is None: - return - if res == b"": - raise OSError(-1, "Empty response") - - if res == b"\xd0": # PINGRESP - await self._as_read(1) # Update .last_rx time - return - op = res[0] - - if op == 0x40: # PUBACK: save pid - sz = await self._as_read(1) - if sz != b"\x02": - raise OSError(-1, "Invalid PUBACK packet") - rcv_pid = await self._as_read(2) - pid = rcv_pid[0] << 8 | rcv_pid[1] - if pid in self.rcv_pids: - self.rcv_pids.discard(pid) - else: - raise OSError(-1, "Invalid pid in PUBACK packet") - - if op == 0x90: # SUBACK - resp = await self._as_read(4) - if resp[3] == 0x80: - raise OSError(-1, "Invalid SUBACK packet") - pid = resp[2] | (resp[1] << 8) - if pid in self.rcv_pids: - self.rcv_pids.discard(pid) - else: - raise OSError(-1, "Invalid pid in SUBACK packet") - - if op == 0xB0: # UNSUBACK - resp = await self._as_read(3) - pid = resp[2] | (resp[1] << 8) - if pid in self.rcv_pids: - self.rcv_pids.discard(pid) - else: - raise OSError(-1) - - if op & 0xF0 != 0x30: - return - sz = await self._recv_len() - topic_len = await self._as_read(2) - topic_len = (topic_len[0] << 8) | topic_len[1] - topic = await self._as_read(topic_len) - sz -= topic_len + 2 - if op & 6: - pid = await self._as_read(2) - pid = pid[0] << 8 | pid[1] - sz -= 2 - msg = await self._as_read(sz) - retained = op & 0x01 - if self._events: - self.queue.put(topic, msg, bool(retained)) - else: - self._cb(topic, msg, bool(retained)) - if op & 6 == 2: # qos 1 - pkt = bytearray(b"\x40\x02\0\0") # Send PUBACK - struct.pack_into("!H", pkt, 2, pid) - await self._as_write(pkt) - elif op & 6 == 4: # qos 2 not supported - raise OSError(-1, "QoS 2 not supported") - - -# MQTTClient class. Handles issues relating to connectivity. - - -class MQTTClient(MQTT_base): - def __init__(self, config): - super().__init__(config) - self._isconnected = False # Current connection state - keepalive = 1000 * self._keepalive # ms - self._ping_interval = keepalive // 4 if keepalive else 20000 - p_i = ( - config["ping_interval"] * 1000 - ) # Can specify shorter e.g. for subscribe-only - if p_i and p_i < self._ping_interval: - self._ping_interval = p_i - self._in_connect = False - self._has_connected = False # Define 'Clean Session' value to use. - self._tasks = [] - if ESP8266: - import esp - - esp.sleep_type( - 0 - ) # Improve connection integrity at cost of power consumption. - - async def wifi_connect(self, quick=False): - s = self._sta_if - if ESP8266: - if s.isconnected(): # 1st attempt, already connected. - return - s.active(True) - s.connect() # ESP8266 remembers connection. - for _ in range(60): - if ( - s.status() != network.STAT_CONNECTING - ): # Break out on fail or success. Check once per sec. - break - await asyncio.sleep(1) - if ( - s.status() == network.STAT_CONNECTING - ): # might hang forever awaiting dhcp lease renewal or something else - s.disconnect() - await asyncio.sleep(1) - if ( - not s.isconnected() - and self._ssid is not None - and self._wifi_pw is not None - ): - s.connect(self._ssid, self._wifi_pw) - while ( - s.status() == network.STAT_CONNECTING - ): # Break out on fail or success. Check once per sec. - await asyncio.sleep(1) - else: - s.active(True) - if RP2: # Disable auto-sleep. - # para 3.6.3 - s.config(pm=0xA11140) - s.connect(self._ssid, self._wifi_pw) - for _ in range(60): # Break out on fail or success. Check once per sec. - await asyncio.sleep(1) - # Loop while connecting or no IP - if s.isconnected(): - break - if ESP32: - if s.status() != network.STAT_CONNECTING: # 1001 - break - elif PYBOARD: # No symbolic constants in network - if not 1 <= s.status() <= 2: - break - elif RP2: # 1 is STAT_CONNECTING. 2 reported by user (No IP?) - if not 1 <= s.status() <= 2: - break - else: # Timeout: still in connecting state - s.disconnect() - await asyncio.sleep(1) - - if not s.isconnected(): # Timed out - raise OSError("Wi-Fi connect timed out") - if not quick: # Skip on first connection only if power saving - # Ensure connection stays up for a few secs. - self.dprint("Checking WiFi integrity.") - for _ in range(5): - if not s.isconnected(): - raise OSError("Connection Unstable") # in 1st 5 secs - await asyncio.sleep(1) - self.dprint("Got reliable connection") - - async def connect( - self, *, quick=False - ): # Quick initial connect option for battery apps - if not self._has_connected: - await self.wifi_connect(quick) # On 1st call, caller handles error - # Note this blocks if DNS lookup occurs. Do it once to prevent - # blocking during later internet outage: - self._addr = socket.getaddrinfo(self.server, self.port)[0][-1] - self._in_connect = True # Disable low level ._isconnected check - try: - if not self._has_connected and self._clean_init and not self._clean: - # Power up. Clear previous session data but subsequently save it. - # Issue #40 - await self._connect(True) # Connect with clean session - try: - async with self.lock: - self._sock.write( - b"\xe0\0" - ) # Force disconnect but keep socket open - except OSError: - pass - self.dprint("Waiting for disconnect") - await asyncio.sleep(2) # Wait for broker to disconnect - self.dprint("About to reconnect with unclean session.") - await self._connect(self._clean) - except Exception: - self._close() - self._in_connect = False # Caller may run .isconnected() - raise - self.rcv_pids.clear() - # If we get here without error broker/LAN must be up. - self._isconnected = True - self._in_connect = False # Low level code can now check connectivity. - if not self._events: - asyncio.create_task(self._wifi_handler(True)) # User handler. - if not self._has_connected: - self._has_connected = True # Use normal clean flag on reconnect. - asyncio.create_task(self._keep_connected()) - # Runs forever unless user issues .disconnect() - - asyncio.create_task(self._handle_msg()) # Task quits on connection fail. - self._tasks.append(asyncio.create_task(self._keep_alive())) - if self.DEBUG: - self._tasks.append(asyncio.create_task(self._memory())) - if self._events: - self.up.set() # Connectivity is up - else: - asyncio.create_task(self._connect_handler(self)) # User handler. - - # Launched by .connect(). Runs until connectivity fails. Checks for and - # handles incoming messages. - async def _handle_msg(self): - try: - while self.isconnected(): - async with self.lock: - await self.wait_msg() # Immediate return if no message - await asyncio.sleep_ms(_DEFAULT_MS) # Let other tasks get lock - - except OSError: - pass - self._reconnect() # Broker or WiFi fail. - - # Keep broker alive MQTT spec 3.1.2.10 Keep Alive. - # Runs until ping failure or no response in keepalive period. - async def _keep_alive(self): - while self.isconnected(): - pings_due = ticks_diff(ticks_ms(), self.last_rx) // self._ping_interval - if pings_due >= 4: - self.dprint("Reconnect: broker fail.") - break - await asyncio.sleep_ms(self._ping_interval) - try: - await self._ping() - except OSError: - break - self._reconnect() # Broker or WiFi fail. - - async def _kill_tasks(self, kill_skt): # Cancel running tasks - for task in self._tasks: - task.cancel() - self._tasks.clear() - await asyncio.sleep_ms(0) # Ensure cancellation complete - if kill_skt: # Close socket - self._close() - - # DEBUG: show RAM messages. - async def _memory(self): - while True: - await asyncio.sleep(20) - gc.collect() - self.dprint("RAM free %d alloc %d", gc.mem_free(), gc.mem_alloc()) - - def isconnected(self): - if self._in_connect: # Disable low-level check during .connect() - return True - if self._isconnected and not self._sta_if.isconnected(): # It's going down. - self._reconnect() - return self._isconnected - - def _reconnect(self): # Schedule a reconnection if not underway. - if self._isconnected: - self._isconnected = False - asyncio.create_task(self._kill_tasks(True)) # Shut down tasks and socket - if self._events: # Signal an outage - self.down.set() - else: - asyncio.create_task(self._wifi_handler(False)) # User handler. - - # Await broker connection. - async def _connection(self): - while not self._isconnected: - await asyncio.sleep(1) - - # Scheduled on 1st successful connection. Runs forever maintaining wifi and - # broker connection. Must handle conditions at edge of WiFi range. - async def _keep_connected(self): - while self._has_connected: - if self.isconnected(): # Pause for 1 second - await asyncio.sleep(1) - gc.collect() - else: # Link is down, socket is closed, tasks are killed - try: - self._sta_if.disconnect() - except OSError: - self.dprint("Wi-Fi not started, unable to disconnect interface") - await asyncio.sleep(1) - try: - await self.wifi_connect() - except OSError: - continue - if ( - not self._has_connected - ): # User has issued the terminal .disconnect() - self.dprint("Disconnected, exiting _keep_connected") - break - try: - await self.connect() - # Now has set ._isconnected and scheduled _connect_handler(). - self.dprint("Reconnect OK!") - except OSError as e: - self.dprint("Error in reconnect. %s", e) - # Can get ECONNABORTED or -1. - # The latter signifies no or bad CONNACK received. - self._close() # Disconnect and try again. - self._in_connect = False - self._isconnected = False - self.dprint("Disconnected, exited _keep_connected") - - async def subscribe(self, topic, qos=0): - qos_check(qos) - while 1: - await self._connection() - try: - return await super().subscribe(topic, qos) - except OSError: - pass - self._reconnect() # Broker or WiFi fail. - - async def unsubscribe(self, topic): - while 1: - await self._connection() - try: - return await super().unsubscribe(topic) - except OSError: - pass - self._reconnect() # Broker or WiFi fail. - - async def publish(self, topic, msg, retain=False, qos=0): - qos_check(qos) - while 1: - await self._connection() - try: - return await super().publish(topic, msg, retain, qos) - except OSError: - pass - self._reconnect() # Broker or WiFi fail. diff --git a/scripts/tempSensor/lib/netman.py b/scripts/tempSensor/lib/netman.py deleted file mode 100644 index 90e39d26..00000000 --- a/scripts/tempSensor/lib/netman.py +++ /dev/null @@ -1,79 +0,0 @@ -# .';:cc;. -# .,',;lol::c. -# ;';lddddlclo -# lcloxxoddodxdool:,. -# cxdddxdodxdkOkkkkkkkd:. -# .ldxkkOOOOkkOO000Okkxkkkkx:. -# .lddxkkOkOOO0OOO0000Okxxxxkkkk: -# 'ooddkkkxxkO0000KK00Okxdoodxkkkko -# .ooodxkkxxxOO000kkkO0KOxolooxkkxxkl -# lolodxkkxxkOx,. .lkdolodkkxxxO. -# doloodxkkkOk .... .,cxO; -# ddoodddxkkkk: ,oxxxkOdc'..o' -# :kdddxxxxd, ,lolccldxxxkkOOOkkkko, -# lOkxkkk; :xkkkkkkkkOOO000OOkkOOk. -# ;00Ok' 'O000OO0000000000OOOO0Od. -# .l0l.;OOO000000OOOOOO000000x, -# .'OKKKK00000000000000kc. -# .:ox0KKKKKKK0kdc,. -# ... -# -# Author: peppe8o -# Date: Jul 24th, 2022 -# Version: 1.0 -# https://peppe8o.com - -# modified by @sgbaird from source: -# https://peppe8o.com/getting-started-with-wifi-on-raspberry-pi-pico-w-and-micropython/ - -import time - -import network -import rp2 -from ubinascii import hexlify - - -def connectWiFi(ssid, password, country=None, wifi_energy_saver=False, retries=3): - for _ in range(retries): - try: - if country is not None: - # https://www.google.com/search?q=wifi+country+codes - rp2.country(country) - wlan = network.WLAN(network.STA_IF) - if not wifi_energy_saver: - wlan.config(pm=0xA11140) # avoid the energy-saving WiFi mode - wlan.active(True) - - mac = hexlify(network.WLAN().config("mac"), ":").decode() - print(f"MAC address: {mac}") - - wlan.connect(ssid, password) - # Wait for connect or fail - max_wait = 10 - while max_wait > 0: - if wlan.status() < 0 or wlan.status() >= 3: - break - max_wait -= 1 - print("waiting for connection...") - time.sleep(1) - - # Handle connection error - if wlan.status() != 3: - raise RuntimeError("network connection failed") - else: - print("connected") - status = wlan.ifconfig() - print("ip = " + status[0]) - return status - except RuntimeError as e: - print(f"Attempt failed with error: {e}. Retrying...") - raise RuntimeError( - "All attempts to connect to the network failed." - "Ensure you are using a 2.4 " - "GHz WiFi network with WPA-2 authentication." - "See the additional prerequisites " - "section from https://doi.org/10.1016/j.xpro.2023.102329 or the " - "https://github.com/sparks-baird/self-driving-lab-demo/issues/76" - "for additional " - "troubleshooting help." - ) diff --git a/scripts/tempSensor/pico_id.txt b/scripts/tempSensor/pico_id.txt deleted file mode 100644 index ad449ae1..00000000 --- a/scripts/tempSensor/pico_id.txt +++ /dev/null @@ -1 +0,0 @@ -e66130100f594628 diff --git a/scripts/tempSensor/umqtt/robust.py b/scripts/tempSensor/umqtt/robust.py deleted file mode 100644 index 2a2b5629..00000000 --- a/scripts/tempSensor/umqtt/robust.py +++ /dev/null @@ -1,44 +0,0 @@ -import utime - -from . import simple - - -class MQTTClient(simple.MQTTClient): - DELAY = 2 - DEBUG = False - - def delay(self, i): - utime.sleep(self.DELAY) - - def log(self, in_reconnect, e): - if self.DEBUG: - if in_reconnect: - print("mqtt reconnect: %r" % e) - else: - print("mqtt: %r" % e) - - def reconnect(self): - i = 0 - while 1: - try: - return super().connect(False) - except OSError as e: - self.log(True, e) - i += 1 - self.delay(i) - - def publish(self, topic, msg, retain=False, qos=0): - while 1: - try: - return super().publish(topic, msg, retain, qos) - except OSError as e: - self.log(False, e) - self.reconnect() - - def wait_msg(self): - while 1: - try: - return super().wait_msg() - except OSError as e: - self.log(False, e) - self.reconnect() diff --git a/scripts/tempSensor/umqtt/simple.py b/scripts/tempSensor/umqtt/simple.py deleted file mode 100644 index 5d09230c..00000000 --- a/scripts/tempSensor/umqtt/simple.py +++ /dev/null @@ -1,217 +0,0 @@ -import usocket as socket -import ustruct as struct - - -class MQTTException(Exception): - pass - - -class MQTTClient: - def __init__( - self, - client_id, - server, - port=0, - user=None, - password=None, - keepalive=0, - ssl=False, - ssl_params={}, - ): - if port == 0: - port = 8883 if ssl else 1883 - self.client_id = client_id - self.sock = None - self.server = server - self.port = port - self.ssl = ssl - self.ssl_params = ssl_params - self.pid = 0 - self.cb = None - self.user = user - self.pswd = password - self.keepalive = keepalive - self.lw_topic = None - self.lw_msg = None - self.lw_qos = 0 - self.lw_retain = False - - def _send_str(self, s): - self.sock.write(struct.pack("!H", len(s))) - self.sock.write(s) - - def _recv_len(self): - n = 0 - sh = 0 - while 1: - b = self.sock.read(1)[0] - n |= (b & 0x7F) << sh - if not b & 0x80: - return n - sh += 7 - - def set_callback(self, f): - self.cb = f - - def set_last_will(self, topic, msg, retain=False, qos=0): - assert 0 <= qos <= 2 - assert topic - self.lw_topic = topic - self.lw_msg = msg - self.lw_qos = qos - self.lw_retain = retain - - def connect(self, clean_session=True): - self.sock = socket.socket() - addr = socket.getaddrinfo(self.server, self.port)[0][-1] - self.sock.connect(addr) - if self.ssl: - # replaced ussl with ssl due to deprecation in MicroPython 1.23.0 - # (not PR'd on source repo, but I'm using mqtt_as in my workflows - # instead, anyway) - import ssl - - self.sock = ssl.wrap_socket(self.sock, **self.ssl_params) - premsg = bytearray(b"\x10\0\0\0\0\0") - msg = bytearray(b"\x04MQTT\x04\x02\0\0") - - sz = 10 + 2 + len(self.client_id) - msg[6] = clean_session << 1 - if self.user is not None: - sz += 2 + len(self.user) + 2 + len(self.pswd) - msg[6] |= 0xC0 - if self.keepalive: - assert self.keepalive < 65536 - msg[7] |= self.keepalive >> 8 - msg[8] |= self.keepalive & 0x00FF - if self.lw_topic: - sz += 2 + len(self.lw_topic) + 2 + len(self.lw_msg) - msg[6] |= 0x4 | (self.lw_qos & 0x1) << 3 | (self.lw_qos & 0x2) << 3 - msg[6] |= self.lw_retain << 5 - - i = 1 - while sz > 0x7F: - premsg[i] = (sz & 0x7F) | 0x80 - sz >>= 7 - i += 1 - premsg[i] = sz - - self.sock.write(premsg, i + 2) - self.sock.write(msg) - # print(hex(len(msg)), hexlify(msg, ":")) - self._send_str(self.client_id) - if self.lw_topic: - self._send_str(self.lw_topic) - self._send_str(self.lw_msg) - if self.user is not None: - self._send_str(self.user) - self._send_str(self.pswd) - resp = self.sock.read(4) - assert resp[0] == 0x20 and resp[1] == 0x02 - if resp[3] != 0: - raise MQTTException(resp[3]) - return resp[2] & 1 - - def disconnect(self): - self.sock.write(b"\xe0\0") - self.sock.close() - - def ping(self): - self.sock.write(b"\xc0\0") - - def publish(self, topic, msg, retain=False, qos=0): - pkt = bytearray(b"\x30\0\0\0") - pkt[0] |= qos << 1 | retain - sz = 2 + len(topic) + len(msg) - if qos > 0: - sz += 2 - assert sz < 2097152 - i = 1 - while sz > 0x7F: - pkt[i] = (sz & 0x7F) | 0x80 - sz >>= 7 - i += 1 - pkt[i] = sz - # print(hex(len(pkt)), hexlify(pkt, ":")) - self.sock.write(pkt, i + 1) - self._send_str(topic) - if qos > 0: - self.pid += 1 - pid = self.pid - struct.pack_into("!H", pkt, 0, pid) - self.sock.write(pkt, 2) - self.sock.write(msg) - if qos == 1: - while 1: - op = self.wait_msg() - if op == 0x40: - sz = self.sock.read(1) - assert sz == b"\x02" - rcv_pid = self.sock.read(2) - rcv_pid = rcv_pid[0] << 8 | rcv_pid[1] - if pid == rcv_pid: - return - elif qos == 2: - assert 0 - - def subscribe(self, topic, qos=0): - assert self.cb is not None, "Subscribe callback is not set" - pkt = bytearray(b"\x82\0\0\0") - self.pid += 1 - struct.pack_into("!BH", pkt, 1, 2 + 2 + len(topic) + 1, self.pid) - # print(hex(len(pkt)), hexlify(pkt, ":")) - self.sock.write(pkt) - self._send_str(topic) - self.sock.write(qos.to_bytes(1, "little")) - while 1: - op = self.wait_msg() - if op == 0x90: - resp = self.sock.read(4) - # print(resp) - assert resp[1] == pkt[2] and resp[2] == pkt[3] - if resp[3] == 0x80: - raise MQTTException(resp[3]) - return - - # Wait for a single incoming MQTT message and process it. - # Subscribed messages are delivered to a callback previously - # set by .set_callback() method. Other (internal) MQTT - # messages processed internally. - def wait_msg(self): - res = self.sock.read(1) - self.sock.setblocking(True) - if res is None: - return None - if res == b"": - raise OSError(-1) - if res == b"\xd0": # PINGRESP - sz = self.sock.read(1)[0] - assert sz == 0 - return None - op = res[0] - if op & 0xF0 != 0x30: - return op - sz = self._recv_len() - topic_len = self.sock.read(2) - topic_len = (topic_len[0] << 8) | topic_len[1] - topic = self.sock.read(topic_len) - sz -= topic_len + 2 - if op & 6: - pid = self.sock.read(2) - pid = pid[0] << 8 | pid[1] - sz -= 2 - msg = self.sock.read(sz) - self.cb(topic, msg) - if op & 6 == 2: - pkt = bytearray(b"\x40\x02\0\0") - struct.pack_into("!H", pkt, 2, pid) - self.sock.write(pkt) - elif op & 6 == 4: - assert 0 - - # Checks whether a pending message from server is available. - # If not, returns immediately with None. Otherwise, does - # the same processing as wait_msg. - def check_msg(self): - self.sock.setblocking(False) - return self.wait_msg() From 3c1855c02296c66ddeb28c8b761d04c8f25e74c8 Mon Sep 17 00:00:00 2001 From: Luthira A <79228258+Luthiraa@users.noreply.github.com> Date: Wed, 12 Mar 2025 00:29:33 -0400 Subject: [PATCH 15/19] Add initial implementation of MQTT client and WiFi connection utility; include pre-commit configuration and license file --- scripts/tempSensor/.pre-commit-config.yaml | 18 + scripts/tempSensor/hivemq-com-chain.der | Bin 0 -> 1289 bytes scripts/tempSensor/lib/LICENSE | 21 + scripts/tempSensor/lib/bme680.py | 474 ++++++++++++ scripts/tempSensor/lib/mqtt_as.py | 824 +++++++++++++++++++++ scripts/tempSensor/lib/netman.py | 79 ++ scripts/tempSensor/pico_id.txt | 1 + scripts/tempSensor/umqtt/robust.py | 44 ++ scripts/tempSensor/umqtt/simple.py | 217 ++++++ 9 files changed, 1678 insertions(+) create mode 100644 scripts/tempSensor/.pre-commit-config.yaml create mode 100644 scripts/tempSensor/hivemq-com-chain.der create mode 100644 scripts/tempSensor/lib/LICENSE create mode 100644 scripts/tempSensor/lib/bme680.py create mode 100644 scripts/tempSensor/lib/mqtt_as.py create mode 100644 scripts/tempSensor/lib/netman.py create mode 100644 scripts/tempSensor/pico_id.txt create mode 100644 scripts/tempSensor/umqtt/robust.py create mode 100644 scripts/tempSensor/umqtt/simple.py diff --git a/scripts/tempSensor/.pre-commit-config.yaml b/scripts/tempSensor/.pre-commit-config.yaml new file mode 100644 index 00000000..4bb444e3 --- /dev/null +++ b/scripts/tempSensor/.pre-commit-config.yaml @@ -0,0 +1,18 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v3.4.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - repo: https://github.com/ambv/black + rev: 21.12b0 + hooks: + - id: black + - repo: https://github.com/PyCQA/isort + rev: 5.9.3 + hooks: + - id: isort + - repo: https://gitlab.com/pycqa/flake8 + rev: 3.9.2 + hooks: + - id: flake8 diff --git a/scripts/tempSensor/hivemq-com-chain.der b/scripts/tempSensor/hivemq-com-chain.der new file mode 100644 index 0000000000000000000000000000000000000000..ac22dcf9e8b888a98c10bd12f2108feb696b68c8 GIT binary patch literal 1289 zcmXqLVr4aGVtTuPnTe5!Nx*wW(B$v4^cNMgd9PsUExlmC%f_kI=F#?@mywa1mBGN@ zklTQhjX9KsO_(V(*ih3z9mL@hR`AR#NiE7tEl~(gO)f3UEU8ooN-a)JEK1H$a4*U) zEie={5C$n_7UuB`4surr%Fi!Rh%hve6X!KDF)%hXHUNVtAlDqoH8L_bwKTPi0y0r7 z6f+QkSjg*>TB2U8;F_0QR9R4B$Ya0-F`YTc(7>RHQ3=`MjI0dIO^o~uKyfanCPqev z^WjgQ9SIS7yT$$AN%e>_w*ZYES-Xz=taGn!mC)RKcWc!CU*FCx-nX7X(K{(B%7#(@ zXaw`O&i{Ra&aV${eIUX`tx0lmYk)j+Yhamh&ET#c)s`xyLPVB|J4zDrml1N z|MGPBk*(476}%FOd&HlI9NXQqqaiM3^$4$X<3;Tn^lSba?QosDo%8{^IJXBIN|d$-C@nmFsZm&_U06-A*R zHauI*{^sv;>9l$F9^Hj4g%KyMe!Y+T6z!sIQ}VojllwvTth2MW^G^FN)TkC1!u2Nc z+xM@JK6Ra#%G=At%*epFxbcTU<5vSdV7SThGcx{XVF4zAHUn7@UzG(E&>Y%qjI6Be z%uH|=qk#}eS{S69!+;G)F)=bgwD7Trv54&6bJ%_9lRYbs9bQtzBhb6Z^x=sY2J#>+ z$}AEFVhtjdTOyxTZ+rTLvABVw?!06V?FG|-3Cni}17GS&^aImqZH}WttF@h7LtUQZ?ft-QN0;vTO$hwn} zb)%+g<|d#oCT)D~X#To#ZsW7Fk5nI8DZHHEGIeod%=yEyfA=mZxVmwgh7$LsPa5ZW zo?m(9+@qo-d(dgpMb7o}y}z6&@K+Gq7)t&&=Y8J%?(W4` z&OToouy*hM$8YSC%nD{2E`QN`_f^s(p><#9T(Q;RS#@Df<-S+8F)n%243nd3uhsdh zEbwNnkITPw$X#6QV#HL#17UX*?xM+#GeNw@ieVHB%ztj0|M=yMFomo-$`pIuIoh+N3@0W84 zaeP$0wrh%Hh(yQIqkRvhW=^imek1eK&|$+FBZm9)TTuMdMC6sT9zcd^!(zYXZ`b3QaIzI*a>1kPyVS=_UK!7wXx`<#;@3Ozu)>5_fOHg k 100: + calc_hum = 100 + if calc_hum < 0: + calc_hum = 0 + return calc_hum + + @property + def altitude(self): + """The altitude based on current + ``pressure`` vs the sea level pressure + (``sea_level_pressure``) - + which you must enter ahead of time)""" + pressure = self.pressure # in Si units for hPascal + return 44330 * (1.0 - math.pow(pressure / self.sea_level_pressure, 0.1903)) + + @property + def gas(self): + """The gas resistance in ohms""" + self._perform_reading() + var1 = ( + (1340 + (5 * self._sw_err)) * (_LOOKUP_TABLE_1[self._gas_range]) + ) / 65536 + var2 = ((self._adc_gas * 32768) - 16777216) + var1 + var3 = (_LOOKUP_TABLE_2[self._gas_range] * var1) / 512 + calc_gas_res = (var3 + (var2 / 2)) / var2 + return int(calc_gas_res) + + def _perform_reading(self): + """Perform a single-shot reading from the sensor + and fill internal data structure for + calculations""" + expired = time.ticks_diff( + self._last_reading, time.ticks_ms() + ) * time.ticks_diff(0, 1) + if 0 <= expired < self._min_refresh_time: + time.sleep_ms(self._min_refresh_time - expired) + + # set filter + self._write(_BME680_REG_CONFIG, [self._filter << 2]) + # turn on temp oversample & pressure oversample + self._write( + _BME680_REG_CTRL_MEAS, + [(self._temp_oversample << 5) | (self._pressure_oversample << 2)], + ) + # turn on humidity oversample + self._write(_BME680_REG_CTRL_HUM, [self._humidity_oversample]) + # gas measurements enabled + self._write(_BME680_REG_CTRL_GAS, [_BME680_RUNGAS]) + + ctrl = self._read_byte(_BME680_REG_CTRL_MEAS) + ctrl = (ctrl & 0xFC) | 0x01 # enable single shot! + self._write(_BME680_REG_CTRL_MEAS, [ctrl]) + new_data = False + while not new_data: + data = self._read(_BME680_REG_MEAS_STATUS, 15) + new_data = data[0] & 0x80 != 0 + time.sleep(0.005) + self._last_reading = time.ticks_ms() + + self._adc_pres = _read24(data[2:5]) / 16 + self._adc_temp = _read24(data[5:8]) / 16 + self._adc_hum = struct.unpack(">H", bytes(data[8:10]))[0] + self._adc_gas = int(struct.unpack(">H", bytes(data[13:15]))[0] / 64) + self._gas_range = data[14] & 0x0F + + var1 = (self._adc_temp / 8) - (self._temp_calibration[0] * 2) + var2 = (var1 * self._temp_calibration[1]) / 2048 + var3 = ((var1 / 2) * (var1 / 2)) / 4096 + var3 = (var3 * self._temp_calibration[2] * 16) / 16384 + + self._t_fine = int(var2 + var3) + + def _read_calibration(self): + """Read & save the calibration coefficients""" + coeff = self._read(_BME680_BME680_COEFF_ADDR1, 25) + coeff += self._read(_BME680_BME680_COEFF_ADDR2, 16) + + coeff = list(struct.unpack(" 0 + self._client_id = config["client_id"] + self._user = config["user"] + self._pswd = config["password"] + self._keepalive = config["keepalive"] + if self._keepalive >= 65536: + raise ValueError("Invalid keepalive time") + self._response_time = config["response_time"] * 1000 + self._max_repubs = config["max_repubs"] + self._clean_init = config["clean_init"] + self._clean = config["clean"] + + will = config["will"] + if will is None: + self._lw_topic = False + else: + self._set_last_will(*will) + + # WiFi config + self._ssid = config["ssid"] + self._wifi_pw = config["wifi_pw"] + self._ssl = config["ssl"] + self._ssl_params = config["ssl_params"] + + # Callbacks and coroutines + if self._events: + self.up = asyncio.Event() + self.down = asyncio.Event() + self.queue = MsgQueue(config["queue_len"]) + else: + self._cb = config["subs_cb"] + self._wifi_handler = config["wifi_coro"] + self._connect_handler = config["connect_coro"] + + # Network settings + self.port = config["port"] or (8883 if self._ssl else 1883) + self.server = config["server"] + + if self.server is None: + raise ValueError("No server specified.") + + self._sock = None + self._sta_if = network.WLAN(network.STA_IF) + self._sta_if.active(True) + + if config["gateway"]: # Called from gateway (hence ESP32) + import aioespnow # Set up ESPNOW + + while not (sta := self._sta_if).active(): + time.sleep(0.1) + sta.config(pm=sta.PM_NONE) # No power management + sta.active(True) + self._espnow = aioespnow.AIOESPNow() + self._espnow.active(True) + + self.newpid = pid_gen() + self.rcv_pids = set() # PUBACK and SUBACK pids awaiting ACK response + self.last_rx = ticks_ms() # Time of last communication from broker + self.lock = asyncio.Lock() + + def _set_last_will(self, topic, msg, retain=False, qos=0): + qos_check(qos) + if not topic: + raise ValueError("Empty topic.") + self._lw_topic = topic + self._lw_msg = msg + self._lw_qos = qos + self._lw_retain = retain + + def dprint(self, msg, *args): + if self.DEBUG: + print(msg % args) + + def _timeout(self, t): + return ticks_diff(ticks_ms(), t) > self._response_time + + async def _as_read(self, n, sock=None): + if sock is None: + sock = self._sock + data = bytearray(n) + buffer = memoryview(data) + size = 0 + t = ticks_ms() + while size < n: + if self._timeout(t) or not self.isconnected(): + raise OSError(-1, "Timeout on socket read") + try: + msg_size = sock.readinto(buffer[size:], n - size) + except OSError as e: # ESP32 issues weird 119 errors here + msg_size = None + if e.args[0] not in BUSY_ERRORS: + raise + if msg_size == 0: # Connection closed by host + raise OSError(-1, "Connection closed by host") + if msg_size is not None: # data received + size += msg_size + t = ticks_ms() + self.last_rx = ticks_ms() + await asyncio.sleep_ms(_SOCKET_POLL_DELAY) + return data + + async def _as_write(self, bytes_wr, length=0, sock=None): + if sock is None: + sock = self._sock + + # Wrap bytes in memoryview to avoid copying during slicing + bytes_wr = memoryview(bytes_wr) + if length: + bytes_wr = bytes_wr[:length] + t = ticks_ms() + while bytes_wr: + if self._timeout(t) or not self.isconnected(): + raise OSError(-1, "Timeout on socket write") + try: + n = sock.write(bytes_wr) + except OSError as e: # ESP32 issues weird 119 errors here + n = 0 + if e.args[0] not in BUSY_ERRORS: + raise + if n: + t = ticks_ms() + bytes_wr = bytes_wr[n:] + await asyncio.sleep_ms(_SOCKET_POLL_DELAY) + + async def _send_str(self, s): + await self._as_write(struct.pack("!H", len(s))) + await self._as_write(s) + + async def _recv_len(self): + n = 0 + sh = 0 + while 1: + res = await self._as_read(1) + b = res[0] + n |= (b & 0x7F) << sh + if not b & 0x80: + return n + sh += 7 + + async def _connect(self, clean): + self._sock = socket.socket() + self._sock.setblocking(False) + try: + self._sock.connect(self._addr) + except OSError as e: + if e.args[0] not in BUSY_ERRORS: + raise + await asyncio.sleep_ms(_DEFAULT_MS) + self.dprint("Connecting to broker.") + if self._ssl: + import ssl + + self._sock = ssl.wrap_socket(self._sock, **self._ssl_params) + premsg = bytearray(b"\x10\0\0\0\0\0") + msg = bytearray(b"\x04MQTT\x04\0\0\0") # Protocol 3.1.1 + + sz = 10 + 2 + len(self._client_id) + msg[6] = clean << 1 + if self._user: + sz += 2 + len(self._user) + 2 + len(self._pswd) + msg[6] |= 0xC0 + if self._keepalive: + msg[7] |= self._keepalive >> 8 + msg[8] |= self._keepalive & 0x00FF + if self._lw_topic: + sz += 2 + len(self._lw_topic) + 2 + len(self._lw_msg) + msg[6] |= 0x4 | (self._lw_qos & 0x1) << 3 | (self._lw_qos & 0x2) << 3 + msg[6] |= self._lw_retain << 5 + i = 1 + while sz > 0x7F: + premsg[i] = (sz & 0x7F) | 0x80 + sz >>= 7 + i += 1 + premsg[i] = sz + await self._as_write(premsg, i + 2) + await self._as_write(msg) + await self._send_str(self._client_id) + if self._lw_topic: + await self._send_str(self._lw_topic) + await self._send_str(self._lw_msg) + if self._user: + await self._send_str(self._user) + await self._send_str(self._pswd) + # Await CONNACK + # read causes ECONNABORTED if broker is out; triggers a reconnect. + resp = await self._as_read(4) + self.dprint("Connected to broker.") # Got CONNACK + if ( + resp[3] != 0 or resp[0] != 0x20 or resp[1] != 0x02 + ): # Bad CONNACK e.g. authentication fail. + raise OSError( + -1, + f"Connect fail: 0x{(resp[0] << 8) + resp[1]:04x} {resp[3]} (README 7)", + ) + + async def _ping(self): + async with self.lock: + await self._as_write(b"\xc0\0") + + # Check internet connectivity by sending DNS lookup to Google's 8.8.8.8 + async def wan_ok( + self, + packet=( + b"$\x1a\x01\x00\x00\x01\x00\x00\x00" + b"\x00\x00\x00\x03www\x06google\x03com\x00" + b"\x00\x01\x00\x01" + ), + ): + if not self.isconnected(): # WiFi is down + return False + length = 32 # DNS query and response packet size + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.setblocking(False) + s.connect(("8.8.8.8", 53)) + await asyncio.sleep(1) + try: + await self._as_write(packet, sock=s) + await asyncio.sleep(2) + res = await self._as_read(length, s) + if len(res) == length: + return True # DNS response size OK + except OSError: # Timeout on read: no connectivity. + return False + finally: + s.close() + return False + + async def broker_up(self): # Test broker connectivity + if not self.isconnected(): + return False + tlast = self.last_rx + if ticks_diff(ticks_ms(), tlast) < 1000: + return True + try: + await self._ping() + except OSError: + return False + t = ticks_ms() + while not self._timeout(t): + await asyncio.sleep_ms(100) + if ticks_diff(self.last_rx, tlast) > 0: # Response received + return True + return False + + async def disconnect(self): + if self._sock is not None: + await self._kill_tasks(False) # Keep socket open + try: + async with self.lock: + self._sock.write(b"\xe0\0") # Close broker connection + await asyncio.sleep_ms(100) + except OSError: + pass + self._close() + self._has_connected = False + + def _close(self): + if self._sock is not None: + self._sock.close() + + def close( + self, + ): # API. See https://github.com/peterhinch/micropython-mqtt/issues/60 + self._close() + try: + self._sta_if.disconnect() # Disconnect Wi-Fi to avoid errors + except OSError: + self.dprint("Wi-Fi not started, unable to disconnect interface") + self._sta_if.active(False) + + async def _await_pid(self, pid): + t = ticks_ms() + while pid in self.rcv_pids: # local copy + if self._timeout(t) or not self.isconnected(): + break # Must repub or bail out + await asyncio.sleep_ms(100) + else: + return True # PID received. All done. + return False + + # qos == 1: coro blocks until wait_msg gets correct PID. + # If WiFi fails completely subclass re-publishes with new PID. + async def publish(self, topic, msg, retain, qos): + pid = next(self.newpid) + if qos: + self.rcv_pids.add(pid) + async with self.lock: + await self._publish(topic, msg, retain, qos, 0, pid) + if qos == 0: + return + + count = 0 + while 1: # Await PUBACK, republish on timeout + if await self._await_pid(pid): + return + # No match + if count >= self._max_repubs or not self.isconnected(): + raise OSError(-1) # Subclass to re-publish with new PID + async with self.lock: + await self._publish(topic, msg, retain, qos, dup=1, pid=pid) # Add pid + count += 1 + self.REPUB_COUNT += 1 + + async def _publish(self, topic, msg, retain, qos, dup, pid): + pkt = bytearray(b"\x30\0\0\0") + pkt[0] |= qos << 1 | retain | dup << 3 + sz = 2 + len(topic) + len(msg) + if qos > 0: + sz += 2 + if sz >= 2097152: + raise MQTTException("Strings too long.") + i = 1 + while sz > 0x7F: + pkt[i] = (sz & 0x7F) | 0x80 + sz >>= 7 + i += 1 + pkt[i] = sz + await self._as_write(pkt, i + 1) + await self._send_str(topic) + if qos > 0: + struct.pack_into("!H", pkt, 0, pid) + await self._as_write(pkt, 2) + await self._as_write(msg) + + # Can raise OSError if WiFi fails. Subclass traps. + async def subscribe(self, topic, qos): + pkt = bytearray(b"\x82\0\0\0") + pid = next(self.newpid) + self.rcv_pids.add(pid) + struct.pack_into("!BH", pkt, 1, 2 + 2 + len(topic) + 1, pid) + async with self.lock: + await self._as_write(pkt) + await self._send_str(topic) + await self._as_write(qos.to_bytes(1, "little")) + + if not await self._await_pid(pid): + raise OSError(-1) + + # Can raise OSError if WiFi fails. Subclass traps. + async def unsubscribe(self, topic): + pkt = bytearray(b"\xa2\0\0\0") + pid = next(self.newpid) + self.rcv_pids.add(pid) + struct.pack_into("!BH", pkt, 1, 2 + 2 + len(topic), pid) + async with self.lock: + await self._as_write(pkt) + await self._send_str(topic) + + if not await self._await_pid(pid): + raise OSError(-1) + + # Wait for a single incoming MQTT message and process it. + # Subscribed messages are delivered to a callback previously + # set by .setup() method. Other (internal) MQTT + # messages processed internally. + # Immediate return if no data available. Called from ._handle_msg(). + async def wait_msg(self): + try: + res = self._sock.read(1) # Throws OSError on WiFi fail + except OSError as e: + if e.args[0] in BUSY_ERRORS: # Needed by RP2 + await asyncio.sleep_ms(0) + return + raise + if res is None: + return + if res == b"": + raise OSError(-1, "Empty response") + + if res == b"\xd0": # PINGRESP + await self._as_read(1) # Update .last_rx time + return + op = res[0] + + if op == 0x40: # PUBACK: save pid + sz = await self._as_read(1) + if sz != b"\x02": + raise OSError(-1, "Invalid PUBACK packet") + rcv_pid = await self._as_read(2) + pid = rcv_pid[0] << 8 | rcv_pid[1] + if pid in self.rcv_pids: + self.rcv_pids.discard(pid) + else: + raise OSError(-1, "Invalid pid in PUBACK packet") + + if op == 0x90: # SUBACK + resp = await self._as_read(4) + if resp[3] == 0x80: + raise OSError(-1, "Invalid SUBACK packet") + pid = resp[2] | (resp[1] << 8) + if pid in self.rcv_pids: + self.rcv_pids.discard(pid) + else: + raise OSError(-1, "Invalid pid in SUBACK packet") + + if op == 0xB0: # UNSUBACK + resp = await self._as_read(3) + pid = resp[2] | (resp[1] << 8) + if pid in self.rcv_pids: + self.rcv_pids.discard(pid) + else: + raise OSError(-1) + + if op & 0xF0 != 0x30: + return + sz = await self._recv_len() + topic_len = await self._as_read(2) + topic_len = (topic_len[0] << 8) | topic_len[1] + topic = await self._as_read(topic_len) + sz -= topic_len + 2 + if op & 6: + pid = await self._as_read(2) + pid = pid[0] << 8 | pid[1] + sz -= 2 + msg = await self._as_read(sz) + retained = op & 0x01 + if self._events: + self.queue.put(topic, msg, bool(retained)) + else: + self._cb(topic, msg, bool(retained)) + if op & 6 == 2: # qos 1 + pkt = bytearray(b"\x40\x02\0\0") # Send PUBACK + struct.pack_into("!H", pkt, 2, pid) + await self._as_write(pkt) + elif op & 6 == 4: # qos 2 not supported + raise OSError(-1, "QoS 2 not supported") + + +# MQTTClient class. Handles issues relating to connectivity. + + +class MQTTClient(MQTT_base): + def __init__(self, config): + super().__init__(config) + self._isconnected = False # Current connection state + keepalive = 1000 * self._keepalive # ms + self._ping_interval = keepalive // 4 if keepalive else 20000 + p_i = ( + config["ping_interval"] * 1000 + ) # Can specify shorter e.g. for subscribe-only + if p_i and p_i < self._ping_interval: + self._ping_interval = p_i + self._in_connect = False + self._has_connected = False # Define 'Clean Session' value to use. + self._tasks = [] + if ESP8266: + import esp + + esp.sleep_type( + 0 + ) # Improve connection integrity at cost of power consumption. + + async def wifi_connect(self, quick=False): + s = self._sta_if + if ESP8266: + if s.isconnected(): # 1st attempt, already connected. + return + s.active(True) + s.connect() # ESP8266 remembers connection. + for _ in range(60): + if ( + s.status() != network.STAT_CONNECTING + ): # Break out on fail or success. Check once per sec. + break + await asyncio.sleep(1) + if ( + s.status() == network.STAT_CONNECTING + ): # might hang forever awaiting dhcp lease renewal or something else + s.disconnect() + await asyncio.sleep(1) + if ( + not s.isconnected() + and self._ssid is not None + and self._wifi_pw is not None + ): + s.connect(self._ssid, self._wifi_pw) + while ( + s.status() == network.STAT_CONNECTING + ): # Break out on fail or success. Check once per sec. + await asyncio.sleep(1) + else: + s.active(True) + if RP2: # Disable auto-sleep. + # para 3.6.3 + s.config(pm=0xA11140) + s.connect(self._ssid, self._wifi_pw) + for _ in range(60): # Break out on fail or success. Check once per sec. + await asyncio.sleep(1) + # Loop while connecting or no IP + if s.isconnected(): + break + if ESP32: + if s.status() != network.STAT_CONNECTING: # 1001 + break + elif PYBOARD: # No symbolic constants in network + if not 1 <= s.status() <= 2: + break + elif RP2: # 1 is STAT_CONNECTING. 2 reported by user (No IP?) + if not 1 <= s.status() <= 2: + break + else: # Timeout: still in connecting state + s.disconnect() + await asyncio.sleep(1) + + if not s.isconnected(): # Timed out + raise OSError("Wi-Fi connect timed out") + if not quick: # Skip on first connection only if power saving + # Ensure connection stays up for a few secs. + self.dprint("Checking WiFi integrity.") + for _ in range(5): + if not s.isconnected(): + raise OSError("Connection Unstable") # in 1st 5 secs + await asyncio.sleep(1) + self.dprint("Got reliable connection") + + async def connect( + self, *, quick=False + ): # Quick initial connect option for battery apps + if not self._has_connected: + await self.wifi_connect(quick) # On 1st call, caller handles error + # Note this blocks if DNS lookup occurs. Do it once to prevent + # blocking during later internet outage: + self._addr = socket.getaddrinfo(self.server, self.port)[0][-1] + self._in_connect = True # Disable low level ._isconnected check + try: + if not self._has_connected and self._clean_init and not self._clean: + # Power up. Clear previous session data but subsequently save it. + # Issue #40 + await self._connect(True) # Connect with clean session + try: + async with self.lock: + self._sock.write( + b"\xe0\0" + ) # Force disconnect but keep socket open + except OSError: + pass + self.dprint("Waiting for disconnect") + await asyncio.sleep(2) # Wait for broker to disconnect + self.dprint("About to reconnect with unclean session.") + await self._connect(self._clean) + except Exception: + self._close() + self._in_connect = False # Caller may run .isconnected() + raise + self.rcv_pids.clear() + # If we get here without error broker/LAN must be up. + self._isconnected = True + self._in_connect = False # Low level code can now check connectivity. + if not self._events: + asyncio.create_task(self._wifi_handler(True)) # User handler. + if not self._has_connected: + self._has_connected = True # Use normal clean flag on reconnect. + asyncio.create_task(self._keep_connected()) + # Runs forever unless user issues .disconnect() + + asyncio.create_task(self._handle_msg()) # Task quits on connection fail. + self._tasks.append(asyncio.create_task(self._keep_alive())) + if self.DEBUG: + self._tasks.append(asyncio.create_task(self._memory())) + if self._events: + self.up.set() # Connectivity is up + else: + asyncio.create_task(self._connect_handler(self)) # User handler. + + # Launched by .connect(). Runs until connectivity fails. Checks for and + # handles incoming messages. + async def _handle_msg(self): + try: + while self.isconnected(): + async with self.lock: + await self.wait_msg() # Immediate return if no message + await asyncio.sleep_ms(_DEFAULT_MS) # Let other tasks get lock + + except OSError: + pass + self._reconnect() # Broker or WiFi fail. + + # Keep broker alive MQTT spec 3.1.2.10 Keep Alive. + # Runs until ping failure or no response in keepalive period. + async def _keep_alive(self): + while self.isconnected(): + pings_due = ticks_diff(ticks_ms(), self.last_rx) // self._ping_interval + if pings_due >= 4: + self.dprint("Reconnect: broker fail.") + break + await asyncio.sleep_ms(self._ping_interval) + try: + await self._ping() + except OSError: + break + self._reconnect() # Broker or WiFi fail. + + async def _kill_tasks(self, kill_skt): # Cancel running tasks + for task in self._tasks: + task.cancel() + self._tasks.clear() + await asyncio.sleep_ms(0) # Ensure cancellation complete + if kill_skt: # Close socket + self._close() + + # DEBUG: show RAM messages. + async def _memory(self): + while True: + await asyncio.sleep(20) + gc.collect() + self.dprint("RAM free %d alloc %d", gc.mem_free(), gc.mem_alloc()) + + def isconnected(self): + if self._in_connect: # Disable low-level check during .connect() + return True + if self._isconnected and not self._sta_if.isconnected(): # It's going down. + self._reconnect() + return self._isconnected + + def _reconnect(self): # Schedule a reconnection if not underway. + if self._isconnected: + self._isconnected = False + asyncio.create_task(self._kill_tasks(True)) # Shut down tasks and socket + if self._events: # Signal an outage + self.down.set() + else: + asyncio.create_task(self._wifi_handler(False)) # User handler. + + # Await broker connection. + async def _connection(self): + while not self._isconnected: + await asyncio.sleep(1) + + # Scheduled on 1st successful connection. Runs forever maintaining wifi and + # broker connection. Must handle conditions at edge of WiFi range. + async def _keep_connected(self): + while self._has_connected: + if self.isconnected(): # Pause for 1 second + await asyncio.sleep(1) + gc.collect() + else: # Link is down, socket is closed, tasks are killed + try: + self._sta_if.disconnect() + except OSError: + self.dprint("Wi-Fi not started, unable to disconnect interface") + await asyncio.sleep(1) + try: + await self.wifi_connect() + except OSError: + continue + if ( + not self._has_connected + ): # User has issued the terminal .disconnect() + self.dprint("Disconnected, exiting _keep_connected") + break + try: + await self.connect() + # Now has set ._isconnected and scheduled _connect_handler(). + self.dprint("Reconnect OK!") + except OSError as e: + self.dprint("Error in reconnect. %s", e) + # Can get ECONNABORTED or -1. + # The latter signifies no or bad CONNACK received. + self._close() # Disconnect and try again. + self._in_connect = False + self._isconnected = False + self.dprint("Disconnected, exited _keep_connected") + + async def subscribe(self, topic, qos=0): + qos_check(qos) + while 1: + await self._connection() + try: + return await super().subscribe(topic, qos) + except OSError: + pass + self._reconnect() # Broker or WiFi fail. + + async def unsubscribe(self, topic): + while 1: + await self._connection() + try: + return await super().unsubscribe(topic) + except OSError: + pass + self._reconnect() # Broker or WiFi fail. + + async def publish(self, topic, msg, retain=False, qos=0): + qos_check(qos) + while 1: + await self._connection() + try: + return await super().publish(topic, msg, retain, qos) + except OSError: + pass + self._reconnect() # Broker or WiFi fail. diff --git a/scripts/tempSensor/lib/netman.py b/scripts/tempSensor/lib/netman.py new file mode 100644 index 00000000..90e39d26 --- /dev/null +++ b/scripts/tempSensor/lib/netman.py @@ -0,0 +1,79 @@ +# .';:cc;. +# .,',;lol::c. +# ;';lddddlclo +# lcloxxoddodxdool:,. +# cxdddxdodxdkOkkkkkkkd:. +# .ldxkkOOOOkkOO000Okkxkkkkx:. +# .lddxkkOkOOO0OOO0000Okxxxxkkkk: +# 'ooddkkkxxkO0000KK00Okxdoodxkkkko +# .ooodxkkxxxOO000kkkO0KOxolooxkkxxkl +# lolodxkkxxkOx,. .lkdolodkkxxxO. +# doloodxkkkOk .... .,cxO; +# ddoodddxkkkk: ,oxxxkOdc'..o' +# :kdddxxxxd, ,lolccldxxxkkOOOkkkko, +# lOkxkkk; :xkkkkkkkkOOO000OOkkOOk. +# ;00Ok' 'O000OO0000000000OOOO0Od. +# .l0l.;OOO000000OOOOOO000000x, +# .'OKKKK00000000000000kc. +# .:ox0KKKKKKK0kdc,. +# ... +# +# Author: peppe8o +# Date: Jul 24th, 2022 +# Version: 1.0 +# https://peppe8o.com + +# modified by @sgbaird from source: +# https://peppe8o.com/getting-started-with-wifi-on-raspberry-pi-pico-w-and-micropython/ + +import time + +import network +import rp2 +from ubinascii import hexlify + + +def connectWiFi(ssid, password, country=None, wifi_energy_saver=False, retries=3): + for _ in range(retries): + try: + if country is not None: + # https://www.google.com/search?q=wifi+country+codes + rp2.country(country) + wlan = network.WLAN(network.STA_IF) + if not wifi_energy_saver: + wlan.config(pm=0xA11140) # avoid the energy-saving WiFi mode + wlan.active(True) + + mac = hexlify(network.WLAN().config("mac"), ":").decode() + print(f"MAC address: {mac}") + + wlan.connect(ssid, password) + # Wait for connect or fail + max_wait = 10 + while max_wait > 0: + if wlan.status() < 0 or wlan.status() >= 3: + break + max_wait -= 1 + print("waiting for connection...") + time.sleep(1) + + # Handle connection error + if wlan.status() != 3: + raise RuntimeError("network connection failed") + else: + print("connected") + status = wlan.ifconfig() + print("ip = " + status[0]) + return status + except RuntimeError as e: + print(f"Attempt failed with error: {e}. Retrying...") + raise RuntimeError( + "All attempts to connect to the network failed." + "Ensure you are using a 2.4 " + "GHz WiFi network with WPA-2 authentication." + "See the additional prerequisites " + "section from https://doi.org/10.1016/j.xpro.2023.102329 or the " + "https://github.com/sparks-baird/self-driving-lab-demo/issues/76" + "for additional " + "troubleshooting help." + ) diff --git a/scripts/tempSensor/pico_id.txt b/scripts/tempSensor/pico_id.txt new file mode 100644 index 00000000..ad449ae1 --- /dev/null +++ b/scripts/tempSensor/pico_id.txt @@ -0,0 +1 @@ +e66130100f594628 diff --git a/scripts/tempSensor/umqtt/robust.py b/scripts/tempSensor/umqtt/robust.py new file mode 100644 index 00000000..2a2b5629 --- /dev/null +++ b/scripts/tempSensor/umqtt/robust.py @@ -0,0 +1,44 @@ +import utime + +from . import simple + + +class MQTTClient(simple.MQTTClient): + DELAY = 2 + DEBUG = False + + def delay(self, i): + utime.sleep(self.DELAY) + + def log(self, in_reconnect, e): + if self.DEBUG: + if in_reconnect: + print("mqtt reconnect: %r" % e) + else: + print("mqtt: %r" % e) + + def reconnect(self): + i = 0 + while 1: + try: + return super().connect(False) + except OSError as e: + self.log(True, e) + i += 1 + self.delay(i) + + def publish(self, topic, msg, retain=False, qos=0): + while 1: + try: + return super().publish(topic, msg, retain, qos) + except OSError as e: + self.log(False, e) + self.reconnect() + + def wait_msg(self): + while 1: + try: + return super().wait_msg() + except OSError as e: + self.log(False, e) + self.reconnect() diff --git a/scripts/tempSensor/umqtt/simple.py b/scripts/tempSensor/umqtt/simple.py new file mode 100644 index 00000000..5d09230c --- /dev/null +++ b/scripts/tempSensor/umqtt/simple.py @@ -0,0 +1,217 @@ +import usocket as socket +import ustruct as struct + + +class MQTTException(Exception): + pass + + +class MQTTClient: + def __init__( + self, + client_id, + server, + port=0, + user=None, + password=None, + keepalive=0, + ssl=False, + ssl_params={}, + ): + if port == 0: + port = 8883 if ssl else 1883 + self.client_id = client_id + self.sock = None + self.server = server + self.port = port + self.ssl = ssl + self.ssl_params = ssl_params + self.pid = 0 + self.cb = None + self.user = user + self.pswd = password + self.keepalive = keepalive + self.lw_topic = None + self.lw_msg = None + self.lw_qos = 0 + self.lw_retain = False + + def _send_str(self, s): + self.sock.write(struct.pack("!H", len(s))) + self.sock.write(s) + + def _recv_len(self): + n = 0 + sh = 0 + while 1: + b = self.sock.read(1)[0] + n |= (b & 0x7F) << sh + if not b & 0x80: + return n + sh += 7 + + def set_callback(self, f): + self.cb = f + + def set_last_will(self, topic, msg, retain=False, qos=0): + assert 0 <= qos <= 2 + assert topic + self.lw_topic = topic + self.lw_msg = msg + self.lw_qos = qos + self.lw_retain = retain + + def connect(self, clean_session=True): + self.sock = socket.socket() + addr = socket.getaddrinfo(self.server, self.port)[0][-1] + self.sock.connect(addr) + if self.ssl: + # replaced ussl with ssl due to deprecation in MicroPython 1.23.0 + # (not PR'd on source repo, but I'm using mqtt_as in my workflows + # instead, anyway) + import ssl + + self.sock = ssl.wrap_socket(self.sock, **self.ssl_params) + premsg = bytearray(b"\x10\0\0\0\0\0") + msg = bytearray(b"\x04MQTT\x04\x02\0\0") + + sz = 10 + 2 + len(self.client_id) + msg[6] = clean_session << 1 + if self.user is not None: + sz += 2 + len(self.user) + 2 + len(self.pswd) + msg[6] |= 0xC0 + if self.keepalive: + assert self.keepalive < 65536 + msg[7] |= self.keepalive >> 8 + msg[8] |= self.keepalive & 0x00FF + if self.lw_topic: + sz += 2 + len(self.lw_topic) + 2 + len(self.lw_msg) + msg[6] |= 0x4 | (self.lw_qos & 0x1) << 3 | (self.lw_qos & 0x2) << 3 + msg[6] |= self.lw_retain << 5 + + i = 1 + while sz > 0x7F: + premsg[i] = (sz & 0x7F) | 0x80 + sz >>= 7 + i += 1 + premsg[i] = sz + + self.sock.write(premsg, i + 2) + self.sock.write(msg) + # print(hex(len(msg)), hexlify(msg, ":")) + self._send_str(self.client_id) + if self.lw_topic: + self._send_str(self.lw_topic) + self._send_str(self.lw_msg) + if self.user is not None: + self._send_str(self.user) + self._send_str(self.pswd) + resp = self.sock.read(4) + assert resp[0] == 0x20 and resp[1] == 0x02 + if resp[3] != 0: + raise MQTTException(resp[3]) + return resp[2] & 1 + + def disconnect(self): + self.sock.write(b"\xe0\0") + self.sock.close() + + def ping(self): + self.sock.write(b"\xc0\0") + + def publish(self, topic, msg, retain=False, qos=0): + pkt = bytearray(b"\x30\0\0\0") + pkt[0] |= qos << 1 | retain + sz = 2 + len(topic) + len(msg) + if qos > 0: + sz += 2 + assert sz < 2097152 + i = 1 + while sz > 0x7F: + pkt[i] = (sz & 0x7F) | 0x80 + sz >>= 7 + i += 1 + pkt[i] = sz + # print(hex(len(pkt)), hexlify(pkt, ":")) + self.sock.write(pkt, i + 1) + self._send_str(topic) + if qos > 0: + self.pid += 1 + pid = self.pid + struct.pack_into("!H", pkt, 0, pid) + self.sock.write(pkt, 2) + self.sock.write(msg) + if qos == 1: + while 1: + op = self.wait_msg() + if op == 0x40: + sz = self.sock.read(1) + assert sz == b"\x02" + rcv_pid = self.sock.read(2) + rcv_pid = rcv_pid[0] << 8 | rcv_pid[1] + if pid == rcv_pid: + return + elif qos == 2: + assert 0 + + def subscribe(self, topic, qos=0): + assert self.cb is not None, "Subscribe callback is not set" + pkt = bytearray(b"\x82\0\0\0") + self.pid += 1 + struct.pack_into("!BH", pkt, 1, 2 + 2 + len(topic) + 1, self.pid) + # print(hex(len(pkt)), hexlify(pkt, ":")) + self.sock.write(pkt) + self._send_str(topic) + self.sock.write(qos.to_bytes(1, "little")) + while 1: + op = self.wait_msg() + if op == 0x90: + resp = self.sock.read(4) + # print(resp) + assert resp[1] == pkt[2] and resp[2] == pkt[3] + if resp[3] == 0x80: + raise MQTTException(resp[3]) + return + + # Wait for a single incoming MQTT message and process it. + # Subscribed messages are delivered to a callback previously + # set by .set_callback() method. Other (internal) MQTT + # messages processed internally. + def wait_msg(self): + res = self.sock.read(1) + self.sock.setblocking(True) + if res is None: + return None + if res == b"": + raise OSError(-1) + if res == b"\xd0": # PINGRESP + sz = self.sock.read(1)[0] + assert sz == 0 + return None + op = res[0] + if op & 0xF0 != 0x30: + return op + sz = self._recv_len() + topic_len = self.sock.read(2) + topic_len = (topic_len[0] << 8) | topic_len[1] + topic = self.sock.read(topic_len) + sz -= topic_len + 2 + if op & 6: + pid = self.sock.read(2) + pid = pid[0] << 8 | pid[1] + sz -= 2 + msg = self.sock.read(sz) + self.cb(topic, msg) + if op & 6 == 2: + pkt = bytearray(b"\x40\x02\0\0") + struct.pack_into("!H", pkt, 2, pid) + self.sock.write(pkt) + elif op & 6 == 4: + assert 0 + + # Checks whether a pending message from server is available. + # If not, returns immediately with None. Otherwise, does + # the same processing as wait_msg. + def check_msg(self): + self.sock.setblocking(False) + return self.wait_msg() From 3f7194b2dc658358eae04013f1707a4e91bb5782 Mon Sep 17 00:00:00 2001 From: Luthira A <79228258+Luthiraa@users.noreply.github.com> Date: Wed, 12 Mar 2025 00:41:21 -0400 Subject: [PATCH 16/19] No code changes made; skipping commit. --- scripts/tempSensor/lib/mqtt_as.py | 352 +++++++++++++++--------------- 1 file changed, 172 insertions(+), 180 deletions(-) diff --git a/scripts/tempSensor/lib/mqtt_as.py b/scripts/tempSensor/lib/mqtt_as.py index ef5f5bd6..d46caf21 100644 --- a/scripts/tempSensor/lib/mqtt_as.py +++ b/scripts/tempSensor/lib/mqtt_as.py @@ -9,6 +9,7 @@ import time from sys import platform +import aioespnow import network import uasyncio as asyncio import usocket as socket @@ -23,13 +24,12 @@ VERSION = (0, 7, 1) -# Default short delay for good SynCom -# throughput (avoid sleep(0) with SynCom). +# Default short delay for good SynCom throughput (avoid sleep(0) with SynCom). _DEFAULT_MS = const(20) _SOCKET_POLL_DELAY = const(5) # 100ms added greatly to publish latency -# Legitimate errors while waiting on a socket. -# See uasyncio __init__.py open_connection(). +# Legitimate errors while waiting on a socket. See uasyncio __init__.py +# open_connection(). ESP32 = platform == "esp32" RP2 = platform == "rp2" @@ -37,7 +37,7 @@ BUSY_ERRORS = [ EINPROGRESS, ETIMEDOUT, - 118, # Add in weird ESP32 errors + 118, # Weird ESP32 error 119, ] elif RP2: @@ -51,7 +51,7 @@ # Default "do little" coroutine for optional user replacement async def eliza(*_): - """Example: set_wifi_handler(coro) - see test program""" + """Example: set_wifi_handler(coro) - see test program.""" await asyncio.sleep_ms(_DEFAULT_MS) @@ -69,7 +69,7 @@ def put(self, *v): self._evt.set() self._wi = (self._wi + 1) % self._size if self._wi == self._ri: # Would indicate empty - self._ri = (self._ri + 1) % self._size # Discard a message + self._ri = (self._ri + 1) % self._size self.discards += 1 def __aiter__(self): @@ -114,6 +114,7 @@ class MQTTException(Exception): def pid_gen(): + """Generator for packet IDs.""" pid = 0 while True: pid = pid + 1 if pid < 65535 else 1 @@ -125,9 +126,11 @@ def qos_check(qos): raise ValueError("Only qos 0 and 1 are supported.") -# MQTT_base class - Handles MQTT protocol class MQTT_base: - REPUB_COUNT = 0 # TEST + """Handles core MQTT protocol, WiFi and broker connectivity is handled by + MQTTClient subclass.""" + + REPUB_COUNT = 0 # For debug or tests DEBUG = False def __init__(self, config): @@ -138,6 +141,7 @@ def __init__(self, config): self._keepalive = config["keepalive"] if self._keepalive >= 65536: raise ValueError("Invalid keepalive time") + self._response_time = config["response_time"] * 1000 self._max_repubs = config["max_repubs"] self._clean_init = config["clean_init"] @@ -168,7 +172,6 @@ def __init__(self, config): # Network settings self.port = config["port"] or (8883 if self._ssl else 1883) self.server = config["server"] - if self.server is None: raise ValueError("No server specified.") @@ -176,8 +179,7 @@ def __init__(self, config): self._sta_if = network.WLAN(network.STA_IF) self._sta_if.active(True) - if config["gateway"]: # Called from gateway (hence ESP32) - import aioespnow # Set up ESPNOW + if config["gateway"]: # Called from gateway (hence ESP32). while not (sta := self._sta_if).active(): time.sleep(0.1) @@ -188,7 +190,7 @@ def __init__(self, config): self.newpid = pid_gen() self.rcv_pids = set() # PUBACK and SUBACK pids awaiting ACK response - self.last_rx = ticks_ms() # Time of last communication from broker + self.last_rx = ticks_ms() self.lock = asyncio.Lock() def _set_last_will(self, topic, msg, retain=False, qos=0): @@ -208,24 +210,26 @@ def _timeout(self, t): return ticks_diff(ticks_ms(), t) > self._response_time async def _as_read(self, n, sock=None): + """Asynchronous read of n bytes from socket.""" if sock is None: sock = self._sock data = bytearray(n) - buffer = memoryview(data) + buff = memoryview(data) size = 0 t = ticks_ms() + while size < n: if self._timeout(t) or not self.isconnected(): raise OSError(-1, "Timeout on socket read") try: - msg_size = sock.readinto(buffer[size:], n - size) - except OSError as e: # ESP32 issues weird 119 errors here + msg_size = sock.readinto(buff[size:], n - size) + except OSError as e: msg_size = None if e.args[0] not in BUSY_ERRORS: raise - if msg_size == 0: # Connection closed by host + if msg_size == 0: raise OSError(-1, "Connection closed by host") - if msg_size is not None: # data received + if msg_size is not None: size += msg_size t = ticks_ms() self.last_rx = ticks_ms() @@ -233,20 +237,21 @@ async def _as_read(self, n, sock=None): return data async def _as_write(self, bytes_wr, length=0, sock=None): + """Asynchronous write of up to 'length' bytes to socket.""" if sock is None: sock = self._sock - # Wrap bytes in memoryview to avoid copying during slicing bytes_wr = memoryview(bytes_wr) if length: bytes_wr = bytes_wr[:length] + t = ticks_ms() while bytes_wr: if self._timeout(t) or not self.isconnected(): raise OSError(-1, "Timeout on socket write") try: n = sock.write(bytes_wr) - except OSError as e: # ESP32 issues weird 119 errors here + except OSError as e: n = 0 if e.args[0] not in BUSY_ERRORS: raise @@ -260,15 +265,16 @@ async def _send_str(self, s): await self._as_write(s) async def _recv_len(self): + """Receive length from MQTT variable-length integer.""" n = 0 - sh = 0 - while 1: + shift = 0 + while True: res = await self._as_read(1) b = res[0] - n |= (b & 0x7F) << sh + n |= (b & 0x7F) << shift if not b & 0x80: return n - sh += 7 + shift += 7 async def _connect(self, clean): self._sock = socket.socket() @@ -278,27 +284,35 @@ async def _connect(self, clean): except OSError as e: if e.args[0] not in BUSY_ERRORS: raise + await asyncio.sleep_ms(_DEFAULT_MS) self.dprint("Connecting to broker.") + if self._ssl: import ssl self._sock = ssl.wrap_socket(self._sock, **self._ssl_params) + premsg = bytearray(b"\x10\0\0\0\0\0") msg = bytearray(b"\x04MQTT\x04\0\0\0") # Protocol 3.1.1 sz = 10 + 2 + len(self._client_id) msg[6] = clean << 1 + if self._user: sz += 2 + len(self._user) + 2 + len(self._pswd) msg[6] |= 0xC0 + if self._keepalive: msg[7] |= self._keepalive >> 8 msg[8] |= self._keepalive & 0x00FF + if self._lw_topic: sz += 2 + len(self._lw_topic) + 2 + len(self._lw_msg) - msg[6] |= 0x4 | (self._lw_qos & 0x1) << 3 | (self._lw_qos & 0x2) << 3 + msg[6] |= 0x4 | (self._lw_qos & 0x1) << 3 + msg[6] |= (self._lw_qos & 0x2) << 3 msg[6] |= self._lw_retain << 5 + i = 1 while sz > 0x7F: premsg[i] = (sz & 0x7F) | 0x80 @@ -308,29 +322,25 @@ async def _connect(self, clean): await self._as_write(premsg, i + 2) await self._as_write(msg) await self._send_str(self._client_id) + if self._lw_topic: await self._send_str(self._lw_topic) await self._send_str(self._lw_msg) if self._user: await self._send_str(self._user) await self._send_str(self._pswd) - # Await CONNACK - # read causes ECONNABORTED if broker is out; triggers a reconnect. + resp = await self._as_read(4) self.dprint("Connected to broker.") # Got CONNACK - if ( - resp[3] != 0 or resp[0] != 0x20 or resp[1] != 0x02 - ): # Bad CONNACK e.g. authentication fail. - raise OSError( - -1, - f"Connect fail: 0x{(resp[0] << 8) + resp[1]:04x} {resp[3]} (README 7)", - ) + # Bad CONNACK e.g. authentication fail + if resp[3] != 0 or resp[0] != 0x20 or resp[1] != 0x02: + code = f"0x{(resp[0] << 8) + resp[1]:04x} {resp[3]}" + raise OSError(-1, f"Connect fail: {code} (README 7)") async def _ping(self): async with self.lock: await self._as_write(b"\xc0\0") - # Check internet connectivity by sending DNS lookup to Google's 8.8.8.8 async def wan_ok( self, packet=( @@ -339,9 +349,10 @@ async def wan_ok( b"\x00\x01\x00\x01" ), ): - if not self.isconnected(): # WiFi is down + """Check internet connectivity by sending DNS lookup to 8.8.8.8.""" + if not self.isconnected(): # WiFi down return False - length = 32 # DNS query and response packet size + length = 32 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.setblocking(False) s.connect(("8.8.8.8", 53)) @@ -351,14 +362,15 @@ async def wan_ok( await asyncio.sleep(2) res = await self._as_read(length, s) if len(res) == length: - return True # DNS response size OK - except OSError: # Timeout on read: no connectivity. + return True + except OSError: return False finally: s.close() return False - async def broker_up(self): # Test broker connectivity + async def broker_up(self): + """Test if broker is responding.""" if not self.isconnected(): return False tlast = self.last_rx @@ -371,16 +383,17 @@ async def broker_up(self): # Test broker connectivity t = ticks_ms() while not self._timeout(t): await asyncio.sleep_ms(100) - if ticks_diff(self.last_rx, tlast) > 0: # Response received + if ticks_diff(self.last_rx, tlast) > 0: return True return False async def disconnect(self): + """Disconnect from broker, keep socket open for 100ms.""" if self._sock is not None: - await self._kill_tasks(False) # Keep socket open + await self._kill_tasks(False) try: async with self.lock: - self._sock.write(b"\xe0\0") # Close broker connection + self._sock.write(b"\xe0\0") await asyncio.sleep_ms(100) except OSError: pass @@ -391,29 +404,29 @@ def _close(self): if self._sock is not None: self._sock.close() - def close( - self, - ): # API. See https://github.com/peterhinch/micropython-mqtt/issues/60 + def close(self): + """API close. See: + https://github.com/peterhinch/micropython-mqtt/issues/60""" self._close() try: - self._sta_if.disconnect() # Disconnect Wi-Fi to avoid errors + self._sta_if.disconnect() except OSError: self.dprint("Wi-Fi not started, unable to disconnect interface") self._sta_if.active(False) async def _await_pid(self, pid): + """Wait for PID to appear in self.rcv_pids, or time out.""" t = ticks_ms() - while pid in self.rcv_pids: # local copy + while pid in self.rcv_pids: if self._timeout(t) or not self.isconnected(): - break # Must repub or bail out + break await asyncio.sleep_ms(100) else: - return True # PID received. All done. + return True return False - # qos == 1: coro blocks until wait_msg gets correct PID. - # If WiFi fails completely subclass re-publishes with new PID. async def publish(self, topic, msg, retain, qos): + """Publish message with optional QoS 1. Retry if no PUBACK.""" pid = next(self.newpid) if qos: self.rcv_pids.add(pid) @@ -423,18 +436,18 @@ async def publish(self, topic, msg, retain, qos): return count = 0 - while 1: # Await PUBACK, republish on timeout + while True: if await self._await_pid(pid): return - # No match if count >= self._max_repubs or not self.isconnected(): - raise OSError(-1) # Subclass to re-publish with new PID + raise OSError(-1) async with self.lock: - await self._publish(topic, msg, retain, qos, dup=1, pid=pid) # Add pid + await self._publish(topic, msg, retain, qos, 1, pid) count += 1 self.REPUB_COUNT += 1 async def _publish(self, topic, msg, retain, qos, dup, pid): + """Core publish method. Called by publish() with or without DUP.""" pkt = bytearray(b"\x30\0\0\0") pkt[0] |= qos << 1 | retain | dup << 3 sz = 2 + len(topic) + len(msg) @@ -455,8 +468,8 @@ async def _publish(self, topic, msg, retain, qos, dup, pid): await self._as_write(pkt, 2) await self._as_write(msg) - # Can raise OSError if WiFi fails. Subclass traps. async def subscribe(self, topic, qos): + """Subscribe with optional QoS.""" pkt = bytearray(b"\x82\0\0\0") pid = next(self.newpid) self.rcv_pids.add(pid) @@ -465,12 +478,11 @@ async def subscribe(self, topic, qos): await self._as_write(pkt) await self._send_str(topic) await self._as_write(qos.to_bytes(1, "little")) - if not await self._await_pid(pid): raise OSError(-1) - # Can raise OSError if WiFi fails. Subclass traps. async def unsubscribe(self, topic): + """Unsubscribe from topic.""" pkt = bytearray(b"\xa2\0\0\0") pid = next(self.newpid) self.rcv_pids.add(pid) @@ -478,20 +490,15 @@ async def unsubscribe(self, topic): async with self.lock: await self._as_write(pkt) await self._send_str(topic) - if not await self._await_pid(pid): raise OSError(-1) - # Wait for a single incoming MQTT message and process it. - # Subscribed messages are delivered to a callback previously - # set by .setup() method. Other (internal) MQTT - # messages processed internally. - # Immediate return if no data available. Called from ._handle_msg(). async def wait_msg(self): + """Wait for one incoming MQTT message.""" try: res = self._sock.read(1) # Throws OSError on WiFi fail except OSError as e: - if e.args[0] in BUSY_ERRORS: # Needed by RP2 + if e.args[0] in BUSY_ERRORS: await asyncio.sleep_ms(0) return raise @@ -501,11 +508,11 @@ async def wait_msg(self): raise OSError(-1, "Empty response") if res == b"\xd0": # PINGRESP - await self._as_read(1) # Update .last_rx time + await self._as_read(1) return op = res[0] - if op == 0x40: # PUBACK: save pid + if op == 0x40: # PUBACK sz = await self._as_read(1) if sz != b"\x02": raise OSError(-1, "Invalid PUBACK packet") @@ -534,183 +541,168 @@ async def wait_msg(self): else: raise OSError(-1) + # If not Publish, bail out if op & 0xF0 != 0x30: return + sz = await self._recv_len() topic_len = await self._as_read(2) - topic_len = (topic_len[0] << 8) | topic_len[1] - topic = await self._as_read(topic_len) - sz -= topic_len + 2 - if op & 6: - pid = await self._as_read(2) - pid = pid[0] << 8 | pid[1] + t_len = (topic_len[0] << 8) | topic_len[1] + topic = await self._as_read(t_len) + sz -= t_len + 2 + pid = None + if op & 6: # QoS + pid_bytes = await self._as_read(2) + pid = pid_bytes[0] << 8 | pid_bytes[1] sz -= 2 msg = await self._as_read(sz) - retained = op & 0x01 + retained = bool(op & 0x01) + if self._events: - self.queue.put(topic, msg, bool(retained)) + self.queue.put(topic, msg, retained) else: - self._cb(topic, msg, bool(retained)) - if op & 6 == 2: # qos 1 - pkt = bytearray(b"\x40\x02\0\0") # Send PUBACK + self._cb(topic, msg, retained) + + # QoS 1 + if op & 6 == 2: + pkt = bytearray(b"\x40\x02\0\0") struct.pack_into("!H", pkt, 2, pid) await self._as_write(pkt) elif op & 6 == 4: # qos 2 not supported raise OSError(-1, "QoS 2 not supported") -# MQTTClient class. Handles issues relating to connectivity. - - class MQTTClient(MQTT_base): + """Handles issues relating to WiFi connectivity & broker re-connects.""" + def __init__(self, config): super().__init__(config) - self._isconnected = False # Current connection state - keepalive = 1000 * self._keepalive # ms - self._ping_interval = keepalive // 4 if keepalive else 20000 - p_i = ( - config["ping_interval"] * 1000 - ) # Can specify shorter e.g. for subscribe-only + self._isconnected = False + keepalive_ms = 1000 * self._keepalive + self._ping_interval = keepalive_ms // 4 if keepalive_ms else 20000 + p_i = config["ping_interval"] * 1000 if p_i and p_i < self._ping_interval: self._ping_interval = p_i self._in_connect = False - self._has_connected = False # Define 'Clean Session' value to use. + self._has_connected = False self._tasks = [] + if ESP8266: import esp - esp.sleep_type( - 0 - ) # Improve connection integrity at cost of power consumption. + esp.sleep_type(0) # Improve connection integrity at cost of power async def wifi_connect(self, quick=False): + """Connect to WiFi, optionally skipping reliability checks.""" s = self._sta_if if ESP8266: - if s.isconnected(): # 1st attempt, already connected. + if s.isconnected(): return s.active(True) - s.connect() # ESP8266 remembers connection. + s.connect() for _ in range(60): - if ( - s.status() != network.STAT_CONNECTING - ): # Break out on fail or success. Check once per sec. + if s.status() != network.STAT_CONNECTING: break await asyncio.sleep(1) - if ( - s.status() == network.STAT_CONNECTING - ): # might hang forever awaiting dhcp lease renewal or something else + if s.status() == network.STAT_CONNECTING: s.disconnect() await asyncio.sleep(1) - if ( - not s.isconnected() - and self._ssid is not None - and self._wifi_pw is not None - ): + if not s.isconnected() and self._ssid and self._wifi_pw: s.connect(self._ssid, self._wifi_pw) - while ( - s.status() == network.STAT_CONNECTING - ): # Break out on fail or success. Check once per sec. + while s.status() == network.STAT_CONNECTING: await asyncio.sleep(1) else: s.active(True) - if RP2: # Disable auto-sleep. - # para 3.6.3 + if RP2: # Disable auto-sleep s.config(pm=0xA11140) s.connect(self._ssid, self._wifi_pw) - for _ in range(60): # Break out on fail or success. Check once per sec. + for _ in range(60): await asyncio.sleep(1) - # Loop while connecting or no IP if s.isconnected(): break if ESP32: - if s.status() != network.STAT_CONNECTING: # 1001 + if s.status() != network.STAT_CONNECTING: break - elif PYBOARD: # No symbolic constants in network + elif PYBOARD: if not 1 <= s.status() <= 2: break - elif RP2: # 1 is STAT_CONNECTING. 2 reported by user (No IP?) + elif RP2: if not 1 <= s.status() <= 2: break - else: # Timeout: still in connecting state + else: s.disconnect() await asyncio.sleep(1) - if not s.isconnected(): # Timed out + if not s.isconnected(): raise OSError("Wi-Fi connect timed out") - if not quick: # Skip on first connection only if power saving - # Ensure connection stays up for a few secs. + + if not quick: self.dprint("Checking WiFi integrity.") for _ in range(5): if not s.isconnected(): - raise OSError("Connection Unstable") # in 1st 5 secs + raise OSError("Connection Unstable") await asyncio.sleep(1) self.dprint("Got reliable connection") - async def connect( - self, *, quick=False - ): # Quick initial connect option for battery apps + async def connect(self, *, quick=False): + """Connect to MQTT broker, optionally skipping reliability checks.""" if not self._has_connected: - await self.wifi_connect(quick) # On 1st call, caller handles error - # Note this blocks if DNS lookup occurs. Do it once to prevent - # blocking during later internet outage: + await self.wifi_connect(quick) self._addr = socket.getaddrinfo(self.server, self.port)[0][-1] - self._in_connect = True # Disable low level ._isconnected check + + self._in_connect = True try: if not self._has_connected and self._clean_init and not self._clean: - # Power up. Clear previous session data but subsequently save it. - # Issue #40 - await self._connect(True) # Connect with clean session + await self._connect(True) try: async with self.lock: - self._sock.write( - b"\xe0\0" - ) # Force disconnect but keep socket open + self._sock.write(b"\xe0\0") except OSError: pass self.dprint("Waiting for disconnect") - await asyncio.sleep(2) # Wait for broker to disconnect + await asyncio.sleep(2) self.dprint("About to reconnect with unclean session.") + await self._connect(self._clean) + except Exception: self._close() - self._in_connect = False # Caller may run .isconnected() + self._in_connect = False raise + self.rcv_pids.clear() - # If we get here without error broker/LAN must be up. self._isconnected = True - self._in_connect = False # Low level code can now check connectivity. + self._in_connect = False + if not self._events: - asyncio.create_task(self._wifi_handler(True)) # User handler. + asyncio.create_task(self._wifi_handler(True)) if not self._has_connected: - self._has_connected = True # Use normal clean flag on reconnect. + self._has_connected = True asyncio.create_task(self._keep_connected()) - # Runs forever unless user issues .disconnect() - asyncio.create_task(self._handle_msg()) # Task quits on connection fail. + asyncio.create_task(self._handle_msg()) self._tasks.append(asyncio.create_task(self._keep_alive())) if self.DEBUG: self._tasks.append(asyncio.create_task(self._memory())) + if self._events: - self.up.set() # Connectivity is up + self.up.set() else: - asyncio.create_task(self._connect_handler(self)) # User handler. + asyncio.create_task(self._connect_handler(self)) - # Launched by .connect(). Runs until connectivity fails. Checks for and - # handles incoming messages. async def _handle_msg(self): + """Continuously read incoming messages until connectivity fails.""" try: while self.isconnected(): async with self.lock: - await self.wait_msg() # Immediate return if no message - await asyncio.sleep_ms(_DEFAULT_MS) # Let other tasks get lock - + await self.wait_msg() + await asyncio.sleep_ms(_DEFAULT_MS) except OSError: pass - self._reconnect() # Broker or WiFi fail. + self._reconnect() - # Keep broker alive MQTT spec 3.1.2.10 Keep Alive. - # Runs until ping failure or no response in keepalive period. async def _keep_alive(self): + """Send periodic PINGREQs to keep broker connection alive.""" while self.isconnected(): pings_due = ticks_diff(ticks_ms(), self.last_rx) // self._ping_interval if pings_due >= 4: @@ -721,52 +713,54 @@ async def _keep_alive(self): await self._ping() except OSError: break - self._reconnect() # Broker or WiFi fail. + self._reconnect() - async def _kill_tasks(self, kill_skt): # Cancel running tasks + async def _kill_tasks(self, kill_skt): + """Cancel running tasks and optionally close the socket.""" for task in self._tasks: task.cancel() self._tasks.clear() - await asyncio.sleep_ms(0) # Ensure cancellation complete - if kill_skt: # Close socket + await asyncio.sleep_ms(0) + if kill_skt: self._close() - # DEBUG: show RAM messages. async def _memory(self): + """Debug task to show RAM usage periodically.""" while True: await asyncio.sleep(20) gc.collect() self.dprint("RAM free %d alloc %d", gc.mem_free(), gc.mem_alloc()) def isconnected(self): - if self._in_connect: # Disable low-level check during .connect() + """Check if WiFi and broker connection is alive.""" + if self._in_connect: return True - if self._isconnected and not self._sta_if.isconnected(): # It's going down. + if self._isconnected and not self._sta_if.isconnected(): self._reconnect() return self._isconnected - def _reconnect(self): # Schedule a reconnection if not underway. + def _reconnect(self): + """Schedule reconnection if not already in progress.""" if self._isconnected: self._isconnected = False - asyncio.create_task(self._kill_tasks(True)) # Shut down tasks and socket - if self._events: # Signal an outage + asyncio.create_task(self._kill_tasks(True)) + if self._events: self.down.set() else: - asyncio.create_task(self._wifi_handler(False)) # User handler. + asyncio.create_task(self._wifi_handler(False)) - # Await broker connection. async def _connection(self): + """Await broker connection.""" while not self._isconnected: await asyncio.sleep(1) - # Scheduled on 1st successful connection. Runs forever maintaining wifi and - # broker connection. Must handle conditions at edge of WiFi range. async def _keep_connected(self): + """Maintains WiFi & broker connectivity, reconnect if link goes down.""" while self._has_connected: - if self.isconnected(): # Pause for 1 second + if self.isconnected(): await asyncio.sleep(1) gc.collect() - else: # Link is down, socket is closed, tasks are killed + else: try: self._sta_if.disconnect() except OSError: @@ -776,49 +770,47 @@ async def _keep_connected(self): await self.wifi_connect() except OSError: continue - if ( - not self._has_connected - ): # User has issued the terminal .disconnect() + if not self._has_connected: self.dprint("Disconnected, exiting _keep_connected") break try: await self.connect() - # Now has set ._isconnected and scheduled _connect_handler(). self.dprint("Reconnect OK!") except OSError as e: self.dprint("Error in reconnect. %s", e) - # Can get ECONNABORTED or -1. - # The latter signifies no or bad CONNACK received. - self._close() # Disconnect and try again. + self._close() self._in_connect = False self._isconnected = False self.dprint("Disconnected, exited _keep_connected") async def subscribe(self, topic, qos=0): + """Subscribe with QoS 0 or 1.""" qos_check(qos) - while 1: + while True: await self._connection() try: return await super().subscribe(topic, qos) except OSError: pass - self._reconnect() # Broker or WiFi fail. + self._reconnect() async def unsubscribe(self, topic): - while 1: + """Unsubscribe from topic.""" + while True: await self._connection() try: return await super().unsubscribe(topic) except OSError: pass - self._reconnect() # Broker or WiFi fail. + self._reconnect() async def publish(self, topic, msg, retain=False, qos=0): + """Publish with optional retain/QoS.""" qos_check(qos) - while 1: + while True: await self._connection() try: return await super().publish(topic, msg, retain, qos) except OSError: pass - self._reconnect() # Broker or WiFi fail. + self._reconnect() From 10be480d818eeed22c86483efd32daf1c2ef6c09 Mon Sep 17 00:00:00 2001 From: Luthira A <79228258+Luthiraa@users.noreply.github.com> Date: Sat, 22 Mar 2025 17:13:49 -0400 Subject: [PATCH 17/19] Remove unused files and configurations from tempSensor directory --- scripts/tempSensor/.pre-commit-config.yaml | 18 -- .../lib/{LICENSE => LICENSE_bme680.txt} | 0 scripts/tempSensor/pico_id.txt | 1 - scripts/tempSensor/umqtt/robust.py | 44 ---- scripts/tempSensor/umqtt/simple.py | 217 ------------------ 5 files changed, 280 deletions(-) delete mode 100644 scripts/tempSensor/.pre-commit-config.yaml rename scripts/tempSensor/lib/{LICENSE => LICENSE_bme680.txt} (100%) delete mode 100644 scripts/tempSensor/pico_id.txt delete mode 100644 scripts/tempSensor/umqtt/robust.py delete mode 100644 scripts/tempSensor/umqtt/simple.py diff --git a/scripts/tempSensor/.pre-commit-config.yaml b/scripts/tempSensor/.pre-commit-config.yaml deleted file mode 100644 index 4bb444e3..00000000 --- a/scripts/tempSensor/.pre-commit-config.yaml +++ /dev/null @@ -1,18 +0,0 @@ -repos: - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.4.0 - hooks: - - id: trailing-whitespace - - id: end-of-file-fixer - - repo: https://github.com/ambv/black - rev: 21.12b0 - hooks: - - id: black - - repo: https://github.com/PyCQA/isort - rev: 5.9.3 - hooks: - - id: isort - - repo: https://gitlab.com/pycqa/flake8 - rev: 3.9.2 - hooks: - - id: flake8 diff --git a/scripts/tempSensor/lib/LICENSE b/scripts/tempSensor/lib/LICENSE_bme680.txt similarity index 100% rename from scripts/tempSensor/lib/LICENSE rename to scripts/tempSensor/lib/LICENSE_bme680.txt diff --git a/scripts/tempSensor/pico_id.txt b/scripts/tempSensor/pico_id.txt deleted file mode 100644 index ad449ae1..00000000 --- a/scripts/tempSensor/pico_id.txt +++ /dev/null @@ -1 +0,0 @@ -e66130100f594628 diff --git a/scripts/tempSensor/umqtt/robust.py b/scripts/tempSensor/umqtt/robust.py deleted file mode 100644 index 2a2b5629..00000000 --- a/scripts/tempSensor/umqtt/robust.py +++ /dev/null @@ -1,44 +0,0 @@ -import utime - -from . import simple - - -class MQTTClient(simple.MQTTClient): - DELAY = 2 - DEBUG = False - - def delay(self, i): - utime.sleep(self.DELAY) - - def log(self, in_reconnect, e): - if self.DEBUG: - if in_reconnect: - print("mqtt reconnect: %r" % e) - else: - print("mqtt: %r" % e) - - def reconnect(self): - i = 0 - while 1: - try: - return super().connect(False) - except OSError as e: - self.log(True, e) - i += 1 - self.delay(i) - - def publish(self, topic, msg, retain=False, qos=0): - while 1: - try: - return super().publish(topic, msg, retain, qos) - except OSError as e: - self.log(False, e) - self.reconnect() - - def wait_msg(self): - while 1: - try: - return super().wait_msg() - except OSError as e: - self.log(False, e) - self.reconnect() diff --git a/scripts/tempSensor/umqtt/simple.py b/scripts/tempSensor/umqtt/simple.py deleted file mode 100644 index 5d09230c..00000000 --- a/scripts/tempSensor/umqtt/simple.py +++ /dev/null @@ -1,217 +0,0 @@ -import usocket as socket -import ustruct as struct - - -class MQTTException(Exception): - pass - - -class MQTTClient: - def __init__( - self, - client_id, - server, - port=0, - user=None, - password=None, - keepalive=0, - ssl=False, - ssl_params={}, - ): - if port == 0: - port = 8883 if ssl else 1883 - self.client_id = client_id - self.sock = None - self.server = server - self.port = port - self.ssl = ssl - self.ssl_params = ssl_params - self.pid = 0 - self.cb = None - self.user = user - self.pswd = password - self.keepalive = keepalive - self.lw_topic = None - self.lw_msg = None - self.lw_qos = 0 - self.lw_retain = False - - def _send_str(self, s): - self.sock.write(struct.pack("!H", len(s))) - self.sock.write(s) - - def _recv_len(self): - n = 0 - sh = 0 - while 1: - b = self.sock.read(1)[0] - n |= (b & 0x7F) << sh - if not b & 0x80: - return n - sh += 7 - - def set_callback(self, f): - self.cb = f - - def set_last_will(self, topic, msg, retain=False, qos=0): - assert 0 <= qos <= 2 - assert topic - self.lw_topic = topic - self.lw_msg = msg - self.lw_qos = qos - self.lw_retain = retain - - def connect(self, clean_session=True): - self.sock = socket.socket() - addr = socket.getaddrinfo(self.server, self.port)[0][-1] - self.sock.connect(addr) - if self.ssl: - # replaced ussl with ssl due to deprecation in MicroPython 1.23.0 - # (not PR'd on source repo, but I'm using mqtt_as in my workflows - # instead, anyway) - import ssl - - self.sock = ssl.wrap_socket(self.sock, **self.ssl_params) - premsg = bytearray(b"\x10\0\0\0\0\0") - msg = bytearray(b"\x04MQTT\x04\x02\0\0") - - sz = 10 + 2 + len(self.client_id) - msg[6] = clean_session << 1 - if self.user is not None: - sz += 2 + len(self.user) + 2 + len(self.pswd) - msg[6] |= 0xC0 - if self.keepalive: - assert self.keepalive < 65536 - msg[7] |= self.keepalive >> 8 - msg[8] |= self.keepalive & 0x00FF - if self.lw_topic: - sz += 2 + len(self.lw_topic) + 2 + len(self.lw_msg) - msg[6] |= 0x4 | (self.lw_qos & 0x1) << 3 | (self.lw_qos & 0x2) << 3 - msg[6] |= self.lw_retain << 5 - - i = 1 - while sz > 0x7F: - premsg[i] = (sz & 0x7F) | 0x80 - sz >>= 7 - i += 1 - premsg[i] = sz - - self.sock.write(premsg, i + 2) - self.sock.write(msg) - # print(hex(len(msg)), hexlify(msg, ":")) - self._send_str(self.client_id) - if self.lw_topic: - self._send_str(self.lw_topic) - self._send_str(self.lw_msg) - if self.user is not None: - self._send_str(self.user) - self._send_str(self.pswd) - resp = self.sock.read(4) - assert resp[0] == 0x20 and resp[1] == 0x02 - if resp[3] != 0: - raise MQTTException(resp[3]) - return resp[2] & 1 - - def disconnect(self): - self.sock.write(b"\xe0\0") - self.sock.close() - - def ping(self): - self.sock.write(b"\xc0\0") - - def publish(self, topic, msg, retain=False, qos=0): - pkt = bytearray(b"\x30\0\0\0") - pkt[0] |= qos << 1 | retain - sz = 2 + len(topic) + len(msg) - if qos > 0: - sz += 2 - assert sz < 2097152 - i = 1 - while sz > 0x7F: - pkt[i] = (sz & 0x7F) | 0x80 - sz >>= 7 - i += 1 - pkt[i] = sz - # print(hex(len(pkt)), hexlify(pkt, ":")) - self.sock.write(pkt, i + 1) - self._send_str(topic) - if qos > 0: - self.pid += 1 - pid = self.pid - struct.pack_into("!H", pkt, 0, pid) - self.sock.write(pkt, 2) - self.sock.write(msg) - if qos == 1: - while 1: - op = self.wait_msg() - if op == 0x40: - sz = self.sock.read(1) - assert sz == b"\x02" - rcv_pid = self.sock.read(2) - rcv_pid = rcv_pid[0] << 8 | rcv_pid[1] - if pid == rcv_pid: - return - elif qos == 2: - assert 0 - - def subscribe(self, topic, qos=0): - assert self.cb is not None, "Subscribe callback is not set" - pkt = bytearray(b"\x82\0\0\0") - self.pid += 1 - struct.pack_into("!BH", pkt, 1, 2 + 2 + len(topic) + 1, self.pid) - # print(hex(len(pkt)), hexlify(pkt, ":")) - self.sock.write(pkt) - self._send_str(topic) - self.sock.write(qos.to_bytes(1, "little")) - while 1: - op = self.wait_msg() - if op == 0x90: - resp = self.sock.read(4) - # print(resp) - assert resp[1] == pkt[2] and resp[2] == pkt[3] - if resp[3] == 0x80: - raise MQTTException(resp[3]) - return - - # Wait for a single incoming MQTT message and process it. - # Subscribed messages are delivered to a callback previously - # set by .set_callback() method. Other (internal) MQTT - # messages processed internally. - def wait_msg(self): - res = self.sock.read(1) - self.sock.setblocking(True) - if res is None: - return None - if res == b"": - raise OSError(-1) - if res == b"\xd0": # PINGRESP - sz = self.sock.read(1)[0] - assert sz == 0 - return None - op = res[0] - if op & 0xF0 != 0x30: - return op - sz = self._recv_len() - topic_len = self.sock.read(2) - topic_len = (topic_len[0] << 8) | topic_len[1] - topic = self.sock.read(topic_len) - sz -= topic_len + 2 - if op & 6: - pid = self.sock.read(2) - pid = pid[0] << 8 | pid[1] - sz -= 2 - msg = self.sock.read(sz) - self.cb(topic, msg) - if op & 6 == 2: - pkt = bytearray(b"\x40\x02\0\0") - struct.pack_into("!H", pkt, 2, pid) - self.sock.write(pkt) - elif op & 6 == 4: - assert 0 - - # Checks whether a pending message from server is available. - # If not, returns immediately with None. Otherwise, does - # the same processing as wait_msg. - def check_msg(self): - self.sock.setblocking(False) - return self.wait_msg() From 79bf3f8bf76de7269e0d6f482f5dc08f036f785c Mon Sep 17 00:00:00 2001 From: Luthira A <79228258+Luthiraa@users.noreply.github.com> Date: Mon, 24 Mar 2025 20:38:05 -0400 Subject: [PATCH 18/19] Refactor tempSensor main.py to use mqtt_as for MQTT connection and add async data publishing; add .gitignore for my_secrets.py --- scripts/tempSensor/.gitignore | 1 + scripts/tempSensor/main.py | 118 ++++++++++++++-------------------- 2 files changed, 48 insertions(+), 71 deletions(-) create mode 100644 scripts/tempSensor/.gitignore diff --git a/scripts/tempSensor/.gitignore b/scripts/tempSensor/.gitignore new file mode 100644 index 00000000..2051cad2 --- /dev/null +++ b/scripts/tempSensor/.gitignore @@ -0,0 +1 @@ +my_secrets.py \ No newline at end of file diff --git a/scripts/tempSensor/main.py b/scripts/tempSensor/main.py index 6a745ec5..ea97523f 100644 --- a/scripts/tempSensor/main.py +++ b/scripts/tempSensor/main.py @@ -1,83 +1,59 @@ +import uasyncio as asyncio import json import time - -from bme680 import BME680_I2C from machine import I2C, Pin, reset -from netman import connectWiFi -from umqtt.simple import MQTTClient - -# Configuration -SSID = "Pixel 8" -PASSWORD = "123456789" -MQTT_BROKER = b"b6bdb89571144b3d8e5ca4bbe666ddb5.s1.eu.hivemq.cloud" -MQTT_PORT = 8883 -MQTT_TOPIC = b"sensors/bme680/data" -MQTT_USER = b"Luthiraa" -MQTT_PASS = b"theboss1010" - -# Initialize I2C and Sensor +from bme680 import BME680_I2C +import mqtt_as +from my_secrets import SSID, PASSWORD, MQTT_BROKER, MQTT_PORT, MQTT_TOPIC, MQTT_USER, MQTT_PASS + +# configure mqtt_as settings +mqtt_as.config["ssid"] = SSID +mqtt_as.config["wifi_pw"] = PASSWORD +mqtt_as.config["server"] = MQTT_BROKER +mqtt_as.config["port"] = MQTT_PORT +mqtt_as.config["user"] = MQTT_USER +mqtt_as.config["password"] = MQTT_PASS +mqtt_as.config["client_id"] = "pico_w" +mqtt_as.config["ssl"] = True +mqtt_as.config["ssl_params"] = {"server_hostname": MQTT_BROKER} +mqtt_as.config["keepalive"] = 60 +mqtt_as.config["clean"] = True + +# init I2C and the BME680 sensor i2c = I2C(0, scl=Pin(5), sda=Pin(4)) bme = BME680_I2C(i2c) - -def connect_to_wifi(): - """Connects to Wi-Fi, restarts on failure.""" - try: - status = connectWiFi(SSID, PASSWORD, country="US", retries=5) - print("Wi-Fi connected! IP:", status[0]) - except Exception as e: - print(f"Wi-Fi error: {e}") - time.sleep(5) - reset() - - -def connect_to_mqtt(): - """Connects to MQTT broker, restarts on failure.""" - client = MQTTClient( - client_id=b"pico_w", - server=MQTT_BROKER, - port=MQTT_PORT, - user=MQTT_USER, - password=MQTT_PASS, - keepalive=60, - ssl=True, - ssl_params={"server_hostname": MQTT_BROKER}, - ) - try: - client.connect() - print("Connected to MQTT broker!") - return client - except Exception as e: - print(f"MQTT error: {e}") - time.sleep(5) - reset() - - -def main(): - connect_to_wifi() - mqtt_client = connect_to_mqtt() - +async def pub_sensor_data(client): + """Periodically publishes sensor data as a JSON payload.""" while True: try: - # Create JSON payload - payload = json.dumps( - { - "temperature": round(bme.temperature, 2), - "humidity": round(bme.humidity, 2), - "pressure": round(bme.pressure, 2), - "gas": round(bme.gas, 2), - } - ) - - mqtt_client.publish(MQTT_TOPIC, payload) + payload = json.dumps({ + "temperature": round(bme.temperature, 2), + "humidity": round(bme.humidity, 2), + "pressure": round(bme.pressure, 2), + "gas": round(bme.gas, 2) + }) + await client.publish(MQTT_TOPIC, payload) print("Published:", payload) - time.sleep(2) - except Exception as e: - print(f"Runtime error: {e}") + print("Runtime error:", e) time.sleep(5) - reset() - + reset() + await asyncio.sleep(2) + +async def main(): + client = mqtt_as.MQTTClient() + await client.connect() + print("Connected to MQTT broker!") + + # Start the sensor data publishing task + asyncio.create_task(pub_sensor_data(client)) + + # Keep the main loop running + while True: + await asyncio.sleep(1) -if __name__ == "__main__": - main() +try: + asyncio.run(main()) +finally: + asyncio.new_event_loop() From 9a2fbe4dfac0762d70e8afd0a250ad33706d6fc0 Mon Sep 17 00:00:00 2001 From: Luthira A <79228258+Luthiraa@users.noreply.github.com> Date: Mon, 24 Mar 2025 20:43:49 -0400 Subject: [PATCH 19/19] Apply pre-commit formatting fixes --- scripts/tempSensor/.gitignore | 2 +- scripts/tempSensor/main.py | 46 +++++++++++++++++++++++------------ 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/scripts/tempSensor/.gitignore b/scripts/tempSensor/.gitignore index 2051cad2..baa3c35b 100644 --- a/scripts/tempSensor/.gitignore +++ b/scripts/tempSensor/.gitignore @@ -1 +1 @@ -my_secrets.py \ No newline at end of file +my_secrets.py diff --git a/scripts/tempSensor/main.py b/scripts/tempSensor/main.py index ea97523f..8bd17d13 100644 --- a/scripts/tempSensor/main.py +++ b/scripts/tempSensor/main.py @@ -1,12 +1,21 @@ -import uasyncio as asyncio import json import time -from machine import I2C, Pin, reset -from bme680 import BME680_I2C + import mqtt_as -from my_secrets import SSID, PASSWORD, MQTT_BROKER, MQTT_PORT, MQTT_TOPIC, MQTT_USER, MQTT_PASS +import uasyncio as asyncio +from bme680 import BME680_I2C +from machine import I2C, Pin, reset +from my_secrets import ( + MQTT_BROKER, + MQTT_PASS, + MQTT_PORT, + MQTT_TOPIC, + MQTT_USER, + PASSWORD, + SSID, +) -# configure mqtt_as settings +# configure mqtt_as settings mqtt_as.config["ssid"] = SSID mqtt_as.config["wifi_pw"] = PASSWORD mqtt_as.config["server"] = MQTT_BROKER @@ -17,43 +26,48 @@ mqtt_as.config["ssl"] = True mqtt_as.config["ssl_params"] = {"server_hostname": MQTT_BROKER} mqtt_as.config["keepalive"] = 60 -mqtt_as.config["clean"] = True +mqtt_as.config["clean"] = True # init I2C and the BME680 sensor i2c = I2C(0, scl=Pin(5), sda=Pin(4)) bme = BME680_I2C(i2c) + async def pub_sensor_data(client): """Periodically publishes sensor data as a JSON payload.""" while True: try: - payload = json.dumps({ - "temperature": round(bme.temperature, 2), - "humidity": round(bme.humidity, 2), - "pressure": round(bme.pressure, 2), - "gas": round(bme.gas, 2) - }) + payload = json.dumps( + { + "temperature": round(bme.temperature, 2), + "humidity": round(bme.humidity, 2), + "pressure": round(bme.pressure, 2), + "gas": round(bme.gas, 2), + } + ) await client.publish(MQTT_TOPIC, payload) print("Published:", payload) except Exception as e: print("Runtime error:", e) time.sleep(5) - reset() + reset() await asyncio.sleep(2) + async def main(): client = mqtt_as.MQTTClient() await client.connect() print("Connected to MQTT broker!") - + # Start the sensor data publishing task asyncio.create_task(pub_sensor_data(client)) - + # Keep the main loop running while True: await asyncio.sleep(1) + try: asyncio.run(main()) finally: - asyncio.new_event_loop() + asyncio.new_event_loop()