Untitled
unknown
plain_text
a year ago
15 kB
8
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