From cc32bfac28cba78464e3c256d65fdf3919d0c65a Mon Sep 17 00:00:00 2001 From: Mathewos Samson Date: Thu, 15 Feb 2024 20:08:55 -0500 Subject: [PATCH 1/8] add step to release built exe, add powershell script to setup for noobs --- .github/workflows/build-exe.yml | 13 +++++++-- .github/workflows/build-linux.yml | 12 ++++++-- sketchy_setup.ps1 | 47 +++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 6 deletions(-) create mode 100644 sketchy_setup.ps1 diff --git a/.github/workflows/build-exe.yml b/.github/workflows/build-exe.yml index f2c8513..c64e810 100644 --- a/.github/workflows/build-exe.yml +++ b/.github/workflows/build-exe.yml @@ -15,9 +15,9 @@ jobs: - name: checkout code uses: actions/checkout@v2 - # - name: Get current date and time - # id: date - # run: echo "::set-output name=date::$(date +'%Y-%m-%dT%H_%M_%S')" + - name: Get current date and time + id: date + run: echo "::set-output name=date::$(date +'%Y-%m-%dT%H_%M_%S')" # - name: Release # uses: softprops/action-gh-release@v1 # with: @@ -74,6 +74,13 @@ jobs: ./dist/* ./build/* + - name: release all files + uses: softprops/action-gh-release@v1 + with: + tag_name: ${{ github.ref_name }}-${{ steps.date.outputs.date }} + files: | + ./* + test-exe: needs: build runs-on: windows-latest diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 8712391..82cf24e 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -15,9 +15,9 @@ jobs: - name: checkout code uses: actions/checkout@v2 - # - name: Get current date and time - # id: date - # run: echo "::set-output name=date::$(date +'%Y-%m-%dT%H_%M_%S')" + - name: Get current date and time + id: date + run: echo "::set-output name=date::$(date +'%Y-%m-%dT%H_%M_%S')" # - name: Release # uses: softprops/action-gh-release@v1 # with: @@ -65,6 +65,12 @@ jobs: path: | ./build/* ./dist/* + - name: release all files + uses: softprops/action-gh-release@v1 + with: + tag_name: ${{ github.ref_name }}-${{ steps.date.outputs.date }} + files: | + ./* test-bin: needs: build diff --git a/sketchy_setup.ps1 b/sketchy_setup.ps1 new file mode 100644 index 0000000..fc3a839 --- /dev/null +++ b/sketchy_setup.ps1 @@ -0,0 +1,47 @@ +# Define variables +$pythonVersion = "3.8.0" +$repoUrl = "https://github.com/KSU-MS/KS5e-Data-Logging.git" +$repoDirectoryName = "logging" +$repoDirectory = Join-Path $env:USERPROFILE "Documents\$repoDirectoryName" +$requirementsFile = "requirements.txt" + +# Function to install Python +function InstallPython { + $pythonInstallerUrl = "https://www.python.org/ftp/python/$pythonVersion/python-$pythonVersion-amd64.exe" + $pythonInstaller = "$env:TEMP\python-installer.exe" + + # Download Python installer + Invoke-WebRequest -Uri $pythonInstallerUrl -OutFile $pythonInstaller + + # Install Python silently + Start-Process -Wait -FilePath $pythonInstaller -ArgumentList "/quiet", "InstallAllUsers=1", "PrependPath=1" + + # Clean up + Remove-Item -Path $pythonInstaller -Force +} + +# Function to clone the repository and install requirements +function CloneRepoAndInstallPackages { + # Clone repository + git clone $repoUrl $repoDirectory + + # Navigate to repository directory + cd $repoDirectory + + # Install Python packages + python -m pip install -r $requirementsFile +} + +# Check if Python is installed +if (-not (Test-Path (Join-Path $env:ProgramFiles "Python" $pythonVersion))) { + InstallPython +} + +# Check if Git is installed +if (-not (Get-Command git -ErrorAction SilentlyContinue)) { + Write-Host "Git is not installed. Please install Git and run the script again." + exit +} + +# Clone repository and install packages +CloneRepoAndInstallPackages From 9914c445e9c1158f33ed79b1699b715730388ad8 Mon Sep 17 00:00:00 2001 From: Mathewos Samson Date: Fri, 16 Feb 2024 02:48:50 -0500 Subject: [PATCH 2/8] big prints between steps, warning if 0 csvs in parse folder, splash screen, printout of parsed folder --- .github/workflows/build-exe.yml | 77 +++++++++++------- .github/workflows/build-linux.yml | 36 +++++++- parser_api.py | 34 +++++--- parser_exe.py | 44 +++++++--- parser_exe.spec | 11 +++ parser_utils/big_text_prints.py | 48 +++++++++++ .../download_latest_dbc_from_releases.py | 2 +- .../folder_selection_utils.py | 20 ++++- .../parser_logger.py | 2 +- readmepics/kennesawmotorsports.jpg | Bin 0 -> 33916 bytes 10 files changed, 213 insertions(+), 61 deletions(-) create mode 100644 parser_utils/big_text_prints.py rename download_latest_dbc_from_releases.py => parser_utils/download_latest_dbc_from_releases.py (96%) rename folder_selection_utils.py => parser_utils/folder_selection_utils.py (66%) rename parser_logger.py => parser_utils/parser_logger.py (97%) create mode 100644 readmepics/kennesawmotorsports.jpg diff --git a/.github/workflows/build-exe.yml b/.github/workflows/build-exe.yml index c64e810..5c2c804 100644 --- a/.github/workflows/build-exe.yml +++ b/.github/workflows/build-exe.yml @@ -15,31 +15,10 @@ jobs: - name: checkout code uses: actions/checkout@v2 - - name: Get current date and time - id: date - run: echo "::set-output name=date::$(date +'%Y-%m-%dT%H_%M_%S')" - # - name: Release - # uses: softprops/action-gh-release@v1 - # with: - # tag_name: ${{ github.ref_name }}-${{ steps.date.outputs.date }} - # files: ./* - - # - name: Set up Python - # uses: actions/setup-python@v2 - # with: - # python-version: '3.8' # Replace '3.x' with your Python version - - # - name: install dependencies - # run: | - # python -m pip install --upgrade pip - # pip install -r requirements.txt - - # - name: run pyinstaller - # run: | - # pyinstaller --onefile ./parser_exe.py - # - name: check what files we cooked up - # run: | - # ls -R + # - name: Get current date and time + # id: date + # run: echo "::set-output name=date::$(date +'%Y-%m-%dT%H_%M_%S')" + - name: install dependencies run: | python -m pip install --upgrade pip @@ -55,6 +34,7 @@ jobs: ls -r mkdir .\dist\test xcopy ".\test" ".\dist\test" /E + copy ".\dataPlots.m" ".\dist" - name: release-downloader uses: robinraju/release-downloader@v1.8 @@ -73,13 +53,14 @@ jobs: path: | ./dist/* ./build/* + ./*.m - - name: release all files - uses: softprops/action-gh-release@v1 - with: - tag_name: ${{ github.ref_name }}-${{ steps.date.outputs.date }} - files: | - ./* + # - name: release all files + # uses: softprops/action-gh-release@v1 + # with: + # tag_name: ${{ github.ref_name }}-${{ steps.date.outputs.date }} + # files: | + # ./* test-exe: needs: build @@ -105,3 +86,37 @@ jobs: # path: | # ./build/* # ./dist/parser_exe + release-bin: + needs: [build,test-exe] + runs-on: windows-latest + steps: + - name: download artifact + uses: actions/download-artifact@v2 + with: + name: parser-exe-windows + path: parser-exe-download + - name: compress artifact + run: | + tar -czvf parser-exe-windows.zip ./parser-exe-download + + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.run_number }} + release_name: ${{ github.ref_name }}_release_windows_${{ github.run_number }} + draft: false + prerelease: false + + - name: Upload release asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./parser-exe-windows.zip + asset_name: parser_exe_windows.zip + asset_content_type: application/zip + \ No newline at end of file diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 82cf24e..e3e2447 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -45,6 +45,7 @@ jobs: run: | ls -R cp -r ./test ./dist + cp ./dataPlots.m ./dist @@ -88,5 +89,38 @@ jobs: chmod u+x ./parser_exe ./parser_exe -h ./parser_exe --getdbc --test -v - + + release-bin: + needs: [build,test-bin] + runs-on: ubuntu-latest + steps: + - name: download artifact + uses: actions/download-artifact@v2 + with: + name: parser-exe-linux + path: parser-exe-download + - name: compress artifact + run: | + tar -czvf parser-exe-linux.tar.gz ./parser-exe-download + + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.run_number }} + release_name: ${{ github.ref_name }}_release_linux_${{ github.run_number }} + draft: false + prerelease: false + + - name: Upload release asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./parser-exe-linux.tar.gz + asset_name: parser_exe_linux.tar.gz + asset_content_type: application/zip \ No newline at end of file diff --git a/parser_api.py b/parser_api.py index f4a00c1..9a67200 100644 --- a/parser_api.py +++ b/parser_api.py @@ -30,10 +30,11 @@ import tempfile import shutil import time -import parser_logger +import parser_utils.parser_logger as parser_logger import logging DEBUG = False # Set True for optional error print statements +PARSER_EXIT_TIMEOUT = 10 def get_dbc_files(path_name='dbc-files') -> cantools.db.Database: @@ -66,6 +67,7 @@ def get_dbc_files(path_name='dbc-files') -> cantools.db.Database: if (len(mega_dbc.messages) > 0): logging.info( f"dbc successfully created with {len(mega_dbc.messages)} messages") + logging.info(f"DBC VERSION: {mega_dbc.version}") return mega_dbc else: logging.warning(f"error: dbc was empty! it has no messages :(") @@ -355,6 +357,16 @@ def parse_folder(input_path, dbc_file: cantools.db.Database): # Generate the main DBC file object for parsing dbc_file = dbc_file dbc_ids = parse_ids_in_database(dbc_file) + num_of_csvs_in_folder = 0 + for file in os.listdir(newpath): + filename = os.fsdecode(file) + if filename.endswith(".CSV") or filename.endswith(".csv"): + logging.debug(f"found csv: {filename}") + num_of_csvs_in_folder+=1 + logging.info(f"found {num_of_csvs_in_folder} CSVs in {newpath}") + if num_of_csvs_in_folder == 0: + logging.warning("there are ZERO CSV files in this folder. did you select the right one?") + # Loops through files and call parse_file on each raw CSV. for file in os.listdir(newpath): filename = os.fsdecode(file) @@ -367,7 +379,7 @@ def parse_folder(input_path, dbc_file: cantools.db.Database): logging.info( f"Successfully parsed: {filename} with {length} lines in {end_time-start_time} seconds") except (ValueError,PermissionError) as e: - logging.error(f"attemp to parse {filename} raised error {e}") + logging.error(f"attempt to parse {filename} raised error {e}") else: logging.debug("Skipped " + filename + " because it does not end in .csv") @@ -413,9 +425,7 @@ def read_files(folder): file_count += 1 except: logging.error('error: Process failed at step 1.') - logging.warning('exiting in 3 seconds...') - time.sleep(3) - sys.exit(0) + return None logging.info('Step 1: found ' + str(file_count) + ' files in the ' + path_name + ' folder') @@ -432,7 +442,11 @@ def create_dataframe(files=[]): try: df_list = [] for f in files: - df = pd.read_csv(f) + try: + df = pd.read_csv(f) + except pd.errors.EmptyDataError as e: + logging.error(f"failed to read csv ({f}) into dataframe: {e}") + continue df_list.append(df) except: logging.error('error: Process failed at step 2.') @@ -481,9 +495,7 @@ def get_time_elapsed(frames=[]): continue except: logging.error('error: Process failed at step 3.') - logging.error('exiting in 3 seconds...') - time.sleep(3) - sys.exit(0) + return None logging.info('Step 3: calculated elapsed time') return df_list @@ -542,9 +554,7 @@ def create_struct(frames=[]): except: logging.error('error: Process failed at step 4.') - logging.warning('exiting in 3 seconds...') - time.sleep(3) - sys.exit(0) + return None logging.info('Step 4: created struct') return struct diff --git a/parser_exe.py b/parser_exe.py index e7eac7f..a85bd3b 100644 --- a/parser_exe.py +++ b/parser_exe.py @@ -1,18 +1,20 @@ -from folder_selection_utils import select_folder_and_get_path,select_folder_and_get_path_dbc -from download_latest_dbc_from_releases import download_latest_release +from parser_utils.folder_selection_utils import select_folder_and_get_path,select_folder_and_get_path_dbc,open_path +from parser_utils.download_latest_dbc_from_releases import download_latest_release from parser_api import * +from parser_utils.big_text_prints import * import sys import argparse import subprocess -import parser_logger, logging +import importlib +import parser_utils.parser_logger as parser_logger, logging sys.path.insert(1, "../telemetry_parsers") ######################################################################## # Entry Point to Framework ######################################################################## - def main(args): - logging.info("Welcome to KSU motorsports parser") + logging.info("Welcome to KSU motorsports CAN parser") + logging.info(PARSER_INIT_TEXT) logging.info("The process will be of two parts: CSV to CSV parsing, and then CSV to MAT parsing.") dbc_found = False dbc_files_folder_good = True @@ -20,7 +22,7 @@ def main(args): logging.info("Downloading latest dbc") download_latest_release() - logging.info("Looking for dbc-files folder: ") + logging.info("Looking for 'dbc-files' folder: ") while not dbc_found: if not os.path.exists("dbc-files") or dbc_files_folder_good == False: logging.warning("'dbc-files' folder was not found or failed to load dbcs.") @@ -40,8 +42,8 @@ def main(args): dbc_file = get_dbc_files(dbc_files_path) elif dbc_files_path is None: logging.warning(f"selected path was {dbc_files_path}, which means you exited or cancelled the prompt") - logging.warning("exiting the program in 3 secs ! byebye") - time.sleep(3) + logging.warning(f"exiting the program in {PARSER_EXIT_TIMEOUT} secs ! byebye") + time.sleep(PARSER_EXIT_TIMEOUT) sys.exit() if dbc_found and dbc_file is not None: break @@ -59,7 +61,8 @@ def main(args): elif dbc_file is None: dbc_files_folder_good = False - logging.info("Beginning CSV to CSV parsing...") + logging.info("beginning CSV to CSV parsing") + logging.info(PARSER_STARTING_TEXT) parsing_folder_path=None if not args.test: logging.info("Select a folder which contains the raw logs to be parsed") @@ -78,19 +81,30 @@ def main(args): except (TypeError,FileNotFoundError) as e: logging.error(f"Error ({type(e)}-{e}) when trying to parse folder {parsing_folder_path} :(") logging.warning("Parsing folder step failed") + logging.info(PARSER_CSV_FINISHED_TEXT) logging.info("Beginning CSV to MAT parsing...") + logging.info(PARSER_MAT_START_TEXT) create_mat_success = create_mat() if create_mat_success: logging.info("Finished CSV to MAT parsing.") elif not create_mat_success: logging.warning("CSV to MAT parsing step failed") - + logging.info(PARSER_MAT_FINISHED_TEXT) logging.info("Parsing Complete.") - logging.info('Program exiting in 3 seconds...') - time.sleep(3) + logging.info(PARSER_FINISHED_TEXT) + if parsing_folder_path is not None: + logging.info(f"the parsed data is in {parsing_folder_path}") + logging.info(f"opening the folder with your parsed files: '{parsing_folder_path}'") + try: + open_path(parsing_folder_path) + except: + logging.error(f"opening {parsing_folder_path} failed.") + logging.info(f'Program exiting in {PARSER_EXIT_TIMEOUT} seconds...') + time.sleep(PARSER_EXIT_TIMEOUT) if __name__ == "__main__": + parser = argparse.ArgumentParser( description='KSU Motorsports parser! \nThese args configure how the parser is run') parser.add_argument('--getdbc',action="store_true" , help='include this flag if you want to download the latest dbc.') @@ -99,4 +113,10 @@ def main(args): parser.add_argument('-v','--verbose',action="store_true",help="will show debug prints (this will spam your console but show more info)") args = parser.parse_args() parser_logger.setup_logger(args.verbose) + if '_PYIBoot_SPLASH' in os.environ and importlib.util.find_spec("pyi_splash"): + import pyi_splash + pyi_splash.update_text("UPDATED TEXT") + time.sleep(5) + pyi_splash.close() + logging.debug('App loaded and splash screen closed.') main(args) diff --git a/parser_exe.spec b/parser_exe.spec index 5b22e2c..bbd12ca 100644 --- a/parser_exe.spec +++ b/parser_exe.spec @@ -14,12 +14,23 @@ a = Analysis( noarchive=False, ) pyz = PYZ(a.pure) +splash = Splash( + '.\\readmepics\\kennesawmotorsports.jpg', + binaries=a.binaries, + datas=a.datas, + text_pos=None, + text_size=12, + minify_script=True, + always_on_top=True, +) exe = EXE( pyz, a.scripts, a.binaries, a.datas, + splash, + splash.binaries, [], name='parser_exe', debug=False, diff --git a/parser_utils/big_text_prints.py b/parser_utils/big_text_prints.py new file mode 100644 index 0000000..a7f09ea --- /dev/null +++ b/parser_utils/big_text_prints.py @@ -0,0 +1,48 @@ +# generate texts here: https://patorjk.com/software/taag/#p=display&f=JS%20Stick%20Letters&t=text%20goes%20here +# make sure to use the triple quotes and "r" before the quotes so python doesnt format it wrong + +PARSER_INIT_TEXT = r""" + __ __ +|__//__`| |__|\/|/__` +| \.__/\__/ | |.__/ + __ __ __ ___ __ +|__)/\ |__)/__`|__ |__) +| /~~\| \.__/|___| \ +""" +PARSER_STARTING_TEXT = r""" + __ __ __ __ __ __ +/ `/__`\ / |__)/\ |__)/__`||\ |/ _` +\__,.__/ \/ | /~~\| \.__/|| \|\__> + _____ _____ +/__`| /\ |__)| +.__/|/~~\| \| """ +PARSER_CSV_FINISHED_TEXT = r""" + __ __ __ __ __ __ +/ `/__`\ / |__)/\ |__)/__`||\ |/ _` +\__,.__/ \/ | /~~\| \.__/|| \|\__> + ___ __ ___ __ +|__||\ ||/__`|__||__ | \ +| || \||.__/| ||___|__/ """ +PARSER_MAT_START_TEXT = r""" + __ __ _____ ___ +/ `/__`\ / |/ \ |\/| /\ | +\__,.__/ \/ |\__/ .| |/~~\| + _____ _____ +/__`| /\ |__)| +.__/|/~~\| \| +""" +PARSER_MAT_FINISHED_TEXT = r""" + __ __ _____ ___ +/ `/__`\ / |/ \ |\/| /\ | +\__,.__/ \/ |\__/ .| |/~~\| + ___ __ ___ __ +|__||\ ||/__`|__||__ | \ +| || \||.__/| ||___|__/ +""" +PARSER_FINISHED_TEXT = r""" + __ __ __ __ +|__)/\ |__)/__`||\ |/ _` +| /~~\| \.__/|| \|\__> + __ __ ___ +| \/ \|\ ||__ +|__/\__/| \||___ """ diff --git a/download_latest_dbc_from_releases.py b/parser_utils/download_latest_dbc_from_releases.py similarity index 96% rename from download_latest_dbc_from_releases.py rename to parser_utils/download_latest_dbc_from_releases.py index 991f57a..a80a9c4 100644 --- a/download_latest_dbc_from_releases.py +++ b/parser_utils/download_latest_dbc_from_releases.py @@ -1,7 +1,7 @@ import requests import os import sys -import parser_logger,logging +import parser_utils.parser_logger as parser_logger,logging repo_owner = "KSU-MS" repo_name = "ksu-ms-dbc" diff --git a/folder_selection_utils.py b/parser_utils/folder_selection_utils.py similarity index 66% rename from folder_selection_utils.py rename to parser_utils/folder_selection_utils.py index 6bba63b..7dcadcd 100644 --- a/folder_selection_utils.py +++ b/parser_utils/folder_selection_utils.py @@ -1,6 +1,9 @@ +import platform +import os import tkinter as tk from tkinter import filedialog -import parser_logger,logging +import logging +import parser_utils.parser_logger as parser_logger def select_folder_and_get_path(): root = tk.Tk() root.attributes("-topmost", True) @@ -27,8 +30,19 @@ def select_folder_and_get_path_dbc(): else: logging.warning("No folder selected") return None - - + +def open_path(path): + system = platform.system() + if system == 'Windows': + logging.debug("detected that operating system is Windows") + os.system(f'start "" "{path}"') + elif system == 'Linux': + logging.debug("detected that operating system is Linux") + elif system == 'Darwin': + os.system(f"xdg-open {path}") + else: + logging.error("Unknown operating system") + # # Call the method to select a folder and get its path # selected_folder_path = select_folder_and_get_path() diff --git a/parser_logger.py b/parser_utils/parser_logger.py similarity index 97% rename from parser_logger.py rename to parser_utils/parser_logger.py index 32d3ae7..e2a6918 100644 --- a/parser_logger.py +++ b/parser_utils/parser_logger.py @@ -34,7 +34,7 @@ def setup_logger(verbose:bool): # Add the handlers to the logger logger.addHandler(console_handler) logger.addHandler(file_handler) - logger.info("logger initialized!") + logger.debug("logger initialized!") # Set up the logger when this module is imported # setup_logger() diff --git a/readmepics/kennesawmotorsports.jpg b/readmepics/kennesawmotorsports.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2028ebda6d82957df699865c32e33dd050764a2d GIT binary patch literal 33916 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 literal 0 HcmV?d00001 From c74e1f9b196be69cff2c9729d28e6f69b69e3cde Mon Sep 17 00:00:00 2001 From: Mathewos Samson Date: Fri, 16 Feb 2024 03:49:41 -0500 Subject: [PATCH 3/8] build both binaries in one workflow --- .github/workflows/build-all.yml | 217 +++++++++++++++++++++++++ .github/workflows/build-exe.yml | 2 +- parser_utils/folder_selection_utils.py | 7 +- 3 files changed, 223 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/build-all.yml diff --git a/.github/workflows/build-all.yml b/.github/workflows/build-all.yml new file mode 100644 index 0000000..8a26a04 --- /dev/null +++ b/.github/workflows/build-all.yml @@ -0,0 +1,217 @@ +name: build and run linux executable + +on: + push: + branches: "main" + pull_request: + branches: "main" + +jobs: + build-linux: + + runs-on: ubuntu-latest + + steps: + - name: checkout code + uses: actions/checkout@v2 + + - name: Get current date and time + id: date + run: echo "::set-output name=date::$(date +'%Y-%m-%dT%H_%M_%S')" + # - name: Release + # uses: softprops/action-gh-release@v1 + # with: + # tag_name: ${{ github.ref_name }}-${{ steps.date.outputs.date }} + # files: ./* + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.8' # Replace '3.x' with your Python version + + - name: install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + - name: get dbc from latest release + uses: pozetroninc/github-action-get-latest-release@master + with: + repository: KSU-MS/ksu-ms-dbc + + - name: run pyinstaller + run: | + pyinstaller --onefile ./parser_exe.py + - name: check what files we cooked up + run: | + ls -R + cp -r ./test ./dist + cp ./dataPlots.m ./dist + + + + - name: release-downloader + uses: robinraju/release-downloader@v1.8 + with: + repository: + KSU-MS/ksu-ms-dbc + latest: true + fileName: "*.dbc" + out-file-path: | + ./dist/dbc-files + + - name: upload artifacts + uses: actions/upload-artifact@v2 + with: + name: parser-exe-linux + path: | + ./build/* + ./dist/* + - name: release all files + uses: softprops/action-gh-release@v1 + with: + tag_name: ${{ github.ref_name }}-${{ steps.date.outputs.date }} + files: | + ./* + + build-windows: + + runs-on: windows-latest + + steps: + - name: checkout code + uses: actions/checkout@v2 + + - name: install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip freeze + + - name: run pyinstaller + run: | + pyinstaller --onefile ./parser_exe.py + + - name: copy test into dist folder + run: | + ls -r + mkdir .\dist\test + xcopy ".\test" ".\dist\test" /E + copy ".\dataPlots.m" ".\dist" + + - name: release-downloader + uses: robinraju/release-downloader@v1.8 + with: + repository: + KSU-MS/ksu-ms-dbc + latest: true + fileName: "*.dbc" + out-file-path: | + ./dist/dbc-files/ + + - name: upload artifacts + uses: actions/upload-artifact@v2 + with: + name: parser-exe-windows + path: | + ./dist/* + ./build/* + ./*.m + + + test-bin: + needs: build-linux + runs-on: ubuntu-latest + steps: + - name: download artifact + uses: actions/download-artifact@v2 + with: + name: parser-exe-linux + path: parser-exe-download + - name: run executable + run: | + cd parser-exe-download + cd dist + chmod u+x ./parser_exe + ./parser_exe -h + ./parser_exe --getdbc --test -v + + test-exe: + needs: build-windows + runs-on: windows-latest + steps: + - name: download artifact + uses: actions/download-artifact@v2 + with: + name: parser-exe-windows + path: parser-exe-download + - name: run executable # watch the - and _ here lol + run: | + ls -r + cd parser-exe-download + cd dist + .\parser_exe.exe -h + .\parser_exe.exe --getdbc --test -v + create-exe-release: + needs: [test-bin,test-exe] + runs-on: ubuntu-latest + steps: + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.run_number }} + release_name: ${{ github.ref_name }}_release_${{ github.run_number }} + draft: false + prerelease: false + + release-bin: + needs: [build-linux,test-bin,create-exe-release] + runs-on: ubuntu-latest + steps: + + - name: download artifact + uses: actions/download-artifact@v2 + with: + name: parser-exe-linux + path: parser-exe-download + + - name: compress artifact + run: | + tar -czvf parser-exe-linux.tar.gz ./parser-exe-download + + - name: Upload release asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ needs.create-exe-release.outputs.upload_url }} + asset_path: ./parser-exe-linux.tar.gz + asset_name: parser_exe_linux.tar.gz + asset_content_type: application/zip + + release-exe: + needs: [build-windows,test-exe] + runs-on: windows-latest + steps: + - name: download artifact + uses: actions/download-artifact@v2 + with: + name: parser-exe-windows + path: parser-exe-download + + - name: compress artifact + run: | + tar -czvf parser-exe-windows.zip ./parser-exe-download + + - name: Upload release asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ needs.create-exe-release.outputs.upload_url }} + asset_path: ./parser-exe-windows.zip + asset_name: parser_exe_windows.zip + asset_content_type: application/zip + \ No newline at end of file diff --git a/.github/workflows/build-exe.yml b/.github/workflows/build-exe.yml index 5c2c804..90af0fb 100644 --- a/.github/workflows/build-exe.yml +++ b/.github/workflows/build-exe.yml @@ -86,7 +86,7 @@ jobs: # path: | # ./build/* # ./dist/parser_exe - release-bin: + release-exe: needs: [build,test-exe] runs-on: windows-latest steps: diff --git a/parser_utils/folder_selection_utils.py b/parser_utils/folder_selection_utils.py index 7dcadcd..b71e6d5 100644 --- a/parser_utils/folder_selection_utils.py +++ b/parser_utils/folder_selection_utils.py @@ -1,5 +1,6 @@ import platform import os +import subprocess import tkinter as tk from tkinter import filedialog import logging @@ -35,11 +36,13 @@ def open_path(path): system = platform.system() if system == 'Windows': logging.debug("detected that operating system is Windows") - os.system(f'start "" "{path}"') + subprocess.run(["start", f"{path}"]) elif system == 'Linux': logging.debug("detected that operating system is Linux") + subprocess.run(["xdg-open", f"{path}"]) elif system == 'Darwin': - os.system(f"xdg-open {path}") + logging.debug("detected that operating system is MacOS") + # os.system(f"xdg-open {path}") else: logging.error("Unknown operating system") From ec3a3ca8e82961d7e47f5b23bd95308cdb26ba94 Mon Sep 17 00:00:00 2001 From: Mathewos Samson Date: Fri, 16 Feb 2024 03:52:51 -0500 Subject: [PATCH 4/8] update job "needs" --- .github/workflows/build-all.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-all.yml b/.github/workflows/build-all.yml index 8a26a04..17d0b41 100644 --- a/.github/workflows/build-all.yml +++ b/.github/workflows/build-all.yml @@ -1,4 +1,4 @@ -name: build and run linux executable +name: build and run executables on: push: @@ -192,7 +192,7 @@ jobs: asset_content_type: application/zip release-exe: - needs: [build-windows,test-exe] + needs: [build-windows,test-exe,create-exe-release] runs-on: windows-latest steps: - name: download artifact @@ -200,7 +200,7 @@ jobs: with: name: parser-exe-windows path: parser-exe-download - + - name: compress artifact run: | tar -czvf parser-exe-windows.zip ./parser-exe-download From 5674dae9bce37e96e427d7a9e4567cfefec99813 Mon Sep 17 00:00:00 2001 From: Mathewos Samson Date: Fri, 16 Feb 2024 04:05:15 -0500 Subject: [PATCH 5/8] fix upload_url output --- .github/workflows/build-all.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build-all.yml b/.github/workflows/build-all.yml index 17d0b41..59dd171 100644 --- a/.github/workflows/build-all.yml +++ b/.github/workflows/build-all.yml @@ -152,6 +152,8 @@ jobs: .\parser_exe.exe -h .\parser_exe.exe --getdbc --test -v create-exe-release: + outputs: + upload_url: ${{ steps.create_release.outputs.upload_url }} needs: [test-bin,test-exe] runs-on: ubuntu-latest steps: From 80c01d7238346b6f3a1003ed67bb35a260c060f7 Mon Sep 17 00:00:00 2001 From: Mathewos Samson Date: Fri, 16 Feb 2024 04:14:51 -0500 Subject: [PATCH 6/8] test --- .github/workflows/build-exe.yml | 4 ++-- .github/workflows/build-linux.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-exe.yml b/.github/workflows/build-exe.yml index 90af0fb..98e32d3 100644 --- a/.github/workflows/build-exe.yml +++ b/.github/workflows/build-exe.yml @@ -3,8 +3,8 @@ name: build and run windows executable on: push: branches: "main" - pull_request: - branches: "main" + # pull_request: + # branches: "main" jobs: build: diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index e3e2447..39c9a9b 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -3,8 +3,8 @@ name: build and run linux executable on: push: branches: "main" - pull_request: - branches: "main" + # pull_request: + # branches: "main" jobs: build: From a6a9384615727f440af6ca92b9b42a3bae72d451 Mon Sep 17 00:00:00 2001 From: mathbrook <32876429+mathbrook@users.noreply.github.com> Date: Fri, 16 Feb 2024 04:33:00 -0500 Subject: [PATCH 7/8] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 6a75545..3b37c11 100644 --- a/README.md +++ b/README.md @@ -90,3 +90,4 @@ _The next steps are optional - only if you want to plot the results_ 1. Either run the file `console_exe.py` with the Python Interpreter or issue the command `py -3 console_exe.py` 2. Select your source of data input and run it - currently the only working data source is "teensy" + - From a3477eaa5892ccaf96642d9062e908c021430511 Mon Sep 17 00:00:00 2001 From: mathbrook <32876429+mathbrook@users.noreply.github.com> Date: Fri, 16 Feb 2024 12:44:13 -0500 Subject: [PATCH 8/8] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3b37c11..9237c6e 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ ## Intro -Welcome to the folder containing all the BENNESAW BATE Racing data acquisition services. The entire system is Python and MatLab based. +Welcome to the folder containing all the KENNESAW STATE FSAE data acquisition services. The entire system is Python and MatLab based. - If you have any questions or need troubleshooting, feel free to reach out to Matthew Samson on Teams, Discord: mathbrook or via email: