Adafruit_Learning_System_Gu.../TMC2209_Camera_Slider/CircuitPython/code.py
2025-05-05 16:05:15 -04:00

575 lines
23 KiB
Python

# SPDX-FileCopyrightText: 2025 Liz Clark for Adafruit Industries
#
# SPDX-License-Identifier: MIT
import time
import supervisor
import rotaryio
import keypad
import board
import busio
import displayio
from adafruit_display_text import label
from fourwire import FourWire
from adafruit_st7789 import ST7789
from adafruit_bitmap_font import bitmap_font
from adafruit_tmc2209 import TMC2209
displayio.release_displays()
RAILS = 520 # length of rails in mm
microsteps = 128
gear_ratio = 41 / 16
shot_velocities = [
20,
15,
10
]
keys = keypad.Keys((board.D2, board.A2, board.A3), value_when_pressed=False, pull=True)
encoder = rotaryio.IncrementalEncoder(board.D7, board.D6)
last_position = None
spi = board.SPI()
tft_cs = board.D10
tft_dc = board.D8
display_bus = FourWire(spi, command=tft_dc, chip_select=tft_cs, reset=board.D9)
display = ST7789(display_bus, width=240, height=240, rowstart=80, auto_refresh=False)
splash = displayio.Group()
display.root_group = splash
bitmap = displayio.OnDiskBitmap(open("/icons.bmp", "rb"))
grid_bg = displayio.TileGrid(bitmap, pixel_shader=bitmap.pixel_shader,
tile_height=100, tile_width=100,
x=(display.width - 100) // 2,
y=(display.height - 100) // 2)
splash.append(grid_bg)
text_group = displayio.Group()
font = bitmap_font.load_font("/Arial-14.bdf")
title_text = "Camera Slider"
title_area = label.Label(font, text=title_text, color=0xFFFFFF)
title_area.anchor_point = (0.5, 0.0)
title_area.anchored_position = (display.width / 2, 25)
text_group.append(title_area)
splash.append(text_group)
font = bitmap_font.load_font("/Arial-14.bdf")
text_area = label.Label(font, text="", color=0xFFFFFF)
text_area.anchor_point = (0.5, 1.0)
text_area.anchored_position = (display.width / 2, display.height - 25)
text_group.append(text_area)
uart = busio.UART(tx=board.TX, rx=board.RX, baudrate=115200, timeout=0.1)
driver1 = TMC2209(uart=uart, addr=0)
driver2 = TMC2209(tx_pin=board.D4, rx_pin=board.D5, addr=0)
version1 = driver1.version
version2 = driver2.version
print(f"TMC2209 #1 Version: 0x{version1:02X}")
print(f"TMC2209 #2 Version: 0x{version2:02X}")
driver1.microsteps = microsteps
print(driver1.microsteps)
driver2.microsteps = microsteps
print(driver2.microsteps)
STEPS_PER_MM = 200 * microsteps / 8
driver1.direction = False
driver2.direction = True
last_pos = 0
select = 0
menu = 0
time_mode = 0
shot_mode = 0
timelapse = True
movement_time = 0
titles = ["Camera Slider", "Motor 1", "Motor 2", "Mode",
"Timelapse", "One-Shot", "Start?", "Running"]
home_text = ["Press to Begin", "0"]
motor1_text = ["Slide to Start Point", "0"]
motor2_text = ["Move to Start", "Move to End", "0"]
mode_text = ["Timelapse", "One-Shot"]
time_text = ["1", "5", "10", "15", "30"]
shot_speeds = [10, 5, 2]
speeds = []
shot_text = ["Slow", "Medium", "Fast"]
start_text = ["Go!", 0]
running_text = ["STOP!", "Pause/Resume"]
running_icons = [6, 7]
mode_icons = [3, 4]
sub_titles = [home_text, motor1_text, motor2_text, mode_text,
time_text, shot_text, start_text, running_text]
motor2_coordinates = [0.0, 0.0]
text_area.text = home_text[0]
display.refresh()
def adv_menu(m):
m = (m + 1) % 8
title_area.text = titles[m]
sub = sub_titles[m]
if m == 4:
grid_bg[0] = 3
elif m == 5:
grid_bg[0] = 4
elif m > 5:
grid_bg[0] = m - 1
else:
grid_bg[0] = m
text_area.text = sub[0]
display.refresh()
return m
motor1_movement = {
"is_active": False,
"current_step": 0,
"total_steps": 0,
"start_pos": 0,
"end_pos": 0,
"step_direction": 1,
"last_step_time": 0,
"step_interval": 0,
"is_paused": False,
"toggle_pause": False,
"stop_requested": False
}
motor2_movement = {
"is_active": False,
"current_step": 0,
"total_steps": 0,
"start_pos": 0,
"end_pos": 0,
"step_direction": 1,
"last_step_time": 0,
"step_interval": 0,
"is_paused": False,
"toggle_pause": False,
"stop_requested": False
}
# pylint: disable=too-many-branches, too-many-statements, inconsistent-return-statements
def calculate_linear_velocity(steps_per_second, clock_frequency=12000000,
micro=128, scaling_factor=6):
frequency = steps_per_second * micro
vactual = int((frequency * (1 << 23)) / (clock_frequency * scaling_factor))
vactual = max(-(1 << 23), min((1 << 23) - 1, vactual))
return vactual
def move_steps_over_time(camera_driver, start_position, end_position,
time_seconds, micro=128, ratio=None):
steps = abs(end_position - start_position)
if camera_driver:
direction = -1 if end_position < start_position else 1
time_seconds = time_seconds * 2
else:
direction = 1 if driver1.direction else -1
if ratio is not None:
steps = steps / ratio
total_microsteps = steps * micro
microsteps_per_second = total_microsteps / time_seconds
fCLK = 12000000
if camera_driver:
vactual = int(microsteps_per_second / (fCLK / (1 << 24)))
else:
vactual = int(microsteps_per_second / (fCLK / (1 << 27)))
velocity = max(-(1 << 23), min((1 << 23) - 1, vactual))
velocity *= direction
return velocity
def calculate_timelapse_velocity(start_position, end_position, duration_seconds, micro=128,
clock_frequency=12000000, scaling_factor=6, min_velocity=100):
total_steps = abs(end_position - start_position)
steps_per_second = total_steps / duration_seconds
full_steps_per_second = steps_per_second / micro
vactual = calculate_linear_velocity(full_steps_per_second, clock_frequency,
micro, scaling_factor)
direction = -1 if end_position < start_position else 1
if abs(vactual) < min_velocity and vactual != 0:
vactual = min_velocity * direction
return vactual
def calculate_rail_velocity(total_steps, duration_sec, direction,
is_timelapse=True, micro=128, clock_frequency=12000000):
steps_per_second = total_steps / duration_sec
full_steps_per_second = steps_per_second / micro
if not is_timelapse:
base_scaling = 1.0
min_velocity = 400
vactual = int((full_steps_per_second * micro * (1 << 23))
/ (clock_frequency * base_scaling))
vactual *= direction
if abs(vactual) < min_velocity:
vactual = min_velocity * direction
else:
base_scaling = 6.0
min_velocity = 50
vactual = int((full_steps_per_second * micro * (1 << 23)) /
(clock_frequency * base_scaling))
vactual *= direction
if abs(vactual) < min_velocity:
vactual = min_velocity * direction
vactual = max(-(1 << 23), min((1 << 23) - 1, vactual))
return vactual
def move_motor_with_rotate(driver, movement_state, start_position=None,
end_position=None, duration_sec=0, micro=128):
if start_position is not None and end_position is not None and not movement_state["is_active"]:
if timelapse:
driver.enable_motor(run_current=20)
scaling_factor = 6
min_velocity = 50
velocity = calculate_timelapse_velocity(
start_position,
end_position,
duration_sec,
micro,
scaling_factor=scaling_factor,
min_velocity=min_velocity
)
else:
driver.enable_motor(run_current=30)
velocity = calculate_rail_velocity(
int(RAILS*STEPS_PER_MM),
duration_sec,
movement_state["step_direction"],
is_timelapse=timelapse,
micro=micro
)
initial_velocity = int(velocity * 0.2)
if abs(initial_velocity) < 200:
initial_velocity = 200 * (1 if velocity > 0 else -1)
driver.rotate(initial_velocity)
movement_state["initial_velocity"] = initial_velocity
movement_state["final_velocity"] = velocity
movement_state["ramp_up_done"] = False
movement_state["ramp_up_time"] = 500
movement_state["total_steps"] = int(RAILS*STEPS_PER_MM)
movement_state["step_direction"] = 1 if end_position > start_position else -1
movement_state["start_pos"] = 0
movement_state["end_pos"] = int(RAILS*STEPS_PER_MM)
movement_state["movement_start_time"] = supervisor.ticks_ms()
movement_state["movement_duration_ms"] = duration_sec * 1000
movement_state["is_active"] = True
movement_state["is_paused"] = False
return
movement_state["total_steps"] = int(RAILS*STEPS_PER_MM)
movement_state["step_direction"] = driver.direction
movement_state["start_pos"] = 0
movement_state["end_pos"] = int(RAILS*STEPS_PER_MM)
if duration_sec > 0 and movement_state["total_steps"] > 0:
movement_state["velocity"] = velocity
driver.rotate(velocity)
movement_state["movement_start_time"] = supervisor.ticks_ms()
movement_state["movement_duration_ms"] = duration_sec * 1000
else:
default_velocity = 2000 * movement_state["step_direction"]
driver.rotate(default_velocity)
movement_state["movement_duration_ms"] = movement_state["total_steps"] * 10
movement_state["movement_start_time"] = supervisor.ticks_ms()
movement_state["is_active"] = True
movement_state["is_paused"] = False
if movement_state["is_active"] and movement_state["toggle_pause"]:
movement_state["is_paused"] = not movement_state["is_paused"]
movement_state["toggle_pause"] = False
if movement_state["is_paused"]:
driver.rotate(0)
movement_state["pause_time"] = supervisor.ticks_ms()
else:
elapsed_ms = movement_state["pause_time"] - movement_state["movement_start_time"]
remaining_ms = movement_state["movement_duration_ms"] - elapsed_ms
if remaining_ms > 0:
driver.rotate(movement_state["velocity"])
movement_state["movement_start_time"] = supervisor.ticks_ms() - elapsed_ms
else:
driver.rotate(0)
driver.disable_motor()
movement_state["is_active"] = False
if movement_state["is_active"] and movement_state["stop_requested"]:
driver.rotate(0)
driver.disable_motor()
movement_state["is_active"] = False
movement_state["stop_requested"] = False
return {
"active": False,
"complete": False,
"progress_percent": (supervisor.ticks_ms() - movement_state["movement_start_time"])
/ movement_state["movement_duration_ms"] * 100,
"stopped_by_user": True
}
if movement_state["is_active"] and not movement_state["is_paused"]:
current_t = supervisor.ticks_ms()
e = current_t - movement_state["movement_start_time"]
if e >= movement_state["movement_duration_ms"]:
print("Movement time complete!")
driver.rotate(0)
driver.disable_motor()
movement_state["is_active"] = False
return {
"active": False,
"complete": True,
"progress_percent": 100,
"stopped_by_user": False
}
return {
"active": movement_state["is_active"],
"paused": movement_state["is_paused"],
"progress_percent": (supervisor.ticks_ms() - movement_state["movement_start_time"]) /
movement_state["movement_duration_ms"] * 100
if movement_state["is_active"] else 0,
"stopped_by_user": False
}
def pause_resume_motor1():
motor1_movement["toggle_pause"] = True
def stop_motor1():
driver1.disable_motor()
driver1.reset_position()
motor1_movement["stop_requested"] = True
def pause_resume_motor2():
motor2_movement["toggle_pause"] = True
def stop_motor2():
driver2.disable_motor()
driver2.reset_position()
motor2_movement["stop_requested"] = True
def stop_all_motors():
driver1.rotate(0)
driver2.rotate(0)
driver1.disable_motor()
driver2.disable_motor()
motor1_movement["is_active"] = False
motor2_movement["is_active"] = False
motor1_movement["stop_requested"] = False
motor2_movement["stop_requested"] = False
time.sleep(0.1)
driver1.disable_motor()
driver1.reset_position()
driver2.reset_position()
while True:
if motor1_movement["is_active"]:
current_time = supervisor.ticks_ms()
elapsed = current_time - motor1_movement["movement_start_time"]
if elapsed >= motor1_movement["movement_duration_ms"]:
driver1.rotate(0)
driver1.disable_motor()
motor1_movement["is_active"] = False
if motor2_movement["is_active"]:
current_time = supervisor.ticks_ms()
elapsed = current_time - motor2_movement["movement_start_time"]
if elapsed >= motor2_movement["movement_duration_ms"]:
driver2.rotate(0)
driver2.disable_motor()
motor2_movement["is_active"] = False
if menu == 7:
active_motors = 0
progress1 = 0
progress2 = 0
if motor1_movement["is_active"]:
active_motors += 1
current_time = supervisor.ticks_ms()
elapsed = current_time - motor1_movement["movement_start_time"]
progress1 = (elapsed / motor1_movement["movement_duration_ms"]) * 100
if motor2_movement["is_active"]:
active_motors += 1
current_time = supervisor.ticks_ms()
elapsed = current_time - motor2_movement["movement_start_time"]
progress2 = (elapsed / motor2_movement["movement_duration_ms"]) * 100
if active_motors > 0:
avg_progress = (progress1 + progress2) / active_motors
text_area.text = f"{running_text[select]} {avg_progress:.1f}%"
display.refresh()
elif active_motors == 0 and (motor1_movement["movement_duration_ms"] > 0
or motor2_movement["movement_duration_ms"] > 0):
text_area.text = "Movement Complete!"
display.refresh()
event = keys.events.get()
if event:
if event.pressed:
print(f"{event.key_number} pressed")
if event.key_number == 0:
if menu == 0:
menu = adv_menu(menu)
elif menu == 2:
if select == 0:
motor2_coordinates[select] = driver2.position
if select == 1:
motor2_coordinates[select] = driver2.position
select += 1
text_area.text = motor2_text[select]
if select > 1:
select = 0
menu = adv_menu(menu)
if motor2_coordinates[0] > motor2_coordinates[1]:
move = motor2_coordinates[0] - motor2_coordinates[1]
else:
move = motor2_coordinates[1] - motor2_coordinates[0]
move = -move
driver2.step(move)
elif menu == 3:
if select == 1:
timelapse = False
menu += 1
select = 0
else:
timelapse = True
menu = adv_menu(menu)
elif menu == 4:
menu += 1
time_mode = select
menu = adv_menu(menu)
select = 0
print(f"{time_text[time_mode]}, timelapse: {timelapse}")
elif menu == 5:
shot_mode = select
menu = adv_menu(menu)
select = 0
print(f"{shot_text[shot_mode]}, timelapse: {timelapse}")
elif menu == 6:
menu = adv_menu(menu)
if timelapse:
movement_time = int(time_text[time_mode]) * 60
print(f"starting a timelapse for {time_text[time_mode]} minutes")
status1 = move_motor_with_rotate(
driver1,
motor1_movement,
start_position=0,
end_position=int(RAILS * STEPS_PER_MM),
duration_sec=movement_time,
microsteps=microsteps
)
if abs(motor2_coordinates[1] - motor2_coordinates[0]) > 0:
velocity2 = move_steps_over_time(camera_driver=True,
start_position=motor2_coordinates[0],
end_position=motor2_coordinates[1],
time_seconds=movement_time,
microsteps=microsteps,
ratio=gear_ratio)
print(f"driver2 velocity is: {velocity2}")
driver2.enable_motor(run_current=25)
driver2.rotate(velocity2)
motor2_movement["is_active"] = True
motor2_movement["start_pos"] = motor2_coordinates[0]
motor2_movement["end_pos"] = motor2_coordinates[1]
motor2_movement["movement_start_time"] = supervisor.ticks_ms()
motor2_movement["movement_duration_ms"] = movement_time * 1000
motor2_movement["velocity"] = velocity2
motor2_movement["total_steps"] = (abs(motor2_coordinates[1] -
motor2_coordinates[0]))
else:
print(f"starting a {shot_text[shot_mode]} one-shot")
movement_time = shot_velocities[shot_mode]
status1 = move_motor_with_rotate(
driver1,
motor1_movement,
start_position=0,
end_position=int(RAILS * STEPS_PER_MM),
duration_sec=movement_time,
microsteps=microsteps
)
if abs(motor2_coordinates[1] - motor2_coordinates[0]) > 0:
velocity2 = move_steps_over_time(camera_driver=True,
start_position=motor2_coordinates[0],
end_position=motor2_coordinates[1],
time_seconds=movement_time,
microsteps=microsteps,
ratio=gear_ratio)
driver2.enable_motor(run_current=25)
driver2.rotate(velocity2)
motor2_movement["is_active"] = True
motor2_movement["start_pos"] = motor2_coordinates[0]
motor2_movement["end_pos"] = motor2_coordinates[1]
motor2_movement["movement_start_time"] = supervisor.ticks_ms()
motor2_movement["movement_duration_ms"] = movement_time * 1000
motor2_movement["velocity"] = velocity2
motor2_movement["total_steps"] = (abs(motor2_coordinates[1] -
motor2_coordinates[0]))
elif menu == 7:
if select == 0:
stop_all_motors()
text_area.text = "Stopping..."
menu = adv_menu(menu)
elif select == 1:
pause_resume_motor1()
pause_resume_motor2()
paused_state = motor1_movement["is_paused"] or motor2_movement["is_paused"]
text_area.text = "Paused" if paused_state else "Running"
display.refresh()
if event.key_number == 1:
if menu == 1:
driver1.direction = False
driver1.reset_position()
menu = adv_menu(menu)
elif menu == 7:
stop_all_motors()
menu = adv_menu(menu)
if event.key_number == 2:
if menu == 1:
driver1.direction = True
driver1.reset_position()
menu = adv_menu(menu)
elif menu == 7:
stop_all_motors()
menu = adv_menu(menu)
display.refresh()
pos = encoder.position
if pos != last_pos:
if pos > last_pos:
if menu == 2:
driver2.step(-10)
if menu == 3:
select = (select + 1) % 2
text_area.text = mode_text[select]
grid_bg[0] = mode_icons[select]
if menu == 4:
select = (select + 1) % len(time_text)
text_area.text = time_text[select]
if menu == 5:
select = (select + 1) % len(shot_text)
text_area.text = shot_text[select]
if menu == 7:
select = (select + 1) % len(running_text)
text_area.text = running_text[select]
grid_bg[0] = running_icons[select]
else:
if menu == 2:
driver2.step(10)
if menu == 3:
select = (select - 1) % 2
text_area.text = mode_text[select]
grid_bg[0] = mode_icons[select]
if menu == 4:
select = (select - 1) % len(time_text)
text_area.text = time_text[select]
if menu == 5:
select = (select - 1) % len(shot_text)
text_area.text = shot_text[select]
if menu == 7:
select = (select - 1) % len(running_text)
text_area.text = running_text[select]
grid_bg[0] = running_icons[select]
last_pos = pos
display.refresh()