Skip to content

Commit

Permalink
Merge pull request #53 from MelbourneHighSchoolRobotics/batched_commands
Browse files Browse the repository at this point in the history
Batched commands
  • Loading branch information
glipR authored Aug 29, 2020
2 parents df86935 + ed836cd commit 278f189
Show file tree
Hide file tree
Showing 13 changed files with 211 additions and 79 deletions.
52 changes: 52 additions & 0 deletions docs/batched_commands.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
Batched Commands
================

Running a batched command
-------------------------

In the previous document, we run the simulator and attach script logic in two separate terminals.

.. code-block:: bash
ev3sim bot.yaml
ev3attach demo.py Robot-0
When testing robot code, as well as competitions, many of these commands will be the same however, and it would be much easier if the entire simulation, with code running, could be invoked by a single command.
In fact, the simulator allows for this!

To run the simulator with two bots both running the demo code, execute the following command:

.. code-block:: bash
ev3sim -b soccer_competition.yaml
This ``-b`` or ``--batch`` flag specifies to use the file ``soccer_competition.yaml`` as the information for the simulator, as well as attaching code to bots.

Defining batched commands
-------------------------

You can write your own batched commands, just as you can write your own bot definitions and bot code. You can find the source for ``soccer_competition.yaml`` `here`_.

.. _here: https://github.com/MelbourneHighSchoolRobotics/ev3sim/tree/main/ev3sim/batched_commands/soccer_competition.yaml

The batched command file looks like the following:

.. code-block:: yaml
preset_file: soccer.yaml
bots:
- name: bot.yaml
scripts:
- demo.py
- name: bot.yaml
scripts:
- demo.py
The ``preset_file`` points to the preset to load (usually specified with the ``-p`` flag in ``ev3sim``, but defaults to ``soccer.yaml``).
After this you can specify any bots to load, as well as scripts to attach to them.

Batched command problems
------------------------

If your computer is not powerful enough to run the number of bots specified with scripts attached, the command may just fail or hang.
This method of loading robots is only supplied for ease of use, and has its problems.
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ ev3sim is a pygame based 2D simulator for robots built using LEGO mindstorms usi
:caption: Contents:

setup
batched_commands
ev3_extensions
customisation
system
27 changes: 15 additions & 12 deletions ev3sim/attach.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
def main():
import sys
import logging
import grpc
import ev3sim.simulation.comm_schema_pb2
import ev3sim.simulation.comm_schema_pb2_grpc
import json
import time
import argparse
from unittest import mock
from queue import Queue
import sys
import logging
import grpc
import ev3sim.simulation.comm_schema_pb2
import ev3sim.simulation.comm_schema_pb2_grpc
import json
import time
import argparse
from unittest import mock
from queue import Queue

def main(passed_args = None):
if passed_args is None:
passed_args = sys.argv

parser = argparse.ArgumentParser(description='Attach a valid ev3dev2 script to the simulation.')
parser.add_argument('filename', type=str, help='The relative or absolute path of the script you want to run')
parser.add_argument('robot_id', nargs='?', type=str, help="The ID of the robot you wish to attach to. Right click a robot to copy it's ID to the clipboard. Defaults to the first robot spawned if unspecified.", default='Robot-0')

args = parser.parse_args()
args = parser.parse_args(passed_args[1:])

robot_id = args.robot_id

Expand Down
11 changes: 11 additions & 0 deletions ev3sim/batched_commands/communications_demo.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
preset_file: soccer.yaml
bots:
- name: bot.yaml
scripts:
- communication_server.py
- name: bot.yaml
scripts:
- communication_client.py
- name: bot.yaml
scripts:
- communication_client.py
8 changes: 8 additions & 0 deletions ev3sim/batched_commands/soccer_competition.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
preset_file: soccer.yaml
bots:
- name: bot.yaml
scripts:
- demo.py
- name: bot.yaml
scripts:
- demo.py
30 changes: 30 additions & 0 deletions ev3sim/batched_run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import sys
import yaml
from ev3sim.file_helper import find_abs
from multiprocessing import Process

def batched_run(batch_file):
from ev3sim.single_run import single_run as sim
from ev3sim.attach import main as attach

batch_path = find_abs(batch_file, allowed_areas=['local', 'local/batched_commands/', 'package', 'package/batched_commands/'])
with open(batch_path, 'r') as f:
config = yaml.safe_load(f)

bot_paths = [x['name'] for x in config['bots']]

sim_process = Process(target=sim, args=[config['preset_file'], bot_paths])
script_processes = []
for i, bot in enumerate(config['bots']):
for script in bot.get('scripts', []):
script_processes.append(Process(target=attach, kwargs={'passed_args': ['Useless', script, f"Robot-{i}"]}))

sim_process.start()
for p in script_processes:
p.start()

# At the moment, just wait for the simulator to finish then kill all attach processes.
# If any attach threads error out, then the stack trace is printed anyways so this is fine.
sim_process.join()
for p in script_processes:
p.terminate()
2 changes: 2 additions & 0 deletions ev3sim/devices/colour/ev3.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ class ColorInteractor(IDeviceInteractor):
name = 'COLOUR'

def tick(self, tick):
if tick == -1:
self.device_class.saved_raw = (0, 0, 0)
try:
self.device_class._calc_raw()
ScriptLoader.instance.object_map[self.getPrefix() + 'light_up'].visual.fill = self.device_class.rgb()
Expand Down
2 changes: 2 additions & 0 deletions ev3sim/devices/ultrasonic/ev3.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ class UltrasonicInteractor(IDeviceInteractor):
UPDATE_PER_SECOND = 5

def tick(self, tick):
if tick == -1:
self.device_class.saved = 0
if tick % (ScriptLoader.instance.GAME_TICK_RATE // self.UPDATE_PER_SECOND) == 0:
self.device_class._calc()
ScriptLoader.instance.object_map[self.getPrefix() + 'light_up'].visual.fill = (
Expand Down
9 changes: 8 additions & 1 deletion ev3sim/robot.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ def connectDevices(self):
self.devices[interactor.port] = interactor.device_class
ScriptLoader.instance.object_map[self.robot_key].robot_class = self.robot_class

def sendDeviceInitTicks(self):
for interactor in ScriptLoader.instance.object_map[self.robot_key].device_interactors:
interactor.tick(-1)

def startUp(self):
self.robot_class.startUp()

Expand Down Expand Up @@ -90,6 +94,8 @@ class Robot:
All robot 'definitions' (see `robots/controllable.yaml`) reference a `class_path` (Which is by default this base class), and the actions of this bot are defined by how the following functions are modified:
"""

spawned = False

def getDevice(self, port):
"""
Returns an instance of the device on the port specified.
Expand Down Expand Up @@ -125,7 +131,8 @@ def onSpawn(self):
As an example, calibrating the compass sensors should be done ``onSpawn``, rather than on ``startUp``.
"""
pass
self.spawned = True
self._interactor.sendDeviceInitTicks()

def tick(self, tick):
"""
Expand Down
87 changes: 23 additions & 64 deletions ev3sim/sim.py
Original file line number Diff line number Diff line change
@@ -1,67 +1,26 @@
def main():

import argparse
import sys
from collections import deque
from queue import Queue
import time
from ev3sim.file_helper import find_abs

parser = argparse.ArgumentParser(description='Run the simulation, include some robots.')
parser.add_argument('--preset', type=str, help="Path of preset file to load. (You shouldn't need to change this, by default it is presets/soccer.yaml)", default='soccer.yaml', dest='preset')
parser.add_argument('robots', nargs='+', help='Path of robots to load. Separate each robot path by a space.')

args = parser.parse_args(sys.argv[1:])

import yaml
from ev3sim.simulation.loader import runFromConfig

preset_file = find_abs(args.preset, allowed_areas=['local', 'local/presets/', 'package', 'package/presets/'])
with open(preset_file, 'r') as f:
config = yaml.safe_load(f)

config['robots'] = config.get('robots', []) + args.robots

shared_data = {
'tick': 0, # Current tick
'write_stack': deque(), # All write actions are processed through this
'data_queue': {}, # Simulation data for each bot
'active_count': {}, # Keeps track of which code connection each bot has.
'bot_locks': {}, # Threading Locks and Conditions for each bot to wait for connection actions
'bot_communications_data': {}, # Buffers and information for all bot communications
'tick_updates': {}, # Simply a dictionary where the simulation tick will push static data, so the other methods are aware of when the simulation has exited.
}

result_bucket = Queue(maxsize=1)

from threading import Thread
from ev3sim.simulation.communication import start_server_with_shared_data

def run(shared_data, result):
try:
runFromConfig(config, shared_data)
except Exception as e:
result.put(('Simulation', e))
return
result.put(True)

comm_thread = Thread(target=start_server_with_shared_data, args=(shared_data, result_bucket), daemon=True)
sim_thread = Thread(target=run, args=(shared_data, result_bucket), daemon=True)

comm_thread.start()
sim_thread.start()

try:
with result_bucket.not_empty:
while not result_bucket._qsize():
result_bucket.not_empty.wait(0.1)
r = result_bucket.get()
if r is not True:
print(f"An error occured in the {r[0]} thread. Raising an error now...")
time.sleep(1)
raise r[1]
except KeyboardInterrupt:
pass
import argparse
import sys
import time
from ev3sim.file_helper import find_abs

parser = argparse.ArgumentParser(description='Run the simulation, include some robots.')
parser.add_argument('--preset', '-p', type=str, help="Path of preset file to load. (You shouldn't need to change this, by default it is presets/soccer.yaml)", default='soccer.yaml', dest='preset')
parser.add_argument('robots', nargs='+', help='Path of robots to load. Separate each robot path by a space.')
parser.add_argument('--batch', '-b', action='store_true', help='Whether to use a batched command to run this simulation.', dest='batched')

def main(passed_args = None):
if passed_args is None:
passed_args = sys.argv

args = parser.parse_args(passed_args[1:])

if args.batched:
from ev3sim.batched_run import batched_run
assert len(args.robots) == 1, "Exactly one batched command file should be provided."
batched_run(args.robots[0])
else:
from ev3sim.single_run import single_run
single_run(args.preset, args.robots)

if __name__ == '__main__':
main()
2 changes: 1 addition & 1 deletion ev3sim/simulation/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ def simulate(self):
sensor_type, specific_sensor, attribute = attribute_path.split()
self.robots[rob_id].getDeviceFromPath(sensor_type, specific_sensor).applyWrite(attribute, value)
for key, robot in self.robots.items():
if key in self.data['data_queue']:
if robot.spawned and key in self.data['data_queue']:
self.data['data_queue'][key].put(robot._interactor.collectDeviceData())
# Handle simulation.
# First of all, check the script can handle the current settings.
Expand Down
57 changes: 57 additions & 0 deletions ev3sim/single_run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import argparse
import sys
from collections import deque
from queue import Queue
import time
from ev3sim.file_helper import find_abs
import yaml
from ev3sim.simulation.loader import runFromConfig

def single_run(preset_filename, robots):

preset_file = find_abs(preset_filename, allowed_areas=['local', 'local/presets/', 'package', 'package/presets/'])
with open(preset_file, 'r') as f:
config = yaml.safe_load(f)

config['robots'] = config.get('robots', []) + robots

shared_data = {
'tick': 0, # Current tick
'write_stack': deque(), # All write actions are processed through this
'data_queue': {}, # Simulation data for each bot
'active_count': {}, # Keeps track of which code connection each bot has.
'bot_locks': {}, # Threading Locks and Conditions for each bot to wait for connection actions
'bot_communications_data': {}, # Buffers and information for all bot communications
'tick_updates': {}, # Simply a dictionary where the simulation tick will push static data, so the other methods are aware of when the simulation has exited.
}

result_bucket = Queue(maxsize=1)

from threading import Thread
from ev3sim.simulation.communication import start_server_with_shared_data

def run(shared_data, result):
try:
runFromConfig(config, shared_data)
except Exception as e:
result.put(('Simulation', e))
return
result.put(True)

comm_thread = Thread(target=start_server_with_shared_data, args=(shared_data, result_bucket), daemon=True)
sim_thread = Thread(target=run, args=(shared_data, result_bucket), daemon=True)

comm_thread.start()
sim_thread.start()

try:
with result_bucket.not_empty:
while not result_bucket._qsize():
result_bucket.not_empty.wait(0.1)
r = result_bucket.get()
if r is not True:
print(f"An error occured in the {r[0]} thread. Raising an error now...")
time.sleep(1)
raise r[1]
except KeyboardInterrupt:
pass
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

setup(
name="ev3sim",
version="1.1.0",
version="1.2.0",
description="Simulate ev3dev programs in Python",
long_description=README,
long_description_content_type="text/markdown",
Expand Down

0 comments on commit 278f189

Please sign in to comment.