Skip to content

Commit 30b51de

Browse files
committed
added move base based on distance/degrees
1 parent c357041 commit 30b51de

6 files changed

Lines changed: 170 additions & 15 deletions

File tree

ai-gateway/constants.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
export declare const VOICE_DURATION_MS_MIN: 100;
22
export declare const VOICE_DURATION_MS_MAX: 10000;
33
export declare const VOICE_DURATION_MS_DEFAULT: 800;
4+
export declare const VOICE_DISTANCE_M_MIN: number;
5+
export declare const VOICE_DISTANCE_M_MAX: number;
6+
export declare const VOICE_ROTATION_DEG_MIN: number;
7+
export declare const VOICE_ROTATION_DEG_MAX: number;
48
export declare const EXECUTE_BASE_MOVE: "execute_base_move";
59
export declare const STOP_BASE_MOVE: "stop_base_move";
610
export declare const REPEAT_BASE_MOVE: "repeat_base_move";

ai-gateway/constants.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,21 @@ const VOICE_DURATION_MS_MAX = 10000;
1313
/** Server schema default when the model omits duration_ms. */
1414
const VOICE_DURATION_MS_DEFAULT = 800;
1515

16+
/**
17+
* Minimum distance (meters) for distance-based translation moves.
18+
* Rotation distance_m is in degrees; this bound is reused for both.
19+
*/
20+
const VOICE_DISTANCE_M_MIN = 0.05;
21+
22+
/** Maximum distance (meters) for distance-based translation; or degrees for rotation. */
23+
const VOICE_DISTANCE_M_MAX = 5.0;
24+
25+
/** Minimum rotation (degrees) for distance-based rotation. */
26+
const VOICE_ROTATION_DEG_MIN = 1.0;
27+
28+
/** Maximum rotation (degrees) for distance-based rotation. */
29+
const VOICE_ROTATION_DEG_MAX = 360.0;
30+
1631
/** Timed holonomic base translation or in-place rotation (action, speed, duration_ms). */
1732
const EXECUTE_BASE_MOVE = "execute_base_move";
1833

@@ -52,6 +67,10 @@ module.exports = {
5267
VOICE_DURATION_MS_MIN,
5368
VOICE_DURATION_MS_MAX,
5469
VOICE_DURATION_MS_DEFAULT,
70+
VOICE_DISTANCE_M_MIN,
71+
VOICE_DISTANCE_M_MAX,
72+
VOICE_ROTATION_DEG_MIN,
73+
VOICE_ROTATION_DEG_MAX,
5574
EXECUTE_BASE_MOVE,
5675
STOP_BASE_MOVE,
5776
REPEAT_BASE_MOVE,

ai-gateway/openai-realtime-api.js

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ const {
2121
VOICE_DURATION_MS_DEFAULT,
2222
VOICE_DURATION_MS_MAX,
2323
VOICE_DURATION_MS_MIN,
24+
VOICE_DISTANCE_M_MIN,
25+
VOICE_DISTANCE_M_MAX,
26+
VOICE_ROTATION_DEG_MIN,
27+
VOICE_ROTATION_DEG_MAX,
2428
} = require("./constants");
2529

2630
/**
@@ -67,8 +71,11 @@ function buildRealtimeVoiceSessionPayload() {
6771
"If ambiguous, prefer slower/shorter movements.",
6872
`Call tool ${EXECUTE_BASE_MOVE} at most ONCE per user utterance.`,
6973
`After a tool result, confirm warmly in one brief sentence — never call ${EXECUTE_BASE_MOVE} again unless the user gives a new command.`,
70-
`Call tool ${EXECUTE_BASE_MOVE} with action (enum), speed (${BASE_MOVE_SPEEDS.join("|")}), duration_ms (${VOICE_DURATION_MS_MIN}-${VOICE_DURATION_MS_MAX}).`,
71-
`Default duration_ms ${VOICE_DURATION_MS_DEFAULT} if unstated; default speed ${BASE_MOVE_SPEED_DEFAULT}.`,
74+
`Call tool ${EXECUTE_BASE_MOVE} with action (enum), speed (${BASE_MOVE_SPEEDS.join("|")}), and EITHER duration_ms OR distance_m — not both.`,
75+
`If the user specifies a distance (e.g. "move forward half a meter", "go forward 0.5 meters"), set distance_m in meters (${VOICE_DISTANCE_M_MIN}${VOICE_DISTANCE_M_MAX}) and omit duration_ms.`,
76+
`If the user specifies a rotation angle (e.g. "rotate 90 degrees", "turn left a quarter turn"), set distance_m in degrees (e.g. 90) and omit duration_ms.`,
77+
`If the user specifies a time (e.g. "move forward for 2 seconds"), set duration_ms and omit distance_m.`,
78+
`If neither distance nor time is specified, use default duration_ms ${VOICE_DURATION_MS_DEFAULT} and omit distance_m.`,
7279
`When the user wants to halt base motion (stop, wait, freeze, do not move, cut that, enough), call tool ${STOP_BASE_MOVE} with no arguments — not ${EXECUTE_BASE_MOVE}.`,
7380
`When the user wants to repeat the last base move (again, same thing, one more time, do that again), call tool ${REPEAT_BASE_MOVE} with no arguments — not ${EXECUTE_BASE_MOVE} with guessed parameters. Ensure you are over 90% confident that the user asked you to repeat previous movement before you move.`,
7481
"Do not respond to the user. Do not speak to the user. Only call tools as instructed.",
@@ -97,11 +104,21 @@ function buildRealtimeVoiceSessionPayload() {
97104
duration_ms: {
98105
type: "integer",
99106
description:
100-
"How long to move in milliseconds before stopping.",
107+
"How long to move in milliseconds before stopping. Use when the user specifies a time. Omit if distance_m is set.",
101108
minimum: VOICE_DURATION_MS_MIN,
102109
maximum: VOICE_DURATION_MS_MAX,
103110
default: VOICE_DURATION_MS_DEFAULT,
104111
},
112+
distance_m: {
113+
type: "number",
114+
description:
115+
"Distance to travel in meters (for translation actions: forward, backward, strafe). " +
116+
"For rotation actions (rotate_left, rotate_right), pass the angle in degrees (e.g. 90 for a quarter turn). " +
117+
`Valid range: translation (${VOICE_DISTANCE_M_MIN}${VOICE_DISTANCE_M_MAX} meters), ` +
118+
`rotation (${VOICE_ROTATION_DEG_MIN}${VOICE_ROTATION_DEG_MAX} degrees). Omit if duration_ms is set.`,
119+
minimum: VOICE_DISTANCE_M_MIN,
120+
maximum: VOICE_ROTATION_DEG_MAX,
121+
},
105122
},
106123
required: ["action"],
107124
additionalProperties: false,

src/pages/operator/tsx/voice/constants.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ import {
1515
VOICE_DURATION_MS_DEFAULT,
1616
VOICE_DURATION_MS_MAX,
1717
VOICE_DURATION_MS_MIN,
18+
VOICE_DISTANCE_M_MIN,
19+
VOICE_DISTANCE_M_MAX,
20+
VOICE_ROTATION_DEG_MIN,
21+
VOICE_ROTATION_DEG_MAX,
1822
} from "ai-gateway/constants";
1923

2024
/**
@@ -32,6 +36,10 @@ export {
3236
VOICE_DURATION_MS_DEFAULT,
3337
VOICE_DURATION_MS_MAX,
3438
VOICE_DURATION_MS_MIN,
39+
VOICE_DISTANCE_M_MIN,
40+
VOICE_DISTANCE_M_MAX,
41+
VOICE_ROTATION_DEG_MIN,
42+
VOICE_ROTATION_DEG_MAX,
3543
};
3644

3745
/** Derived from ai-gateway action/speed enums (operator client). */
@@ -48,6 +56,14 @@ export type ExecuteBaseMoveArgs = {
4856
action: BaseMoveAction;
4957
speed?: BaseMoveSpeed;
5058
duration_ms?: number;
59+
/**
60+
* Distance-based move target.
61+
* - Translation actions (forward/backward/strafe): meters.
62+
* - Rotation actions (rotate_left/rotate_right): degrees (converted to radians client-side).
63+
* When set, the robot moves until odom confirms the target is reached instead of relying
64+
* on a fixed time duration. Falls back to an estimated duration if odom is unavailable.
65+
*/
66+
distance_m?: number;
5167
};
5268

5369
/** Tool result sent back on the Realtime data channel (operator client). */
@@ -111,6 +127,28 @@ export function clampDurationMs(ms: number): number {
111127
);
112128
}
113129

130+
/**
131+
* Clamp a distance_m value from tool args before robot dispatch.
132+
* - For translation: value is in meters.
133+
* - For rotation: value is in degrees (caller converts to radians).
134+
*
135+
* @param d raw distance from Realtime tool arguments
136+
* @returns clamped value in [VOICE_DISTANCE_M_MIN, VOICE_DISTANCE_M_MAX]
137+
*/
138+
export function clampDistanceM(d: number): number {
139+
return Math.max(VOICE_DISTANCE_M_MIN, Math.min(VOICE_DISTANCE_M_MAX, d));
140+
}
141+
142+
/**
143+
* Clamp a rotation degrees value from tool args before robot dispatch.
144+
*
145+
* @param d raw rotation in degrees from Realtime tool arguments
146+
* @returns clamped value in [VOICE_ROTATION_DEG_MIN, VOICE_ROTATION_DEG_MAX]
147+
*/
148+
export function clampRotationDeg(d: number): number {
149+
return Math.max(VOICE_ROTATION_DEG_MIN, Math.min(VOICE_ROTATION_DEG_MAX, d));
150+
}
151+
114152
/** Union of tool names in VOICE_TOOLS; used for typed dispatch in realtimeSession.ts. */
115153
export type VoiceToolName = (typeof VOICE_TOOLS)[number];
116154

src/pages/operator/tsx/voice/executeBaseMove.ts

Lines changed: 81 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ import {
88
BASE_MOVE_ACTIONS,
99
BASE_MOVE_SPEED_DEFAULT,
1010
BASE_MOVE_SPEEDS,
11+
clampDistanceM,
1112
clampDurationMs,
13+
clampRotationDeg,
1214
VOICE_DURATION_MS_DEFAULT,
1315
VOICE_MOVE_DEDUPE_MS,
1416
type BaseMoveAction,
@@ -56,6 +58,7 @@ let lastExecutedMove: {
5658
action: BaseMoveAction;
5759
speed: BaseMoveSpeed;
5860
duration_ms: number;
61+
distance_m: number | undefined;
5962
at: number;
6063
} | null = null;
6164

@@ -83,6 +86,7 @@ function isDuplicateVoiceMove(
8386
action: BaseMoveAction,
8487
speed: BaseMoveSpeed,
8588
duration_ms: number,
89+
distance_m: number | undefined,
8690
): boolean {
8791
if (lastExecutedMove === null) {
8892
return false;
@@ -94,7 +98,8 @@ function isDuplicateVoiceMove(
9498
return (
9599
lastExecutedMove.action === action &&
96100
lastExecutedMove.speed === speed &&
97-
lastExecutedMove.duration_ms === duration_ms
101+
lastExecutedMove.duration_ms === duration_ms &&
102+
lastExecutedMove.distance_m === distance_m
98103
);
99104
}
100105

@@ -139,6 +144,7 @@ function formatMoveOkDetail(
139144
action: BaseMoveAction,
140145
speed: BaseMoveSpeed,
141146
duration_ms: number,
147+
distance_m: number | undefined,
142148
mode: VoiceMoveExecutionMode,
143149
linX: number,
144150
linY: number,
@@ -149,11 +155,14 @@ function formatMoveOkDetail(
149155
mode === "button_provider"
150156
? ` via ${button ?? "button"}`
151157
: " via direct drive";
152-
let detail = `${action} at ${speed} for ${duration_ms}ms${via}`;
158+
const distanceStr = distance_m !== undefined
159+
? ` for ${distance_m.toFixed(5)} m (odom-tracked)`
160+
: ` for ${duration_ms}ms`;
161+
let detail = `${action} at ${speed}${distanceStr}${via}`;
153162
if (mode === "direct") {
154-
detail += ` (~linVelX=${linX.toFixed(4)}, linVelY=${linY.toFixed(4)}`;
163+
detail += ` (~linVelX=${linX.toFixed(5)}, linVelY=${linY.toFixed(5)}`;
155164
if (angVel !== 0) {
156-
detail += `, angVel=${angVel.toFixed(4)}`;
165+
detail += `, angVel=${angVel.toFixed(5)}`;
157166
}
158167
detail += ")";
159168
}
@@ -183,6 +192,7 @@ export function coerceExecuteArgs(raw: Record<string, unknown>): {
183192
action: BaseMoveAction;
184193
speed: BaseMoveSpeed;
185194
duration_ms: number;
195+
distance_m: number | undefined;
186196
} | null {
187197
const action = raw.action as string | undefined;
188198
if (!action || !VALID_EXECUTE_ACTIONS.has(action)) {
@@ -195,6 +205,22 @@ export function coerceExecuteArgs(raw: Record<string, unknown>): {
195205
}
196206
const speed = speedRaw as BaseMoveSpeed;
197207

208+
const isRotation = action === "rotate_left" || action === "rotate_right";
209+
210+
// Parse distance_m first — if present it takes priority over duration_ms.
211+
let distance_m: number | undefined;
212+
if (raw.distance_m !== undefined && raw.distance_m !== null) {
213+
const rawDist =
214+
typeof raw.distance_m === "number"
215+
? raw.distance_m
216+
: typeof raw.distance_m === "string"
217+
? Number.parseFloat(raw.distance_m)
218+
: NaN;
219+
if (!Number.isNaN(rawDist) && rawDist > 0) {
220+
distance_m = isRotation ? clampRotationDeg(rawDist) : clampDistanceM(rawDist);
221+
}
222+
}
223+
198224
let duration_ms =
199225
typeof raw.duration_ms === "number"
200226
? raw.duration_ms
@@ -208,7 +234,7 @@ export function coerceExecuteArgs(raw: Record<string, unknown>): {
208234

209235
duration_ms = clampDurationMs(duration_ms);
210236

211-
return { action: action as BaseMoveAction, speed, duration_ms };
237+
return { action: action as BaseMoveAction, speed, duration_ms, distance_m };
212238
}
213239

214240
export function executeBaseMoveOnProvider(
@@ -225,11 +251,11 @@ export function executeBaseMoveOnProvider(
225251
};
226252
}
227253

228-
const { action, speed, duration_ms } = coerced;
254+
const { action, speed, duration_ms, distance_m } = coerced;
229255

230256
if (
231257
!opts?.skipDedupe &&
232-
isDuplicateVoiceMove(action, speed, duration_ms)
258+
isDuplicateVoiceMove(action, speed, duration_ms, distance_m)
233259
) {
234260
return {
235261
ok: false,
@@ -241,6 +267,50 @@ export function executeBaseMoveOnProvider(
241267
const mode = voiceMoveExecutionContext?.mode ?? "direct";
242268
prepareVoiceMoveUi(speed);
243269

270+
// --- Distance-based path ---
271+
// Compute an estimated duration from distance / velocity, then dispatch
272+
// through the standard timed path. Safe, simple, no odom dependency.
273+
if (distance_m !== undefined) {
274+
const isRotation = action === "rotate_left" || action === "rotate_right";
275+
const { linX, linY, angVel } = velocitiesForAction(action);
276+
277+
// For rotation the model sends degrees; convert to radians for time estimation.
278+
const targetNative = isRotation ? (distance_m * Math.PI) / 180 : distance_m;
279+
const speed_mps = isRotation
280+
? Math.abs(angVel)
281+
: Math.max(Math.abs(linX), Math.abs(linY));
282+
const estimatedMs = speed_mps > 0
283+
? Math.round((targetNative / speed_mps) * 1000)
284+
: VOICE_DURATION_MS_DEFAULT;
285+
const clampedMs = clampDurationMs(estimatedMs);
286+
287+
if (mode === "button_provider") {
288+
const button = VOICE_ACTION_TO_BUTTON[action];
289+
const started = provider.timedButtonPadPress(button, clampedMs);
290+
if (!started) {
291+
return busyOrDisconnectedResult(provider, "timedButtonPadPress");
292+
}
293+
lastVoiceBaseMove = { action, speed, duration_ms: clampedMs, distance_m };
294+
lastExecutedMove = { action, speed, duration_ms: clampedMs, distance_m, at: Date.now() };
295+
return {
296+
ok: true,
297+
detail: formatMoveOkDetail(action, speed, clampedMs, distance_m, mode, 0, 0, 0, button),
298+
};
299+
}
300+
301+
const started = provider.timedBaseDrive(linX, linY, clampedMs, angVel);
302+
if (!started) {
303+
return busyOrDisconnectedResult(provider, "timedBaseDrive");
304+
}
305+
lastVoiceBaseMove = { action, speed, duration_ms: clampedMs, distance_m };
306+
lastExecutedMove = { action, speed, duration_ms: clampedMs, distance_m, at: Date.now() };
307+
return {
308+
ok: true,
309+
detail: formatMoveOkDetail(action, speed, clampedMs, distance_m, mode, linX, linY, angVel),
310+
};
311+
}
312+
313+
// --- Duration-based path (original) ---
244314
if (mode === "button_provider") {
245315
const button = VOICE_ACTION_TO_BUTTON[action];
246316
const started = provider.timedButtonPadPress(button, duration_ms);
@@ -251,13 +321,14 @@ export function executeBaseMoveOnProvider(
251321
);
252322
}
253323
lastVoiceBaseMove = { action, speed, duration_ms };
254-
lastExecutedMove = { action, speed, duration_ms, at: Date.now() };
324+
lastExecutedMove = { action, speed, duration_ms, distance_m: undefined, at: Date.now() };
255325
return {
256326
ok: true,
257327
detail: formatMoveOkDetail(
258328
action,
259329
speed,
260330
duration_ms,
331+
undefined,
261332
mode,
262333
0,
263334
0,
@@ -274,14 +345,15 @@ export function executeBaseMoveOnProvider(
274345
}
275346

276347
lastVoiceBaseMove = { action, speed, duration_ms };
277-
lastExecutedMove = { action, speed, duration_ms, at: Date.now() };
348+
lastExecutedMove = { action, speed, duration_ms, distance_m: undefined, at: Date.now() };
278349

279350
return {
280351
ok: true,
281352
detail: formatMoveOkDetail(
282353
action,
283354
speed,
284355
duration_ms,
356+
undefined,
285357
mode,
286358
linX,
287359
linY,

src/pages/operator/tsx/voice/realtimeSession.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,16 @@ function formatToolCallLog(
8787
string,
8888
unknown
8989
>;
90-
argsSummary = JSON.stringify({
90+
const summary: Record<string, unknown> = {
9191
action: parsed.action,
9292
speed: parsed.speed ?? BASE_MOVE_SPEED_DEFAULT,
93-
duration_ms: parsed.duration_ms ?? VOICE_DURATION_MS_DEFAULT,
94-
});
93+
};
94+
if (parsed.distance_m !== undefined && parsed.distance_m !== null) {
95+
summary.distance_m = parsed.distance_m;
96+
} else {
97+
summary.duration_ms = parsed.duration_ms ?? VOICE_DURATION_MS_DEFAULT;
98+
}
99+
argsSummary = JSON.stringify(summary);
95100
} catch {
96101
//
97102
}

0 commit comments

Comments
 (0)