Source code for QC.SCAN

  1"""I2C multiplexer scanner for detecting sensors across all MUX channels.
  2
  3Iterates through all four I2C MUX channels, runs ``i2cdetect`` on each,
  4and cross-references discovered addresses against a known sensor map
  5(:obj:`sensorList`) to produce a summary table showing sensor names,
  6expected vs. actual channels, and detection status. This is primarily a
  7factory-floor diagnostic helper used to confirm that every I2C peripheral
  8on an Oizom mainboard is responding on the correct MUX leg.
  9
 10Example:
 11    Run directly from the project root to perform a full bus sweep::
 12
 13        $ python -m QC.SCAN  # doctest: +SKIP
 14
 15Note:
 16    Requires the ``i2cdetect`` utility (``i2c-tools``) and read access
 17    to ``/dev/i2c-*``. Switching MUX channels uses
 18    :meth:`drivers.gpio.gpio.select_I2C`, which drives MUX-select GPIOs
 19    on the host Raspberry Pi.
 20"""
 21
 22import subprocess
 23
 24from drivers.gpio import gpio
 25
 26sensorList = {
 27    "0x29_1": "0x29 - Light Sensor(TSL2591)",
 28    "0x31_0": "0x31 - Co2(ELT)",
 29    "0x36_3": "0x36 - Battery",
 30    "0x41_2": "0x41 - ATHP SELECTOR",
 31    "0x44_2": "0x44 - ATH/ATHP/SHT",
 32    "0x53_1": "0x53 - UV Sensor(LTR390)",
 33    "0x60_1": "0x60 - OLD UV(SI1147)",
 34    "0x60_2": "0x60 - ATHP Pressure",
 35    "0x61_0": "0x61 - 4-20mA Current",
 36    "0x62_0": "0x62 - CO2(Sensirion)",
 37    "0x76_0": "0x76 - Box Temp(BME280)",
 38    "0x77_0": "0x77 - Box Temp(BME280)",
 39}
 40
 41
[docs] 42def detect_i2c_addresses(bus_number: int = 0) -> list[int]: 43 """Detect all responding I2C addresses on the specified bus. 44 45 Shells out to ``i2cdetect -y <bus_number>`` and parses the resulting 46 grid into a list of integers. Cells marked ``--`` are skipped, and 47 only acknowledged addresses are returned. 48 49 Args: 50 bus_number: I2C bus number passed to ``i2cdetect -y``. Defaults 51 to ``0`` which corresponds to ``/dev/i2c-0`` on the Pi. 52 53 Returns: 54 List of integer I2C addresses (7-bit) that acknowledged during 55 the scan. Returns an empty list if ``i2cdetect`` fails. 56 57 Raises: 58 subprocess.CalledProcessError: Caught internally and logged; the 59 function returns ``[]`` in that case rather than re-raising. 60 61 Example: 62 Sweep bus 0 and print the discovered addresses in hex:: 63 64 >>> for addr in detect_i2c_addresses(0): # doctest: +SKIP 65 ... print(hex(addr)) 66 67 Note: 68 Requires ``i2c-tools`` to be installed on the host. The current 69 process must also have permission to read ``/dev/i2c-<bus>``, 70 which usually means membership in the ``i2c`` group or running 71 as root. 72 """ 73 try: 74 output = subprocess.check_output(["i2cdetect", "-y", str(bus_number)], universal_newlines=True) 75 addresses = [] 76 lines = output.split("\n")[1:] # Skip the header row 77 for line in lines: 78 parts = line.split() 79 if parts: 80 for part in parts[1:]: # Skip the first column which is the row header 81 if part != "--": 82 addresses.append(int(part, 16)) 83 return addresses 84 except subprocess.CalledProcessError as e: 85 print(f"An error occurred while detecting I2C addresses: {e}") 86 return []
87 88
[docs] 89def scan_i2c_mux() -> None: 90 """Scan all four I2C MUX channels and print a sensor detection report. 91 92 For each of the four MUX channels (0-3) this routine switches the 93 multiplexer via :meth:`drivers.gpio.gpio.select_I2C`, calls 94 :func:`detect_i2c_addresses` to enumerate live peripherals, and 95 finally compares the results against :obj:`sensorList`. A formatted 96 plain-text table is printed to stdout summarising channel, address, 97 sensor name, and a ``Status`` column that flags expected hits, 98 relocations, and unknown devices. 99 100 Returns: 101 None. Output is rendered to stdout for human inspection. 102 103 Raises: 104 OSError: If switching MUX channels via GPIO fails (propagated 105 from :class:`drivers.gpio.gpio`). 106 107 Example: 108 Run a full scan from a Python REPL on the device:: 109 110 >>> scan_i2c_mux() # doctest: +SKIP 111 112 Note: 113 This function is hardware-only and will not produce meaningful 114 output when executed on a development workstation. Must be run 115 with sufficient privileges to manipulate GPIO and the I2C bus. 116 """ 117 # Create a mapping from address to expected channel and sensor name 118 address_to_info = {} 119 for key, value in sensorList.items(): 120 addr, ch = key.split("_") 121 address_to_info[addr] = {"channel": int(ch), "name": value} 122 123 all_addresses = {} 124 for channel in range(4): 125 gpio.select_I2C(channel) 126 addresses = detect_i2c_addresses() 127 all_addresses[channel] = addresses 128 129 # Collect data for table 130 table_data = [] 131 for channel in range(4): 132 addresses = all_addresses[channel] 133 for num in addresses: 134 addr_hex = hex(num) 135 key = f"{addr_hex}_{channel}" 136 if key in sensorList: 137 status = "OK" 138 name = sensorList[key].split(" - ", 1)[1] if " - " in sensorList[key] else sensorList[key] 139 elif addr_hex in address_to_info: 140 expected_ch = address_to_info[addr_hex]["channel"] 141 name = ( 142 address_to_info[addr_hex]["name"].split(" - ", 1)[1] 143 if " - " in address_to_info[addr_hex]["name"] 144 else address_to_info[addr_hex]["name"] 145 ) 146 status = f"Position changed from {expected_ch} to {channel}" 147 else: 148 name = f"Unknown sensor at {addr_hex}" 149 status = "Unknown" 150 table_data.append([channel, addr_hex, name, status]) 151 152 # Print table header 153 print("\nI2C Mux Scan Results") 154 print("=" * 80) 155 print(f"{'Channel':<8} | {'Address':<8} | {'Sensor':<30} | {'Status'}") 156 print("-" * 80) 157 158 # Print table rows 159 current_channel = 0 160 for row in table_data: 161 channel, addr, sensor, status = row 162 if current_channel != channel: 163 print("-" * 80) 164 current_channel = channel 165 print(f"{channel:<8} | {addr:<8} | {sensor:<30} | {status}") 166 167 print("=" * 80) 168 print("Scan complete.\n")
169 170 171if __name__ == "__main__": 172 scan_i2c_mux()