diff --git a/doc/development.md b/doc/development.md index b789ff7..fe8c844 100644 --- a/doc/development.md +++ b/doc/development.md @@ -60,3 +60,15 @@ sphinx-build -M html doc build --keep-going ```bash pyinstaller --noconfirm .\pyx2cscope_win.spec ``` + +## Creating artifacts to upload to github release page +This script will execute the pyinstaller command listed above, +include the script file to start the web interface, zip +the contents of the dist folder and add the whell file +available on pypi in the dist folder. + +```bash +python -m scripts/build.py +``` + +## Creating artifacts to upload to GitHu diff --git a/pyproject.toml b/pyproject.toml index ba482a3..76bfeef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "pyx2cscope" -version = "0.5.0" +version = "0.5.1" description = "python implementation of X2Cscope" authors = [ "Yash Agarwal", @@ -32,11 +32,10 @@ pyyaml ="^6.0.1" numpy = "^1.26.0" matplotlib = "^3.7.2" PyQt5 = "^5.15.9" -pyqtgraph= "^0.13.7" +pyqtgraph = "^0.13.7" mchplnet = "0.3.0" flask = "^3.0.3" - [tool.ruff] line-length = 120 # only allow the specified pydocstyle convention diff --git a/pyx2cscope/__init__.py b/pyx2cscope/__init__.py index 20ab5fc..bcf7753 100644 --- a/pyx2cscope/__init__.py +++ b/pyx2cscope/__init__.py @@ -1,19 +1,11 @@ """This module contains the pyx2cscope package. -Version: 0.5.0 +Version: 0.5.1 """ -# Apply eventlet monkey patch before any other imports if web interface is requested -import sys - -if "-w" in sys.argv or "--web" in sys.argv: - import eventlet - eventlet.monkey_patch() - import logging -__version__ = "0.5.0" - +__version__ = "0.5.1" def set_logger( level: int = logging.ERROR, diff --git a/pyx2cscope/examples/array_example.py b/pyx2cscope/examples/array_example.py new file mode 100644 index 0000000..4e88045 --- /dev/null +++ b/pyx2cscope/examples/array_example.py @@ -0,0 +1,63 @@ +"""PyX2CScope array example reference. + +This example shows different use cases for single and multidimensional arrays. + +We define 3 different arrays: + +static uint8_t my_array1D[3] = {3, 2, 1}; +static uint8_t my_array2D[2][3] = { {6, 5, 4}, {3, 2, 1} }; +static uint8_t my_array3D[2][2][3] = { + { + {12, 11, 10}, {9, 8, 7}, + }, + { + {6, 5, 4}, {3, 2, 1}, + } +}; +""" + +import logging + +from pyx2cscope.utils import get_com_port, get_elf_file_path +from pyx2cscope.x2cscope import X2CScope + +# Set up logging +logging.basicConfig( + level=logging.INFO, + filename=__file__ + ".log", +) + +# X2C Scope Set up +elf_file = get_elf_file_path() +com_port = get_com_port() +x2c_scope = X2CScope(port=com_port, elf_file=elf_file) + +my_array_1d = x2c_scope.get_variable("my_array1D") +my_array_2d = x2c_scope.get_variable("my_array2D") +my_array_3d = x2c_scope.get_variable("my_array3D") + +print(my_array_1d.get_value()) +# [3, 2, 1] +print(my_array_2d.get_value()) +# [6, 5, 4, 3, 2, 1] +print(my_array_3d.get_value()) +# [12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1] + +my_array_2d_10 = x2c_scope.get_variable("my_array2D[1][0]") +my_array_2d_10.set_value(10) +print(my_array_2d.get_value()) +# [6, 5, 4, 10, 2, 1] + +my_array_2d[4] = 11 +print(my_array_2d.get_value()) +# [6, 5, 4, 10, 11, 1] + +print(my_array_2d[5]) +# 1 + +my_array_3d_102 = x2c_scope.get_variable("my_array3D[1][0][2]") +my_array_3d_102.set_value(10) +print(my_array_3d.get_value()) +# [12, 11, 10, 9, 8, 7, 6, 5, 10, 3, 2, 1] + + diff --git a/pyx2cscope/examples/testingArray.py b/pyx2cscope/examples/testingArray.py deleted file mode 100644 index a34f2ec..0000000 --- a/pyx2cscope/examples/testingArray.py +++ /dev/null @@ -1,29 +0,0 @@ -"""This example is for testing array functionality implementation.""" - -import logging - -import matplotlib.pyplot as plt - -from pyx2cscope.utils import get_com_port, get_elf_file_path -from pyx2cscope.x2cscope import X2CScope - -# Set up logging -logging.basicConfig( - level=logging.INFO, - filename=__file__ + ".log", -) - -# X2C Scope Set up -elf_file = get_elf_file_path() -com_port = get_com_port() -x2c_scope = X2CScope(port=com_port, elf_file=elf_file) - -variable = x2c_scope.get_variable("ScopeArray") -variable1 = x2c_scope.get_variable("motor.estimator.zsmt.iqHistory") -variable2 = x2c_scope.get_variable("motor.estimator.zsmt.idHistory") -variable3 = x2c_scope.get_variable("personal") -value = variable3.get_value() -print(value) -print(value[50]) -plt.plot(value) -plt.show() diff --git a/pyx2cscope/gui/web/app.py b/pyx2cscope/gui/web/app.py index ff8793c..e51185e 100644 --- a/pyx2cscope/gui/web/app.py +++ b/pyx2cscope/gui/web/app.py @@ -3,10 +3,6 @@ This module holds and handles the main url and forward the relative urls to the specific pages (blueprints). """ -import eventlet - -eventlet.monkey_patch() - import logging import os import webbrowser @@ -176,7 +172,8 @@ def main(host="localhost", web_port=5000, new=True, *args, **kwargs): print("Server is open for external requests!") if os.environ.get('DEBUG') != 'true': - socketio.run(app, debug=False, host=host, port=web_port) + socketio.run(app, debug=False, host=host, port=web_port, + allow_unsafe_werkzeug=True) else: socketio.run(app, debug=True, host=host, port=web_port, allow_unsafe_werkzeug=True, use_reloader=False) diff --git a/pyx2cscope/gui/web/extensions.py b/pyx2cscope/gui/web/extensions.py index ffd93fe..afb4beb 100644 --- a/pyx2cscope/gui/web/extensions.py +++ b/pyx2cscope/gui/web/extensions.py @@ -3,31 +3,16 @@ This module provides SocketIO configuration and lock creation functions that adapt to the current environment (production or debug mode). """ -import os - from flask_socketio import SocketIO -# Only enable eventlet in production, not during debugging -if os.environ.get('DEBUG', None) is None: # None means production - import eventlet - eventlet.monkey_patch() - socketio = SocketIO(cors_allowed_origins="*", async_mode='eventlet') - - def create_lock(): - """Create an eventlet-based semaphore lock. +socketio = SocketIO(cors_allowed_origins="*", async_mode='threading') +import threading - Returns: - eventlet.semaphore.Semaphore: A semaphore lock for thread synchronization. - """ - return eventlet.semaphore.Semaphore() -else: - socketio = SocketIO(cors_allowed_origins="*", async_mode='threading') - import threading - def create_lock(): - """Create a threading-based lock. +def create_lock(): + """Create a threading-based lock. - Returns: - threading.Lock: A lock for thread synchronization. - """ - return threading.Lock() + Returns: + threading.Lock: A lock for thread synchronization. + """ + return threading.Lock() diff --git a/pyx2cscope/gui/web/scope.py b/pyx2cscope/gui/web/scope.py index c8382dc..0f202db 100644 --- a/pyx2cscope/gui/web/scope.py +++ b/pyx2cscope/gui/web/scope.py @@ -3,10 +3,9 @@ This module provides the WebScope class for managing watch and scope variables through the web interface. """ +import numbers import time -from pandas.core.dtypes.inference import is_number - from pyx2cscope.gui.web import extensions from pyx2cscope.x2cscope import TriggerConfig, X2CScope @@ -127,7 +126,7 @@ def set_watch_rate(self, rate): Args: rate (float): Polling rate in seconds (must be between 0 and MAX_WATCH_RATE). """ - if is_number(rate) and 0 < rate < self.MAX_WATCH_RATE: + if isinstance(rate, numbers.Number) and 0 < rate < self.MAX_WATCH_RATE: self.watch_rate = rate def clear_watch_var(self): diff --git a/pyx2cscope/parser/generic_parser.py b/pyx2cscope/parser/generic_parser.py index a853587..d6478a9 100644 --- a/pyx2cscope/parser/generic_parser.py +++ b/pyx2cscope/parser/generic_parser.py @@ -4,6 +4,8 @@ """ import logging +import math +from itertools import product from elftools.construct.lib import ListContainer from elftools.dwarf.dwarf_expr import DWARFExprParser @@ -106,6 +108,7 @@ def _get_base_type_die(self, current_die): if type_attr: ref_addr = type_attr.value + current_die.cu.cu_offset return self.dwarf_info.get_DIE_from_refaddr(ref_addr) + return None def _get_end_die(self, current_die): """Find the end DIE of a type iteratively.""" @@ -224,7 +227,8 @@ def _process_array_type(self, end_die, member_name, offset): """ members = {} array_members = {} - array_size = self._get_array_length(end_die) + array_dimensions = self._get_array_dimensions(end_die) + array_size = math.prod(array_dimensions) base_type_die = self._get_base_type_die(end_die) self._process_end_die(members, base_type_die, member_name, offset) if members: @@ -241,11 +245,13 @@ def _process_array_type(self, end_die, member_name, offset): } # Generate array members, e.g.: array[0], array[1], ..., array[i] - for i in range(array_size): + ranges = [range(d) for d in array_dimensions] + for idx, idx_tuple in enumerate(product(*ranges)): + idx_str = ''.join(f'[{i}]' for i in idx_tuple) for name, values in members.items(): - element_name = name.replace(member_name, f"{member_name}[{i}]") + element_name = name + idx_str array_members[element_name] = values.copy() - array_members[element_name]["address_offset"] += i * idx_size + array_members[element_name]["address_offset"] += idx * idx_size return array_members @@ -344,16 +350,18 @@ def _process_structure_type(self, die, parent_name: str, offset=0): return members @staticmethod - def _get_array_length(type_die): - """Gets the length of an array type.""" - array_length = 0 + def _get_array_dimensions(type_die): + """Gets the length of an array type. + + Multidimensional arrays have multiple children with the tag DW_TAG_subrange_type. + """ + dimensions = [] for child in type_die.iter_children(): if child.tag == "DW_TAG_subrange_type": array_length_attr = child.attributes.get("DW_AT_upper_bound") if array_length_attr: - array_length = array_length_attr.value + 1 - break - return array_length + dimensions.append(array_length_attr.value + 1) + return dimensions @staticmethod def _process_base_type(end_die, parent_name, offset): diff --git a/pyx2cscope_win.spec b/pyx2cscope_win.spec index 49d00da..83a10e0 100644 --- a/pyx2cscope_win.spec +++ b/pyx2cscope_win.spec @@ -6,8 +6,15 @@ a = Analysis( ['pyx2cscope\\__main__.py'], pathex=[], binaries=[], - datas=[('pyx2cscope\\gui\\web\\static', 'pyx2cscope\\gui\\web\\static'), ('pyx2cscope\\gui\\web\\templates', 'pyx2cscope\\gui\\web\\templates'), ('pyx2cscope\\gui\\img', 'pyx2cscope\\gui\\img')], - hiddenimports=[], + datas=[ + ('pyx2cscope\\gui\\web\\static', 'pyx2cscope\\gui\\web\\static'), + ('pyx2cscope\\gui\\web\\templates', 'pyx2cscope\\gui\\web\\templates'), + ('pyx2cscope\\gui\\img', 'pyx2cscope\\gui\\img'), + ], + hiddenimports=[ + 'engineio.async_threading', + 'engineio.async_drivers.threading' + ], hookspath=[], hooksconfig={}, runtime_hooks=[], @@ -35,6 +42,7 @@ exe = EXE( entitlements_file=None, icon=['pyx2cscope\\gui\\img\\pyx2cscope.ico'], ) + coll = COLLECT( exe, a.binaries, diff --git a/requirements.txt b/requirements.txt index 8413943..d48fa07 100644 Binary files a/requirements.txt and b/requirements.txt differ diff --git a/scripts/build.py b/scripts/build.py new file mode 100644 index 0000000..764e26e --- /dev/null +++ b/scripts/build.py @@ -0,0 +1,154 @@ +"""Build script for pyX2Cscope. + +This script automates the build process for pyX2Cscope, including downloading +the wheel from PyPI, building the executable, and creating a distribution package. +""" + +import os +import shutil +import subprocess +import sys +import zipfile +from datetime import datetime + +import requests + +import pyx2cscope + +project_dir = os.path.join(os.path.dirname(pyx2cscope.__file__), "..") +os.chdir(project_dir) + + +def get_version(): + """Extract version from pyproject.toml or use a default.""" + try: + with open("pyproject.toml", "r") as f: + for line in f: + if line.strip().startswith("version"): + return line.split("=")[1].strip().strip("\"'") + except Exception: + pass + # Fallback to current date if version not found. + return datetime.now().strftime("%Y.%m.%d") + + +def build_executable(): + """Build the executable using PyInstaller.""" + print("Building executable with PyInstaller...") + result = subprocess.run( + ["pyinstaller", "--noconfirm", "--clean", "pyx2cscope_win.spec"], + capture_output=True, + text=True, + check=False, + ) + if result.returncode != 0: + print("Error building executable:") + print(result.stderr) + sys.exit(1) + print("Build completed successfully!") + + +def copy_batch_file(): + """Copy the batch file to the dist directory.""" + print("Copying batch file...") + dst_dir = os.path.join("dist", "pyX2Cscope") + src = os.path.join("scripts", "startWebInterface.bat") + dst = os.path.join(dst_dir, "startWebInterface.bat") + + try: + os.makedirs(dst_dir, exist_ok=True) + shutil.copy2(src, dst) + print(f"Successfully copied {src} to {dst}") + return dst_dir + except Exception as e: + print(f"Error copying file: {e}") + sys.exit(1) + + +def create_zip_archive(source_dir, version): + """Create a zip archive of the distribution. + + Args: + source_dir: Directory containing files to be zipped. + version: Version string to include in the zip filename. + + Returns: + Path to the created zip file. + """ + print("Creating zip archive...") + dist_path = os.path.join("dist") + zip_filename = f"pyX2Cscope_win_amd64_{version}.zip" + zip_path = os.path.join(dist_path, zip_filename) + + # Remove existing zip if it exists. + if os.path.exists(zip_path): + os.remove(zip_path) + + # Create zip file. + with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zipf: + for root, _dirs, files in os.walk(source_dir): + for file in files: + file_path = os.path.join(root, file) + arcname = os.path.join("pyX2Cscope", os.path.basename(file)) + zipf.write(file_path, arcname) + + print(f"Created archive: {zip_path}") + return zip_path + + +def download_wheel(version): + """Download the wheel from PyPI. + + Args: + version: Version of the wheel to download. + + Returns: + Path to the downloaded wheel file. + """ + print(f"Downloading wheel package version {version} from PyPI...") + wheel_name = f"pyx2cscope-{version}-py3-none-any.whl" + url = f"https://files.pythonhosted.org/packages/py3/p/pyx2cscope/{wheel_name}" + + try: + response = requests.get(url, stream=True) + response.raise_for_status() + + os.makedirs("dist", exist_ok=True) + wheel_path = os.path.join("dist", wheel_name) + + with open(wheel_path, "wb") as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + + print(f"Successfully downloaded {wheel_name}") + return wheel_path + except Exception as e: + print(f"Error downloading wheel: {e}") + sys.exit(1) + + +def main(): + """Main function to run the build process.""" + version = get_version() + print(f"Building pyX2Cscope version {version}") + + # Build the executable. + build_executable() + + # Copy the batch file. + dist_dir = copy_batch_file() + + # Create zip archive. + zip_path = create_zip_archive(dist_dir, version) + print(f"Zip archive created: {zip_path}") + + # Download the wheel from PyPI. + wheel_path = download_wheel(version) + print(f"Downloaded wheel: {wheel_path}") + + print("\nBuild process completed successfully!") + print(f"Executable location: {dist_dir}") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/scripts/startWebInterface.bat b/scripts/startWebInterface.bat new file mode 100644 index 0000000..de668a3 --- /dev/null +++ b/scripts/startWebInterface.bat @@ -0,0 +1 @@ +pyX2Cscope.exe -w