Untitled

 avatar
unknown
plain_text
a year ago
15 kB
17
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)
Editor is loading...
Leave a Comment