Untitled

 avatar
unknown
plain_text
a month ago
15 kB
1
Indexable
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