""" slow_sweep_v2.py — Drive turntable through a slow sweep via L298N stepper. Usage: python3 slow_sweep_v2.py [--degrees 360] [--rpm 0.5] [--shots 36] --degrees Total rotation (default 360) --rpm Rotation speed in RPM (default 0.5 = 1 rev per 2 minutes) --shots If set, pause every (degrees/shots) degrees and print "CAPTURE " so an external trigger script or you manually fire the camera. --reverse Step in reverse direction 7200 steps = 1 full revolution = 0.05 deg/step = 20 steps/deg """ import serial, time, sys, argparse PORT = '/dev/ttyACM0' BAUD = 115200 STEPS_PER_REV = 14400 # half-step mode DEGREES_PER_STEP = 360.0 / STEPS_PER_REV # 0.025 STEPS_PER_DEGREE = STEPS_PER_REV / 360.0 # 40.0 def connect(): s = serial.Serial(PORT, BAUD, timeout=2) s.setDTR(False); s.setRTS(False) time.sleep(1) s.reset_input_buffer() return s def send_step(s, n, delay_ms): s.write(f'STEP {n} {delay_ms}\n'.encode()) deadline = time.time() + abs(n) * delay_ms / 1000.0 + 5 while time.time() < deadline: line = s.readline().decode(errors='replace').strip() if '[STEP]' in line: return line return None def read_enc(s): s.write(b'ENC\n') deadline = time.time() + 3 while time.time() < deadline: line = s.readline().decode(errors='replace').strip() if '[ENC]' in line: return line return '[ENC] timeout' def main(): ap = argparse.ArgumentParser() ap.add_argument('--degrees', type=float, default=360.0, help='Total rotation in degrees (default 360)') ap.add_argument('--rpm', type=float, default=0.5, help='Speed in RPM (default 0.5)') ap.add_argument('--shots', type=int, default=0, help='Number of capture pauses evenly spaced across sweep') ap.add_argument('--reverse', action='store_true', help='Rotate in reverse direction') args = ap.parse_args() total_steps = round(args.degrees * STEPS_PER_DEGREE) # delay_ms per step from RPM: RPM * STEPS_PER_REV steps/min => steps/sec => ms/step steps_per_sec = args.rpm * STEPS_PER_REV / 60.0 delay_ms = max(1, round(1000.0 / steps_per_sec)) direction = -1 if args.reverse else 1 eta_sec = total_steps * delay_ms / 1000.0 print(f"{'='*52}") print(f" PROJECT SLOW-SWEEP v2") print(f"{'='*52}") print(f" Degrees : {args.degrees}°") print(f" Steps : {total_steps}") print(f" Speed : {args.rpm} RPM ({delay_ms} ms/step)") print(f" Direction : {'reverse (CCW)' if args.reverse else 'forward (CW)'}") print(f" ETA : {eta_sec:.0f}s ({eta_sec/60:.1f} min)") if args.shots: interval_deg = args.degrees / args.shots print(f" Shots : {args.shots} (every {interval_deg:.1f}°)") print(f"{'='*52}") s = connect() s.write(b'ZERO\n') time.sleep(0.3) batch = 50 # steps per serial command sent = 0 shot_num = 0 next_shot_step = round((args.degrees / args.shots) * STEPS_PER_DEGREE) if args.shots else None start = time.time() try: while sent < total_steps: # Check if next shot pause is due if args.shots and sent >= next_shot_step * (shot_num + 1): shot_num += 1 elapsed = time.time() - start deg_done = sent * DEGREES_PER_STEP print(f"\n [SHOT {shot_num}/{args.shots}] at {deg_done:.1f}° t={elapsed:.1f}s") input(" >> Press Enter to continue...") this_batch = min(batch, total_steps - sent) step_cmd = this_batch * direction send_step(s, step_cmd, delay_ms) sent += this_batch elapsed = time.time() - start pct = sent / total_steps * 100 deg = sent * DEGREES_PER_STEP remain = (total_steps - sent) * delay_ms / 1000.0 sys.stdout.write( f'\r {deg:6.1f}° / {args.degrees}° ({pct:5.1f}%) ' f'elapsed={elapsed:.0f}s remain~{remain:.0f}s ' ) sys.stdout.flush() except KeyboardInterrupt: print('\n [INTERRUPTED]') print() enc_line = read_enc(s) elapsed = time.time() - start print(f"\n Encoder final : {enc_line}") print(f" Time elapsed : {elapsed:.1f}s ({elapsed/60:.1f} min)") s.write(b'RELEASE\n') time.sleep(0.3) s.close() print(" Coils released. Done.") if __name__ == '__main__': main()