diff --git a/.gitignore b/.gitignore index 5f8659b8..bfe5a72d 100644 --- a/.gitignore +++ b/.gitignore @@ -147,3 +147,6 @@ cython_debug/ ev3sim/user_config.yaml # Generated logs ev3sim/logs + +# Python embeddable with pip +python_embed* diff --git a/FileAssociation.nsh b/FileAssociation.nsh index 0fcafd69..3cc260d4 100644 --- a/FileAssociation.nsh +++ b/FileAssociation.nsh @@ -144,7 +144,7 @@ NoBackup: WriteRegStr HKCR "$R0\DefaultIcon" "" "$R2,0" Skip: WriteRegStr HKCR "$R0\shell\open" "" "Open" - WriteRegStr HKCR "$R0\shell\open\command" "" '"$R2" "%1" --open' + WriteRegStr HKCR "$R0\shell\open\command" "" '"$R2" "%1"' Pop $1 Pop $0 diff --git a/MANIFEST.in b/MANIFEST.in index 4445d412..030b8313 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,4 +3,5 @@ global-include *.bot global-include *.sim global-include *.png global-include *.py -graft ev3sim/assets \ No newline at end of file +graft ev3sim/assets +prune venv diff --git a/build_exe.py b/build_exe.py index af193738..768189a6 100644 --- a/build_exe.py +++ b/build_exe.py @@ -1,34 +1,40 @@ -import argparse, sys -import PyInstaller.__main__ +import os +import shutil from subprocess import Popen from ev3sim import __version__ -parse = argparse.ArgumentParser() -parse.add_argument("--admin", action="store_true", dest="admin") - -res = parse.parse_args(sys.argv[1:]) - # First, generate the version file to be used in generation. with open("version_file_template.txt", "r") as f: string = f.read().replace("", __version__) with open("version_file.txt", "w") as f: f.write(string) -# Then generate the build. -PyInstaller.__main__.run( - [ - "-y", - "executable_entry.spec", - ] -) +if os.path.exists("dist"): + shutil.rmtree("dist") +os.makedirs("dist", exist_ok=True) +os.makedirs("dist/ev3sim", exist_ok=True) +if os.path.exists("dist/python_embed"): + shutil.rmtree("dist/python_embed") +shutil.copytree("python_embed-32", "dist/python_embed") -import os +if os.path.exists("dist/ev3sim/user_config.yaml"): + os.remove("dist/ev3sim/user_config.yaml") + +process = Popen("makensis config.nsi") +process.wait() +shutil.move("installer.exe", "installer-32bit.exe") + +if os.path.exists("dist"): + shutil.rmtree("dist") +os.makedirs("dist", exist_ok=True) +os.makedirs("dist/ev3sim", exist_ok=True) +if os.path.exists("dist/python_embed"): + shutil.rmtree("dist/python_embed") +shutil.copytree("python_embed-64", "dist/python_embed") -if os.path.exists("dist/ev3sim/ev3sim/user_config.yaml"): - os.remove("dist/ev3sim/ev3sim/user_config.yaml") +if os.path.exists("dist/ev3sim/user_config.yaml"): + os.remove("dist/ev3sim/user_config.yaml") -if res.admin: - process = Popen("makensis config.nsi") -else: - process = Popen("makensis config-no-admin.nsi") +process = Popen("makensis config.nsi") process.wait() +shutil.move("installer.exe", "installer-64bit.exe") diff --git a/config-no-admin.nsi b/config-no-admin.nsi deleted file mode 100644 index cef7b236..00000000 --- a/config-no-admin.nsi +++ /dev/null @@ -1,124 +0,0 @@ -;--------------------------------- -;Includes -!include MUI2.nsh -!include "FileAssociation.nsh" - -;--------------------------------- -;About -Name "EV3Sim" -OutFile "one_click_no_admin.exe" -Unicode true -InstallDirRegKey HKCU "Software\EV3Sim" "" - -Function .onInit -StrCpy $InstDir "$LocalAppData\$(^Name)" -SetShellVarContext Current -FunctionEnd - -;--------------------------------- -;Styling - -!define MUI_ICON "ev3sim\assets\Logo.ico" -!define MUI_UNICON "ev3sim\assets\Logo.ico" -!define MUI_HEADERIMAGE -!define MUI_HEADERIMAGE_BITMAP "ev3sim\assets\Logo.bmp" -!define MUI_HEADERIMAGE_UNBITMAP "ev3sim\assets\Logo.bmp" -!define MUI_HEADERIMAGE_RIGHT -!define MUI_WELCOMEFINISHPAGE_BITMAP "ev3sim\assets\installer_preview.bmp" - -!define MUI_PAGE_HEADER_TEXT "EV3Sim" -!define MUI_PAGE_HEADER_SUBTEXT "Robotics Simulator" - -!define MUI_DIRECTORYPAGE_VARIABLE $InstDir - -!define MUI_WELCOMEPAGE_TITLE "Welcome to EV3Sim!" -!define MUI_WELCOMEPAGE_TEXT "You'll need to select a few options before you can get started with ev3sim." - -!define MUI_LICENSEPAGE_TEXT_TOP "You must agree to the following (short) license before you can use EV3Sim." - -!define MUI_INSTFILESPAGE_FINISHHEADER_TEXT "Installation Complete!" -!define MUI_INSTFILESPAGE_ABORTHEADER_TEXT "Installation Aborted." - -!define MUI_FINISHPAGE_TITLE "All Done!" -!define MUI_FINISHPAGE_RUN "$InstDir\ev3sim.exe" -!define MUI_FINISHPAGE_SHOWREADME "https://ev3sim.mhsrobotics.club/" -!define MUI_FINISHPAGE_SHOWREADME_NOTCHECKED -!define MUI_FINISHPAGE_SHOWREADME_TEXT "Go to documentation." - -;--------------------------------- -;Pages - -!insertmacro MUI_PAGE_WELCOME -!insertmacro MUI_PAGE_LICENSE "LICENSE" -!insertmacro MUI_PAGE_DIRECTORY -!insertmacro MUI_PAGE_INSTFILES -!insertmacro MUI_PAGE_FINISH - -!insertmacro MUI_UNPAGE_CONFIRM -!insertmacro MUI_UNPAGE_INSTFILES - -;-------------------------------- -;Languages - -!insertmacro MUI_LANGUAGE "English" - -;--------------------------------- -;Sections -Section "Dummy Section" SecDummy -SetOutPath "$InstDir" -File /nonfatal /a /r "dist\ev3sim\" -WriteRegStr HKCU "Software\EV3Sim" "" $InstDir -IfFileExists "$InstDir\ev3sim\user_config.yaml" update -;Generate the default user config if not in update. -CopyFiles "$InstDir\ev3sim\presets\default_config.yaml" "$InstDir\ev3sim\user_config.yaml" -update: -;Start Menu -createDirectory "$SMPROGRAMS\MHS_Robotics" -createShortCut "$SMPROGRAMS\MHS_Robotics\EV3Sim.lnk" "$InstDir\ev3sim.exe" "" "$InstDir\ev3sim.exe" 0 -;File Associations -;URL associations for custom tasks. -WriteRegStr HKCR "ev3simc" "" "URL:ev3simc Protocol" -WriteRegStr HKCR "ev3simc" "URL Protocol" "" -WriteRegStr HKCR "ev3simc\shell" "" "" -WriteRegStr HKCR "ev3simc\DefaultIcon" "" "$InstDir\ev3sim.exe,0" -WriteRegStr HKCR "ev3simc\shell\open" "" "" -WriteRegStr HKCR "ev3simc\shell\open\command" "" '"$InstDir\ev3sim.exe" "%l" --custom-url' -;Open sims by default. -${registerExtensionOpen} "$InstDir\ev3sim.exe" ".sim" "ev3sim.sim_file" -${registerExtensionEdit} "$InstDir\ev3sim.exe" ".sim" "ev3sim.sim_file" -;Open bots by default. -${registerExtensionOpen} "$InstDir\ev3sim.exe" ".bot" "ev3sim.bot_file" -;Create uninstaller -WriteUninstaller "$InstDir\Uninstall.exe" -WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\EV3Sim" "DisplayName" "EV3Sim - Robotics Simulator" -WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\EV3Sim" "UninstallString" "$\"$InstDir\Uninstall.exe$\"" -WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\EV3Sim" "QuietUninstallString" "$\"$InstDir\Uninstall.exe$\" /S" -SectionEnd - -;--------------------------------- -;Descriptions - -;Language strings -LangString DESC_SecDummy ${LANG_ENGLISH} "A test section." - -;Assign language strings to sections -!insertmacro MUI_FUNCTION_DESCRIPTION_BEGIN -!insertmacro MUI_DESCRIPTION_TEXT ${SecDummy} $(DESC_SecDummy) -!insertmacro MUI_FUNCTION_DESCRIPTION_END - -;--------------------------------- -;Uninstaller Section - -Section "Uninstall" -Delete /REBOOTOK "$InstDir\Uninstall.exe" -RMDir /R /REBOOTOK "$InstDir" -DeleteRegKey /ifempty HKCU "Software\EV3Sim" -DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\EV3Sim" -;File Associations -${unregisterExtension} ".sim" "ev3sim.sim_file" -${unregisterExtension} ".bot" "ev3sim.bot_file" -;Remove Start Menu launcher -Delete /REBOOTOK "$SMPROGRAMS\MHS_Robotics\EV3Sim.lnk" -;Try to remove the Start Menu folder - this will only happen if it is empty -RMDir /R /REBOOTOK "$SMPROGRAMS\MHS_Robotics" -SectionEnd diff --git a/config.nsi b/config.nsi index 96bde5c6..2cd29102 100644 --- a/config.nsi +++ b/config.nsi @@ -2,40 +2,20 @@ ;Includes !include MUI2.nsh !include "FileAssociation.nsh" -RequestExecutionLevel admin ;--------------------------------- ;About Name "EV3Sim" -OutFile "one_click.exe" +OutFile "installer.exe" Unicode true InstallDirRegKey HKCU "Software\EV3Sim" "" -Var IsAdminMode -Var oldDir -!macro SetAdminMode -StrCpy $IsAdminMode 1 -SetShellVarContext All -!macroend -!macro SetUserMode -StrCpy $IsAdminMode 0 -SetShellVarContext Current -!macroend + +Var ExeLocation Function .onInit -StrCpy $oldDir "$Programfiles\$(^Name)" -; Need to do this before SetShellVarContext StrCpy $InstDir "$LocalAppData\$(^Name)" -UserInfo::GetAccountType -Pop $0 -${IfThen} $0 != "Admin" ${|} Goto setmode_currentuser ${|} - -!insertmacro SetAdminMode -Goto finalize_mode - -setmode_currentuser: -!insertmacro SetUserMode - -finalize_mode: +StrCpy $ExeLocation "$InstDir\python_embed\Scripts\ev3sim.exe" +SetShellVarContext Current FunctionEnd ;--------------------------------- @@ -63,7 +43,7 @@ FunctionEnd !define MUI_INSTFILESPAGE_ABORTHEADER_TEXT "Installation Aborted." !define MUI_FINISHPAGE_TITLE "All Done!" -!define MUI_FINISHPAGE_RUN "$InstDir\ev3sim.exe" +!define MUI_FINISHPAGE_RUN "$ExeLocation" !define MUI_FINISHPAGE_SHOWREADME "https://ev3sim.mhsrobotics.club/" !define MUI_FINISHPAGE_SHOWREADME_NOTCHECKED !define MUI_FINISHPAGE_SHOWREADME_TEXT "Go to documentation." @@ -88,42 +68,49 @@ FunctionEnd ;--------------------------------- ;Sections Section "Dummy Section" SecDummy +; Remove previous installation +IfFileExists "$InstDir\python_embed\Lib\site-packages\ev3sim\user_config.yaml" update no_update +update: +CopyFiles "$InstDir\python_embed\Lib\site-packages\ev3sim\user_config.yaml" "$InstDir\default_config.yaml" +no_update: +RMDir /r "$InstDir\python_embed" SetOutPath "$InstDir" -File /nonfatal /a /r "dist\ev3sim\" +File /nonfatal /a /r "dist\" WriteRegStr HKCU "Software\EV3Sim" "" $InstDir -IfFileExists "$InstDir\ev3sim\user_config.yaml" update -;Generate the default user config if not in update. -CopyFiles "$InstDir\ev3sim\presets\default_config.yaml" "$InstDir\ev3sim\user_config.yaml" -update: -; Check for old installation in program files. -${IfThen} $IsAdminMode == 0 ${|} Goto after_previous_install ${|} -IfFileExists "$oldDir\ev3sim\user_config.yaml" next_step after_previous_install -next_step: -; Keep old settings and remove the install directory. -CopyFiles "$oldDir\ev3sim\user_config.yaml" "$InstDir\ev3sim\user_config.yaml" -Delete /REBOOTOK "$InstDir\Uninstall.exe" -RMDir /R /REBOOTOK "$oldDir" -;Remove Start Menu launcher -Delete /REBOOTOK "$SMPROGRAMS\MHS_Robotics\EV3Sim.lnk" -;Try to remove the Start Menu folder - this will only happen if it is empty -RMDir /R /REBOOTOK "$SMPROGRAMS\MHS_Robotics" -after_previous_install: + +;Run pip install process. pythonw seems to not finish correctly, and so ev3sim doesn't get installed. +;To use test.pypi: '"$InstDir\python_embed\python.exe" -m pip install --trusted-host pypi.org --trusted-host files.pythonhosted.org -i https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple ev3sim==2.1.8.post1' +ExecDos::exec '"$InstDir\python_embed\python.exe" -m pip install --trusted-host pypi.org --trusted-host files.pythonhosted.org ev3sim' "" "$InstDir\pip.log" +Pop $0 +StrCmp "0" $0 fine + +MessageBox MB_OK "Installation failed, check '$InstDir\pip.log'" +Quit + +fine: + +;Do user_config stuff +IfFileExists "$InstDir\default_config.yaml" second_update +CopyFiles "$InstDir\python_embed\Lib\site-packages\ev3sim\presets\default_config.yaml" "$InstDir\default_config.yaml" +second_update: +CopyFiles "$InstDir\default_config.yaml" "$InstDir\python_embed\Lib\site-packages\ev3sim\user_config.yaml" + ;Start Menu createDirectory "$SMPROGRAMS\MHS_Robotics" -createShortCut "$SMPROGRAMS\MHS_Robotics\EV3Sim.lnk" "$InstDir\ev3sim.exe" "" "$InstDir\ev3sim.exe" 0 +createShortCut "$SMPROGRAMS\MHS_Robotics\EV3Sim.lnk" "$ExeLocation" "" "$ExeLocation" 0 ;File Associations ;URL associations for custom tasks. WriteRegStr HKCR "ev3simc" "" "URL:ev3simc Protocol" WriteRegStr HKCR "ev3simc" "URL Protocol" "" WriteRegStr HKCR "ev3simc\shell" "" "" -WriteRegStr HKCR "ev3simc\DefaultIcon" "" "$InstDir\ev3sim.exe,0" +WriteRegStr HKCR "ev3simc\DefaultIcon" "" "$ExeLocation,0" WriteRegStr HKCR "ev3simc\shell\open" "" "" -WriteRegStr HKCR "ev3simc\shell\open\command" "" '"$InstDir\ev3sim.exe" "%l" --custom-url' +WriteRegStr HKCR "ev3simc\shell\open\command" "" '"$ExeLocation" "%l" --custom-url' ;Open sims by default. -${registerExtensionOpen} "$InstDir\ev3sim.exe" ".sim" "ev3sim.sim_file" -${registerExtensionEdit} "$InstDir\ev3sim.exe" ".sim" "ev3sim.sim_file" +${registerExtensionOpen} "$ExeLocation" ".sim" "ev3sim.sim_file" +${registerExtensionEdit} "$ExeLocation" ".sim" "ev3sim.sim_file" ;Open bots by default. -${registerExtensionOpen} "$InstDir\ev3sim.exe" ".bot" "ev3sim.bot_file" +${registerExtensionOpen} "$ExeLocation" ".bot" "ev3sim.bot_file" ;Create uninstaller WriteUninstaller "$InstDir\Uninstall.exe" WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\EV3Sim" "DisplayName" "EV3Sim - Robotics Simulator" diff --git a/creating_embed.md b/creating_embed.md new file mode 100644 index 00000000..6e73a393 --- /dev/null +++ b/creating_embed.md @@ -0,0 +1,22 @@ +# Creating the python embed folder + +First, Ensure you have the matching python installed locally, as you'll need to copy some files over. + +1. Download get-pip.py and run `python get-pip.py`. +2. Change the contents of `python39._pth` to + +``` +python39.zip +. +.\Lib +.\Lib\site-packages + +# Uncomment to run site.main() automatically +#import site +``` + +3. Copy `tcl` to `tcl` (Local install to embed) +4. Copy `Lib/tkinter` to `Lib/tkinter` (Local install to embed) +5. Copy `DLLs/_tkinter.pyd`, `DLLs/tcl86t.dll`, `DLLs/tk86t.dll` to `_tkinter.pyd`, ... (NOT in DLLs folder in embed) + +After this, `python -m build_exe` should generate a good installer. diff --git a/ev3sim/__main__.py b/ev3sim/__main__.py new file mode 100644 index 00000000..ce6f4a27 --- /dev/null +++ b/ev3sim/__main__.py @@ -0,0 +1,3 @@ +from ev3sim.entry import main + +main() diff --git a/ev3sim/batched_run.py b/ev3sim/batched_run.py index 7322794e..6c740e1f 100644 --- a/ev3sim/batched_run.py +++ b/ev3sim/batched_run.py @@ -32,7 +32,7 @@ def simulate(batch_file, preset_filename, bot_paths, seed, override_settings, *q initialiseFromConfig(config, send_queues, recv_queues) -def batched_run(batch_file, bind_addr, seed): +def batched_run(batch_file, seed): with open(batch_file, "r") as f: config = yaml.safe_load(f) diff --git a/ev3sim/constants.py b/ev3sim/constants.py index 2d81c0c3..bfc4e488 100644 --- a/ev3sim/constants.py +++ b/ev3sim/constants.py @@ -22,3 +22,5 @@ # Command information EV3SIM_BOT_COMMAND = "EV3SIM_BOT_COMMAND" +EV3SIM_PRINT = "EV3SIM_PRINT" +EV3SIM_MESSAGE_POSTED = "EV3SIM_MESSAGE_POSTED" diff --git a/ev3sim/entry.py b/ev3sim/entry.py new file mode 100644 index 00000000..508d6bec --- /dev/null +++ b/ev3sim/entry.py @@ -0,0 +1,396 @@ +import argparse +import sys +import pygame +import yaml +from os import remove +from os.path import join, dirname, abspath, basename, isfile, sep, exists, relpath + +from ev3sim import __version__ +import ev3sim +from ev3sim.file_helper import WorkspaceError, find_abs, find_abs_directory, make_relative +from ev3sim.search_locations import bot_locations, config_locations, preset_locations +from ev3sim.simulation.loader import StateHandler +from ev3sim.updates import handle_updates +from ev3sim.utils import checkVersion +from ev3sim.visual.manager import ScreenObjectManager + +parser = argparse.ArgumentParser(description="Run the ev3sim graphical user interface.") +parser.add_argument( + "elem", + nargs="?", + type=str, + default=None, + help="""If specified, will try to open this file for use in ev3sim. Valid selections are: + - Python files, separate from bots (open / edit) + - Python files, located within bots (open / edit) + - Bot folders / config.bot files (open / sometimes edit) + - Simulation preset files (.sim) (open / sometimes edit) + - Custom task folders (open) +""", +) +parser.add_argument( + "--edit", + action="store_true", + help="Provided 'elem' is given, Edits the 'elem', rather than opening it.", + dest="edit", +) +parser.add_argument( + "--custom-url", + action="store_true", + help="Downloads and installs a custom task", + dest="custom_url", +) +parser.add_argument( + "--open_user_config", + action="store_true", + help="Debug tool to open the user_config file", + dest="open_config", +) +parser.add_argument( + "--no-debug", + action="store_false", + help="Disables the debug interface", + dest="debug", +) +parser.add_argument( + "--version", + "-v", + action="store_true", + help="Show the version of ev3sim.", + dest="version", +) + + +def raise_error(error_message): + return [ScreenObjectManager.SCREEN_UPDATE], [ + { + "panels": [ + { + "text": error_message, + "type": "accept", + "button": "Close", + "action": None, + } + ] + } + ] + + +def run_bot(bot_folder, script_name=None, edit=False): + from ev3sim.validation.bot_files import BotValidator + + if not exists(join(bot_folder, "config.bot")): + return raise_error( + f"The bot {bot_folder} has been messed with, it is missing config.bot. If it has been deleted, please put it back there." + ) + + with open(join(bot_folder, "config.bot"), "r") as f: + config = yaml.safe_load(f) + if script_name is not None: + config["script"] = script_name + config["type"] = "mindstorms" if script_name.endswith(".ev3") else "python" + with open(join(bot_folder, "config.bot"), "w") as f: + f.write(yaml.dump(config)) + + if not BotValidator.validate_file(bot_folder): + return raise_error(f"There is something wrong with the robot {bot_folder}, and so it cannot be opened or used.") + + try: + relative_dir, relative_path = make_relative(bot_folder, ["workspace"]) + if relative_path.startswith("robots"): + raise ValueError() + sim_paths = [join(dirname(bot_folder), "sim.sim")] + except: + relative_dir, relative_path = make_relative(bot_folder, bot_locations()) + sim_paths = [ + find_abs("presets/soccer.sim", ["package"]), + find_abs("presets/rescue.sim", ["package"]), + ] + + if not edit: + found = False + for sim in sim_paths: + with open(sim, "r") as f: + config = yaml.safe_load(f) + for botpath in config["bots"]: + if botpath == relative_path: + found = True + break + if found: + return run_sim(sim) + # Not in the predefined presets. + # Use the testing preset. + sim_path = find_abs("presets/testing.sim", ["package"]) + with open(sim_path, "r") as f: + test_config = yaml.safe_load(f) + + test_config["bots"] = [relative_path] + with open(sim_path, "w") as f: + f.write(yaml.dump(test_config)) + + return run_sim(sim_path) + else: + return [ScreenObjectManager.SCREEN_BOT_EDIT], [ + { + "bot_file": bot_folder, + "bot_dir_file": (relative_dir, relative_path), + } + ] + + +def run_code(script_name): + real = find_abs("examples/robots/default_testing", ["package"]) + rel_dir, rel_path = make_relative(real, bot_locations()) + with open(join(real, "config.bot"), "r") as f: + bot_config = yaml.safe_load(f) + bot_config["script"] = script_name + bot_config["type"] = "mindstorms" if script_name.endswith(".ev3") else "python" + with open(join(real, "config.bot"), "w") as f: + f.write(yaml.dump(bot_config)) + + sim_path = find_abs("presets/testing.sim", ["package"]) + with open(sim_path, "r") as f: + test_config = yaml.safe_load(f) + + test_config["bots"] = [rel_path] + with open(sim_path, "w") as f: + f.write(yaml.dump(test_config)) + + return run_sim(sim_path) + + +def run_sim(sim_path, edit=False): + from ev3sim.validation.batch_files import BatchValidator + + if not BatchValidator.validate_file(sim_path): + return raise_error(f"There is something wrong with the sim {sim_path}, and so it cannot be opened or used.") + if not edit: + return [ScreenObjectManager.SCREEN_SIM], [ + { + "batch": sim_path, + } + ] + import importlib + + with open(sim_path, "r") as f: + conf = yaml.safe_load(f) + with open(find_abs(conf["preset_file"], preset_locations())) as f: + preset = yaml.safe_load(f) + if "visual_settings" not in preset: + return raise_error("This preset cannot be edited.") + mname, cname = preset["visual_settings"].rsplit(".", 1) + klass = getattr(importlib.import_module(mname), cname) + + return [ScreenObjectManager.SCREEN_SETTINGS], [ + { + "file": sim_path, + "settings": klass, + "allows_filename_change": True, + "extension": "sim", + } + ] + + +def main(passed_args=None): + if passed_args is None: + args = parser.parse_args(sys.argv[1:]) + else: + # Just give some generic input + args = parser.parse_args(["blank.yaml"]) + args.__dict__.update(passed_args) + + # Useful for a few things. + ev3sim_folder = dirname(abspath(__file__)) + + # Step 1: Handling helper commands + + if args.version: + import ev3sim + + print(f"Running ev3sim version {ev3sim.__version__}") + return + + if args.open_config: + from ev3sim.utils import open_file, APP_EXPLORER + + fname = join(ev3sim_folder, "user_config.yaml") + open_file(fname, APP_EXPLORER) + return + + # Step 2: Safely load configs and set up error reporting + + try: + # This should always exist. + conf_file = find_abs("user_config.yaml", allowed_areas=config_locations()) + with open(conf_file, "r") as f: + conf = yaml.safe_load(f) + + handler = StateHandler() + checkVersion() + handler.setConfig(**conf) + # If no workspace has been set, place it in the package directory. + if not handler.WORKSPACE_FOLDER: + handler.setConfig( + **{ + "app": { + "workspace_folder": find_abs_directory("package/workspace", create=True), + } + } + ) + except Exception as e: + import traceback as tb + + error = "".join(tb.format_exception(None, e, e.__traceback__)) + with open(join(ev3sim_folder, "error_log.txt"), "w") as f: + f.write(error) + print(f"An error occurred before sentry could load, check {join(ev3sim_folder, 'error_log.txt')}") + sys.exit(1) + + if handler.SEND_CRASH_REPORTS: + import sentry_sdk + + sentry_sdk.init( + "https://847cb34de3b548bd9cf0ca4434ab02ed@o522431.ingest.sentry.io/5633878", + release=__version__, + ) + + # Step 3: Identify what screens we need to show and start up any other processes + + pushed_screens = [] + pushed_kwargss = [{}] + if args.elem: + # We have been given a path of some sort as an argument, figure out what it is and run with it. + # First, figure out what type it is. + if args.elem[-1] in "/\\": + args.elem = args.elem[:-1] + + if args.custom_url: + import time + import requests + import zipfile + from urllib.parse import urlparse + + zip_url = args.elem.replace("ev3simc://", "https://") + + # Save the temp file here. + c_path = dirname(__file__) + fn = basename(urlparse(zip_url).path) + fn = fn if fn.strip() else f"dload{str(int(time.time()))[:5]}" + zip_path = c_path + sep + fn + r = requests.get(zip_url, verify=True) + with open(zip_path, "wb") as f: + f.write(r.content) + + extract_path = find_abs_directory("workspace/custom/") + with zipfile.ZipFile(zip_path, "r") as zip_ref: + name = zip_ref.namelist()[0].split("\\")[0].split("/")[0] + zip_ref.extractall(extract_path) + if isfile(zip_path): + remove(zip_path) + found = True + pushed_screens = [ + ScreenObjectManager.SCREEN_MENU, + ScreenObjectManager.SCREEN_BATCH, + ScreenObjectManager.SCREEN_UPDATE, + ] + + def action(result): + if not result: + # Delete + import shutil + + shutil.rmtree(join(extract_path, name)) + + pushed_kwargss = [ + {}, + {"selected": join(extract_path, name, "sim.sim")}, + { + "panels": [ + { + "type": "boolean", + "text": ( + "Custom tasks downloaded from the internet can do anything to your computer." + " Only use custom tasks from a developer you trust." + ), + "button_yes": "Accept", + "button_no": "Delete", + "action": action, + } + ] + }, + ] + + elif not exists(args.elem): + pushed_screens, pushed_kwargss = raise_error(f"Unknown path {args.elem}.") + elif args.elem.endswith(".py") or args.elem.endswith(".ev3"): + # Python file, either in bot or completely separate. + folder = dirname(args.elem) + config_path = join(folder, "config.bot") + if exists(config_path): + # We are in a bot directory. + pushed_screens, pushed_kwargss = run_bot(folder, basename(args.elem), edit=args.edit) + else: + pushed_screens, pushed_kwargss = run_code(args.elem) + elif args.elem.endswith(".bot"): + # Bot file. + folder = dirname(args.elem) + pushed_screens, pushed_kwargss = run_bot(folder, edit=args.edit) + elif args.elem.endswith(".sim"): + pushed_screens, pushed_kwargss = run_sim(args.elem, edit=args.edit) + else: + # Some sort of folder. Either a bot folder, or custom task folder. + config_path = join(args.elem, "config.bot") + sim_path = join(args.elem, "sim.sim") + if exists(config_path): + pushed_screens, pushed_kwargss = run_bot(args.elem, edit=args.edit) + elif exists(sim_path): + pushed_screens, pushed_kwargss = run_sim(sim_path, edit=args.edit) + else: + pushed_screens, pushed_kwargss = raise_error( + f"EV3Sim does not know how to open {args.elem}{' for editing' if args.edit else ''}." + ) + + try: + StateHandler.instance.startUp(push_screens=pushed_screens, push_kwargss=pushed_kwargss) + except WorkspaceError: + pass + + updates = handle_updates() + if updates: + ScreenObjectManager.instance.pushScreen( + ScreenObjectManager.instance.SCREEN_UPDATE, + panels=updates, + ) + + if args.debug: + try: + import debugpy + + debugpy.listen(15995) + except RuntimeError as e: + print("Warning: Couldn't start the debugger") + + # Step 4: Mainloop + + try: + handler.mainLoop() + except KeyboardInterrupt: + pass + except Exception as e: + import traceback as tb + + error = "".join(tb.format_exception(None, e, e.__traceback__)) + with open(join(ev3sim_folder, "error_log.txt"), "w") as f: + f.write(error) + print(f"An error occurred, check {join(ev3sim_folder, 'error_log.txt')} for details.") + + pygame.quit() + handler.is_running = False + try: + handler.closeProcesses() + except: + pass + + +if __name__ == "__main__": + main() diff --git a/ev3sim/examples/robots/default_testing/config.bot b/ev3sim/examples/robots/default_testing/config.bot new file mode 100644 index 00000000..421d24ab --- /dev/null +++ b/ev3sim/examples/robots/default_testing/config.bot @@ -0,0 +1,111 @@ +base_plate: + children: + - collider: inherit + friction: 0.8 + key: phys_obj-child-1 + physics: true + position: + - 8.3 + - 0.0 + restitution: 0.2 + type: object + visual: + fill: '#878E88' + height: 6 + name: Rectangle + width: 1 + zPos: 2.5 + - collider: inherit + friction: 0.8 + key: phys_obj-child-2 + physics: true + position: + - 9.0 + - 3.0 + restitution: 0.2 + rotation: 60 + type: object + visual: + fill: '#878E88' + height: 1 + name: Rectangle + width: 3 + zPos: 2.75 + - collider: inherit + friction: 0.8 + key: phys_obj-child-3 + physics: true + position: + - 9.0 + - -3.0 + restitution: 0.2 + rotation: -60 + type: object + visual: + fill: '#878E88' + height: 1 + name: Rectangle + width: 3 + zPos: 2.875 + collider: inherit + friction: 0.8 + key: phys_obj + mass: 5 + restitution: 0.2 + visual: + fill: '#878E88' + name: Circle + radius: 8.5 + stroke: '#ffffff' + stroke_width: 0.1 + zPos: 1 +devices: +- LargeMotor: + port: outB + position: + - 0 + - 4 + rotation: 0 + zPos: 3 +- LargeMotor: + port: outC + position: + - 0 + - -4 + rotation: 0 + zPos: 3.5 +- ColorSensor: + port: in2 + position: + - -7.5 + - 0 + rotation: 180 + zPos: 3.75 +- UltrasonicSensor: + port: in3 + position: + - 7.5 + - 0 + rotation: 0 + zPos: 3.875 +- InfraredSensor: + port: in1 + position: + - 0 + - 7.5 + rotation: 90 + zPos: 3.9375 +- CompassSensor: + port: in4 + position: + - 0.0 + - 0.0 + rotation: 0.0 + zPos: 3.96875 +- Button: + port: up + position: + - -4.0 + - 0.0 + zPos: 3.984375 +hidden: true diff --git a/ev3sim/examples/robots/default_testing/preview.png b/ev3sim/examples/robots/default_testing/preview.png new file mode 100644 index 00000000..8a845191 Binary files /dev/null and b/ev3sim/examples/robots/default_testing/preview.png differ diff --git a/ev3sim/file_helper.py b/ev3sim/file_helper.py index 35ec956f..7d490434 100644 --- a/ev3sim/file_helper.py +++ b/ev3sim/file_helper.py @@ -121,6 +121,15 @@ def find_abs_directory(dirpath, create=True): return fpath +def make_relative(fpath, relative_dirs): + apath = os.path.normpath(fpath) + for rdir in relative_dirs: + real = os.path.normpath(find_abs_directory(rdir)) + if apath.startswith(real): + return rdir, os.path.relpath(apath, start=real).replace("\\", "/") + raise ValueError(f"Could not find {fpath} in any of {relative_dirs}") + + def ensure_workspace_filled(ws_path): # If the workspace changes, then we should create the necessary folders if not os.path.exists(ws_path): @@ -134,3 +143,19 @@ def ensure_workspace_filled(ws_path): with open(os.path.join(find_abs("default_launch.json", ["package/presets/"])), "r") as fr: with open(launch_path, "w") as fw: fw.write(fr.read()) + # add settings.json, redirecting pythonpath. + settings_path = os.path.join(ws_path, ".vscode", "settings.json") + if not os.path.exists(settings_path): + up = os.path.dirname + file_location = os.path.join(up(up(up(up(__file__)))), "python.exe").replace("/", "\\").replace("\\", "\\\\") + settings = ( + """\ +{ + "python.pythonPath": \"""" + + file_location + + """\" +}""" + ) + + with open(settings_path, "w") as fw: + fw.write(settings) diff --git a/ev3sim/gui.py b/ev3sim/gui.py deleted file mode 100644 index d0b6f56c..00000000 --- a/ev3sim/gui.py +++ /dev/null @@ -1,341 +0,0 @@ -import argparse -import pygame -import requests -import sentry_sdk -import sys -import yaml -import os -import time -from multiprocessing import Queue, Process - -import ev3sim -from ev3sim.file_helper import WorkspaceError, find_abs, find_abs_directory -from ev3sim.simulation.loader import StateHandler -from ev3sim.search_locations import batch_locations, bot_locations, config_locations, preset_locations -from ev3sim.visual.manager import ScreenObjectManager -from ev3sim.updates import handle_updates - - -def get_latest_version(q): - try: - from luddite import get_version_pypi - - v = get_version_pypi("ev3sim") - q.put(v) - except: - q.put(ev3sim.__version__) - - -def checkVersion(): - Q = Queue() - process = Process(target=get_latest_version, args=(Q,)) - process.start() - process.join(2) - if process.is_alive(): - process.terminate() - ScreenObjectManager.NEW_VERSION = False - else: - ScreenObjectManager.NEW_VERSION = Q.get() != ev3sim.__version__ - - -parser = argparse.ArgumentParser(description="Run the ev3sim graphical user interface.") -parser.add_argument( - "elem", - nargs="?", - type=str, - default=None, - help="If specified, will begin the gui focusing on this file.", -) -parser.add_argument( - "--config", - "-c", - type=str, - default=None, - help="Provide a file with some configurable values for the screen.", -) -parser.add_argument( - "--from_main", - action="store_true", - help="This should only be set programmatically, if using the CLI then ignore.", - dest="from_main", -) -parser.add_argument( - "--open", - action="store_true", - help="Opens the current bot/sim file.", - dest="open", -) -parser.add_argument( - "--edit", - action="store_true", - help="Edits the current bot/sim file.", - dest="edit", -) -parser.add_argument( - "--custom-url", - action="store_true", - help="Downloads and installs a custom task", - dest="custom_url", -) -parser.add_argument( - "--open_user_config", - action="store_true", - help="Debug tool to open the user_config file", - dest="open_config", -) -parser.add_argument( - "--debug", - action="store_true", - help="Enable the debug interface", - dest="debug", -) - - -def main(passed_args=None): - if passed_args is None: - args = parser.parse_args(sys.argv[1:]) - else: - args = parser.parse_args([]) - args.__dict__.update(passed_args) - - if args.open_config: - import platform - import subprocess - - fname = os.path.join(os.path.dirname(os.path.abspath(__file__)), "user_config.yaml") - - if platform.system() == "Windows": - subprocess.Popen(["explorer", "/select,", fname]) - elif platform.system() == "Darwin": - subprocess.Popen(["open", fname]) - else: - subprocess.Popen(["xdg-open", fname]) - return - - pushed_screens = [] - pushed_kwargss = [{}] - should_quit = False - - if not args.from_main: - # Try loading a user config. If one does not exist, then generate one. - try: - conf_file = find_abs("user_config.yaml", allowed_areas=config_locations()) - with open(conf_file, "r") as f: - conf = yaml.safe_load(f) - except: - with open(os.path.join(find_abs("default_config.yaml", ["package/presets/"])), "r") as fr: - conf = yaml.safe_load(fr) - with open(os.path.join(find_abs_directory(config_locations()[-1]), "user_config.yaml"), "w") as fw: - fw.write(yaml.dump(conf)) - - if args.config is not None: - config_path = find_abs(args.config, allowed_areas=config_locations()) - with open(config_path, "r") as f: - config = yaml.safe_load(f) - conf.update(config) - - handler = StateHandler() - checkVersion() - handler.setConfig(**conf) - - if handler.SEND_CRASH_REPORTS: - # We are entering from main. Initialise sentry - sentry_sdk.init( - "https://847cb34de3b548bd9cf0ca4434ab02ed@o522431.ingest.sentry.io/5633878", - release=ev3sim.__version__, - ) - - if args.elem: - # First, figure out what type it is. - found = False - if args.custom_url: - from urllib.parse import urlparse - import zipfile - - zip_url = args.elem.replace("ev3simc://", "https://") - - # Save the temp file here. - c_path = os.path.dirname(__file__) - fn = os.path.basename(urlparse(zip_url).path) - fn = fn if fn.strip() else f"dload{str(int(time.time()))[:5]}" - zip_path = c_path + os.path.sep + fn - r = requests.get(zip_url, verify=True) - with open(zip_path, "wb") as f: - f.write(r.content) - - extract_path = find_abs_directory("workspace/custom/") - with zipfile.ZipFile(zip_path, "r") as zip_ref: - name = zip_ref.namelist()[0].split("\\")[0].split("/")[0] - zip_ref.extractall(extract_path) - if os.path.isfile(zip_path): - os.remove(zip_path) - found = True - pushed_screens = [ - ScreenObjectManager.SCREEN_MENU, - ScreenObjectManager.SCREEN_BATCH, - ScreenObjectManager.SCREEN_UPDATE, - ] - - def action(result): - if not result: - # Delete - import shutil - - shutil.rmtree(os.path.join(extract_path, name)) - - pushed_kwargss = [ - {}, - {"selected": os.path.join(extract_path, name, "sim.sim")}, - { - "panels": [ - { - "type": "boolean", - "text": ( - "Custom tasks downloaded from the internet can do anything to your computer." - " Only use custom tasks from a developer you trust." - ), - "button_yes": "Accept", - "button_no": "Delete", - "action": action, - } - ] - }, - ] - - from ev3sim.validation.batch_files import BatchValidator - from ev3sim.validation.bot_files import BotValidator - - if not found: - try: - if BatchValidator.validate_file(args.elem): - # Valid batch file. - if args.open: - from ev3sim.sim import main - - args.batch = args.elem - - should_quit = True - main(args.__dict__) - found = True - return - elif args.edit: - import importlib - - with open(args.elem, "r") as f: - conf = yaml.safe_load(f) - with open(find_abs(conf["preset_file"], preset_locations())) as f: - preset = yaml.safe_load(f) - mname, cname = preset["visual_settings"].rsplit(".", 1) - klass = getattr(importlib.import_module(mname), cname) - - pushed_screens = [ScreenObjectManager.SCREEN_SETTINGS] - pushed_kwargss = [ - { - "file": args.elem, - "settings": klass, - "allows_filename_change": True, - "extension": "sim", - } - ] - found = True - except: - pass - if not found: - try: - fname = os.path.split(args.elem)[0] - for possible_dir in bot_locations(): - dir_path = find_abs_directory(possible_dir, create=True) - if fname.startswith(dir_path): - fname = fname[len(dir_path) :] - bot_path = os.path.join(dir_path, fname) - break - if BotValidator.validate_file(bot_path): - if args.open: - pushed_screens = [ScreenObjectManager.SCREEN_BOT_EDIT] - pushed_kwargss = [ - { - "bot_file": bot_path, - "bot_dir_file": (possible_dir, fname), - } - ] - found = True - except: - pass - - if should_quit: - # WHY DOES THIS HAPPEN? - pygame.quit() - StateHandler.instance.is_running = False - try: - StateHandler.instance.closeProcesses() - except: - pass - raise ValueError( - "Seems like something died :( Most likely the preset you are trying to load caused some issues." - ) - - try: - StateHandler.instance.startUp(push_screens=pushed_screens, push_kwargss=pushed_kwargss) - except WorkspaceError: - pass - - if args.elem and args.from_main: - args.simulation_kwargs.update( - { - "batch": args.elem, - } - ) - - # We want to start on the simulation screen. - ScreenObjectManager.instance.screen_stack = [] - ScreenObjectManager.instance.pushScreen(ScreenObjectManager.instance.SCREEN_SIM, **args.simulation_kwargs) - - updates = handle_updates() - if updates: - ScreenObjectManager.instance.pushScreen( - ScreenObjectManager.instance.SCREEN_UPDATE, - panels=updates, - ) - - actual_error = None - error = None - - if args.debug: - try: - import debugpy - - debugpy.listen(15995) - except RuntimeError as e: - print("Warning: Couldn't start the debugger") - - try: - StateHandler.instance.mainLoop() - except KeyboardInterrupt: - pass - except Exception as e: - import traceback - - print("An error occured in the Simulator :( Please see `error_log.txt` in your workspace.") - actual_error = e - error = traceback.format_exc() - if os.path.exists(os.path.join(StateHandler.WORKSPACE_FOLDER, "error_log.txt")): - os.remove(os.path.join(StateHandler.WORKSPACE_FOLDER, "error_log.txt")) - try: - with open(os.path.join(StateHandler.WORKSPACE_FOLDER, "error_log.txt"), "w") as f: - f.write(error) - except FileNotFoundError: - # If workspace is not defined, this might be useful. - with open("error_log.txt", "w") as f: - f.write(error) - pygame.quit() - StateHandler.instance.is_running = False - try: - StateHandler.instance.closeProcesses() - except: - pass - if error is not None: - raise actual_error - - -if __name__ == "__main__": - main() diff --git a/ev3sim/logging.py b/ev3sim/logging.py index 65659264..409102ff 100644 --- a/ev3sim/logging.py +++ b/ev3sim/logging.py @@ -50,12 +50,6 @@ def reportError(self, robot_id, traceback): f.write(traceback) def openLog(self, robot_id): - import platform - import subprocess + from ev3sim.utils import open_file, APP_EXPLORER - if platform.system() == "Windows": - subprocess.Popen(["explorer", "/select,", self.getFilename(robot_id)]) - elif platform.system() == "Darwin": - subprocess.Popen(["open", self.getFilename(robot_id)]) - else: - subprocess.Popen(["xdg-open", self.getFilename(robot_id)]) + open_file(self.getFilename(robot_id), APP_EXPLORER) diff --git a/ev3sim/presets/testing.sim b/ev3sim/presets/testing.sim new file mode 100644 index 00000000..b168fb2f --- /dev/null +++ b/ev3sim/presets/testing.sim @@ -0,0 +1,3 @@ +bots: +- default_testing +preset_file: soccer.yaml diff --git a/ev3sim/sim.py b/ev3sim/sim.py index 28367d8e..6990d213 100644 --- a/ev3sim/sim.py +++ b/ev3sim/sim.py @@ -1,80 +1,15 @@ -import argparse -import sys import time -from random import randint, seed +from random import randint, seed as random_seed -parser = argparse.ArgumentParser(description="Run the simulation, include some robots.") -parser.add_argument("batch", nargs="?", help="Path of the batch file to run the simulation with.") -parser.add_argument( - "--bind_addr", - default="[::1]:50051", - metavar="address:port", - help="The IP address and port to run on (you shouldn't need to change this). Default is [::1]:50051 (localhost only). Use [::]:50051 to listen on all network interfaces.", -) -parser.add_argument( - "--seed", - "-s", - type=int, - default=None, - help="Used to seed randomisation, integer from 0 to 2^32-1. Will generate randomly if left blank.", -) -parser.add_argument( - "--not_command_line", - action="store_false", - help="This should only be set programmatically, if using the CLI then ignore.", - dest="command_line", -) -parser.add_argument( - "--version", - "-v", - action="store_true", - help="Show the version of ev3sim.", - dest="version", -) - -def main(passed_args=None): - if passed_args is None: - args = parser.parse_args(sys.argv[1:]) - else: - args = parser.parse_args(["blank.yaml"]) - args.__dict__.update(passed_args) - - if args.version: - import ev3sim - - print(f"Running ev3sim version {ev3sim.__version__}") - return - - if args.command_line: - # We need to launch the gui first. - from ev3sim.gui import main - - main( - passed_args={ - "elem": args.batch, - "simulation_kwargs": { - "seed": args.seed, - "bind_addr": args.bind_addr, - "version": args.version, - }, - "from_main": True, - } - ) - return - - if args.seed is None: - seed(time.time()) +def start_batch(batch, seed=None): + if seed is None: + random_seed(time.time()) # Seed for numpy randomisation is 0 to 2^32-1, inclusive. - args.seed = randint(0, (1 << 32) - 1) + seed = randint(0, (1 << 32) - 1) - print(f"Simulating with seed {args.seed}") + print(f"Simulating with seed {seed}") from ev3sim.batched_run import batched_run - assert args.batch, "Please provide a batch file!" - batched_run(args.batch, args.bind_addr, args.seed) - - -if __name__ == "__main__": - main() + batched_run(batch, seed) diff --git a/ev3sim/simulation/loader.py b/ev3sim/simulation/loader.py index b16be5ca..80a7874b 100644 --- a/ev3sim/simulation/loader.py +++ b/ev3sim/simulation/loader.py @@ -214,6 +214,15 @@ def handleWrites(self): ) elif write_type == MESSAGE_PRINT: Logger.instance.writeMessage(data["robot_id"], data["data"], **data.get("kwargs", {})) + + class Event: + pass + + event = Event() + event.type = EV3SIM_PRINT + event.robot_id = data["robot_id"] + event.message = data["data"] + ScreenObjectManager.instance.unhandled_events.append(event) elif write_type == MESSAGE_INPUT_REQUESTED: self.requestInput(data["robot_id"], data["message"]) elif write_type == BOT_COMMAND: @@ -291,8 +300,17 @@ def postInput(self, message, preffered_output=None): # First, try to grab an existing request from the queue. for i, output in enumerate(self.input_requests): if preffered_output is None or preffered_output == output: - self.consumeMessage(message, output) del self.input_requests[i] + self.consumeMessage(message, output) + + class Event: + pass + + event = Event() + event.type = EV3SIM_MESSAGE_POSTED + event.output = output + event.message = message + ScreenObjectManager.instance.unhandled_events.append(event) break else: self.input_messages.append([message, preffered_output]) @@ -301,8 +319,8 @@ def requestInput(self, output, message): # First, try to grab an existing message from the queue. for i, (msg, out) in enumerate(self.input_messages): if out is None or out == output: - self.consumeMessage(msg, output) del self.input_messages[i] + self.consumeMessage(msg, output) break else: self.input_requests.append(output) @@ -372,12 +390,11 @@ def startUp(self, **kwargs): man = ScreenObjectManager() man.startScreen(**kwargs) - def beginSimulation(self, **kwargs): + def beginSimulation(self, batch, seed=None): self.is_simulating = True - from ev3sim.sim import main + from ev3sim.sim import start_batch - kwargs["command_line"] = False - main(passed_args=kwargs) + start_batch(batch, seed=seed) def mainLoop(self): last_vis_update = time.time() - 1.1 / ScriptLoader.instance.VISUAL_TICK_RATE diff --git a/ev3sim/utils.py b/ev3sim/utils.py index 802a6d45..e4095b46 100644 --- a/ev3sim/utils.py +++ b/ev3sim/utils.py @@ -43,3 +43,83 @@ def recursive_merge(dict1, dict2): recursive_merge(dict1[key], dict2[key]) else: dict1[key] = dict2[key] + + +def _latest_version(q, i): + from ev3sim import __version__ + from luddite import get_version_pypi + + q._internal_size = i + try: + v = get_version_pypi("ev3sim") + q.put(v) + except: + q.put(__version__) + + +def checkVersion(): + from multiprocessing import Process + from ev3sim import __version__ + from ev3sim.visual.manager import ScreenObjectManager + + Q = Queue() + process = Process(target=_latest_version, args=(Q, Q._internal_size)) + process.start() + process.join(2) + if process.is_alive(): + process.terminate() + ScreenObjectManager.NEW_VERSION = False + else: + ScreenObjectManager.NEW_VERSION = Q.get() != __version__ + + +APP_VSCODE = "VSCODE" +APP_MINDSTORMS = "MINDSTORMS" +APP_EXPLORER = "EXPLORER" + + +def open_file(filepath, pref_app, folder=""): + import os + import platform + import subprocess + + if pref_app != APP_EXPLORER: + # Try opening with vs or mindstorms + if platform.system() == "Windows": + paths = [ + os.path.join(os.environ["ALLUSERSPROFILE"], "Microsoft", "Windows", "Start Menu", "Programs"), + os.path.join(os.environ["APPDATA"], "Microsoft", "Windows", "Start Menu", "Programs"), + ] + for path in paths: + if os.path.exists(path): + for fd in os.listdir(path): + if pref_app == APP_VSCODE and "Visual Studio Code" in fd: + f = os.path.join(path, fd) + for file in os.listdir(f): + if folder: + subprocess.run( + f'start "code" "{os.path.join(f, file)}" ""{folder}" --goto "{filepath}""', + shell=True, + ) + else: + subprocess.run( + f'start "code" "{os.path.join(f, file)}" ""{filepath}""', + shell=True, + ) + return + if pref_app == APP_MINDSTORMS and "MINDSTORMS" in fd: + f = os.path.join(path, fd) + for file in os.listdir(f): + subprocess.run( + f'start "{os.path.join(f, file)}" "{filepath}"', + shell=True, + ) + return + + if platform.system() == "Windows": + subprocess.Popen(["explorer", "/select,", filepath]) + elif platform.system() == "Darwin": + subprocess.Popen(["open", filepath]) + else: + subprocess.Popen(["xdg-open", filepath]) + return diff --git a/ev3sim/validation/batch_files.py b/ev3sim/validation/batch_files.py index ad7639ef..27d23e0f 100644 --- a/ev3sim/validation/batch_files.py +++ b/ev3sim/validation/batch_files.py @@ -6,7 +6,7 @@ class BatchValidator(Validator): FILE_EXT = "sim" REQUIRED_KEYS = ["preset_file", "bots"] - AVAILABLE_KEYS = REQUIRED_KEYS + ["settings", "hidden"] + AVAILABLE_KEYS = REQUIRED_KEYS + ["settings", "hidden", "edit_allowed"] @classmethod def validate_json(cls, json_obj) -> bool: diff --git a/ev3sim/visual/manager.py b/ev3sim/visual/manager.py index e3a697e0..fc681bf3 100644 --- a/ev3sim/visual/manager.py +++ b/ev3sim/visual/manager.py @@ -20,7 +20,6 @@ class ScreenObjectManager: SCREEN_BOTS = "BOT_SELECT" SCREEN_BATCH = "BATCH_SELECT" SCREEN_SETTINGS = "SETTINGS" - SCREEN_WORKSPACE = "WORKSPACE" SCREEN_UPDATE = "UPDATE" SCREEN_BOT_EDIT = "BOT_EDIT" SCREEN_RESCUE_EDIT = "RESCUE_EDIT" @@ -83,10 +82,6 @@ def initScreens(self): from ev3sim.visual.menus.update_dialog import UpdateMenu self.screens[self.SCREEN_UPDATE] = UpdateMenu((self.SCREEN_WIDTH, self.SCREEN_HEIGHT)) - # Workspace select dialog - from ev3sim.visual.menus.workspace_menu import WorkspaceMenu - - self.screens[self.SCREEN_WORKSPACE] = WorkspaceMenu((self.SCREEN_WIDTH, self.SCREEN_HEIGHT)) # Menu screen from ev3sim.visual.menus.main import MainMenu @@ -186,10 +181,7 @@ def startScreen(self, push_screens=None, push_kwargss={}): for screen, kwargs in zip(push_screens, push_kwargss): self.pushScreen(screen, **kwargs) if not push_screens: - if not StateHandler.WORKSPACE_FOLDER: - self.pushScreen(self.SCREEN_WORKSPACE) - else: - self.pushScreen(self.SCREEN_MENU) + self.pushScreen(self.SCREEN_MENU) def registerVisual( self, obj: "visual.objects.IVisualElement", key, kill_time=None, overwrite_key=False diff --git a/ev3sim/visual/menus/batch_select.py b/ev3sim/visual/menus/batch_select.py index f8f7d5bc..0561f02d 100644 --- a/ev3sim/visual/menus/batch_select.py +++ b/ev3sim/visual/menus/batch_select.py @@ -4,10 +4,10 @@ import pygame import pygame_gui import sentry_sdk -from ev3sim.file_helper import find_abs, find_abs_directory +from ev3sim.file_helper import find_abs, find_abs_directory, make_relative from ev3sim.validation.batch_files import BatchValidator from ev3sim.visual.menus.base_menu import BaseMenu -from ev3sim.search_locations import asset_locations, batch_locations +from ev3sim.search_locations import asset_locations, batch_locations, bot_locations class BatchMenu(BaseMenu): @@ -192,6 +192,29 @@ def generateObjects(self): self._all_objs.append(self.code_button) self._all_objs.append(self.code_icon) + bots_button_pos = ( + self._size[0] * 0.9 - preview_size[0] + 10, + self._size[1] * 0.1 + preview_size[1] + 10, + ) + self.bots_button = pygame_gui.elements.UIButton( + relative_rect=pygame.Rect(*bots_button_pos, *code_size), + text="", + manager=self, + object_id=pygame_gui.core.ObjectID("bot-bots", "settings_buttons"), + ) + self.addButtonEvent("bot-bots", self.clickBots) + if not self.bots_enable: + self.bots_button.disable() + bots_icon_path = find_abs("ui/bot.png", allowed_areas=asset_locations()) + self.bots_icon = pygame_gui.elements.UIImage( + relative_rect=pygame.Rect(*self.iconPos(bots_button_pos, code_size, code_icon_size), *code_icon_size), + image_surface=pygame.image.load(bots_icon_path), + manager=self, + object_id=pygame_gui.core.ObjectID("bots-icon"), + ) + self._all_objs.append(self.bots_button) + self._all_objs.append(self.bots_icon) + start_size = self._size[0] / 4, min(self._size[1] / 4, 120) start_icon_size = start_size[1] * 0.6, start_size[1] * 0.6 start_button_pos = (self._size[0] * 0.9 - start_size[0], self._size[1] * 0.9 - start_size[1]) @@ -251,6 +274,26 @@ def clickStart(self): ScreenObjectManager.SCREEN_SIM, batch=self.available_batches[self.batch_index][1] ) + def clickBots(self): + # Shouldn't happen but lets be safe. + if self.batch_index == -1: + return + from ev3sim.visual.manager import ScreenObjectManager + + sim_path = self.available_batches[self.batch_index][1] + + with open(sim_path, "r") as f: + sim_config = yaml.safe_load(f) + + complete_path = find_abs(sim_config["bots"][0], bot_locations()) + relative_dir, relative_path = make_relative(complete_path, bot_locations()) + + return ScreenObjectManager.instance.pushScreen( + ScreenObjectManager.SCREEN_BOT_EDIT, + bot_file=complete_path, + bot_dir_file=(relative_dir, relative_path), + ) + def clickRemove(self): # Shouldn't happen but lets be safe. if self.batch_index == -1: @@ -261,6 +304,8 @@ def clickRemove(self): self.setBatchIndex(-1) def clickCode(self): + from ev3sim.utils import open_file, APP_VSCODE, APP_MINDSTORMS + # Shouldn't happen but lets be safe. if self.batch_index == -1: return @@ -268,82 +313,16 @@ def clickCode(self): conf = yaml.safe_load(f) if conf.get("type", "python") == "mindstorms": script_location = conf.get("script", "program.ev3") - import platform - import subprocess - - if platform.system() == "Windows": - # If Mindstorms is in the start menu, we can likely find it. - found = False - for path in [ - os.path.join(os.environ["ALLUSERSPROFILE"], "Microsoft", "Windows", "Start Menu", "Programs"), - os.path.join(os.environ["APPDATA"], "Microsoft", "Windows", "Start Menu", "Programs"), - ]: - if os.path.exists(path): - for folder in os.listdir(path): - # There are multiple versions of mindstorms. - if "MINDSTORMS" in folder: - f = os.path.join(path, folder) - for file in os.listdir(f): - found = True - subprocess.run( - f'start "{os.path.join(f, file)}" "{os.path.join(self.available_batches[self.batch_index][2], "bot", script_location)}"', - shell=True, - ) - if not found: - subprocess.Popen( - [ - "explorer", - "/select,", - os.path.join(self.available_batches[self.batch_index][2], "bot", script_location), - ] - ) - elif platform.system() == "Darwin": - subprocess.Popen( - ["open", os.path.join(self.available_batches[self.batch_index][2], "bot", script_location)] - ) - else: - subprocess.Popen( - ["xdg-open", os.path.join(self.available_batches[self.batch_index][2], "bot", script_location)] - ) + + open_file(os.path.join(self.available_batches[self.batch_index][2], "bot", script_location), APP_MINDSTORMS) else: script_location = conf.get("script", "code.py") - import platform - import subprocess - - if platform.system() == "Windows": - # If Mindstorms is in the start menu, we can likely find it. - found = False - for path in [ - os.path.join(os.environ["ALLUSERSPROFILE"], "Microsoft", "Windows", "Start Menu", "Programs"), - os.path.join(os.environ["APPDATA"], "Microsoft", "Windows", "Start Menu", "Programs"), - ]: - if os.path.exists(path): - for folder in os.listdir(path): - # There are multiple versions of vscode. - if "Visual Studio Code" in folder: - f = os.path.join(path, folder) - for file in os.listdir(f): - found = True - subprocess.run( - f'start "code" "{os.path.join(f, file)}" ""{os.path.join(find_abs_directory("workspace"))}" --goto "{os.path.join(self.available_batches[self.batch_index][2], "bot", script_location)}""', - shell=True, - ) - if not found: - subprocess.Popen( - [ - "explorer", - "/select,", - os.path.join(self.available_batches[self.batch_index][2], "bot", script_location), - ] - ) - elif platform.system() == "Darwin": - subprocess.Popen( - ["open", os.path.join(self.available_batches[self.batch_index][2], "bot", script_location)] - ) - else: - subprocess.Popen( - ["xdg-open", os.path.join(self.available_batches[self.batch_index][2], "bot", script_location)] - ) + + open_file( + os.path.join(self.available_batches[self.batch_index][2], "bot", script_location), + APP_VSCODE, + folder=os.path.join(find_abs_directory("workspace")), + ) def handleEvent(self, event): super().handleEvent(event) @@ -362,6 +341,14 @@ def setBatchIndex(self, new_index): self.start_enable = new_index != -1 self.remove_enable = new_index != -1 self.code_enable = new_index != -1 + if new_index != -1: + sim_path = self.available_batches[self.batch_index][1] + + with open(sim_path, "r") as f: + sim_config = yaml.safe_load(f) + self.bots_enable = sim_config.get("edit_allowed", True) + else: + self.bots_enable = False self.regenerateObjects() def incrementBatchIndex(self, amount): diff --git a/ev3sim/visual/menus/bot_menu.py b/ev3sim/visual/menus/bot_menu.py index 44c708e5..a7d1a622 100644 --- a/ev3sim/visual/menus/bot_menu.py +++ b/ev3sim/visual/menus/bot_menu.py @@ -389,73 +389,26 @@ def clickEdit(self): ScreenObjectManager.instance.screens[ScreenObjectManager.SCREEN_BOT_EDIT].clearEvents() def clickCode(self): + from ev3sim.utils import open_file, APP_VSCODE, APP_MINDSTORMS + # Shouldn't happen but lets be safe. if self.bot_index == -1: return with open(os.path.join(self.available_bots[self.bot_index][1], "config.bot")) as f: conf = yaml.safe_load(f) + if conf.get("type", "python") == "mindstorms": script_location = conf.get("script", "program.ev3") - import platform - import subprocess - - if platform.system() == "Windows": - # If Mindstorms is in the start menu, we can likely find it. - found = False - for path in [ - os.path.join(os.environ["ALLUSERSPROFILE"], "Microsoft", "Windows", "Start Menu", "Programs"), - os.path.join(os.environ["APPDATA"], "Microsoft", "Windows", "Start Menu", "Programs"), - ]: - if os.path.exists(path): - for folder in os.listdir(path): - # There are multiple versions of mindstorms. - if "MINDSTORMS" in folder: - f = os.path.join(path, folder) - for file in os.listdir(f): - found = True - subprocess.run( - f'start "{os.path.join(f, file)}" "{os.path.join(self.available_bots[self.bot_index][1], script_location)}"', - shell=True, - ) - if not found: - subprocess.Popen( - ["explorer", "/select,", os.path.join(self.available_bots[self.bot_index][1], script_location)] - ) - elif platform.system() == "Darwin": - subprocess.Popen(["open", os.path.join(self.available_bots[self.bot_index][1], script_location)]) - else: - subprocess.Popen(["xdg-open", os.path.join(self.available_bots[self.bot_index][1], script_location)]) + + open_file(os.path.join(self.available_bots[self.bot_index][1], script_location), APP_MINDSTORMS) else: script_location = conf.get("script", "code.py") - import platform - import subprocess - - if platform.system() == "Windows": - # If Mindstorms is in the start menu, we can likely find it. - found = False - for path in [ - os.path.join(os.environ["ALLUSERSPROFILE"], "Microsoft", "Windows", "Start Menu", "Programs"), - os.path.join(os.environ["APPDATA"], "Microsoft", "Windows", "Start Menu", "Programs"), - ]: - if os.path.exists(path): - for folder in os.listdir(path): - # There are multiple versions of vscode. - if "Visual Studio Code" in folder: - f = os.path.join(path, folder) - for file in os.listdir(f): - found = True - subprocess.run( - f'start "code" "{os.path.join(f, file)}" ""{os.path.join(find_abs_directory("workspace"))}" --goto "{os.path.join(self.available_bots[self.bot_index][1], script_location)}""', - shell=True, - ) - if not found: - subprocess.Popen( - ["explorer", "/select,", os.path.join(self.available_bots[self.bot_index][1], script_location)] - ) - elif platform.system() == "Darwin": - subprocess.Popen(["open", os.path.join(self.available_bots[self.bot_index][1], script_location)]) - else: - subprocess.Popen(["xdg-open", os.path.join(self.available_bots[self.bot_index][1], script_location)]) + + open_file( + os.path.join(self.available_bots[self.bot_index][1], script_location), + APP_VSCODE, + folder=os.path.join(find_abs_directory("workspace")), + ) def clickSelect(self): # Shouldn't happen but lets be safe. diff --git a/ev3sim/visual/menus/workspace_menu.py b/ev3sim/visual/menus/workspace_menu.py deleted file mode 100644 index d30937aa..00000000 --- a/ev3sim/visual/menus/workspace_menu.py +++ /dev/null @@ -1,96 +0,0 @@ -import pygame -import pygame_gui -from ev3sim.file_helper import find_abs -from ev3sim.visual.menus.base_menu import BaseMenu -from ev3sim.search_locations import config_locations - - -class WorkspaceMenu(BaseMenu): - def generateObjects(self): - # In order to respect theme changes, objects must be built in initWithKwargs - self.bg = pygame_gui.elements.UIPanel( - relative_rect=pygame.Rect(0, 0, *self._size), - starting_layer_height=-1, - manager=self, - object_id=pygame_gui.core.ObjectID("background"), - ) - self._all_objs.append(self.bg) - - text_size = (self._size[0] / 2, self._size[1] / 2) - self.text_panel = pygame_gui.elements.UIPanel( - relative_rect=pygame.Rect(text_size[0] / 2, text_size[1] / 2, *text_size), - starting_layer_height=-0.5, - manager=self, - object_id=pygame_gui.core.ObjectID("text_background"), - ) - self._all_objs.append(self.text_panel) - - button_ratio = 4 - button_size = (self._size[0] / 4, self._size[1] / 3) - button_size = ( - min(button_size[0], button_size[1] * button_ratio), - min(button_size[1], button_size[0] / button_ratio), - ) - self.text = pygame_gui.elements.UITextBox( - html_text="""\ -In order to use ev3sim, you need to specify a workspace folder.

\ -Bots and presets you create will be stored in this folder.\ -""", - relative_rect=pygame.Rect( - text_size[0] / 2 + 30, text_size[1] / 2 + 30, text_size[0] - 60, text_size[1] - button_size[1] - 90 - ), - manager=self, - object_id=pygame_gui.core.ObjectID("text_dialog_workspace", "text_dialog"), - ) - self._all_objs.append(self.text) - - self.select = pygame_gui.elements.UIButton( - relative_rect=pygame.Rect( - text_size[0] - button_size[0] / 2, 3 * text_size[1] / 2 - button_size[1] - 30, *button_size - ), - text="Select", - manager=self, - object_id=pygame_gui.core.ObjectID("select_button", "menu_button"), - ) - self.addButtonEvent("select_button", self.clickSelect) - self._all_objs.append(self.select) - super().generateObjects() - - def clickSelect(self): - # Open file dialog. - import yaml - from ev3sim.simulation.loader import StateHandler - from ev3sim.visual.manager import ScreenObjectManager - - def onComplete(directory): - if not directory: - return - conf_file = find_abs("user_config.yaml", allowed_areas=config_locations()) - with open(conf_file, "r") as f: - conf = yaml.safe_load(f) - conf["app"]["workspace_folder"] = directory - with open(conf_file, "w") as f: - f.write(yaml.dump(conf)) - StateHandler.WORKSPACE_FOLDER = directory - ScreenObjectManager.instance.popScreen() - ScreenObjectManager.instance.pushScreen(ScreenObjectManager.SCREEN_MENU) - - import platform - - if platform.system() == "Windows": - from tkinter import Tk - from tkinter.filedialog import askdirectory - - Tk().withdraw() - directory = askdirectory() - onComplete(directory) - else: - self.addFileDialog( - "Select Workspace", - None, - True, - onComplete, - ) - - def onPop(self): - pass diff --git a/ev3sim/visual/settings/main_settings.py b/ev3sim/visual/settings/main_settings.py index c5d4acd1..cc113123 100644 --- a/ev3sim/visual/settings/main_settings.py +++ b/ev3sim/visual/settings/main_settings.py @@ -16,10 +16,4 @@ Checkbox(["app", "console_log"], True, "Console", (lambda s: (0, 170) if s[0] < 540 else (s[0] / 2, 70))), ], }, - { - "height": (lambda s: 90), - "objects": [ - FileEntry(["app", "workspace_folder"], "", True, None, "Workspace", (lambda s: (0, 20))), - ], - }, ] diff --git a/pyproject.toml b/pyproject.toml index 4e42573b..e2627264 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,4 @@ [tool.black] line-length = 120 target-version = ['py38'] +exclude = '(python_embed*)|(venv)' \ No newline at end of file diff --git a/setup.py b/setup.py index 33298caa..4f78a22f 100644 --- a/setup.py +++ b/setup.py @@ -38,8 +38,8 @@ include_package_data=True, install_requires=REQUIREMENTS, entry_points={ - "console_scripts": [ - "ev3sim=ev3sim.gui:main", + "gui_scripts": [ + "ev3sim=ev3sim.entry:main", ] }, )