Skip to content

Commit d75c568

Browse files
committed
final change
1 parent 502d4c0 commit d75c568

File tree

3 files changed

+176
-57
lines changed

3 files changed

+176
-57
lines changed

electron_demo/turtle_tf2/README.md

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ The turtle_tf2 demo showcases:
99
- **Transform Broadcasting**: Multiple TF2 broadcasters publishing coordinate frame relationships
1010
- **Transform Listening**: Real-time monitoring and visualization of coordinate transformations
1111
- **Turtle Simulation**: Integration with turtlesim for turtle pose tracking and control
12+
- **Turtle Following**: Intelligent turtle2 behavior that automatically follows turtle1 using TF2 transforms
1213
- **3D Visualization**: WebGL-based rendering using Three.js for immersive coordinate frame visualization
1314
- **Interactive Controls**: Web interface for spawning turtles, controlling motion, and managing transforms
1415

@@ -21,6 +22,13 @@ The turtle_tf2 demo showcases:
2122
- **Fixed Frame Broadcaster**: Maintains constant offset transforms
2223
- **Turtle Transform Broadcaster**: Converts turtle poses to TF2 transforms
2324

25+
### Turtle Following System
26+
27+
- **Real-time Following**: turtle2 automatically follows turtle1 using distance and angle calculations
28+
- **Smart Movement**: Proportional velocity control based on distance to target
29+
- **Collision Avoidance**: turtle2 stops when within optimal following distance (0.5 units)
30+
- **Transform Integration**: Following logic uses turtle pose data from TF2 coordinate frames
31+
2432
### Visualization
2533

2634
- **3D Scene**: Interactive Three.js environment with orbit controls
@@ -133,8 +141,9 @@ npm start
133141
```
134142

135143
4. **Use the web interface to**:
136-
- Click "Spawn Turtle1" to create the first turtle
137144
- Click "Spawn Turtle2" to create the second turtle
145+
- Use WASD keys to control turtle1 movement
146+
- Watch turtle2 automatically follow turtle1
138147
- Click "Start Demo" to initialize all TF2 broadcasters
139148
- Use frame toggle buttons to show/hide specific transforms
140149

@@ -146,15 +155,16 @@ npm start
146155
- **TF2 Dynamic Broadcaster**: Publishes time-varying `carrot1_static → carrot1_dynamic` transform
147156
- **Fixed Frame Broadcaster**: Publishes constant offset `turtle1 → carrot1_fixed` transform
148157
- **Turtle TF2 Broadcaster**: Converts turtle poses to `world → turtle1/turtle2` transforms
149-
- **Turtle TF2 Listener**: Controls turtle2 to follow turtle1 using transform lookups
158+
- **Turtle TF2 Listener**: Monitors turtle poses and controls turtle2 following behavior using real-time transform data
150159

151160
### Renderer Process (renderer.js)
152161

153162
- **3D Scene Management**: Three.js scene setup with lighting and camera controls
154163
- **Coordinate Frame Visualization**: Colored axes representation (X=red, Y=green, Z=blue)
155164
- **Turtle Rendering**: 3D turtle models with real-time pose updates
165+
- **Following Logic**: Calculates distance, angle, and velocity commands for turtle2 following behavior
156166
- **Transform Monitoring**: Live display of transform data and frame relationships
157-
- **User Interaction**: Control buttons and visual feedback systems
167+
- **User Interaction**: Control buttons, keyboard handling, and visual feedback systems
158168

159169
### HTML Interface (index.html)
160170

@@ -201,7 +211,7 @@ world → turtle2
201211
- **A**: Turn left
202212
- **D**: Turn right
203213

204-
💡 **Tip**: Click on the 3D visualization area first to ensure keyboard focus, then use WASD keys to drive turtle1 around the turtlesim environment.
214+
💡 **Tip**: Click on the 3D visualization area first to ensure keyboard focus, then use WASD keys to drive turtle1 around the turtlesim environment. turtle2 will automatically follow turtle1!
205215

206216
**Camera Controls**:
207217

@@ -214,10 +224,30 @@ world → turtle2
214224

215225
### Turtle Management
216226

217-
- **Spawn Turtle1**: Creates turtle1 at position (5.5, 5.5)
218-
- **Spawn Turtle2**: Creates turtle2 at position (4.0, 2.0)
227+
- **Spawn Turtle2**: Creates turtle2 at position (4.0, 2.0) - turtle1 is automatically spawned by turtlesim
219228
- **Stop All**: Halts all turtle motion commands
220229

230+
### Turtle Following Behavior
231+
232+
Once turtle2 is spawned, it will automatically follow turtle1 with the following intelligent behaviors:
233+
234+
- **Distance-based Speed**: turtle2 moves faster when far from turtle1, slower when close
235+
- **Angle Correction**: turtle2 continuously adjusts its heading to face turtle1
236+
- **Smart Stopping**: turtle2 stops moving when within 0.5 units of turtle1 to avoid collision
237+
- **Real-time Updates**: Following commands are sent every second based on current turtle positions
238+
239+
**Following Algorithm Details**:
240+
241+
- **Linear Velocity**: Proportional to distance (max speed: 2.0 units/sec)
242+
- **Angular Velocity**: Proportional to angle difference (4.0 × angle error)
243+
- **Minimum Following Distance**: 0.5 units (prevents excessive oscillation)
244+
245+
You can observe the following behavior by:
246+
247+
1. Spawning turtle2 using the "Spawn Turtle2" button
248+
2. Using WASD keys to move turtle1 around
249+
3. Watching turtle2 chase turtle1 in both the turtlesim window and 3D visualization
250+
221251
### Demo Control
222252

223253
- **Start Demo**: Initializes all TF2 broadcasters and systems
@@ -246,10 +276,12 @@ world → turtle2
246276
- Check if ROS2 daemon is running: `ros2 daemon status`
247277
- Verify ROS2 installation: `ros2 --version`
248278

249-
2. **"Turtlesim not responding"**
279+
2. **"Turtlesim not responding" or "Failed to spawn turtle2"**
250280

251281
- Verify turtlesim is running: `ros2 run turtlesim turtlesim_node`
252282
- Check available topics: `ros2 topic list`
283+
- Ensure spawn service is available: `ros2 service list | grep spawn`
284+
- Try restarting turtlesim_node if spawn calls fail
253285

254286
3. **"No transforms detected"**
255287

electron_demo/turtle_tf2/main.js

Lines changed: 91 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -285,10 +285,12 @@ async function createTurtleTf2Listener() {
285285
'/turtle2/cmd_vel'
286286
);
287287

288+
// Store turtle2 velocity publisher
289+
turtleTf2Nodes.turtle2VelocityPublisher = velocityPublisher;
290+
288291
// Create service client to spawn turtle2
289292
const spawner = node.createClient('turtlesim/srv/Spawn', '/spawn');
290293

291-
let turtleSpawned = false;
292294
let turtleSpawningServiceReady = false;
293295

294296
// Transform listener functionality
@@ -342,7 +344,7 @@ async function createTurtleTf2Listener() {
342344

343345
// Simple following logic (in real implementation, this would use TF lookup)
344346
// For demo purposes, we'll simulate the transform lookup behavior
345-
if (turtleSpawned) {
347+
if (turtleTf2Nodes.turtle2Spawned) {
346348
// This is a simplified version - in real TF2, we'd lookup transforms
347349
// For the demo, we'll let the renderer handle the following logic
348350
if (mainWindow) {
@@ -591,75 +593,104 @@ ipcMain.on('turtle-velocity-command', (event, data) => {
591593
});
592594

593595
// Handle spawning requests from renderer
594-
ipcMain.on('spawn-turtle-request', async (event, data) => {
596+
ipcMain.on('spawn-turtle-request', (event, data) => {
595597
const { name, x, y, theta } = data;
596598

597599
if (turtleTf2Nodes.listener) {
598-
try {
599-
console.log(
600-
`Spawn request received: ${name || 'turtle2'} at (${x || 4}, ${y || 2}, ${theta || 0})`
601-
);
600+
console.log(
601+
`Spawn request received: ${name || 'turtle2'} at (${x || 4}, ${y || 2}, ${theta || 0})`
602+
);
602603

603-
// Use the existing spawner from the listener node or create one
604-
const listenerNode = turtleTf2Nodes.listener;
605-
let spawner = listenerNode._spawner;
604+
// Use the existing spawner from the listener node or create one
605+
const listenerNode = turtleTf2Nodes.listener;
606+
let spawner = listenerNode._spawner;
606607

607-
if (!spawner) {
608-
console.log('Creating new spawn service client...');
609-
spawner = listenerNode.createClient('turtlesim/srv/Spawn', '/spawn');
610-
listenerNode._spawner = spawner; // Cache it
608+
if (!spawner) {
609+
console.log('Creating new spawn service client...');
610+
spawner = listenerNode.createClient('turtlesim/srv/Spawn', '/spawn');
611+
listenerNode._spawner = spawner; // Cache it
611612

612-
// Wait for service to be ready
613-
let retries = 0;
614-
while (!spawner.isServiceServerAvailable() && retries < 10) {
615-
console.log(`Waiting for spawn service... (attempt ${retries + 1})`);
616-
await new Promise((resolve) => setTimeout(resolve, 200));
613+
// Wait for service to be ready with timeout
614+
let retries = 0;
615+
const maxRetries = 10;
616+
617+
const checkService = () => {
618+
if (spawner.isServiceServerAvailable()) {
619+
performSpawn();
620+
} else if (retries < maxRetries) {
617621
retries++;
622+
console.log(`Waiting for spawn service... (attempt ${retries})`);
623+
setTimeout(checkService, 200);
624+
} else {
625+
console.error('Turtlesim spawn service not available after waiting');
626+
if (mainWindow) {
627+
mainWindow.webContents.send('spawn-error', {
628+
message: 'Turtlesim spawn service not available after waiting',
629+
});
630+
}
618631
}
619-
}
632+
};
620633

621-
if (!spawner.isServiceServerAvailable()) {
622-
throw new Error('Turtlesim spawn service not available after waiting');
623-
}
634+
checkService();
635+
} else {
636+
performSpawn();
637+
}
624638

625-
// Use the simplest possible request format
639+
function performSpawn() {
626640
const xPos = x || 4.0;
627641
const yPos = y || 2.0;
628642
const angle = theta || 0.0;
629643
const turtleName = name || 'turtle2';
630644

631645
console.log(`Spawning ${turtleName} at (${xPos}, ${yPos}, ${angle})`);
632646

633-
// Try the most basic service call format
634-
const response = await spawner.sendRequest({
635-
x: xPos,
636-
y: yPos,
637-
theta: angle,
638-
name: turtleName,
639-
});
640-
641-
console.log('Spawn response:', response);
642-
643-
if (response) {
644-
const spawnedName = response.name || response || turtleName;
645-
console.log(`Successfully spawned ${spawnedName}`);
646-
647-
if (mainWindow) {
648-
mainWindow.webContents.send('turtle-spawned', {
649-
name: spawnedName,
647+
// Use callback pattern as required by rclnodejs
648+
try {
649+
spawner.sendRequest(
650+
{
650651
x: xPos,
651652
y: yPos,
652653
theta: angle,
654+
name: turtleName,
655+
},
656+
(response) => {
657+
console.log('Spawn response:', response);
658+
659+
if (response && response.name) {
660+
const spawnedName = response.name;
661+
console.log(`Successfully spawned ${spawnedName}`);
662+
663+
// Set turtle spawned flag for following logic
664+
if (spawnedName === 'turtle2') {
665+
turtleTf2Nodes.turtle2Spawned = true;
666+
}
667+
668+
if (mainWindow) {
669+
mainWindow.webContents.send('turtle-spawned', {
670+
name: spawnedName,
671+
x: xPos,
672+
y: yPos,
673+
theta: angle,
674+
});
675+
}
676+
} else {
677+
console.error('Invalid or empty response from spawn service');
678+
if (mainWindow) {
679+
mainWindow.webContents.send('spawn-error', {
680+
message: 'Invalid response from spawn service',
681+
});
682+
}
683+
}
684+
}
685+
);
686+
} catch (error) {
687+
console.error(`Error sending spawn request: ${error.message}`);
688+
if (mainWindow) {
689+
mainWindow.webContents.send('spawn-error', {
690+
message: `Error sending spawn request: ${error.message}`,
653691
});
654692
}
655693
}
656-
} catch (error) {
657-
console.error(`Failed to spawn ${name || 'turtle2'}:`, error);
658-
if (mainWindow) {
659-
mainWindow.webContents.send('spawn-error', {
660-
message: `Failed to spawn ${name || 'turtle2'}: ${error.message}`,
661-
});
662-
}
663694
}
664695
} else {
665696
console.error('Listener node not initialized');
@@ -683,6 +714,18 @@ ipcMain.on('turtle-cmd-vel', (event, data) => {
683714
}
684715
});
685716

717+
// Handle turtle2 following commands
718+
ipcMain.on('turtle2-cmd-vel', (event, data) => {
719+
if (turtleTf2Nodes.turtle2VelocityPublisher) {
720+
const velocity = {
721+
linear: data.linear,
722+
angular: data.angular,
723+
};
724+
// Send velocity command to turtle2
725+
turtleTf2Nodes.turtle2VelocityPublisher.publish(velocity);
726+
}
727+
});
728+
686729
app.whenReady().then(async () => {
687730
createWindow();
688731

electron_demo/turtle_tf2/renderer.js

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ let scene, camera, renderer, controls;
1717
let turtles = {};
1818
let frames = {};
1919
let coordinateFrames = {};
20+
let turtlePoses = {}; // Track turtle poses for following logic
2021

2122
// Transform tracking
2223
let activeTransforms = new Map();
@@ -283,6 +284,9 @@ function createFrame(name, position, quaternion, color, scale = 0.5) {
283284
}
284285

285286
function updateTurtlePose(name, pose) {
287+
// Store the pose for following logic
288+
turtlePoses[name] = pose;
289+
286290
if (!turtles[name]) {
287291
// Create turtle if it doesn't exist
288292
const color = name === 'turtle1' ? 0x00ff00 : 0x0088ff;
@@ -507,8 +511,48 @@ function setupROSListeners() {
507511

508512
// Listen for follow requests
509513
ipcRenderer.on('request-turtle-follow', (event) => {
510-
// Implement turtle following logic if needed
511-
console.log('Follow request received');
514+
// Implement turtle2 following turtle1 logic
515+
if (turtlePoses['turtle1'] && turtlePoses['turtle2']) {
516+
const turtle1Pose = turtlePoses['turtle1'];
517+
const turtle2Pose = turtlePoses['turtle2'];
518+
519+
// Calculate distance and angle to turtle1
520+
const dx = turtle1Pose.x - turtle2Pose.x;
521+
const dy = turtle1Pose.y - turtle2Pose.y;
522+
const distance = Math.sqrt(dx * dx + dy * dy);
523+
524+
// Calculate angle to target
525+
const targetAngle = Math.atan2(dy, dx);
526+
const currentAngle = turtle2Pose.theta;
527+
528+
// Calculate angular difference (handling wrap-around)
529+
let angleDiff = targetAngle - currentAngle;
530+
while (angleDiff > Math.PI) angleDiff -= 2 * Math.PI;
531+
while (angleDiff < -Math.PI) angleDiff += 2 * Math.PI;
532+
533+
// Calculate velocities
534+
const linearVel = Math.min(2.0 * distance, 2.0); // Max linear velocity of 2.0
535+
const angularVel = 4.0 * angleDiff; // Proportional angular velocity
536+
537+
// Only move if there's significant distance
538+
if (distance > 0.5) {
539+
// Send velocity command to turtle2
540+
ipcRenderer.send('turtle2-cmd-vel', {
541+
linear: { x: linearVel, y: 0.0, z: 0.0 },
542+
angular: { x: 0.0, y: 0.0, z: angularVel },
543+
});
544+
545+
console.log(
546+
`Turtle2 following: distance=${distance.toFixed(2)}, linear=${linearVel.toFixed(2)}, angular=${angularVel.toFixed(2)}`
547+
);
548+
} else {
549+
// Stop turtle2 when close enough
550+
ipcRenderer.send('turtle2-cmd-vel', {
551+
linear: { x: 0.0, y: 0.0, z: 0.0 },
552+
angular: { x: 0.0, y: 0.0, z: 0.0 },
553+
});
554+
}
555+
}
512556
});
513557
}
514558
function updateTransformList(transform) {

0 commit comments

Comments
 (0)