1414# limitations under the License.
1515# ----------------------------------------------------------------------------
1616
17+ import select
18+ import sys
19+ import tty
20+ import termios
21+ import time
1722import cv2
1823import torch
1924import numpy as np
2530
2631
2732def run_keyboard_control_for_camera (
28- sensor : Camera ,
33+ sensor : Camera | str ,
2934 trans_step : float = 0.01 ,
3035 rot_step : float = 1.0 ,
3136 vis_pose : bool = False ,
3237) -> None :
3338 """Run keyboard control loop for camera pose adjustment.
3439
3540 Args:
36- sensor (Camera): Camera sensor to control.
41+ sensor (Camera | str ): Camera sensor or name of the camera to control.
3742 trans_step (float, optional): Translation step size. Defaults to 0.01.
3843 rot_step (float, optional): Rotation step size in degrees. Defaults to 1.0.
3944 vis_pose (bool, optional): Whether to visualize the camera pose in axis form. Defaults to False.
4045 """
46+ from embodichain .lab .sim import SimulationManager
47+
48+ sim = SimulationManager .get_instance ()
49+
50+ if vis_pose and sim .is_rt_enabled :
51+ log_warning (
52+ "'vis_pose' is not fully supported with ray tracing enabled. Will be fixed in future updates."
53+ )
54+ return
55+
56+ if isinstance (sensor , str ):
57+ sensor = sim .get_sensor (uid = sensor )
58+
4159 if sensor .num_instances > 1 :
4260 log_warning (
4361 "Multiple sensor instances detected. Keyboard control will only work for one instance."
@@ -62,12 +80,10 @@ def run_keyboard_control_for_camera(
6280
6381 marker = None
6482 if vis_pose :
65- from embodichain .lab .sim import SimulationManager
6683 from embodichain .lab .sim .cfg import MarkerCfg
6784
6885 init_axis_pose = sensor .get_arena_pose (to_matrix = True ).squeeze ().numpy ()
6986
70- sim = SimulationManager .get_instance ()
7187 marker = sim .draw_marker (
7288 cfg = MarkerCfg (
7389 name = "camera_axis" ,
@@ -227,3 +243,265 @@ def run_keyboard_control_for_camera(
227243 cv2 .destroyAllWindows ()
228244 except Exception as e :
229245 log_warning (f"cv2.destroyAllWindows() failed: { e } " )
246+
247+
248+ def run_keyboard_control_for_light (
249+ light : object | str ,
250+ trans_step : float = 0.01 ,
251+ intensity_step : float = 1.0 ,
252+ falloff_step : float = 1.0 ,
253+ color_step : float = 0.05 ,
254+ vis_pose : bool = False ,
255+ ) -> None :
256+ """Run keyboard control loop for light adjustment.
257+
258+ Args:
259+ light (Light | str): Light object or name of the light to control.
260+ trans_step (float, optional): Translation step size. Defaults to 0.01.
261+ intensity_step (float, optional): Intensity adjustment step. Defaults to 0.1.
262+ falloff_step (float, optional): Falloff/radius adjustment step. Defaults to 0.1.
263+ color_step (float, optional): Color channel adjustment step. Defaults to 0.05.
264+ vis_pose (bool, optional): Whether to visualize the light position with a marker. Defaults to False.
265+ """
266+ from embodichain .lab .sim .objects import Light
267+ from embodichain .lab .sim import SimulationManager
268+
269+ sim = SimulationManager .get_instance ()
270+
271+ if vis_pose and sim .is_rt_enabled :
272+ log_warning (
273+ "'vis_pose' is not fully supported with ray tracing enabled. Will be fixed in future updates."
274+ )
275+ return
276+
277+ if isinstance (light , str ):
278+ light : Light = sim .get_light (uid = light )
279+
280+ if light .num_instances > 1 :
281+ log_warning (
282+ "Multiple light instances detected. Keyboard control will only work for one instance."
283+ )
284+ return
285+
286+ log_info ("\n === Light Control ===" )
287+ log_info ("Translation controls:" )
288+ log_info (" W/S: Move forward/backward (Z-axis)" )
289+ log_info (" A/D: Move left/right (Y-axis)" )
290+ log_info (" Q/E: Move up/down (X-axis)" )
291+ log_info ("\n Intensity controls:" )
292+ log_info (" I/K: Increase/decrease intensity" )
293+ log_info ("\n Falloff controls:" )
294+ log_info (" U/O: Increase/decrease falloff radius" )
295+ log_info ("\n Color controls:" )
296+ log_info (" T/Y: Increase/decrease red channel" )
297+ log_info (" G/H: Increase/decrease green channel" )
298+ log_info (" B/N: Increase/decrease blue channel" )
299+ log_info ("\n Other controls:" )
300+ log_info (" R: Reset to initial values" )
301+ log_info (" P: Print current light properties" )
302+ log_info (" ESC: Exit control mode" )
303+
304+ # Store initial values from config
305+ init_pose = light .get_local_pose ()
306+ init_color = torch .as_tensor (light .cfg .color ).clone ()
307+ init_intensity = float (light .cfg .intensity )
308+ init_falloff = float (light .cfg .radius )
309+
310+ # Current values
311+ current_color = init_color .clone ()
312+ current_intensity = init_intensity
313+ current_falloff = init_falloff
314+
315+ marker = None
316+ if vis_pose :
317+ from embodichain .lab .sim import SimulationManager
318+ from embodichain .lab .sim .cfg import MarkerCfg
319+
320+ init_marker_pose = light .get_local_pose (to_matrix = True ).squeeze ().numpy ()
321+
322+ sim = SimulationManager .get_instance ()
323+ marker = sim .draw_marker (
324+ cfg = MarkerCfg (
325+ name = "light_marker" ,
326+ marker_type = "axis" ,
327+ axis_xpos = [init_marker_pose ],
328+ axis_size = 0.002 ,
329+ axis_len = 0.05 ,
330+ )
331+ )
332+
333+ # TODO: We may add node to BatchEntity object.
334+ marker [0 ].node .attach_node (light ._entities [0 ].get_node ())
335+
336+ log_info ("\n Light control active. Press keys to adjust light properties..." )
337+
338+ # Save terminal settings
339+ old_settings = termios .tcgetattr (sys .stdin )
340+ tty .setcbreak (sys .stdin .fileno ())
341+
342+ def get_key ():
343+ """Non-blocking keyboard input."""
344+ if select .select ([sys .stdin ], [], [], 0 )[0 ]:
345+ return sys .stdin .read (1 )
346+ return None
347+
348+ try :
349+
350+ while True :
351+ current_pose = light .get_local_pose ().squeeze ().numpy ()
352+
353+ # Non-blocking key input
354+ key = get_key ()
355+
356+ if key is None :
357+ continue
358+ elif key in ["\x1b " ]: # Q or ESC
359+ if vis_pose :
360+ sim .remove_marker ("light_marker" )
361+ log_info ("Exiting light control mode..." )
362+ break
363+
364+ property_changed = False
365+ new_pose = current_pose .copy ()
366+
367+ # Translation controls
368+ if key in ["w" , "W" ]:
369+ new_pose [2 ] += trans_step
370+ property_changed = True
371+ log_info (f"Moving forward: Z += { trans_step } " )
372+ elif key in ["s" , "S" ]:
373+ new_pose [2 ] -= trans_step
374+ property_changed = True
375+ log_info (f"Moving backward: Z -= { trans_step } " )
376+ elif key in ["a" , "A" ]:
377+ new_pose [1 ] -= trans_step
378+ property_changed = True
379+ log_info (f"Moving left: Y -= { trans_step } " )
380+ elif key in ["d" , "D" ]:
381+ new_pose [1 ] += trans_step
382+ property_changed = True
383+ log_info (f"Moving right: Y += { trans_step } " )
384+ elif key in ["q" , "Q" ]:
385+ new_pose [0 ] += trans_step
386+ property_changed = True
387+ log_info (f"Moving up: X += { trans_step } " )
388+ elif key in ["e" , "E" ]:
389+ new_pose [0 ] -= trans_step
390+ property_changed = True
391+ log_info (f"Moving down: X -= { trans_step } " )
392+
393+ # Intensity controls
394+ elif key in ["i" , "I" ]:
395+ current_intensity += intensity_step
396+ current_intensity = max (0.0 , current_intensity )
397+ light .set_intensity (torch .tensor (current_intensity ))
398+ property_changed = True
399+ log_info (f"Intensity increased to: { current_intensity :.2f} " )
400+ elif key in ["k" , "K" ]:
401+ current_intensity -= intensity_step
402+ current_intensity = max (0.0 , current_intensity )
403+ light .set_intensity (torch .tensor (current_intensity ))
404+ property_changed = True
405+ log_info (f"Intensity decreased to: { current_intensity :.2f} " )
406+
407+ # Falloff controls
408+ elif key in ["u" , "U" ]:
409+ current_falloff += falloff_step
410+ current_falloff = max (0.0 , current_falloff )
411+ light .set_falloff (torch .tensor (current_falloff ))
412+ property_changed = True
413+ log_info (f"Falloff increased to: { current_falloff :.2f} " )
414+ elif key in ["o" , "O" ]:
415+ current_falloff -= falloff_step
416+ current_falloff = max (0.0 , current_falloff )
417+ light .set_falloff (torch .tensor (current_falloff ))
418+ property_changed = True
419+ log_info (f"Falloff decreased to: { current_falloff :.2f} " )
420+
421+ # Color controls - Red channel
422+ elif key in ["t" , "T" ]:
423+ current_color [0 ] += color_step
424+ current_color [0 ] = torch .clamp (current_color [0 ], 0.0 , 1.0 )
425+ light .set_color (current_color )
426+ property_changed = True
427+ log_info (f"Red channel increased to: { current_color [0 ]:.2f} " )
428+ elif key in ["y" , "Y" ]:
429+ current_color [0 ] -= color_step
430+ current_color [0 ] = torch .clamp (current_color [0 ], 0.0 , 1.0 )
431+ light .set_color (current_color )
432+ property_changed = True
433+ log_info (f"Red channel decreased to: { current_color [0 ]:.2f} " )
434+
435+ # Color controls - Green channel
436+ elif key in ["g" , "G" ]:
437+ current_color [1 ] += color_step
438+ current_color [1 ] = torch .clamp (current_color [1 ], 0.0 , 1.0 )
439+ light .set_color (current_color )
440+ property_changed = True
441+ log_info (f"Green channel increased to: { current_color [1 ]:.2f} " )
442+ elif key in ["h" , "H" ]:
443+ current_color [1 ] -= color_step
444+ current_color [1 ] = torch .clamp (current_color [1 ], 0.0 , 1.0 )
445+ light .set_color (current_color )
446+ property_changed = True
447+ log_info (f"Green channel decreased to: { current_color [1 ]:.2f} " )
448+
449+ # Color controls - Blue channel
450+ elif key in ["b" , "B" ]:
451+ current_color [2 ] += color_step
452+ current_color [2 ] = torch .clamp (current_color [2 ], 0.0 , 1.0 )
453+ light .set_color (current_color )
454+ property_changed = True
455+ log_info (f"Blue channel increased to: { current_color [2 ]:.2f} " )
456+ elif key in ["n" , "N" ]:
457+ current_color [2 ] -= color_step
458+ current_color [2 ] = torch .clamp (current_color [2 ], 0.0 , 1.0 )
459+ light .set_color (current_color )
460+ property_changed = True
461+ log_info (f"Blue channel decreased to: { current_color [2 ]:.2f} " )
462+
463+ # Reset control
464+ elif key in ["r" , "R" ]:
465+ current_color = init_color .clone ()
466+ current_intensity = init_intensity
467+ current_falloff = init_falloff
468+ light .set_local_pose (init_pose )
469+ light .set_color (current_color )
470+ light .set_intensity (torch .tensor (current_intensity ))
471+ light .set_falloff (torch .tensor (current_falloff ))
472+ property_changed = True
473+ log_info ("Reset to initial light properties" )
474+
475+ # Print current properties
476+ elif key in ["p" , "P" ]:
477+ translation = current_pose [:3 ]
478+ log_info ("\n === Current Light Properties ===" )
479+ log_info (f"Position: { translation } " )
480+ log_info (f"Color (RGB): { current_color .numpy ()} " )
481+ log_info (f"Intensity: { current_intensity :.2f} " )
482+ log_info (f"Falloff: { current_falloff :.2f} " )
483+
484+ # Update pose if translation changed
485+ if property_changed and not np .allclose (new_pose , current_pose ):
486+ light_pose = torch .as_tensor (new_pose , dtype = torch .float32 ).unsqueeze_ (
487+ 0
488+ )
489+ light .set_local_pose (light_pose )
490+
491+ # Update simulation if any property changed
492+ if property_changed and vis_pose :
493+ sim .update (step = 1 )
494+
495+ except KeyboardInterrupt :
496+ if vis_pose :
497+ sim .remove_marker ("light_marker" )
498+ log_info ("\n Control loop interrupted by user (Ctrl+C)" )
499+ except Exception as e :
500+ log_error (f"Error in light control loop: { e } " )
501+ finally :
502+ try :
503+ # Restore terminal settings
504+ termios .tcsetattr (sys .stdin , termios .TCSADRAIN , old_settings )
505+ except :
506+ pass
507+ log_info ("Light control loop terminated" )
0 commit comments