From 7f9d2402c6f0f388243f25b32122015c7eb3b0b2 Mon Sep 17 00:00:00 2001 From: Pete Attayek Date: Thu, 19 Mar 2026 07:21:58 -0400 Subject: [PATCH 1/9] Add clibrate_strobe_output.py script to web-server folder --- .../web-server/calibrate_strobe_output.py | 651 ++++++++++++++++++ 1 file changed, 651 insertions(+) create mode 100644 Software/web-server/calibrate_strobe_output.py diff --git a/Software/web-server/calibrate_strobe_output.py b/Software/web-server/calibrate_strobe_output.py new file mode 100644 index 00000000..1d14bcd7 --- /dev/null +++ b/Software/web-server/calibrate_strobe_output.py @@ -0,0 +1,651 @@ +#!/usr/bin/env python3 +""" +SPDX-License-Identifier: GPL-2.0-only */ +Copyright (C) 2022-2025, Verdant Consultants, LLC. + +PiTrac Controller Board Strobe Output Calibration Module + +Adjusts the strobe output to a default (or selected) current output by sending a range of values to the digital +potentiometer on the board (via SPI) and then repeatedly checking the ADC to see if the desired output has been +reached (as the LED current iteratively goes down as up DAC output goes down). + +WARNING ------ This code has not been tested against actual hardware yet, so it could still be harmful + to whatever hardware you are running on. Caveat emptor! + Please run this step by step in a debugger to make sure that it is sending appropriate + data to the Controller Board. + +""" +import spidev +# Will need to run +# sudo apt install python3-rpi.gpio +# sudo apt install python3-lgpio +from gpiozero import LED +import time +import sys +import argparse +import os +import gc + + +import logging + +# We expect to be running this utility in the standard directory where PiTrac is installed, and where there +# should be the config.manager.py and configurations.json files. If running somewhere else, the user will need to either put +# a copy of those files there, or else set the PiTrac home directory appropriately. +# That directory is typically at: /usr/lib/pitrac/web-server +DEFAULT_PITRAC_PATH = "/usr/lib/pitrac/web-server" +sys.path.append(DEFAULT_PITRAC_PATH) + +from config_manager import ConfigurationManager + + +logger = logging.getLogger(__name__) + + +class StrobeOutputCalibrator: + + # The final DAC setting and corresponding ADC output will be saved to the user_settings.json file, + # so we can read them later if needed. These are the search keys for those values in the JSON file. + DAC_SETTING_JSON_PATH = "gs_config.strobing.kDAC_setting" + + # These are the more-or-less standard SPI bus and device numbers for the Raspberry Pi. + SPI_BUS = 0 + SPI_DAC_DEVICE = 0 # DAC is on CS0 + SPI_ADC_DEVICE = 1 # ADC is on CS1 + + # Max speed for the ADC is 1.1 MHz and DAC is 20 MHz + SPI_MAX_SPEED_HZ = 1000000 # 1 MHz + + # This is the pin that we will use to toggle the strobe output on and off through the DIAG pin + # on the Connector Board. The strobe needs to be on for the ADC to read the LED current, + # so we will toggle this pin on just before reading the ADC, and then toggle it off just after. + # Note - this corresponds to the BCM pin number, not the physical pin number. + # So this is physical pin 38 on the Raspberry Pi header. + DIAG_GPIO_PIN = 20 + + # This is the maximum safe strobe current for the V3 LED + DEFAULT_TARGET_LED_CURRENT_SETTING = 7.0 # amps + + + # We should NEVER go below this LDO voltage + ABSOLUTE_LOWEST_LDO_VOLTAGE = 4.5 # volts + ABSOLUTE_HIGHEST_LDO_VOLTAGE = 11 # volts + + MAX_DAC_SETTING = 0xFF # 255 # It's an 8-bit DAC, so max value is 2^8 - 1 + MIN_DAC_SETTING = 0 # Note - + + # This value is just a guess for now and works on 1 board. It should be high enough so that the LDO voltage in all devices + # (even accounting for variations) will be low enough not to hurt our standard strobe, but high enough to be above the minimum LDO voltage. + # This settins will only be used to set the DAC to a known, safe level if there's some failure + # in the calibration process. + PRESUMED_SAFE_DAC_SETTING = 0x96 # 150 + + # For the MCP4801 commands, see: https://ww1.microchip.com/downloads/en/DeviceDoc/22244B.pdf + MCP4801_BASE_WRITE_CMD_SET_OUTPUT = 0b00110000 # Standard (1x) gain (bit 13) (Vout=Vref*D/4096), VREF buffered, active, no shutdown, D4-D7 are 0 + + # For the MCP3202 commands, see: https://www.google.com/aclk?sa=L&ai=DChsSEwj3ncmJqIKTAxWQK60GHXKrGJQYACICCAEQABoCcHY&ae=2&co=1&ase=2&gclid=CjwKCAiAh5XNBhAAEiwA_Bu8Fae9UWeNq0dHjoQ9N4wHFrRnEvETyzrYVl5xmGvUyNR0uFNVP5IlQRoCaAcQAvD_BwE&cid=CAASWeRo2UUWEQuONJENmHLbquopI4y29-vAFb0frx7hQM6bjuRo_TZk1PIihnFuJO8jpNwTGiEFOnOQWgmJHHFTivCGeaaKvgUk8x67yLoRpN_bIoGgiweiu6Vk&cce=2&category=acrcp_v1_71&sig=AOD64_3LJYqvTzD3ozkW0pkVDNgovi6vvA&q&nis=4&adurl&ved=2ahUKEwi1w8KJqIKTAxXKFDQIHVsXEX8Q0Qx6BAgPEAE + MCP3202_READ_CH0_SINGLE_ENDED_CMD = 0 | 0x80 # Channel 0, single-ended - LED Current Sense Resistor Voltage + MCP3202_READ_CH1_SINGLE_ENDED_CMD = 0 | 0xc0 # Channel 1, single-ended - LDO Gate Voltage + + # Will be set when the class is initialized + spi_dac = None + spi_adc = None + + diag_pin = None + + def __init__(self): + + self.config_manager = ConfigurationManager() + + + def setup_spi_channels(self): + logger.debug(f"Setting up SPI channels...") + + success = False + + try: + # DAC Setup + self.spi_dac = spidev.SpiDev() + self.spi_dac.open(self.SPI_BUS, self.SPI_DAC_DEVICE) + + # Set SPI speed + self.spi_dac.max_speed_hz = self.SPI_MAX_SPEED_HZ + + # Set SPI mode common modes are 0 or 3) + self.spi_dac.mode = 0b00 # Mode 0 + + + # ADC Setup + self.spi_adc = spidev.SpiDev() + self.spi_adc.open(self.SPI_BUS, self.SPI_ADC_DEVICE) + + # Set SPI speed + self.spi_adc.max_speed_hz = self.SPI_MAX_SPEED_HZ + + # Set SPI mode common modes are 0 or 3) + self.spi_adc.mode = 0b00 # Mode 0 + + + # Signal that all went well with the SPI setup + success = True + + except Exception as e: + logger.error(f"An error occurred when setting up the SPI connections: {e}") + success = False + + return success + + + def close_spi(self): + if self.spi_dac is not None: + self.spi_dac.close() # Always close the SPI connection when done + if self.spi_adc is not None: + self.spi_adc.close() + + def open_gpio_system(self): + logger.debug(f"Setting up GPIO pin {self.DIAG_GPIO_PIN}...") + + success = False + + try: + self.diag_pin = LED(self.DIAG_GPIO_PIN) # Use Broadcom pin-numbering scheme + + # Signal that all went well with the SPI setup + success = True + + except Exception as e: + logger.error(f"An error occurred when setting up the GPIO system: {e}") + success = False + + return success + + def close_gpio_system(self): + # Cleanup all GPIO pins to their default state + if self.diag_pin is not None: + self.diag_pin.close() + + def get_ADC_value_CH0(self): + # Start bit is always the first byte, then the channel and mode bits are combined into the second byte, + # and the third byte is just a timing placeholder for the response, because we need the last 2 of 3 bytes of response, + # but our command is only 2 bytes + message_to_send = [0x01, self.MCP3202_READ_CH0_SINGLE_ENDED_CMD, 0x00] + logger.debug(f"Message to send to ADC (to get value): {[format(b, '02x') for b in message_to_send]}") + + response_bytes = self.spi_adc.xfer2(message_to_send) + + # The result is 12-bits. The first byte returned is just random - the MISO line is null + # when the command is sent, so nothing was really sent. + # The second byte contains the top 4 bits (masked with 0x0F as some bits may be null) + # The third byte contains the least-significant 8 bits + + # Put the top 4 bits and lower 8 bits together to get the full 12-bit ADC value + adc_value = (response_bytes[1] & 0x0F) << 8 | response_bytes[2] + + return adc_value + + def get_ADC_value_CH1(self): + # Start bit is always the first byte, then the channel and mode bits are combined into the second byte, + # and the third byte is just a timing placeholder for the response, because we need the last 2 of 3 bytes of response, + # but our command is only 2 bytes + message_to_send = [0x01, self.MCP3202_READ_CH1_SINGLE_ENDED_CMD, 0x00] + logger.debug(f"Message to send to ADC (to get value): {[format(b, '02x') for b in message_to_send]}") + + response_bytes = self.spi_adc.xfer2(message_to_send) + + # The result is 12-bits. The first byte returned is just random - the MISO line is null + # when the command is sent, so nothing was really sent. + # The second byte contains the top 4 bits (masked with 0x0F as some bits may be null) + # The third byte contains the least-significant 8 bits + + # Put the top 4 bits and lower 8 bits together to get the full 12-bit ADC value + adc_value = (response_bytes[1] & 0x0F) << 8 | response_bytes[2] + + return adc_value + + + def get_LDO_voltage(self): + # We need to measure LDO voltage to make sure we are safe to raise the DIAG pin to high + adc_value = self.get_ADC_value_CH1() + + # *3 because of the resistor divider made up of 2k top and 1k bottom, so (1 / (2 + 1)) scaling factor + LDO_voltage = (3.3 / 4096) * adc_value * 3 # Convert ADC value to voltage + return LDO_voltage + + def get_LED_current(self): + # We need to turn on the strobe output through the DIAG pin before we read the ADC, + # because a valid LED current sense voltage is only present when the strobe is on. + # and then turn it right back off again. + message_to_send = [0x01, self.MCP3202_READ_CH0_SINGLE_ENDED_CMD, 0x00] + logger.debug(f"Message to send to ADC (to get value): {[format(b, '02x') for b in message_to_send]}") + + spi = self.spi_adc + diag = self.diag_pin + + # --- PREPARE FOR CRITICAL TIMING --- + + # Disable Python's random memory cleaning + gc.disable() + + # Grab the highest possible Real-Time OS Priority + try: + param = os.sched_param(os.sched_get_priority_max(os.SCHED_FIFO)) + os.sched_setscheduler(0, os.SCHED_FIFO, param) + except (PermissionError, AttributeError): + logger.debug(f"WARNING: sudo permissions not established or OS scheduling priority not supported.") + pass # Fails if not running as root/sudo or on Windows + + # Yield CPU to get a fresh, full time-slice from Linux + time.sleep(0) + + + try: + # --- BEGIN DETERMINISTIC HARDWARE BLOCK --- + diag.on() + response_bytes = spi.xfer2(message_to_send) + diag.off() + # --- END DETERMINISTIC HARDWARE BLOCK --- + + finally: + # --- RETURN TO NORMAL OS BEHAVIOR --- + + # Give up real-time priority (return to normal scheduler) + try: + param = os.sched_param(0) + os.sched_setscheduler(0, os.SCHED_OTHER, param) + except (PermissionError, AttributeError): + pass + + # Turn memory management back on + gc.enable() + + adc_value = (response_bytes[1] & 0x0F) << 8 | response_bytes[2] + LED_current = (3.3 / 4096) * adc_value * 10 # Convert ADC value to current + return LED_current + + def turn_diag_pin_off(self): + logger.debug(f"turn_diag_pin_off") + if self.diag_pin is not None: + self.diag_pin.off() + + + def short_pause(self): + time.sleep(0.1) + + + def set_DAC(self, value): + msb_data = self.MCP4801_BASE_WRITE_CMD_SET_OUTPUT | ((value >> 4) & 0x0F) # Get the top 4 bits of the value and combine with the command + + lsb_data = (value << 4) & 0xF0 # Get the bottom 4 bits of the value into the top 4 bits of the second byte (the bottom 4 bits of the second byte + # are ignored by the DAC, so it doesn't matter what we put there) + + message_to_send = [msb_data, lsb_data] # Get the pot value + + logger.debug(f"\nset_DAC: Message to send to DAC: {[format(b, '02x') for b in message_to_send]}") + # We don't use the response + response = self.spi_dac.xfer2(message_to_send) + + + def get_calibration_settings_from_json(self): + + current_DAC_output_value = -1 + current_DAC_setting = self.config_manager.get_config(self.DAC_SETTING_JSON_PATH) + if current_DAC_setting is None: + logger.debug(f"Current DAC Setting: ") + else: + logger.debug(f"Current DAC Setting: {current_DAC_setting}") + current_DAC_output_value = int(current_DAC_setting) + + return current_DAC_output_value + + + def set_DAC_to_safest_level(self): + logger.error(f"Calibration failed. Setting DAC voltage to highest level of {self.PRESUMED_SAFE_DAC_SETTING} (presumed-safe strobe level) for safety.") + self.set_DAC(self.PRESUMED_SAFE_DAC_SETTING) + + + def json_file_has_calibration_settings(self): + logger.debug(f"Checking whether json_file_has_calibration_settings") + current_DAC_setting = self.config_manager.get_config(self.DAC_SETTING_JSON_PATH) + if current_DAC_setting is None: + return False + else: + return True + + def find_DAC_start_setting(self): + DAC_max_setting = self.MAX_DAC_SETTING + 1 + for i in range(DAC_max_setting): + DAC_start_setting = i + # set DAC value + self.set_DAC(i) + # wait for DAC value to take effect + self.short_pause() + # check the LDO voltage + LDO_voltage = self.get_LDO_voltage() + logger.debug(f"DAC Value: {format(i, '02x')}, Computed LDO voltage (from ADC): {format(LDO_voltage, '0.2f')}" ) + # if LDO voltage drops below ABSOLUTE_LOWEST_LDO_VOLTAGE then break the loop + if LDO_voltage < self.ABSOLUTE_LOWEST_LDO_VOLTAGE: + # set starting DAC value to lowest voltage above the absolute minimum + DAC_start_setting = i - 1 + break + + return DAC_start_setting, LDO_voltage + + + def calibrate_board(self, target_LED_current): + + # find the minimum safe LDO voltage to supply the MCP1407 gate driver + DAC_start_setting, LDO_voltage = self.find_DAC_start_setting() + + # If even a DAC value of 0 was below the ABSOLUTE_LOWEST_LDO_VOLTAGE then fail calibration + if DAC_start_setting < 0: + logger.debug(f"DAC value of 0 is below minimum LDO voltage ({format(self.ABSOLUTE_LOWEST_LDO_VOLTAGE, '0.2f')}): {format(LDO_voltage, '0.2f')}") + return False, -1, -1 + + + logger.debug(f"calibrate_board called with target_LED_current = {target_LED_current}, DAC_start_setting = 0x{format(DAC_start_setting, '02x')}") + + # Now, starting at the max DAC value (0xFF) + # we will iteratively decrease the DAC setting until we get to + # the desired ADC output (or just under it) + + current_DAC_setting = DAC_start_setting + final_DAC_setting = self.MIN_DAC_SETTING + + # just picking a number that we should always be above at the start of the loop, + # so that we can save the first reading as the best one so far even if it's not + # above the target + max_LED_current_so_far = 0.0 + + # We will start at the max DAC setting and then count down while + # looking for the point where the corresponding LED current goes just above the target_LED_current, + # then increase 1 value to ensure we are <= target_LED_current. + + logger.debug(f"calibrate_board starting loop. Desired output is {target_LED_current}") + + # Stop immediately if we ever have an error + while (current_DAC_setting >= self.MIN_DAC_SETTING): + + self.set_DAC(current_DAC_setting) + + # Wait a moment for the setting to take effect + self.short_pause() + + # check the LDO voltage to ensure that we are within the safe bounds + LDO_voltage = self.get_LDO_voltage() + # if we are below the ABSOLUTE_LOWEST_LDO_VOLTAGE, it is unsafe to pulse the DIAG pin. Decrease DAC value and continue + if LDO_voltage < self.ABSOLUTE_LOWEST_LDO_VOLTAGE: + logger.debug(f"Measured LDO_voltage ({LDO_voltage}) was below ABSOLUTE_LOWEST_LDO_VOLTAGE of {self.ABSOLUTE_LOWEST_LDO_VOLTAGE} volts. Trying next DAC value.") + # Continue counting down + final_DAC_setting = current_DAC_setting + current_DAC_setting -= 1 + continue + + # if we are above the ABSOLUTE_HIGHEST_LDO_VOLTAGE, then we have to stop and fail the calibration + if LDO_voltage > self.ABSOLUTE_HIGHEST_LDO_VOLTAGE: + logger.debug(f"Measured LDO_voltage ({LDO_voltage}) was above ABSOLUTE_HIGHEST_LDO_VOLTAGE of {self.ABSOLUTE_HIGHEST_LDO_VOLTAGE} volts. Stopping calibration, as something is wrong.") + return False, -1, -1 + + # Note reading the LED current also pulses the strobe through the DIAG pin, + # which is necessary to get a valid reading, but also means that we are toggling + # the strobe on and off repeatedly during this calibration process, which is not ideal. + # But we need to do it in order to get accurate LED current readings. + LED_current = self.get_LED_current() + logger.debug(f"current_DAC_setting: {format(current_DAC_setting, '02x')}, LED_current: {LED_current}") + + # As we are slowly increasing the LED current, have we reached our desired set-point for the LED current yet? + if LED_current > target_LED_current: + logger.debug(f" ---> Reached above the target_LED_current ({target_LED_current}). LED_current is: {LED_current}. Stopping calibration here...") + final_DAC_setting = current_DAC_setting + 1 # Step back to the last setting that was just before we reached our target + break + + # We have not yet reached the target. TBD - This is a little redundant, maybe change + # Keep track of where we were. + if LED_current > max_LED_current_so_far: + + # Save the current output as the best one so far, even if it's not over the target, + # because we want to get as close as possible without going over + max_LED_current_so_far = LED_current + + # Continue counting down + final_DAC_setting = current_DAC_setting + current_DAC_setting -= 1 + + # There are a couple of possible edge cases here. And either of them indicate that something probably went wrong somewhere even if we + # thought we had a success. + # If so, err on the safe side and consider this a failure + if current_DAC_setting <= self.MIN_DAC_SETTING: + logger.debug(f"Reached MIN_DAC_SETTING ({self.MIN_DAC_SETTING}) without ever reaching target_LED_current ({target_LED_current}). This generally indicates a problem. Failing calibration.") + return False, -1, -1 + if current_DAC_setting >= self.MAX_DAC_SETTING: + logger.debug(f"The MAX_DAC_SETTING resulted in an LED current above the target. This generally indicates a problem. Failing calibration.") + return False, -1, -1 + + # Now, using the best DAC setting we found, average the output voltage a few times to + # get a more accurate reading of the output voltage at that setting + # take an average of n pulses + n = 10 + while True: + self.set_DAC(final_DAC_setting) + self.short_pause() + + # check if LDO voltage is above the minimum + LDO_voltage = self.get_LDO_voltage() + if LDO_voltage < self.ABSOLUTE_LOWEST_LDO_VOLTAGE: + # Fallback to the last known good measurement + final_DAC_setting -= 1 + break + + # Take an average of n pulses + LED_current_sum = 0 + for _ in range(n): + LED_current_sum += self.get_LED_current() + self.short_pause() + LED_current = LED_current_sum / n + + if LED_current > target_LED_current: + # Current is still slightly too high, step the DAC setting + final_DAC_setting += 1 + else: + # We are at or below the target current, we're done + break + + logger.debug(f"calibrate_board -- final_DAC_setting: {format(final_DAC_setting, '02x')}, LED_current: {LED_current}") + + return True, final_DAC_setting, LED_current + + + def cleanup_for_exit(self): + self.turn_diag_pin_off() + self.short_pause() + self.close_spi() + self.close_gpio_system() + + # ----------------------- + + +def main(): + + success = True + + # The calibrator class does all of the work here + calibrator = StrobeOutputCalibrator() + + parser = argparse.ArgumentParser(description="PiTrac Controller Board Strobe Output Calibrator. This tool iteratively adjusts the board's DAC in order to find the right setting for the desired LED current for the strobe LED circuit.\nWARNING - Setting the LDO voltage below 4.5v can break your Control Board.") + parser.add_argument("-v", "--verbose", action="store_true", help="Verbose output") + parser.add_argument("-q", "--quiet", action="store_true", help="Quiet output") + parser.add_argument("-w", "--overwrite", action="store_true", help="Overwrites any existing strobe setting in user_settings.json") + parser.add_argument("--target_output", default=0,type=float, help="Set target LED current output (in volts) (ADVANCED)") + parser.add_argument("--ignore", action="store_true", help="Attempt calibration even if the Controller Board version is not 3.0 (ADVANCED)") + action_group = parser.add_mutually_exclusive_group() + action_group.add_argument("-p", "--print_settings", action="store_true", help="Print the current DAC setting and last ADC measurement from the user_settings.json file") + action_group.add_argument("-a0", "--read_ADC_CH0", action="store_true", help="Measure and print the current (12-bit) ADC CH0 measurement from the Connector Board") + action_group.add_argument("-a1", "--read_ADC_CH1", action="store_true", help="Measure and print the current (12-bit) ADC CH1 measurement from the Connector Board)") + action_group.add_argument("-c", "--read_LED_current", action="store_true", help="Compute and print the current LED current (based on the ADC CH0 measurement) from the Connector Board") + action_group.add_argument("-l", "--read_LDO_voltage", action="store_true", help="Compute and print the current LDO value (based on the ADC CH1 measurement) from the Connector Board") + action_group.add_argument("--DAC_setting", default=None,type=int, help="Set the DAC input to a specific value. Value is 8 bits long (ADVANCED).") + action_group.add_argument("--get_DAC_start", action="store_true", help="Check LDO voltage while sweeping DAC values to find lowest safe value") + + args = parser.parse_args() + + if args.verbose: + logging.basicConfig(level=logging.DEBUG) + elif args.quiet: + logging.basicConfig(level=logging.WARNING) + else: + logging.basicConfig(level=logging.INFO) + + + logger.debug(f"Calibrator initialized") + + if calibrator.setup_spi_channels() == False: + logger.error(f"SPI initialization failed. Cannot proceed with calibration.") + return 1 + + if calibrator.open_gpio_system() == False: + logger.error(f"GPIO initialization failed. Cannot proceed with calibration.") + calibrator.close_gpio_system() + return 1 + + try: + + # Process other options + + if args.read_ADC_CH0: + ADC_response = calibrator.get_ADC_value_CH0() + logger.info(f"Value read from ADC: {format(ADC_response, '02x')}" ) + + elif args.read_ADC_CH1: + ADC_response = calibrator.get_ADC_value_CH1() + logger.info(f"Value read from ADC: {format(ADC_response, '02x')}" ) + + elif args.read_LDO_voltage: + LDO_Voltage = calibrator.get_LDO_voltage() + logger.info(f"Computed LDO voltage (from ADC): {format(LDO_Voltage, '0.2f')}" ) + + elif args.read_LED_current: + + # ensure that the LDO voltage is within safe bounds before pulsing the DIAG pin + LDO_Voltage = calibrator.get_LDO_voltage() + if LDO_Voltage > calibrator.ABSOLUTE_LOWEST_LDO_VOLTAGE: + LED_current = calibrator.get_LED_current() + logger.info(f"Computed LED current (from ADC): {format(LED_current, '0.2f')}" ) + else: + logger.warning(f"LDO voltage is below minimum value, cannot safely pulse DIAG pin to get LED current. LDO Voltage: {format(LDO_Voltage, '0.2f')}") + return 1 + + elif args.DAC_setting is not None: + desired_DAC_setting = args.DAC_setting + + # Check if desired DAC setting is within the allowable bounds of MIN_DAC_SETTING and MAX_DAC_SETTING + if desired_DAC_setting > calibrator.MAX_DAC_SETTING: + logger.warning(f"Maximum allowable DAC setting is: {format(calibrator.MAX_DAC_SETTING, '02x')}" ) + return 1 + if desired_DAC_setting < calibrator.MIN_DAC_SETTING: + logger.warning(f"Minimum allowable DAC setting is: {format(calibrator.MIN_DAC_SETTING, '02x')}" ) + return 1 + + # Set the DAC value + calibrator.set_DAC(desired_DAC_setting) + # Wait a moment for the setting to take effect + calibrator.short_pause() + + # check the LDO voltage + LDO_voltage = calibrator.get_LDO_voltage() + logger.warning(f"DAC is set to: {format(desired_DAC_setting, '02x')}" ) + if LDO_voltage < calibrator.ABSOLUTE_LOWEST_LDO_VOLTAGE: + logger.warning(f"LDO voltage is below minimum value. This is VERY DANGEROUS. If this is unintentional, run --get_DAC_start to find the minimum safe DAC value.") + + elif args.print_settings: + DAC_value = calibrator.get_calibration_settings_from_json() + + if DAC_value < 0: + logger.debug(f"Current DAC Setting: ") + else: + logger.info(f"DAC value from user settings: {format(DAC_value, '02x')}" ) + + elif args.get_DAC_start: + # sweep DAC values from low to high + DAC_start_setting, LDO_voltage = calibrator.find_DAC_start_setting() + + # If even a DAC value of 0 was below the ABSOLUTE_LOWEST_LDO_VOLTAGE then fail calibration + if DAC_start_setting < 0: + logger.warning(f"DAC value of 0 is below minimum LDO voltage ({format(calibrator.ABSOLUTE_LOWEST_LDO_VOLTAGE, '0.2f')}): {format(LDO_voltage, '0.2f')}\nThis indicates a problem with the controller board") + else: + + calibrator.set_DAC(DAC_start_setting) + + # Wait a moment for the setting to take effect + calibrator.short_pause() + + # check the LDO voltage + LDO_voltage = calibrator.get_LDO_voltage() + + logger.info(f"DAC_start_setting = 0x{format(DAC_start_setting, '02x')}. LDO_voltage = {format(LDO_voltage, '0.2f')}") + + else: + # Default calibration behavior - iteratively find the closest setting for the DAC that will get the desired ADC reading (but not under) + logger.info(f"Calibrating PiTrac Control Board. This may take a minute or two. Please wait..." ) + + if calibrator.json_file_has_calibration_settings() and not args.overwrite: + logger.error(f"Calibration settings already exist in user_settings.json. Use the --overwrite flag to overwrite them.") + return 1 + + control_board_version = calibrator.config_manager.get_config("gs_config.strobing.kConnectionBoardVersion") + logger.debug(f"control_board_version = {control_board_version}") + if control_board_version is None: + control_board_version = "0" + + control_board_version_value = int(control_board_version) + + # This calibration function is only relevant for the Version 3.x Control Board + if control_board_version_value != 3 and not args.ignore: + logger.error(f"The controller board is the wrong version ({control_board_version_value}) for this calibration utility. Must be using a Verison 3 board.") + return 1 + + target_LED_current = calibrator.DEFAULT_TARGET_LED_CURRENT_SETTING + + if (args.target_output > 0.0): + target_LED_current = args.target_output + + logger.debug(f"target_LED_current = {target_LED_current}") + + + # Perform the actual calibration here + success, final_DAC_setting, LED_current = calibrator.calibrate_board(target_LED_current) + + + if success and final_DAC_setting > 0: + logger.info(f"Calibration successful. Final DAC setting: {format(final_DAC_setting, '02x')}, corresponding LED current: {format(LED_current, '0.2f')}") + # Save the final DAC setting and corresponding ADC output to the user_settings.json file for later reference + calibrator.config_manager.set_config(calibrator.DAC_SETTING_JSON_PATH, final_DAC_setting) + else: + logger.info(f"Calibration failed.") + calibrator.set_DAC_to_safest_level() + + + calibrator.short_pause() + + logger.debug(f"Calibration operation completed." ) + + except KeyboardInterrupt: + print("\nCtrl+C pressed. Performing cleanup...") + # Add your cleanup code here (e.g., closing files, releasing resources) + calibrator.cleanup_for_exit() + + except Exception as e: + logger.debug(f"An error occurred: {e}") + calibrator.set_DAC_to_safest_level() + + return 1 # Failure + + finally: + calibrator.cleanup_for_exit() + + if success: + return 0 + else: + return 1 + + +if __name__ == "__main__": + sys.exit(main()) From 0ea8f2ed6fa616189161853d481462a01b95b371 Mon Sep 17 00:00:00 2001 From: Pete Attayek Date: Thu, 19 Mar 2026 11:56:06 -0400 Subject: [PATCH 2/9] Set current limits for both V3 LED and Old 100W LED --- Software/web-server/calibrate_strobe_output.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Software/web-server/calibrate_strobe_output.py b/Software/web-server/calibrate_strobe_output.py index 1d14bcd7..8b1d1666 100644 --- a/Software/web-server/calibrate_strobe_output.py +++ b/Software/web-server/calibrate_strobe_output.py @@ -64,7 +64,9 @@ class StrobeOutputCalibrator: DIAG_GPIO_PIN = 20 # This is the maximum safe strobe current for the V3 LED - DEFAULT_TARGET_LED_CURRENT_SETTING = 7.0 # amps + V3_TARGET_LED_CURRENT_SETTING = 10.0 # amps + # This is the maximum safe strobe current for the old 100W LED + OLD_TARGET_LED_CURRENT_SETTING = 7.0 # amps # We should NEVER go below this LDO voltage @@ -471,6 +473,7 @@ def main(): calibrator = StrobeOutputCalibrator() parser = argparse.ArgumentParser(description="PiTrac Controller Board Strobe Output Calibrator. This tool iteratively adjusts the board's DAC in order to find the right setting for the desired LED current for the strobe LED circuit.\nWARNING - Setting the LDO voltage below 4.5v can break your Control Board.") + parser.add_argument("-o", "--old_LED", action="store_true", help="PiTrac is using old 100W LED. Default behavior is V3 LED") parser.add_argument("-v", "--verbose", action="store_true", help="Verbose output") parser.add_argument("-q", "--quiet", action="store_true", help="Quiet output") parser.add_argument("-w", "--overwrite", action="store_true", help="Overwrites any existing strobe setting in user_settings.json") @@ -602,10 +605,13 @@ def main(): logger.error(f"The controller board is the wrong version ({control_board_version_value}) for this calibration utility. Must be using a Verison 3 board.") return 1 - target_LED_current = calibrator.DEFAULT_TARGET_LED_CURRENT_SETTING - if (args.target_output > 0.0): target_LED_current = args.target_output + elif (args.old_LED): + target_LED_current = calibrator.OLD_TARGET_LED_CURRENT_SETTING + else: + target_LED_current = calibrator.V3_TARGET_LED_CURRENT_SETTING + logger.debug(f"target_LED_current = {target_LED_current}") From 9b4737f1246341dc37cc3bebf5b231b79bb577b3 Mon Sep 17 00:00:00 2001 From: Pete Attayek Date: Thu, 19 Mar 2026 15:03:16 -0400 Subject: [PATCH 3/9] Update PCB Assembly documentation with connection guide for V3 --- .../PiTrac_V3_Connection_Guide.svg | 1 + docs/hardware/pcb-assembly.md | 27 ++++++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 docs/assets/images/enclosure_assembly/PiTrac_V3_Connection_Guide.svg diff --git a/docs/assets/images/enclosure_assembly/PiTrac_V3_Connection_Guide.svg b/docs/assets/images/enclosure_assembly/PiTrac_V3_Connection_Guide.svg new file mode 100644 index 00000000..458a8234 --- /dev/null +++ b/docs/assets/images/enclosure_assembly/PiTrac_V3_Connection_Guide.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/hardware/pcb-assembly.md b/docs/hardware/pcb-assembly.md index 938f97c5..5f2f5416 100644 --- a/docs/hardware/pcb-assembly.md +++ b/docs/hardware/pcb-assembly.md @@ -217,7 +217,7 @@ After assembly, must run current calibration before you will be able to capture ### Pi GPIO -- **J3:** 8-pin GPIO header for control signals (see assembly guide for pinout) +- **J3:** 8-pin GPIO header for control signals (see below for Connection Guide for Raspberry Pi 5) ### LED Output @@ -228,6 +228,31 @@ After assembly, must run current calibration before you will be able to capture - **J6 (USB-A):** Originally for LED strip, but Pi5 has USB 2.0 ports already - this is redundant +## Connection Guide for Raspberry Pi 5 + +| V3 Connector Board | Raspberry Pi 5 | +|----------|----------| +| GND | GND (Pin 39) | +| DIAG | GPIO 20 (Pin 38) | +| CS0 | GPIO 8 (Pin 24) | +| MOSI | GPIO 10 (Pin 19) | +| MISO | GPIO 9 (Pin 21) | +| CLK | GPIO 11 (Pin 23) | +| CS1 | GPIO 7 (Pin 26) | +| V3P3 | 3V3 (Pin 1) | + +| Global Shutter Camera 2 | Raspberry Pi 5 | +|----------|----------| +| Trig+ | GPIO 25 (Pin 22) | +| Trig- | GND (Pin 20) | + +| V3 Connector Board | V3 IRLED Board | +|----------|----------| +| VIR+ | VIR+ | +| VIR- | VIR- | + + ![PiTrac V3 Connection Guide]({{ '/assets/images/hardware/PiTrac_V3_Connection_Guide.svg' | relative_url }}) + ## Configuring PiTrac You'll need to tell PiTrac which version of the Connection Board you are using. This is done in the Configuration screen in the UI. From 6aeb9d21a61da8ea4eb6f86cb300ae8b7f244edc Mon Sep 17 00:00:00 2001 From: Pete Attayek Date: Thu, 19 Mar 2026 15:13:20 -0400 Subject: [PATCH 4/9] Update assembly instructions to only say 1 rpi instead of 1-2 --- docs/hardware/parts-list.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/hardware/parts-list.md b/docs/hardware/parts-list.md index b04f2810..cb2bfcf6 100644 --- a/docs/hardware/parts-list.md +++ b/docs/hardware/parts-list.md @@ -18,10 +18,10 @@ This document provides a comprehensive list of all components needed to build a | Quantity | Hardware | Purpose | Link | |----------|----------|---------|------| -| 1-2 | Raspberry Pi 5 (8 GB recommended) | Main embedded computer | https://vilros.com/products/raspberry-pi-5?variant=40065551302750 -| 1-2 | Active Cooler Kit | Required to keep temps low and timing consistent | https://a.co/d/dsl7saU -| 1-2 | MicroSD card (64 GB recommended) | For RPi5 filesystem | https://www.amazon.com/Amazon-Basics-microSDXC-Memory-Adapter/dp/B08TJTB8XS -| 1-2 | 1ft USB-C to USB-C Cable | For powering RPi5 from connector board | https://www.amazon.com/Anker-Charging-MacBook-Samsung-Nintendo/dp/B09H2DMR4K +| 1 | Raspberry Pi 5 (8 GB recommended) | Main embedded computer | https://vilros.com/products/raspberry-pi-5?variant=40065551302750 +| 1 | Active Cooler Kit | Required to keep temps low and timing consistent | https://a.co/d/dsl7saU +| 1 | MicroSD card (64 GB recommended) | For RPi5 filesystem | https://www.amazon.com/Amazon-Basics-microSDXC-Memory-Adapter/dp/B08TJTB8XS +| 1 | 1ft USB-C to USB-C Cable | For powering RPi5 from connector board | https://www.amazon.com/Anker-Charging-MacBook-Samsung-Nintendo/dp/B09H2DMR4K ## Camera and Lighting Hardware From 73991654a617070abf42659d87ffa7831ecb1696 Mon Sep 17 00:00:00 2001 From: Pete Attayek Date: Fri, 20 Mar 2026 08:55:27 -0400 Subject: [PATCH 5/9] Change ADC and DAC to use SPI1 instead of SPI0. Change DIAG pin to SPI0 MOSI pin (GPIO 10) --- Software/web-server/calibrate_strobe_output.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/Software/web-server/calibrate_strobe_output.py b/Software/web-server/calibrate_strobe_output.py index 8b1d1666..3ce2803c 100644 --- a/Software/web-server/calibrate_strobe_output.py +++ b/Software/web-server/calibrate_strobe_output.py @@ -16,9 +16,6 @@ """ import spidev -# Will need to run -# sudo apt install python3-rpi.gpio -# sudo apt install python3-lgpio from gpiozero import LED import time import sys @@ -48,8 +45,8 @@ class StrobeOutputCalibrator: # so we can read them later if needed. These are the search keys for those values in the JSON file. DAC_SETTING_JSON_PATH = "gs_config.strobing.kDAC_setting" - # These are the more-or-less standard SPI bus and device numbers for the Raspberry Pi. - SPI_BUS = 0 + # SPI bus 1 (SPI1/auxiliary SPI) and device numbers for the Raspberry Pi. + SPI_BUS = 1 SPI_DAC_DEVICE = 0 # DAC is on CS0 SPI_ADC_DEVICE = 1 # ADC is on CS1 @@ -59,9 +56,9 @@ class StrobeOutputCalibrator: # This is the pin that we will use to toggle the strobe output on and off through the DIAG pin # on the Connector Board. The strobe needs to be on for the ADC to read the LED current, # so we will toggle this pin on just before reading the ADC, and then toggle it off just after. - # Note - this corresponds to the BCM pin number, not the physical pin number. - # So this is physical pin 38 on the Raspberry Pi header. - DIAG_GPIO_PIN = 20 + # Note - this corresponds to the BCM pin number, not the physical pin number. + # So this is physical pin 19 on the Raspberry Pi header. + DIAG_GPIO_PIN = 10 # This is the maximum safe strobe current for the V3 LED V3_TARGET_LED_CURRENT_SETTING = 10.0 # amps From a6475b14324e93dae3cb3017472e6a35c898b413 Mon Sep 17 00:00:00 2001 From: Pete Attayek Date: Fri, 20 Mar 2026 11:02:55 -0400 Subject: [PATCH 6/9] Cleanup calibrate_strobe_output.py script --- .../web-server/calibrate_strobe_output.py | 263 +++++++++--------- 1 file changed, 126 insertions(+), 137 deletions(-) diff --git a/Software/web-server/calibrate_strobe_output.py b/Software/web-server/calibrate_strobe_output.py index 3ce2803c..d8bbb123 100644 --- a/Software/web-server/calibrate_strobe_output.py +++ b/Software/web-server/calibrate_strobe_output.py @@ -5,9 +5,9 @@ PiTrac Controller Board Strobe Output Calibration Module -Adjusts the strobe output to a default (or selected) current output by sending a range of values to the digital -potentiometer on the board (via SPI) and then repeatedly checking the ADC to see if the desired output has been -reached (as the LED current iteratively goes down as up DAC output goes down). +Adjusts the strobe output to a default (or selected) current output by sending a range of values to the DAC +on the board (via SPI) and then repeatedly checking the ADC to see if the desired output has been +reached (as the LED current iteratively goes down as the DAC output goes down). WARNING ------ This code has not been tested against actual hardware yet, so it could still be harmful to whatever hardware you are running on. Caveat emptor! @@ -71,7 +71,7 @@ class StrobeOutputCalibrator: ABSOLUTE_HIGHEST_LDO_VOLTAGE = 11 # volts MAX_DAC_SETTING = 0xFF # 255 # It's an 8-bit DAC, so max value is 2^8 - 1 - MIN_DAC_SETTING = 0 # Note - + MIN_DAC_SETTING = 0 # This value is just a guess for now and works on 1 board. It should be high enough so that the LDO voltage in all devices # (even accounting for variations) will be low enough not to hurt our standard strobe, but high enough to be above the minimum LDO voltage. @@ -82,7 +82,7 @@ class StrobeOutputCalibrator: # For the MCP4801 commands, see: https://ww1.microchip.com/downloads/en/DeviceDoc/22244B.pdf MCP4801_BASE_WRITE_CMD_SET_OUTPUT = 0b00110000 # Standard (1x) gain (bit 13) (Vout=Vref*D/4096), VREF buffered, active, no shutdown, D4-D7 are 0 - # For the MCP3202 commands, see: https://www.google.com/aclk?sa=L&ai=DChsSEwj3ncmJqIKTAxWQK60GHXKrGJQYACICCAEQABoCcHY&ae=2&co=1&ase=2&gclid=CjwKCAiAh5XNBhAAEiwA_Bu8Fae9UWeNq0dHjoQ9N4wHFrRnEvETyzrYVl5xmGvUyNR0uFNVP5IlQRoCaAcQAvD_BwE&cid=CAASWeRo2UUWEQuONJENmHLbquopI4y29-vAFb0frx7hQM6bjuRo_TZk1PIihnFuJO8jpNwTGiEFOnOQWgmJHHFTivCGeaaKvgUk8x67yLoRpN_bIoGgiweiu6Vk&cce=2&category=acrcp_v1_71&sig=AOD64_3LJYqvTzD3ozkW0pkVDNgovi6vvA&q&nis=4&adurl&ved=2ahUKEwi1w8KJqIKTAxXKFDQIHVsXEX8Q0Qx6BAgPEAE + # For the MCP3202 commands, see: https://ww1.microchip.com/downloads/en/DeviceDoc/21034F.pdf MCP3202_READ_CH0_SINGLE_ENDED_CMD = 0 | 0x80 # Channel 0, single-ended - LED Current Sense Resistor Voltage MCP3202_READ_CH1_SINGLE_ENDED_CMD = 0 | 0xc0 # Channel 1, single-ended - LDO Gate Voltage @@ -110,7 +110,7 @@ def setup_spi_channels(self): # Set SPI speed self.spi_dac.max_speed_hz = self.SPI_MAX_SPEED_HZ - # Set SPI mode common modes are 0 or 3) + # Set SPI mode (common modes are 0 or 3) self.spi_dac.mode = 0b00 # Mode 0 @@ -121,7 +121,7 @@ def setup_spi_channels(self): # Set SPI speed self.spi_adc.max_speed_hz = self.SPI_MAX_SPEED_HZ - # Set SPI mode common modes are 0 or 3) + # Set SPI mode (common modes are 0 or 3) self.spi_adc.mode = 0b00 # Mode 0 @@ -163,43 +163,30 @@ def close_gpio_system(self): if self.diag_pin is not None: self.diag_pin.close() - def get_ADC_value_CH0(self): - # Start bit is always the first byte, then the channel and mode bits are combined into the second byte, + def _get_ADC_value(self, channel_cmd): + # Start bit is always the first byte, then the channel and mode bits are combined into the second byte, # and the third byte is just a timing placeholder for the response, because we need the last 2 of 3 bytes of response, # but our command is only 2 bytes - message_to_send = [0x01, self.MCP3202_READ_CH0_SINGLE_ENDED_CMD, 0x00] + message_to_send = [0x01, channel_cmd, 0x00] logger.debug(f"Message to send to ADC (to get value): {[format(b, '02x') for b in message_to_send]}") response_bytes = self.spi_adc.xfer2(message_to_send) - # The result is 12-bits. The first byte returned is just random - the MISO line is null - # when the command is sent, so nothing was really sent. + # The result is 12-bits. The first byte returned is just random - the MISO line is null + # when the command is sent, so nothing was really sent. # The second byte contains the top 4 bits (masked with 0x0F as some bits may be null) # The third byte contains the least-significant 8 bits - + # Put the top 4 bits and lower 8 bits together to get the full 12-bit ADC value adc_value = (response_bytes[1] & 0x0F) << 8 | response_bytes[2] return adc_value - def get_ADC_value_CH1(self): - # Start bit is always the first byte, then the channel and mode bits are combined into the second byte, - # and the third byte is just a timing placeholder for the response, because we need the last 2 of 3 bytes of response, - # but our command is only 2 bytes - message_to_send = [0x01, self.MCP3202_READ_CH1_SINGLE_ENDED_CMD, 0x00] - logger.debug(f"Message to send to ADC (to get value): {[format(b, '02x') for b in message_to_send]}") - - response_bytes = self.spi_adc.xfer2(message_to_send) - - # The result is 12-bits. The first byte returned is just random - the MISO line is null - # when the command is sent, so nothing was really sent. - # The second byte contains the top 4 bits (masked with 0x0F as some bits may be null) - # The third byte contains the least-significant 8 bits - - # Put the top 4 bits and lower 8 bits together to get the full 12-bit ADC value - adc_value = (response_bytes[1] & 0x0F) << 8 | response_bytes[2] + def get_ADC_value_CH0(self): + return self._get_ADC_value(self.MCP3202_READ_CH0_SINGLE_ENDED_CMD) - return adc_value + def get_ADC_value_CH1(self): + return self._get_ADC_value(self.MCP3202_READ_CH1_SINGLE_ENDED_CMD) def get_LDO_voltage(self): @@ -241,19 +228,21 @@ def get_LED_current(self): # --- BEGIN DETERMINISTIC HARDWARE BLOCK --- diag.on() response_bytes = spi.xfer2(message_to_send) - diag.off() # --- END DETERMINISTIC HARDWARE BLOCK --- finally: # --- RETURN TO NORMAL OS BEHAVIOR --- - + + # Always turn off DIAG first — leaving it HIGH means the strobe stays on + diag.off() + # Give up real-time priority (return to normal scheduler) try: param = os.sched_param(0) os.sched_setscheduler(0, os.SCHED_OTHER, param) except (PermissionError, AttributeError): pass - + # Turn memory management back on gc.enable() @@ -332,125 +321,127 @@ def find_DAC_start_setting(self): def calibrate_board(self, target_LED_current): - # find the minimum safe LDO voltage to supply the MCP1407 gate driver - DAC_start_setting, LDO_voltage = self.find_DAC_start_setting() - - # If even a DAC value of 0 was below the ABSOLUTE_LOWEST_LDO_VOLTAGE then fail calibration - if DAC_start_setting < 0: - logger.debug(f"DAC value of 0 is below minimum LDO voltage ({format(self.ABSOLUTE_LOWEST_LDO_VOLTAGE, '0.2f')}): {format(LDO_voltage, '0.2f')}") - return False, -1, -1 + # find the minimum safe LDO voltage to supply the MCP1407 gate driver + DAC_start_setting, LDO_voltage = self.find_DAC_start_setting() + # If even a DAC value of 0 was below the ABSOLUTE_LOWEST_LDO_VOLTAGE then fail calibration + if DAC_start_setting < 0: + logger.debug(f"DAC value of 0 is below minimum LDO voltage ({format(self.ABSOLUTE_LOWEST_LDO_VOLTAGE, '0.2f')}): {format(LDO_voltage, '0.2f')}") + return False, -1, -1 - logger.debug(f"calibrate_board called with target_LED_current = {target_LED_current}, DAC_start_setting = 0x{format(DAC_start_setting, '02x')}") + logger.debug(f"calibrate_board called with target_LED_current = {target_LED_current}, DAC_start_setting = 0x{format(DAC_start_setting, '02x')}") - # Now, starting at the max DAC value (0xFF) - # we will iteratively decrease the DAC setting until we get to - # the desired ADC output (or just under it) + # Now, starting at the max DAC value (0xFF) + # we will iteratively decrease the DAC setting until we get to + # the desired ADC output (or just under it) - current_DAC_setting = DAC_start_setting - final_DAC_setting = self.MIN_DAC_SETTING + current_DAC_setting = DAC_start_setting + final_DAC_setting = self.MIN_DAC_SETTING - # just picking a number that we should always be above at the start of the loop, - # so that we can save the first reading as the best one so far even if it's not - # above the target - max_LED_current_so_far = 0.0 + # just picking a number that we should always be above at the start of the loop, + # so that we can save the first reading as the best one so far even if it's not + # above the target + max_LED_current_so_far = 0.0 - # We will start at the max DAC setting and then count down while - # looking for the point where the corresponding LED current goes just above the target_LED_current, - # then increase 1 value to ensure we are <= target_LED_current. + # We will start at the max DAC setting and then count down while + # looking for the point where the corresponding LED current goes just above the target_LED_current, + # then increase 1 value to ensure we are <= target_LED_current. - logger.debug(f"calibrate_board starting loop. Desired output is {target_LED_current}") + logger.debug(f"calibrate_board starting loop. Desired output is {target_LED_current}") - # Stop immediately if we ever have an error - while (current_DAC_setting >= self.MIN_DAC_SETTING): - - self.set_DAC(current_DAC_setting) - - # Wait a moment for the setting to take effect - self.short_pause() - - # check the LDO voltage to ensure that we are within the safe bounds - LDO_voltage = self.get_LDO_voltage() - # if we are below the ABSOLUTE_LOWEST_LDO_VOLTAGE, it is unsafe to pulse the DIAG pin. Decrease DAC value and continue - if LDO_voltage < self.ABSOLUTE_LOWEST_LDO_VOLTAGE: - logger.debug(f"Measured LDO_voltage ({LDO_voltage}) was below ABSOLUTE_LOWEST_LDO_VOLTAGE of {self.ABSOLUTE_LOWEST_LDO_VOLTAGE} volts. Trying next DAC value.") - # Continue counting down - final_DAC_setting = current_DAC_setting - current_DAC_setting -= 1 - continue - - # if we are above the ABSOLUTE_HIGHEST_LDO_VOLTAGE, then we have to stop and fail the calibration - if LDO_voltage > self.ABSOLUTE_HIGHEST_LDO_VOLTAGE: - logger.debug(f"Measured LDO_voltage ({LDO_voltage}) was above ABSOLUTE_HIGHEST_LDO_VOLTAGE of {self.ABSOLUTE_HIGHEST_LDO_VOLTAGE} volts. Stopping calibration, as something is wrong.") - return False, -1, -1 - - # Note reading the LED current also pulses the strobe through the DIAG pin, - # which is necessary to get a valid reading, but also means that we are toggling - # the strobe on and off repeatedly during this calibration process, which is not ideal. - # But we need to do it in order to get accurate LED current readings. - LED_current = self.get_LED_current() - logger.debug(f"current_DAC_setting: {format(current_DAC_setting, '02x')}, LED_current: {LED_current}") - - # As we are slowly increasing the LED current, have we reached our desired set-point for the LED current yet? - if LED_current > target_LED_current: - logger.debug(f" ---> Reached above the target_LED_current ({target_LED_current}). LED_current is: {LED_current}. Stopping calibration here...") - final_DAC_setting = current_DAC_setting + 1 # Step back to the last setting that was just before we reached our target - break + # Stop immediately if we ever have an error + while (current_DAC_setting >= self.MIN_DAC_SETTING): - # We have not yet reached the target. TBD - This is a little redundant, maybe change - # Keep track of where we were. - if LED_current > max_LED_current_so_far: + self.set_DAC(current_DAC_setting) - # Save the current output as the best one so far, even if it's not over the target, - # because we want to get as close as possible without going over - max_LED_current_so_far = LED_current + # Wait a moment for the setting to take effect + self.short_pause() + # check the LDO voltage to ensure that we are within the safe bounds + LDO_voltage = self.get_LDO_voltage() + # if we are below the ABSOLUTE_LOWEST_LDO_VOLTAGE, it is unsafe to pulse the DIAG pin. Decrease DAC value and continue + if LDO_voltage < self.ABSOLUTE_LOWEST_LDO_VOLTAGE: + logger.debug(f"Measured LDO_voltage ({LDO_voltage}) was below ABSOLUTE_LOWEST_LDO_VOLTAGE of {self.ABSOLUTE_LOWEST_LDO_VOLTAGE} volts. Trying next DAC value.") # Continue counting down final_DAC_setting = current_DAC_setting current_DAC_setting -= 1 + continue - # There are a couple of possible edge cases here. And either of them indicate that something probably went wrong somewhere even if we - # thought we had a success. - # If so, err on the safe side and consider this a failure - if current_DAC_setting <= self.MIN_DAC_SETTING: - logger.debug(f"Reached MIN_DAC_SETTING ({self.MIN_DAC_SETTING}) without ever reaching target_LED_current ({target_LED_current}). This generally indicates a problem. Failing calibration.") - return False, -1, -1 - if current_DAC_setting >= self.MAX_DAC_SETTING: - logger.debug(f"The MAX_DAC_SETTING resulted in an LED current above the target. This generally indicates a problem. Failing calibration.") + # if we are above the ABSOLUTE_HIGHEST_LDO_VOLTAGE, then we have to stop and fail the calibration + if LDO_voltage > self.ABSOLUTE_HIGHEST_LDO_VOLTAGE: + logger.debug(f"Measured LDO_voltage ({LDO_voltage}) was above ABSOLUTE_HIGHEST_LDO_VOLTAGE of {self.ABSOLUTE_HIGHEST_LDO_VOLTAGE} volts. Stopping calibration, as something is wrong.") return False, -1, -1 - # Now, using the best DAC setting we found, average the output voltage a few times to - # get a more accurate reading of the output voltage at that setting - # take an average of n pulses - n = 10 - while True: - self.set_DAC(final_DAC_setting) + # Note reading the LED current also pulses the strobe through the DIAG pin, + # which is necessary to get a valid reading, but also means that we are toggling + # the strobe on and off repeatedly during this calibration process, which is not ideal. + # But we need to do it in order to get accurate LED current readings. + LED_current = self.get_LED_current() + logger.debug(f"current_DAC_setting: {format(current_DAC_setting, '02x')}, LED_current: {LED_current}") + + # As we are slowly increasing the LED current, have we reached our desired set-point for the LED current yet? + if LED_current > target_LED_current: + logger.debug(f" ---> Reached above the target_LED_current ({target_LED_current}). LED_current is: {LED_current}. Stopping calibration here...") + final_DAC_setting = current_DAC_setting + 1 # Step back to the last setting that was just before we reached our target + break + + # We have not yet reached the target. TBD - This is a little redundant, maybe change + # Keep track of where we were. + if LED_current > max_LED_current_so_far: + + # Save the current output as the best one so far, even if it's not over the target, + # because we want to get as close as possible without going over + max_LED_current_so_far = LED_current + + # Continue counting down + final_DAC_setting = current_DAC_setting + current_DAC_setting -= 1 + + # There are a couple of possible edge cases here. And either of them indicate that something probably went wrong somewhere even if we + # thought we had a success. + # If so, err on the safe side and consider this a failure + if current_DAC_setting <= self.MIN_DAC_SETTING: + logger.debug(f"Reached MIN_DAC_SETTING ({self.MIN_DAC_SETTING}) without ever reaching target_LED_current ({target_LED_current}). This generally indicates a problem. Failing calibration.") + return False, -1, -1 + if current_DAC_setting >= self.MAX_DAC_SETTING: + logger.debug(f"The MAX_DAC_SETTING resulted in an LED current above the target. This generally indicates a problem. Failing calibration.") + return False, -1, -1 + + # Now, using the best DAC setting we found, average the output voltage a few times to + # get a more accurate reading of the output voltage at that setting + # take an average of n pulses + n = 10 + while True: + self.set_DAC(final_DAC_setting) + self.short_pause() + + # check if LDO voltage is above the minimum + LDO_voltage = self.get_LDO_voltage() + if LDO_voltage < self.ABSOLUTE_LOWEST_LDO_VOLTAGE: + # Fallback to the last known good measurement + final_DAC_setting -= 1 + break + + # Take an average of n pulses + LED_current_sum = 0 + for _ in range(n): + LED_current_sum += self.get_LED_current() self.short_pause() - - # check if LDO voltage is above the minimum - LDO_voltage = self.get_LDO_voltage() - if LDO_voltage < self.ABSOLUTE_LOWEST_LDO_VOLTAGE: - # Fallback to the last known good measurement - final_DAC_setting -= 1 - break + LED_current = LED_current_sum / n - # Take an average of n pulses - LED_current_sum = 0 - for _ in range(n): - LED_current_sum += self.get_LED_current() - self.short_pause() - LED_current = LED_current_sum / n - - if LED_current > target_LED_current: - # Current is still slightly too high, step the DAC setting - final_DAC_setting += 1 - else: - # We are at or below the target current, we're done - break + if LED_current > target_LED_current: + # Current is still slightly too high, step the DAC setting + final_DAC_setting += 1 + if final_DAC_setting > self.MAX_DAC_SETTING: + logger.error(f"Averaging loop exceeded MAX_DAC_SETTING ({self.MAX_DAC_SETTING}) without reaching target current. Failing calibration.") + return False, -1, -1 + else: + # We are at or below the target current, we're done + break - logger.debug(f"calibrate_board -- final_DAC_setting: {format(final_DAC_setting, '02x')}, LED_current: {LED_current}") + logger.debug(f"calibrate_board -- final_DAC_setting: {format(final_DAC_setting, '02x')}, LED_current: {LED_current}") - return True, final_DAC_setting, LED_current + return True, final_DAC_setting, LED_current def cleanup_for_exit(self): @@ -474,7 +465,7 @@ def main(): parser.add_argument("-v", "--verbose", action="store_true", help="Verbose output") parser.add_argument("-q", "--quiet", action="store_true", help="Quiet output") parser.add_argument("-w", "--overwrite", action="store_true", help="Overwrites any existing strobe setting in user_settings.json") - parser.add_argument("--target_output", default=0,type=float, help="Set target LED current output (in volts) (ADVANCED)") + parser.add_argument("--target_output", default=0,type=float, help="Set target LED current output (in amps) (ADVANCED)") parser.add_argument("--ignore", action="store_true", help="Attempt calibration even if the Controller Board version is not 3.0 (ADVANCED)") action_group = parser.add_mutually_exclusive_group() action_group.add_argument("-p", "--print_settings", action="store_true", help="Print the current DAC setting and last ADC measurement from the user_settings.json file") @@ -497,11 +488,11 @@ def main(): logger.debug(f"Calibrator initialized") - if calibrator.setup_spi_channels() == False: + if not calibrator.setup_spi_channels(): logger.error(f"SPI initialization failed. Cannot proceed with calibration.") return 1 - if calibrator.open_gpio_system() == False: + if not calibrator.open_gpio_system(): logger.error(f"GPIO initialization failed. Cannot proceed with calibration.") calibrator.close_gpio_system() return 1 @@ -632,11 +623,9 @@ def main(): except KeyboardInterrupt: print("\nCtrl+C pressed. Performing cleanup...") - # Add your cleanup code here (e.g., closing files, releasing resources) - calibrator.cleanup_for_exit() except Exception as e: - logger.debug(f"An error occurred: {e}") + logger.error(f"An error occurred: {e}") calibrator.set_DAC_to_safest_level() return 1 # Failure From 254c5c38c68bb5a765b7a26cce5393e5bad62780 Mon Sep 17 00:00:00 2001 From: Pete Attayek Date: Fri, 20 Mar 2026 18:40:46 -0400 Subject: [PATCH 7/9] change old led current to 9A --- Software/web-server/calibrate_strobe_output.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Software/web-server/calibrate_strobe_output.py b/Software/web-server/calibrate_strobe_output.py index d8bbb123..6aeef664 100644 --- a/Software/web-server/calibrate_strobe_output.py +++ b/Software/web-server/calibrate_strobe_output.py @@ -63,7 +63,7 @@ class StrobeOutputCalibrator: # This is the maximum safe strobe current for the V3 LED V3_TARGET_LED_CURRENT_SETTING = 10.0 # amps # This is the maximum safe strobe current for the old 100W LED - OLD_TARGET_LED_CURRENT_SETTING = 7.0 # amps + OLD_TARGET_LED_CURRENT_SETTING = 9.0 # amps # We should NEVER go below this LDO voltage From a1279142ae67773794235c0fb325b17bdbf98b23 Mon Sep 17 00:00:00 2001 From: Pete Attayek Date: Fri, 20 Mar 2026 18:41:39 -0400 Subject: [PATCH 8/9] Add installation files for spi1 setup and Python runtime dependencies --- packaging/build.sh | 2 +- packaging/scripts/configure-cameras.sh | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/packaging/build.sh b/packaging/build.sh index 2e109739..3a370683 100755 --- a/packaging/build.sh +++ b/packaging/build.sh @@ -349,7 +349,7 @@ build_dev() { done # Python runtime dependencies for CLI tool - for pkg in python3 python3-pip python3-yaml python3-opencv python3-numpy; do + for pkg in python3 python3-pip python3-yaml python3-opencv python3-numpy python3-lgpio python3-rpi-lgpio python3-gpiozero; do if ! dpkg -l | grep -q "^ii $pkg"; then missing_deps+=("$pkg") fi diff --git a/packaging/scripts/configure-cameras.sh b/packaging/scripts/configure-cameras.sh index 0422461e..be3f9608 100755 --- a/packaging/scripts/configure-cameras.sh +++ b/packaging/scripts/configure-cameras.sh @@ -148,10 +148,9 @@ configure_boot_config() { log_info " Slot 2 type: ${slot2_type:-none}" log_info " Has InnoMaker: $has_innomaker" - # Skip if no cameras detected + # Issue warning if no cameras detected, but continue to configure base system parameters if [[ "$num_cameras" -eq 0 ]]; then - log_warn "No cameras detected, skipping config.txt configuration" - return 0 + log_warn "No cameras detected. Camera-specific overlays will be skipped, but base system parameters will be configured." fi backup_config_txt "$config_path" @@ -188,6 +187,13 @@ dtparam=spi=on" log_info " dtparam=spi=on already exists, skipping" fi + if ! grep -q "^dtoverlay=spi1-2cs" "$config_path"; then + config_block="$config_block +dtoverlay=spi1-2cs" + else + log_info " dtoverlay=spi1-2cs already exists, skipping" + fi + if ! grep -q "^force_turbo=" "$config_path"; then config_block="$config_block force_turbo=1" From 3b8bedd08e5c45f094f4f7a5f236991a3c3e3666 Mon Sep 17 00:00:00 2001 From: Pete Attayek Date: Fri, 20 Mar 2026 21:05:32 -0400 Subject: [PATCH 9/9] Update wiring diagram and documentation --- .../PiTrac_V3_Connection_Guide.svg | 2 +- docs/hardware/pcb-assembly.md | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/assets/images/enclosure_assembly/PiTrac_V3_Connection_Guide.svg b/docs/assets/images/enclosure_assembly/PiTrac_V3_Connection_Guide.svg index 458a8234..e26cf64a 100644 --- a/docs/assets/images/enclosure_assembly/PiTrac_V3_Connection_Guide.svg +++ b/docs/assets/images/enclosure_assembly/PiTrac_V3_Connection_Guide.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/docs/hardware/pcb-assembly.md b/docs/hardware/pcb-assembly.md index 5f2f5416..2f444e8b 100644 --- a/docs/hardware/pcb-assembly.md +++ b/docs/hardware/pcb-assembly.md @@ -233,12 +233,12 @@ After assembly, must run current calibration before you will be able to capture | V3 Connector Board | Raspberry Pi 5 | |----------|----------| | GND | GND (Pin 39) | -| DIAG | GPIO 20 (Pin 38) | -| CS0 | GPIO 8 (Pin 24) | -| MOSI | GPIO 10 (Pin 19) | -| MISO | GPIO 9 (Pin 21) | -| CLK | GPIO 11 (Pin 23) | -| CS1 | GPIO 7 (Pin 26) | +| DIAG | GPIO 10 (Pin 19) | +| CS0 | GPIO 18 (Pin 12) | +| MOSI | GPIO 20 (Pin 38) | +| MISO | GPIO 19 (Pin 35) | +| CLK | GPIO 21 (Pin 40) | +| CS1 | GPIO 17 (Pin 11) | | V3P3 | 3V3 (Pin 1) | | Global Shutter Camera 2 | Raspberry Pi 5 |