IR Sensor Driver

# 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