Untitled
import customtkinter as ctk from PIL import Image, ImageTk, ImageDraw import json import time import threading from gpiozero import OutputDevice, Button import board import busio import adafruit_vl53l0x from adafruit_tca9548a import TCA9548A from adafruit_pca9685 import PCA9685 from adafruit_motor import servo class TurntableController: def __init__(self): self.is_initialized = False # GPIO setup self.direction_pin = OutputDevice(20) self.pulse_pin = OutputDevice(21) self.hall_sensor = Button(18, pull_up=True) self.enable_plus = OutputDevice(23) self.enable_minus = OutputDevice(24) # Initialize I2C and hardware interface try: self.i2c = busio.I2C(board.SCL, board.SDA) self.mux = TCA9548A(self.i2c) self.pca = PCA9685(self.mux[3]) self.pca.frequency = 50 self.servo_motor = servo.Servo(self.pca.channels[0], min_pulse=1300, max_pulse=3600) self.servo_motor.angle = 0 except Exception as e: print(f"Error initializing hardware: {e}") return # Position tracking self.current_position = 0 self.is_calibrated = False self.positions = { 'bin1': 0, 'bin2': 90, 'bin3': 180, 'bin4': 270 } # Motor control parameters self.steps_per_revolution = 1600 self.step_delay = 0.001 self.direction_delay = 0.001 self.enable_delay = 0.2 # Movement queue self.movement_queue = [] self.is_moving = False # Initialize self.disable_motor() self.calibrate() self.is_initialized = True def calibrate(self): """Find home position using hall sensor""" print("Calibrating turntable...") try: self.enable_motor() time.sleep(0.1) self.direction_pin.value = True attempts = 0 while not self.hall_sensor.is_pressed: self.pulse_pin.on() time.sleep(0.002) self.pulse_pin.off() time.sleep(0.002) attempts += 1 if attempts % 100 == 0: print(f"Searching for home... {attempts} steps") print("Home position found!") self.current_position = 0 self.is_calibrated = True finally: time.sleep(0.1) self.disable_motor() def enable_motor(self): self.enable_minus.on() self.enable_plus.off() def disable_motor(self): self.enable_minus.off() self.enable_plus.on() def move_to_bin(self, bin_id): """Queue movement to specified bin""" if not bin_id in self.positions: print(f"Invalid bin ID: {bin_id}") return self.movement_queue.append(bin_id) if not self.is_moving: self._process_movement_queue() def _process_movement_queue(self): """Process queued movements""" if not self.movement_queue: self.is_moving = False return self.is_moving = True target_bin = self.movement_queue.pop(0) try: if not self.is_calibrated: self.calibrate() target_position = self.positions[target_bin] steps, direction = self._calculate_steps(target_position) self.enable_motor() time.sleep(self.enable_delay) self.direction_pin.value = direction time.sleep(0.01) for _ in range(steps): self.pulse_pin.on() time.sleep(self.step_delay) self.pulse_pin.off() time.sleep(self.step_delay) time.sleep(0.01) self.current_position = target_position finally: self.disable_motor() threading.Timer(0.1, self._process_movement_queue).start() def _calculate_steps(self, target_position): """Calculate steps needed to reach target position""" diff = target_position - self.current_position if abs(diff) > 180: if diff > 0: diff -= 360 else: diff += 360 compensation_factor = 1.02 steps = int(abs(diff) * self.steps_per_revolution * compensation_factor / 360) direction = diff > 0 return steps, direction def cleanup(self): """Cleanup hardware resources""" try: if hasattr(self, 'servo_motor'): self.servo_motor.angle = 0 time.sleep(0.5) if hasattr(self, 'pca'): for channel in self.pca.channels: try: channel.duty_cycle = 0 except: pass self.pca.deinit() if hasattr(self, 'i2c'): self.i2c.deinit() except Exception as e: print(f"Error during cleanup: {e}") class CloseButton(ctk.CTkCanvas): def __init__(self, parent, command, size=30, **kwargs): super().__init__(parent, width=size, height=size, **kwargs) self.size = size self.command = command self.configure(bg='#1c1c1e', highlightthickness=0) self.bind('<Button-1>', lambda e: command()) self.bind('<Enter>', self.on_enter) self.bind('<Leave>', self.on_leave) self.draw() def draw(self, hover=False): self.delete('all') color = '#ff3b30' if hover else '#86868b' padding = self.size * 0.3 self.create_line(padding, padding, self.size-padding, self.size-padding, fill=color, width=2) self.create_line(self.size-padding, padding, padding, self.size-padding, fill=color, width=2) def on_enter(self, event): self.draw(hover=True) def on_leave(self, event): self.draw(hover=False) class CircularProgress(ctk.CTkCanvas): _instances = [] _turntable = None @classmethod def set_turntable(cls, turntable): cls._turntable = turntable def __init__(self, parent, bin_id, size=200, base_color='#f5f5f7'): super().__init__( parent, width=size, height=size, bg='white', highlightthickness=0 ) self._destroyed = False CircularProgress._instances.append(self) # Store bin ID and configuration self.bin_id = bin_id self.bin_config = self.load_bin_config(bin_id) # Initialize properties self.size = size self.stroke_width = 8 self.radius = (size - self.stroke_width) / 2 self.center = size / 2 self.fill_level = 0 self.base_color = base_color self.dark_mode = False # Enhanced animation properties self.last_press_time = 0 self.cooldown_period = 5.0 self.can_press = True self.is_pressed = False self.press_scale = 1.0 self.press_animation_active = False self.servo_active = False # Pulse animation properties self.pulse_active = False self.pulse_scale = 1.0 self.pulse_growing = True self.PULSE_MIN = 0.9 self.PULSE_MAX = 1.0 self.PULSE_STEP = 0.007 # Movement timing self.SERVO_OPEN_TIME = 10 self.servo_timer = None # Create image for drawing self.im = Image.new('RGBA', (1000, 1000)) self.arc = None # Bind events self.bind('<Button-1>', self.on_press) self.bind('<ButtonRelease-1>', self.on_release) self.bind('<Destroy>', self._on_destroy) # Initialize self.target_fill_level = self.fill_level self.animate() self.load_fill_level() self.draw() def load_bin_config(self, bin_id): try: with open('bin_config.json', 'r') as f: config = json.load(f) return next((bin_config for bin_config in config['bins'] if bin_config['id'] == bin_id), None) except Exception as e: print(f"Error loading bin config: {e}") return { 'id': bin_id, 'name': f'Bin {bin_id[-1]}', 'color': '#e5e5e7', 'color_dark': '#2c2c2e', 'is_default': True } def animate(self): if self._destroyed: return current_time = time.time() # Update can_press status if not self.can_press and current_time - self.last_press_time >= self.cooldown_period: self.can_press = True # Handle fill level animation if self.target_fill_level != self.fill_level: diff = self.target_fill_level - self.fill_level self.fill_level += diff * 0.2 if abs(diff) < 0.5: self.fill_level = self.target_fill_level # Handle press animation if self.press_animation_active: if self.is_pressed: self.press_scale = max(self.PULSE_MIN, self.press_scale - self.PULSE_STEP) if self.press_scale == self.PULSE_MIN: self.press_animation_active = self.is_pressed else: self.press_scale = min(self.PULSE_MAX, self.press_scale + self.PULSE_STEP) if self.press_scale == self.PULSE_MAX: self.press_animation_active = False # Handle pulse animation during servo active state if self.servo_active: if self.pulse_growing: self.pulse_scale = min(self.PULSE_MAX, self.pulse_scale + self.PULSE_STEP) if self.pulse_scale >= self.PULSE_MAX: self.pulse_growing = False else: self.pulse_scale = max(self.PULSE_MIN, self.pulse_scale - self.PULSE_STEP) if self.pulse_scale <= self.PULSE_MIN: self.pulse_growing = True else: if self.pulse_scale != 1.0: self.pulse_scale = min(1.0, self.pulse_scale + self.PULSE_STEP) # Apply combined scale final_scale = self.press_scale * self.pulse_scale # Only redraw if there's been a change if self.press_animation_active or self.servo_active or self.target_fill_level != self.fill_level: self.draw(scale=final_scale) self.after(16, self.animate) def handle_manual_fill(self): """Handle manual filling of the bin""" if not self._turntable or not self._turntable.is_initialized: return # Check if any other bin is active for instance in self._instances: if instance != self and instance.servo_active: return if self.servo_active: self.close_servo() else: self.start_filling_process() def start_filling_process(self): """Start the bin filling process""" try: # Move to bin position self._turntable.move_to_bin(self.bin_id) # Open servo and start visual feedback if hasattr(self._turntable, 'servo_motor'): self._turntable.servo_motor.angle = 90 self.servo_active = True # Start auto-close timer self.servo_timer = self.after( int(self.SERVO_OPEN_TIME * 1000), self.close_servo ) except Exception as e: print(f"Error in fill process: {e}") self.close_servo() def close_servo(self): """Close the servo and update fill level""" try: # Cancel any pending timer if self.servo_timer: self.after_cancel(self.servo_timer) self.servo_timer = None # Close servo if hasattr(self._turntable, 'servo_motor'): self._turntable.servo_motor.angle = 0 self.servo_active = False time.sleep(0.5) # Wait for contents to settle # Update fill level here if needed # You can add your fill level measurement code here except Exception as e: print(f"Error closing servo: {e}") # Ensure servo is closed try: if hasattr(self._turntable, 'servo_motor'): self._turntable.servo_motor.angle = 0 self.servo_active = False except: pass def on_press(self, event): current_time = time.time() if current_time - self.last_press_time >= self.cooldown_period: self.is_pressed = True self.press_animation_active = True self.last_press_time = current_time self.can_press = False print(f"{self.bin_id} button pressed") # Handle manual fill behavior self.handle_manual_fill() # ... (rest of the CircularProgress class implementation remains the same) def create_main_interface(): root = ctk.CTk() root.title("Bin Interface") root.geometry("1024x600+0+0") root.overrideredirect(True) # Set dark mode (can be changed) dark_mode = False bg_color = '#1c1c1e' if dark_mode else '#f5f5f7' # Initialize turntable turntable = TurntableController() CircularProgress.set_turntable(turntable)
Leave a Comment