|
| 1 | +# SPDX-FileCopyrightText: 2024 johnpark for Adafruit Industries |
| 2 | +# |
| 3 | +# SPDX-License-Identifier: MIT |
| 4 | +''' |
| 5 | +Servo Commander |
| 6 | +- Feather Reverse TFT ESP32-S3 + two servos + two push encoders |
| 7 | +- test servo ranges with encoder rotation |
| 8 | +- store saved positions with enc button + D0, D1, D2 buttons |
| 9 | +- recall saved positions with D0, D1, D2 |
| 10 | +- playback animation by pressing both push encoders |
| 11 | +''' |
| 12 | + |
| 13 | +import time |
| 14 | +import board |
| 15 | +import displayio |
| 16 | +import terminalio |
| 17 | +from adafruit_display_text import label |
| 18 | +from adafruit_display_shapes.rect import Rect |
| 19 | +import rotaryio |
| 20 | +import pwmio |
| 21 | +from adafruit_motor import servo |
| 22 | +import keypad |
| 23 | + |
| 24 | +# Define custom servo pulse range variables |
| 25 | +s_cfgs = [ |
| 26 | + {'min_pulse': 600, 'max_pulse': 2400, 'min_ang': 10, 'max_ang': 170}, |
| 27 | + {'min_pulse': 600, 'max_pulse': 2300, 'min_ang': 10, 'max_ang': 170} |
| 28 | +] |
| 29 | + |
| 30 | +# Setup the PWM output for the servos |
| 31 | +pwm_pins = [board.D10, board.D11] |
| 32 | +servo_motors = [] |
| 33 | + |
| 34 | +for i, config in enumerate(s_cfgs): |
| 35 | + pwm = pwmio.PWMOut(pwm_pins[i], duty_cycle=2**15, frequency=50) |
| 36 | + servo_motor = servo.Servo(pwm, min_pulse=config['min_pulse'], max_pulse=config['max_pulse']) |
| 37 | + servo_motors.append(servo_motor) |
| 38 | + servo_motor.angle = 90 |
| 39 | + |
| 40 | +s_saves = [ |
| 41 | + [s_cfgs[0]['min_ang'], 90, s_cfgs[0]['max_ang']], |
| 42 | + [s_cfgs[1]['min_ang'], 90, s_cfgs[1]['max_ang']] |
| 43 | +] |
| 44 | + |
| 45 | +# Setup the rotary encs |
| 46 | +encs = [ |
| 47 | + rotaryio.IncrementalEncoder(board.A2, board.A1), |
| 48 | + rotaryio.IncrementalEncoder(board.A5, board.A4) |
| 49 | +] |
| 50 | + |
| 51 | +for enc in encs: |
| 52 | + enc.position = 90 |
| 53 | + |
| 54 | +last_positions = [enc.position for enc in encs] |
| 55 | + |
| 56 | +# Setup the buttons |
| 57 | +enc_buttons = keypad.Keys((board.D13, board.D12), value_when_pressed=False, pull=True) |
| 58 | +tft_d0_button = keypad.Keys((board.D0,), value_when_pressed=False, pull=True) |
| 59 | +tft_buttons = keypad.Keys((board.D1, board.D2), value_when_pressed=True, pull=True) |
| 60 | + |
| 61 | +def set_servo_angle(s_servo_motor, s_angle, s_enc, min_ang, max_ang): |
| 62 | + s_angle = min(max(s_angle, min_ang), max_ang) |
| 63 | + s_servo_motor.angle = s_angle |
| 64 | + s_enc.position = s_angle |
| 65 | + |
| 66 | +def playback(mode, speed, steps): |
| 67 | + for k in range(3): # Loop through each save position (assuming 3 saves per motor) |
| 68 | + for m in range(len(servo_motors)): |
| 69 | + p_servo_motor = servo_motors[m] |
| 70 | + p_enc = encs[m] |
| 71 | + save = s_saves[m][k] # Get the k-th save position for the m-th motor |
| 72 | + |
| 73 | + if mode: |
| 74 | + direction = 1 if save > enc.position else -1 |
| 75 | + for p_angle in range(enc.position, save, direction * steps): |
| 76 | + p_servo_motor.angle = p_angle |
| 77 | + time.sleep(speed) |
| 78 | + p_enc.position = save |
| 79 | + else: |
| 80 | + servo_motor.angle = save |
| 81 | + time.sleep(0.75) |
| 82 | + |
| 83 | + |
| 84 | +# Setup the display |
| 85 | +display = board.DISPLAY |
| 86 | +group = displayio.Group() |
| 87 | +background_rect = Rect(0, 10, display.width, display.height - 10, fill=0x000010) |
| 88 | +group.append(background_rect) |
| 89 | +mid_bar = Rect(116, 0, 3, display.height, fill=0x00000) |
| 90 | +group.append(mid_bar) |
| 91 | +top_bar = Rect(0, 0, display.width, 20, fill=0x000000) |
| 92 | +group.append(top_bar) |
| 93 | + |
| 94 | +FONT = terminalio.FONT |
| 95 | +TXTCOL = 0xFFFF00 |
| 96 | + |
| 97 | +# Create labels |
| 98 | +labels = [] |
| 99 | +for i, config in enumerate(s_cfgs): |
| 100 | + lbl = label.Label(FONT, text=f"Pulse: {config['min_pulse']}-{config['max_pulse']}",color=TXTCOL, |
| 101 | + scale=1, anchor_point=(0, 0), anchored_position=(5 + 125 * i, 5)) |
| 102 | + labels.append(lbl) |
| 103 | + group.append(lbl) |
| 104 | + |
| 105 | +for i in range(2): |
| 106 | + lbl = label.Label(FONT, text="Angle:-", color=TXTCOL, scale=2, anchor_point=(0, 0), |
| 107 | + anchored_position=(4 + 126 * i, 24)) |
| 108 | + labels.append(lbl) |
| 109 | + group.append(lbl) |
| 110 | + |
| 111 | +for i in range(2): |
| 112 | + for j in range(3): |
| 113 | + lbl = label.Label(FONT, text=f"D{j}:{s_saves[i][j]}", color=TXTCOL, scale=2, |
| 114 | + anchor_point=(0, 0), anchored_position=(4 + i * 126, 48 + 24 * j)) |
| 115 | + labels.append(lbl) |
| 116 | + group.append(lbl) |
| 117 | + |
| 118 | +display.root_group = group |
| 119 | + |
| 120 | +modifier1 = False |
| 121 | +modifier2 = False |
| 122 | + |
| 123 | +print("[]-Servo Commander READY-[]") |
| 124 | + |
| 125 | +while True: |
| 126 | + enc_button_event = enc_buttons.events.get() |
| 127 | + if enc_button_event: |
| 128 | + if enc_button_event.pressed: |
| 129 | + if enc_button_event.key_number == 0: |
| 130 | + modifier1 = True |
| 131 | + elif enc_button_event.key_number == 1: |
| 132 | + modifier2 = True |
| 133 | + |
| 134 | + if enc_button_event.released: |
| 135 | + if enc_button_event.key_number == 0: |
| 136 | + modifier1 = False |
| 137 | + elif enc_button_event.key_number == 1: |
| 138 | + modifier2 = False |
| 139 | + |
| 140 | + if modifier1 and modifier2: |
| 141 | + print("Playback") |
| 142 | + playback(True, 0.006, 2) |
| 143 | + |
| 144 | + tft_d0_button_event = tft_d0_button.events.get() |
| 145 | + if tft_d0_button_event and tft_d0_button_event.pressed: |
| 146 | + if modifier1: |
| 147 | + s_saves[0][0] = min(max(encs[0].position, s_cfgs[0]['min_ang']), s_cfgs[0]['max_ang']) |
| 148 | + print("D0 save motor1:", s_saves[0][0]) |
| 149 | + labels[4].text = f"D0:{s_saves[0][0]}" |
| 150 | + elif modifier2: |
| 151 | + s_saves[1][0] = min(max(encs[1].position, s_cfgs[1]['min_ang']), s_cfgs[1]['max_ang']) |
| 152 | + print("D0 save motor2:", s_saves[1][0]) |
| 153 | + labels[7].text = f"D0:{s_saves[1][0]}" |
| 154 | + else: |
| 155 | + for i in range(len(servo_motors)): |
| 156 | + servo_motor = servo_motors[i] |
| 157 | + enc = encs[i] |
| 158 | + s_save = s_saves[i][0] |
| 159 | + config = s_cfgs[i] |
| 160 | + print(f"D0 recalled motor{i+1}:", s_save) |
| 161 | + set_servo_angle(servo_motor, s_save, enc, config['min_ang'], config['max_ang']) |
| 162 | + labels[2 + i].text = f"Angle:{s_save}" |
| 163 | + |
| 164 | + tft_buttons_event = tft_buttons.events.get() |
| 165 | + if tft_buttons_event and tft_buttons_event.pressed: |
| 166 | + if tft_buttons_event.key_number == 0: |
| 167 | + if modifier1: |
| 168 | + s_saves[0][1] = min(max(encs[0].position,s_cfgs[0]['min_ang']),s_cfgs[0]['max_ang']) |
| 169 | + print("D1 save motor1:", s_saves[0][1]) |
| 170 | + labels[5].text = f"D1:{s_saves[0][1]}" |
| 171 | + elif modifier2: |
| 172 | + s_saves[1][1] = min(max(encs[1].position,s_cfgs[1]['min_ang']),s_cfgs[1]['max_ang']) |
| 173 | + print("D1 save motor2:", s_saves[1][1]) |
| 174 | + labels[8].text = f"D1:{s_saves[1][1]}" |
| 175 | + else: |
| 176 | + for i in range(len(servo_motors)): |
| 177 | + servo_motor = servo_motors[i] |
| 178 | + enc = encs[i] |
| 179 | + s_save = s_saves[i][1] |
| 180 | + config = s_cfgs[i] |
| 181 | + print(f"D1 recalled motor{i+1}:", s_save) |
| 182 | + set_servo_angle(servo_motor, s_save, enc, config['min_ang'], config['max_ang']) |
| 183 | + labels[2 + i].text = f"Angle:{s_save}" |
| 184 | + elif tft_buttons_event.key_number == 1: |
| 185 | + if modifier1: |
| 186 | + s_saves[0][2] = min(max(encs[0].position,s_cfgs[0]['min_ang']),s_cfgs[0]['max_ang']) |
| 187 | + print("D2 save motor1:", s_saves[0][2]) |
| 188 | + labels[6].text = f"D2:{s_saves[0][2]}" |
| 189 | + elif modifier2: |
| 190 | + s_saves[1][2] = min(max(encs[1].position,s_cfgs[1]['min_ang']),s_cfgs[1]['max_ang']) |
| 191 | + print("D2 save motor2:", s_saves[1][2]) |
| 192 | + labels[9].text = f"D2:{s_saves[1][2]}" |
| 193 | + else: |
| 194 | + for i in range(len(servo_motors)): |
| 195 | + servo_motor = servo_motors[i] |
| 196 | + enc = encs[i] |
| 197 | + s_save = s_saves[i][2] |
| 198 | + config = s_cfgs[i] |
| 199 | + print(f"D2 recalled motor{i+1}:", s_save) |
| 200 | + set_servo_angle(servo_motor, s_save, enc, config['min_ang'], config['max_ang']) |
| 201 | + labels[2 + i].text = f"Angle:{s_save}" |
| 202 | + |
| 203 | + for i in range(len(servo_motors)): |
| 204 | + current_position = encs[i].position |
| 205 | + if current_position != last_positions[i]: |
| 206 | + config = s_cfgs[i] |
| 207 | + angle = min(max(current_position, config['min_ang']), config['max_ang']) |
| 208 | + servo_motor = servo_motors[i] |
| 209 | + enc = encs[i] |
| 210 | + set_servo_angle(servo_motor, angle, enc, config['min_ang'], config['max_ang']) |
| 211 | + labels[2 + i].text = f"Angle:{angle}" |
| 212 | + last_positions[i] = current_position |
0 commit comments