Untitled
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