-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathmeasure_ppc_encoder_only.py
More file actions
204 lines (166 loc) · 7.1 KB
/
measure_ppc_encoder_only.py
File metadata and controls
204 lines (166 loc) · 7.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
#!/usr/bin/env python3
"""Interactive encoder-only pulses-per-centimeter calibration.
This wizard drives the robot forward a fixed distance using encoder ticks,
then lets you adjust the pulses-per-centimeter constant based on how far the
robot actually travelled. The refined value is stored in the shared
calibration config so movement routines, telemetry, and planners remain in sync.
Usage:
python measure_ppc_encoder_only.py
"""
from __future__ import annotations
import math
import time
from typing import Tuple
from advanced import (
init_bot_control,
cleanup,
get_latest_encoders,
move_by_ticks,
stop_motors,
wait_for_encoder_data,
)
from calibration_config import (
load_pulses_per_cm,
save_pulses_per_cm,
)
# CRITICAL: This speed must match the speed used during path execution
# (telemetry_ui.py, move_control.py) to ensure calibration accuracy.
# Different speeds cause different motor behavior, momentum, and backlash.
DEFAULT_SPEED = 45
DEFAULT_DISTANCE_CM = 80.0
TOLERANCE_TICKS = 80
SETTLE_DELAY_S = 0.4
def wait_for_encoders(timeout: float = 10.0) -> None:
print("Waiting for live encoder data…", end=" ")
if wait_for_encoder_data(timeout):
print("✅ ready!")
return
raise RuntimeError("No encoder telemetry received. Check ESP32 link.")
def get_encoder_snapshot() -> Tuple[int, int]:
encoders, _ = get_latest_encoders()
return int(encoders.get("m1", 0)), int(encoders.get("m2", 0))
def wait_for_move_completion(target_ticks: int, start_left: int, start_right: int,
timeout: float = 25.0) -> bool:
deadline = time.time() + timeout
while time.time() < deadline:
cur_left, cur_right = get_encoder_snapshot()
left_delta = abs(cur_left - start_left)
right_delta = abs(cur_right - start_right)
if left_delta >= max(0, target_ticks - TOLERANCE_TICKS) and \
right_delta >= max(0, target_ticks - TOLERANCE_TICKS):
return True
time.sleep(0.1)
return False
def perform_forward_move(ppc: float, distance_cm: float, speed: int = DEFAULT_SPEED) -> Tuple[int, int, int, float, float]:
ticks = max(1, int(round(ppc * distance_cm)))
start_left, start_right = get_encoder_snapshot()
print(f"\n➡️ Driving forward {distance_cm:.1f} cm using {ppc:.2f} pulses/cm ({ticks} ticks)…")
move_by_ticks(ticks, ticks, speed, speed)
completed = wait_for_move_completion(ticks, start_left, start_right)
stop_motors()
time.sleep(SETTLE_DELAY_S)
end_left, end_right = get_encoder_snapshot()
left_delta = end_left - start_left
right_delta = end_right - start_right
avg_ticks = (abs(left_delta) + abs(right_delta)) / 2.0
approx_distance = avg_ticks / ppc if ppc else float("nan")
if not completed:
print("⚠️ Move timeout reached; encoders may not have hit target ticks.")
print(f" Encoder Δ: left={left_delta}, right={right_delta}, avg={avg_ticks:.1f} ticks")
if math.isfinite(approx_distance):
print(f" Approx distance using current PPC: {approx_distance:.2f} cm")
return left_delta, right_delta, ticks, avg_ticks, approx_distance
def prompt_measured_distance(expected_cm: float) -> float | None:
while True:
response = input(
f"Enter measured distance in cm (blank = {expected_cm:.1f}, 'r' to redo run, 'q' to quit): "
).strip()
if not response:
return expected_cm
lowered = response.lower()
if lowered in {"r", "redo", "retry"}:
return None
if lowered in {"q", "quit", "exit"}:
raise KeyboardInterrupt
try:
measured = float(response)
if measured <= 0:
raise ValueError
return measured
except ValueError:
print("Please enter a positive number, blank, 'r', or 'q'.")
def interactive_calibration() -> None:
print("=== Encoder Pulses-Per-Centimeter Calibrator ===")
print("This wizard will drive the robot forward a fixed distance.")
print("Measure how far it actually moved, and we'll refine the pulses/cm value.")
init_bot_control(verbose_telemetry=False)
wait_for_encoders()
current_ppc = load_pulses_per_cm()
print(f"Starting from current config value: {current_ppc:.2f} pulses/cm")
move_distance = DEFAULT_DISTANCE_CM
try:
iteration = 1
while True:
print(f"\n=== Iteration {iteration} ===")
print("Place the robot at the start mark with enough space to move forward.")
input("Press Enter when ready to begin the drive…")
_, _, target_ticks, avg_ticks, approx_distance = perform_forward_move(
current_ppc, move_distance, DEFAULT_SPEED
)
try:
measured_cm = prompt_measured_distance(move_distance)
except KeyboardInterrupt:
print("\nCalibration cancelled by user. No changes saved.")
break
if measured_cm is None:
print("Redoing the run with the same settings…")
continue
if measured_cm <= 0:
print("Measured distance must be positive. Let's try again.")
continue
suggested_ppc = avg_ticks / measured_cm if measured_cm else current_ppc
if not math.isfinite(suggested_ppc) or suggested_ppc <= 0:
print("Could not compute a valid pulses/cm suggestion. Rerunning…")
continue
print("\n--- Result Summary ---")
print(f"Measured distance: {measured_cm:.2f} cm")
print(f"Average encoder ticks: {avg_ticks:.1f}")
print(f"Current pulses/cm: {current_ppc:.3f}")
print(f"Suggested pulses/cm: {suggested_ppc:.3f}")
decision = input("Accept suggested value? [Y]es / [R]erun / [M]anual / [Q]uit without saving: ").strip().lower()
if decision in {"", "y", "yes"}:
save_pulses_per_cm(suggested_ppc)
print(f"Saved pulses-per-centimeter = {suggested_ppc:.3f}")
break
if decision in {"m", "manual"}:
manual_input = input("Enter pulses/cm to use: ").strip()
try:
manual_value = float(manual_input)
if manual_value <= 0:
raise ValueError
current_ppc = manual_value
except ValueError:
print("Invalid number; keeping previous value.")
continue
if decision in {"q", "quit"}:
print("Exiting without saving changes.")
break
current_ppc = suggested_ppc
iteration += 1
finally:
stop_motors()
cleanup()
print("Cleanup complete.")
def main() -> None:
try:
interactive_calibration()
except KeyboardInterrupt:
print("\nCalibration cancelled by user.")
stop_motors()
cleanup()
except Exception as exc:
print(f"❌ Calibration failed: {exc}")
stop_motors()
cleanup()
if __name__ == "__main__":
main()