Menagerie World

This sample world includes numerous performance robots and a fixed vision system. It is intended for exploring interactive and ensemble performance within the HL A11 space.

The scene includes the following:

The vision system script uses the OpenCV Python module for image processing. This can be installed in your Python system using pip3 install opencv-python.

The sample code makes extensive use of the Webots simulated radio system for inter-agent messaging on a common channel. The message string format is inspired by OSC: each message begins with an ‘address’ string identifying an endpoint or data channel, optionally followed by space-delimited arguments. E.g., “/center_light/color 255 0 0” tells the spotlight named center_light to emit a pure red color. More messaging examples can be found in director.py.

The world file is menagerie.wbt, which can be found within the Webots.zip archive. The necessary components are also isolated in the menagerie.zip archive.

../_images/menagerie.png

Screenshot of Webots model of the menagerie world. Robots in the scene include a fixed camera on the back wall, three controllable spotlights, two boom-wheel machines, two ZYY arms, and an actuated chest. Not seen is a disembodied ‘director’ robot which coordinates activity over the simulated radio channel. The camera view is show in the lower left display and the processed vision target in the lower right display.

World File

The world file specifies all the elements of the scene: camera position, background scenery, lighting, fixed objects, active robots, and the disembodied ‘director’.

  1#VRML_SIM R2023b utf8
  2
  3EXTERNPROTO "../protos/HL-A11-room.proto"
  4EXTERNPROTO "../protos/ramp.proto"
  5EXTERNPROTO "../protos/zyy.proto"
  6EXTERNPROTO "../protos/chest.proto"
  7EXTERNPROTO "../protos/spotlight.proto"
  8EXTERNPROTO "../protos/fixed-camera.proto"
  9EXTERNPROTO "../protos/cartesian-target.proto"
 10EXTERNPROTO "../protos/boom-wheel.proto"
 11EXTERNPROTO "../protos/actuated-ramp.proto"
 12
 13WorldInfo {
 14}
 15Viewpoint {
 16  # orientation 0 0 -1 3.14159
 17  # position 7.5 0 1.3
 18  orientation 0.108  -0.0825566  -0.990721  4.38712
 19  position 2.87298  -9.19051  3.15944
 20  followType "None"
 21}
 22Background {
 23  skyColor [
 24    0.1 0.1 0.1
 25  ]
 26}
 27HL-A11-room {
 28  transparency 0.6
 29}
 30ramp {
 31  translation 0.8 -2 0
 32  width 0.2
 33  length 0.4
 34}
 35
 36spotlight {
 37   location 1.5 -2 0.1
 38   rotation 0.68 0.68 -0.28 -1.1
 39   name "left_light"
 40   customData "color_cycle"
 41}
 42
 43spotlight {
 44   location 1.5 2 0.1
 45   rotation -0.73 0.42 0.55 -1.63
 46   name "right_light"
 47   customData "color_cycle"   
 48}
 49
 50spotlight {
 51   location -1.5 0 2
 52   rotation 0 1 0 2.6
 53   name "center_light"
 54}
 55
 56fixed-camera {
 57  location -1.5 0 1.5
 58  fieldOfView 60
 59}
 60
 61zyy {
 62  translation 0 1.0 0.0
 63  name "right_arm"
 64  controller "menagerie_arm"  
 65}
 66zyy {
 67  translation 0 -1.0 0.0
 68  name "left_arm"
 69  controller "menagerie_arm"
 70}
 71chest {
 72  width 0.5
 73  depth 0.5
 74  baseHeight 0.5
 75}
 76
 77cartesian-target {
 78  translation 2.3 0 0
 79  radius 0.5
 80}
 81boom-wheel {
 82  translation 0.8 1.4 0
 83  name "right_wheel"
 84}
 85boom-wheel {
 86  translation 0.8 -1.4 0
 87  name "left_wheel"
 88}
 89actuated-ramp {
 90  translation 1.4 1.4 -0.025
 91  rotation 0 0 1 1.57
 92  width 0.3
 93  length 0.5
 94}
 95
 96Robot {
 97  name "director"
 98  controller "director"
 99  children [
100    Receiver {
101      channel 1
102    }
103    Emitter {
104      channel 1    
105    }
106  ]
107}

Director Script

The overall scene is coordinated by the director script attached to a Robot node with no associated physical form. This script monitors status messages on the radio network, user keyboard input, and potentially other data sources, then transmits instructions over the radio network to the actors. This script would be a good location for full-scene state machines or event sequencing.

  1# director.py
  2#
  3# Sample Webots controller file for a disembodied robot which coordinates
  4# activity between sensory robots and actor robots.
  5
  6# No copyright, 2024, Garth Zeglin.  This file is explicitly placed in the
  7# public domain.
  8
  9print("loading director.py...")
 10
 11################################################################
 12# standard Python libraries
 13import math
 14
 15# Import the Webots API.
 16from controller import Robot
 17from controller import Keyboard
 18
 19# Define the time step in milliseconds between controller updates.
 20EVENT_LOOP_DT = 200
 21
 22################################################################
 23class Director(Robot):
 24
 25    def __init__(self):
 26        super(Director, self).__init__()
 27
 28        self.robot_name = self.getName()
 29        print("%s: controller connected." % (self.robot_name))
 30
 31        # Connect to the radio emitter and receiver.  The radio runs at the same
 32        # rate as the event loop as it is the main function of this robot.
 33        self.receiver = self.getDevice('receiver')
 34        self.emitter  = self.getDevice('emitter')
 35        self.receiver.enable(EVENT_LOOP_DT)
 36
 37        # Connect to keyboard input from the GUI.
 38        self.keyboard = Keyboard()
 39        self.keyboard.enable(EVENT_LOOP_DT)
 40
 41        # Initialize top-level script state machines.
 42        self.state_timer = 2*EVENT_LOOP_DT
 43        self.state_index = 0
 44        self.target_timer = 0
 45
 46        return
 47
 48    #================================================================
 49    def poll_receiver(self):
 50        """Process all available radio messages."""
 51        while self.receiver.getQueueLength() > 0:
 52            packet = self.receiver.getString()
 53            print("%s received: %s" % (self.robot_name, packet))
 54            tokens = packet.split()
 55            if len(tokens) >  0:
 56                if tokens[0] == '/camera/target/dropped':
 57                    self.emitter.send(b"/left_arm/mode path")
 58                    self.emitter.send(b"/right_arm/mode path")
 59                    self.emitter.send(b"/left_wheel/speed -5")
 60                    self.target_timer = 1000
 61
 62                elif tokens[0] == '/camera/target':
 63                    if self.target_timer <= 0:
 64                        self.emitter.send(b"/left_arm/mode pose")
 65                        self.emitter.send(b"/right_arm/mode pose")
 66                        self.emitter.send(b"/left_wheel/speed -15")
 67                        self.target_timer = 1000
 68
 69            # done with packet processing, advance to the next packet
 70            self.receiver.nextPacket()
 71        # no more data
 72        return
 73
 74    #================================================================
 75    def poll_keyboard(self):
 76        """Process all available pressed keyboard keys.  Note that the keyboard
 77        is treated as a state, not events; holding a key will continue to
 78        generate a reported key, and briefly pressing a key might be lost
 79        between samples.
 80        """
 81        while True:
 82            key = self.keyboard.getKey()
 83            if key == -1:
 84                return
 85            else:
 86                code      = key & Keyboard.KEY
 87                modifiers = "shift" if key & Keyboard.SHIFT else ""
 88                modifiers += "ctrl" if key & Keyboard.CONTROL else ""
 89                modifiers += "alt"  if key & Keyboard.ALT else ""
 90
 91                # convert the integer key number to a lowercase single-character string
 92                letter = chr(code).lower()
 93                print("%s: user pressed key %d (%c, %s)" % (self.robot_name, key, letter, modifiers))
 94
 95                # demonstrate translating a keypress to a network message
 96                if letter == 'p':
 97                    self.emitter.send(b"/left_arm/mode pose")
 98
 99                elif letter == 's':
100                    self.emitter.send(b"/left_arm/mode path")
101
102                elif letter == 'r':
103                    self.emitter.send(b"/center_light/color 255 0 0")
104                    print("%s: sending lighting command" % (self.robot_name))
105
106                elif letter == 'b':
107                    self.emitter.send(b"/center_light/color 0 0 0")
108                    print("%s: sending lighting command" % (self.robot_name))
109
110                elif letter == 'f':
111                    self.emitter.send(b"/left_wheel/speed -20")
112                    print("%s: sending speed command" % (self.robot_name))
113
114                elif letter == 'c':
115                    self.emitter.send(b"/chest/period 5")
116                    print("%s: sending period command" % (self.robot_name))
117
118    #================================================================
119    def poll_scene_sequence(self):
120        """State machine update function to walk through a series of scene phases."""
121
122        # Update the state timers
123        self.state_timer -= EVENT_LOOP_DT
124        self.target_timer -= EVENT_LOOP_DT
125
126        # If the timer has elapsed, reset the timer and update the state.
127        if self.state_timer < 0:
128            self.state_timer += 5000
129
130            # here is where scene changes could be programmed
131
132    #================================================================
133    def run(self):
134
135        # Run loop to execute a periodic script until the simulation quits.
136        # If the controller returns -1, the simulator is quitting.
137        while self.step(EVENT_LOOP_DT) != -1:
138
139            # Poll the simulated radio receiver.
140            self.poll_receiver()
141
142            # Poll user input.
143            self.poll_keyboard()
144
145            # Read simulator clock time.
146            t = self.getTime()
147
148            # Update scene state machine.
149            self.poll_scene_sequence()
150
151################################################################
152
153controller = Director()
154controller.run()