# IR sensor driver
# by Kai De La Cruz & Kenzie Goldman 2/17/25
from pyb import Pin, Timer, ADC import time
class QTR_HD_15A:
def __init__(self, adc_pins, num_sensors, power_odd, power_even):
"""Initialize the QTR-HD-15A reflectance sensor array.
:param adc_pins: List of ADC-capable pin names (e.g., \['X1', 'X2',
...\]) :param num_sensors: Number of sensors in the array (default
15)."""
if len(adc_pins) != num_sensors:
raise ValueError("Number of ADC pins must match the number of sensors")
self.num_sensors = num_sensors
self.adc_pins = adc_pins
self.adc_channels = [ADC(Pin(pin)) for pin in adc_pins]
self.enabled = False
self.power_odd = Pin(power_odd, Pin.OUT_PP) # Power pin for odd sensors
self.power_even = Pin(power_even, Pin.OUT_PP) # Power pin for even sensors
self.calibrated_min = [4095] * num_sensors # Assuming 12-bit ADC
self.calibrated_max = [0] * num_sensors
self.disable # Sensor is initially disabled
def enable(self):
"""Enable the sensor."""
self.power_odd.high()
self.power_even.high() # Turn on power to the sensors
self.enabled = True
def disable(self):
"""Disable the sensor."""
self.power_odd.low()
self.power_even.low() # Turn off power to the sensors
self.enabled = False
def set_pwm_duty_cycle(self, duty_cycle):
""" Set the PWM duty cycle to control the brightness of the IR LEDs.
:param duty_cycle: Duty cycle value (0 to 100).
"""
if 0 <= duty_cycle <= 100:
self.channel_odd.pulse_width_percent(duty_cycle)
#print(f"Set PWM duty cycle to {duty_cycle}%")
else:
raise ValueError("Duty cycle must be between 0 and 100")
def read_raw(self):
"""Read raw values from the sensor array.
:return: List of raw ADC values."""
if not self.enabled:
print("Sensor is disabled. Cannot read raw values.")
return [0] * self.num_sensors
values = [adc.read() for adc in self.adc_channels]
return values
def calibrate_light(self, num_samples=100):
"""Calibrate the sensor by finding min/max values over multiple samples.
:param num_samples: Number of calibration cycles."""
if not self.enabled:
print("Sensor is disabled. Cannot calibrate.")
return
print("Starting calibration light...")
time.sleep_ms(3000)
for _ in range(num_samples):
values = self.read_raw()
for i in range(self.num_sensors):
if values[i] < self.calibrated_min[i]:
self.calibrated_min[i] = values[i]
if values[i] > self.calibrated_max[i]:
self.calibrated_max[i] = values[i]
time.sleep_ms(10)
print("Calibration light complete.")
def calibrate_dark(self, num_samples=100):
"""Calibrate the sensor by finding min/max values over multiple samples for a dark
background.
:param num_samples: Number of calibration cycles."""
if not self.enabled:
print("Sensor is disabled. Cannot calibrate.")
return
print("Starting calibration dark...")
time.sleep_ms(3000)
for _ in range(num_samples):
values = self.read_raw()
for i in range(self.num_sensors):
if values[i] < self.calibrated_min[i]:
self.calibrated_min[i] = values[i]
if values[i] > self.calibrated_max[i]:
self.calibrated_max[i] = values[i]
time.sleep_ms(10)
self.min_value = min(values)
self.max_value = max(values)
self.ave_value = sum(values) / len(values)
print("Calibration dark complete.")
def return_dark(self, num_samples=100):
return self.min_value, self.max_value, self.ave_value
def save_calibration(self, file):
"""Save calibration data to a simple text file."""
try:
with open(file, "w") as f:
for i in range(self.num_sensors):
f.write(f"{self.calibrated_min[i]} {self.calibrated_max[i]}\n")
print("Calibration data saved.")
except Exception as e:
print("Error saving calibration:", e)
def load_calibration(self, file):
"""Load calibration data from the text file if it exists."""
try:
with open(file, "r") as f:
lines = f.readlines()
if len(lines) == self.num_sensors:
for i, line in enumerate(lines):
min_val, max_val = map(int, line.strip().split())
self.calibrated_min[i] = min_val
self.calibrated_max[i] = max_val
print("Calibration data loaded.")
return min_val, max_val
else:
print("Calibration file invalid, using default values.")
except OSError:
print("No calibration file found, using default values.")
except Exception as e:
print("Error loading calibration:", e)
def read_calibrated(self):
if not self.enabled:
print("Sensor is disabled. Cannot read calibrated values.")
return [0] * self.num_sensors
raw_values = self.read_raw()
calibrated_values = []
for i in range(self.num_sensors):
min_val = self.calibrated_min[i]
max_val = self.calibrated_max[i]
if max_val - min_val == 0:
calibrated_values.append(0)
else:
scaled_value = (raw_values[i] - min_val) * 1000 // (max_val - min_val)
calibrated_values.append(max(0, min(1000, scaled_value)))
return calibrated_values
def read_line_position(self):
"""Determine the position of the line (weighted average of sensor indices).
:return: Line position as a float (0 to num_sensors - 1)."""
if not self.enabled:
print("Sensor is disabled. Cannot read line position.")
return (self.num_sensors - 1) / 2 # Default to center if sensor is disabled
calibrated_values = self.read_calibrated()
weighted_sum = 0
sum_values = 0
for i in range(self.num_sensors):
weighted_sum += calibrated_values[i] * i
sum_values += calibrated_values[i]
if sum_values == 0:
return (self.num_sensors - 1) / 2 # Default to center if no line is detected
return weighted_sum / sum_values