Skip to content

Commit 31363a8

Browse files
committed
tools: add odometry calibration tool
- Orchestrate calibration via Socket.IO connection to cogip-server - Coordinate firmware operations through FirmwareAdapter - Provide interactive user interface via ConsoleUI - Include calculation engine for odometry parameter tuning
1 parent 1111347 commit 31363a8

File tree

8 files changed

+1314
-0
lines changed

8 files changed

+1314
-0
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import logging
2+
3+
from cogip.utils.logger import Logger
4+
5+
logger = Logger("cogip-odometry-calibration", level=logging.WARNING)
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
#!/usr/bin/env python3
2+
"""
3+
CLI entry point for the Odometry Calibration tool.
4+
5+
Provides a command-line interface to calibrate robot odometry parameters
6+
by communicating with the firmware via SocketIO through cogip-server.
7+
"""
8+
9+
from __future__ import annotations
10+
import asyncio
11+
import logging
12+
from importlib.resources import files
13+
from typing import Annotated
14+
15+
import typer
16+
import yaml
17+
18+
from cogip.models.firmware_parameter import FirmwareParametersGroup
19+
from cogip.tools.firmware_odometry_calibration import logger
20+
from cogip.tools.firmware_odometry_calibration.odometry_calibration import OdometryCalibration
21+
22+
23+
def main_opt(
24+
*,
25+
server_url: Annotated[
26+
str | None,
27+
typer.Option(
28+
"-s",
29+
"--server-url",
30+
help="cogip-server URL",
31+
envvar="COGIP_SOCKETIO_SERVER_URL",
32+
),
33+
] = None,
34+
robot_id: Annotated[
35+
int,
36+
typer.Option(
37+
"-i",
38+
"--id",
39+
min=1,
40+
help="Robot ID.",
41+
envvar=["ROBOT_ID"],
42+
),
43+
] = 1,
44+
debug: Annotated[
45+
bool,
46+
typer.Option(
47+
"-d",
48+
"--debug",
49+
help="Turn on debug messages",
50+
envvar="CALIBRATION_DEBUG",
51+
),
52+
] = False,
53+
):
54+
if debug:
55+
logger.setLevel(logging.DEBUG)
56+
57+
if not server_url:
58+
server_url = f"http://localhost:809{robot_id}"
59+
60+
# Load bundled parameters definition YAML
61+
params_resource = files("cogip.tools.firmware_odometry_calibration").joinpath("odometry_parameters.yaml")
62+
with params_resource.open() as f:
63+
parameters_data = yaml.safe_load(f)
64+
65+
parameters_group = FirmwareParametersGroup.model_validate(parameters_data["parameters"])
66+
67+
# Run calibration
68+
calibration = OdometryCalibration(server_url, parameters_group)
69+
asyncio.run(calibration.run())
70+
71+
72+
def main():
73+
"""
74+
Run odometry calibration tool.
75+
76+
During installation of cogip-tools, `setuptools` is configured
77+
to create the `cogip-odometry-calibration` script using this function as entrypoint.
78+
"""
79+
typer.run(main_opt)
80+
81+
82+
if __name__ == "__main__":
83+
main()
Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
"""
2+
Odometry Calibration Calculator.
3+
4+
Pure mathematical functions for computing calibration parameters.
5+
6+
Calibrates three parameters for differential drive odometry:
7+
wheel distance, left wheel radius, and right wheel radius.
8+
9+
Calibration process:
10+
Phase 1 (Turn in Place): Spin N rotations to compute wheel distance from encoder arcs
11+
Phase 2 (Square Path): Drive squares to isolate linear motion and find wheel radius ratio (beta)
12+
Phase 3 (Straight Line): Drive known distance to solve for actual wheel radius using beta
13+
"""
14+
15+
import math
16+
17+
from .types import CalibrationResult, CalibrationState
18+
19+
20+
def compute_alpha_coefficients(
21+
turns: int,
22+
lticks_delta: int,
23+
rticks_delta: int,
24+
encoder_ticks: float,
25+
) -> tuple[float, float]:
26+
"""
27+
Compute alpha coefficients from turn-in-place data.
28+
29+
Alpha represents wheel rotations per robot rotation.
30+
31+
Args:
32+
turns: Number of full rotations performed
33+
lticks_delta: Change in left encoder ticks
34+
rticks_delta: Change in right encoder ticks
35+
encoder_ticks: Encoder ticks per wheel revolution
36+
37+
Returns:
38+
Tuple of (alpha_l, alpha_r)
39+
"""
40+
alpha_l = lticks_delta / (encoder_ticks * turns)
41+
alpha_r = rticks_delta / (encoder_ticks * turns)
42+
return alpha_l, alpha_r
43+
44+
45+
def compute_wheel_distance(
46+
alpha_l: float,
47+
alpha_r: float,
48+
left_wheel_radius: float,
49+
right_wheel_radius: float,
50+
) -> float:
51+
"""
52+
Compute wheel distance from alpha coefficients and wheel radii.
53+
54+
Formula: axletrack = |alpha_l| * radius_l + |alpha_r| * radius_r
55+
56+
During rotation in place, wheels turn in opposite directions, so alpha
57+
coefficients have opposite signs. We use absolute values to sum their
58+
contributions.
59+
60+
Args:
61+
alpha_l: Left wheel rotations per robot rotation (may be negative)
62+
alpha_r: Right wheel rotations per robot rotation (may be negative)
63+
left_wheel_radius: Current left wheel radius estimate
64+
right_wheel_radius: Current right wheel radius estimate
65+
66+
Returns:
67+
Computed wheel distance in mm
68+
"""
69+
return abs(alpha_l) * left_wheel_radius + abs(alpha_r) * right_wheel_radius
70+
71+
72+
def compute_wheel_distance_result(
73+
turns: int,
74+
lticks_delta: int,
75+
rticks_delta: int,
76+
encoder_ticks: float,
77+
left_wheel_radius: float,
78+
right_wheel_radius: float,
79+
left_polarity: float = 1.0,
80+
right_polarity: float = 1.0,
81+
) -> tuple[CalibrationResult, CalibrationState]:
82+
"""
83+
Phase 1: Compute wheel distance from turn-in-place data.
84+
85+
Args:
86+
turns: Number of full rotations performed
87+
lticks_delta: Change in left encoder ticks (raw)
88+
rticks_delta: Change in right encoder ticks (raw)
89+
encoder_ticks: Encoder ticks per wheel revolution
90+
left_wheel_radius: Current left wheel radius
91+
right_wheel_radius: Current right wheel radius
92+
left_polarity: Left encoder polarity correction (+1 or -1)
93+
right_polarity: Right encoder polarity correction (+1 or -1)
94+
95+
Returns:
96+
Tuple of (CalibrationResult, updated CalibrationState)
97+
"""
98+
# Apply polarity correction
99+
lticks_delta = int(lticks_delta * left_polarity)
100+
rticks_delta = int(rticks_delta * right_polarity)
101+
102+
alpha_l, alpha_r = compute_alpha_coefficients(turns, lticks_delta, rticks_delta, encoder_ticks)
103+
new_wheels_distance = compute_wheel_distance(alpha_l, alpha_r, left_wheel_radius, right_wheel_radius)
104+
105+
result = CalibrationResult(
106+
wheels_distance=new_wheels_distance,
107+
right_wheel_radius=right_wheel_radius,
108+
left_wheel_radius=left_wheel_radius,
109+
)
110+
111+
state = CalibrationState(
112+
alpha_l=alpha_l,
113+
alpha_r=alpha_r,
114+
beta=0.0,
115+
)
116+
117+
return result, state
118+
119+
120+
def compute_beta_coefficient(
121+
lticks_linear: float,
122+
rticks_linear: float,
123+
) -> float | None:
124+
"""
125+
Compute beta coefficient (radius ratio) from linear encoder components.
126+
127+
For straight-line motion, both wheels travel the same distance D:
128+
D = 2π * radius_l * (lticks / encoder_ticks)
129+
D = 2π * radius_r * (rticks / encoder_ticks)
130+
131+
Therefore: radius_r / radius_l = lticks / rticks
132+
133+
Beta = radius_r / radius_l = lticks_linear / rticks_linear
134+
135+
Args:
136+
lticks_linear: Linear component of left encoder ticks
137+
rticks_linear: Linear component of right encoder ticks
138+
139+
Returns:
140+
Beta coefficient (radius_r / radius_l), or None if rticks_linear is too small
141+
"""
142+
if abs(rticks_linear) < 1:
143+
return None
144+
return lticks_linear / rticks_linear
145+
146+
147+
def compute_right_wheel_radius_result(
148+
squares: int,
149+
lticks_delta: int,
150+
rticks_delta: int,
151+
state: CalibrationState,
152+
encoder_ticks: float,
153+
left_wheel_radius: float,
154+
left_polarity: float = 1.0,
155+
right_polarity: float = 1.0,
156+
) -> tuple[CalibrationResult, CalibrationState] | None:
157+
"""
158+
Phase 2: Compute right wheel radius from square trajectory data.
159+
160+
Args:
161+
squares: Number of square paths performed
162+
lticks_delta: Change in left encoder ticks (raw)
163+
rticks_delta: Change in right encoder ticks (raw)
164+
state: Current calibration state with alpha coefficients
165+
encoder_ticks: Encoder ticks per wheel revolution
166+
left_wheel_radius: Current left wheel radius
167+
left_polarity: Left encoder polarity correction (+1 or -1)
168+
right_polarity: Right encoder polarity correction (+1 or -1)
169+
170+
Returns:
171+
Tuple of (CalibrationResult, updated CalibrationState), or None if computation fails
172+
"""
173+
# Apply polarity correction
174+
lticks_delta = int(lticks_delta * left_polarity)
175+
rticks_delta = int(rticks_delta * right_polarity)
176+
177+
# Subtract rotation component to get linear component
178+
lticks_linear = lticks_delta - (state.alpha_l * encoder_ticks * squares)
179+
rticks_linear = rticks_delta - (state.alpha_r * encoder_ticks * squares)
180+
181+
beta = compute_beta_coefficient(lticks_linear, rticks_linear)
182+
if beta is None:
183+
return None
184+
185+
new_right_wheel_radius = beta * left_wheel_radius
186+
new_wheels_distance = compute_wheel_distance(
187+
state.alpha_l, state.alpha_r, left_wheel_radius, new_right_wheel_radius
188+
)
189+
190+
result = CalibrationResult(
191+
wheels_distance=new_wheels_distance,
192+
right_wheel_radius=new_right_wheel_radius,
193+
left_wheel_radius=left_wheel_radius,
194+
)
195+
196+
updated_state = CalibrationState(
197+
alpha_l=state.alpha_l,
198+
alpha_r=state.alpha_r,
199+
beta=beta,
200+
)
201+
202+
return result, updated_state
203+
204+
205+
def compute_left_wheel_radius_result(
206+
distance_mm: int,
207+
lticks_delta: int,
208+
rticks_delta: int,
209+
state: CalibrationState,
210+
encoder_ticks: float,
211+
left_polarity: float = 1.0,
212+
right_polarity: float = 1.0,
213+
) -> tuple[CalibrationResult, CalibrationState] | None:
214+
"""
215+
Phase 3: Compute left wheel radius from straight line data.
216+
217+
For straight-line motion of distance D:
218+
D = 2π * radius_l * (lticks / encoder_ticks)
219+
D = 2π * radius_r * (rticks / encoder_ticks)
220+
221+
With radius_r = beta * radius_l, summing both equations:
222+
2D = 2π * radius_l * (lticks + beta * rticks) / encoder_ticks
223+
224+
Formula: radius_l = D * encoder_ticks / (π * (lticks + beta * rticks))
225+
226+
Args:
227+
distance_mm: Distance traveled in mm
228+
lticks_delta: Change in left encoder ticks (raw)
229+
rticks_delta: Change in right encoder ticks (raw)
230+
state: Current calibration state with alpha and beta coefficients
231+
encoder_ticks: Encoder ticks per wheel revolution
232+
left_polarity: Left encoder polarity correction (+1 or -1)
233+
right_polarity: Right encoder polarity correction (+1 or -1)
234+
235+
Returns:
236+
Tuple of (CalibrationResult, CalibrationState), or None if computation fails
237+
"""
238+
# Apply polarity correction
239+
lticks_delta = int(lticks_delta * left_polarity)
240+
rticks_delta = int(rticks_delta * right_polarity)
241+
242+
denominator = math.pi * (lticks_delta + state.beta * rticks_delta)
243+
244+
if abs(denominator) < 1:
245+
return None
246+
247+
new_left_wheel_radius = (distance_mm * encoder_ticks) / denominator
248+
new_right_wheel_radius = state.beta * new_left_wheel_radius
249+
new_wheels_distance = compute_wheel_distance(
250+
state.alpha_l, state.alpha_r, new_left_wheel_radius, new_right_wheel_radius
251+
)
252+
253+
result = CalibrationResult(
254+
wheels_distance=new_wheels_distance,
255+
right_wheel_radius=new_right_wheel_radius,
256+
left_wheel_radius=new_left_wheel_radius,
257+
)
258+
259+
# State unchanged in phase 3
260+
return result, state

0 commit comments

Comments
 (0)