From 3b2c0a4cb46c10835f533474337f040d9ee22d44 Mon Sep 17 00:00:00 2001 From: Filip Darmanovic Date: Sat, 5 Nov 2022 23:17:06 +0100 Subject: [PATCH 1/4] Added support for non-i2c iio devices --- README.md | 6 +++- idetect.py | 76 ++++++++++++++++++++++++++++++++++++++----------- information.py | 4 +-- reading.py | 11 ++++--- sensor.py | 26 +++++++++-------- transformers.py | 40 +++++++++++++------------- 6 files changed, 106 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index 2fd55de..1ecbc59 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,11 @@ This command searches the running kernel for all the drivers it includes and pri | TSL4531 | TAOS TSL4531 ambient light sensors | tsl4531 | 0x29 | Not tested | | VEML6070 | VEML6070 UV A light sensor | veml6070 | 0x38, 0x39 | Yes, works | -By default, the block searches for sensors on SMBus number 1 (/dev/i2c-1) however you can set the bus number (an integer value) using the `BUS_NUMBER` service variable. +There are different sensor search modes currently supported, and they are set using the `DETECT_SENSORS` service variable: +- `I2C` (default) - Searches only the SMBus + By default, the block searches for sensors on SMBus number 1 (/dev/i2c-1) however you can set the bus number (an integer value) using the `BUS_NUMBER` service variable. +- `IIO_STRICT` - Skips the SMBus search and compares the names of all available iio devices to a list of supported ones. +- `IIO` - Accepts every available iio device, even unverified ones. ### Publishing Data diff --git a/idetect.py b/idetect.py index a977482..2b3a4bd 100644 --- a/idetect.py +++ b/idetect.py @@ -5,11 +5,12 @@ import errno import os import iio -import json import time +import iio +from typing import Set, Dict # Decimal address values -devices = { +i2c_devices: Dict[int,str] = { 12: "ad5446", 13: "ad5446", 14: "ad5446", @@ -38,12 +39,18 @@ 119: "multiple" } +# List of supported iio devices (superset of i2c devices) +iio_device_names: Set[str] = { + "dht11", # Also applies to the DHT22 + *set(i2c_devices.values()) +} + del_drivers = { "ad5446.c": "ad5446.c", "apds9960": "apds9960", "bme680": "bme680-i2c", "bmp280": "bmp280-i2c", -"dht11": "dht11", +#"dht11": "dht11", "hdc100": "hdc100", "htu21": "htu21", "mcp320x": "mcp320x", @@ -76,7 +83,7 @@ def read_chip_id(bus, device, loc): return chip_id -def detect_iio_sensors(): +def detect_i2c_sensors(): bus_number = int(os.getenv('BUS_NUMBER', '1')) # default 1 indicates /dev/i2c-1 bus = SMBus(bus_number) device_count = 0 @@ -111,7 +118,7 @@ def detect_iio_sensors(): print("Found device (via i2cdetect) at 0x40") if device_count > 0: # We want to remove any existing devices if they are present - # First unintatntiate the i2c bus + # First uninstantiate the i2c bus print("======== Removing existing devices from the i2c bus... ========") for device in active: print("Deleting device found at {0}.".format(hex(device))) @@ -123,7 +130,7 @@ def detect_iio_sensors(): print("======== Unloading any existing modules... ========") # Next unload any present devices using modprobe -rv output = subprocess.check_output("lsmod").decode() # TODO: replace check_output with run variant - d = [] + active_modules = set() i = 0 for line in output.split('\n'): i = i + 1 @@ -136,13 +143,10 @@ def detect_iio_sensors(): mod_name = lsmod_module[0][0:find_underscore] # strip underscore and everything following else: mod_name = lsmod_module[0] - d.append(mod_name) - # remove duplicates from list - dd = [] - [dd.append(x) for x in d if x not in dd] + active_modules.add(mod_name) # find in dict #print("Currently loaded modules: {0}".format(dd)) - for x in dd: + for x in active_modules: if x in del_drivers: print("Unloading module {0} as {1}.".format(x, del_drivers[x])) subprocess.run(["modprobe", "-r", x]) @@ -152,7 +156,7 @@ def detect_iio_sensors(): #print("Keys: {0}".format(devices.keys())) new_active = [] for x in active: - if x in devices.keys(): + if x in i2c_devices.keys(): new_active.append(x) else: print("Device at {0} not in known supported drivers.".format(hex(x))) @@ -165,10 +169,10 @@ def detect_iio_sensors(): subprocess.run(["modprobe", "industrialio"]) new_active_count = 0 for device in new_active: - if devices[device] != "multiple": - print("Loading device {0} on address {1}.".format(devices[device], hex(device))) - subprocess.run(["modprobe", devices[device]]) - new_device = "echo {0} {1} > /sys/bus/i2c/devices/i2c-1/new_device".format(devices[device], hex(device)) + if i2c_devices[device] != "multiple": + print("Loading device {0} on address {1}.".format(i2c_devices[device], hex(device))) + subprocess.run(["modprobe", i2c_devices[device]]) + new_device = "echo {0} {1} > /sys/bus/i2c/devices/i2c-1/new_device".format(i2c_devices[device], hex(device)) os_out = os.system(new_device) if os_out > 0: print("New device {0} exit code: {1}".format(hex(device), os_out)) @@ -221,3 +225,43 @@ def detect_iio_sensors(): bus = None return 0 +def detect_iio_sensors( + strict: bool, + context: iio.Context, + supported_devices: Set[str] = iio_device_names +) -> int: + """Fetches number of iio (supported) devices inside the iio context. + + Args: + strict (bool): Return only the number of detected devices whose names are in supported_devices + context (iio.Context): iio Context + supported_devices(Set[str]): Names of supported devices + + Returns: + int: Nr of (supported) iio devices detected. + """ + + nr_devices = 0 + + if strict: + for device in context.devices: + if device.name in supported_devices: + nr_devices += 1 + else: + nr_devices = len(context.devices) + + return nr_devices + +def detect_sensors(context: iio.Context) -> int: + detection_mode = os.getenv("DETECT_SENSORS", "I2C") + print("Sensor detection mode: {0}".format(detection_mode)) + + if detection_mode == "I2C": + return detect_i2c_sensors() + elif detection_mode == "IIO_STRICT": + return detect_iio_sensors(True, context) + elif detection_mode == "IIO": + return detect_iio_sensors(False, context) + else: + print("Unknown value for env variable DETECT_SENSORS: {0}".format(detection_mode)) + return 0 diff --git a/information.py b/information.py index f7bb6b6..4cbf000 100644 --- a/information.py +++ b/information.py @@ -26,8 +26,8 @@ def __init__(self, context): """ self.context = context - def write_information(self): - """Write the information about the current context.""" + def print_information(self): + """Print the information about the current context.""" self._context_info() def _context_info(self): diff --git a/reading.py b/reading.py index 572b2d7..8aa291c 100644 --- a/reading.py +++ b/reading.py @@ -12,7 +12,7 @@ class Reading: """Class for retrieving readings from devices.""" - def __init__(self, context): + def __init__(self, context: iio.Context): """ Class constructor. Args: @@ -69,7 +69,7 @@ def write_reading(self): return reading - def _device_read(self, dev): + def _device_read(self, dev: iio.Device): reads = {} for channel in dev.channels: @@ -87,7 +87,7 @@ def _device_read(self, dev): return reads @staticmethod - def _channel_attribute_value(channel, channel_attr): + def _channel_attribute_value(channel: iio.Channel, channel_attr: str): v = 0 try: v = channel.attrs[channel_attr].value @@ -97,10 +97,9 @@ def _channel_attribute_value(channel, channel_attr): return v class IIO_READER: - data = None - + def __init__(self): - print("initializing reading") + print("Initializing IIO reader...") def get_readings(self, context): reading = Reading(context) diff --git a/sensor.py b/sensor.py index db1e8b6..dcb3732 100644 --- a/sensor.py +++ b/sensor.py @@ -46,17 +46,17 @@ class balenaSense(): def __init__(self): print("Initializing sensors...") + self.context = _create_context() + self.sensor = IIO_READER() + # Print the iio info + information = Information(self.context) + information.print_information() + # First, use iio to detect supported sensors - self.device_count = idetect.detect_iio_sensors() + self.device_count = idetect.detect_sensors(self.context) if self.device_count > 0: self.readfrom = "iio_sensors" - self.context = _create_context() - self.sensor = IIO_READER() - # Print the iio info - information = Information(self.context) - information.write_information() - # More sensor types can be added here # make sure to change the value of self.readfrom @@ -103,6 +103,8 @@ def background_web(server_socket): use_httpserver = os.getenv('ALWAYS_USE_HTTPSERVER', 0) publish_interval = os.getenv('MQTT_PUB_INTERVAL', '8') publish_topic = os.getenv('MQTT_PUB_TOPIC', 'sensors') + mqtt_client = None + balenasense = None try: interval = float(publish_interval) @@ -122,15 +124,15 @@ def background_web(server_socket): if mqtt_address != "none": print("Starting mqtt client, publishing to {0}:1883".format(mqtt_address)) print("Using MQTT publish interval: {0} sec(s)".format(interval)) - client = mqtt.Client() + mqtt_client = mqtt.Client() try: - client.connect(mqtt_address, 1883, 60) + mqtt_client.connect(mqtt_address, 1883, 60) except Exception as e: print("Error connecting to mqtt. ({0})".format(str(e))) mqtt_address = "none" enable_httpserver = "True" else: - client.loop_start() + mqtt_client.loop_start() balenasense = balenaSense() else: enable_httpserver = "True" @@ -150,6 +152,6 @@ def background_web(server_socket): t.start() while True: - if mqtt_address != "none": - client.publish(publish_topic, json.dumps(balenasense.sample())) + if mqtt_client is not None and balenasense is not None: + mqtt_client.publish(publish_topic, json.dumps(balenasense.sample())) time.sleep(interval) diff --git a/transformers.py b/transformers.py index f2253a2..0930346 100644 --- a/transformers.py +++ b/transformers.py @@ -1,6 +1,6 @@ import os -def device_transform(device_name, fields): +def device_transform(device_name: str, fields: dict) -> dict: # Create a dict copy to work on... # So we're not iterating a changing dict @@ -12,11 +12,11 @@ def device_transform(device_name, fields): if field == "humidityrelative": new_fields["humidity"] = new_fields.pop("humidityrelative") elif field == "temp": - x = fields[field] + val = fields[field] if os.getenv('TEMP_UNIT', 'C') == 'F': - new_fields[field]= ((x/1000) * 1.8) + 32 + new_fields[field]= ((val/1000) * 1.8) + 32 else: - new_fields[field] = x/1000 + new_fields[field] = val/1000 new_fields["temperature"] = new_fields.pop("temp") elif device_name == "bme280": @@ -26,43 +26,43 @@ def device_transform(device_name, fields): new_fields[field] = fields[field]/1000 new_fields["humidity"] = new_fields.pop("humidityrelative") elif field == "temp": - x = fields[field] + val = fields[field] if os.getenv('TEMP_UNIT', 'C') == 'F': - new_fields[field]= ((x/1000) * 1.8) + 32 + new_fields[field]= ((val/1000) * 1.8) + 32 else: - new_fields[field] = x/1000 + new_fields[field] = val/1000 new_fields["temperature"] = new_fields.pop("temp") elif field == "pressure": - x = fields[field] - new_fields[field] = x * 10 + val = fields[field] + new_fields[field] = val * 10 elif device_name == "bmp280": print("Transforming {0} value(s)...".format(device_name)) for field in fields: if field == "temp": - x = fields[field] + val = fields[field] if os.getenv('TEMP_UNIT', 'C') == 'F': - new_fields[field] = ((x/1000) * 1.8) + 32 + new_fields[field] = ((val/1000) * 1.8) + 32 else: - new_fields[field] = x/1000 + new_fields[field] = val/1000 new_fields["temperature"] = new_fields.pop("temp") elif field == "pressure": - x = fields[field] - new_fields[field] = x * 10 + val = fields[field] + new_fields[field] = val * 10 - elif device_name == "htu21": + elif device_name == "htu21" or device_name == "dht11": print("Transforming {0} value(s)...".format(device_name)) for field in fields: if field == "temp": - x = fields[field] + val = fields[field] if os.getenv('TEMP_UNIT', 'C') == 'F': - new_fields[field]= ((x/1000) * 1.8) + 32 + new_fields[field]= ((val/1000) * 1.8) + 32 else: - new_fields[field] = x/1000 + new_fields[field] = val/1000 new_fields["temperature"] = new_fields.pop("temp") if field == "humidityrelative": - x = fields[field] - new_fields[field] = x/1000 + val = fields[field] + new_fields[field] = val/1000 new_fields["humidity"] = new_fields.pop("humidityrelative") return new_fields From f59692613c45f6495cd23850cf5996c6309d924e Mon Sep 17 00:00:00 2001 From: Filip Darmanovic Date: Sun, 6 Nov 2022 20:18:21 +0100 Subject: [PATCH 2/4] Handling @ symbol in iio device names --- idetect.py | 2 +- transformers.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/idetect.py b/idetect.py index 2b3a4bd..ec17a67 100644 --- a/idetect.py +++ b/idetect.py @@ -245,7 +245,7 @@ def detect_iio_sensors( if strict: for device in context.devices: - if device.name in supported_devices: + if device.name.split('@')[0] in supported_devices: nr_devices += 1 else: nr_devices = len(context.devices) diff --git a/transformers.py b/transformers.py index 0930346..9dbc671 100644 --- a/transformers.py +++ b/transformers.py @@ -50,7 +50,7 @@ def device_transform(device_name: str, fields: dict) -> dict: val = fields[field] new_fields[field] = val * 10 - elif device_name == "htu21" or device_name == "dht11": + elif device_name == "htu21" or "dht11" in device_name: print("Transforming {0} value(s)...".format(device_name)) for field in fields: if field == "temp": From a6d7440f02cbd361b669bd96ec6564579a12de2b Mon Sep 17 00:00:00 2001 From: Filip Darmanovic Date: Sun, 6 Nov 2022 22:55:51 +0100 Subject: [PATCH 3/4] Better MQTT error handling --- sensor.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/sensor.py b/sensor.py index dcb3732..91ddd2d 100644 --- a/sensor.py +++ b/sensor.py @@ -29,17 +29,16 @@ def mqtt_detect(): try: r = requests.get(url).json() - except Exception as e: - print("Error looking for MQTT service: {0}".format(str(e))) - return False - else: services = r[app_name]['services'].keys() + print("Supervisor response: {0}".format(str(r))) if "mqtt" in services: return True else: return False - + except Exception as e: + print("Error looking for MQTT service: {0}".format(str(e))) + return False class balenaSense(): readfrom = 'unset' @@ -118,7 +117,7 @@ def background_web(server_socket): enable_httpserver = "False" - if mqtt_detect() and mqtt_address == "none": + if mqtt_address == "none" and mqtt_detect(): mqtt_address = "mqtt" if mqtt_address != "none": From 2e907718a5b2965449ef138b1afac4f3e1481a9b Mon Sep 17 00:00:00 2001 From: Filip Darmanovic Date: Mon, 7 Nov 2022 00:30:43 +0100 Subject: [PATCH 4/4] Updated README with the DHT11 --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 1ecbc59..41c65bf 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,10 @@ This command searches the running kernel for all the drivers it includes and pri | ADS1015 | Texas Instruments ADS1015 ADC | ti-ads1015 | 0x48 - 0x4B | Yes, NOT working | | TSL4531 | TAOS TSL4531 ambient light sensors | tsl4531 | 0x29 | Not tested | | VEML6070 | VEML6070 UV A light sensor | veml6070 | 0x38, 0x39 | Yes, works | +| DHT11 | DHT11/DHT22/AM2302 | dht11 | N/A | Yes, works (with overlay) | + +_"With overlay" means that the device tree overlay has to be enabled at boot time. This is done using the "Define DT overlays" option in the device configuration page on balenaCloud."_ +_Devices with no address are not available on the SMbus and can only be detected in `IIO*` modes_ There are different sensor search modes currently supported, and they are set using the `DETECT_SENSORS` service variable: - `I2C` (default) - Searches only the SMBus