diff --git a/README.md b/README.md
index 5acd0c9..01e064b 100644
--- a/README.md
+++ b/README.md
@@ -25,30 +25,169 @@ Documentation is available on our website: https://www.lgsvlsimulator.com/docs/p
# install in development mode
pip3 install --user -e .
-# Running unit tests
+# LGSVL PythonAPI Scenario Randomizer
- # run all unittests
- python3 -m unittest discover -v -c
+The Scenario Randomizer is split into four components:
+
+
LGSVL Sim:
+
+LGSVL (LG Silicon Valley Lab) Simulator is simulation software developed for the acceleration of safe autonomous vehicle development. The current version as of writing this document is 2019.10, however LG have stated that they plan on releasing a new version every month or so. LGSVL is required for the Scenario Randomizer.
+
+ LGSVL PythonAPI:
+
+LGSVL Simulator exposes runtime functionality to a Python API which you can use to manipulate object placement and vehicle movement in a loaded scene, retreive sensor configuration and data, control weather, time state, and more. The Scenario Randomization script is written using this PythonAPI. The full documentation can be found here: https://www.lgsvlsimulator.com/docs/python-api/
+
+ ROSBridge:
+
+ROSBridge allows for the connection between ROS and the vehicle. This means that once the ROSBridge connection is established we can view the vehicle’s sensor output, or send it commands.
+
+ Scenario Randomizer GUI:
+
+The GUI was made in Python using the Tkinter module. The GUI allows the user to control the parameters of the scenario without needing to enter the code, such as the amount of NPC Vehicles to spawn, the runtime of the scenario, or the timescale.
+
+
+To proceed, please download the latest version of the LGSVL Sim and the Scenario Randomizer:
+
+The latest version of the LGSVL Simulator can be downloaded/cloned from here: https://www.lgsvlsimulator.com/
+
+The Scenario Randomizer can be cloned from here: https://bitbucket.org/Bibman/pythonapi/src/master/
+
+
+# Launching through the bash script
+
+Each component (RosBridge, GUI, LGSVL sim) can be manually run on it’s own, however for ease of use I have included a bash script which launches everything simultaneously, and uses xdotool to skip the graphical settings menu of the simulator. To run it, cd into where you cloned the repository, and execute the launch script.
+
+ cd ~/PythonAPI
+ ./launch.sh
- # run single test module
- python3 -m unittest -v -c tests/test_XXXX.py
+Furthermore, if you would like to change the graphical settings of the simulator from its’ default state, you can add the -r argument to the launch command.
+
+ ./launch.sh -r
+
+# Launching each component manually
+
+ LGSVL:
+
+In the LGSVL folder, find the ‘simulator’ executable and run it. It should open a graphical settings menu. Select the graphics that best fit your specifications and press ‘OK’. After that you should be greeted with a menu similar to this one.
+
+
+
+Press the ‘Open Browser’ button, and a webpage should open. More on this webpage later.
+
+ ROSBridge:
+
+Assuming you have ROS installed and a stable internet connection, open a new terminal and type the following.
+
+ roslaunch rosbridge_server rosbridge_websocket.launch
+
+ GUI:
+
+cd into PythonAPI/RS_scripts and run the following command.
+
+ python3 GUI.py
+
+# The LGSVL sim website
+
+
+
+There are four tabs on the website; Maps, Vehicles, Clusters, and Simulations
+The following links explain in-depth about each tab.
+
+Maps: https://www.lgsvlsimulator.com/docs/maps-tab/
+
+Vehicles: https://www.lgsvlsimulator.com/docs/vehicles-tab/
+
+Clusters: https://www.lgsvlsimulator.com/docs/clusters-tab/
+
+Simulations: https://www.lgsvlsimulator.com/docs/simulations-tab/
+
+An important thing to note is that the Scenario Randomizer is currently only set to work when everything is running on one computer
+Once you have a simulation under the simulation tab that is using the map, vehicle, and cluster of your choice, with respect to the notes above, you can select it then press the button on the bottom of the webpage to run the simulation.
+The simulation is now running and waiting for the API to determine the scenario passed from the GUI.
+
+
+
+# The GUI
+
+The GUI is split into four tabs; Run, Replay, Output, and Params. The GUI allows for running multiple scenarios using a particular LGSVL map and vehicle, replaying these randomly generated scenarios, and saving them to a file to be replayed at a later time.
+
+ The Run tab
- # run individual test case
- python3 -m unittest -v tests.test_XXX.TestCaseXXX.test_XXX
- python3 -m unittest -v tests.test_Simulator.TestSimulator.test_unload_scene
+
+
+The entry boxes are labelled and fairly self-explanatory, however I will go over them regardless.
+
+Vehicle name – As it states in the label under the entry box, it is vital that the name is entered exactly as it is written on the LGSVL sim website
+
+Seed – An integer between -2147483649 and 2147483647 that determines the behaviour of the NPC vehicles
+
+NPCs – A dropdown box for selecting how many NPCs to spawn
+
+Map – Similar to the vehicle name, it is critical the name is exactly as it appears on the LGSVL site
-# Creating test coverage report
+Runs – The amount of times to randomly generate a scenario using the selected values
- # (one time only) install coverage.py
- pip3 install --user coverage
+Runtime – The time in seconds of each scenario generated using the selected values
+
+Timescale – The timescale of each scenario, however be aware that it is fairly buggy
+
+RUN – The activation button, runs the simulation with the selected variables
+
+If a value entered raises an exception, the GUI displays it at the bottom, under the “RUN” button
+
+ The Replay tab
- # run all tests with coverage
- ~/.local/bin/coverage run -m unittest discover
+
+
+This tab is used for running currently available replays of scenarios, whether they are scenarios that were just run and are therefore still saved locally, or scenarios that were saved previously and have now been loaded.
+
+Replay key – The key of the replay you wish to run
+
+Key list – A list of all replay keys currently available, this updates as stored scenarios get created or deleted
+
+REPLAY – Starts the replay using the entered key
+
+Clear Stored Replays – Clears all currently locally stored replays
+
+Replays are either stored locally or saved to a file. Locally stored replays are saved in the temp folder, meaning they persist until the computer is shut off. Clearing the stored replays deletes all replays in the temp folder, meaning if you loaded replays from a file, and cleared stored replays, the file containing the replays still exists, however the replays are no longer in the temp folder so you will need to load them in again if you wish to replay them.
+
+ The Output tab
- # generate html report
- ~/.local/bin/coverage html --omit "~/.local/*","tests/*"
+
+
+As its’ name suggests, the output tab has a console-like output, for each run displaying its’ result (collision or no collision), its’ seed, and its’ replay key. It also has buttons for clearing the output log, saving specific replays to a file, and loading specific replays from a file.
+
+The “SAVE” button opens a scrollable list of checkboxes, each one referring to an available replay key.
+
+
+
+“Save” opens another popup, this time asking under which directory you would like to save the selected scenarios. The scenarios will be saved in a file titled “LGSVL” followed by the date and time of when the scenarios were saved. Changing the name will not cause errors.
+
+
+
+Back on the Output tab, the “LOAD” button allows for loading an already saved folder of scenarios. Pressing on it produces the same popup that appears when pressing “Clear Stored Replays” on the Replay tab.
+
+
+
+Upon pressing “Yes”, the same directory selection from the “SAVE” button will appear. This time, enter a folder that was created by saving scenarios. Ensure you have entered it before pressing “OK”, as shown below.
+
+
+
+Pressing “OK” will load the replays from the selected folder, and they can now be used in the Replay tab.
+
+ The Params tab
- # output is in htmlcov/index.html
+
+
+The Params tab has extra functionality for creating new scenarios.
+
+Distance between vehicles – The distance in meters that vehicles will spawn from one another. Larger vehicles, such as the School Bus and Box Truck will use double this distance. If nothing is entered the default value is 5. Using too small of a value will cause vehicles to instantiate inside each other, and fly around wildly
+
+Spawn start and end positions – The range in meters that vehicles can spawn in relation to the EGO vehicle. For example; if start is 10 and end is 300, the closest an NPC vehicle can spawn to the EGO vehicle is 10 meters and the farthest is 300 meters. If nothing is entered the default values are 10 and 500
+
+Vehicle list – The vehicles to be used in the next scenarios, by default all vehicles are turned on
+
+A timeout will occur if the selected params make it impossible to run the Scenario Randomizer. For example, trying to fit 20 vehicles between 10 and 15 meters from the EGO vehicle will cause a timeout.
# Copyright and License
diff --git a/RS_scripts/GUI.py b/RS_scripts/GUI.py
new file mode 100755
index 0000000..7d21380
--- /dev/null
+++ b/RS_scripts/GUI.py
@@ -0,0 +1,684 @@
+import os
+import shutil
+import sys
+import tempfile
+import tkinter as tk
+from tkinter import filedialog
+from tkinter import ttk
+from tkinter.scrolledtext import ScrolledText
+from datetime import datetime
+import Randomizer
+
+# general GUI setup
+root = tk.Tk()
+root.title("LGSVL PythonAPI Scenario Randomizer")
+root.resizable(width=False, height=False)
+root.geometry('700x350')
+program_directory = sys.path[0]
+program_directory = program_directory.replace('RS_scripts', 'misc')
+root.iconphoto(True, tk.PhotoImage(file=os.path.join(program_directory, "yoinkPY.png")))
+
+# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+# TAB SETUP
+tabControl = ttk.Notebook(root)
+tab1 = ttk.Frame(tabControl)
+tabControl.add(tab1, text="Run")
+
+tab2 = ttk.Frame(tabControl)
+tabControl.add(tab2, text="Replay")
+
+tab3 = ttk.Frame(tabControl)
+tabControl.add(tab3, text="Output")
+
+tab4 = ttk.Frame(tabControl)
+tabControl.add(tab4, text='Params')
+
+
+# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+# These functions are mostly helper functions, which are called by various buttons and entry boxes
+
+# validate only ints are entered in entry box, negatives not allowed
+# noinspection PyUnusedLocal
+def validate_int(action, index, value_if_allowed,
+ prior_value, text, validation_type, trigger_type, widget_name):
+ # action=1 -> insert
+ if action == '1':
+ if text in '0123456789':
+ try:
+ int(value_if_allowed)
+ return True
+ except ValueError:
+ return False
+ else:
+ return False
+ else:
+ return True
+
+
+# validate only ints are entered in entry box, negatives allowed
+# noinspection PyUnusedLocal
+def validate_int_negatives(action, index, value_if_allowed,
+ prior_value, text, validation_type, trigger_type, widget_name):
+ # action=1 -> insert
+ if action == '1':
+ if text in '0123456789':
+ try:
+ float(value_if_allowed)
+ return True
+ except ValueError:
+ return False
+ if text in '-' and value_if_allowed == '-':
+ return True
+ else:
+ return False
+ else:
+ return True
+
+
+# validate only ints or floats are entered in entry box, negatives not allowed
+# noinspection PyUnusedLocal
+def validate_float(action, index, value_if_allowed,
+ prior_value, text, validation_type, trigger_type, widget_name):
+ # action=1 -> insert
+ if action == '1':
+ if text in '0123456789.':
+ try:
+ float(value_if_allowed)
+ return True
+ except ValueError:
+ return False
+ else:
+ return False
+ else:
+ return True
+
+
+# validate only ints or floats are entered in entry box, negatives not allowed
+# noinspection PyUnusedLocal
+def validate_float_spinbox(action, index, value_if_allowed,
+ prior_value, text, validation_type, trigger_type, widget_name):
+ # action=1 -> insert
+ if action == '1':
+ if text in '0123456789.':
+ try:
+ float(value_if_allowed)
+ except ValueError:
+ return False
+ else:
+ if float(value_if_allowed) > 1.0:
+ return False
+ else:
+ return True
+ else:
+ return False
+ else:
+ return True
+
+
+# validate only ints or floats are entered in entry box, negatives not allowed
+# noinspection PyUnusedLocal
+def validate_float_timeofday(action, index, value_if_allowed,
+ prior_value, text, validation_type, trigger_type, widget_name):
+ # action=1 -> insert
+ if action == '1':
+ if text in '0123456789.':
+ try:
+ float(value_if_allowed)
+ except ValueError:
+ return False
+ else:
+ if float(value_if_allowed) > 24.0:
+ return False
+ else:
+ return True
+ else:
+ return False
+ else:
+ return True
+
+
+# add passed argument text to the output tab's scrolledtext
+def addEntryContentToScrolledText(text, textwidget):
+ entryValue = text
+ textwidget.configure(state='normal')
+ if entryValue != "":
+ textwidget.insert("insert", (entryValue + "\n"))
+ textwidget.configure(state='disabled')
+ textwidget.see('end')
+
+
+# helper function to return all the keys of all the current existing scenarios
+def getKeys(file):
+ if file == '':
+ filename = tempfile.gettempdir() + '/or/pickleDict.txt'
+ else:
+ filename = file
+ keys = []
+ with open(filename, 'r') as f:
+ lines = f.read().splitlines()
+ if not lines:
+ return None
+ for line in lines:
+ keys.append(line[-4:])
+ return keys
+
+
+# python doesn't like numbers with zeros in front (0001, 0002...)
+# this helper function returns the number with the zeros in front by turning it to a string
+def fixNumbers(number):
+ numbers = [char for char in str(number)]
+ strFixed = str(number)
+ zeros = 4 - len(numbers)
+ for x in range(zeros):
+ strFixed = "0" + strFixed
+ return strFixed
+
+
+# helper function, allows you to pass functions to be called together, each with respective parameters
+def combine_funcs(*funcs):
+ def combined_func(*args, **kwargs):
+ for f in funcs:
+ f(*args, **kwargs)
+
+ return combined_func
+
+
+# the popup that appears when pressing the save button, places all existing scenarios as checkboxes, allowing you to
+# select an arbitrary amount to be saved. the save button kills the popup and calls save_selected_scenarios with the
+# selected checkboxes
+def popup_select_scenarios(keys):
+ win = tk.Toplevel()
+ win.resizable(width=False, height=False)
+ win.geometry('200x225')
+ win.wm_title("Select scenarios to be saved")
+
+ canvas = tk.Canvas(win, width=200, height=175)
+
+ x = 0
+ y = 0
+ var_list = []
+ for key in keys:
+ var = tk.IntVar()
+ var_list.append(var)
+ cb = tk.Checkbutton(canvas, text=key, variable=var)
+ canvas.create_window(y * 75, x * 25, anchor='nw', window=cb)
+ if x == 6:
+ y += 1
+ x = 0
+ else:
+ x += 1
+
+ canvas.grid(row=0, column=0)
+ if len(keys) > 14:
+ scroll_x = tk.Scrollbar(win, orient="horizontal", command=canvas.xview)
+ scroll_x.grid(row=1, column=0, sticky="ew")
+
+ canvas.configure(xscrollcommand=scroll_x.set)
+ canvas.configure(scrollregion=canvas.bbox("all"))
+
+ save_button = ttk.Button(win, text="Save",
+ command=combine_funcs(win.destroy, lambda: save_selected_scenarios(var_list, keys)))
+ save_button.grid(row=3, column=0, columnspan=2)
+
+
+# given the list of checkboxes, check if any are selected. if none are create error popup and close the first popup. if
+# any are selected, open a directory selector popup, and in the chosen directory copy the selected scenarios to the
+# directory, in a file titled "LGSVL" followed by today's date and the current time. also create a new pickleDict which
+# is a subset of the current pickleDict, however it only contains the relevant scenarios and not the unselected ones
+def save_selected_scenarios(var_list, keys):
+ values = []
+ keysToBeSaved = []
+ for var in var_list:
+ values.append(var.get())
+ if all(i == 0 for i in values):
+ error_popup('Error: No scenarios selected')
+ else:
+ indices = [i for i, e in enumerate(values) if e == 1]
+ x = 0
+ for key in keys:
+ if x in indices:
+ keysToBeSaved.append(key)
+ x += 1
+ # print(keysToBeSaved)
+ homedir = os.environ['HOME']
+ root.directory = tk.filedialog.askdirectory(initialdir=homedir)
+ if root.directory == ():
+ return
+ pfiles, dict_lines = get_picklefiles(keysToBeSaved, '')
+ path = root.directory + '/LGSVL ' + str(
+ datetime.strptime(str(datetime.now().replace(microsecond=0)), '%Y-%m-%d %H:%M:%S').strftime(
+ '%d-%m-%Y %H:%M:%S'))
+ try:
+ os.mkdir(path)
+ except OSError:
+ addEntryContentToScrolledText("Creation of the directory %s failed" % path, tab3_output_scrolledtext)
+ else:
+ addEntryContentToScrolledText("Successfully created the directory %s " % path, tab3_output_scrolledtext)
+ copy_pfiles(pfiles, '', path)
+ write_pickledict(dict_lines, path)
+
+
+# given a list of names of pickle files to be copied and a path for them to be copied to, copy the pickle files to the
+# directory at the given path
+def copy_pfiles(pfiles, fromPath, toPath):
+ if fromPath == '':
+ fromPath = tempfile.gettempdir() + '/or'
+ if toPath == '':
+ if not os.path.isdir(tempfile.gettempdir() + '/or'):
+ try:
+ os.mkdir(tempfile.gettempdir() + '/or')
+ except OSError:
+ print("Creation of the directory %s failed" % tempfile.gettempdir() + '/or')
+ return
+ else:
+ print("Successfully created the directory %s " % tempfile.gettempdir() + '/or')
+ toPath = tempfile.gettempdir() + '/or'
+ directory = os.fsencode(fromPath)
+ for file in os.listdir(directory):
+ filename = os.fsdecode(file)
+ # str_filename = str(filename)
+ if filename in pfiles:
+ shutil.copy(fromPath + '/' + filename, toPath)
+ continue
+ else:
+ continue
+
+
+# given lines to be written in the pickleDict, and the path of where the pickle files will be stored, write a new
+# pickleDict file
+def write_pickledict(lines, path):
+ file = path + '/pickleDict.txt'
+ f = open(file, 'w')
+ f.write('\n'.join(lines))
+ f.close()
+
+
+# given the keys of the selected scenarios, return a list of corresponding file names, and corresponding lines
+# in pickleDict
+def get_picklefiles(keys, path):
+ if path == '':
+ path = tempfile.gettempdir() + '/or/pickleDict.txt'
+ else:
+ path = path + '/pickleDict.txt'
+ files = []
+ lines_with_file = []
+ with open(path, 'r') as f:
+ lines = f.read().splitlines()
+ for line in lines:
+ if line[-4:] in keys:
+ lines_with_file.append(line)
+ files.append(line.replace(line[-6:], ''))
+ return files, lines_with_file
+
+
+# clear the output tab
+def clear_output():
+ tab3_output_scrolledtext.configure(state='normal')
+ tab3_output_scrolledtext.delete('1.0', 'end')
+ tab3_output_scrolledtext.configure(state='disabled')
+
+
+# check if any pickle files exist. if they do forward them to them select scenarios popup
+def save_to_file():
+ try:
+ keys = getKeys('')
+ except FileNotFoundError:
+ error_popup('Error: no scenarios exist')
+ else:
+ popup_select_scenarios(keys)
+
+
+# generic error message popup, print passed message with okay button to close popup
+def error_popup(msg):
+ pop = tk.Toplevel()
+ pop.wm_title("ERROR")
+ label = tk.Label(pop, text=msg)
+ label.grid(row=0, column=0)
+ btn = ttk.Button(pop, text="Okay", command=pop.destroy)
+ btn.grid(row=1, column=0)
+
+
+# are you sure popup, msg is the are you sure question, command is the function to call if yes is pressed
+def are_you_sure_popup(msg, command):
+ pop = tk.Toplevel()
+ label = tk.Label(pop, text=msg)
+ label.grid(columnspan=2)
+ btn_y = tk.Button(pop, text='Yes', bg='green', command=combine_funcs(command, pop.destroy))
+ btn_n = tk.Button(pop, text='No', command=pop.destroy, bg='red')
+ btn_y.grid(row=1)
+ btn_n.grid(row=1, column=1)
+ root.wait_window(pop)
+
+
+# check that all the pickle files that are stored in the pickleDict file actually exist. this is to ensure no
+# shenanigans and/or tomfoolery have taken place in the files
+def validate_pickles(path):
+ try:
+ keys = getKeys(path + '/pickleDict.txt')
+ except FileNotFoundError:
+ return False
+ except TypeError:
+ return True, None
+
+ files, lines = get_picklefiles(keys, path)
+
+ directory = os.fsencode(path)
+ for file in os.listdir(directory):
+ filename = os.fsdecode(file)
+ if filename not in files:
+ if filename == 'pickleDict.txt':
+ continue
+ else:
+ return False
+
+ return True, files
+ # print(keys)
+
+
+# show and format nicely the existing pickle files in the temp folder, as checkboxes
+def update_available_replays(canvas):
+ try:
+ keys = getKeys('')
+ if keys == '':
+ canvas.place_forget()
+ x = 0
+ y = 0
+ lbs = []
+ for key in keys:
+ lb = tk.Label(canvas, text=key)
+ lbs.append(lb)
+ canvas.create_window(y * 55, x * 17.5, anchor='nw', window=lb)
+ if x == 6:
+ y += 1
+ x = 0
+ else:
+ x += 1
+
+ canvas.place(relx=0.5, rely=0.4, anchor='center')
+ if len(keys) > 14:
+ scroll_x = tk.Scrollbar(tab2, orient="horizontal", command=canvas.xview)
+ scroll_x.place(relx=0.5, rely=0.60, anchor='center', width=200)
+
+ canvas.configure(xscrollcommand=scroll_x.set)
+ canvas.configure(scrollregion=canvas.bbox("all"))
+ except FileNotFoundError:
+ canvas.place_forget()
+
+
+# checks if the port that has been passed to the function as an integer is currently at this very moment being used by
+# some program that is currently running on the computer on which this function is being run and returns true if the
+# passed port is being used by a program on this computer and returns false if the port is not being used, you fool
+def is_port_in_use(port):
+ import socket
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
+ return s.connect_ex(('localhost', port)) == 0
+
+
+# create an array of strings of names of selected vehicles to be used in the scenario
+def array_vehiclenames():
+ names = ["Sedan", "SUV", "Jeep", "Hatchback", "SchoolBus", "BoxTruck"]
+ vars = [var_sedan.get(), var_suv.get(), var_jeep.get(), var_hatchback.get(), var_schoolbus.get(),
+ var_boxtruck.get()]
+ indices = [i for i, x in enumerate(vars) if x == 1]
+ return [names[i] for i in indices]
+
+
+# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+# These functions are ones that are called from different buttons, and rely on the helper functions
+# in the code section above
+
+# run button function, first checks if LGSVL is running, if not it exits the function and displays an error
+# otherwise, it attempts to run an instance of the Randomizer run function using the parameters input by the user
+# If the vehicle doesn't exist, or there is some sort of error in the entries, an error is displayed. If the run was
+# successful, run it again however many times are left according to the runs entry box
+def run():
+ tab1_error_label.grid_forget()
+ tab1_nameerror_label.grid_forget()
+ tab1_serveroffline_label.grid_forget()
+ tab1_unabletospawn_label.grid_forget()
+
+ if not is_port_in_use(8181):
+ tab1_serveroffline_label.grid(columnspan=2, column=1)
+ return
+ try:
+ msg = Randomizer.run(tab1_vehicle_name_entry.get(), variable_NPCs.get(), tab1_map_entry.get(),
+ tab1_runtime_entry.get(), tab1_seed_entry.get(), tab1_timescale_entry.get(),
+ distbetween=tab4_distbetweencars_entry.get(), cars_to_use=array_vehiclenames(),
+ spawn_start=tab4_spawnstart_entry.get(), spawn_end=tab4_spawnend_entry.get(),
+ rain=tab4_rain_entry.get(), fog=tab4_fog_entry.get(), wetness=tab4_wetness_entry.get(),
+ timeofday=tab4_timeofday_entry.get(), fixed=var_fixed.get())
+ addEntryContentToScrolledText(msg, tab3_output_scrolledtext)
+ except ZeroDivisionError:
+ tab1_nameerror_label.grid(columnspan=2, column=1)
+ except ValueError:
+ tab1_error_label.grid(columnspan=2, column=1)
+ except TimeoutError:
+ tab1_unabletospawn_label.grid(columnspan=2, column=1)
+ else:
+ for x in range(int(tab1_runs_entry.get()) - 1):
+ msg = Randomizer.run(tab1_vehicle_name_entry.get(), variable_NPCs.get(), tab1_map_entry.get(),
+ tab1_runtime_entry.get(), tab1_seed_entry.get(), tab1_timescale_entry.get(),
+ distbetween=tab4_distbetweencars_entry.get(), cars_to_use=array_vehiclenames(),
+ spawn_start=tab4_spawnstart_entry.get(), spawn_end=tab4_spawnend_entry.get(),
+ rain=tab4_rain_entry.get(), fog=tab4_fog_entry.get(), wetness=tab4_wetness_entry.get(),
+ timeofday=tab4_timeofday_entry.get(), fixed=var_fixed.get())
+ addEntryContentToScrolledText(msg, tab3_output_scrolledtext)
+ update_available_replays(tab2_replays_canvas)
+
+
+# replay button function, attempts to run an instance of Randomizer replay function with the key entered by the user
+# as the passed key. if the key is invalid, an error is displayed
+def Replay():
+ tab2_error_label.pack_forget()
+ try:
+ Randomizer.replay(tab2_replaykey_entry.get())
+ except ZeroDivisionError:
+ tab2_error_label.pack()
+
+
+# clear button function, calls are_you_sure_popup helper function with the warning and a reference to the do_Clear func
+def Clear():
+ are_you_sure_popup("Doing this will delete all stored scenarios\n Scenarios can be saved in the output tab\n "
+ "Continue?", do_Clear)
+
+
+# clear (delete) the /or temp file, clear the output in the output tab, and update the available replays in tab2
+def do_Clear():
+ shutil.rmtree(tempfile.gettempdir() + '/or', ignore_errors=True)
+ clear_output()
+ update_available_replays(tab2_replays_canvas)
+
+
+# ask user for replays file location, confirm no shenanigans in the saved replays file, then copy the files from
+# folder to temp folder, which is where the other functions search for replays
+def load_from_file():
+ Clear()
+ # True if NO was pressed on popup
+ if os.path.isdir(tempfile.gettempdir() + '/or'):
+ return
+ homedir = os.environ['HOME']
+ root.directory = tk.filedialog.askdirectory(initialdir=homedir)
+ try:
+ validate, files = validate_pickles(root.directory)
+ except TypeError:
+ return
+ if not validate:
+ error_popup('Error: Can not load file, something has been deleted, renamed, or moved')
+ else:
+ if files is None:
+ return
+ else:
+ source = root.directory
+ if not os.path.isdir(tempfile.gettempdir() + '/or'):
+ try:
+ os.mkdir(tempfile.gettempdir() + '/or')
+ except OSError:
+ return
+ dest = tempfile.gettempdir() + '/or'
+ all_files = os.listdir(source)
+ for f in all_files:
+ src = source + '/' + str(f)
+ shutil.copy(src, dest)
+ addEntryContentToScrolledText("Successfully loaded from %s " % source, tab3_output_scrolledtext)
+ update_available_replays(tab2_replays_canvas)
+
+
+# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+# Run tab widgets
+tab1_vehicle_name_label = tk.Label(tab1, text="Vehicle name")
+tab1_vehicle_name_note_label = tk.Label(tab1,
+ text="Important: Enter vehicle name exactly as it appears on LGSVL "
+ "control site", wraplength=275)
+tab1_seed_label = tk.Label(tab1, text="Seed (leave blank for random)")
+tab1_NPCs_label = tk.Label(tab1, text="NPCs")
+tab1_map_label = tk.Label(tab1, text="Map")
+tab1_runs_label = tk.Label(tab1, text="Runs")
+tab1_runtime_label = tk.Label(tab1, text="Runtime")
+tab1_timescale_label = tk.Label(tab1, text="Timescale")
+tab1_nameerror_label = tk.Label(tab1, text="Error: Vehicle name does not exist")
+tab1_error_label = tk.Label(tab1, text="Error: Illegal value entered")
+tab1_serveroffline_label = tk.Label(tab1, text='Unable to connect to LGSVL')
+tab1_unabletospawn_label = tk.Label(tab1, text='Spawning timed out')
+tab1_vehicle_name_entry = tk.Entry(tab1)
+OPTIONS_NPCs = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28,
+ 29, 30]
+variable_NPCs = tk.StringVar(root)
+variable_NPCs.set(OPTIONS_NPCs[19])
+tab1_NPCs_entry = tk.OptionMenu(tab1, variable_NPCs, *OPTIONS_NPCs)
+tab1_map_entry = tk.Entry(tab1)
+tab1_map_entry.insert(0, "BorregasAve")
+vcmd = (root.register(validate_int),
+ '%d', '%i', '%P', '%s', '%S', '%v', '%V', '%W')
+vcmd2 = (root.register(validate_int_negatives),
+ '%d', '%i', '%P', '%s', '%S', '%v', '%V', '%W')
+vcmd3 = (root.register(validate_float),
+ '%d', '%i', '%P', '%s', '%S', '%v', '%V', '%W')
+vcmd4 = (root.register(validate_float_spinbox),
+ '%d', '%i', '%P', '%s', '%S', '%v', '%V', '%W')
+vcmd5 = (root.register(validate_float_timeofday),
+ '%d', '%i', '%P', '%s', '%S', '%v', '%V', '%W')
+tab1_seed_entry = tk.Entry(tab1, validate='key', validatecommand=vcmd2)
+tab1_runs_entry = tk.Entry(tab1, validate='key', validatecommand=vcmd)
+tab1_runtime_entry = tk.Entry(tab1, validate='key', validatecommand=vcmd3)
+tab1_timescale_entry = tk.Entry(tab1, validate='key', validatecommand=vcmd3)
+tab1_timescale_entry.insert(0, "1")
+tab1_run_button = tk.Button(tab1, text="RUN", command=run)
+# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+# Run tab widget placement
+tab1_vehicle_name_label.grid(row=0, column=0, padx=15, pady=15, sticky="w")
+tab1_vehicle_name_entry.grid(row=0, column=1, pady=15, sticky="w")
+
+tab1_vehicle_name_note_label.grid(row=1, columnspan=2, padx=15, pady=15)
+
+tab1_seed_label.grid(row=2, column=0, padx=15, pady=15, sticky="w")
+tab1_seed_entry.grid(row=2, column=1, pady=15, sticky="w")
+
+tab1_NPCs_label.grid(row=3, column=0, padx=15, pady=15, sticky="w")
+tab1_NPCs_entry.grid(row=3, column=1, pady=15, sticky="w")
+
+tab1_map_label.grid(row=0, column=2, padx=15, pady=15, sticky="e")
+tab1_map_entry.grid(row=0, column=3, padx=15, pady=15, sticky="w")
+
+tab1_runs_label.grid(row=1, column=2, padx=15, pady=15, sticky="e")
+tab1_runs_entry.grid(row=1, column=3, padx=15, pady=15, sticky="w")
+
+tab1_runtime_label.grid(row=2, column=2, padx=15, pady=15, sticky="e")
+tab1_runtime_entry.grid(row=2, column=3, padx=15, pady=15, sticky="w")
+
+tab1_timescale_label.grid(row=3, column=2, padx=15, pady=15, sticky="e")
+tab1_timescale_entry.grid(row=3, column=3, padx=15, pady=15, sticky="w")
+
+tab1_run_button.grid(row=4, columnspan=2, column=1)
+
+# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+# Replay tab widgets + placement
+tab2_replaykey_label = tk.Label(tab2, text="Replay key")
+tab2_replaykey_entry = tk.Entry(tab2, validate='key', validatecommand=vcmd, justify='center')
+tab2_error_label = tk.Label(tab2, text="Error: Replay key not found")
+tab2_replaykey_button = tk.Button(tab2, text="REPLAY", command=Replay)
+tab2_clearreplays_button = tk.Button(tab2, text="Clear Stored Replays", command=Clear)
+
+tab2_replaykey_label.config(anchor='center')
+tab2_error_label.config(anchor='n')
+tab2_replaykey_label.pack()
+tab2_replaykey_entry.pack()
+tab2_replaykey_button.place(relx=0.5, rely=0.75, anchor='center')
+tab2_clearreplays_button.place(relx=0.5, rely=0.95, anchor='s')
+
+tab2_replays_canvas = tk.Canvas(tab2, width=175, height=125)
+# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+# Output tab widgets + placement
+tab3_output_frame = tk.Frame(tab3)
+tab3_output_scrolledtext = ScrolledText(tab3_output_frame, font=("Helvetica", 11), width=70, height=20,
+ state='disabled')
+tab3_clear_button = tk.Button(tab3, text="CLEAR", command=clear_output)
+tab3_save_button = tk.Button(tab3, text="SAVE", padx=15, command=save_to_file)
+tab3_load_button = tk.Button(tab3, text="LOAD", padx=15, command=load_from_file)
+tab3_output_frame.pack(side='right')
+tab3_output_scrolledtext.pack()
+tab3_clear_button.place(relx=0.135, rely=0.10, anchor='e')
+tab3_save_button.place(relx=0.135, rely=0.20, anchor='e')
+tab3_load_button.place(relx=0.135, rely=0.30, anchor='e')
+# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+# Param tab widgets + placement
+tab4_distbetweencars_label = tk.Label(tab4, text='Distance between vehicles')
+tab4_distbetweencars_entry = tk.Entry(tab4, validate='key', validatecommand=vcmd3)
+tab4_spawnstart_label = tk.Label(tab4, text='Spawn start position')
+tab4_spawnstart_entry = tk.Entry(tab4, validate='key', validatecommand=vcmd3)
+tab4_spawnend_label = tk.Label(tab4, text='Spawn end position')
+tab4_spawnend_entry = tk.Entry(tab4, validate='key', validatecommand=vcmd3)
+tab4_rain_label = tk.Label(tab4, text='Rain')
+tab4_rain_entry = tk.Spinbox(tab4, format="%.2f", increment=0.01, from_=-0.01, to=1,
+ validate="key", validatecommand=vcmd4, width=8)
+tab4_fog_label = tk.Label(tab4, text='Fog')
+tab4_fog_entry = tk.Spinbox(tab4, format="%.2f", increment=0.01, from_=-0.01, to=1,
+ validate="key", validatecommand=vcmd4, width=8)
+tab4_wetness_label = tk.Label(tab4, text='Wetness')
+tab4_wetness_entry = tk.Spinbox(tab4, format="%.2f", increment=0.01, from_=-0.01, to=1,
+ validate="key", validatecommand=vcmd4, width=8)
+tab4_timeofday_label = tk.Label(tab4, text='Time of day ')
+tab4_timeofday_entry = tk.Spinbox(tab4, format="%.2f", increment=0.01, from_=-0.01, to=24,
+ validate="key", validatecommand=vcmd5, width=8)
+var_fixed = tk.IntVar(value=1)
+tab4_fixed_cb = tk.Checkbutton(tab4, text="Fixed", variable=var_fixed).grid(row=5, column=3, sticky='W', padx=50)
+
+tab4_distbetweencars_label.grid(row=1, padx=5, pady=15)
+tab4_distbetweencars_entry.grid(row=1, column=1, padx=5, pady=15)
+tab4_spawnstart_label.grid(row=2, padx=5, pady=15)
+tab4_spawnstart_entry.grid(row=2, column=1, padx=5, pady=15)
+tab4_spawnend_label.grid(row=3, padx=5, pady=15)
+tab4_spawnend_entry.grid(row=3, column=1, padx=5, pady=15)
+tab4_rain_label.grid(row=4, padx=5, pady=15)
+tab4_rain_entry.grid(row=4, column=1, padx=5, pady=15)
+tab4_fog_label.grid(row=5, padx=5, pady=15)
+tab4_fog_entry.grid(row=5, column=1, padx=5, pady=15)
+tab4_wetness_label.grid(row=6, padx=5, pady=15)
+tab4_wetness_entry.grid(row=6, column=1, padx=5, pady=15)
+tab4_timeofday_label.grid(row=5, column=2, padx=5, pady=15)
+tab4_timeofday_entry.grid(row=5, column=2, columnspan=2, padx=5, pady=15)
+
+# Car checkboxes:
+# names = ["Sedan", "SUV", "Jeep", "Hatchback", "SchoolBus", "BoxTruck"]
+var_sedan = tk.IntVar(value=1)
+var_suv = tk.IntVar(value=1)
+var_jeep = tk.IntVar(value=1)
+var_hatchback = tk.IntVar(value=1)
+var_schoolbus = tk.IntVar(value=1)
+var_boxtruck = tk.IntVar(value=1)
+tab4_cb_frame = tk.Frame(tab4)
+tab4_cb_frame.grid(rowspan=3, row=1, column=2)
+tab4_sedan_cb = tk.Checkbutton(tab4_cb_frame, text="Sedan", variable=var_sedan).grid(row=1, column=3, sticky='W',
+ padx=50)
+tab4_suv_cb = tk.Checkbutton(tab4_cb_frame, text="SUV", variable=var_suv).grid(row=2, column=3, sticky='W', padx=50)
+tab4_jeep_cb = tk.Checkbutton(tab4_cb_frame, text="Jeep", variable=var_jeep).grid(row=3, column=3, sticky='W', padx=50)
+tab4_hatchback_cb = tk.Checkbutton(tab4_cb_frame, text="Hatchback", variable=var_hatchback).grid(row=4, column=3,
+ sticky='W', padx=50)
+tab4_schoolbus_cb = tk.Checkbutton(tab4_cb_frame, text="School Bus", variable=var_schoolbus).grid(row=5, column=3,
+ sticky='W', padx=50)
+tab4_boxtruck_cb = tk.Checkbutton(tab4_cb_frame, text="Box Truck", variable=var_boxtruck).grid(row=6, column=3,
+ sticky='W', padx=50)
+# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+# set tabs, update the displayed available replays (in the temp folder), and then run the GUI
+tabControl.pack(expan=1, fill="both")
+update_available_replays(tab2_replays_canvas)
+root.mainloop()
diff --git a/RS_scripts/Randomizer.py b/RS_scripts/Randomizer.py
new file mode 100755
index 0000000..b1e8fd8
--- /dev/null
+++ b/RS_scripts/Randomizer.py
@@ -0,0 +1,565 @@
+#!/usr/bin/env python3
+import errno
+import math
+import os
+import pickle
+import random
+import re
+import tempfile
+import time
+import lgsvl
+import signal
+
+# output message, will be referenced later
+endMsg = 'default'
+
+# run function, receives vehicle name, amount of NPCs, map name, runtime, seed, and timescale parameters, then creates
+# a random scenario using these variables
+def run(VN, NPC, MAP, RT, SD, TS, distbetween=None, spawn_start=None, spawn_end=None, cars_to_use=None,
+ rain=None, fog=None, wetness=None, timeofday=None, fixed=None):
+ # Main spawning script
+ # Initial simulator setup
+ sim = lgsvl.Simulator(os.environ.get("SIMULATOR_HOST", "127.0.0.1"), 8181)
+
+ # timeout handler
+ def handler(signum, frame):
+ raise TimeoutError
+
+ # initialize variables
+ try:
+ vehicleName = VN
+ NPCCount = int(NPC)
+ map = MAP
+ runtime = float(RT)
+ timescale = float(TS)
+ except ValueError:
+ sim.stop()
+ sim.close()
+ raise ValueError
+ if SD == '':
+ seed = random.randint(-2147483649, 2147483647)
+ else:
+ seed = int(SD)
+
+ if sim.current_scene == map:
+ sim.reset()
+ else:
+ # Seed (optional) is an Integer (-2,147,483,648 - 2,147,483,647) that determines the "random" behavior of the
+ # NPC vehicles and rain effects.
+ sim.load(scene=map, seed=seed)
+
+ # Selected cars to use in the scenario
+ if cars_to_use is None or cars_to_use == '':
+ cars_to_use = ["Sedan", "SUV", "Jeep", "Hatchback", "SchoolBus", "BoxTruck"]
+
+ # Distance between vehicles (doubled for long vehicles)
+ if distbetween is None or distbetween == '':
+ distbetween = 5.0
+ else:
+ distbetween = float(distbetween)
+
+ # Minimum and maximum distance for NPCs to be spawned from EGO vehicle
+ if spawn_start is None or spawn_start == '':
+ spawn_start = 10.0
+ else:
+ spawn_start = float(spawn_start)
+
+ if spawn_end is None or spawn_end == '':
+ spawn_end = 500.0
+ else:
+ spawn_end = float(spawn_end)
+
+ if rain == '-0.01':
+ rain = round(random.uniform(0, 1),2)
+ else:
+ rain = float(rain)
+
+ if fog == '-0.01':
+ fog = round(random.uniform(0, 1),2)
+ else:
+ fog = float(fog)
+
+ if wetness == '-0.01':
+ wetness = round(random.uniform(0, 1),2)
+ else:
+ wetness = float(wetness)
+
+ if timeofday == '-0.01':
+ timeofday = round(random.uniform(0, 24),2)
+ else:
+ timeofday = float(timeofday)
+
+ if fixed == 1:
+ fixed = True
+ else:
+ fixed = False
+
+ # Handling weather and time
+ sim.weather = lgsvl.WeatherState(rain=rain, fog=fog, wetness=wetness)
+ sim.set_time_of_day(timeofday, fixed=fixed)
+
+ NPCNameList = []
+
+ # Bigger NPC vehicles (important later when spawning)
+ bigLads = ["SchoolBus", "BoxTruck"]
+ # List of transforms representing good spawn points
+ spawns = sim.get_spawn()
+
+ # Creates new agent state, makes it's transform equal to the first good spawn point.
+ state = lgsvl.AgentState()
+ state.transform = spawns[0]
+
+ try:
+ a = sim.add_agent(vehicleName, lgsvl.AgentType.EGO, state)
+ except Exception:
+ signal.alarm(0)
+ sim.stop()
+ sim.close()
+ raise ZeroDivisionError
+ sensors = a.get_sensors()
+ # Lexus2016RXHybrid (Autoware) OR CAR [, , , ] An EGO will not connect to a bridge unless commanded to
+
+ print("Bridge connected:", a.bridge_connected)
+
+ # The EGO is now looking for a bridge at the specified IP and port
+ a.connect_bridge("192.168.43.64", 9090)
+
+ print("Waiting for connection...")
+ while not a.bridge_connected:
+ time.sleep(1)
+
+ print("Bridge connected:", a.bridge_connected)
+
+ signal.signal(signal.SIGALRM, handler)
+ signal.alarm(15 + int(runtime))
+
+ sx = spawns[0].position.x
+ sy = spawns[0].position.y
+ sz = spawns[0].position.z
+ states = [state]
+ statesToReplay = []
+ AgentList = {
+ a: "Ego"
+ }
+
+ collided = False
+
+ # on collision callback, gets called when the EGO vehicle collides with another agent (NPC or pedestrian)
+ # if the EGO vehicle collides with another agent, end the simulation, output the data to output.txt
+ def on_collision(agent1, agent2, contact):
+ global collided
+ collided = True
+ name1 = AgentList[agent1]
+ name2 = AgentList[agent2] if agent2 is not None else "OBSTACLE"
+ Pkey = addPickled()
+ msg = "{} collided with {} at {} \n Seed: {}\n Replay Key: {}\n \n".format(name1, name2, contact, seed, Pkey)
+ output = "{} collided with {} at {}".format(name1, name2, contact)
+ print(output)
+ print("Killing sim...")
+ sim.stop()
+ sim.close()
+ global endMsg
+ endMsg = msg
+
+ # The above on_collision function needs to be added to the callback list of each vehicle
+ a.on_collision(on_collision)
+
+ # firstly checks if /tmp/or already exists (from previous runs of this script) if it doesn't, create it. the
+ # pickle module can take a variable and save it to a file, in order to be read by another script at a later
+ # point. the file that the pickle is saved in is unreadable by users. this function checks the next available
+ # pickle file name (pickle1, pickle2...) and then dumps a list to said file, where the first element of the list
+ # is a string of all the states of all the NPC vehicles in this run of the simulation, and the second element of
+ # the list is their respective vehicle names(types). For ease of use, each pickled file is assigned a key,
+ # going from 0001 - 9999. these keys are a sort of reference to a pickled file, and the list of keys and their
+ # respective files can be seen in /tmp/or/pickleDict.txt
+ def addPickled():
+ i = 1
+ path = tempfile.gettempdir() + "/or/pickle"
+ filename = tempfile.gettempdir() + "/or/pickleDict.txt"
+ is_first = False
+ if not os.path.exists(os.path.dirname(filename)):
+ try:
+ os.makedirs(os.path.dirname(filename))
+ is_first = True
+ except OSError as exc: # Guard against race condition
+ if exc.errno != errno.EEXIST:
+ raise
+
+ while True:
+ testPath = path + str(i)
+ if not os.path.isfile(testPath):
+ f = open(testPath, "wb")
+ break
+ else:
+ i = i + 1
+ info = "{" + vehicleName + "} {" + map + "} {" + str(seed) + "} {" + str(runtime) + "} {" + str(timescale) + "}"
+ weather = "{" + str(rain) + "} {" + str(fog) + "} {" + str(wetness) + "} {" + str(timeofday) + "} {" + str(fixed) + "}"
+ stuffToPickle = [statesToReplay, NPCNameList, info, weather]
+ pickle.dump(stuffToPickle, f)
+ f.close()
+ f = open(filename, "a")
+ index = "pickle" + str(i)
+ Pkey = getNextKey()
+ dict = "{}: {}".format(index, Pkey) if is_first else "\n{}: {}".format(index, Pkey)
+ f.write(dict)
+ f.close()
+ return Pkey
+
+ # helper function for addPickled(), this function returns the next available key to use as a
+ # referencer to a pickled file (0001, 0002, 0003...)
+ def getNextKey():
+ filename = tempfile.gettempdir() + "/or/pickleDict.txt"
+ with open(filename, 'r') as f:
+ lines = f.read().splitlines()
+ if not lines:
+ return "0001"
+ nums = []
+ for line in lines:
+ nums.append(int(line[-4:]))
+ key = int(fixNumbers(min(nums) + 1))
+ while True:
+ if key not in nums:
+ break
+ key += 1
+ return fixNumbers(key)
+
+ # python doesn't like numbers with zeros in front (0001, 0002...)
+ # this helper function returns the number with the zeros in front by turning it to a string
+ def fixNumbers(number):
+ numbers = [char for char in str(number)]
+ strFixed = str(number)
+ zeros = 4 - len(numbers)
+ for x in range(zeros):
+ strFixed = "0" + strFixed
+ return strFixed
+
+ # Function that returns true if two given agent states are at least dist away from each other on the x axis (forward
+ # axis), also specifically checks if one of them is a larger vehicle, and if so it sets the distance between them
+ # accordingly to avoid clipping
+ def checkValidPos(state1, state2, dist, biglad):
+ if biglad:
+ return abs(dot(state1.transform.position - state2.transform.position,
+ lgsvl.utils.transform_to_forward(state1))) > dist * 2.5
+ else:
+ return abs(
+ dot(state1.transform.position - state2.transform.position,
+ lgsvl.utils.transform_to_forward(state1))) > dist
+
+ # Dot product function
+ def dot(vector1, vector2):
+ return (vector1.x * vector2.x) + (vector1.y * vector2.y) + (vector1.z * vector2.z)
+
+ # Add new car after checking it's validity by looping through all the cars that have already
+ # spawned and using the checkValidPos helper function. if position is invalid, moves the car
+ # back by 5 meters and tries again (position being off the map will be handled later)
+ def addPos(state, states, biglad):
+ restart = True
+ finishCount = 0
+ count = 0
+ while restart:
+ for i in range(len(states)):
+ if NPCNameList[i] in bigLads:
+ biglad = True
+ if not (checkValidPos(state, states[i], distbetween, biglad)):
+ finishCount = 0
+ state.transform.position -= distbetween * lgsvl.utils.transform_to_forward(state)
+ break
+ else:
+ finishCount += 1
+ if finishCount == len(states):
+ restart = False
+ return state
+ count += 1
+ if count == 10:
+ break
+
+ # helper function to check if a passed state is off the map, returns true if it is on the map
+ # the function uses the map_point_on_lane function provided by lgsvl to create a point on the
+ # road using the position passed into the function, then compares the created position and the
+ # actual position to determine whether or not the position given is on a road or off the map
+ def checkOffMap(stateC):
+ if stateC is None:
+ return False
+ point = stateC.transform.position
+ newState = lgsvl.AgentState
+ newState.transform = sim.map_point_on_lane(point)
+ return abs((newState.transform.position.x - stateC.transform.position.x)) + abs(
+ (newState.transform.position.y - stateC.transform.position.y)) + abs(
+ (newState.transform.position.z - stateC.transform.position.z)) < 3.0
+
+ # Distance between two transform.positions
+ def dis2Positions(pos1, pos2):
+ return math.sqrt((pos1.x - pos2.x) ** 2 + (pos1.y - pos2.y) ** 2 + (pos1.z - pos2.z) ** 2)
+
+ # Function to create a new random position between spawn_start and spawn_end away from the EGO vehicle, and select a
+ # random vehicle type from the list of vehicle types
+ def randPos():
+ name = random.choice(cars_to_use)
+
+ # Creates a random point around the EGO
+ angle = random.uniform(0.0, -1 * math.pi)
+ dist = random.uniform(spawn_start, spawn_end)
+
+ point = lgsvl.Vector(sx + dist * math.cos(angle), sy, sz + dist * math.sin(angle))
+
+ # Creates an NPC on a lane that is closest to the random point
+ stateF = lgsvl.AgentState()
+ stateF.transform = sim.map_point_on_lane(point)
+ return stateF, name
+
+ # Main spawning function, uses the addPos function to create a new valid random position.
+ # if the new position is off the map, it nullifies the position and adds 1 to the spawning loop indexing variable
+ # if the new position is on the map, it is verified as a valid position and is spawned in the simulation
+ def spawn(state, NPCsToSpawn):
+ state, name = randPos()
+ NPCNameList.append(name)
+ if name in bigLads:
+ state = addPos(state, states, True)
+ else:
+ state = addPos(state, states, False)
+ if not checkOffMap(state):
+ state = None
+ NPCsToSpawn += 1
+ del NPCNameList[-1]
+ if state is not None:
+ states.append(state)
+ statesToReplay.append(state)
+ npc = sim.add_agent(name, lgsvl.AgentType.NPC, state)
+ AgentList[npc] = name
+ npc.follow_closest_lane(True, 11.1)
+ return NPCsToSpawn
+
+ # Function to send passed npc and corresponding state off edge of map (currently useless)
+ def driveOffEdge(npc, state):
+ waypoints = [
+ lgsvl.DriveWaypoint(state.transform.position + (lgsvl.utils.transform_to_forward(state.transform) * 3), 5,
+ lgsvl.Vector(0, 0, 0), 0, False, 0)]
+ npc.follow(waypoints, loop=False)
+
+ # true if close enough to edge to kill self, false if not (currently useless)
+ def shouldDie(state):
+ testPos = state.transform.position + (lgsvl.utils.transform_to_forward(state.transform) * 3)
+ testState = lgsvl.AgentState
+ testState.transform = state.transform
+ testState.transform.position = testPos
+ testState.transform = sim.map_point_on_lane(testPos)
+ return dis2Positions(testPos, testState.transform.position) > 3.0
+
+ # main function, has the spawning loop and then starts the simulation after all vehicles have been spawned, and also
+ # outputs to output.txt in the case of no collisions occurring
+ def start(NPCsToSpawn):
+ x = 0
+ while x < NPCsToSpawn:
+ NPCsToSpawn = spawn(state, NPCsToSpawn)
+ x += 1
+
+ sim.run(time_limit=runtime, time_scale=timescale)
+ if not collided:
+ Pkey = addPickled()
+ msg = "Simulation ended with no collisions \n Seed: {}\n Replay Key: {}\n \n".format(seed, Pkey)
+ sim.stop()
+ sim.close()
+ global endMsg
+ endMsg = msg
+
+ start(NPCCount)
+ signal.alarm(0)
+ return endMsg
+ pass
+
+
+# replay function, recieves replay key as parameter
+def replay(key):
+ sim = lgsvl.Simulator(os.environ.get("SIMULATOR_HOST", "127.0.0.1"), 8181)
+
+ # this script handles the replay feature. it is initialized in the same way as hagrala.py, however the spawning is
+ # not done randomly and is instead done using a list of states of NPC vehicles from a given run of hagrala.py
+ # this script relies on pickle and the files placed in /tmp/or by hagrala.py
+
+ # given a key, match that key with its corresponding pickle file using the pickleDict text file,
+ # and return the pickle file. if no such key exists print not found
+ def getPickled(key):
+ filename = tempfile.gettempdir() + "/or/pickleDict.txt"
+ try:
+ f = open(filename, "r")
+ except FileNotFoundError:
+ return FileNotFoundError
+ stringList = f.read()
+ f.close()
+ pickleList = stringList.splitlines()
+ for i in pickleList:
+ Ckey = i[-4:]
+ if Ckey == key:
+ return i.split(":")[0]
+ return 0 / 0
+
+ # helper function; given a string, return the bit between firstShit and secondShit
+ # for example given the string "abcd1234", if firstShit is abc and secondShit is 34, return is "d12"
+ def spliceBetween(str, firstShit, secondShit):
+ try:
+ found = re.search(firstShit + '(.+?)' + secondShit, str).group(1)
+ except AttributeError:
+ return None
+ return found
+
+ # function that splits the simulators settings into ints and strings respectively
+ def splitInfo(info):
+ info = info[1:-1].split('} {')
+ vn, map, seed, runtime, timescale = info
+ return str(vn), str(map), int(seed), float(runtime), float(timescale)
+
+ # function that splits the simulators weather settings into floats
+ def splitWeather(weather):
+ weather = weather[1:-1].split('} {')
+ rain, fog, wetness, timeofday, fixed = weather
+ return float(rain), float(fog), float(wetness), float(timeofday), bool(fixed)
+
+ # gets the string of states, the list of names, and the replay info from the pickle file using the key inputted
+ # by the user
+ PickleToUnpickle = getPickled(key)
+ if PickleToUnpickle == FileNotFoundError:
+ return 0 / 0
+ unpickled = pickle.load(open(tempfile.gettempdir() + "/or/" + PickleToUnpickle, "rb"))
+ transformString = str(unpickled[0])
+ names = unpickled[1]
+ info = unpickled[2]
+ weather = unpickled[3]
+ vehicleName, map, seed, runtime, timescale = splitInfo(info)
+ rain, fog, wetness, timeofday, fixed = splitWeather(weather)
+
+ if sim.current_scene == map:
+ sim.reset()
+ else:
+ # Seed (optional) is an Integer (-2,147,483,648 - 2,147,483,647) that determines the "random" behavior of the
+ # NPC vehicles and rain effects.
+ sim.load(scene=map, seed=seed)
+
+ # Handling weather and time
+ sim.weather = lgsvl.WeatherState(rain=rain, fog=fog, wetness=wetness)
+ sim.set_time_of_day(timeofday, fixed=fixed)
+
+ # List of transforms representing good spawn points
+ spawns = sim.get_spawn()
+ # Creates new agent state, makes it's transform equal to the first good spawn point.
+ state = lgsvl.AgentState()
+ state.transform = spawns[0]
+ a = sim.add_agent(vehicleName, lgsvl.AgentType.EGO, state)
+ sensors = a.get_sensors()
+ # Lexus2016RXHybrid (Autoware) OR CAR [, , , ] An EGO will not connect to a bridge unless commanded to
+
+ print("Bridge connected:", a.bridge_connected)
+
+ # The EGO is now looking for a bridge at the specified IP and port
+ a.connect_bridge("192.168.43.64", 9090)
+
+ print("Waiting for connection...")
+ sensors = a.get_sensors()
+ while not a.bridge_connected:
+ time.sleep(1)
+
+ print("Bridge connected: ", a.bridge_connected)
+
+ AgentList = {
+ a: "Ego"
+ }
+
+ # on_collision is still used in this script, however it is not output to a text file and instead printed in the
+ # terminal
+ def on_collision(agent1, agent2, contact):
+ global collided
+ collided = True
+ name1 = AgentList[agent1]
+ name2 = AgentList[agent2] if agent2 is not None else "OBSTACLE"
+ print("{} collided with {} at {} \n".format(name1, name2, contact))
+ sim.stop()
+ sim.close()
+
+ collided = False
+
+ # The above on_collision function needs to be added to the callback list of each vehicle
+ a.on_collision(on_collision)
+
+ # this function receives the state string and splits it into position and rotation, and creates and returns a new
+ # state with these position and rotation values. it essentially turns the big string of states into actual state
+ # objects. it is very hardcoded and i am sorry, but jesus will judge me when it is my time
+ def strToTransform(str):
+ state = lgsvl.AgentState()
+ initialSplice = spliceBetween(str, '', 'Vector')
+ str = str.replace(initialSplice, '')
+ posString = spliceBetween(str, '', ', rotation')
+ str = str.replace(posString, '')
+ splice = spliceBetween(str, '', 'Vector')
+ str = str.replace(splice, '')
+ rotString = spliceBetween(str, '', 'velocity')
+ rotString = rotString[:-5]
+ px, py, pz = strToVector(posString)
+ rx, ry, rz = strToVector(rotString)
+ position = lgsvl.Vector(px, py, pz)
+ rotation = lgsvl.Vector(rx, ry, rz)
+ state.transform.position = position
+ state.transform.rotation = rotation
+ return state
+
+ # helper function for strToTransform, given a Vector as a string, return the actual x y and z as floats
+ def strToVector(str):
+ str = str.replace('Vector', '')
+ str = str.replace('(', '')
+ str = str.replace(')', '')
+ str = '[' + str + ']'
+ list = str.strip('][').split(', ')
+ x = float(list[0])
+ y = float(list[1])
+ z = float(list[2])
+ return x, y, z
+
+ # helper function to return the first state as a string from the list of states as a string
+ def getFirstState(str):
+ try:
+ found = spliceBetween(str, '{', '}')
+ except AttributeError:
+ return None
+ if found is None:
+ return None
+ found = "{" + found + "}"
+ return found
+
+ # main looping function
+ def start(transformString):
+ i = 0
+ # spawning loop
+ while True:
+ stateStr = getFirstState(transformString)
+ if stateStr is None:
+ break
+ transformString = transformString.replace(stateStr, '')
+ state = strToTransform(stateStr)
+ name = names[i]
+ npc = sim.add_agent(name, lgsvl.AgentType.NPC, state)
+ npc.follow_closest_lane(True, 11.1)
+ i += 1
+ sim.run(time_limit=runtime, time_scale=timescale)
+ if not collided:
+ print("Simulation ended with no collisions")
+ sim.stop()
+ sim.close()
+
+ start(transformString)
+ pass
+
+
+# if this function is not called from the GUI for the purpose of debugging
+if __name__ == "__main__":
+ ans = input("run or replay\n")
+ if not ans == "run" and not ans == "replay":
+ print("invalid command")
+ if ans is "run":
+ run("OR CAR", 20, "BorregasAve", 3, '', 1)
+ elif ans == 'replay':
+ try:
+ replay('0001')
+ except ZeroDivisionError:
+ print('no scenarios exist')
diff --git a/launch.sh b/launch.sh
new file mode 100755
index 0000000..0290f42
--- /dev/null
+++ b/launch.sh
@@ -0,0 +1,49 @@
+#!/bin/bash
+if [ $# -eq 0 ]
+ then
+ SEARCH="Mouse"
+ if [ "$SEARCH" = "" ]; then
+ exit 1
+ fi
+ Yaxis=$(xrandr --current | grep '*' | uniq | awk '{print $1}' | cut -d 'x' -f2)
+ ids=$(xinput --list | awk -v search="$SEARCH" \
+ '$0 ~ search {match($0, /id=[0-9]+/);\
+ if (RSTART) \
+ print substr($0, RSTART+3, RLENGTH-3)\
+ }'\
+ )
+
+ for i in $ids
+ do
+ xinput set-prop $i 'Device Enabled' 0
+ done
+ version=$(find ~ 2>&1 -type d -name "lgsvlsimulator*" -not -path "*/Trash/*" | grep -v "Permission denied" | head -1)
+ $version/simulator >/dev/null 2>&1 & gnome-terminal -- roslaunch rosbridge_server rosbridge_websocket.launch
+ sleep 0.3
+ wid=$(xdotool search --name "LGSVL Simulator Configuration")
+ sleep 0.1 && xdotool windowfocus $wid && xdotool key --window $wid Tab Tab Tab Tab Tab KP_Enter
+ sleep 4
+ Xaxis=$(xrandr --current | grep '*' | uniq | awk '{print $1}' | cut -d 'x' -f1)
+ Yaxis=$(xrandr --current | grep '*' | uniq | awk '{print $1}' | cut -d 'x' -f2)
+ Xaxis=$[$Xaxis/2]
+ Yaxis=$[10*$Yaxis/18]
+ xdotool mousemove --sync $Xaxis $Yaxis
+ sleep 0.25
+ xdotool click 1
+ #Re-enable mouse
+ sleep 0.2
+ for i in $ids
+ do
+ xinput set-prop $i 'Device Enabled' 1
+ done
+ sleep 0.1
+ python3 ~/PythonAPI/RS_scripts/GUI.py
+fi
+
+if [[ $1 = "-r" ]];
+then
+ version=$(find ~ 2>&1 -type d -name "lgsvlsimulator*" -not -path "*/Trash/*" | grep -v "Permission denied" | head -1)
+ $version/simulator & gnome-terminal -- roslaunch rosbridge_server rosbridge_websocket.launch
+ python3 ~/PythonAPI/RS_scripts/GUI.py
+fi
+
diff --git a/lgsvl/__init__.pyc b/lgsvl/__init__.pyc
new file mode 100644
index 0000000..14a9701
Binary files /dev/null and b/lgsvl/__init__.pyc differ
diff --git a/lgsvl/geometry.pyc b/lgsvl/geometry.pyc
new file mode 100644
index 0000000..6b77774
Binary files /dev/null and b/lgsvl/geometry.pyc differ
diff --git a/lgsvl/simulator.pyc b/lgsvl/simulator.pyc
new file mode 100644
index 0000000..824d1d2
Binary files /dev/null and b/lgsvl/simulator.pyc differ
diff --git a/misc/1.png b/misc/1.png
new file mode 100644
index 0000000..8cb4ad4
Binary files /dev/null and b/misc/1.png differ
diff --git a/misc/10.png b/misc/10.png
new file mode 100644
index 0000000..eefff7d
Binary files /dev/null and b/misc/10.png differ
diff --git a/misc/11.png b/misc/11.png
new file mode 100644
index 0000000..c738cf0
Binary files /dev/null and b/misc/11.png differ
diff --git a/misc/12.png b/misc/12.png
new file mode 100644
index 0000000..ad516a5
Binary files /dev/null and b/misc/12.png differ
diff --git a/misc/13.png b/misc/13.png
new file mode 100644
index 0000000..348a147
Binary files /dev/null and b/misc/13.png differ
diff --git a/misc/14.png b/misc/14.png
new file mode 100644
index 0000000..19e5f2f
Binary files /dev/null and b/misc/14.png differ
diff --git a/misc/15.png b/misc/15.png
new file mode 100644
index 0000000..029d82a
Binary files /dev/null and b/misc/15.png differ
diff --git a/misc/2.png b/misc/2.png
new file mode 100644
index 0000000..51e828e
Binary files /dev/null and b/misc/2.png differ
diff --git a/misc/3.png b/misc/3.png
new file mode 100644
index 0000000..5b8c597
Binary files /dev/null and b/misc/3.png differ
diff --git a/misc/4.png b/misc/4.png
new file mode 100644
index 0000000..5f14041
Binary files /dev/null and b/misc/4.png differ
diff --git a/misc/5.png b/misc/5.png
new file mode 100644
index 0000000..a600a33
Binary files /dev/null and b/misc/5.png differ
diff --git a/misc/6.png b/misc/6.png
new file mode 100644
index 0000000..adc7b2c
Binary files /dev/null and b/misc/6.png differ
diff --git a/misc/7.png b/misc/7.png
new file mode 100644
index 0000000..6b16cda
Binary files /dev/null and b/misc/7.png differ
diff --git a/misc/8.png b/misc/8.png
new file mode 100644
index 0000000..363c33a
Binary files /dev/null and b/misc/8.png differ
diff --git a/misc/9.png b/misc/9.png
new file mode 100644
index 0000000..b01e5bb
Binary files /dev/null and b/misc/9.png differ
diff --git a/misc/yoinkPY.png b/misc/yoinkPY.png
new file mode 100644
index 0000000..0abb272
Binary files /dev/null and b/misc/yoinkPY.png differ