From df496ec5f1c7e6279ea5571e981df1eae77ffbe4 Mon Sep 17 00:00:00 2001 From: X2Cscope <73351552+X2Cscope@users.noreply.github.com> Date: Tue, 25 Nov 2025 15:49:09 +0100 Subject: [PATCH 01/10] update branch dev with current main state (#104) * including poetry on quality.txt * fix import pyx2cscope on test scripts * excluding csv files from example folder * including sphinx autodoc * Github action for github pages * Move github action to the correct dir * Do action on any branch * Github action dependency fix python version * Fix: Removed old dependency pylnet * Fix python version for doc generation action * Adding static code analysis ruff, pylint, isort * Fixing code structure and linter errors, plus isort and black * restructure versioning * Updated sphinx doc style, updated LOGO and index * adding pytest and initial tests * Added tests action * Fix versioning, fix package install for pytest * Added sphinx auto api and updated doc index * Added favico and isort,black * added runConfiguration to VCS (.idea) * Updated actions to release and document the project * fix documentation action * Adding verbose to poetry publish * Feat array support (#20) * getting array size from die * added support for Array. * added support for Array on 32-bit. * added support for Array. * added array support for structure. * adding array functionality. 1. added function is_array(). * working array functionality for 16-bit and 1st level structure on 32-bit * enhanced XC16 compiler error message * fixed _get_array_values * adding array behavior to variable Now after getting a variable from the factory, it is possible to call my_array[0] my_array[-2] len(my_array) * Included Mocker for Serial test * including setitem on array * test wrong elf file path 16/32 * fixing SerialStub.py with real data * reformatting bytearray * expanding black to 120 characters * install XC16 for test * install XC16 on documentation.yml * install XC16 documentation.yml 2 * sudo install on XC16 * including XC16 on PATH * including XC16 on GITHUB_PATH --------- Co-authored-by: Yash Agarwal Co-authored-by: Edras Pacola Co-authored-by: Mark Wendler * Including X2C Web interface (#23) * including variables on scope_channel * watch_view working * export chart as csv * splitting views * handling empty scope channels * including argparse to main * connect/disconnect * open watch and scope on new window * addind load and save urls * enable production mode * download config files watch/scope * added method clear_scope_channel * qrcode for redirect * localhost instead of host * update mchplnet version --------- Co-authored-by: Edras Pacola * Update documentation, bugfixes, and minor enhancements * excluded isort and black * include ruff with google convention * replace black and isort by ruff on action workflow # I - isort style for imports # N - pep8-naming # F - pyflakes # D - pydocstyle # PL - pylint lint.extend-select = ["I", "N", "F", "PL", "D"] * adding config.ini to the examples folder * theme read-the-docs for sphinx-build * including argparse on main * update version * adapt web to new Variable class name * enhancing documentation --------- Co-authored-by: Edras Pacola Co-authored-by: Yash Agarwal Co-authored-by: Mark Wendler * Fix dependencies (#31) * defining argparse Namespace * including flask dependency * unittest for web X2C * enhancing logger function * Fix documentation links * enhancing error msg if read elf not available * Including additional instructions to install at system level * fixing submodules * Unused project files clean-up * including test library on quality.txt * including sample frequency on scope_view.py * Fixing automated test dead loop, removed unused variable * Ruff fix of unused imports * bump version to 0.2.1 * Milestone v0.3.0 (#52) * Feat argument to pass elf file (#45) * include elf and port arguments Made a clean-up on the main file. No modules are loaded and according to user selection, the respective module is loaded and executed. Arguments -e --elf and -p --port have being included. Additionally, the utils module was moved from examples to the main pyX2Cscope module. So the elf file and port configuration over config.ini can be called. A small logic for elf file and port was also added, see _args_check() * pointing to the main mchplnet project --------- Co-authored-by: Edras Pacola * pointer of mchplnet to main (#49) Co-authored-by: Edras Pacola * Fix web interface offline (#50) * disabling setup group when connected * renaming xc2scope.py to x2cscope.py * script files available offline * Bug-Fix for dsPIC33ck and version number change. * pointer to mchplnet main * removed legend from chart * added warning when the server is open for ext. request --------- Co-authored-by: Edras Pacola Co-authored-by: Yash Agarwal * Executable scripts entry (#51) * #43 scripts entry point added to project toml * adding test for binary install * Updating readme with executable interface and python minimum version added to dependencies * Feat elfparser32 array (#47) Fixing ELF_parser32 to support array functionality. * Fixing ELF_parser32 to support array functionality. * implemented sucessfully the array functionality for 32 bit parser. * Updated test for elfparser32 for array. * Working for dspic33A. * changes for dsPIC33A. 1. need a new name for A-core. * To make the floating point value work for dspic33a. * fixed floating point trigger level issue. * added watchview plot. * added scaling. * searchable combo boxes. * Re-initialize all the variable on disconnect and connect * some layout fixes * load and save config. * getting address variable, need to work on address. * getting variable, need to work on address. * Scope Sample Time implementation. * bug fix-scaling factor. * implemented real time axis. 1. scope sample time box. * implemented real time x-axis. * name change. corrected the data set size as per documentation which is based on the microcontroller type. corrected the data set size as per documentation which is based on the microcontroller type. getting now accurate time frame. * bug fixed. to make parser working for GUI with xc16-readelf.exe in the folder * Added New tab3. With add variable and remove variable. * scailing and scaled value. * added scaling functionality. including elf parser in save config. also user could change later if elf file not found. save and load config also now loads all the variable for tab 3. * feature added: 1. as soon as the config file is loaded, it will also check for the elf file and if both are loaded it will connect to the serial. 2. also the comport and baudrate are selected so if the user is on the same comport and same baudrate, it will attempt to connect automatically. Elfparser getting more variable with address. Optimized to parse address faster. * bugfix. taking into consideration only full datasets. * implemented extern defined variable functionality. * Optimization is to work for all variable data types. * added functionality to access device info from x2cscope.py * Auto serial connect from load config. * Working auto connect com port. * Working auto serial connect. --------- Co-authored-by: Yash Agarwal --------- Co-authored-by: Mark Wendler Co-authored-by: Edras Pacola Co-authored-by: Edras Pacola Co-authored-by: Yash Agarwal * Bugfix/doc/images (#54) * changed the absolute path to images in README.md * changed version number to 0.3.1. * removed unwanted requirement --------- Co-authored-by: Yash Agarwal * Bugfix dependencies (#56) * Fix misspelling xc2scope to x2cscope * Update readme with manual reinstall * Fix version at __init__.py * Fix toml version format * fix pip install --user pyx2cscope (#57) * Fix documentation pip install --user (#59) * fix pip install --user pyx2cscope * fix documentation pip install --user * bump version after fix documentation * Milestone 0.4 (#78) * Feat connect without elf (#65) * including DummyParser * DummyParser does not need an elf file, * moved locations of the images. (#67) * update blockdiagram * pyinstaller to requirements * Add build executable github actions and pyinstaller spec * add icon for windows executable * adding requirements.txt for buildexecutables * Feat unified parser (#66) * changed version number * introduced new implementation of dwarfExpr. * adding array functionality to structures. * added demo example * Feat update unit test (#64) * remove xc-16 install from github test * added pyx2cscope package install * including test for non-existing variable on 16 and 32 bits * test array pointer * Bug Fix- Single shot working. * Adding mchplnet local dependency checkout * removed repeated variable info. * Updating package dependecy versions * Add shared lib dependecy and webInterface Bash script * Feat import export variable list (#68) * adding functions to import/export variables * pickle, yaml, and elf file format * adding example program * added test to check for import/export - single variable - multiple variables - all variables - yaml, pickle, and elf file * include test for dspic33ak (#74) * CamelCase to sneak_case and bring examples to same naming convention x2c_scope * Documentation simplification and improvements. * run buildexe action only on manual trigger * Fix web interface thread (#77) * replace constructor elf to import method * web is thread safe with x2cscope * handling invalid watch and scope load files * bug, x2c is not initialized solved Co-authored-by: Edras Pacola Co-authored-by: Yash Agarwal Co-authored-by: Mark Wendler * Milestone 0.4 - minor update to 0.4.1 (#90) * fix scope trigger and enumeration type (#87) * removed MCU data width scaling, trigger point calculation for array element * reinserting get_variable_raw This method was refactored in version 0.1 to internal method _get_variable_instance. VariableInfo was moved from Parser class to Variable class. The example SFR_Example.py was made to test this behavior but was not included into the tests. It is not yet included. * refactored some examples to fix logging * fix SFR example to import the right library * change get_device_id example * Fixed enum variable types (static only) inside structures still not working * Enhancing enum implementation with architecture considerations (data width) * fixed import error on LNet the code was importing the module and should import the class. --------- Co-authored-by: Edras Pacola * bump version to 0.4.1. --------- Co-authored-by: X2Cscope <73351552+X2Cscope@users.noreply.github.com> * Milestone 0.4.2 - minor update elf parser supports structure, enums and unions (#93) * Fix struct enum (#92) * merged process_end_die * enum working with structs and arrays * general variable instantiation is now optimized. * Before creating VarInfo objects, a valid address is checked. * including tests for enum tests for 16 and 32 bit cores * optimize offset calculation on struct members * include test to check struct member address * union is partially available: if a union is multiple of bytes it is working. if a union contain elements that have size of some bits, the variable is detected and included, but read and write on these variables are unpredictable. Do not use. * ruff fix * bump version 0.4.2 --------- Co-authored-by: Edras Pacola * Milestone v0.4.3 - minor update, bit size variables and enhancements (#96) * removed arguments from class Variable - Class Variable needs VariableInfo parameter and LNet to get instantiated. - Instead of adding more parameters to the constructor, we store the VariableInfo as a member. * moved set_value to Variable class, added _set_value as abstract function - moved call to checking value_range to parent class - enables generic processing before moving to specific processing * reading bit-size variables under unions / structs regardless variable's byte size * writing bit-size variables regardless byte size * increasing number of variables on generic_gui * changed default generic_gui logger level from warning to error - this avoids minor level messages showing up on terminal * fixed behavior when variable name is not found If the variable does not exist in elf file, we log message "Variable '{name} not found!" and return None If the variable is found, but an error occurred when parsing, we log the error message and return None * Changing prints over the code to logging.debug * Changed contact information * ruff check * bump version --------- Co-authored-by: Edras Pacola * bump v0.4.4 Bugfix v0.4.3 -> v0.4.4 (#97) * VariableInfo goes under info member * fixed enum byte size an enumeration has variable byte size, and depends on the mcu architecture. the byte size is read during parsing and available at variable.info.byte_size * fixed sample time window calculation * renamed method from scope_sample_time to get_scope_sample_time * calculation is based on retrieved values and not on magic numbers * adapted generic_gui.py to correct function call on get_sample_time * formatting code * enhancing docs * fixed web gui with new VarInfo dataclass * bump version 0.4.4 * fixing documentation and image links * fix README.md * fix web initial message error --------- Co-authored-by: Edras Pacola * Milestone v0.5.0 (#102) * Websockets for webx2c (#101) * fixed html files name pattern on webx2cscope * convert to bootstrap 5 * update mchplnet pointer main@v0.2.2 * fixed bug removing variables from scope view web * visual fix on mobile view * fixed overlay of dropdown and header of table * variable hide on scope_view.js * adding variable legends to the chart. when you click on the legend item, it hides or shows the line chart. * fixing demos * websockets for watch-view and scope-view * pointing to mchplnet v0.3.0 * bug fix - trigger_level trigger_level must be in range of variable if trigger_level is outside this range, it clamps to max or min accordingly * removing prints * fix doc misspelling * fixing example readability * when adding a variable on watch-view, read the value before * fixed labels on x-axis on webview * fix views when connect/disconnect * default log level for x2cscope is error * enhancing serial_stub.py for get_ram and put_ram * included test for thread safety * fixing interface on mchplnet * enhancing examples * enhancing existing tests * enhancing github actions documentation.yml - if the merging branch is the main, check if we have a new version * fixing to default snake_case on mchplnet * update mchplnet pointer * fix docstrings * fix imports format * fix docstring on tests * removing elf16 parser - elf16_parser.py was deprecated on previous versions and now it was removed. - The current elf parser implementation handles both 16 and 32 bit architectures and is based on pyelftools. * include linter ignore. - generic gui needs to be refactored. * removing debug prints * including version check and auto-update of version on __init__.py on pre commit * update mchplnet to version 0.3.0 * fix mchplnet dependency * enhancing enum_variables added specific methods for enum: - get_enum_list - set_enum_value - get_enum_value * importing eventlet in case webinterface is called --------- Co-authored-by: Edras Pacola Co-authored-by: Edras Pacola * Version 0.5.1 (#103) * removing eventlet from webinterface this package is deprecated and should not be used for new projects. socketio use only threading from python * fix installer dependencies pyinstaller needs to add the hidden dependencies of web interface * automate artifacts generation this script: - builds the exe - copy the .bat file to start web interface - zip the contents - download the wheel file from pypi - place everything under dist folder to upload to release page on github * enhance development documentation * bug fix on multidimensional array Only the first indeces of multidimensional matrices were being addressed. Added matrix example on example folder * version 0.5.1 --------- Co-authored-by: Edras Pacola --------- Co-authored-by: Edras Pacola Co-authored-by: Mark Wendler Co-authored-by: Yash Agarwal Co-authored-by: Edras Pacola # Conflicts: # .github/workflows/documentation.yml # .gitignore # doc/development.md # doc/example.md # doc/gui_qt.md # doc/gui_web.md # doc/index.rst # doc/scripting.rst # mchplnet # pyproject.toml # pyx2cscope/__init__.py # pyx2cscope/__main__.py # pyx2cscope/examples/SFR_Example.py # pyx2cscope/gui/__init__.py # pyx2cscope/gui/web/app.py # pyx2cscope/gui/web/scope.py # pyx2cscope/gui/web/static/js/scope_view.js # pyx2cscope/gui/web/static/js/script.js # pyx2cscope/gui/web/static/js/watch_view.js # pyx2cscope/gui/web/templates/header.html # pyx2cscope/gui/web/templates/index.html # pyx2cscope/gui/web/templates/navbar.html # pyx2cscope/gui/web/templates/sample_control.html # pyx2cscope/gui/web/templates/setup.html # pyx2cscope/gui/web/templates/source_config.html # pyx2cscope/gui/web/templates/watch_view.html # pyx2cscope/gui/web/views/scope_view.py # pyx2cscope/gui/web/views/watch_view.py # pyx2cscope/gui/web/ws_handlers.py # pyx2cscope/parser/elf_parser.py # pyx2cscope/parser/generic_parser.py # pyx2cscope/utils.py # pyx2cscope/variable/variable_factory.py # pyx2cscope/x2cscope.py # pyx2cscope_linux.spec # pyx2cscope_win.spec # quality.txt # requirements.txt # scripts/build.py # tests/test_pyx2cscope_class.py --- .gitignore | 1 - doc/development.md | 2 + doc/example.md | 2 +- mchplnet | 2 +- pyx2cscope/examples/SFR_Example.py | 3 - pyx2cscope/gui/generic_gui/__init__.py | 1 + pyx2cscope/gui/generic_gui/detachable_gui.py | 1414 +++++++++++ pyx2cscope/gui/generic_gui/generic_gui.py | 2345 ++++++++++++++++++ pyx2cscope/parser/elf_parser.py | 1 + pyx2cscope/parser/generic_parser.py | 16 +- pyx2cscope/variable/variable_factory.py | 3 + pyx2cscope/x2cscope.py | 8 +- pyx2cscope_linux.spec | 1 + 13 files changed, 3786 insertions(+), 13 deletions(-) create mode 100644 pyx2cscope/gui/generic_gui/__init__.py create mode 100644 pyx2cscope/gui/generic_gui/detachable_gui.py create mode 100644 pyx2cscope/gui/generic_gui/generic_gui.py diff --git a/.gitignore b/.gitignore index d08a1541..0fe10539 100644 --- a/.gitignore +++ b/.gitignore @@ -142,4 +142,3 @@ dmypy.json #backup files .bkp -/.claude/ diff --git a/doc/development.md b/doc/development.md index ac5f110f..863c71e4 100644 --- a/doc/development.md +++ b/doc/development.md @@ -70,3 +70,5 @@ available on pypi in the dist folder. ```bash python scripts/build.py ``` + +## Creating artifacts to upload to GitHu diff --git a/doc/example.md b/doc/example.md index 9ae83c11..61a16cef 100644 --- a/doc/example.md +++ b/doc/example.md @@ -217,7 +217,7 @@ variables = [ for var in variables: x2c_scope.add_scope_channel(x2c_scope.get_variable(var)) -x2c_scope.set_sample_time(0) +x2c_scope.set_sample_time(1) # Create the plot plt.ion() # Turn on interactive mode diff --git a/mchplnet b/mchplnet index a1b9669e..d950279f 160000 --- a/mchplnet +++ b/mchplnet @@ -1 +1 @@ -Subproject commit a1b9669e222f8c3a6cfc3f2ee90d3e46a36f17b4 +Subproject commit d950279f674939548130fc5078330bda3e5e553b diff --git a/pyx2cscope/examples/SFR_Example.py b/pyx2cscope/examples/SFR_Example.py index ddfe5a6c..b499f632 100644 --- a/pyx2cscope/examples/SFR_Example.py +++ b/pyx2cscope/examples/SFR_Example.py @@ -31,6 +31,3 @@ print(f"LATD: 0x{latd_value:04X} ({latd_value})") print(f"TMR1: 0x{tmr1_value:04X} ({tmr1_value})") print("-" * 40) - - - diff --git a/pyx2cscope/gui/generic_gui/__init__.py b/pyx2cscope/gui/generic_gui/__init__.py new file mode 100644 index 00000000..1d624f3c --- /dev/null +++ b/pyx2cscope/gui/generic_gui/__init__.py @@ -0,0 +1 @@ +"""FOC Gui: a general gui for motor control.""" diff --git a/pyx2cscope/gui/generic_gui/detachable_gui.py b/pyx2cscope/gui/generic_gui/detachable_gui.py new file mode 100644 index 00000000..5d8a9f60 --- /dev/null +++ b/pyx2cscope/gui/generic_gui/detachable_gui.py @@ -0,0 +1,1414 @@ +"""Detachable genenric GUI for X2Cscope.""" + +import logging +import os +import sys +import time +from collections import deque +from datetime import datetime + +import matplotlib +import numpy as np +import pyqtgraph as pg +import serial.tools.list_ports +from PyQt5 import QtGui +from PyQt5.QtCore import QFileInfo, QMutex, QRegExp, QSettings, Qt, QTimer, pyqtSlot +from PyQt5.QtGui import QIcon, QRegExpValidator +from PyQt5.QtWidgets import ( + QApplication, + QCheckBox, + QComboBox, + QDockWidget, + QFileDialog, + QGridLayout, + QGroupBox, + QHBoxLayout, + QLabel, + QLineEdit, + QMainWindow, + QMessageBox, + QPushButton, + QSizePolicy, + QSlider, + QStyleFactory, + QVBoxLayout, + QWidget, +) + +from pyx2cscope.gui import img as img_src +from pyx2cscope.x2cscope import TriggerConfig, X2CScope + +logging.basicConfig(level=logging.ERROR) + +matplotlib.use("QtAgg") + + +class X2cscopeGui(QMainWindow): + """Main GUI class for the pyX2Cscope application.""" + + def __init__(self): + """Initializing all the elements required.""" + super().__init__() + + self.triggerVariable = None + self.initialize_variables() + self.init_ui() + + def initialize_variables(self): + """Initialize instance variables.""" + self.sampling_active = False + self.offset_boxes = None + self.plot_checkboxes = None + self.scaled_value_boxes = None + self.scaling_boxes = None + self.Value_var_boxes = None + self.combo_boxes = None + self.live_checkboxes = None + self.timer_list = None + self.VariableList = [] + self.old_Variable_list = [] + self.var_factory = None + self.ser = None + self.timerValue = 500 + self.port_combo = QComboBox() + self.layout = None + self.slider_var1 = QSlider(Qt.Horizontal) + self.plot_button = QPushButton("Plot") + self.mutex = QMutex() + self.grid_layout = QGridLayout() + self.box_layout = QHBoxLayout() + self.timer1 = QTimer() + self.timer2 = QTimer() + self.timer3 = QTimer() + self.timer4 = QTimer() + self.timer5 = QTimer() + self.plot_update_timer = QTimer() # Timer for continuous plot update + self.timer() + self.offset_var() + self.plot_var_check() + self.scaling_var() + self.value_var() + self.live_var() + self.scaled_value() + self.combo_box() + self.sampletime = QLineEdit() + self.unit_var() + self.Connect_button = QPushButton("Connect") + self.baud_combo = QComboBox() + self.select_file_button = QPushButton("Select elf file") + self.error_shown = False + self.plot_window_open = False + self.settings = QSettings("MyCompany", "MyApp") + self.file_path: str = self.settings.value("file_path", "", type=str) + self.init_variables() + + def init_variables(self): + """Some extra variables define.""" + self.selected_var_indices = [ + 0, + 0, + 0, + 0, + 0, + ] # List to store selected variable indices + self.selected_variables = [] # List to store selected variables + decimal_regex = QRegExp("-?[0-9]+(\\.[0-9]+)?") + self.decimal_validator = QRegExpValidator(decimal_regex) + + self.plot_data = deque(maxlen=250) # Store plot data for all variables + self.plot_colors = [ + "b", + "g", + "r", + "c", + "m", + "y", + "k", + ] # colours for different plot + # Add self.labels on top + self.labels = [ + "Live", + "Variable", + "Value", + "Scaling", + "Offset", + "Scaled Value", + "Unit", + "Plot", + ] + + def init_ui(self): + """Initialize the user interface.""" + self.setup_application_style() + self.create_central_widget() + self.create_dockable_tabs() + self.setup_window_properties() + self.refresh_ports() + + def setup_application_style(self): + """Set the application style.""" + QApplication.setStyle(QStyleFactory.create("Fusion")) + + def create_central_widget(self): + """Create the central widget.""" + central_widget = QWidget(self) + self.setCentralWidget(central_widget) + self.layout = QVBoxLayout(central_widget) + + def create_dockable_tabs(self): + """Create dockable tabs for the main window.""" + self.watch_view_dock = QDockWidget("WatchView", self) + self.scope_view_dock = QDockWidget("ScopeView", self) + + self.tab1 = QWidget() + self.tab2 = QWidget() + + self.watch_view_dock.setWidget(self.tab1) + self.scope_view_dock.setWidget(self.tab2) + + self.addDockWidget(Qt.LeftDockWidgetArea, self.watch_view_dock) + self.addDockWidget(Qt.RightDockWidgetArea, self.scope_view_dock) + + self.setup_tab1() + self.setup_tab2() + + def setup_window_properties(self): + """Set up the main window properties.""" + self.setWindowTitle("pyX2Cscope") + mchp_img = os.path.join(os.path.dirname(img_src.__file__), "pyx2cscope.jpg") + self.setWindowIcon(QtGui.QIcon(mchp_img)) + + def combo_box(self): + """Initializing combo boxes.""" + self.combo_box5 = QComboBox() + self.combo_box4 = QComboBox() + self.combo_box3 = QComboBox() + self.combo_box2 = QComboBox() + self.combo_box1 = QComboBox() + + def scaled_value(self): + """Initializing Scaled variable.""" + self.ScaledValue_var1 = QLineEdit(self) + self.ScaledValue_var2 = QLineEdit(self) + self.ScaledValue_var3 = QLineEdit(self) + self.ScaledValue_var4 = QLineEdit(self) + self.ScaledValue_var5 = QLineEdit(self) + + def live_var(self): + """Initializing live variable.""" + self.Live_var1 = QCheckBox(self) + self.Live_var2 = QCheckBox(self) + self.Live_var3 = QCheckBox(self) + self.Live_var4 = QCheckBox(self) + self.Live_var5 = QCheckBox(self) + + def value_var(self): + """Initializing value variable.""" + self.Value_var1 = QLineEdit(self) + self.Value_var2 = QLineEdit(self) + self.Value_var3 = QLineEdit(self) + self.Value_var4 = QLineEdit(self) + self.Value_var5 = QLineEdit(self) + + def timer(self): + """Initializing timer.""" + self.timer5 = QTimer() + self.timer4 = QTimer() + self.timer3 = QTimer() + self.timer2 = QTimer() + self.timer1 = QTimer() + + def offset_var(self): + """Initializing Offset Variable.""" + self.offset_var1 = QLineEdit() + self.offset_var2 = QLineEdit() + self.offset_var3 = QLineEdit() + self.offset_var4 = QLineEdit() + self.offset_var5 = QLineEdit() + + def plot_var_check(self): + """Initializing plot variable check boxes.""" + self.plot_var5_checkbox = QCheckBox() + self.plot_var2_checkbox = QCheckBox() + self.plot_var4_checkbox = QCheckBox() + self.plot_var3_checkbox = QCheckBox() + self.plot_var1_checkbox = QCheckBox() + + def scaling_var(self): + """Initializing Scaling variable.""" + self.Scaling_var1 = QLineEdit(self) + self.Scaling_var2 = QLineEdit(self) + self.Scaling_var3 = QLineEdit(self) + self.Scaling_var4 = QLineEdit(self) + self.Scaling_var5 = QLineEdit(self) + + def unit_var(self): + """Initializing unit variable.""" + self.Unit_var1 = QLineEdit(self) + self.Unit_var2 = QLineEdit(self) + self.Unit_var3 = QLineEdit(self) + self.Unit_var4 = QLineEdit(self) + self.Unit_var5 = QLineEdit(self) + + def setup_tab1(self): + """Set up the first tab with the original functionality.""" + self.tab1.layout = QVBoxLayout() + self.tab1.setLayout(self.tab1.layout) + + grid_layout = QGridLayout() + self.tab1.layout.addLayout(grid_layout) + + self.setup_port_layout(grid_layout) + self.setup_baud_layout(grid_layout) + self.setup_sampletime_layout(grid_layout) + self.setup_variable_layout(grid_layout) + self.setup_connections() + + def setup_tab2(self): + """Set up the second tab with the scope functionality.""" + self.tab2.layout = QVBoxLayout() + self.tab2.setLayout(self.tab2.layout) + + main_grid_layout = QGridLayout() + self.tab2.layout.addLayout(main_grid_layout) + + # Set up individual components + trigger_group = self.create_trigger_configuration_group() + variable_group = self.create_variable_selection_group() + self.scope_plot_widget = self.create_scope_plot_widget() + button_layout = self.create_save_load_buttons() + + # Add the group boxes to the main layout with stretch factors + main_grid_layout.addWidget(trigger_group, 0, 0) + main_grid_layout.addWidget(variable_group, 0, 1) + + # Set the column stretch factors to make the variable group larger + main_grid_layout.setColumnStretch(0, 1) # Trigger configuration box + main_grid_layout.setColumnStretch(1, 3) # Variable selection box + + # Add the plot widget for scope view + self.tab2.layout.addWidget(self.scope_plot_widget) + + # Add Save and Load buttons + self.tab2.layout.addLayout(button_layout) + + def create_trigger_configuration_group(self): + """Create the trigger configuration group box.""" + trigger_group = QGroupBox("Trigger Configuration") + trigger_layout = QVBoxLayout() + trigger_group.setLayout(trigger_layout) + + grid_layout_trigger = QGridLayout() + trigger_layout.addLayout(grid_layout_trigger) + + self.single_shot_checkbox = QCheckBox("Single Shot") + self.sample_time_factor = QLineEdit("1") + self.sample_time_factor.setValidator(self.decimal_validator) + self.trigger_mode_combo = QComboBox() + self.trigger_mode_combo.addItems(["Auto", "Triggered"]) + self.trigger_edge_combo = QComboBox() + self.trigger_edge_combo.addItems(["Rising", "Falling"]) + self.trigger_level_edit = QLineEdit("0") + self.trigger_level_edit.setValidator(self.decimal_validator) + self.trigger_delay_edit = QLineEdit("0") + self.trigger_delay_edit.setValidator(self.decimal_validator) + + self.scope_sampletime_edit = QLineEdit( + "50" + ) # Default sample time in microseconds + self.scope_sampletime_edit.setValidator(self.decimal_validator) + + # Total Time + self.total_time_label = QLabel("Total Time (ms):") + self.total_time_value = QLineEdit("0") + self.total_time_value.setReadOnly(True) + + self.scope_sample_button = QPushButton("Sample") + self.scope_sample_button.setFixedSize(100, 30) + self.scope_sample_button.clicked.connect(self.start_sampling) + + # Arrange widgets in grid layout + grid_layout_trigger.addWidget(self.single_shot_checkbox, 0, 0, 1, 2) + grid_layout_trigger.addWidget(QLabel("Sample Time Factor"), 1, 0) + grid_layout_trigger.addWidget(self.sample_time_factor, 1, 1) + grid_layout_trigger.addWidget(QLabel("Scope Sample Time (µs):"), 2, 0) + grid_layout_trigger.addWidget(self.scope_sampletime_edit, 2, 1) + grid_layout_trigger.addWidget(self.total_time_label, 3, 0) + grid_layout_trigger.addWidget(self.total_time_value, 3, 1) + grid_layout_trigger.addWidget(QLabel("Trigger Mode:"), 4, 0) + grid_layout_trigger.addWidget(self.trigger_mode_combo, 4, 1) + grid_layout_trigger.addWidget(QLabel("Trigger Edge:"), 5, 0) + grid_layout_trigger.addWidget(self.trigger_edge_combo, 5, 1) + grid_layout_trigger.addWidget(QLabel("Trigger Level:"), 6, 0) + grid_layout_trigger.addWidget(self.trigger_level_edit, 6, 1) + grid_layout_trigger.addWidget(QLabel("Trigger Delay:"), 7, 0) + grid_layout_trigger.addWidget(self.trigger_delay_edit, 7, 1) + grid_layout_trigger.addWidget(self.scope_sample_button, 8, 0, 1, 2) + + return trigger_group + + def create_variable_selection_group(self): + """Create the variable selection group box.""" + variable_group = QGroupBox("Variable Selection") + variable_layout = QVBoxLayout() + variable_group.setLayout(variable_layout) + + grid_layout_variable = QGridLayout() + variable_layout.addLayout(grid_layout_variable) + number_of_variables = 8 + + self.scope_var_lines = [QLineEdit() for _ in range(number_of_variables)] + self.trigger_var_checkbox = [QCheckBox() for _ in range(number_of_variables)] + self.scope_channel_checkboxes = [QCheckBox() for _ in range(number_of_variables)] + self.scope_scaling_boxes = [QLineEdit("1") for _ in range(number_of_variables)] + + for checkbox in self.scope_channel_checkboxes: + checkbox.setChecked(True) + + for line_edit in self.scope_var_lines: + line_edit.setReadOnly(True) + line_edit.setPlaceholderText("Search Variable") + line_edit.installEventFilter(self) + + # Add "Search Variable" label + grid_layout_variable.addWidget(QLabel("Search Variable"), 0, 1) + grid_layout_variable.addWidget(QLabel("Trigger"), 0, 0) + grid_layout_variable.addWidget(QLabel("Gain"), 0, 2) + grid_layout_variable.addWidget(QLabel("Visible"), 0, 3) + + for i, (line_edit, trigger_checkbox, scale_box, show_checkbox) in enumerate( + zip( + self.scope_var_lines, + self.trigger_var_checkbox, + self.scope_scaling_boxes, + self.scope_channel_checkboxes, + ) + ): + line_edit.setMinimumHeight(20) + trigger_checkbox.setMinimumHeight(20) + show_checkbox.setMinimumHeight(20) + scale_box.setMinimumHeight(20) + scale_box.setFixedSize(50, 20) + scale_box.setValidator(self.decimal_validator) + + line_edit.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) + grid_layout_variable.addWidget(trigger_checkbox, i + 1, 0) + grid_layout_variable.addWidget(line_edit, i + 1, 1) + grid_layout_variable.addWidget(scale_box, i + 1, 2) + grid_layout_variable.addWidget(show_checkbox, i + 1, 3) + + trigger_checkbox.stateChanged.connect( + lambda state, x=i: self.handle_scope_checkbox_change(state, x) + ) + scale_box.editingFinished.connect(self.update_scope_plot) + show_checkbox.stateChanged.connect(self.update_scope_plot) + + return variable_group + + def create_scope_plot_widget(self): + """Create the scope plot widget.""" + scope_plot_widget = pg.PlotWidget(title="Scope Plot") + scope_plot_widget.setBackground("w") + scope_plot_widget.addLegend() + scope_plot_widget.showGrid(x=True, y=True) + scope_plot_widget.getViewBox().setMouseMode(pg.ViewBox.RectMode) + + return scope_plot_widget + + def create_save_load_buttons(self): + """Create the save and load buttons.""" + self.save_button_scope = QPushButton("Save Config") + self.load_button_scope = QPushButton("Load Config") + self.save_button_scope.setFixedSize(100, 30) + self.load_button_scope.setFixedSize(100, 30) + + button_layout = QHBoxLayout() + button_layout.addWidget(self.save_button_scope) + button_layout.addWidget(self.load_button_scope) + + return button_layout + + def handle_scope_checkbox_change(self, state, index): + """Handle the change in the state of the scope view checkboxes.""" + if state == Qt.Checked: + for i, checkbox in enumerate(self.trigger_var_checkbox): + if i != index: + checkbox.setChecked(False) + self.triggerVariable = self.scope_var_combos[index].currentText() + logging.debug(f"Checked variable: {self.scope_var_combos[index].currentText()}") + else: + self.triggerVariable = None + + def setup_port_layout(self, layout): + """Set up the port selection layout.""" + port_layout = QGridLayout() + port_label = QLabel("Select Port:") + + refresh_button = QPushButton() + refresh_button.setFixedSize(25, 25) + refresh_button.clicked.connect(self.refresh_ports) + refresh_img = os.path.join(os.path.dirname(img_src.__file__), "refresh.png") + refresh_button.setIcon(QIcon(refresh_img)) + + self.select_file_button.setEnabled(True) + self.select_file_button.clicked.connect(self.select_elf_file) + + port_layout.addWidget(port_label, 0, 0) + port_layout.addWidget(self.port_combo, 0, 1) + port_layout.addWidget(refresh_button, 0, 2) + + layout.addLayout(port_layout, 1, 0) + + def setup_baud_layout(self, layout): + """Set up the baud rate selection layout.""" + baud_layout = QGridLayout() + baud_label = QLabel("Select Baud Rate:") + baud_layout.addWidget(baud_label, 0, 0) + baud_layout.addWidget(self.baud_combo, 0, 1) + + self.baud_combo.addItems(["38400", "115200", "230400", "460800", "921600"]) + default_baud_rate = "115200" + index = self.baud_combo.findText(default_baud_rate, Qt.MatchFixedString) + if index >= 0: + self.baud_combo.setCurrentIndex(index) + + layout.addLayout(baud_layout, 2, 0) + + def setup_sampletime_layout(self, layout): + """Set up the sample time layout.""" + self.Connect_button.clicked.connect(self.toggle_connection) + self.Connect_button.setFixedSize(100, 30) + self.Connect_button.setMinimumHeight(30) + + self.sampletime.setText("500") + self.sampletime.setValidator(self.decimal_validator) + self.sampletime.editingFinished.connect(self.sampletime_edit) + self.sampletime.setFixedSize(50, 20) + + sampletime_layout = QHBoxLayout() + sampletime_layout.addWidget(QLabel("Sampletime"), alignment=Qt.AlignLeft) + sampletime_layout.addWidget(self.sampletime, alignment=Qt.AlignLeft) + sampletime_layout.addWidget(QLabel("ms"), alignment=Qt.AlignLeft) + sampletime_layout.addStretch(1) + sampletime_layout.addWidget(self.Connect_button, alignment=Qt.AlignRight) + + layout.addLayout(sampletime_layout, 3, 0) + layout.addWidget(self.select_file_button, 4, 0) + + def setup_variable_layout(self, layout): + """Set up the variable selection layout.""" + self.timer_list = [ + self.timer1, + self.timer2, + self.timer3, + self.timer4, + self.timer5, + ] + + for col, label in enumerate(self.labels): + self.grid_layout.addWidget(QLabel(label), 0, col) + + self.live_checkboxes = [ + self.Live_var1, + self.Live_var2, + self.Live_var3, + self.Live_var4, + self.Live_var5, + ] + self.combo_boxes = [ + self.combo_box1, + self.combo_box2, + self.combo_box3, + self.combo_box4, + self.combo_box5, + ] + self.Value_var_boxes = [ + self.Value_var1, + self.Value_var2, + self.Value_var3, + self.Value_var4, + self.Value_var5, + ] + self.scaling_boxes = [ + self.Scaling_var1, + self.Scaling_var2, + self.Scaling_var3, + self.Scaling_var4, + self.Scaling_var5, + ] + self.scaled_value_boxes = [ + self.ScaledValue_var1, + self.ScaledValue_var2, + self.ScaledValue_var3, + self.ScaledValue_var4, + self.ScaledValue_var5, + ] + unit_boxes = [ + self.Unit_var1, + self.Unit_var2, + self.Unit_var3, + self.Unit_var4, + self.Unit_var5, + ] + self.plot_checkboxes = [ + self.plot_var1_checkbox, + self.plot_var2_checkbox, + self.plot_var3_checkbox, + self.plot_var4_checkbox, + self.plot_var5_checkbox, + ] + self.offset_boxes = [ + self.offset_var1, + self.offset_var2, + self.offset_var3, + self.offset_var4, + self.offset_var5, + ] + + for row_index, ( + live_var, + combo_box, + value_var, + scaling_var, + offset_var, + scaled_value_var, + unit_var, + plot_checkbox, + ) in enumerate( + zip( + self.live_checkboxes, + self.combo_boxes, + self.Value_var_boxes, + self.scaling_boxes, + self.offset_boxes, + self.scaled_value_boxes, + unit_boxes, + self.plot_checkboxes, + ), + 1, + ): + live_var.setEnabled(False) + combo_box.setEnabled(False) + combo_box.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) + value_var.setText("0") + value_var.setValidator(self.decimal_validator) + scaling_var.setText("1") + offset_var.setText("0") + offset_var.setValidator(self.decimal_validator) + scaled_value_var.setText("0") + scaled_value_var.setValidator(self.decimal_validator) + display_row = row_index # Use a different variable name for the assignment + if display_row > 1: + display_row += 1 + self.grid_layout.addWidget(live_var, display_row, 0) + self.grid_layout.addWidget(combo_box, display_row, 1) + if display_row == 1: + self.grid_layout.addWidget(self.slider_var1, display_row + 1, 0, 1, 7) + + self.grid_layout.addWidget(value_var, display_row, 2) + self.grid_layout.addWidget(scaling_var, display_row, 3) + self.grid_layout.addWidget(offset_var, display_row, 4) + self.grid_layout.addWidget(scaled_value_var, display_row, 5) + self.grid_layout.addWidget(unit_var, display_row, 6) + self.grid_layout.addWidget(plot_checkbox, display_row, 7) + plot_checkbox.stateChanged.connect( + lambda state, x=row_index - 1: self.update_watch_plot() + ) + + layout.addLayout(self.grid_layout, 5, 0) + + # Add the plot widget for watch view + self.watch_plot_widget = pg.PlotWidget(title="Watch Plot") + self.watch_plot_widget.setBackground("w") + self.watch_plot_widget.addLegend() # Add legend to the plot widget + self.watch_plot_widget.showGrid(x=True, y=True) # Enable grid lines + self.tab1.layout.addWidget(self.watch_plot_widget) + + def setup_connections(self): + """Set up connections for various widgets.""" + self.plot_button.clicked.connect(self.plot_data_plot) + + for timer, combo_box, value_var in zip( + self.timer_list, self.combo_boxes, self.Value_var_boxes + ): + timer.timeout.connect( + lambda cb=combo_box, v_var=value_var: self.handle_var_update( + cb.currentText(), v_var + ) + ) + + for combo_box, value_var in zip(self.combo_boxes, self.Value_var_boxes): + combo_box.currentIndexChanged.connect( + lambda cb=combo_box, v_var=value_var: self.handle_variable_getram( + self.VariableList[cb], v_var + ) + ) + for combo_box, value_var in zip(self.combo_boxes, self.Value_var_boxes): + value_var.editingFinished.connect( + lambda cb=combo_box, v_var=value_var: self.handle_variable_putram( + cb.currentText(), v_var + ) + ) + + self.connect_editing_finished() + + for timer, live_var in zip(self.timer_list, self.live_checkboxes): + live_var.stateChanged.connect( + lambda state, lv=live_var, tm=timer: self.var_live(lv, tm) + ) + + self.slider_var1.setMinimum(-32768) + self.slider_var1.setMaximum(32767) + self.slider_var1.setEnabled(False) + self.slider_var1.valueChanged.connect(self.slider_var1_changed) + + self.plot_update_timer.timeout.connect( + self.update_watch_plot + ) # Connect the QTimer to the update method + + def connect_editing_finished(self): + """Connect editingFinished signals for value and scaling inputs.""" + for ( + scaling, + value_var, + scaled_value, + offset, + ) in zip( + self.scaling_boxes, + self.Value_var_boxes, + self.scaled_value_boxes, + self.offset_boxes, + ): + + def connect_editing_finished( + sc_edit=scaling, + v_edit=value_var, + scd_edit=scaled_value, + off_edit=offset, + ): + def on_editing_finished(): + self.update_scaled_value(sc_edit, v_edit, scd_edit, off_edit) + + return on_editing_finished + + value_var.editingFinished.connect(connect_editing_finished()) + + for ( + scaling, + value_var, + scaled_value, + offset, + ) in zip( + self.scaling_boxes, + self.Value_var_boxes, + self.scaled_value_boxes, + self.offset_boxes, + ): + + def connect_editing_finished( + sc_edit=scaling, + v_edit=value_var, + scd_edit=scaled_value, + off_edit=offset, + ): + def on_editing_finished(): + self.update_scaled_value(sc_edit, v_edit, scd_edit, off_edit) + + return on_editing_finished + + scaling.editingFinished.connect(connect_editing_finished()) + + for ( + scaling, + value_var, + scaled_value, + offset, + ) in zip( + self.scaling_boxes, + self.Value_var_boxes, + self.scaled_value_boxes, + self.offset_boxes, + ): + + def connect_text_changed( + sc_edit=scaling, + v_edit=value_var, + scd_edit=scaled_value, + off_edit=offset, + ): + def on_text_changed(): + self.update_scaled_value(sc_edit, v_edit, scd_edit, off_edit) + + return on_text_changed + + value_var.textChanged.connect(connect_text_changed()) + + for ( + scaling, + value_var, + scaled_value, + offset, + ) in zip( + self.scaling_boxes, + self.Value_var_boxes, + self.scaled_value_boxes, + self.offset_boxes, + ): + + def connect_text_changed( + sc_edit=scaling, + v_edit=value_var, + scd_edit=scaled_value, + off_edit=offset, + ): + def on_text_changed(): + self.update_scaled_value(sc_edit, v_edit, scd_edit, off_edit) + + return on_text_changed + + offset.editingFinished.connect(connect_text_changed()) + + @pyqtSlot() + def var_live(self, live_var, timer): + """Handles the state change of live variable checkboxes. + + Args: + live_var (QCheckBox): The checkbox representing a live variable. + timer (QTimer): The timer associated with the live variable. + """ + try: + if live_var.isChecked(): + if not timer.isActive(): + timer.start(self.timerValue) + elif timer.isActive(): + timer.stop() + except Exception as e: + logging.error(e) + self.handle_error(f"Live Variable: {e}") + + @pyqtSlot() + def update_scaled_value(self, scaling_var, value_var, scaled_value_var, offset_var): + """Updates the scaled value based on the provided scaling factor and offset. + + Args: + scaling_var : Input field for the scaling factor. + value_var : Input field for the raw value. + scaled_value_var : Input field for the scaled value. + offset_var : Input field for the offset. + """ + scaling_text = scaling_var.text() + value_text = value_var.text() + offset_text = offset_var.text() + try: + value = float(value_text) + if offset_text.startswith("-"): + float_offset = float(offset_text.lstrip("-")) + offset = -1 * float_offset + else: + offset = float(offset_text) + if scaling_text.startswith("-"): + float_scaling = float(scaling_text.lstrip("-")) + scaling = -1 * float_scaling + else: + scaling = float(scaling_text) + scaled_value = (scaling * value) + offset + scaled_value_var.setText("{:.2f}".format(scaled_value)) + except Exception as e: + logging.error(e) + self.handle_error(f"Error update Scaled Value: {e}") + + def plot_data_update(self): + """Updates the data for plotting.""" + try: + timestamp = datetime.now() + if len(self.plot_data) > 0: + last_timestamp = self.plot_data[-1][0] + time_diff = ( + timestamp - last_timestamp + ).total_seconds() * 1000 # to convert time in ms. + else: + time_diff = 0 + + def safe_float(value): + try: + return float(value) + except ValueError: + return 0.0 + + self.plot_data.append( + ( + timestamp, + time_diff, + safe_float(self.ScaledValue_var1.text()), + safe_float(self.ScaledValue_var2.text()), + safe_float(self.ScaledValue_var3.text()), + safe_float(self.ScaledValue_var4.text()), + safe_float(self.ScaledValue_var5.text()), + ) + ) + except Exception as e: + logging.error(e) + + def update_watch_plot(self): + """Updates the plot in the WatchView tab with new data.""" + try: + if not self.plot_data: + return + + data = np.array(self.plot_data, dtype=object).T + time_diffs = np.array(data[1], dtype=float) + values = [np.array(data[i], dtype=float) for i in range(2, 7)] + + # Keep the last plot lines to avoid clearing and recreate them + plot_lines = {} + for item in self.watch_plot_widget.plotItem.items: + if isinstance(item, pg.PlotDataItem): + plot_lines[item.name()] = item + + for i, (value, combo_box, plot_var) in enumerate( + zip(values, self.combo_boxes, self.plot_checkboxes) + ): + if plot_var.isChecked() and combo_box.currentIndex() != 0: + if combo_box.currentText() in plot_lines: + plot_line = plot_lines[combo_box.currentText()] + plot_line.setData(np.cumsum(time_diffs), value) + else: + self.watch_plot_widget.plot( + np.cumsum(time_diffs), + value, + pen=pg.mkPen(color=self.plot_colors[i], width=2), + # Thicker plot line + name=combo_box.currentText(), + ) + + self.watch_plot_widget.setLabel("left", "Value") + self.watch_plot_widget.setLabel("bottom", "Time", units="ms") + self.watch_plot_widget.showGrid(x=True, y=True) # Enable grid lines + except Exception as e: + logging.error(e) + + def plot_data_plot(self): + """Initializes and starts data plotting.""" + try: + if not self.plot_data: + return + + self.update_watch_plot() + self.update_scope_plot() + + if not self.plot_window_open: + self.plot_window_open = True + except Exception as e: + logging.error(e) + + def update_scope_plot(self): + """Updates the plot in the ScopeView tab with new data and scaling.""" + try: + if not self.sampling_active: + return + + if not self.x2cscope.is_scope_data_ready(): + return + + data_storage = {} + for channel, data in self.x2cscope.get_scope_channel_data().items(): + data_storage[channel] = data + + self.scope_plot_widget.clear() + + for i, (channel, data) in enumerate(data_storage.items()): + checkbox_state = self.scope_channel_checkboxes[i].isChecked() + logging.debug( + f"Channel {channel}: Checkbox is {'checked' if checkbox_state else 'unchecked'}" + ) + if checkbox_state: # Check if the checkbox is checked + scale_factor = float( + self.scope_scaling_boxes[i].text() + ) # Get the scaling factor + # time_values = self.real_sampletime # Generate time values in ms + # start = self.real_sampletime / len(data) + start = 0 + time_values = np.linspace(start, self.real_sampletime, len(data)) + data_scaled = ( + np.array(data, dtype=float) * scale_factor + ) # Apply the scaling factor + self.scope_plot_widget.plot( + time_values, + data_scaled, + pen=pg.mkPen(color=self.plot_colors[i], width=2), + name=f"Channel {channel}", + ) + logging.debug( + f"Plotting channel {channel} with color {self.plot_colors[i]}" + ) + else: + logging.debug(f"Not plotting channel {channel}") + self.scope_plot_widget.setLabel("left", "Value") + self.scope_plot_widget.setLabel("bottom", "Time", units="ms") + self.scope_plot_widget.showGrid(x=True, y=True) + except Exception as e: + error_message = f"Error updating scope plot: {e}" + logging.error(error_message) + self.handle_error(error_message) + + def handle_error(self, error_message: str): + """Displays an error message in a message box. + + Args: + error_message (str): The error message to display. + """ + msg_box = QMessageBox(self) + msg_box.setWindowTitle("Error") + msg_box.setText(error_message) + msg_box.setStandardButtons(QMessageBox.Ok) + msg_box.exec_() + + def sampletime_edit(self): + """Handles the editing of the sample time value.""" + try: + new_sample_time = int(self.sampletime.text()) + if new_sample_time != self.timerValue: + self.timerValue = new_sample_time + for timer in self.timer_list: + if timer.isActive(): + timer.start(self.timerValue) + except ValueError as e: + logging.error(e) + self.handle_error(f"Invalid sample time: {e}") + + @pyqtSlot() + def handle_var_update(self, counter, value_var): + """Handles the update of variable values from the microcontroller. + + Args: + counter: The variable to update. + value_var (QLineEdit): The input field to display the updated value. + """ + try: + if counter is not None: + counter = self.x2cscope.get_variable(counter) + value = counter.get_value() + value_var.setText(str(value)) + if value_var == self.Value_var1: + self.slider_var1.setValue(int(value)) + self.plot_data_update() + except Exception as e: + error_message = f"Error: {e}" + logging.error(error_message) + self.handle_error(error_message) + + def slider_var1_changed(self, value): + """Handles the change in slider value for Variable 1. + + Args: + value (int): The new value of the slider. + """ + if self.combo_box1.currentIndex() == 0: + self.handle_error("Select Variable") + else: + self.Value_var1.setText(str(value)) + self.update_scaled_value( + self.Scaling_var1, + self.Value_var1, + self.ScaledValue_var1, + self.offset_var1, + ) + self.handle_variable_putram(self.combo_box1.currentText(), self.Value_var1) + + @pyqtSlot() + def handle_variable_getram(self, variable, value_var): + """Handle the retrieval of values from RAM for the specified variable. + + Args: + variable: The variable to retrieve the value for. + value_var: The QLineEdit widget to display the retrieved value. + """ + try: + current_variable = variable + + for index, combo_box in enumerate(self.combo_boxes): + if combo_box.currentText() == current_variable: + self.selected_var_indices[index] = combo_box.currentIndex() + + if current_variable and current_variable != "None": + counter = self.x2cscope.get_variable(current_variable) + value = counter.get_value() + value_var.setText(str(value)) + if value_var == self.Value_var1: + self.slider_var1.setValue(int(value)) + + if current_variable not in self.selected_variables: + self.selected_variables.append(current_variable) + + except Exception as e: + error_message = f"Error: {e}" + logging.error(error_message) + self.handle_error(error_message) + + @pyqtSlot() + def handle_variable_putram(self, variable, value_var): + """Handle the writing of values to RAM for the specified variable. + + Args: + variable: The variable to write the value to. + value_var: The QLineEdit widget to get the value from. + """ + try: + current_variable = variable + value = float(value_var.text()) + + if current_variable and current_variable != "None": + counter = self.x2cscope.get_variable(current_variable) + counter.set_value(value) + + except Exception as e: + error_message = f"Error: {e}" + logging.error(error_message) + self.handle_error(error_message) + + @pyqtSlot() + def select_elf_file(self): + """Open a file dialog to select an ELF file. + + This method opens a file dialog for the user to select an ELF file. + The selected file path is then stored in settings for later use. + """ + file_dialog = QFileDialog() + file_dialog.setNameFilter("ELF Files (*.elf)") + file_dialog.setFileMode(QFileDialog.ExistingFile) + fileinfo = QFileInfo(self.file_path) + self.select_file_button.setText(fileinfo.fileName()) + + if self.file_path: + file_dialog.setDirectory(self.file_path) + if file_dialog.exec_(): + selected_files = file_dialog.selectedFiles() + if selected_files: + self.file_path = selected_files[0] + self.settings.setValue("file_path", self.file_path) + self.select_file_button.setText(QFileInfo(self.file_path).fileName()) + + def refresh_combo_box(self): + """Refresh the contents of the variable selection combo boxes. + + This method repopulates the combo boxes used for variable selection + with the updated list of variables. + """ + if self.VariableList is not None: + for index, combo_box in enumerate(self.combo_boxes): + selected_index = self.selected_var_indices[index] + current_selected_text = combo_box.currentText() + + combo_box.clear() + combo_box.addItems(self.VariableList) + + if current_selected_text in self.VariableList: + combo_box.setCurrentIndex(combo_box.findText(current_selected_text)) + else: + combo_box.setCurrentIndex(selected_index) + + for combo in self.scope_var_combos: + combo.clear() + combo.addItems(self.VariableList) + else: + logging.warning("VariableList is None. Unable to refresh combo boxes.") + + def refresh_ports(self): + """Refresh the list of available serial ports. + + This method updates the combo box containing the list of available + serial ports to reflect the current state of the system. + """ + available_ports = [port.device for port in serial.tools.list_ports.comports()] + self.port_combo.clear() + self.port_combo.addItems(available_ports) + + @pyqtSlot() + def toggle_connection(self): + """Handle the connection or disconnection of the serial port. + + This method establishes or terminates the serial connection based on + the current state of the connection. + """ + if self.file_path == "": + QMessageBox.warning(self, "Error", "Please select an ELF file.") + self.select_elf_file() + return + + if self.ser is None or not self.ser.is_open: + for timer in self.timer_list: + if timer.isActive(): + timer.stop() + self.plot_data.clear() + self.connect_serial() + else: + self.disconnect_serial() + + def disconnect_serial(self): + """Disconnect the current serial connection. + + This method safely terminates the existing serial connection, if any, + and updates the UI to reflect the disconnection. + """ + try: + if self.ser is not None and self.ser.is_open: + self.ser.stop() + self.ser = None + + self.Connect_button.setText("Connect") + self.Connect_button.setEnabled(True) + self.select_file_button.setEnabled(True) + widget_list = [self.port_combo, self.baud_combo] + + for widget in widget_list: + widget.setEnabled(True) + + for combo_box in self.combo_boxes: + combo_box.setEnabled(False) + + for live_var in self.live_checkboxes: + live_var.setEnabled(False) + + self.slider_var1.setEnabled(False) + for timer in self.timer_list: + if timer.isActive(): + timer.stop() + + self.plot_update_timer.stop() # Stop the continuous plot update + + except Exception as e: + error_message = f"Error while disconnecting: {e}" + logging.error(error_message) + self.handle_error(error_message) + + def connect_serial(self): + """Establish a serial connection based on the current UI settings. + + This method sets up a serial connection using the selected port and + baud rate. It also initializes the variable factory and updates the + UI to reflect the connection state. + """ + try: + if self.ser is not None and self.ser.is_open: + self.disconnect_serial() + + port = self.port_combo.currentText() + baud_rate = int(self.baud_combo.currentText()) + + self.x2cscope = X2CScope( + port=port, elf_file=self.file_path, baud_rate=baud_rate + ) + self.ser = self.x2cscope.interface + self.VariableList = self.x2cscope.list_variables() + if self.VariableList: + self.VariableList.insert(0, "None") + else: + return + self.refresh_combo_box() + logging.info("Serial Port Configuration:") + logging.info(f"Port: {port}") + logging.info(f"Baud Rate: {baud_rate}") + + self.Connect_button.setText("Disconnect") + self.Connect_button.setEnabled(True) + + widget_list = [self.port_combo, self.baud_combo, self.select_file_button] + + for widget in widget_list: + widget.setEnabled(False) + + for combo_box in self.combo_boxes: + combo_box.setEnabled(True) + self.slider_var1.setEnabled(True) + + for live_var in self.live_checkboxes: + live_var.setEnabled(True) + + timer_list = [] + for i in range(len(self.live_checkboxes)): + timer_list.append((self.live_checkboxes[i], self.timer_list[i])) + + for live_var, timer in timer_list: + if live_var.isChecked(): + timer.start(self.timerValue) + + self.plot_update_timer.start( + self.timerValue + ) # Start the continuous plot update + + except Exception as e: + error_message = f"Error while connecting: {e}" + logging.error(error_message) + self.handle_error(error_message) + + def close_plot_window(self): + """Close the plot window if it is open. + + This method stops the animation and closes the plot window, if it is open. + """ + self.plot_window_open = False + + def close_event(self, event): + """Handle the event when the main window is closed. + + Args: + event: The close event. + + This method ensures that all resources are properly released and the + application is closed cleanly. + """ + if self.sampling_active: + self.sampling_active = False + if self.ser: + self.disconnect_serial() + event.accept() + + def start_sampling(self): + """Start the sampling process.""" + try: + if self.sampling_active: + self.sampling_active = False + self.scope_sample_button.setText("Sample") + logging.info("Stopped sampling.") + else: + self.x2cscope.clear_all_scope_channel() + for combo in self.scope_var_combos: + variable_name = combo.currentText() + if variable_name and variable_name != "None": + variable = self.x2cscope.get_variable(variable_name) + self.x2cscope.add_scope_channel(variable) + + self.x2cscope.set_sample_time( + int(self.sample_time_factor.text()) + ) # set sample time factor + self.sampling_active = True + self.configure_trigger() + self.scope_sample_button.setText("Stop") + logging.info("Started sampling.") + self.x2cscope.request_scope_data() + self.sample_scope_data( + single_shot=self.single_shot_checkbox.isChecked() + ) + except Exception as e: + error_message = f"Error starting sampling: {e}" + logging.error(error_message) + self.handle_error(error_message) + + def configure_trigger(self): + """Configure the trigger settings.""" + try: + if self.triggerVariable is not None: + variable_name = self.triggerVariable + variable = self.x2cscope.get_variable(variable_name) + + # Handle empty string for trigger level and delay + trigger_level_text = self.trigger_level_edit.text().strip() + trigger_delay_text = self.trigger_delay_edit.text().strip() + + if not trigger_level_text: + trigger_level = 0 + else: + try: + trigger_level = float(trigger_level_text) + logging.debug(trigger_level) + except ValueError: + logging.error( + f"Invalid trigger level value: {trigger_level_text}" + ) + self.handle_error( + f"Invalid trigger level value: {trigger_level_text}" + ) + return + + if not trigger_delay_text: + trigger_delay = 0 + else: + try: + trigger_delay = int(trigger_delay_text) + except ValueError: + logging.error( + f"Invalid trigger delay value: {trigger_delay_text}" + ) + self.handle_error( + f"Invalid trigger delay value: {trigger_delay_text}" + ) + return + + trigger_edge = ( + 0 if self.trigger_edge_combo.currentText() == "Rising" else 1 + ) + trigger_mode = ( + 0 if self.trigger_mode_combo.currentText() == "Auto" else 1 + ) + + trigger_config = TriggerConfig( + variable=variable, + trigger_level=trigger_level, + trigger_mode=trigger_mode, + trigger_delay=trigger_delay, + trigger_edge=trigger_edge, + ) + self.x2cscope.set_scope_trigger(trigger_config) + logging.info("Trigger configured.") + except Exception as e: + error_message = f"Error configuring trigger: {e}" + logging.error(error_message) + self.handle_error(error_message) + + def sample_scope_data(self, single_shot=False): + """Sample the scope data.""" + try: + while self.sampling_active: + if self.x2cscope.is_scope_data_ready(): + logging.info("Scope data is ready.") + + data_storage = {} + for channel, data in self.x2cscope.get_scope_channel_data( + valid_data=False + ).items(): + data_storage[channel] = data + + self.scope_plot_widget.clear() + for i, (channel, data) in enumerate(data_storage.items()): + if self.scope_channel_checkboxes[ + i + ].isChecked(): # Check if the channel is enabled + time_values = np.array( + [j * 0.001 for j in range(len(data))], dtype=float + ) # milliseconds + data_scaled = np.array(data, dtype=float) + self.scope_plot_widget.plot( + time_values, + data_scaled, + pen=pg.mkPen(color=self.plot_colors[i], width=2), + name=f"Channel {channel}", + ) + + self.scope_plot_widget.setLabel("left", "Value") + self.scope_plot_widget.setLabel("bottom", "Time", units="ms") + self.scope_plot_widget.showGrid(x=True, y=True) # Enable grid lines + + if single_shot: + break + + self.x2cscope.request_scope_data() + logging.debug("Requested next scope data.") + + QApplication.processEvents() # Keep the GUI responsive + + time.sleep(0.1) + + self.sampling_active = False + self.scope_sample_button.setText("Sample") + logging.info("Data collection complete.") + except Exception as e: + error_message = f"Error sampling scope data: {e}" + logging.error(error_message) + self.handle_error(error_message) + + +if __name__ == "__main__": + app = QApplication(sys.argv) + ex = X2cscopeGui() + ex.show() + sys.exit(app.exec_()) diff --git a/pyx2cscope/gui/generic_gui/generic_gui.py b/pyx2cscope/gui/generic_gui/generic_gui.py new file mode 100644 index 00000000..47ef675c --- /dev/null +++ b/pyx2cscope/gui/generic_gui/generic_gui.py @@ -0,0 +1,2345 @@ +"""replicate of X2Cscpope.""" + +import logging + +logging.basicConfig(level=logging.ERROR) +import json +import os +import sys +import time +from collections import deque +from datetime import datetime + +import matplotlib +import numpy as np +import pyqtgraph as pg # Added pyqtgraph for interactive plotting +import serial.tools.list_ports # Import the serial module to fix the NameError +from PyQt5 import QtCore, QtGui +from PyQt5.QtCore import QFileInfo, QMutex, QRegExp, QSettings, Qt, QTimer, pyqtSlot +from PyQt5.QtGui import QIcon, QRegExpValidator +from PyQt5.QtWidgets import ( + QApplication, + QCheckBox, + QComboBox, + QDialog, + QDialogButtonBox, + QFileDialog, + QGridLayout, + QGroupBox, + QHBoxLayout, + QLabel, + QLineEdit, + QListWidget, + QMainWindow, + QMessageBox, + QPushButton, + QScrollArea, + QSizePolicy, + QSlider, + QStyleFactory, + QTabWidget, + QVBoxLayout, + QWidget, +) + +from pyx2cscope.gui import img as img_src +from pyx2cscope.x2cscope import TriggerConfig, X2CScope + +logging.basicConfig(level=logging.DEBUG) + +matplotlib.use("QtAgg") # This sets the backend to Qt for Matplotlib + + +class VariableSelectionDialog(QDialog): + """Initialize the variable selection dialog. + + Args: + variables (list): A list of available variables to select from. + parent (QWidget): The parent widget. + """ + + def __init__(self, variables, parent=None): + """Set up the user interface components for the variable selection dialog.""" + super().__init__(parent) + self.variables = variables + self.selected_variable = None + + self.init_ui() + + def init_ui(self): + """Initializing UI component.""" + self.setWindowTitle("Search Variable") + self.setMinimumSize(300, 400) + + self.layout = QVBoxLayout() + + self.search_bar = QLineEdit(self) + self.search_bar.setPlaceholderText("Search...") + self.search_bar.textChanged.connect(self.filter_variables) + self.layout.addWidget(self.search_bar) + + self.variable_list = QListWidget(self) + self.variable_list.addItems(self.variables) + self.variable_list.itemDoubleClicked.connect(self.accept_selection) + self.layout.addWidget(self.variable_list) + + self.button_box = QDialogButtonBox( + QDialogButtonBox.Ok | QDialogButtonBox.Cancel, self + ) + self.button_box.accepted.connect(self.accept_selection) + self.button_box.rejected.connect(self.reject) + self.layout.addWidget(self.button_box) + + self.setLayout(self.layout) + + def filter_variables(self, text): + """Filter the variables based on user input in the search bar. + + Args: + text (str): The input text to filter variables. + """ + self.variable_list.clear() + filtered_variables = [ + var for var in self.variables if text.lower() in var.lower() + ] + self.variable_list.addItems(filtered_variables) + + def accept_selection(self): + """Accept the selection when a variable is chosen from the list. + + The selected variable is set as the `selected_variable` and the dialog is accepted. + """ + selected_items = self.variable_list.selectedItems() + if selected_items: + self.selected_variable = selected_items[0].text() + self.accept() + + +class X2cscopeGui(QMainWindow): + """Main GUI class for the pyX2Cscope application.""" + + def __init__(self, *args, **kwargs): + """Initializing all the elements required.""" + super().__init__() + self.x2cscope = None # Ensures it is initialized to None at start + self.x2cscope_initialized = ( + False # Flag to ensure error message is shown only once + ) + self.last_error_time = ( + None # Attribute to track the last time an error was shown + ) + self.triggerVariable = None + self.elf_file_loaded = False + self.config_file_loaded = False + self.device_info_labels = {} # Dictionary to hold the device information labels + self.initialize_variables() + self.init_ui() + + def initialize_variables(self): + """Initialize instance variables.""" + self.timeout = 5 + self.sampling_active = False + self.scaling_edits_tab3 = [] # Track scaling fields for Tab 3 + self.offset_edits_tab3 = [] # Track offset fields for Tab 3 + self.scaled_value_edits_tab3 = [] # Track scaled value fields for Tab 3 + self.offset_boxes = None + self.plot_checkboxes = None + self.scaled_value_boxes = None + self.scaling_boxes = None + self.Value_var_boxes = None + self.line_edit_boxes = None + self.live_checkboxes = None + self.timer_list = None + self.VariableList = [] + self.old_Variable_list = [] + self.var_factory = None + self.ser = None + self.timerValue = 500 + self.port_combo = QComboBox() + self.layout = None + self.slider_var1 = QSlider(Qt.Horizontal) + self.plot_button = QPushButton("Plot") + self.mutex = QMutex() + self.grid_layout = QGridLayout() + self.box_layout = QHBoxLayout() + self.timer1 = QTimer() + self.timer2 = QTimer() + self.timer3 = QTimer() + self.timer4 = QTimer() + self.timer5 = QTimer() + self.plot_update_timer = QTimer() # Timer for continuous plot update + self.timer() + self.offset_var() + self.plot_var_check() + self.scaling_var() + self.value_var() + self.live_var() + self.scaled_value() + self.line_edit() + self.sampletime = QLineEdit() + self.unit_var() + self.Connect_button = QPushButton("Connect") + self.baud_combo = QComboBox() + self.select_file_button = QPushButton("Select elf file") + self.error_shown = False + self.plot_window_open = False + self.settings = QSettings("MyCompany", "MyApp") + self.file_path: str = self.settings.value("file_path", "", type=str) + self.initi_variables() + + def initi_variables(self): + """Some variable initialisation.""" + self.device_info_labels = { + "processor_id": QLabel("Loading Processor ID ..."), + "uc_width": QLabel("Loading UC Width..."), + "date": QLabel("Loading Date..."), + "time": QLabel("Loading Time..."), + "appVer": QLabel("Loading App Version..."), + "dsp_state": QLabel("Loading DSP State..."), + } + self.selected_var_indices = [ + 0, + 0, + 0, + 0, + 0, + ] # List to store selected variable indices + self.selected_variables = [] # List to store selected variables + self.previous_selected_variables = {} # Dictionary to store previous selections + decimal_regex = QRegExp("-?[0-9]+(\\.[0-9]+)?") + self.decimal_validator = QRegExpValidator(decimal_regex) + + self.plot_data = deque(maxlen=250) # Store plot data for all variables + self.plot_colors = [ + "b", + "g", + "r", + "c", + "m", + "y", + "k", + ] # colours for different plot + # Add self.labels on top + self.labels = [ + "Live", + "Variable", + "Value", + "Scaling", + "Offset", + "Scaled Value", + "Unit", + "Plot", + ] + + def init_ui(self): + """Initialize the user interface.""" + self.setup_application_style() + self.create_central_widget() + self.create_tabs() + self.setup_tabs() + self.setup_window_properties() + # self.setup_device_info_ui() # Set up the device information section + self.refresh_ports() + + def setup_application_style(self): + """Set the application style.""" + QApplication.setStyle(QStyleFactory.create("Fusion")) + + def create_central_widget(self): + """Create the central widget.""" + central_widget = QWidget(self) + self.setCentralWidget(central_widget) + self.layout = QVBoxLayout(central_widget) + self.tab_widget = QTabWidget() + self.layout.addWidget(self.tab_widget) + + def update_device_info(self): + """Fetch device information from the connected device and update the labels.""" + try: + device_info = self.x2cscope.get_device_info() + self.device_info_labels["processor_id"].setText( + f" {device_info['processor_id']}" + ) + self.device_info_labels["uc_width"].setText(f"{device_info['uc_width']}") + self.device_info_labels["date"].setText(f"{device_info['date']}") + self.device_info_labels["time"].setText(f"{device_info['time']}") + self.device_info_labels["appVer"].setText(f"{device_info['AppVer']}") + self.device_info_labels["dsp_state"].setText(f"{device_info['dsp_state']}") + except Exception as e: + self.handle_error(f"Error fetching device info: {e}") + + def create_tabs(self): + """Create tabs for the main window.""" + self.tab1 = QWidget() + self.tab2 = QWidget() + self.tab3 = QWidget() # New tab for WatchView Only + self.tab_widget.addTab(self.tab1, "WatchPlot") + self.tab_widget.addTab(self.tab2, "ScopeView") + self.tab_widget.addTab(self.tab3, "WatchView") # Add third tab + + def setup_tabs(self): + """Set up the contents of each tab.""" + self.setup_tab1() + self.setup_tab2() + self.setup_tab3() # Setup for the third tab + + def setup_window_properties(self): + """Set up the main window properties.""" + self.setWindowTitle("pyX2Cscope") + mchp_img = os.path.join(os.path.dirname(img_src.__file__), "pyx2cscope.jpg") + self.setWindowIcon(QtGui.QIcon(mchp_img)) + + def line_edit(self): + """Initializing line edits.""" + self.line_edit5 = QLineEdit() + self.line_edit4 = QLineEdit() + self.line_edit3 = QLineEdit() + self.line_edit2 = QLineEdit() + self.line_edit1 = QLineEdit() + + for line_edit in [ + self.line_edit1, + self.line_edit2, + self.line_edit3, + self.line_edit4, + self.line_edit5, + ]: + line_edit.setReadOnly(True) + line_edit.setPlaceholderText("Search Variable") + line_edit.installEventFilter(self) + + def scaled_value(self): + """Initializing Scaled variable.""" + self.ScaledValue_var1 = QLineEdit(self) + self.ScaledValue_var2 = QLineEdit(self) + self.ScaledValue_var3 = QLineEdit(self) + self.ScaledValue_var4 = QLineEdit(self) + self.ScaledValue_var5 = QLineEdit(self) + + def live_var(self): + """Initializing live variable.""" + self.Live_var1 = QCheckBox(self) + self.Live_var2 = QCheckBox(self) + self.Live_var3 = QCheckBox(self) + self.Live_var4 = QCheckBox(self) + self.Live_var5 = QCheckBox(self) + + def value_var(self): + """Initializing value variable.""" + self.Value_var1 = QLineEdit(self) + self.Value_var2 = QLineEdit(self) + self.Value_var3 = QLineEdit(self) + self.Value_var4 = QLineEdit(self) + self.Value_var5 = QLineEdit(self) + + def timer(self): + """Initializing timers.""" + self.timers = [QTimer() for _ in range(5)] + + def offset_var(self): + """Initializing Offset Variable.""" + self.offset_var1 = QLineEdit() + self.offset_var2 = QLineEdit() + self.offset_var3 = QLineEdit() + self.offset_var4 = QLineEdit() + self.offset_var5 = QLineEdit() + + def plot_var_check(self): + """Initializing plot variable check boxes.""" + self.plot_var5_checkbox = QCheckBox() + self.plot_var2_checkbox = QCheckBox() + self.plot_var4_checkbox = QCheckBox() + self.plot_var3_checkbox = QCheckBox() + self.plot_var1_checkbox = QCheckBox() + + def scaling_var(self): + """Initializing Scaling variable.""" + self.Scaling_var1 = QLineEdit(self) + self.Scaling_var2 = QLineEdit(self) + self.Scaling_var3 = QLineEdit(self) + self.Scaling_var4 = QLineEdit(self) + self.Scaling_var5 = QLineEdit(self) + + def unit_var(self): + """Initializing unit variable.""" + self.Unit_var1 = QLineEdit(self) + self.Unit_var2 = QLineEdit(self) + self.Unit_var3 = QLineEdit(self) + self.Unit_var4 = QLineEdit(self) + self.Unit_var5 = QLineEdit(self) + + # noinspection PyUnresolvedReferences + def setup_tab1(self): + """Set up the first tab with the original functionality.""" + self.tab1.layout = QVBoxLayout() + self.tab1.setLayout(self.tab1.layout) + + grid_layout = QGridLayout() + self.tab1.layout.addLayout(grid_layout) + + self.setup_port_layout(grid_layout) + # self.setup_baud_layout(grid_layout) + self.setup_sampletime_layout(grid_layout) + self.setup_variable_layout(grid_layout) + self.setup_connections() + + # Add Save and Load buttons + self.save_button_watch = QPushButton("Save Config") + self.load_button_watch = QPushButton("Load Config") + self.save_button_watch.setFixedSize(100, 30) + self.load_button_watch.setFixedSize(100, 30) + self.save_button_watch.clicked.connect(self.save_config) + self.load_button_watch.clicked.connect(self.load_config) + + button_layout = QHBoxLayout() + button_layout.addWidget(self.save_button_watch) + button_layout.addWidget(self.load_button_watch) + self.tab1.layout.addLayout(button_layout) + + def setup_tab2(self): + """Set up the second tab with the scope functionality.""" + self.tab2.layout = QVBoxLayout() + self.tab2.setLayout(self.tab2.layout) + + main_grid_layout = QGridLayout() + self.tab2.layout.addLayout(main_grid_layout) + + # Set up individual components + trigger_group = self.create_trigger_configuration_group() + variable_group = self.create_variable_selection_group() + self.scope_plot_widget = self.create_scope_plot_widget() + button_layout = self.create_save_load_buttons() + + # Add the group boxes to the main layout with stretch factors + main_grid_layout.addWidget(trigger_group, 0, 0) + main_grid_layout.addWidget(variable_group, 0, 1) + + # Set the column stretch factors to make the variable group larger + main_grid_layout.setColumnStretch(0, 1) # Trigger configuration box + main_grid_layout.setColumnStretch(1, 3) # Variable selection box + + # Add the plot widget for scope view + self.tab2.layout.addWidget(self.scope_plot_widget) + + # Add Save and Load buttons + self.tab2.layout.addLayout(button_layout) + + def create_trigger_configuration_group(self): + """Create the trigger configuration group box.""" + trigger_group = QGroupBox("Trigger Configuration") + trigger_layout = QVBoxLayout() + trigger_group.setLayout(trigger_layout) + + grid_layout_trigger = QGridLayout() + trigger_layout.addLayout(grid_layout_trigger) + + self.single_shot_checkbox = QCheckBox("Single Shot") + self.sample_time_factor = QLineEdit("1") + self.sample_time_factor.setValidator(self.decimal_validator) + self.trigger_mode_combo = QComboBox() + self.trigger_mode_combo.addItems(["Auto", "Triggered"]) + self.trigger_edge_combo = QComboBox() + self.trigger_edge_combo.addItems(["Rising", "Falling"]) + self.trigger_level_edit = QLineEdit("0") + self.trigger_level_edit.setValidator(self.decimal_validator) + self.trigger_delay_edit = QLineEdit("0") + self.trigger_delay_edit.setValidator(self.decimal_validator) + + self.scope_sampletime_edit = QLineEdit("50") # Default sample time in microseconds + self.scope_sampletime_edit.setValidator(self.decimal_validator) + + # Total Time + self.total_time_label = QLabel("Total Time (ms):") + self.total_time_value = QLineEdit("0") + self.total_time_value.setReadOnly(True) + + self.scope_sample_button = QPushButton("Sample") + self.scope_sample_button.setFixedSize(100, 30) + self.scope_sample_button.clicked.connect(self.start_sampling) + + # Arrange widgets in grid layout + grid_layout_trigger.addWidget(self.single_shot_checkbox, 0, 0, 1, 2) + grid_layout_trigger.addWidget(QLabel("Sample Time Factor"), 1, 0) + grid_layout_trigger.addWidget(self.sample_time_factor, 1, 1) + grid_layout_trigger.addWidget(QLabel("Scope Sample Time (µs):"), 2, 0) + grid_layout_trigger.addWidget(self.scope_sampletime_edit, 2, 1) + grid_layout_trigger.addWidget(self.total_time_label, 3, 0) + grid_layout_trigger.addWidget(self.total_time_value, 3, 1) + grid_layout_trigger.addWidget(QLabel("Trigger Mode:"), 4, 0) + grid_layout_trigger.addWidget(self.trigger_mode_combo, 4, 1) + grid_layout_trigger.addWidget(QLabel("Trigger Edge:"), 5, 0) + grid_layout_trigger.addWidget(self.trigger_edge_combo, 5, 1) + grid_layout_trigger.addWidget(QLabel("Trigger Level:"), 6, 0) + grid_layout_trigger.addWidget(self.trigger_level_edit, 6, 1) + grid_layout_trigger.addWidget(QLabel("Trigger Delay:"), 7, 0) + grid_layout_trigger.addWidget(self.trigger_delay_edit, 7, 1) + grid_layout_trigger.addWidget(self.scope_sample_button, 8, 0, 1, 2) + + return trigger_group + + def create_variable_selection_group(self): + """Create the variable selection group box.""" + variable_group = QGroupBox("Variable Selection") + variable_layout = QVBoxLayout() + variable_group.setLayout(variable_layout) + + grid_layout_variable = QGridLayout() + variable_layout.addLayout(grid_layout_variable) + number_of_variables = 8 + self.scope_var_lines = [QLineEdit() for _ in range(number_of_variables)] + self.trigger_var_checkbox = [QCheckBox() for _ in range(number_of_variables)] + self.scope_channel_checkboxes = [QCheckBox() for _ in range(number_of_variables)] + self.scope_scaling_boxes = [QLineEdit("1") for _ in range(number_of_variables)] + + for checkbox in self.scope_channel_checkboxes: + checkbox.setChecked(True) + + for line_edit in self.scope_var_lines: + line_edit.setReadOnly(True) + line_edit.setPlaceholderText("Search Variable") + line_edit.installEventFilter(self) + + # Add "Search Variable" label + grid_layout_variable.addWidget(QLabel("Search Variable"), 0, 1) + grid_layout_variable.addWidget(QLabel("Trigger"), 0, 0) + grid_layout_variable.addWidget(QLabel("Gain"), 0, 2) + grid_layout_variable.addWidget(QLabel("Visible"), 0, 3) + + for i, (line_edit, trigger_checkbox, scale_box, show_checkbox) in enumerate( + zip( + self.scope_var_lines, + self.trigger_var_checkbox, + self.scope_scaling_boxes, + self.scope_channel_checkboxes, + ) + ): + line_edit.setMinimumHeight(20) + trigger_checkbox.setMinimumHeight(20) + show_checkbox.setMinimumHeight(20) + scale_box.setMinimumHeight(20) + scale_box.setFixedSize(50, 20) + scale_box.setValidator(self.decimal_validator) + + line_edit.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) + grid_layout_variable.addWidget(trigger_checkbox, i + 1, 0) + grid_layout_variable.addWidget(line_edit, i + 1, 1) + grid_layout_variable.addWidget(scale_box, i + 1, 2) + grid_layout_variable.addWidget(show_checkbox, i + 1, 3) + + trigger_checkbox.stateChanged.connect( + lambda state, x=i: self.handle_scope_checkbox_change(state, x) + ) + scale_box.editingFinished.connect(self.update_scope_plot) + show_checkbox.stateChanged.connect(self.update_scope_plot) + + return variable_group + + def create_scope_plot_widget(self): + """Create the scope plot widget.""" + scope_plot_widget = pg.PlotWidget(title="Scope Plot") + scope_plot_widget.setBackground("w") + scope_plot_widget.addLegend() + scope_plot_widget.showGrid(x=True, y=True) + scope_plot_widget.getViewBox().setMouseMode(pg.ViewBox.RectMode) + + return scope_plot_widget + + def create_save_load_buttons(self): + """Create the save and load buttons.""" + self.save_button_scope = QPushButton("Save Config") + self.load_button_scope = QPushButton("Load Config") + self.save_button_scope.setFixedSize(100, 30) + self.load_button_scope.setFixedSize(100, 30) + self.save_button_scope.clicked.connect(self.save_config) + self.load_button_scope.clicked.connect(self.load_config) + + button_layout = QHBoxLayout() + button_layout.addWidget(self.save_button_scope) + button_layout.addWidget(self.load_button_scope) + + return button_layout + + def handle_scope_checkbox_change(self, state, index): + """Handle the change in the state of the scope view checkboxes.""" + if state == Qt.Checked: + for i, checkbox in enumerate(self.trigger_var_checkbox): + if i != index: + checkbox.setChecked(False) + self.triggerVariable = self.scope_var_lines[index].text() + logging.debug(f"Checked variable: {self.scope_var_lines[index].text()}") + else: + self.triggerVariable = None + + def setup_port_layout(self, layout): + """Set up the port selection, baud rate, and device information layout in two sections.""" + # Create the left layout for device information (QVBoxLayout) + left_layout = QGridLayout() + + # Add device information labels to the left side + for label_key, label in self.device_info_labels.items(): + info_label = QLabel(label_key.replace("_", " ").capitalize() + ":") + info_label.setAlignment(Qt.AlignLeft) + + row = list(self.device_info_labels.keys()).index( + label_key + ) # Get the row index + device_info_layout = ( + QGridLayout() + ) # Create a row layout for label and its value + device_info_layout.addWidget(info_label, row, 0, Qt.AlignRight) + device_info_layout.addWidget(label, row, 1, alignment=Qt.AlignRight) + left_layout.addLayout(device_info_layout, row, 0, Qt.AlignLeft) + + # Create the right layout for COM port and settings (QGridLayout) + right_layout = QGridLayout() + + # COM Port Selection + port_label = QLabel("Select Port:") + self.port_combo.setFixedSize(100, 25) # Set fixed size for the port combo box + refresh_button = QPushButton() + refresh_button.setFixedSize(25, 25) + refresh_button.clicked.connect(self.refresh_ports) + refresh_img = os.path.join(os.path.dirname(img_src.__file__), "refresh.png") + refresh_button.setIcon(QIcon(refresh_img)) + + # Add COM Port widgets to the right layout + right_layout.addWidget(port_label, 0, 0, alignment=Qt.AlignRight) + right_layout.addWidget(self.port_combo, 0, 1) + right_layout.addWidget(refresh_button, 0, 2) + + # Baud Rate Selection + baud_label = QLabel("Select Baud Rate:") + self.baud_combo.setFixedSize(100, 25) + self.baud_combo.addItems(["38400", "115200", "230400", "460800", "921600"]) + default_baud_rate = "115200" + index = self.baud_combo.findText(default_baud_rate, Qt.MatchFixedString) + if index >= 0: + self.baud_combo.setCurrentIndex(index) + + # Add Baud Rate widgets to the right layout + right_layout.addWidget(baud_label, 1, 0, alignment=Qt.AlignRight) + right_layout.addWidget(self.baud_combo, 1, 1) + + # Add Connect button and Sample time + self.Connect_button.setFixedSize(100, 30) + self.Connect_button.clicked.connect(self.toggle_connection) + sampletime_label = QLabel("Sample Time WatchPlot:") + self.sampletime.setFixedSize(100, 30) + self.sampletime.setText("500") + + # Add Connect and Sample Time widgets to the right layout + self.sampletime.setValidator(self.decimal_validator) + self.sampletime.editingFinished.connect(self.sampletime_edit) + right_layout.addWidget(sampletime_label, 2, 0, alignment=Qt.AlignRight) + right_layout.addWidget(self.sampletime, 2, 1, alignment=Qt.AlignLeft) + right_layout.addWidget(QLabel("ms"), 2, 2, alignment=Qt.AlignLeft) + right_layout.addWidget(self.Connect_button, 3, 1, alignment=Qt.AlignBottom) + + # Create a horizontal layout to contain both left and right sections + horizontal_layout = QHBoxLayout() + + # Add left (device information) and right (settings) layouts to the horizontal layout + horizontal_layout.addLayout(left_layout) + horizontal_layout.addLayout(right_layout) + + # Finally, add the horizontal layout to the grid at a specific row and column + layout.addLayout(horizontal_layout, 0, 0, 1, 2) # Span 1 row and 2 columns + + def setup_sampletime_layout(self, layout): + """Set up the sample time layout.""" + # self.Connect_button.clicked.connect(self.toggle_connection) + self.select_file_button.clicked.connect(self.select_elf_file) + layout.addWidget(self.select_file_button, 4, 0) + + def setup_variable_layout(self, layout): + """Set up the variable selection layout.""" + self.timer_list = [ + self.timer1, + self.timer2, + self.timer3, + self.timer4, + self.timer5, + ] + + for col, label in enumerate(self.labels): + self.grid_layout.addWidget(QLabel(label), 0, col) + + self.live_checkboxes = [ + self.Live_var1, + self.Live_var2, + self.Live_var3, + self.Live_var4, + self.Live_var5, + ] + self.line_edit_boxes = [ + self.line_edit1, + self.line_edit2, + self.line_edit3, + self.line_edit4, + self.line_edit5, + ] + self.Value_var_boxes = [ + self.Value_var1, + self.Value_var2, + self.Value_var3, + self.Value_var4, + self.Value_var5, + ] + self.scaling_boxes = [ + self.Scaling_var1, + self.Scaling_var2, + self.Scaling_var3, + self.Scaling_var4, + self.Scaling_var5, + ] + self.scaled_value_boxes = [ + self.ScaledValue_var1, + self.ScaledValue_var2, + self.ScaledValue_var3, + self.ScaledValue_var4, + self.ScaledValue_var5, + ] + unit_boxes = [ + self.Unit_var1, + self.Unit_var2, + self.Unit_var3, + self.Unit_var4, + self.Unit_var5, + ] + self.plot_checkboxes = [ + self.plot_var1_checkbox, + self.plot_var2_checkbox, + self.plot_var3_checkbox, + self.plot_var4_checkbox, + self.plot_var5_checkbox, + ] + self.offset_boxes = [ + self.offset_var1, + self.offset_var2, + self.offset_var3, + self.offset_var4, + self.offset_var5, + ] + + for row_index, ( + live_var, + line_edit, + value_var, + scaling_var, + offset_var, + scaled_value_var, + unit_var, + plot_checkbox, + ) in enumerate( + zip( + self.live_checkboxes, + self.line_edit_boxes, + self.Value_var_boxes, + self.scaling_boxes, + self.offset_boxes, + self.scaled_value_boxes, + unit_boxes, + self.plot_checkboxes, + ), + 1, + ): + live_var.setEnabled(False) + line_edit.setEnabled(False) + + # Set size policy for variable name (line_edit) and value field to expand + line_edit.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) + value_var.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) + + value_var.setText("0") + value_var.setValidator(self.decimal_validator) + scaling_var.setText("1") + offset_var.setText("0") + offset_var.setValidator(self.decimal_validator) + scaled_value_var.setText("0") + scaled_value_var.setValidator(self.decimal_validator) + display_row = row_index # Use a different variable name for the assignment + if display_row > 1: + display_row += 1 + self.grid_layout.addWidget(live_var, display_row, 0) + self.grid_layout.addWidget(line_edit, display_row, 1) + if display_row == 1: + self.grid_layout.addWidget(self.slider_var1, display_row + 1, 0, 1, 7) + + self.grid_layout.addWidget(value_var, display_row, 2) + self.grid_layout.addWidget(scaling_var, display_row, 3) + self.grid_layout.addWidget(offset_var, display_row, 4) + self.grid_layout.addWidget(scaled_value_var, display_row, 5) + self.grid_layout.addWidget(unit_var, display_row, 6) + self.grid_layout.addWidget(plot_checkbox, display_row, 7) + plot_checkbox.stateChanged.connect( + lambda state, x=row_index - 1: self.update_watch_plot() + ) + + layout.addLayout(self.grid_layout, 5, 0) + + # Adjust the column stretch factors to ensure proper resizing + self.grid_layout.setColumnStretch(1, 5) # Variable column expands more + self.grid_layout.setColumnStretch(2, 2) # Value column + self.grid_layout.setColumnStretch(3, 1) # Scaling column + self.grid_layout.setColumnStretch(4, 1) # Offset column + self.grid_layout.setColumnStretch(5, 2) # Scaled Value column + self.grid_layout.setColumnStretch(6, 1) # Unit column + + # Add the plot widget for watch view + self.watch_plot_widget = pg.PlotWidget(title="Watch Plot") + self.watch_plot_widget.setBackground("w") + self.watch_plot_widget.addLegend() # Add legend to the plot widget + self.watch_plot_widget.showGrid(x=True, y=True) # Enable grid lines + # Change to 1-button zoom mode + self.watch_plot_widget.getViewBox().setMouseMode(pg.ViewBox.RectMode) + self.tab1.layout.addWidget(self.watch_plot_widget) + + def setup_connections(self): + """Set up connections for various widgets.""" + self.plot_button.clicked.connect(self.plot_data_plot) + + for timer, line_edit, value_var in zip( + self.timer_list, self.line_edit_boxes, self.Value_var_boxes + ): + timer.timeout.connect( + lambda cb=line_edit, v_var=value_var: self.handle_var_update( + cb.text(), v_var + ) + ) + + for line_edit, value_var in zip(self.line_edit_boxes, self.Value_var_boxes): + value_var.editingFinished.connect( + lambda cb=line_edit, v_var=value_var: self.handle_variable_putram( + cb.text(), v_var + ) + ) + + self.connect_editing_finished() + + for timer, live_var in zip(self.timer_list, self.live_checkboxes): + live_var.stateChanged.connect( + lambda state, lv=live_var, tm=timer: self.var_live(lv, tm) + ) + + self.slider_var1.setMinimum(-32768) + self.slider_var1.setMaximum(32767) + self.slider_var1.setEnabled(False) + self.slider_var1.valueChanged.connect(self.slider_var1_changed) + + self.plot_update_timer.timeout.connect( + self.update_watch_plot + ) # Connect the QTimer to the update method + + def connect_editing_finished(self): + """Connect editingFinished signals for value and scaling inputs.""" + for ( + scaling, + value_var, + scaled_value, + offset, + ) in zip( + self.scaling_boxes, + self.Value_var_boxes, + self.scaled_value_boxes, + self.offset_boxes, + ): + + def connect_editing_finished( + sc_edit=scaling, + v_edit=value_var, + scd_edit=scaled_value, + off_edit=offset, + ): + def on_editing_finished(): + self.update_scaled_value(sc_edit, v_edit, scd_edit, off_edit) + + return on_editing_finished + + value_var.editingFinished.connect(connect_editing_finished()) + + for ( + scaling, + value_var, + scaled_value, + offset, + ) in zip( + self.scaling_boxes, + self.Value_var_boxes, + self.scaled_value_boxes, + self.offset_boxes, + ): + + def connect_editing_finished( + sc_edit=scaling, + v_edit=value_var, + scd_edit=scaled_value, + off_edit=offset, + ): + def on_editing_finished(): + self.update_scaled_value(sc_edit, v_edit, scd_edit, off_edit) + + return on_editing_finished + + scaling.editingFinished.connect(connect_editing_finished()) + + for ( + scaling, + value_var, + scaled_value, + offset, + ) in zip( + self.scaling_boxes, + self.Value_var_boxes, + self.scaled_value_boxes, + self.offset_boxes, + ): + + def connect_text_changed( + sc_edit=scaling, + v_edit=value_var, + scd_edit=scaled_value, + off_edit=offset, + ): + def on_text_changed(): + self.update_scaled_value(sc_edit, v_edit, scd_edit, off_edit) + + return on_text_changed + + value_var.textChanged.connect(connect_text_changed()) + + for ( + scaling, + value_var, + scaled_value, + offset, + ) in zip( + self.scaling_boxes, + self.Value_var_boxes, + self.scaled_value_boxes, + self.offset_boxes, + ): + + def connect_text_changed( + sc_edit=scaling, + v_edit=value_var, + scd_edit=scaled_value, + off_edit=offset, + ): + def on_text_changed(): + self.update_scaled_value(sc_edit, v_edit, scd_edit, off_edit) + + return on_text_changed + + offset.editingFinished.connect(connect_text_changed()) + + @pyqtSlot() + def var_live(self, live_var, timer): + """Handles the state change of live variable checkboxes. + + Args: + live_var (QCheckBox): The checkbox representing a live variable. + timer (QTimer): The timer associated with the live variable. + """ + try: + if live_var.isChecked(): + if not timer.isActive(): + timer.start(self.timerValue) + elif timer.isActive(): + timer.stop() + except Exception as e: + logging.error(e) + self.handle_error(f"Live Variable: {e}") + + @pyqtSlot() + def update_scaled_value(self, scaling_var, value_var, scaled_value_var, offset_var): + """Updates the scaled value based on the provided scaling factor and offset. + + Args: + scaling_var : Input field for the scaling factor. + value_var : Input field for the raw value. + scaled_value_var : Input field for the scaled value. + offset_var : Input field for the offset. + """ + scaling_text = scaling_var.text() + value_text = value_var.text() + offset_text = offset_var.text() + try: + value = float(value_text) + if offset_text.startswith("-"): + float_offset = float(offset_text.lstrip("-")) + offset = -1 * float_offset + else: + offset = float(offset_text) + if scaling_text.startswith("-"): + float_scaling = float(scaling_text.lstrip("-")) + scaling = -1 * float_scaling + else: + scaling = float(scaling_text) + scaled_value = (scaling * value) + offset + scaled_value_var.setText("{:.2f}".format(scaled_value)) + except Exception as e: + logging.error(e) + self.handle_error(f"Error update Scaled Value: {e}") + + def plot_data_update(self): + """Updates the data for plotting.""" + try: + timestamp = datetime.now() + if len(self.plot_data) > 0: + last_timestamp = self.plot_data[-1][0] + time_diff = ( + timestamp - last_timestamp + ).total_seconds() * 1000 # to convert time in ms. + else: + time_diff = 0 + + def safe_float(value): + try: + return float(value) + except ValueError: + return 0.0 + + self.plot_data.append( + ( + timestamp, + time_diff, + safe_float(self.ScaledValue_var1.text()), + safe_float(self.ScaledValue_var2.text()), + safe_float(self.ScaledValue_var3.text()), + safe_float(self.ScaledValue_var4.text()), + safe_float(self.ScaledValue_var5.text()), + ) + ) + except Exception as e: + logging.error(e) + + def update_watch_plot(self): + """Updates the plot in the WatchView tab with new data.""" + try: + if not self.plot_data: + return + + # Clear the plot and remove old labels + self.watch_plot_widget.clear() + + data = np.array(self.plot_data, dtype=object).T + time_diffs = np.array(data[1], dtype=float) + values = [np.array(data[i], dtype=float) for i in range(2, 7)] + + # Keep track of plot lines to avoid clearing and recreating them unnecessarily + for i, (value, line_edit, plot_var) in enumerate( + zip(values, self.line_edit_boxes, self.plot_checkboxes) + ): + # Check if the variable should be plotted and is not empty + if plot_var.isChecked() and line_edit.text() != "": + self.watch_plot_widget.plot( + np.cumsum(time_diffs), + value, + pen=pg.mkPen( + color=self.plot_colors[i], width=2 + ), # Thicker plot line + name=line_edit.text(), + ) + + # Reset plot labels + self.watch_plot_widget.setLabel("left", "Value") + self.watch_plot_widget.setLabel("bottom", "Time", units="ms") + self.watch_plot_widget.showGrid(x=True, y=True) # Enable grid lines + except Exception as e: + logging.error(e) + + def update_scope_plot(self): + """Updates the plot in the ScopeView tab with new data and scaling.""" + try: + if not self.sampling_active: + return + + if not self.x2cscope.is_scope_data_ready(): + return + + data_storage = {} + for channel, data in self.x2cscope.get_scope_channel_data(valid_data=True).items(): + data_storage[channel] = data + + self.scope_plot_widget.clear() + + for i, (channel, data) in enumerate(data_storage.items()): + checkbox_state = self.scope_channel_checkboxes[i].isChecked() + logging.debug( + f"Channel {channel}: Checkbox is {'checked' if checkbox_state else 'unchecked'}" + ) + if checkbox_state: # Check if the checkbox is checked + scale_factor = float( + self.scope_scaling_boxes[i].text() + ) # Get the scaling factor + # time_values = self.real_sampletime # Generate time values in ms + # start = self.real_sampletime / len(data) + start = 0 + time_values = np.linspace(start, self.real_sampletime, len(data)) + data_scaled = ( + np.array(data, dtype=float) * scale_factor + ) # Apply the scaling factor + self.scope_plot_widget.plot( + time_values, + data_scaled, + pen=pg.mkPen(color=self.plot_colors[i], width=2), + name=f"Channel {channel}", + ) + logging.debug( + f"Plotting channel {channel} with color {self.plot_colors[i]}" + ) + else: + logging.debug(f"Not plotting channel {channel}") + self.scope_plot_widget.setLabel("left", "Value") + self.scope_plot_widget.setLabel("bottom", "Time", units="ms") + self.scope_plot_widget.showGrid(x=True, y=True) + except Exception as e: + error_message = f"Error updating scope plot: {e}" + logging.error(error_message) + self.handle_error(error_message) + + def plot_data_plot(self): + """Initializes and starts data plotting.""" + try: + if not self.plot_data: + return + + self.update_watch_plot() + self.update_scope_plot() + + if not self.plot_window_open: + self.plot_window_open = True + except Exception as e: + logging.error(e) + + def handle_error(self, error_message: str): + """Displays an error message in a message box with a cooldown period.""" + current_time = time.time() + if self.last_error_time is None or ( + current_time - self.last_error_time > self.timeout + ): # Cooldown period of 5 seconds + msg_box = QMessageBox(self) + msg_box.setWindowTitle("Error") + msg_box.setText(error_message) + msg_box.setStandardButtons(QMessageBox.Ok) + msg_box.exec_() + self.last_error_time = current_time # Update the last error time + + def sampletime_edit(self): + """Handles the editing of the sample time value.""" + try: + new_sample_time = int(self.sampletime.text()) + if new_sample_time != self.timerValue: + self.timerValue = new_sample_time + for timer in self.timer_list: + if timer.isActive(): + timer.start(self.timerValue) + except ValueError as e: + logging.error(e) + self.handle_error(f"Invalid sample time: {e}") + + @pyqtSlot() + def handle_var_update(self, counter, value_var): + """Handles the update of variable values from the microcontroller. + + Args: + counter: The variable to update. + value_var (QLineEdit): The input field to display the updated value. + """ + if not self.is_connected(): + return # Do not proceed if the device is not connected + try: + if counter is not None: + counter = self.x2cscope.get_variable(counter) + value = counter.get_value() + value_var.setText(str(value)) + if value_var == self.Value_var1: + self.slider_var1.setValue(int(value)) + self.plot_data_update() + except Exception as e: + error_message = f"Error: {e}" + logging.error(error_message) + self.handle_error(error_message) + + def slider_var1_changed(self, value): + """Handles the change in slider value for Variable 1. + + Args: + value (int): The new value of the slider. + """ + if self.line_edit1.text() == "": + self.handle_error("Search Variable") + else: + self.Value_var1.setText(str(value)) + self.update_scaled_value( + self.Scaling_var1, + self.Value_var1, + self.ScaledValue_var1, + self.offset_var1, + ) + self.handle_variable_putram(self.line_edit1.text(), self.Value_var1) + + @pyqtSlot() + def handle_variable_getram(self, variable, value_var): + """Handle the retrieval of values from RAM for the specified variable. + + Args: + variable: The variable to retrieve the value for. + value_var: The QLineEdit widget to display the retrieved value. + """ + if not self.is_connected(): + return # Do not proceed if the device is not connected + + try: + current_variable = variable + + for index, line_edit in enumerate(self.line_edit_boxes): + if line_edit.text() == current_variable: + self.selected_var_indices[index] = current_variable + + if current_variable and current_variable != "None": + counter = self.x2cscope.get_variable(current_variable) + value = counter.get_value() + value_var.setText(str(value)) + if value_var == self.Value_var1: + self.slider_var1.setValue(int(value)) + + if current_variable not in self.selected_variables: + self.selected_variables.append(current_variable) + + except Exception as e: + error_message = f"Error: {e}" + logging.error(error_message) + self.handle_error(error_message) + + @pyqtSlot() + def handle_variable_putram(self, variable, value_var): + """Handle the writing of values to RAM for the specified variable. + + Args: + variable: The variable to write the value to. + value_var: The QLineEdit widget to get the value from. + """ + if not self.is_connected(): + return # Do not proceed if the device is not connected + try: + current_variable = variable + value = float(value_var.text()) + + if current_variable and current_variable != "None": + counter = self.x2cscope.get_variable(current_variable) + counter.set_value(value) + + except Exception as e: + error_message = f"Error: {e}" + logging.error(error_message) + self.handle_error(error_message) + + @pyqtSlot() + def select_elf_file(self): + """Function to select elf file.""" + file_dialog = QFileDialog() + file_dialog.setNameFilter("ELF Files (*.elf)") + file_dialog.setFileMode(QFileDialog.ExistingFile) + if self.file_path: + file_dialog.setDirectory(os.path.dirname(self.file_path)) + if file_dialog.exec_(): + selected_files = file_dialog.selectedFiles() + if selected_files: + self.file_path = selected_files[0] + self.settings.setValue("file_path", self.file_path) + self.select_file_button.setText(QFileInfo(self.file_path).fileName()) + self.elf_file_loaded = True + + # If Auto Connect is selected, attempt to auto-connect to the first available port + + def refresh_line_edit(self): + """Refresh the contents of the variable selection line edits. + + This method repopulates the line edits used for variable selection + with the updated list of variables. + """ + if self.VariableList is not None: + for index, line_edit in enumerate(self.line_edit_boxes): + current_selected_text = line_edit.text() + + if current_selected_text in self.VariableList: + line_edit.setText(current_selected_text) + else: + line_edit.setText("") + + for line_edit in self.scope_var_lines: + current_selected_text = line_edit.text() + + if current_selected_text in self.VariableList: + line_edit.setText(current_selected_text) + else: + line_edit.setText("") + else: + logging.warning("VariableList is None. Unable to refresh line edits.") + + def refresh_ports(self): + """Refresh the list of available serial ports. + + This method updates the combo box containing the list of available + serial ports to reflect the current state of the system. + """ + available_ports = [port.device for port in serial.tools.list_ports.comports()] + self.port_combo.clear() + self.port_combo.addItem("Auto Connect") # Add an Auto Connect option + self.port_combo.addItems(available_ports) + + @pyqtSlot() + def toggle_connection(self): + """Handle the connection or disconnection of the serial port. + + This method establishes or terminates the serial connection based on + the current state of the connection. + """ + if self.file_path == "": + QMessageBox.warning(self, "Error", "Please select an ELF file.") + self.select_elf_file() + return + + # Check if already connected + if self.ser is not None and self.ser.is_open: + # Call the disconnect function if already connected + logging.info("Already connected, disconnecting now.") + self.disconnect_serial() + else: + # Handle connection logic if not connected + for label in self.device_info_labels.values(): + label.setText("Loading...") + for timer in self.timer_list: + if timer.isActive(): + timer.stop() + self.plot_data.clear() + self.save_selected_variables() # Save the current selections before connecting + + try: + self.connect_serial() # Attempt to connect + if self.ser is not None and self.ser.is_open: + # Fetch device information after successful connection + self.update_device_info() + except Exception as e: + logging.error(e) + self.handle_error(f"Error connecting: {e}") + + def handle_failed_connection(self): + """Popping up a window for the errors.""" + choice = QMessageBox.question( + self, + "Connection Failed", + "Failed to connect with the current settings. Would you like to adjust the settings?", + QMessageBox.Yes | QMessageBox.No, + QMessageBox.No, + ) + if choice == QMessageBox.Yes: + # Optionally, bring up settings dialog or similar + self.show_connection_settings() + + def save_selected_variables(self): + """Save the current selections of variables in WatchView and ScopeView.""" + self.previous_selected_variables = { + "watch": [(le.text(), le.text()) for le in self.line_edit_boxes], + "scope": [(le.text(), le.text()) for le in self.scope_var_lines], + } + + def restore_selected_variables(self): + """Restore the previously selected variables in WatchView and ScopeView.""" + if "watch" in self.previous_selected_variables: + for le, (var, _) in zip( + self.line_edit_boxes, self.previous_selected_variables["watch"] + ): + le.setText(var) + + if "scope" in self.previous_selected_variables: + for le, (var, _) in zip( + self.scope_var_lines, self.previous_selected_variables["scope"] + ): + le.setText(var) + + def disconnect_serial(self): + """Disconnect the current serial connection. + + This method safely terminates the existing serial connection, if any, + and updates the UI to reflect the disconnection. + """ + try: + if self.ser is not None and self.ser.is_open: + self.ser.stop() + self.ser = None + + self.Connect_button.setText("Connect") + self.Connect_button.setEnabled(True) + self.select_file_button.setEnabled(True) + widget_list = [self.port_combo, self.baud_combo] + + for widget in widget_list: + widget.setEnabled(True) + + for line_edit in self.line_edit_boxes: + line_edit.setEnabled(False) + + for live_var in self.live_checkboxes: + live_var.setEnabled(False) + + self.slider_var1.setEnabled(False) + for timer in self.timer_list: + if timer.isActive(): + timer.stop() + + self.plot_update_timer.stop() # Stop the continuous plot update + + except Exception as e: + error_message = f"Error while disconnecting: {e}" + logging.error(error_message) + self.handle_error(error_message) + + if self.ser and self.ser.is_open: + self.ser.close() + self.Connect_button.setText("Connect") + + def connect_serial(self): + """Establish a serial connection based on the current UI settings.""" + try: + # Disconnect if already connected + if self.ser is not None and self.ser.is_open: + self.disconnect_serial() + + baud_rate = int(self.baud_combo.currentText()) + + # Check if Auto Connect is selected + if self.port_combo.currentText() == "Auto Connect": + self.auto_connect_serial(baud_rate) + else: + self.manual_connect_serial(baud_rate) + + except Exception as e: + error_message = f"Error while connecting: {e}" + logging.error(error_message) + self.handle_error(error_message) + + def auto_connect_serial(self, baud_rate): + """Attempt to auto-connect to available COM ports.""" + available_ports = [port.device for port in serial.tools.list_ports.comports()] + + # Iterate through available ports and attempt to connect + for port in available_ports: + if self.connect_to_port(port, baud_rate): + return # Exit once a connection is established + + # If no ports were successfully connected + self.handle_error( + "Auto-connect failed to connect to any available COM ports. Please check your connection!" + ) + raise Exception("Auto-connect failed to connect to any available COM ports.") + + def manual_connect_serial(self, baud_rate): + """Attempt to manually connect to the selected COM port.""" + port = self.port_combo.currentText() + logging.info(f"Trying to connect to {port} manually.") + + # Retry mechanism: try to connect twice if the first attempt fails + for attempt in range(2): + if self.connect_to_port(port, baud_rate): + return # Exit once the connection is successful + logging.info(f"Retrying connection to {port} (Attempt {attempt + 1})") + + raise Exception(f"Failed to connect to {port} after multiple attempts.") + + def connect_to_port(self, port, baud_rate): + """Attempt to establish a connection to the specified port.""" + try: + logging.info(f"Trying to connect to {port}...") + self.x2cscope = X2CScope( + port=port, elf_file=self.file_path, baud_rate=baud_rate + ) + self.ser = self.x2cscope.interface + + # If connection is successful + logging.info(f"Connected to {port} successfully.") + self.select_file_button.setText(QFileInfo(self.file_path).fileName()) + self.port_combo.setCurrentText( + port + ) # Update combo box with the successful port + self.setup_connected_state() # Handle UI updates after connection + return True + except OSError as e: + logging.error(f"Failed to connect to {port}: {e}") + return False + except Exception as e: + logging.error(f"Unexpected error connecting to {port}: {e}") + return False + + def setup_connected_state(self): + """Handle the UI updates and logic when a connection is successfully established.""" + # Refresh the variable list from the device + self.VariableList = self.x2cscope.list_variables() + if self.VariableList: + self.VariableList.insert(0, "None") + self.refresh_line_edit() + + # Update the UI elements + self.Connect_button.setText("Disconnect") + self.Connect_button.setEnabled(True) + + widget_list = [self.port_combo, self.baud_combo, self.select_file_button] + for widget in widget_list: + widget.setEnabled(False) + + for line_edit in self.line_edit_boxes: + line_edit.setEnabled(True) + self.slider_var1.setEnabled(True) + + for live_var in self.live_checkboxes: + live_var.setEnabled(True) + + # Start any live variable timers + for timer, live_var in zip(self.timer_list, self.live_checkboxes): + if live_var.isChecked(): + timer.start(self.timerValue) + + # Start the continuous plot update + self.plot_update_timer.start(self.timerValue) + + # Restore any selected variables that were saved before disconnecting + self.restore_selected_variables() + + def close_plot_window(self): + """Close the plot window if it is open. + + This method stops the animation and closes the plot window if it is open. + """ + self.plot_window_open = False + + def close_event(self, event): + """Handle the event when the main window is closed. + + Args: + event: The close event. + + This method ensures that all resources are properly released and the + application is closed cleanly. + """ + if self.sampling_active: + self.sampling_active = False + if self.ser: + self.disconnect_serial() + event.accept() + + def start_sampling(self): + """Start the sampling process.""" + if not self.is_connected(): + return # Do not proceed if the device is not connected + try: + a = time.time() + if self.sampling_active: + self.sampling_active = False + self.scope_sample_button.setText("Sample") + logging.info("Stopped sampling.") + + # Stop sampling and timers + if self.scope_timer.isActive(): + self.scope_timer.stop() # Stop the periodic sampling + + self.x2cscope.clear_all_scope_channel() # Clears channels and stops requests + else: + self.x2cscope.clear_all_scope_channel() + for line_edit in self.scope_var_lines: + variable_name = line_edit.text() + if variable_name and variable_name != "None": + variable = self.x2cscope.get_variable(variable_name) + self.x2cscope.add_scope_channel(variable) + + self.x2cscope.set_sample_time( + int(self.sample_time_factor.text()) + ) # set sample time factor + + # Set the scope sample time from the user input in microseconds + scope_sample_time_us = int(self.scope_sampletime_edit.text()) + self.real_sampletime = self.x2cscope.get_scope_sample_time( + scope_sample_time_us + ) + logging.debug(f"Real sample time: {self.real_sampletime} µs") # Check this value + + # Update the Total Time display + self.total_time_value.setText(str(self.real_sampletime)) + + self.sampling_active = True + self.configure_trigger() + self.scope_sample_button.setText("Stop") + logging.info("Started sampling.") + self.x2cscope.request_scope_data() + self.sample_scope_data( + single_shot=self.single_shot_checkbox.isChecked() + ) + b = time.time() + logging.debug(f"time execution '{b - a}'") + except Exception as e: + error_message = f"Error starting sampling: {e}" + logging.error(error_message) + self.handle_error(error_message) + + def configure_trigger(self): + """Configure the trigger settings.""" + if not self.is_connected(): + return # Do not proceed if the device is not connected + try: + if self.triggerVariable is not None: + variable_name = self.triggerVariable + variable = self.x2cscope.get_variable(variable_name) + + # Handle empty string for trigger level and delay + trigger_level_text = self.trigger_level_edit.text().strip() + trigger_delay_text = self.trigger_delay_edit.text().strip() + + if not trigger_level_text: + trigger_level = 0 + else: + try: + trigger_level = int(trigger_level_text) # YA + logging.debug(trigger_level) + except ValueError: + logging.error( + f"Invalid trigger level value: {trigger_level_text}" + ) + self.handle_error( + f"Invalid trigger level value: {trigger_level_text}" + ) + return + + if not trigger_delay_text: + trigger_delay = 0 + else: + try: + trigger_delay = int(trigger_delay_text) + except ValueError: + logging.error( + f"Invalid trigger delay value: {trigger_delay_text}" + ) + self.handle_error( + f"Invalid trigger delay value: {trigger_delay_text}" + ) + return + + trigger_edge = ( + 0 if self.trigger_edge_combo.currentText() == "Rising" else 1 + ) + trigger_mode = ( + 2 if self.trigger_mode_combo.currentText() == "Auto" else 1 + ) + + trigger_config = TriggerConfig( + variable=variable, + trigger_level=trigger_level, + trigger_mode=trigger_mode, + trigger_delay=trigger_delay, + trigger_edge=trigger_edge, + ) + self.x2cscope.set_scope_trigger(trigger_config) + logging.info("Trigger configured.") + except Exception as e: + error_message = f"Error configuring trigger: {e}" + logging.error(error_message) + self.handle_error(error_message) + + def sample_scope_data(self, single_shot=False): + """Sample the scope data using QTimer for non-blocking updates.""" + try: + if not self.is_connected(): + return # Do not proceed if the device is not connected + + self.sampling_active = True + self.scope_sample_button.setText("Stop") # Update button text + + # Create a QTimer for periodic scope data requests + self.scope_timer = QTimer() + self.scope_timer.timeout.connect( + lambda: self._sample_scope_data_timer(single_shot) + ) + self.scope_timer.start(250) # Adjust the interval (milliseconds) as needed + + except Exception as e: + error_message = f"Error starting sampling: {e}" + logging.error(error_message) + self.handle_error(error_message) + + def _sample_scope_data_timer(self, single_shot): + """Function that QTimer calls periodically to handle scope data sampling.""" + try: + # Retry mechanism for single-shot mode + if not self.x2cscope.is_scope_data_ready(): + if single_shot: + QTimer.singleShot(250, lambda: self._sample_scope_data_timer(single_shot)) + return # Exit if data is not ready + + logging.info("Scope data is ready.") + data_storage = {} + for channel, data in self.x2cscope.get_scope_channel_data().items(): + data_storage[channel] = data + + # Plot the data + self.scope_plot_widget.clear() + for i, (channel, data) in enumerate(data_storage.items()): + if self.scope_channel_checkboxes[i].isChecked(): # Check if the channel is enabled + scale_factor = float(self.scope_scaling_boxes[i].text()) # Get the scaling factor + start = 0 + time_values = np.linspace(start, self.real_sampletime, len(data)) + data_scaled = ( + np.array(data, dtype=float) * scale_factor + ) # Apply the scaling factor + self.scope_plot_widget.plot( + time_values, + data_scaled, + pen=pg.mkPen( + color=self.plot_colors[i], width=2 + ), # Thicker plot line + name=f"Channel {channel}", + ) + + # Update plot labels and grid + self.scope_plot_widget.setLabel("left", "Value") + self.scope_plot_widget.setLabel("bottom", "Time", units="ms") + self.scope_plot_widget.showGrid(x=True, y=True) # Enable grid lines + + # Stop timer if single-shot mode is active + if single_shot: + self.scope_timer.stop() # Stop the timer + self.sampling_active = False + self.scope_sample_button.setText("Sample") # Update button text + + # Request new data for the next tick + if self.x2cscope.is_scope_data_ready(): + self.x2cscope.request_scope_data() + + except Exception as e: + error_message = f"Error sampling scope data: {e}" + logging.error(error_message) + self.handle_error(error_message) + self.scope_timer.stop() # Stop timer on error + self.sampling_active = False + self.scope_sample_button.setText("Sample") # Update button text + + def save_config(self): + """Save current working config.""" + try: + # Configuration dictionary includes the path to the ELF file + config = { + "elf_file": self.file_path, # Store the current ELF file path + "com_port": self.port_combo.currentText(), + "baud_rate": self.baud_combo.currentText(), + "watch_view": { + "variables": [le.text() for le in self.line_edit_boxes], + "values": [ve.text() for ve in self.Value_var_boxes], + "scaling": [sc.text() for sc in self.scaling_boxes], + "offsets": [off.text() for off in self.offset_boxes], + "visible": [cb.isChecked() for cb in self.plot_checkboxes], + "live": [cb.isChecked() for cb in self.live_checkboxes], + }, + "scope_view": { + "variables": [le.text() for le in self.scope_var_lines], + "trigger": [cb.isChecked() for cb in self.trigger_var_checkbox], + "scale": [sc.text() for sc in self.scope_scaling_boxes], + "show": [cb.isChecked() for cb in self.scope_channel_checkboxes], + "trigger_variable": self.triggerVariable, + "trigger_level": self.trigger_level_edit.text(), + "trigger_delay": self.trigger_delay_edit.text(), + "trigger_edge": self.trigger_edge_combo.currentText(), + "trigger_mode": self.trigger_mode_combo.currentText(), + "sample_time_factor": self.sample_time_factor.text(), + "single_shot": self.single_shot_checkbox.isChecked(), + }, + "tab3_view": { + "variables": [le.text() for le in self.variable_line_edits], + "values": [ve.text() for ve in self.value_line_edits], + "scaling": [sc.text() for sc in self.scaling_edits_tab3], + "offsets": [off.text() for off in self.offset_edits_tab3], + "scaled_values": [sv.text() for sv in self.scaled_value_edits_tab3], + "live": [cb.isChecked() for cb in self.live_tab3], + }, + } + file_path, _ = QFileDialog.getSaveFileName( + self, "Save Configuration", "", "JSON Files (*.json)" + ) + if file_path: + with open(file_path, "w") as file: + json.dump(config, file, indent=4) + logging.info(f"Configuration saved to {file_path}") + except Exception as e: + logging.error(f"Error saving configuration: {e}") + self.handle_error(f"Error saving configuration: {e}") + + def load_config(self): + """Loads a pre-saved/configured config file and applies its settings to the application. + + The method first prompts the user to select a configuration file. If a valid file is selected, + it parses the JSON contents and loads settings related to the general application configuration, + WatchView, ScopeView, and Tab 3. If an ELF file path is missing or incorrect, the user is prompted + to reselect it. If connection to a COM port fails, it attempts to connect to available ports. + """ + try: + file_path = self.prompt_for_file() + if file_path: + config = self.load_json_file(file_path) + self.load_general_settings(config) + self.load_watch_view(config.get("watch_view", {})) + self.load_scope_view(config.get("scope_view", {})) + self.load_tab3_view(config.get("tab3_view", {})) + logging.info(f"Configuration loaded from {file_path}") + except Exception as e: + logging.error(f"Error loading configuration: {e}") + self.handle_error(f"Error loading configuration: {e}") + + def prompt_for_file(self): + """Prompts the user to select a configuration file through a file dialog. + + :return: The file path selected by the user, or None if no file was selected. + """ + file_path, _ = QFileDialog.getOpenFileName( + self, "Load Configuration", "", "JSON Files (*.json)" + ) + return file_path if file_path else None + + def load_json_file(self, file_path): + """Loads a JSON file from the specified file path. + + :param file_path: The path to the JSON configuration file. + :return: Parsed JSON content as a dictionary. + """ + with open(file_path, "r") as file: + return json.load(file) + + def load_general_settings(self, config): + """Loads general configuration settings such as COM port, baud rate, and ELF file path. + + If the ELF file does not exist, prompts the user to select a new one. Attempts to connect + to the specified COM port or other available ports if the connection fails. + + :param config: A dictionary containing general configuration settings. + """ + self.config_file_loaded = True + config_port = config.get("com_port", "") + self.baud_combo.setCurrentText(config.get("baud_rate", "")) + + elf_file_path = config.get("elf_file", "") + if os.path.exists(elf_file_path): + self.file_path = elf_file_path + self.elf_file_loaded = True + else: + self.show_file_not_found_warning(elf_file_path) + self.select_elf_file() + + self.handle_connection(config_port) + self.select_file_button.setText(QFileInfo(self.file_path).fileName()) + self.settings.setValue("file_path", self.file_path) + + def show_file_not_found_warning(self, elf_file_path): + """Shows a warning message if the specified ELF file does not exist. + + :param elf_file_path: The path to the ELF file that was not found. + """ + QMessageBox.warning( + self, "File Not Found", f"The ELF file {elf_file_path} does not exist." + ) + + def handle_connection(self, config_port): + """Attempts to connect to the specified COM port or any available port. + + If the connection to the specified port fails, it tries to connect to other available ports. + If no port connection is successful, it shows a warning message. + + :param config_port: The port specified in the configuration file. + """ + if not self.is_connected(): + available_ports = [ + port.device for port in serial.tools.list_ports.comports() + ] + if config_port in available_ports and self.attempt_connection(): + logging.info(f"Connected to the specified port: {config_port}") + else: + self.try_other_ports(available_ports) + + def try_other_ports(self, available_ports): + """Attempts to connect to other available COM ports if the specified port connection fails. + + :param available_ports: A list of available COM ports. + """ + for port in available_ports: + self.port_combo.setCurrentText(port) + if self.attempt_connection(): + logging.info(f"Connected to an alternative port: {port}") + break + else: + QMessageBox.warning( + self, + "Connection Failed", + "Could not connect to any available ports. Please check your connection.", + ) + + def load_watch_view(self, watch_view): + """Loads the WatchView settings from the configuration file. + + This includes variables, values, scaling, offsets, plot visibility, and live status. + + :param watch_view: A dictionary containing WatchView settings. + """ + for le, var in zip(self.line_edit_boxes, watch_view.get("variables", [])): + le.setText(var) + for ve, val in zip(self.Value_var_boxes, watch_view.get("values", [])): + ve.setText(val) + for sc, scale in zip(self.scaling_boxes, watch_view.get("scaling", [])): + sc.setText(scale) + for off, offset in zip(self.offset_boxes, watch_view.get("offsets", [])): + off.setText(offset) + for cb, visible in zip(self.plot_checkboxes, watch_view.get("visible", [])): + cb.setChecked(visible) + for cb, live in zip(self.live_checkboxes, watch_view.get("live", [])): + cb.setChecked(live) + + def load_scope_view(self, scope_view): + """Loads the ScopeView settings from the configuration file. + + This includes variables, trigger settings, and sampling configuration. + + :param scope_view: A dictionary containing ScopeView settings. + """ + for le, var in zip(self.scope_var_lines, scope_view.get("variables", [])): + le.setText(var) + for cb, trigger in zip( + self.trigger_var_checkbox, scope_view.get("trigger", []) + ): + cb.setChecked(trigger) + self.triggerVariable = scope_view.get("trigger_variable", "") + self.trigger_level_edit.setText(scope_view.get("trigger_level", "")) + self.trigger_delay_edit.setText(scope_view.get("trigger_delay", "")) + self.trigger_edge_combo.setCurrentText(scope_view.get("trigger_edge", "")) + self.trigger_mode_combo.setCurrentText(scope_view.get("trigger_mode", "")) + self.sample_time_factor.setText(scope_view.get("sample_time_factor", "")) + self.single_shot_checkbox.setChecked(scope_view.get("single_shot", False)) + + def load_tab3_view(self, tab3_view): + """Loads the configuration settings for Tab 3 (WatchView). + + This includes variables, values, scaling, offsets, scaled values, and live status. + + :param tab3_view: A dictionary containing Tab 3 settings. + """ + self.clear_tab3() + for var, val, sc, off, sv, live in zip( + tab3_view.get("variables", []), + tab3_view.get("values", []), + tab3_view.get("scaling", []), + tab3_view.get("offsets", []), + tab3_view.get("scaled_values", []), + tab3_view.get("live", []), + ): + self.add_variable_row() + self.variable_line_edits[-1].setText(var) + self.value_line_edits[-1].setText(val) + self.scaling_edits_tab3[-1].setText(sc) + self.offset_edits_tab3[-1].setText(off) + self.scaled_value_edits_tab3[-1].setText(sv) + self.live_tab3[-1].setChecked(live) + + def attempt_connection(self): + """Attempt to connect to the selected port and ELF file.""" + if self.elf_file_loaded: + try: + self.toggle_connection() # Trigger connection + if self.ser and self.ser.is_open: + return True + except Exception as e: + logging.error(f"Connection failed: {e}") + self.handle_error(f"Connection failed: {e}") + return False + + def is_connected(self): + """Check if the serial connection is established and the device is connected.""" + return self.ser is not None and self.ser.is_open + + def clear_tab3(self): + """Remove all variable rows in Tab 3 efficiently.""" + if not self.row_widgets: # Check if there is anything to clear + return # Skip clearing if already empty + + # Block updates to the GUI while making changes + self.tab3.layout().blockSignals(True) + try: + while self.row_widgets: + for widget in self.row_widgets.pop(): + widget.setVisible(False) # Hide widget to improve performance + self.watchview_grid.removeWidget(widget) # Remove widget from grid + widget.deleteLater() # Schedule widget for deletion + + self.current_row = 1 # Reset the row count + finally: + self.tab3.layout().blockSignals(False) # Ensure signals are re-enabled + self.tab3.layout().update() # Force an update to the layout + + def setup_tab3(self): + """Set up the third tab (WatchView Only) with Add/Remove Variable buttons and live functionality.""" + self.tab3.layout = QVBoxLayout() + self.tab3.setLayout(self.tab3.layout) + + # Add a scroll area to the layout + scroll_area = QScrollArea() + scroll_area_widget = QWidget() # Widget to hold the scroll area content + self.tab3.layout.addWidget(scroll_area) + + # Create a vertical layout inside the scroll area + scroll_area_layout = QVBoxLayout(scroll_area_widget) + scroll_area_widget.setLayout(scroll_area_layout) + + scroll_area.setWidget(scroll_area_widget) + scroll_area.setWidgetResizable(True) # Make the scroll area resizable + + # Create grid layout for adding rows similar to WatchView + self.watchview_grid = QGridLayout() + scroll_area_layout.addLayout(self.watchview_grid) + + # Set margins and spacing to remove excess gaps + self.watchview_grid.setContentsMargins(0, 0, 0, 0) # Remove margins + self.watchview_grid.setVerticalSpacing( + 2 + ) # Reduce vertical spacing between rows + self.watchview_grid.setHorizontalSpacing( + 5 + ) # Reduce horizontal spacing between columns + + # Add header row for the grid with proper alignment and size policy + headers = [ + "Live", + "Variable", + "Value", + "Scaling", + "Offset", + "Scaled Value", + "Unit", + "Remove", + ] + for i, header in enumerate(headers): + label = QLabel(header) + label.setAlignment(Qt.AlignCenter) # Center align the headers + label.setSizePolicy( + QSizePolicy.Minimum, QSizePolicy.Fixed + ) # Prevent labels from stretching vertically + self.watchview_grid.addWidget(label, 0, i) + + # Set column stretch to allow the variable field to resize + self.watchview_grid.setColumnStretch(1, 5) # Column for 'Variable' + self.watchview_grid.setColumnStretch(2, 2) # Column for 'Value' + self.watchview_grid.setColumnStretch(3, 1) # Column for 'Scaling' + self.watchview_grid.setColumnStretch(4, 1) # Column for 'Offset' + self.watchview_grid.setColumnStretch(5, 1) # Column for 'Scaled Value' + self.watchview_grid.setColumnStretch(6, 1) # Column for 'Unit' + + # Keep track of the current row count + self.current_row = 1 + + # Timer for updating live variables + self.live_update_timer = QTimer() + self.live_update_timer.timeout.connect(self.update_live_variables) + self.live_update_timer.start(500) # Set the update interval (500 ms) + + # Store references to live checkboxes and variables + self.live_tab3 = [] + self.variable_line_edits = [] + self.value_line_edits = [] + self.row_widgets = [] + + # Add button to add more variables at the bottom + self.add_variable_button = QPushButton("Add Variable") + scroll_area_layout.addWidget(self.add_variable_button, alignment=Qt.AlignBottom) + self.add_variable_button.clicked.connect(self.add_variable_row) + + # Add Load and Save Config buttons + self.save_button_tab3 = QPushButton("Save Config") + self.load_button_tab3 = QPushButton("Load Config") + self.save_button_tab3.setFixedSize(100, 30) + self.load_button_tab3.setFixedSize(100, 30) + button_layout = QHBoxLayout() + button_layout.addWidget(self.save_button_tab3) + button_layout.addWidget(self.load_button_tab3) + scroll_area_layout.addLayout(button_layout) + + # Connect buttons to their functions + self.save_button_tab3.clicked.connect(self.save_config) + self.load_button_tab3.clicked.connect(self.load_config) + + # Adjust the scroll area margins to remove any additional gaps + scroll_area_layout.setContentsMargins(0, 0, 0, 0) + + @pyqtSlot() + def add_variable_row(self): + """Add a row of widgets to represent a variable in the WatchView Only tab with live functionality.""" + row = self.current_row + + # Create widgets for the row + live_checkbox = QCheckBox(self) + variable_edit = QLineEdit(self) + value_edit = QLineEdit(self) + scaling_edit = QLineEdit(self) + offset_edit = QLineEdit(self) + scaled_value_edit = QLineEdit(self) + unit_edit = QLineEdit(self) + remove_button = QPushButton("Remove", self) + + # Set default values for scaling and offset + scaling_edit.setText("1") # Default scaling to 1 + offset_edit.setText("0") # Default offset to 0 + + # Set placeholder text for variable search (like in WatchView) + variable_edit.setPlaceholderText("Search Variable") + + # Make scaled value read-only + scaled_value_edit.setReadOnly(True) + + # Set size policies to make the variable name and value resize dynamically + variable_edit.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) + value_edit.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) + + # Add the widgets to the grid layout + self.watchview_grid.addWidget(live_checkbox, row, 0) + self.watchview_grid.addWidget(variable_edit, row, 1) + self.watchview_grid.addWidget(value_edit, row, 2) + self.watchview_grid.addWidget(scaling_edit, row, 3) + self.watchview_grid.addWidget(offset_edit, row, 4) + self.watchview_grid.addWidget(scaled_value_edit, row, 5) + self.watchview_grid.addWidget(unit_edit, row, 6) + self.watchview_grid.addWidget(remove_button, row, 7) + + # Set column stretch to allow the variable field to resize + self.watchview_grid.setColumnStretch(1, 5) # Column for 'Variable' + self.watchview_grid.setColumnStretch(2, 2) # Column for 'Value' + self.watchview_grid.setColumnStretch(3, 1) # Column for 'Scaling' + self.watchview_grid.setColumnStretch(4, 1) # Column for 'Offset' + self.watchview_grid.setColumnStretch(5, 1) # Column for 'Scaled Value' + self.watchview_grid.setColumnStretch(6, 1) # Column for 'Unit' + + # Connect remove button to function to remove the row + remove_button.clicked.connect( + lambda: self.remove_variable_row( + live_checkbox, + variable_edit, + value_edit, + scaling_edit, + offset_edit, + scaled_value_edit, + unit_edit, + remove_button, + ) + ) + + # Connect the variable search to the dialog + variable_edit.installEventFilter(self) + + # Connect value editing to set value using handle_putram when Enter is pressed + value_edit.editingFinished.connect( + lambda: self.handle_variable_putram(variable_edit.text(), value_edit) + ) + + # Connect scaling and offset fields to recalculate the scaled value + scaling_edit.editingFinished.connect( + lambda: self.update_scaled_value_tab3( + value_edit, scaling_edit, offset_edit, scaled_value_edit + ) + ) + offset_edit.editingFinished.connect( + lambda: self.update_scaled_value_tab3( + value_edit, scaling_edit, offset_edit, scaled_value_edit + ) + ) + + # Calculate and show the scaled value immediately using the default scaling and offset + self.update_scaled_value_tab3( + value_edit, scaling_edit, offset_edit, scaled_value_edit + ) + + # Add widgets to the lists for tracking + self.live_tab3.append(live_checkbox) + self.variable_line_edits.append(variable_edit) + self.value_line_edits.append(value_edit) + + # Track scaling and offset for live updates in Tab 3 + self.scaling_edits_tab3.append(scaling_edit) + self.offset_edits_tab3.append(offset_edit) + self.scaled_value_edits_tab3.append(scaled_value_edit) + + # Track the row widgets to remove them easily + self.row_widgets.append( + ( + live_checkbox, + variable_edit, + value_edit, + scaling_edit, + offset_edit, + scaled_value_edit, + unit_edit, + remove_button, + ) + ) + + # Increment the current row counter + self.current_row += 1 + + def remove_variable_row( + self, + live_checkbox, + variable_edit, + value_edit, + scaling_edit, + offset_edit, + scaled_value_edit, + unit_edit, + remove_button, + ): + """Remove a specific row in the WatchView Only tab.""" + # Remove the widgets from the grid layout + for widget in [ + live_checkbox, + variable_edit, + value_edit, + scaling_edit, + offset_edit, + scaled_value_edit, + unit_edit, + remove_button, + ]: + widget.deleteLater() + + # Remove the corresponding widgets from the tracking lists + self.live_tab3.remove(live_checkbox) + self.variable_line_edits.remove(variable_edit) + self.value_line_edits.remove(value_edit) + self.scaling_edits_tab3.remove(scaling_edit) # Remove from a scaling list + self.offset_edits_tab3.remove(offset_edit) # Remove from offset list + self.scaled_value_edits_tab3.remove( + scaled_value_edit + ) # Remove from a scaled value list + + # Remove the widget references from the row widgets tracking + self.row_widgets.remove( + ( + live_checkbox, + variable_edit, + value_edit, + scaling_edit, + offset_edit, + scaled_value_edit, + unit_edit, + remove_button, + ) + ) + + # Decrement the row count + self.current_row -= 1 + + # Adjust the layout for remaining rows + self.rearrange_grid() + + def rearrange_grid(self): + """Rearrange the grid layout after a row has been removed.""" + # Clear the entire grid layout + for i in reversed(range(self.watchview_grid.count())): + widget = self.watchview_grid.itemAt(i).widget() + if widget is not None: + widget.setParent(None) + + # Add the headers again + headers = [ + "Live", + "Variable", + "Value", + "Scaling", + "Offset", + "Scaled Value", + "Unit", + "Remove", + ] + for i, header in enumerate(headers): + self.watchview_grid.addWidget(QLabel(header), 0, i) + + # Add the remaining rows back to the grid + for row, widgets in enumerate(self.row_widgets, start=1): + for col, widget in enumerate(widgets): + self.watchview_grid.addWidget(widget, row, col) + + def eventFilter(self, source, event): # noqa: N802 #Overriding 3rd party function. + """Event filter to handle line edit click events for variable selection.""" + if event.type() == QtCore.QEvent.MouseButtonPress: + if isinstance(source, QLineEdit): + dialog = VariableSelectionDialog(self.VariableList, self) + if dialog.exec_() == QDialog.Accepted: + selected_variable = dialog.selected_variable + if selected_variable: + source.setText(selected_variable) + # Get the initial value from the microcontroller (if applicable) + try: + self.handle_variable_getram( + selected_variable, + self.value_line_edits[ + self.variable_line_edits.index(source) + ], + ) + except Exception as e: + logging.debug(e) + + return super().eventFilter(source, event) + + def update_live_variables(self): + """Update the values of variables in real-time if live checkbox is checked.""" + for ( + checkbox, + variable_edit, + value_edit, + scaling_edit, + offset_edit, + scaled_value_edit, + ) in zip( + self.live_tab3, + self.variable_line_edits, + self.value_line_edits, + self.scaling_edits_tab3, + self.offset_edits_tab3, + self.scaled_value_edits_tab3, + ): + + if checkbox.isChecked() and variable_edit.text(): + # Fetch the variable value from the microcontroller + variable_name = variable_edit.text() + self.handle_variable_getram(variable_name, value_edit) + + # Update the scaled value in real-time based on the raw value, scaling, and offset + self.update_scaled_value_tab3( + value_edit, scaling_edit, offset_edit, scaled_value_edit + ) + + @pyqtSlot() + def update_scaled_value_tab3( + self, value_edit, scaling_edit, offset_edit, scaled_value_edit + ): + """Updates the scaled value in both Tab 1 and Tab 3 based on the provided scaling factor and offset. + + Args: + value_edit : Input field for the raw value. + scaling_edit : Input field for the scaling factor. + offset_edit : Input field for the offset. + scaled_value_edit : Output field for the scaled value. + """ + try: + value = float(value_edit.text()) + scaling = float(scaling_edit.text()) if scaling_edit.text() else 1.0 + offset = float(offset_edit.text()) if offset_edit.text() else 0.0 + scaled_value = (scaling * value) + offset + scaled_value_edit.setText(f"{scaled_value:.2f}") + except ValueError as e: + logging.error(f"Error updating scaled value: {e}") + scaled_value_edit.setText("0.00") + + +if __name__ == "__main__": + app = QApplication(sys.argv) + ex = X2cscopeGui() + ex.show() + sys.exit(app.exec_()) diff --git a/pyx2cscope/parser/elf_parser.py b/pyx2cscope/parser/elf_parser.py index 618c27f1..373ac1f2 100644 --- a/pyx2cscope/parser/elf_parser.py +++ b/pyx2cscope/parser/elf_parser.py @@ -45,6 +45,7 @@ def __init__(self, elf_path: str): self.symbol_table = {} self._load_elf_file() + self._map_variables() self._load_symbol_table() self._map_variables() self._map_registers() diff --git a/pyx2cscope/parser/generic_parser.py b/pyx2cscope/parser/generic_parser.py index 6a3772ff..b8e524d1 100644 --- a/pyx2cscope/parser/generic_parser.py +++ b/pyx2cscope/parser/generic_parser.py @@ -69,7 +69,7 @@ def _get_die_variable(self, die_struct): elif die_struct.attributes.get("DW_AT_external") and die_struct.attributes.get("DW_AT_name") is not None: if die_struct.tag != "DW_TAG_variable": return - self.die_variable = die_struct + self.die_variable = die_struct self.is_sfr = True else: return @@ -90,6 +90,9 @@ def _process_die(self, die): members = {} self._process_end_die(members, self.die_variable, self.var_name, 0) + # Uncomment and use if base type processing is needed + # base_type_die = self._get_base_type_die(self.die_variable) + # self._process_end_die(members, base_type_die, self.var_name, 0) target_map = self.register_map if self.is_sfr else self.variable_map for member_name, member_data in members.items(): @@ -411,11 +414,16 @@ def _map_variables(self) -> dict[str, VariableInfo]: self.expression_parser = DWARFExprParser(die.cu.structs) self._process_die(die) + # #Update type _Bool to bool + # for var_info in self.variable_map.values(): + # if var_info.type == "_Bool": + # var_info.type = "bool" + return self.variable_map if __name__ == "__main__": - + # elf_file = r"..\..\tests\data\qspin_foc_same54.elf" elf_file = r"..\..\..\tests\data\dsPIC33ak128mc106_foc.elf" elf_reader = GenericParser(elf_file) @@ -425,13 +433,13 @@ def _map_variables(self) -> dict[str, VariableInfo]: print(variable_map) print(len(variable_map)) print("'''''''''''''''''''''''''''''''''''''''' ") - + print("Array variables:") for var_info in variable_map.values(): if var_info.array_size > 0: print(var_info) print("'''''''''''''''''''''''''''''''''''''''' ") - + print("register variables:") print(register_map) print(len(register_map)) diff --git a/pyx2cscope/variable/variable_factory.py b/pyx2cscope/variable/variable_factory.py index 1fa11699..66fdd580 100644 --- a/pyx2cscope/variable/variable_factory.py +++ b/pyx2cscope/variable/variable_factory.py @@ -196,6 +196,9 @@ def get_variable(self, name: str, sfr: bool = False) -> Variable | None: """ try: variable_info = self.parser.get_var_info(name, sfr=sfr) + if variable_info is None: + logging.error(f"Variable '{name}' not found!") + return None if variable_info is None: logging.error(f"Variable '{name}' not found!") return None diff --git a/pyx2cscope/x2cscope.py b/pyx2cscope/x2cscope.py index 293425ca..67e52d4b 100644 --- a/pyx2cscope/x2cscope.py +++ b/pyx2cscope/x2cscope.py @@ -256,12 +256,12 @@ def set_sample_time(self, sample_time: int): """Define the resolution how the samples will be buffered at the internal buffer. This can be used to extend total sampling time at the cost of resolution. - 0 = every sample, 1 = every 2nd sample, 2 = every 3rd sample … + 1 = every sample, 2 = every 2nd sample, 3 = every 3rd sample … Args: sample_time (int): The sample time factor. """ - sample_time = 0 if sample_time < 0 else sample_time + sample_time = 1 if sample_time < 1 else sample_time self.scope_setup.set_sample_time_factor(sample_time) def set_scope_state(self, scope_state: int): @@ -290,6 +290,7 @@ def is_scope_data_ready(self) -> bool: bool: True if the scope data is ready, False otherwise. """ scope_data = self.lnet.load_parameters() + logging.debug(scope_data) return ( scope_data.scope_state == 0 or scope_data.data_array_pointer == scope_data.data_array_used_length @@ -341,8 +342,9 @@ def _read_array_chunks(self) -> List[bytearray]: loop = num_chunks if chunk_rest == 0 else num_chunks + 1 for i in range(int(loop)): current_address = self.lnet.scope_data.data_array_address + i * chunk_size - data_size = chunk_size if i < int(num_chunks) else int(chunk_rest) try: + # Read the chunk of data + data_size = chunk_size if i < int(num_chunks) else int(chunk_rest)try: data = self.lnet.get_ram_array(current_address, data_size, data_type) chunk_data.extend(data) except Exception as e: diff --git a/pyx2cscope_linux.spec b/pyx2cscope_linux.spec index 4a5014cf..24d90190 100644 --- a/pyx2cscope_linux.spec +++ b/pyx2cscope_linux.spec @@ -1,5 +1,6 @@ # -*- mode: python ; coding: utf-8 -*- + a = Analysis( ['pyx2cscope/__main__.py'], pathex=[], From ffeca3c54290823e71307ff296e1a35b591ae974 Mon Sep 17 00:00:00 2001 From: Edras Pacola Date: Thu, 5 Mar 2026 23:53:27 +0100 Subject: [PATCH 02/10] V0.6.0 (#106) * fix zip relative path for artifacts * tcp-ip interface implemented * optimizing code * enhanced interface factory for default values according to some arguments, we may select the default interface type default_args = { "port": LNetSerial, "host": LNetTcpIp, "bus": LNetCan, "id": LNetLin, } * unit test for tcp interface * adding dashboard tab to web view * Adding new widgets and config * including scripting tab on web view * including multiple interfaces * fix for trigger_level * transparent widgets on view * refactoring generic gui to best practices Moved from a single file to a more modular structure with separate files for controllers, dialogs, models, tabs, and workers. This should improve maintainability and readability of the codebase. The old generic_gui.py file has been renamed to generic_gui_old.py for reference during the transition period. Changes to be committed: modified: pyx2cscope/gui/generic_gui/__init__.py new file: pyx2cscope/gui/generic_gui/controllers/__init__.py new file: pyx2cscope/gui/generic_gui/controllers/config_manager.py new file: pyx2cscope/gui/generic_gui/controllers/connection_manager.py deleted: pyx2cscope/gui/generic_gui/detachable_gui.py new file: pyx2cscope/gui/generic_gui/dialogs/__init__.py new file: pyx2cscope/gui/generic_gui/dialogs/variable_selection.py renamed: pyx2cscope/gui/generic_gui/generic_gui.py -> pyx2cscope/gui/generic_gui/generic_gui_old.py new file: pyx2cscope/gui/generic_gui/main_window.py new file: pyx2cscope/gui/generic_gui/models/__init__.py new file: pyx2cscope/gui/generic_gui/models/app_state.py new file: pyx2cscope/gui/generic_gui/tabs/__init__.py new file: pyx2cscope/gui/generic_gui/tabs/base_tab.py new file: pyx2cscope/gui/generic_gui/tabs/scope_view_tab.py new file: pyx2cscope/gui/generic_gui/tabs/watch_plot_tab.py new file: pyx2cscope/gui/generic_gui/tabs/watch_view_tab.py new file: pyx2cscope/gui/generic_gui/workers/__init__.py new file: pyx2cscope/gui/generic_gui/workers/data_poller.py * color on scope view and moving scope position and tabs * dual window for watch and plot view * renaming generic_gui folder to qt fixing also imports adding tcp and can interface setting setup as default tab * loading message when connecting * scripting tab working * update doc for gui qt * update mchplnet pointer * unit tests for gui qt and web * ruff check * mchplnet v0.4.1 + fix utils.py + docs * added update rate on widgets * reading SFRs * including SFRs on Web and GUI Qt * update project version --------- Co-authored-by: Edras Pacola --- .claude/settings.local.json | 8 + doc/development.md | 2 - pyx2cscope/examples/tcp_demo.py | 1 + pyx2cscope/gui/generic_gui/__init__.py | 1 - pyx2cscope/gui/generic_gui/detachable_gui.py | 1414 ----------- pyx2cscope/gui/generic_gui/generic_gui.py | 2345 ------------------ pyx2cscope/gui/qt/models/app_state.py | 2 +- pyx2cscope/gui/web/scope.py | 10 + pyx2cscope/gui/web/static/js/script.js | 10 + pyx2cscope/gui/web/templates/index.html | 6 + pyx2cscope/gui/web/templates/index_sv.html | 22 - pyx2cscope/gui/web/templates/index_wv.html | 20 - pyx2cscope/parser/elf_parser.py | 1 - pyx2cscope/parser/generic_parser.py | 8 - pyx2cscope/x2cscope.py | 2 +- 15 files changed, 37 insertions(+), 3815 deletions(-) create mode 100644 .claude/settings.local.json delete mode 100644 pyx2cscope/gui/generic_gui/__init__.py delete mode 100644 pyx2cscope/gui/generic_gui/detachable_gui.py delete mode 100644 pyx2cscope/gui/generic_gui/generic_gui.py delete mode 100644 pyx2cscope/gui/web/templates/index_sv.html delete mode 100644 pyx2cscope/gui/web/templates/index_wv.html diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 00000000..2d00e606 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,8 @@ +{ + "permissions": { + "allow": [ + "Bash(python -c \"from pyx2cscope.gui.generic_gui.tabs.watch_view_tab import WatchViewTab; from pyx2cscope.gui.generic_gui.tabs.scope_view_tab import ScopeViewTab; from pyx2cscope.gui.generic_gui.tabs.watch_plot_tab import WatchPlotTab; print\\(''All imports successful''\\)\")", + "Bash(ruff check .)" + ] + } +} diff --git a/doc/development.md b/doc/development.md index 863c71e4..ac5f110f 100644 --- a/doc/development.md +++ b/doc/development.md @@ -70,5 +70,3 @@ available on pypi in the dist folder. ```bash python scripts/build.py ``` - -## Creating artifacts to upload to GitHu diff --git a/pyx2cscope/examples/tcp_demo.py b/pyx2cscope/examples/tcp_demo.py index 468b673c..86c6034c 100644 --- a/pyx2cscope/examples/tcp_demo.py +++ b/pyx2cscope/examples/tcp_demo.py @@ -14,6 +14,7 @@ phase_current = x2cscope.get_variable("my_counter") x2cscope.add_scope_channel(phase_current) + x2cscope.request_scope_data() while not stop_requested(): diff --git a/pyx2cscope/gui/generic_gui/__init__.py b/pyx2cscope/gui/generic_gui/__init__.py deleted file mode 100644 index 1d624f3c..00000000 --- a/pyx2cscope/gui/generic_gui/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""FOC Gui: a general gui for motor control.""" diff --git a/pyx2cscope/gui/generic_gui/detachable_gui.py b/pyx2cscope/gui/generic_gui/detachable_gui.py deleted file mode 100644 index 5d8a9f60..00000000 --- a/pyx2cscope/gui/generic_gui/detachable_gui.py +++ /dev/null @@ -1,1414 +0,0 @@ -"""Detachable genenric GUI for X2Cscope.""" - -import logging -import os -import sys -import time -from collections import deque -from datetime import datetime - -import matplotlib -import numpy as np -import pyqtgraph as pg -import serial.tools.list_ports -from PyQt5 import QtGui -from PyQt5.QtCore import QFileInfo, QMutex, QRegExp, QSettings, Qt, QTimer, pyqtSlot -from PyQt5.QtGui import QIcon, QRegExpValidator -from PyQt5.QtWidgets import ( - QApplication, - QCheckBox, - QComboBox, - QDockWidget, - QFileDialog, - QGridLayout, - QGroupBox, - QHBoxLayout, - QLabel, - QLineEdit, - QMainWindow, - QMessageBox, - QPushButton, - QSizePolicy, - QSlider, - QStyleFactory, - QVBoxLayout, - QWidget, -) - -from pyx2cscope.gui import img as img_src -from pyx2cscope.x2cscope import TriggerConfig, X2CScope - -logging.basicConfig(level=logging.ERROR) - -matplotlib.use("QtAgg") - - -class X2cscopeGui(QMainWindow): - """Main GUI class for the pyX2Cscope application.""" - - def __init__(self): - """Initializing all the elements required.""" - super().__init__() - - self.triggerVariable = None - self.initialize_variables() - self.init_ui() - - def initialize_variables(self): - """Initialize instance variables.""" - self.sampling_active = False - self.offset_boxes = None - self.plot_checkboxes = None - self.scaled_value_boxes = None - self.scaling_boxes = None - self.Value_var_boxes = None - self.combo_boxes = None - self.live_checkboxes = None - self.timer_list = None - self.VariableList = [] - self.old_Variable_list = [] - self.var_factory = None - self.ser = None - self.timerValue = 500 - self.port_combo = QComboBox() - self.layout = None - self.slider_var1 = QSlider(Qt.Horizontal) - self.plot_button = QPushButton("Plot") - self.mutex = QMutex() - self.grid_layout = QGridLayout() - self.box_layout = QHBoxLayout() - self.timer1 = QTimer() - self.timer2 = QTimer() - self.timer3 = QTimer() - self.timer4 = QTimer() - self.timer5 = QTimer() - self.plot_update_timer = QTimer() # Timer for continuous plot update - self.timer() - self.offset_var() - self.plot_var_check() - self.scaling_var() - self.value_var() - self.live_var() - self.scaled_value() - self.combo_box() - self.sampletime = QLineEdit() - self.unit_var() - self.Connect_button = QPushButton("Connect") - self.baud_combo = QComboBox() - self.select_file_button = QPushButton("Select elf file") - self.error_shown = False - self.plot_window_open = False - self.settings = QSettings("MyCompany", "MyApp") - self.file_path: str = self.settings.value("file_path", "", type=str) - self.init_variables() - - def init_variables(self): - """Some extra variables define.""" - self.selected_var_indices = [ - 0, - 0, - 0, - 0, - 0, - ] # List to store selected variable indices - self.selected_variables = [] # List to store selected variables - decimal_regex = QRegExp("-?[0-9]+(\\.[0-9]+)?") - self.decimal_validator = QRegExpValidator(decimal_regex) - - self.plot_data = deque(maxlen=250) # Store plot data for all variables - self.plot_colors = [ - "b", - "g", - "r", - "c", - "m", - "y", - "k", - ] # colours for different plot - # Add self.labels on top - self.labels = [ - "Live", - "Variable", - "Value", - "Scaling", - "Offset", - "Scaled Value", - "Unit", - "Plot", - ] - - def init_ui(self): - """Initialize the user interface.""" - self.setup_application_style() - self.create_central_widget() - self.create_dockable_tabs() - self.setup_window_properties() - self.refresh_ports() - - def setup_application_style(self): - """Set the application style.""" - QApplication.setStyle(QStyleFactory.create("Fusion")) - - def create_central_widget(self): - """Create the central widget.""" - central_widget = QWidget(self) - self.setCentralWidget(central_widget) - self.layout = QVBoxLayout(central_widget) - - def create_dockable_tabs(self): - """Create dockable tabs for the main window.""" - self.watch_view_dock = QDockWidget("WatchView", self) - self.scope_view_dock = QDockWidget("ScopeView", self) - - self.tab1 = QWidget() - self.tab2 = QWidget() - - self.watch_view_dock.setWidget(self.tab1) - self.scope_view_dock.setWidget(self.tab2) - - self.addDockWidget(Qt.LeftDockWidgetArea, self.watch_view_dock) - self.addDockWidget(Qt.RightDockWidgetArea, self.scope_view_dock) - - self.setup_tab1() - self.setup_tab2() - - def setup_window_properties(self): - """Set up the main window properties.""" - self.setWindowTitle("pyX2Cscope") - mchp_img = os.path.join(os.path.dirname(img_src.__file__), "pyx2cscope.jpg") - self.setWindowIcon(QtGui.QIcon(mchp_img)) - - def combo_box(self): - """Initializing combo boxes.""" - self.combo_box5 = QComboBox() - self.combo_box4 = QComboBox() - self.combo_box3 = QComboBox() - self.combo_box2 = QComboBox() - self.combo_box1 = QComboBox() - - def scaled_value(self): - """Initializing Scaled variable.""" - self.ScaledValue_var1 = QLineEdit(self) - self.ScaledValue_var2 = QLineEdit(self) - self.ScaledValue_var3 = QLineEdit(self) - self.ScaledValue_var4 = QLineEdit(self) - self.ScaledValue_var5 = QLineEdit(self) - - def live_var(self): - """Initializing live variable.""" - self.Live_var1 = QCheckBox(self) - self.Live_var2 = QCheckBox(self) - self.Live_var3 = QCheckBox(self) - self.Live_var4 = QCheckBox(self) - self.Live_var5 = QCheckBox(self) - - def value_var(self): - """Initializing value variable.""" - self.Value_var1 = QLineEdit(self) - self.Value_var2 = QLineEdit(self) - self.Value_var3 = QLineEdit(self) - self.Value_var4 = QLineEdit(self) - self.Value_var5 = QLineEdit(self) - - def timer(self): - """Initializing timer.""" - self.timer5 = QTimer() - self.timer4 = QTimer() - self.timer3 = QTimer() - self.timer2 = QTimer() - self.timer1 = QTimer() - - def offset_var(self): - """Initializing Offset Variable.""" - self.offset_var1 = QLineEdit() - self.offset_var2 = QLineEdit() - self.offset_var3 = QLineEdit() - self.offset_var4 = QLineEdit() - self.offset_var5 = QLineEdit() - - def plot_var_check(self): - """Initializing plot variable check boxes.""" - self.plot_var5_checkbox = QCheckBox() - self.plot_var2_checkbox = QCheckBox() - self.plot_var4_checkbox = QCheckBox() - self.plot_var3_checkbox = QCheckBox() - self.plot_var1_checkbox = QCheckBox() - - def scaling_var(self): - """Initializing Scaling variable.""" - self.Scaling_var1 = QLineEdit(self) - self.Scaling_var2 = QLineEdit(self) - self.Scaling_var3 = QLineEdit(self) - self.Scaling_var4 = QLineEdit(self) - self.Scaling_var5 = QLineEdit(self) - - def unit_var(self): - """Initializing unit variable.""" - self.Unit_var1 = QLineEdit(self) - self.Unit_var2 = QLineEdit(self) - self.Unit_var3 = QLineEdit(self) - self.Unit_var4 = QLineEdit(self) - self.Unit_var5 = QLineEdit(self) - - def setup_tab1(self): - """Set up the first tab with the original functionality.""" - self.tab1.layout = QVBoxLayout() - self.tab1.setLayout(self.tab1.layout) - - grid_layout = QGridLayout() - self.tab1.layout.addLayout(grid_layout) - - self.setup_port_layout(grid_layout) - self.setup_baud_layout(grid_layout) - self.setup_sampletime_layout(grid_layout) - self.setup_variable_layout(grid_layout) - self.setup_connections() - - def setup_tab2(self): - """Set up the second tab with the scope functionality.""" - self.tab2.layout = QVBoxLayout() - self.tab2.setLayout(self.tab2.layout) - - main_grid_layout = QGridLayout() - self.tab2.layout.addLayout(main_grid_layout) - - # Set up individual components - trigger_group = self.create_trigger_configuration_group() - variable_group = self.create_variable_selection_group() - self.scope_plot_widget = self.create_scope_plot_widget() - button_layout = self.create_save_load_buttons() - - # Add the group boxes to the main layout with stretch factors - main_grid_layout.addWidget(trigger_group, 0, 0) - main_grid_layout.addWidget(variable_group, 0, 1) - - # Set the column stretch factors to make the variable group larger - main_grid_layout.setColumnStretch(0, 1) # Trigger configuration box - main_grid_layout.setColumnStretch(1, 3) # Variable selection box - - # Add the plot widget for scope view - self.tab2.layout.addWidget(self.scope_plot_widget) - - # Add Save and Load buttons - self.tab2.layout.addLayout(button_layout) - - def create_trigger_configuration_group(self): - """Create the trigger configuration group box.""" - trigger_group = QGroupBox("Trigger Configuration") - trigger_layout = QVBoxLayout() - trigger_group.setLayout(trigger_layout) - - grid_layout_trigger = QGridLayout() - trigger_layout.addLayout(grid_layout_trigger) - - self.single_shot_checkbox = QCheckBox("Single Shot") - self.sample_time_factor = QLineEdit("1") - self.sample_time_factor.setValidator(self.decimal_validator) - self.trigger_mode_combo = QComboBox() - self.trigger_mode_combo.addItems(["Auto", "Triggered"]) - self.trigger_edge_combo = QComboBox() - self.trigger_edge_combo.addItems(["Rising", "Falling"]) - self.trigger_level_edit = QLineEdit("0") - self.trigger_level_edit.setValidator(self.decimal_validator) - self.trigger_delay_edit = QLineEdit("0") - self.trigger_delay_edit.setValidator(self.decimal_validator) - - self.scope_sampletime_edit = QLineEdit( - "50" - ) # Default sample time in microseconds - self.scope_sampletime_edit.setValidator(self.decimal_validator) - - # Total Time - self.total_time_label = QLabel("Total Time (ms):") - self.total_time_value = QLineEdit("0") - self.total_time_value.setReadOnly(True) - - self.scope_sample_button = QPushButton("Sample") - self.scope_sample_button.setFixedSize(100, 30) - self.scope_sample_button.clicked.connect(self.start_sampling) - - # Arrange widgets in grid layout - grid_layout_trigger.addWidget(self.single_shot_checkbox, 0, 0, 1, 2) - grid_layout_trigger.addWidget(QLabel("Sample Time Factor"), 1, 0) - grid_layout_trigger.addWidget(self.sample_time_factor, 1, 1) - grid_layout_trigger.addWidget(QLabel("Scope Sample Time (µs):"), 2, 0) - grid_layout_trigger.addWidget(self.scope_sampletime_edit, 2, 1) - grid_layout_trigger.addWidget(self.total_time_label, 3, 0) - grid_layout_trigger.addWidget(self.total_time_value, 3, 1) - grid_layout_trigger.addWidget(QLabel("Trigger Mode:"), 4, 0) - grid_layout_trigger.addWidget(self.trigger_mode_combo, 4, 1) - grid_layout_trigger.addWidget(QLabel("Trigger Edge:"), 5, 0) - grid_layout_trigger.addWidget(self.trigger_edge_combo, 5, 1) - grid_layout_trigger.addWidget(QLabel("Trigger Level:"), 6, 0) - grid_layout_trigger.addWidget(self.trigger_level_edit, 6, 1) - grid_layout_trigger.addWidget(QLabel("Trigger Delay:"), 7, 0) - grid_layout_trigger.addWidget(self.trigger_delay_edit, 7, 1) - grid_layout_trigger.addWidget(self.scope_sample_button, 8, 0, 1, 2) - - return trigger_group - - def create_variable_selection_group(self): - """Create the variable selection group box.""" - variable_group = QGroupBox("Variable Selection") - variable_layout = QVBoxLayout() - variable_group.setLayout(variable_layout) - - grid_layout_variable = QGridLayout() - variable_layout.addLayout(grid_layout_variable) - number_of_variables = 8 - - self.scope_var_lines = [QLineEdit() for _ in range(number_of_variables)] - self.trigger_var_checkbox = [QCheckBox() for _ in range(number_of_variables)] - self.scope_channel_checkboxes = [QCheckBox() for _ in range(number_of_variables)] - self.scope_scaling_boxes = [QLineEdit("1") for _ in range(number_of_variables)] - - for checkbox in self.scope_channel_checkboxes: - checkbox.setChecked(True) - - for line_edit in self.scope_var_lines: - line_edit.setReadOnly(True) - line_edit.setPlaceholderText("Search Variable") - line_edit.installEventFilter(self) - - # Add "Search Variable" label - grid_layout_variable.addWidget(QLabel("Search Variable"), 0, 1) - grid_layout_variable.addWidget(QLabel("Trigger"), 0, 0) - grid_layout_variable.addWidget(QLabel("Gain"), 0, 2) - grid_layout_variable.addWidget(QLabel("Visible"), 0, 3) - - for i, (line_edit, trigger_checkbox, scale_box, show_checkbox) in enumerate( - zip( - self.scope_var_lines, - self.trigger_var_checkbox, - self.scope_scaling_boxes, - self.scope_channel_checkboxes, - ) - ): - line_edit.setMinimumHeight(20) - trigger_checkbox.setMinimumHeight(20) - show_checkbox.setMinimumHeight(20) - scale_box.setMinimumHeight(20) - scale_box.setFixedSize(50, 20) - scale_box.setValidator(self.decimal_validator) - - line_edit.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) - grid_layout_variable.addWidget(trigger_checkbox, i + 1, 0) - grid_layout_variable.addWidget(line_edit, i + 1, 1) - grid_layout_variable.addWidget(scale_box, i + 1, 2) - grid_layout_variable.addWidget(show_checkbox, i + 1, 3) - - trigger_checkbox.stateChanged.connect( - lambda state, x=i: self.handle_scope_checkbox_change(state, x) - ) - scale_box.editingFinished.connect(self.update_scope_plot) - show_checkbox.stateChanged.connect(self.update_scope_plot) - - return variable_group - - def create_scope_plot_widget(self): - """Create the scope plot widget.""" - scope_plot_widget = pg.PlotWidget(title="Scope Plot") - scope_plot_widget.setBackground("w") - scope_plot_widget.addLegend() - scope_plot_widget.showGrid(x=True, y=True) - scope_plot_widget.getViewBox().setMouseMode(pg.ViewBox.RectMode) - - return scope_plot_widget - - def create_save_load_buttons(self): - """Create the save and load buttons.""" - self.save_button_scope = QPushButton("Save Config") - self.load_button_scope = QPushButton("Load Config") - self.save_button_scope.setFixedSize(100, 30) - self.load_button_scope.setFixedSize(100, 30) - - button_layout = QHBoxLayout() - button_layout.addWidget(self.save_button_scope) - button_layout.addWidget(self.load_button_scope) - - return button_layout - - def handle_scope_checkbox_change(self, state, index): - """Handle the change in the state of the scope view checkboxes.""" - if state == Qt.Checked: - for i, checkbox in enumerate(self.trigger_var_checkbox): - if i != index: - checkbox.setChecked(False) - self.triggerVariable = self.scope_var_combos[index].currentText() - logging.debug(f"Checked variable: {self.scope_var_combos[index].currentText()}") - else: - self.triggerVariable = None - - def setup_port_layout(self, layout): - """Set up the port selection layout.""" - port_layout = QGridLayout() - port_label = QLabel("Select Port:") - - refresh_button = QPushButton() - refresh_button.setFixedSize(25, 25) - refresh_button.clicked.connect(self.refresh_ports) - refresh_img = os.path.join(os.path.dirname(img_src.__file__), "refresh.png") - refresh_button.setIcon(QIcon(refresh_img)) - - self.select_file_button.setEnabled(True) - self.select_file_button.clicked.connect(self.select_elf_file) - - port_layout.addWidget(port_label, 0, 0) - port_layout.addWidget(self.port_combo, 0, 1) - port_layout.addWidget(refresh_button, 0, 2) - - layout.addLayout(port_layout, 1, 0) - - def setup_baud_layout(self, layout): - """Set up the baud rate selection layout.""" - baud_layout = QGridLayout() - baud_label = QLabel("Select Baud Rate:") - baud_layout.addWidget(baud_label, 0, 0) - baud_layout.addWidget(self.baud_combo, 0, 1) - - self.baud_combo.addItems(["38400", "115200", "230400", "460800", "921600"]) - default_baud_rate = "115200" - index = self.baud_combo.findText(default_baud_rate, Qt.MatchFixedString) - if index >= 0: - self.baud_combo.setCurrentIndex(index) - - layout.addLayout(baud_layout, 2, 0) - - def setup_sampletime_layout(self, layout): - """Set up the sample time layout.""" - self.Connect_button.clicked.connect(self.toggle_connection) - self.Connect_button.setFixedSize(100, 30) - self.Connect_button.setMinimumHeight(30) - - self.sampletime.setText("500") - self.sampletime.setValidator(self.decimal_validator) - self.sampletime.editingFinished.connect(self.sampletime_edit) - self.sampletime.setFixedSize(50, 20) - - sampletime_layout = QHBoxLayout() - sampletime_layout.addWidget(QLabel("Sampletime"), alignment=Qt.AlignLeft) - sampletime_layout.addWidget(self.sampletime, alignment=Qt.AlignLeft) - sampletime_layout.addWidget(QLabel("ms"), alignment=Qt.AlignLeft) - sampletime_layout.addStretch(1) - sampletime_layout.addWidget(self.Connect_button, alignment=Qt.AlignRight) - - layout.addLayout(sampletime_layout, 3, 0) - layout.addWidget(self.select_file_button, 4, 0) - - def setup_variable_layout(self, layout): - """Set up the variable selection layout.""" - self.timer_list = [ - self.timer1, - self.timer2, - self.timer3, - self.timer4, - self.timer5, - ] - - for col, label in enumerate(self.labels): - self.grid_layout.addWidget(QLabel(label), 0, col) - - self.live_checkboxes = [ - self.Live_var1, - self.Live_var2, - self.Live_var3, - self.Live_var4, - self.Live_var5, - ] - self.combo_boxes = [ - self.combo_box1, - self.combo_box2, - self.combo_box3, - self.combo_box4, - self.combo_box5, - ] - self.Value_var_boxes = [ - self.Value_var1, - self.Value_var2, - self.Value_var3, - self.Value_var4, - self.Value_var5, - ] - self.scaling_boxes = [ - self.Scaling_var1, - self.Scaling_var2, - self.Scaling_var3, - self.Scaling_var4, - self.Scaling_var5, - ] - self.scaled_value_boxes = [ - self.ScaledValue_var1, - self.ScaledValue_var2, - self.ScaledValue_var3, - self.ScaledValue_var4, - self.ScaledValue_var5, - ] - unit_boxes = [ - self.Unit_var1, - self.Unit_var2, - self.Unit_var3, - self.Unit_var4, - self.Unit_var5, - ] - self.plot_checkboxes = [ - self.plot_var1_checkbox, - self.plot_var2_checkbox, - self.plot_var3_checkbox, - self.plot_var4_checkbox, - self.plot_var5_checkbox, - ] - self.offset_boxes = [ - self.offset_var1, - self.offset_var2, - self.offset_var3, - self.offset_var4, - self.offset_var5, - ] - - for row_index, ( - live_var, - combo_box, - value_var, - scaling_var, - offset_var, - scaled_value_var, - unit_var, - plot_checkbox, - ) in enumerate( - zip( - self.live_checkboxes, - self.combo_boxes, - self.Value_var_boxes, - self.scaling_boxes, - self.offset_boxes, - self.scaled_value_boxes, - unit_boxes, - self.plot_checkboxes, - ), - 1, - ): - live_var.setEnabled(False) - combo_box.setEnabled(False) - combo_box.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) - value_var.setText("0") - value_var.setValidator(self.decimal_validator) - scaling_var.setText("1") - offset_var.setText("0") - offset_var.setValidator(self.decimal_validator) - scaled_value_var.setText("0") - scaled_value_var.setValidator(self.decimal_validator) - display_row = row_index # Use a different variable name for the assignment - if display_row > 1: - display_row += 1 - self.grid_layout.addWidget(live_var, display_row, 0) - self.grid_layout.addWidget(combo_box, display_row, 1) - if display_row == 1: - self.grid_layout.addWidget(self.slider_var1, display_row + 1, 0, 1, 7) - - self.grid_layout.addWidget(value_var, display_row, 2) - self.grid_layout.addWidget(scaling_var, display_row, 3) - self.grid_layout.addWidget(offset_var, display_row, 4) - self.grid_layout.addWidget(scaled_value_var, display_row, 5) - self.grid_layout.addWidget(unit_var, display_row, 6) - self.grid_layout.addWidget(plot_checkbox, display_row, 7) - plot_checkbox.stateChanged.connect( - lambda state, x=row_index - 1: self.update_watch_plot() - ) - - layout.addLayout(self.grid_layout, 5, 0) - - # Add the plot widget for watch view - self.watch_plot_widget = pg.PlotWidget(title="Watch Plot") - self.watch_plot_widget.setBackground("w") - self.watch_plot_widget.addLegend() # Add legend to the plot widget - self.watch_plot_widget.showGrid(x=True, y=True) # Enable grid lines - self.tab1.layout.addWidget(self.watch_plot_widget) - - def setup_connections(self): - """Set up connections for various widgets.""" - self.plot_button.clicked.connect(self.plot_data_plot) - - for timer, combo_box, value_var in zip( - self.timer_list, self.combo_boxes, self.Value_var_boxes - ): - timer.timeout.connect( - lambda cb=combo_box, v_var=value_var: self.handle_var_update( - cb.currentText(), v_var - ) - ) - - for combo_box, value_var in zip(self.combo_boxes, self.Value_var_boxes): - combo_box.currentIndexChanged.connect( - lambda cb=combo_box, v_var=value_var: self.handle_variable_getram( - self.VariableList[cb], v_var - ) - ) - for combo_box, value_var in zip(self.combo_boxes, self.Value_var_boxes): - value_var.editingFinished.connect( - lambda cb=combo_box, v_var=value_var: self.handle_variable_putram( - cb.currentText(), v_var - ) - ) - - self.connect_editing_finished() - - for timer, live_var in zip(self.timer_list, self.live_checkboxes): - live_var.stateChanged.connect( - lambda state, lv=live_var, tm=timer: self.var_live(lv, tm) - ) - - self.slider_var1.setMinimum(-32768) - self.slider_var1.setMaximum(32767) - self.slider_var1.setEnabled(False) - self.slider_var1.valueChanged.connect(self.slider_var1_changed) - - self.plot_update_timer.timeout.connect( - self.update_watch_plot - ) # Connect the QTimer to the update method - - def connect_editing_finished(self): - """Connect editingFinished signals for value and scaling inputs.""" - for ( - scaling, - value_var, - scaled_value, - offset, - ) in zip( - self.scaling_boxes, - self.Value_var_boxes, - self.scaled_value_boxes, - self.offset_boxes, - ): - - def connect_editing_finished( - sc_edit=scaling, - v_edit=value_var, - scd_edit=scaled_value, - off_edit=offset, - ): - def on_editing_finished(): - self.update_scaled_value(sc_edit, v_edit, scd_edit, off_edit) - - return on_editing_finished - - value_var.editingFinished.connect(connect_editing_finished()) - - for ( - scaling, - value_var, - scaled_value, - offset, - ) in zip( - self.scaling_boxes, - self.Value_var_boxes, - self.scaled_value_boxes, - self.offset_boxes, - ): - - def connect_editing_finished( - sc_edit=scaling, - v_edit=value_var, - scd_edit=scaled_value, - off_edit=offset, - ): - def on_editing_finished(): - self.update_scaled_value(sc_edit, v_edit, scd_edit, off_edit) - - return on_editing_finished - - scaling.editingFinished.connect(connect_editing_finished()) - - for ( - scaling, - value_var, - scaled_value, - offset, - ) in zip( - self.scaling_boxes, - self.Value_var_boxes, - self.scaled_value_boxes, - self.offset_boxes, - ): - - def connect_text_changed( - sc_edit=scaling, - v_edit=value_var, - scd_edit=scaled_value, - off_edit=offset, - ): - def on_text_changed(): - self.update_scaled_value(sc_edit, v_edit, scd_edit, off_edit) - - return on_text_changed - - value_var.textChanged.connect(connect_text_changed()) - - for ( - scaling, - value_var, - scaled_value, - offset, - ) in zip( - self.scaling_boxes, - self.Value_var_boxes, - self.scaled_value_boxes, - self.offset_boxes, - ): - - def connect_text_changed( - sc_edit=scaling, - v_edit=value_var, - scd_edit=scaled_value, - off_edit=offset, - ): - def on_text_changed(): - self.update_scaled_value(sc_edit, v_edit, scd_edit, off_edit) - - return on_text_changed - - offset.editingFinished.connect(connect_text_changed()) - - @pyqtSlot() - def var_live(self, live_var, timer): - """Handles the state change of live variable checkboxes. - - Args: - live_var (QCheckBox): The checkbox representing a live variable. - timer (QTimer): The timer associated with the live variable. - """ - try: - if live_var.isChecked(): - if not timer.isActive(): - timer.start(self.timerValue) - elif timer.isActive(): - timer.stop() - except Exception as e: - logging.error(e) - self.handle_error(f"Live Variable: {e}") - - @pyqtSlot() - def update_scaled_value(self, scaling_var, value_var, scaled_value_var, offset_var): - """Updates the scaled value based on the provided scaling factor and offset. - - Args: - scaling_var : Input field for the scaling factor. - value_var : Input field for the raw value. - scaled_value_var : Input field for the scaled value. - offset_var : Input field for the offset. - """ - scaling_text = scaling_var.text() - value_text = value_var.text() - offset_text = offset_var.text() - try: - value = float(value_text) - if offset_text.startswith("-"): - float_offset = float(offset_text.lstrip("-")) - offset = -1 * float_offset - else: - offset = float(offset_text) - if scaling_text.startswith("-"): - float_scaling = float(scaling_text.lstrip("-")) - scaling = -1 * float_scaling - else: - scaling = float(scaling_text) - scaled_value = (scaling * value) + offset - scaled_value_var.setText("{:.2f}".format(scaled_value)) - except Exception as e: - logging.error(e) - self.handle_error(f"Error update Scaled Value: {e}") - - def plot_data_update(self): - """Updates the data for plotting.""" - try: - timestamp = datetime.now() - if len(self.plot_data) > 0: - last_timestamp = self.plot_data[-1][0] - time_diff = ( - timestamp - last_timestamp - ).total_seconds() * 1000 # to convert time in ms. - else: - time_diff = 0 - - def safe_float(value): - try: - return float(value) - except ValueError: - return 0.0 - - self.plot_data.append( - ( - timestamp, - time_diff, - safe_float(self.ScaledValue_var1.text()), - safe_float(self.ScaledValue_var2.text()), - safe_float(self.ScaledValue_var3.text()), - safe_float(self.ScaledValue_var4.text()), - safe_float(self.ScaledValue_var5.text()), - ) - ) - except Exception as e: - logging.error(e) - - def update_watch_plot(self): - """Updates the plot in the WatchView tab with new data.""" - try: - if not self.plot_data: - return - - data = np.array(self.plot_data, dtype=object).T - time_diffs = np.array(data[1], dtype=float) - values = [np.array(data[i], dtype=float) for i in range(2, 7)] - - # Keep the last plot lines to avoid clearing and recreate them - plot_lines = {} - for item in self.watch_plot_widget.plotItem.items: - if isinstance(item, pg.PlotDataItem): - plot_lines[item.name()] = item - - for i, (value, combo_box, plot_var) in enumerate( - zip(values, self.combo_boxes, self.plot_checkboxes) - ): - if plot_var.isChecked() and combo_box.currentIndex() != 0: - if combo_box.currentText() in plot_lines: - plot_line = plot_lines[combo_box.currentText()] - plot_line.setData(np.cumsum(time_diffs), value) - else: - self.watch_plot_widget.plot( - np.cumsum(time_diffs), - value, - pen=pg.mkPen(color=self.plot_colors[i], width=2), - # Thicker plot line - name=combo_box.currentText(), - ) - - self.watch_plot_widget.setLabel("left", "Value") - self.watch_plot_widget.setLabel("bottom", "Time", units="ms") - self.watch_plot_widget.showGrid(x=True, y=True) # Enable grid lines - except Exception as e: - logging.error(e) - - def plot_data_plot(self): - """Initializes and starts data plotting.""" - try: - if not self.plot_data: - return - - self.update_watch_plot() - self.update_scope_plot() - - if not self.plot_window_open: - self.plot_window_open = True - except Exception as e: - logging.error(e) - - def update_scope_plot(self): - """Updates the plot in the ScopeView tab with new data and scaling.""" - try: - if not self.sampling_active: - return - - if not self.x2cscope.is_scope_data_ready(): - return - - data_storage = {} - for channel, data in self.x2cscope.get_scope_channel_data().items(): - data_storage[channel] = data - - self.scope_plot_widget.clear() - - for i, (channel, data) in enumerate(data_storage.items()): - checkbox_state = self.scope_channel_checkboxes[i].isChecked() - logging.debug( - f"Channel {channel}: Checkbox is {'checked' if checkbox_state else 'unchecked'}" - ) - if checkbox_state: # Check if the checkbox is checked - scale_factor = float( - self.scope_scaling_boxes[i].text() - ) # Get the scaling factor - # time_values = self.real_sampletime # Generate time values in ms - # start = self.real_sampletime / len(data) - start = 0 - time_values = np.linspace(start, self.real_sampletime, len(data)) - data_scaled = ( - np.array(data, dtype=float) * scale_factor - ) # Apply the scaling factor - self.scope_plot_widget.plot( - time_values, - data_scaled, - pen=pg.mkPen(color=self.plot_colors[i], width=2), - name=f"Channel {channel}", - ) - logging.debug( - f"Plotting channel {channel} with color {self.plot_colors[i]}" - ) - else: - logging.debug(f"Not plotting channel {channel}") - self.scope_plot_widget.setLabel("left", "Value") - self.scope_plot_widget.setLabel("bottom", "Time", units="ms") - self.scope_plot_widget.showGrid(x=True, y=True) - except Exception as e: - error_message = f"Error updating scope plot: {e}" - logging.error(error_message) - self.handle_error(error_message) - - def handle_error(self, error_message: str): - """Displays an error message in a message box. - - Args: - error_message (str): The error message to display. - """ - msg_box = QMessageBox(self) - msg_box.setWindowTitle("Error") - msg_box.setText(error_message) - msg_box.setStandardButtons(QMessageBox.Ok) - msg_box.exec_() - - def sampletime_edit(self): - """Handles the editing of the sample time value.""" - try: - new_sample_time = int(self.sampletime.text()) - if new_sample_time != self.timerValue: - self.timerValue = new_sample_time - for timer in self.timer_list: - if timer.isActive(): - timer.start(self.timerValue) - except ValueError as e: - logging.error(e) - self.handle_error(f"Invalid sample time: {e}") - - @pyqtSlot() - def handle_var_update(self, counter, value_var): - """Handles the update of variable values from the microcontroller. - - Args: - counter: The variable to update. - value_var (QLineEdit): The input field to display the updated value. - """ - try: - if counter is not None: - counter = self.x2cscope.get_variable(counter) - value = counter.get_value() - value_var.setText(str(value)) - if value_var == self.Value_var1: - self.slider_var1.setValue(int(value)) - self.plot_data_update() - except Exception as e: - error_message = f"Error: {e}" - logging.error(error_message) - self.handle_error(error_message) - - def slider_var1_changed(self, value): - """Handles the change in slider value for Variable 1. - - Args: - value (int): The new value of the slider. - """ - if self.combo_box1.currentIndex() == 0: - self.handle_error("Select Variable") - else: - self.Value_var1.setText(str(value)) - self.update_scaled_value( - self.Scaling_var1, - self.Value_var1, - self.ScaledValue_var1, - self.offset_var1, - ) - self.handle_variable_putram(self.combo_box1.currentText(), self.Value_var1) - - @pyqtSlot() - def handle_variable_getram(self, variable, value_var): - """Handle the retrieval of values from RAM for the specified variable. - - Args: - variable: The variable to retrieve the value for. - value_var: The QLineEdit widget to display the retrieved value. - """ - try: - current_variable = variable - - for index, combo_box in enumerate(self.combo_boxes): - if combo_box.currentText() == current_variable: - self.selected_var_indices[index] = combo_box.currentIndex() - - if current_variable and current_variable != "None": - counter = self.x2cscope.get_variable(current_variable) - value = counter.get_value() - value_var.setText(str(value)) - if value_var == self.Value_var1: - self.slider_var1.setValue(int(value)) - - if current_variable not in self.selected_variables: - self.selected_variables.append(current_variable) - - except Exception as e: - error_message = f"Error: {e}" - logging.error(error_message) - self.handle_error(error_message) - - @pyqtSlot() - def handle_variable_putram(self, variable, value_var): - """Handle the writing of values to RAM for the specified variable. - - Args: - variable: The variable to write the value to. - value_var: The QLineEdit widget to get the value from. - """ - try: - current_variable = variable - value = float(value_var.text()) - - if current_variable and current_variable != "None": - counter = self.x2cscope.get_variable(current_variable) - counter.set_value(value) - - except Exception as e: - error_message = f"Error: {e}" - logging.error(error_message) - self.handle_error(error_message) - - @pyqtSlot() - def select_elf_file(self): - """Open a file dialog to select an ELF file. - - This method opens a file dialog for the user to select an ELF file. - The selected file path is then stored in settings for later use. - """ - file_dialog = QFileDialog() - file_dialog.setNameFilter("ELF Files (*.elf)") - file_dialog.setFileMode(QFileDialog.ExistingFile) - fileinfo = QFileInfo(self.file_path) - self.select_file_button.setText(fileinfo.fileName()) - - if self.file_path: - file_dialog.setDirectory(self.file_path) - if file_dialog.exec_(): - selected_files = file_dialog.selectedFiles() - if selected_files: - self.file_path = selected_files[0] - self.settings.setValue("file_path", self.file_path) - self.select_file_button.setText(QFileInfo(self.file_path).fileName()) - - def refresh_combo_box(self): - """Refresh the contents of the variable selection combo boxes. - - This method repopulates the combo boxes used for variable selection - with the updated list of variables. - """ - if self.VariableList is not None: - for index, combo_box in enumerate(self.combo_boxes): - selected_index = self.selected_var_indices[index] - current_selected_text = combo_box.currentText() - - combo_box.clear() - combo_box.addItems(self.VariableList) - - if current_selected_text in self.VariableList: - combo_box.setCurrentIndex(combo_box.findText(current_selected_text)) - else: - combo_box.setCurrentIndex(selected_index) - - for combo in self.scope_var_combos: - combo.clear() - combo.addItems(self.VariableList) - else: - logging.warning("VariableList is None. Unable to refresh combo boxes.") - - def refresh_ports(self): - """Refresh the list of available serial ports. - - This method updates the combo box containing the list of available - serial ports to reflect the current state of the system. - """ - available_ports = [port.device for port in serial.tools.list_ports.comports()] - self.port_combo.clear() - self.port_combo.addItems(available_ports) - - @pyqtSlot() - def toggle_connection(self): - """Handle the connection or disconnection of the serial port. - - This method establishes or terminates the serial connection based on - the current state of the connection. - """ - if self.file_path == "": - QMessageBox.warning(self, "Error", "Please select an ELF file.") - self.select_elf_file() - return - - if self.ser is None or not self.ser.is_open: - for timer in self.timer_list: - if timer.isActive(): - timer.stop() - self.plot_data.clear() - self.connect_serial() - else: - self.disconnect_serial() - - def disconnect_serial(self): - """Disconnect the current serial connection. - - This method safely terminates the existing serial connection, if any, - and updates the UI to reflect the disconnection. - """ - try: - if self.ser is not None and self.ser.is_open: - self.ser.stop() - self.ser = None - - self.Connect_button.setText("Connect") - self.Connect_button.setEnabled(True) - self.select_file_button.setEnabled(True) - widget_list = [self.port_combo, self.baud_combo] - - for widget in widget_list: - widget.setEnabled(True) - - for combo_box in self.combo_boxes: - combo_box.setEnabled(False) - - for live_var in self.live_checkboxes: - live_var.setEnabled(False) - - self.slider_var1.setEnabled(False) - for timer in self.timer_list: - if timer.isActive(): - timer.stop() - - self.plot_update_timer.stop() # Stop the continuous plot update - - except Exception as e: - error_message = f"Error while disconnecting: {e}" - logging.error(error_message) - self.handle_error(error_message) - - def connect_serial(self): - """Establish a serial connection based on the current UI settings. - - This method sets up a serial connection using the selected port and - baud rate. It also initializes the variable factory and updates the - UI to reflect the connection state. - """ - try: - if self.ser is not None and self.ser.is_open: - self.disconnect_serial() - - port = self.port_combo.currentText() - baud_rate = int(self.baud_combo.currentText()) - - self.x2cscope = X2CScope( - port=port, elf_file=self.file_path, baud_rate=baud_rate - ) - self.ser = self.x2cscope.interface - self.VariableList = self.x2cscope.list_variables() - if self.VariableList: - self.VariableList.insert(0, "None") - else: - return - self.refresh_combo_box() - logging.info("Serial Port Configuration:") - logging.info(f"Port: {port}") - logging.info(f"Baud Rate: {baud_rate}") - - self.Connect_button.setText("Disconnect") - self.Connect_button.setEnabled(True) - - widget_list = [self.port_combo, self.baud_combo, self.select_file_button] - - for widget in widget_list: - widget.setEnabled(False) - - for combo_box in self.combo_boxes: - combo_box.setEnabled(True) - self.slider_var1.setEnabled(True) - - for live_var in self.live_checkboxes: - live_var.setEnabled(True) - - timer_list = [] - for i in range(len(self.live_checkboxes)): - timer_list.append((self.live_checkboxes[i], self.timer_list[i])) - - for live_var, timer in timer_list: - if live_var.isChecked(): - timer.start(self.timerValue) - - self.plot_update_timer.start( - self.timerValue - ) # Start the continuous plot update - - except Exception as e: - error_message = f"Error while connecting: {e}" - logging.error(error_message) - self.handle_error(error_message) - - def close_plot_window(self): - """Close the plot window if it is open. - - This method stops the animation and closes the plot window, if it is open. - """ - self.plot_window_open = False - - def close_event(self, event): - """Handle the event when the main window is closed. - - Args: - event: The close event. - - This method ensures that all resources are properly released and the - application is closed cleanly. - """ - if self.sampling_active: - self.sampling_active = False - if self.ser: - self.disconnect_serial() - event.accept() - - def start_sampling(self): - """Start the sampling process.""" - try: - if self.sampling_active: - self.sampling_active = False - self.scope_sample_button.setText("Sample") - logging.info("Stopped sampling.") - else: - self.x2cscope.clear_all_scope_channel() - for combo in self.scope_var_combos: - variable_name = combo.currentText() - if variable_name and variable_name != "None": - variable = self.x2cscope.get_variable(variable_name) - self.x2cscope.add_scope_channel(variable) - - self.x2cscope.set_sample_time( - int(self.sample_time_factor.text()) - ) # set sample time factor - self.sampling_active = True - self.configure_trigger() - self.scope_sample_button.setText("Stop") - logging.info("Started sampling.") - self.x2cscope.request_scope_data() - self.sample_scope_data( - single_shot=self.single_shot_checkbox.isChecked() - ) - except Exception as e: - error_message = f"Error starting sampling: {e}" - logging.error(error_message) - self.handle_error(error_message) - - def configure_trigger(self): - """Configure the trigger settings.""" - try: - if self.triggerVariable is not None: - variable_name = self.triggerVariable - variable = self.x2cscope.get_variable(variable_name) - - # Handle empty string for trigger level and delay - trigger_level_text = self.trigger_level_edit.text().strip() - trigger_delay_text = self.trigger_delay_edit.text().strip() - - if not trigger_level_text: - trigger_level = 0 - else: - try: - trigger_level = float(trigger_level_text) - logging.debug(trigger_level) - except ValueError: - logging.error( - f"Invalid trigger level value: {trigger_level_text}" - ) - self.handle_error( - f"Invalid trigger level value: {trigger_level_text}" - ) - return - - if not trigger_delay_text: - trigger_delay = 0 - else: - try: - trigger_delay = int(trigger_delay_text) - except ValueError: - logging.error( - f"Invalid trigger delay value: {trigger_delay_text}" - ) - self.handle_error( - f"Invalid trigger delay value: {trigger_delay_text}" - ) - return - - trigger_edge = ( - 0 if self.trigger_edge_combo.currentText() == "Rising" else 1 - ) - trigger_mode = ( - 0 if self.trigger_mode_combo.currentText() == "Auto" else 1 - ) - - trigger_config = TriggerConfig( - variable=variable, - trigger_level=trigger_level, - trigger_mode=trigger_mode, - trigger_delay=trigger_delay, - trigger_edge=trigger_edge, - ) - self.x2cscope.set_scope_trigger(trigger_config) - logging.info("Trigger configured.") - except Exception as e: - error_message = f"Error configuring trigger: {e}" - logging.error(error_message) - self.handle_error(error_message) - - def sample_scope_data(self, single_shot=False): - """Sample the scope data.""" - try: - while self.sampling_active: - if self.x2cscope.is_scope_data_ready(): - logging.info("Scope data is ready.") - - data_storage = {} - for channel, data in self.x2cscope.get_scope_channel_data( - valid_data=False - ).items(): - data_storage[channel] = data - - self.scope_plot_widget.clear() - for i, (channel, data) in enumerate(data_storage.items()): - if self.scope_channel_checkboxes[ - i - ].isChecked(): # Check if the channel is enabled - time_values = np.array( - [j * 0.001 for j in range(len(data))], dtype=float - ) # milliseconds - data_scaled = np.array(data, dtype=float) - self.scope_plot_widget.plot( - time_values, - data_scaled, - pen=pg.mkPen(color=self.plot_colors[i], width=2), - name=f"Channel {channel}", - ) - - self.scope_plot_widget.setLabel("left", "Value") - self.scope_plot_widget.setLabel("bottom", "Time", units="ms") - self.scope_plot_widget.showGrid(x=True, y=True) # Enable grid lines - - if single_shot: - break - - self.x2cscope.request_scope_data() - logging.debug("Requested next scope data.") - - QApplication.processEvents() # Keep the GUI responsive - - time.sleep(0.1) - - self.sampling_active = False - self.scope_sample_button.setText("Sample") - logging.info("Data collection complete.") - except Exception as e: - error_message = f"Error sampling scope data: {e}" - logging.error(error_message) - self.handle_error(error_message) - - -if __name__ == "__main__": - app = QApplication(sys.argv) - ex = X2cscopeGui() - ex.show() - sys.exit(app.exec_()) diff --git a/pyx2cscope/gui/generic_gui/generic_gui.py b/pyx2cscope/gui/generic_gui/generic_gui.py deleted file mode 100644 index 47ef675c..00000000 --- a/pyx2cscope/gui/generic_gui/generic_gui.py +++ /dev/null @@ -1,2345 +0,0 @@ -"""replicate of X2Cscpope.""" - -import logging - -logging.basicConfig(level=logging.ERROR) -import json -import os -import sys -import time -from collections import deque -from datetime import datetime - -import matplotlib -import numpy as np -import pyqtgraph as pg # Added pyqtgraph for interactive plotting -import serial.tools.list_ports # Import the serial module to fix the NameError -from PyQt5 import QtCore, QtGui -from PyQt5.QtCore import QFileInfo, QMutex, QRegExp, QSettings, Qt, QTimer, pyqtSlot -from PyQt5.QtGui import QIcon, QRegExpValidator -from PyQt5.QtWidgets import ( - QApplication, - QCheckBox, - QComboBox, - QDialog, - QDialogButtonBox, - QFileDialog, - QGridLayout, - QGroupBox, - QHBoxLayout, - QLabel, - QLineEdit, - QListWidget, - QMainWindow, - QMessageBox, - QPushButton, - QScrollArea, - QSizePolicy, - QSlider, - QStyleFactory, - QTabWidget, - QVBoxLayout, - QWidget, -) - -from pyx2cscope.gui import img as img_src -from pyx2cscope.x2cscope import TriggerConfig, X2CScope - -logging.basicConfig(level=logging.DEBUG) - -matplotlib.use("QtAgg") # This sets the backend to Qt for Matplotlib - - -class VariableSelectionDialog(QDialog): - """Initialize the variable selection dialog. - - Args: - variables (list): A list of available variables to select from. - parent (QWidget): The parent widget. - """ - - def __init__(self, variables, parent=None): - """Set up the user interface components for the variable selection dialog.""" - super().__init__(parent) - self.variables = variables - self.selected_variable = None - - self.init_ui() - - def init_ui(self): - """Initializing UI component.""" - self.setWindowTitle("Search Variable") - self.setMinimumSize(300, 400) - - self.layout = QVBoxLayout() - - self.search_bar = QLineEdit(self) - self.search_bar.setPlaceholderText("Search...") - self.search_bar.textChanged.connect(self.filter_variables) - self.layout.addWidget(self.search_bar) - - self.variable_list = QListWidget(self) - self.variable_list.addItems(self.variables) - self.variable_list.itemDoubleClicked.connect(self.accept_selection) - self.layout.addWidget(self.variable_list) - - self.button_box = QDialogButtonBox( - QDialogButtonBox.Ok | QDialogButtonBox.Cancel, self - ) - self.button_box.accepted.connect(self.accept_selection) - self.button_box.rejected.connect(self.reject) - self.layout.addWidget(self.button_box) - - self.setLayout(self.layout) - - def filter_variables(self, text): - """Filter the variables based on user input in the search bar. - - Args: - text (str): The input text to filter variables. - """ - self.variable_list.clear() - filtered_variables = [ - var for var in self.variables if text.lower() in var.lower() - ] - self.variable_list.addItems(filtered_variables) - - def accept_selection(self): - """Accept the selection when a variable is chosen from the list. - - The selected variable is set as the `selected_variable` and the dialog is accepted. - """ - selected_items = self.variable_list.selectedItems() - if selected_items: - self.selected_variable = selected_items[0].text() - self.accept() - - -class X2cscopeGui(QMainWindow): - """Main GUI class for the pyX2Cscope application.""" - - def __init__(self, *args, **kwargs): - """Initializing all the elements required.""" - super().__init__() - self.x2cscope = None # Ensures it is initialized to None at start - self.x2cscope_initialized = ( - False # Flag to ensure error message is shown only once - ) - self.last_error_time = ( - None # Attribute to track the last time an error was shown - ) - self.triggerVariable = None - self.elf_file_loaded = False - self.config_file_loaded = False - self.device_info_labels = {} # Dictionary to hold the device information labels - self.initialize_variables() - self.init_ui() - - def initialize_variables(self): - """Initialize instance variables.""" - self.timeout = 5 - self.sampling_active = False - self.scaling_edits_tab3 = [] # Track scaling fields for Tab 3 - self.offset_edits_tab3 = [] # Track offset fields for Tab 3 - self.scaled_value_edits_tab3 = [] # Track scaled value fields for Tab 3 - self.offset_boxes = None - self.plot_checkboxes = None - self.scaled_value_boxes = None - self.scaling_boxes = None - self.Value_var_boxes = None - self.line_edit_boxes = None - self.live_checkboxes = None - self.timer_list = None - self.VariableList = [] - self.old_Variable_list = [] - self.var_factory = None - self.ser = None - self.timerValue = 500 - self.port_combo = QComboBox() - self.layout = None - self.slider_var1 = QSlider(Qt.Horizontal) - self.plot_button = QPushButton("Plot") - self.mutex = QMutex() - self.grid_layout = QGridLayout() - self.box_layout = QHBoxLayout() - self.timer1 = QTimer() - self.timer2 = QTimer() - self.timer3 = QTimer() - self.timer4 = QTimer() - self.timer5 = QTimer() - self.plot_update_timer = QTimer() # Timer for continuous plot update - self.timer() - self.offset_var() - self.plot_var_check() - self.scaling_var() - self.value_var() - self.live_var() - self.scaled_value() - self.line_edit() - self.sampletime = QLineEdit() - self.unit_var() - self.Connect_button = QPushButton("Connect") - self.baud_combo = QComboBox() - self.select_file_button = QPushButton("Select elf file") - self.error_shown = False - self.plot_window_open = False - self.settings = QSettings("MyCompany", "MyApp") - self.file_path: str = self.settings.value("file_path", "", type=str) - self.initi_variables() - - def initi_variables(self): - """Some variable initialisation.""" - self.device_info_labels = { - "processor_id": QLabel("Loading Processor ID ..."), - "uc_width": QLabel("Loading UC Width..."), - "date": QLabel("Loading Date..."), - "time": QLabel("Loading Time..."), - "appVer": QLabel("Loading App Version..."), - "dsp_state": QLabel("Loading DSP State..."), - } - self.selected_var_indices = [ - 0, - 0, - 0, - 0, - 0, - ] # List to store selected variable indices - self.selected_variables = [] # List to store selected variables - self.previous_selected_variables = {} # Dictionary to store previous selections - decimal_regex = QRegExp("-?[0-9]+(\\.[0-9]+)?") - self.decimal_validator = QRegExpValidator(decimal_regex) - - self.plot_data = deque(maxlen=250) # Store plot data for all variables - self.plot_colors = [ - "b", - "g", - "r", - "c", - "m", - "y", - "k", - ] # colours for different plot - # Add self.labels on top - self.labels = [ - "Live", - "Variable", - "Value", - "Scaling", - "Offset", - "Scaled Value", - "Unit", - "Plot", - ] - - def init_ui(self): - """Initialize the user interface.""" - self.setup_application_style() - self.create_central_widget() - self.create_tabs() - self.setup_tabs() - self.setup_window_properties() - # self.setup_device_info_ui() # Set up the device information section - self.refresh_ports() - - def setup_application_style(self): - """Set the application style.""" - QApplication.setStyle(QStyleFactory.create("Fusion")) - - def create_central_widget(self): - """Create the central widget.""" - central_widget = QWidget(self) - self.setCentralWidget(central_widget) - self.layout = QVBoxLayout(central_widget) - self.tab_widget = QTabWidget() - self.layout.addWidget(self.tab_widget) - - def update_device_info(self): - """Fetch device information from the connected device and update the labels.""" - try: - device_info = self.x2cscope.get_device_info() - self.device_info_labels["processor_id"].setText( - f" {device_info['processor_id']}" - ) - self.device_info_labels["uc_width"].setText(f"{device_info['uc_width']}") - self.device_info_labels["date"].setText(f"{device_info['date']}") - self.device_info_labels["time"].setText(f"{device_info['time']}") - self.device_info_labels["appVer"].setText(f"{device_info['AppVer']}") - self.device_info_labels["dsp_state"].setText(f"{device_info['dsp_state']}") - except Exception as e: - self.handle_error(f"Error fetching device info: {e}") - - def create_tabs(self): - """Create tabs for the main window.""" - self.tab1 = QWidget() - self.tab2 = QWidget() - self.tab3 = QWidget() # New tab for WatchView Only - self.tab_widget.addTab(self.tab1, "WatchPlot") - self.tab_widget.addTab(self.tab2, "ScopeView") - self.tab_widget.addTab(self.tab3, "WatchView") # Add third tab - - def setup_tabs(self): - """Set up the contents of each tab.""" - self.setup_tab1() - self.setup_tab2() - self.setup_tab3() # Setup for the third tab - - def setup_window_properties(self): - """Set up the main window properties.""" - self.setWindowTitle("pyX2Cscope") - mchp_img = os.path.join(os.path.dirname(img_src.__file__), "pyx2cscope.jpg") - self.setWindowIcon(QtGui.QIcon(mchp_img)) - - def line_edit(self): - """Initializing line edits.""" - self.line_edit5 = QLineEdit() - self.line_edit4 = QLineEdit() - self.line_edit3 = QLineEdit() - self.line_edit2 = QLineEdit() - self.line_edit1 = QLineEdit() - - for line_edit in [ - self.line_edit1, - self.line_edit2, - self.line_edit3, - self.line_edit4, - self.line_edit5, - ]: - line_edit.setReadOnly(True) - line_edit.setPlaceholderText("Search Variable") - line_edit.installEventFilter(self) - - def scaled_value(self): - """Initializing Scaled variable.""" - self.ScaledValue_var1 = QLineEdit(self) - self.ScaledValue_var2 = QLineEdit(self) - self.ScaledValue_var3 = QLineEdit(self) - self.ScaledValue_var4 = QLineEdit(self) - self.ScaledValue_var5 = QLineEdit(self) - - def live_var(self): - """Initializing live variable.""" - self.Live_var1 = QCheckBox(self) - self.Live_var2 = QCheckBox(self) - self.Live_var3 = QCheckBox(self) - self.Live_var4 = QCheckBox(self) - self.Live_var5 = QCheckBox(self) - - def value_var(self): - """Initializing value variable.""" - self.Value_var1 = QLineEdit(self) - self.Value_var2 = QLineEdit(self) - self.Value_var3 = QLineEdit(self) - self.Value_var4 = QLineEdit(self) - self.Value_var5 = QLineEdit(self) - - def timer(self): - """Initializing timers.""" - self.timers = [QTimer() for _ in range(5)] - - def offset_var(self): - """Initializing Offset Variable.""" - self.offset_var1 = QLineEdit() - self.offset_var2 = QLineEdit() - self.offset_var3 = QLineEdit() - self.offset_var4 = QLineEdit() - self.offset_var5 = QLineEdit() - - def plot_var_check(self): - """Initializing plot variable check boxes.""" - self.plot_var5_checkbox = QCheckBox() - self.plot_var2_checkbox = QCheckBox() - self.plot_var4_checkbox = QCheckBox() - self.plot_var3_checkbox = QCheckBox() - self.plot_var1_checkbox = QCheckBox() - - def scaling_var(self): - """Initializing Scaling variable.""" - self.Scaling_var1 = QLineEdit(self) - self.Scaling_var2 = QLineEdit(self) - self.Scaling_var3 = QLineEdit(self) - self.Scaling_var4 = QLineEdit(self) - self.Scaling_var5 = QLineEdit(self) - - def unit_var(self): - """Initializing unit variable.""" - self.Unit_var1 = QLineEdit(self) - self.Unit_var2 = QLineEdit(self) - self.Unit_var3 = QLineEdit(self) - self.Unit_var4 = QLineEdit(self) - self.Unit_var5 = QLineEdit(self) - - # noinspection PyUnresolvedReferences - def setup_tab1(self): - """Set up the first tab with the original functionality.""" - self.tab1.layout = QVBoxLayout() - self.tab1.setLayout(self.tab1.layout) - - grid_layout = QGridLayout() - self.tab1.layout.addLayout(grid_layout) - - self.setup_port_layout(grid_layout) - # self.setup_baud_layout(grid_layout) - self.setup_sampletime_layout(grid_layout) - self.setup_variable_layout(grid_layout) - self.setup_connections() - - # Add Save and Load buttons - self.save_button_watch = QPushButton("Save Config") - self.load_button_watch = QPushButton("Load Config") - self.save_button_watch.setFixedSize(100, 30) - self.load_button_watch.setFixedSize(100, 30) - self.save_button_watch.clicked.connect(self.save_config) - self.load_button_watch.clicked.connect(self.load_config) - - button_layout = QHBoxLayout() - button_layout.addWidget(self.save_button_watch) - button_layout.addWidget(self.load_button_watch) - self.tab1.layout.addLayout(button_layout) - - def setup_tab2(self): - """Set up the second tab with the scope functionality.""" - self.tab2.layout = QVBoxLayout() - self.tab2.setLayout(self.tab2.layout) - - main_grid_layout = QGridLayout() - self.tab2.layout.addLayout(main_grid_layout) - - # Set up individual components - trigger_group = self.create_trigger_configuration_group() - variable_group = self.create_variable_selection_group() - self.scope_plot_widget = self.create_scope_plot_widget() - button_layout = self.create_save_load_buttons() - - # Add the group boxes to the main layout with stretch factors - main_grid_layout.addWidget(trigger_group, 0, 0) - main_grid_layout.addWidget(variable_group, 0, 1) - - # Set the column stretch factors to make the variable group larger - main_grid_layout.setColumnStretch(0, 1) # Trigger configuration box - main_grid_layout.setColumnStretch(1, 3) # Variable selection box - - # Add the plot widget for scope view - self.tab2.layout.addWidget(self.scope_plot_widget) - - # Add Save and Load buttons - self.tab2.layout.addLayout(button_layout) - - def create_trigger_configuration_group(self): - """Create the trigger configuration group box.""" - trigger_group = QGroupBox("Trigger Configuration") - trigger_layout = QVBoxLayout() - trigger_group.setLayout(trigger_layout) - - grid_layout_trigger = QGridLayout() - trigger_layout.addLayout(grid_layout_trigger) - - self.single_shot_checkbox = QCheckBox("Single Shot") - self.sample_time_factor = QLineEdit("1") - self.sample_time_factor.setValidator(self.decimal_validator) - self.trigger_mode_combo = QComboBox() - self.trigger_mode_combo.addItems(["Auto", "Triggered"]) - self.trigger_edge_combo = QComboBox() - self.trigger_edge_combo.addItems(["Rising", "Falling"]) - self.trigger_level_edit = QLineEdit("0") - self.trigger_level_edit.setValidator(self.decimal_validator) - self.trigger_delay_edit = QLineEdit("0") - self.trigger_delay_edit.setValidator(self.decimal_validator) - - self.scope_sampletime_edit = QLineEdit("50") # Default sample time in microseconds - self.scope_sampletime_edit.setValidator(self.decimal_validator) - - # Total Time - self.total_time_label = QLabel("Total Time (ms):") - self.total_time_value = QLineEdit("0") - self.total_time_value.setReadOnly(True) - - self.scope_sample_button = QPushButton("Sample") - self.scope_sample_button.setFixedSize(100, 30) - self.scope_sample_button.clicked.connect(self.start_sampling) - - # Arrange widgets in grid layout - grid_layout_trigger.addWidget(self.single_shot_checkbox, 0, 0, 1, 2) - grid_layout_trigger.addWidget(QLabel("Sample Time Factor"), 1, 0) - grid_layout_trigger.addWidget(self.sample_time_factor, 1, 1) - grid_layout_trigger.addWidget(QLabel("Scope Sample Time (µs):"), 2, 0) - grid_layout_trigger.addWidget(self.scope_sampletime_edit, 2, 1) - grid_layout_trigger.addWidget(self.total_time_label, 3, 0) - grid_layout_trigger.addWidget(self.total_time_value, 3, 1) - grid_layout_trigger.addWidget(QLabel("Trigger Mode:"), 4, 0) - grid_layout_trigger.addWidget(self.trigger_mode_combo, 4, 1) - grid_layout_trigger.addWidget(QLabel("Trigger Edge:"), 5, 0) - grid_layout_trigger.addWidget(self.trigger_edge_combo, 5, 1) - grid_layout_trigger.addWidget(QLabel("Trigger Level:"), 6, 0) - grid_layout_trigger.addWidget(self.trigger_level_edit, 6, 1) - grid_layout_trigger.addWidget(QLabel("Trigger Delay:"), 7, 0) - grid_layout_trigger.addWidget(self.trigger_delay_edit, 7, 1) - grid_layout_trigger.addWidget(self.scope_sample_button, 8, 0, 1, 2) - - return trigger_group - - def create_variable_selection_group(self): - """Create the variable selection group box.""" - variable_group = QGroupBox("Variable Selection") - variable_layout = QVBoxLayout() - variable_group.setLayout(variable_layout) - - grid_layout_variable = QGridLayout() - variable_layout.addLayout(grid_layout_variable) - number_of_variables = 8 - self.scope_var_lines = [QLineEdit() for _ in range(number_of_variables)] - self.trigger_var_checkbox = [QCheckBox() for _ in range(number_of_variables)] - self.scope_channel_checkboxes = [QCheckBox() for _ in range(number_of_variables)] - self.scope_scaling_boxes = [QLineEdit("1") for _ in range(number_of_variables)] - - for checkbox in self.scope_channel_checkboxes: - checkbox.setChecked(True) - - for line_edit in self.scope_var_lines: - line_edit.setReadOnly(True) - line_edit.setPlaceholderText("Search Variable") - line_edit.installEventFilter(self) - - # Add "Search Variable" label - grid_layout_variable.addWidget(QLabel("Search Variable"), 0, 1) - grid_layout_variable.addWidget(QLabel("Trigger"), 0, 0) - grid_layout_variable.addWidget(QLabel("Gain"), 0, 2) - grid_layout_variable.addWidget(QLabel("Visible"), 0, 3) - - for i, (line_edit, trigger_checkbox, scale_box, show_checkbox) in enumerate( - zip( - self.scope_var_lines, - self.trigger_var_checkbox, - self.scope_scaling_boxes, - self.scope_channel_checkboxes, - ) - ): - line_edit.setMinimumHeight(20) - trigger_checkbox.setMinimumHeight(20) - show_checkbox.setMinimumHeight(20) - scale_box.setMinimumHeight(20) - scale_box.setFixedSize(50, 20) - scale_box.setValidator(self.decimal_validator) - - line_edit.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) - grid_layout_variable.addWidget(trigger_checkbox, i + 1, 0) - grid_layout_variable.addWidget(line_edit, i + 1, 1) - grid_layout_variable.addWidget(scale_box, i + 1, 2) - grid_layout_variable.addWidget(show_checkbox, i + 1, 3) - - trigger_checkbox.stateChanged.connect( - lambda state, x=i: self.handle_scope_checkbox_change(state, x) - ) - scale_box.editingFinished.connect(self.update_scope_plot) - show_checkbox.stateChanged.connect(self.update_scope_plot) - - return variable_group - - def create_scope_plot_widget(self): - """Create the scope plot widget.""" - scope_plot_widget = pg.PlotWidget(title="Scope Plot") - scope_plot_widget.setBackground("w") - scope_plot_widget.addLegend() - scope_plot_widget.showGrid(x=True, y=True) - scope_plot_widget.getViewBox().setMouseMode(pg.ViewBox.RectMode) - - return scope_plot_widget - - def create_save_load_buttons(self): - """Create the save and load buttons.""" - self.save_button_scope = QPushButton("Save Config") - self.load_button_scope = QPushButton("Load Config") - self.save_button_scope.setFixedSize(100, 30) - self.load_button_scope.setFixedSize(100, 30) - self.save_button_scope.clicked.connect(self.save_config) - self.load_button_scope.clicked.connect(self.load_config) - - button_layout = QHBoxLayout() - button_layout.addWidget(self.save_button_scope) - button_layout.addWidget(self.load_button_scope) - - return button_layout - - def handle_scope_checkbox_change(self, state, index): - """Handle the change in the state of the scope view checkboxes.""" - if state == Qt.Checked: - for i, checkbox in enumerate(self.trigger_var_checkbox): - if i != index: - checkbox.setChecked(False) - self.triggerVariable = self.scope_var_lines[index].text() - logging.debug(f"Checked variable: {self.scope_var_lines[index].text()}") - else: - self.triggerVariable = None - - def setup_port_layout(self, layout): - """Set up the port selection, baud rate, and device information layout in two sections.""" - # Create the left layout for device information (QVBoxLayout) - left_layout = QGridLayout() - - # Add device information labels to the left side - for label_key, label in self.device_info_labels.items(): - info_label = QLabel(label_key.replace("_", " ").capitalize() + ":") - info_label.setAlignment(Qt.AlignLeft) - - row = list(self.device_info_labels.keys()).index( - label_key - ) # Get the row index - device_info_layout = ( - QGridLayout() - ) # Create a row layout for label and its value - device_info_layout.addWidget(info_label, row, 0, Qt.AlignRight) - device_info_layout.addWidget(label, row, 1, alignment=Qt.AlignRight) - left_layout.addLayout(device_info_layout, row, 0, Qt.AlignLeft) - - # Create the right layout for COM port and settings (QGridLayout) - right_layout = QGridLayout() - - # COM Port Selection - port_label = QLabel("Select Port:") - self.port_combo.setFixedSize(100, 25) # Set fixed size for the port combo box - refresh_button = QPushButton() - refresh_button.setFixedSize(25, 25) - refresh_button.clicked.connect(self.refresh_ports) - refresh_img = os.path.join(os.path.dirname(img_src.__file__), "refresh.png") - refresh_button.setIcon(QIcon(refresh_img)) - - # Add COM Port widgets to the right layout - right_layout.addWidget(port_label, 0, 0, alignment=Qt.AlignRight) - right_layout.addWidget(self.port_combo, 0, 1) - right_layout.addWidget(refresh_button, 0, 2) - - # Baud Rate Selection - baud_label = QLabel("Select Baud Rate:") - self.baud_combo.setFixedSize(100, 25) - self.baud_combo.addItems(["38400", "115200", "230400", "460800", "921600"]) - default_baud_rate = "115200" - index = self.baud_combo.findText(default_baud_rate, Qt.MatchFixedString) - if index >= 0: - self.baud_combo.setCurrentIndex(index) - - # Add Baud Rate widgets to the right layout - right_layout.addWidget(baud_label, 1, 0, alignment=Qt.AlignRight) - right_layout.addWidget(self.baud_combo, 1, 1) - - # Add Connect button and Sample time - self.Connect_button.setFixedSize(100, 30) - self.Connect_button.clicked.connect(self.toggle_connection) - sampletime_label = QLabel("Sample Time WatchPlot:") - self.sampletime.setFixedSize(100, 30) - self.sampletime.setText("500") - - # Add Connect and Sample Time widgets to the right layout - self.sampletime.setValidator(self.decimal_validator) - self.sampletime.editingFinished.connect(self.sampletime_edit) - right_layout.addWidget(sampletime_label, 2, 0, alignment=Qt.AlignRight) - right_layout.addWidget(self.sampletime, 2, 1, alignment=Qt.AlignLeft) - right_layout.addWidget(QLabel("ms"), 2, 2, alignment=Qt.AlignLeft) - right_layout.addWidget(self.Connect_button, 3, 1, alignment=Qt.AlignBottom) - - # Create a horizontal layout to contain both left and right sections - horizontal_layout = QHBoxLayout() - - # Add left (device information) and right (settings) layouts to the horizontal layout - horizontal_layout.addLayout(left_layout) - horizontal_layout.addLayout(right_layout) - - # Finally, add the horizontal layout to the grid at a specific row and column - layout.addLayout(horizontal_layout, 0, 0, 1, 2) # Span 1 row and 2 columns - - def setup_sampletime_layout(self, layout): - """Set up the sample time layout.""" - # self.Connect_button.clicked.connect(self.toggle_connection) - self.select_file_button.clicked.connect(self.select_elf_file) - layout.addWidget(self.select_file_button, 4, 0) - - def setup_variable_layout(self, layout): - """Set up the variable selection layout.""" - self.timer_list = [ - self.timer1, - self.timer2, - self.timer3, - self.timer4, - self.timer5, - ] - - for col, label in enumerate(self.labels): - self.grid_layout.addWidget(QLabel(label), 0, col) - - self.live_checkboxes = [ - self.Live_var1, - self.Live_var2, - self.Live_var3, - self.Live_var4, - self.Live_var5, - ] - self.line_edit_boxes = [ - self.line_edit1, - self.line_edit2, - self.line_edit3, - self.line_edit4, - self.line_edit5, - ] - self.Value_var_boxes = [ - self.Value_var1, - self.Value_var2, - self.Value_var3, - self.Value_var4, - self.Value_var5, - ] - self.scaling_boxes = [ - self.Scaling_var1, - self.Scaling_var2, - self.Scaling_var3, - self.Scaling_var4, - self.Scaling_var5, - ] - self.scaled_value_boxes = [ - self.ScaledValue_var1, - self.ScaledValue_var2, - self.ScaledValue_var3, - self.ScaledValue_var4, - self.ScaledValue_var5, - ] - unit_boxes = [ - self.Unit_var1, - self.Unit_var2, - self.Unit_var3, - self.Unit_var4, - self.Unit_var5, - ] - self.plot_checkboxes = [ - self.plot_var1_checkbox, - self.plot_var2_checkbox, - self.plot_var3_checkbox, - self.plot_var4_checkbox, - self.plot_var5_checkbox, - ] - self.offset_boxes = [ - self.offset_var1, - self.offset_var2, - self.offset_var3, - self.offset_var4, - self.offset_var5, - ] - - for row_index, ( - live_var, - line_edit, - value_var, - scaling_var, - offset_var, - scaled_value_var, - unit_var, - plot_checkbox, - ) in enumerate( - zip( - self.live_checkboxes, - self.line_edit_boxes, - self.Value_var_boxes, - self.scaling_boxes, - self.offset_boxes, - self.scaled_value_boxes, - unit_boxes, - self.plot_checkboxes, - ), - 1, - ): - live_var.setEnabled(False) - line_edit.setEnabled(False) - - # Set size policy for variable name (line_edit) and value field to expand - line_edit.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) - value_var.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) - - value_var.setText("0") - value_var.setValidator(self.decimal_validator) - scaling_var.setText("1") - offset_var.setText("0") - offset_var.setValidator(self.decimal_validator) - scaled_value_var.setText("0") - scaled_value_var.setValidator(self.decimal_validator) - display_row = row_index # Use a different variable name for the assignment - if display_row > 1: - display_row += 1 - self.grid_layout.addWidget(live_var, display_row, 0) - self.grid_layout.addWidget(line_edit, display_row, 1) - if display_row == 1: - self.grid_layout.addWidget(self.slider_var1, display_row + 1, 0, 1, 7) - - self.grid_layout.addWidget(value_var, display_row, 2) - self.grid_layout.addWidget(scaling_var, display_row, 3) - self.grid_layout.addWidget(offset_var, display_row, 4) - self.grid_layout.addWidget(scaled_value_var, display_row, 5) - self.grid_layout.addWidget(unit_var, display_row, 6) - self.grid_layout.addWidget(plot_checkbox, display_row, 7) - plot_checkbox.stateChanged.connect( - lambda state, x=row_index - 1: self.update_watch_plot() - ) - - layout.addLayout(self.grid_layout, 5, 0) - - # Adjust the column stretch factors to ensure proper resizing - self.grid_layout.setColumnStretch(1, 5) # Variable column expands more - self.grid_layout.setColumnStretch(2, 2) # Value column - self.grid_layout.setColumnStretch(3, 1) # Scaling column - self.grid_layout.setColumnStretch(4, 1) # Offset column - self.grid_layout.setColumnStretch(5, 2) # Scaled Value column - self.grid_layout.setColumnStretch(6, 1) # Unit column - - # Add the plot widget for watch view - self.watch_plot_widget = pg.PlotWidget(title="Watch Plot") - self.watch_plot_widget.setBackground("w") - self.watch_plot_widget.addLegend() # Add legend to the plot widget - self.watch_plot_widget.showGrid(x=True, y=True) # Enable grid lines - # Change to 1-button zoom mode - self.watch_plot_widget.getViewBox().setMouseMode(pg.ViewBox.RectMode) - self.tab1.layout.addWidget(self.watch_plot_widget) - - def setup_connections(self): - """Set up connections for various widgets.""" - self.plot_button.clicked.connect(self.plot_data_plot) - - for timer, line_edit, value_var in zip( - self.timer_list, self.line_edit_boxes, self.Value_var_boxes - ): - timer.timeout.connect( - lambda cb=line_edit, v_var=value_var: self.handle_var_update( - cb.text(), v_var - ) - ) - - for line_edit, value_var in zip(self.line_edit_boxes, self.Value_var_boxes): - value_var.editingFinished.connect( - lambda cb=line_edit, v_var=value_var: self.handle_variable_putram( - cb.text(), v_var - ) - ) - - self.connect_editing_finished() - - for timer, live_var in zip(self.timer_list, self.live_checkboxes): - live_var.stateChanged.connect( - lambda state, lv=live_var, tm=timer: self.var_live(lv, tm) - ) - - self.slider_var1.setMinimum(-32768) - self.slider_var1.setMaximum(32767) - self.slider_var1.setEnabled(False) - self.slider_var1.valueChanged.connect(self.slider_var1_changed) - - self.plot_update_timer.timeout.connect( - self.update_watch_plot - ) # Connect the QTimer to the update method - - def connect_editing_finished(self): - """Connect editingFinished signals for value and scaling inputs.""" - for ( - scaling, - value_var, - scaled_value, - offset, - ) in zip( - self.scaling_boxes, - self.Value_var_boxes, - self.scaled_value_boxes, - self.offset_boxes, - ): - - def connect_editing_finished( - sc_edit=scaling, - v_edit=value_var, - scd_edit=scaled_value, - off_edit=offset, - ): - def on_editing_finished(): - self.update_scaled_value(sc_edit, v_edit, scd_edit, off_edit) - - return on_editing_finished - - value_var.editingFinished.connect(connect_editing_finished()) - - for ( - scaling, - value_var, - scaled_value, - offset, - ) in zip( - self.scaling_boxes, - self.Value_var_boxes, - self.scaled_value_boxes, - self.offset_boxes, - ): - - def connect_editing_finished( - sc_edit=scaling, - v_edit=value_var, - scd_edit=scaled_value, - off_edit=offset, - ): - def on_editing_finished(): - self.update_scaled_value(sc_edit, v_edit, scd_edit, off_edit) - - return on_editing_finished - - scaling.editingFinished.connect(connect_editing_finished()) - - for ( - scaling, - value_var, - scaled_value, - offset, - ) in zip( - self.scaling_boxes, - self.Value_var_boxes, - self.scaled_value_boxes, - self.offset_boxes, - ): - - def connect_text_changed( - sc_edit=scaling, - v_edit=value_var, - scd_edit=scaled_value, - off_edit=offset, - ): - def on_text_changed(): - self.update_scaled_value(sc_edit, v_edit, scd_edit, off_edit) - - return on_text_changed - - value_var.textChanged.connect(connect_text_changed()) - - for ( - scaling, - value_var, - scaled_value, - offset, - ) in zip( - self.scaling_boxes, - self.Value_var_boxes, - self.scaled_value_boxes, - self.offset_boxes, - ): - - def connect_text_changed( - sc_edit=scaling, - v_edit=value_var, - scd_edit=scaled_value, - off_edit=offset, - ): - def on_text_changed(): - self.update_scaled_value(sc_edit, v_edit, scd_edit, off_edit) - - return on_text_changed - - offset.editingFinished.connect(connect_text_changed()) - - @pyqtSlot() - def var_live(self, live_var, timer): - """Handles the state change of live variable checkboxes. - - Args: - live_var (QCheckBox): The checkbox representing a live variable. - timer (QTimer): The timer associated with the live variable. - """ - try: - if live_var.isChecked(): - if not timer.isActive(): - timer.start(self.timerValue) - elif timer.isActive(): - timer.stop() - except Exception as e: - logging.error(e) - self.handle_error(f"Live Variable: {e}") - - @pyqtSlot() - def update_scaled_value(self, scaling_var, value_var, scaled_value_var, offset_var): - """Updates the scaled value based on the provided scaling factor and offset. - - Args: - scaling_var : Input field for the scaling factor. - value_var : Input field for the raw value. - scaled_value_var : Input field for the scaled value. - offset_var : Input field for the offset. - """ - scaling_text = scaling_var.text() - value_text = value_var.text() - offset_text = offset_var.text() - try: - value = float(value_text) - if offset_text.startswith("-"): - float_offset = float(offset_text.lstrip("-")) - offset = -1 * float_offset - else: - offset = float(offset_text) - if scaling_text.startswith("-"): - float_scaling = float(scaling_text.lstrip("-")) - scaling = -1 * float_scaling - else: - scaling = float(scaling_text) - scaled_value = (scaling * value) + offset - scaled_value_var.setText("{:.2f}".format(scaled_value)) - except Exception as e: - logging.error(e) - self.handle_error(f"Error update Scaled Value: {e}") - - def plot_data_update(self): - """Updates the data for plotting.""" - try: - timestamp = datetime.now() - if len(self.plot_data) > 0: - last_timestamp = self.plot_data[-1][0] - time_diff = ( - timestamp - last_timestamp - ).total_seconds() * 1000 # to convert time in ms. - else: - time_diff = 0 - - def safe_float(value): - try: - return float(value) - except ValueError: - return 0.0 - - self.plot_data.append( - ( - timestamp, - time_diff, - safe_float(self.ScaledValue_var1.text()), - safe_float(self.ScaledValue_var2.text()), - safe_float(self.ScaledValue_var3.text()), - safe_float(self.ScaledValue_var4.text()), - safe_float(self.ScaledValue_var5.text()), - ) - ) - except Exception as e: - logging.error(e) - - def update_watch_plot(self): - """Updates the plot in the WatchView tab with new data.""" - try: - if not self.plot_data: - return - - # Clear the plot and remove old labels - self.watch_plot_widget.clear() - - data = np.array(self.plot_data, dtype=object).T - time_diffs = np.array(data[1], dtype=float) - values = [np.array(data[i], dtype=float) for i in range(2, 7)] - - # Keep track of plot lines to avoid clearing and recreating them unnecessarily - for i, (value, line_edit, plot_var) in enumerate( - zip(values, self.line_edit_boxes, self.plot_checkboxes) - ): - # Check if the variable should be plotted and is not empty - if plot_var.isChecked() and line_edit.text() != "": - self.watch_plot_widget.plot( - np.cumsum(time_diffs), - value, - pen=pg.mkPen( - color=self.plot_colors[i], width=2 - ), # Thicker plot line - name=line_edit.text(), - ) - - # Reset plot labels - self.watch_plot_widget.setLabel("left", "Value") - self.watch_plot_widget.setLabel("bottom", "Time", units="ms") - self.watch_plot_widget.showGrid(x=True, y=True) # Enable grid lines - except Exception as e: - logging.error(e) - - def update_scope_plot(self): - """Updates the plot in the ScopeView tab with new data and scaling.""" - try: - if not self.sampling_active: - return - - if not self.x2cscope.is_scope_data_ready(): - return - - data_storage = {} - for channel, data in self.x2cscope.get_scope_channel_data(valid_data=True).items(): - data_storage[channel] = data - - self.scope_plot_widget.clear() - - for i, (channel, data) in enumerate(data_storage.items()): - checkbox_state = self.scope_channel_checkboxes[i].isChecked() - logging.debug( - f"Channel {channel}: Checkbox is {'checked' if checkbox_state else 'unchecked'}" - ) - if checkbox_state: # Check if the checkbox is checked - scale_factor = float( - self.scope_scaling_boxes[i].text() - ) # Get the scaling factor - # time_values = self.real_sampletime # Generate time values in ms - # start = self.real_sampletime / len(data) - start = 0 - time_values = np.linspace(start, self.real_sampletime, len(data)) - data_scaled = ( - np.array(data, dtype=float) * scale_factor - ) # Apply the scaling factor - self.scope_plot_widget.plot( - time_values, - data_scaled, - pen=pg.mkPen(color=self.plot_colors[i], width=2), - name=f"Channel {channel}", - ) - logging.debug( - f"Plotting channel {channel} with color {self.plot_colors[i]}" - ) - else: - logging.debug(f"Not plotting channel {channel}") - self.scope_plot_widget.setLabel("left", "Value") - self.scope_plot_widget.setLabel("bottom", "Time", units="ms") - self.scope_plot_widget.showGrid(x=True, y=True) - except Exception as e: - error_message = f"Error updating scope plot: {e}" - logging.error(error_message) - self.handle_error(error_message) - - def plot_data_plot(self): - """Initializes and starts data plotting.""" - try: - if not self.plot_data: - return - - self.update_watch_plot() - self.update_scope_plot() - - if not self.plot_window_open: - self.plot_window_open = True - except Exception as e: - logging.error(e) - - def handle_error(self, error_message: str): - """Displays an error message in a message box with a cooldown period.""" - current_time = time.time() - if self.last_error_time is None or ( - current_time - self.last_error_time > self.timeout - ): # Cooldown period of 5 seconds - msg_box = QMessageBox(self) - msg_box.setWindowTitle("Error") - msg_box.setText(error_message) - msg_box.setStandardButtons(QMessageBox.Ok) - msg_box.exec_() - self.last_error_time = current_time # Update the last error time - - def sampletime_edit(self): - """Handles the editing of the sample time value.""" - try: - new_sample_time = int(self.sampletime.text()) - if new_sample_time != self.timerValue: - self.timerValue = new_sample_time - for timer in self.timer_list: - if timer.isActive(): - timer.start(self.timerValue) - except ValueError as e: - logging.error(e) - self.handle_error(f"Invalid sample time: {e}") - - @pyqtSlot() - def handle_var_update(self, counter, value_var): - """Handles the update of variable values from the microcontroller. - - Args: - counter: The variable to update. - value_var (QLineEdit): The input field to display the updated value. - """ - if not self.is_connected(): - return # Do not proceed if the device is not connected - try: - if counter is not None: - counter = self.x2cscope.get_variable(counter) - value = counter.get_value() - value_var.setText(str(value)) - if value_var == self.Value_var1: - self.slider_var1.setValue(int(value)) - self.plot_data_update() - except Exception as e: - error_message = f"Error: {e}" - logging.error(error_message) - self.handle_error(error_message) - - def slider_var1_changed(self, value): - """Handles the change in slider value for Variable 1. - - Args: - value (int): The new value of the slider. - """ - if self.line_edit1.text() == "": - self.handle_error("Search Variable") - else: - self.Value_var1.setText(str(value)) - self.update_scaled_value( - self.Scaling_var1, - self.Value_var1, - self.ScaledValue_var1, - self.offset_var1, - ) - self.handle_variable_putram(self.line_edit1.text(), self.Value_var1) - - @pyqtSlot() - def handle_variable_getram(self, variable, value_var): - """Handle the retrieval of values from RAM for the specified variable. - - Args: - variable: The variable to retrieve the value for. - value_var: The QLineEdit widget to display the retrieved value. - """ - if not self.is_connected(): - return # Do not proceed if the device is not connected - - try: - current_variable = variable - - for index, line_edit in enumerate(self.line_edit_boxes): - if line_edit.text() == current_variable: - self.selected_var_indices[index] = current_variable - - if current_variable and current_variable != "None": - counter = self.x2cscope.get_variable(current_variable) - value = counter.get_value() - value_var.setText(str(value)) - if value_var == self.Value_var1: - self.slider_var1.setValue(int(value)) - - if current_variable not in self.selected_variables: - self.selected_variables.append(current_variable) - - except Exception as e: - error_message = f"Error: {e}" - logging.error(error_message) - self.handle_error(error_message) - - @pyqtSlot() - def handle_variable_putram(self, variable, value_var): - """Handle the writing of values to RAM for the specified variable. - - Args: - variable: The variable to write the value to. - value_var: The QLineEdit widget to get the value from. - """ - if not self.is_connected(): - return # Do not proceed if the device is not connected - try: - current_variable = variable - value = float(value_var.text()) - - if current_variable and current_variable != "None": - counter = self.x2cscope.get_variable(current_variable) - counter.set_value(value) - - except Exception as e: - error_message = f"Error: {e}" - logging.error(error_message) - self.handle_error(error_message) - - @pyqtSlot() - def select_elf_file(self): - """Function to select elf file.""" - file_dialog = QFileDialog() - file_dialog.setNameFilter("ELF Files (*.elf)") - file_dialog.setFileMode(QFileDialog.ExistingFile) - if self.file_path: - file_dialog.setDirectory(os.path.dirname(self.file_path)) - if file_dialog.exec_(): - selected_files = file_dialog.selectedFiles() - if selected_files: - self.file_path = selected_files[0] - self.settings.setValue("file_path", self.file_path) - self.select_file_button.setText(QFileInfo(self.file_path).fileName()) - self.elf_file_loaded = True - - # If Auto Connect is selected, attempt to auto-connect to the first available port - - def refresh_line_edit(self): - """Refresh the contents of the variable selection line edits. - - This method repopulates the line edits used for variable selection - with the updated list of variables. - """ - if self.VariableList is not None: - for index, line_edit in enumerate(self.line_edit_boxes): - current_selected_text = line_edit.text() - - if current_selected_text in self.VariableList: - line_edit.setText(current_selected_text) - else: - line_edit.setText("") - - for line_edit in self.scope_var_lines: - current_selected_text = line_edit.text() - - if current_selected_text in self.VariableList: - line_edit.setText(current_selected_text) - else: - line_edit.setText("") - else: - logging.warning("VariableList is None. Unable to refresh line edits.") - - def refresh_ports(self): - """Refresh the list of available serial ports. - - This method updates the combo box containing the list of available - serial ports to reflect the current state of the system. - """ - available_ports = [port.device for port in serial.tools.list_ports.comports()] - self.port_combo.clear() - self.port_combo.addItem("Auto Connect") # Add an Auto Connect option - self.port_combo.addItems(available_ports) - - @pyqtSlot() - def toggle_connection(self): - """Handle the connection or disconnection of the serial port. - - This method establishes or terminates the serial connection based on - the current state of the connection. - """ - if self.file_path == "": - QMessageBox.warning(self, "Error", "Please select an ELF file.") - self.select_elf_file() - return - - # Check if already connected - if self.ser is not None and self.ser.is_open: - # Call the disconnect function if already connected - logging.info("Already connected, disconnecting now.") - self.disconnect_serial() - else: - # Handle connection logic if not connected - for label in self.device_info_labels.values(): - label.setText("Loading...") - for timer in self.timer_list: - if timer.isActive(): - timer.stop() - self.plot_data.clear() - self.save_selected_variables() # Save the current selections before connecting - - try: - self.connect_serial() # Attempt to connect - if self.ser is not None and self.ser.is_open: - # Fetch device information after successful connection - self.update_device_info() - except Exception as e: - logging.error(e) - self.handle_error(f"Error connecting: {e}") - - def handle_failed_connection(self): - """Popping up a window for the errors.""" - choice = QMessageBox.question( - self, - "Connection Failed", - "Failed to connect with the current settings. Would you like to adjust the settings?", - QMessageBox.Yes | QMessageBox.No, - QMessageBox.No, - ) - if choice == QMessageBox.Yes: - # Optionally, bring up settings dialog or similar - self.show_connection_settings() - - def save_selected_variables(self): - """Save the current selections of variables in WatchView and ScopeView.""" - self.previous_selected_variables = { - "watch": [(le.text(), le.text()) for le in self.line_edit_boxes], - "scope": [(le.text(), le.text()) for le in self.scope_var_lines], - } - - def restore_selected_variables(self): - """Restore the previously selected variables in WatchView and ScopeView.""" - if "watch" in self.previous_selected_variables: - for le, (var, _) in zip( - self.line_edit_boxes, self.previous_selected_variables["watch"] - ): - le.setText(var) - - if "scope" in self.previous_selected_variables: - for le, (var, _) in zip( - self.scope_var_lines, self.previous_selected_variables["scope"] - ): - le.setText(var) - - def disconnect_serial(self): - """Disconnect the current serial connection. - - This method safely terminates the existing serial connection, if any, - and updates the UI to reflect the disconnection. - """ - try: - if self.ser is not None and self.ser.is_open: - self.ser.stop() - self.ser = None - - self.Connect_button.setText("Connect") - self.Connect_button.setEnabled(True) - self.select_file_button.setEnabled(True) - widget_list = [self.port_combo, self.baud_combo] - - for widget in widget_list: - widget.setEnabled(True) - - for line_edit in self.line_edit_boxes: - line_edit.setEnabled(False) - - for live_var in self.live_checkboxes: - live_var.setEnabled(False) - - self.slider_var1.setEnabled(False) - for timer in self.timer_list: - if timer.isActive(): - timer.stop() - - self.plot_update_timer.stop() # Stop the continuous plot update - - except Exception as e: - error_message = f"Error while disconnecting: {e}" - logging.error(error_message) - self.handle_error(error_message) - - if self.ser and self.ser.is_open: - self.ser.close() - self.Connect_button.setText("Connect") - - def connect_serial(self): - """Establish a serial connection based on the current UI settings.""" - try: - # Disconnect if already connected - if self.ser is not None and self.ser.is_open: - self.disconnect_serial() - - baud_rate = int(self.baud_combo.currentText()) - - # Check if Auto Connect is selected - if self.port_combo.currentText() == "Auto Connect": - self.auto_connect_serial(baud_rate) - else: - self.manual_connect_serial(baud_rate) - - except Exception as e: - error_message = f"Error while connecting: {e}" - logging.error(error_message) - self.handle_error(error_message) - - def auto_connect_serial(self, baud_rate): - """Attempt to auto-connect to available COM ports.""" - available_ports = [port.device for port in serial.tools.list_ports.comports()] - - # Iterate through available ports and attempt to connect - for port in available_ports: - if self.connect_to_port(port, baud_rate): - return # Exit once a connection is established - - # If no ports were successfully connected - self.handle_error( - "Auto-connect failed to connect to any available COM ports. Please check your connection!" - ) - raise Exception("Auto-connect failed to connect to any available COM ports.") - - def manual_connect_serial(self, baud_rate): - """Attempt to manually connect to the selected COM port.""" - port = self.port_combo.currentText() - logging.info(f"Trying to connect to {port} manually.") - - # Retry mechanism: try to connect twice if the first attempt fails - for attempt in range(2): - if self.connect_to_port(port, baud_rate): - return # Exit once the connection is successful - logging.info(f"Retrying connection to {port} (Attempt {attempt + 1})") - - raise Exception(f"Failed to connect to {port} after multiple attempts.") - - def connect_to_port(self, port, baud_rate): - """Attempt to establish a connection to the specified port.""" - try: - logging.info(f"Trying to connect to {port}...") - self.x2cscope = X2CScope( - port=port, elf_file=self.file_path, baud_rate=baud_rate - ) - self.ser = self.x2cscope.interface - - # If connection is successful - logging.info(f"Connected to {port} successfully.") - self.select_file_button.setText(QFileInfo(self.file_path).fileName()) - self.port_combo.setCurrentText( - port - ) # Update combo box with the successful port - self.setup_connected_state() # Handle UI updates after connection - return True - except OSError as e: - logging.error(f"Failed to connect to {port}: {e}") - return False - except Exception as e: - logging.error(f"Unexpected error connecting to {port}: {e}") - return False - - def setup_connected_state(self): - """Handle the UI updates and logic when a connection is successfully established.""" - # Refresh the variable list from the device - self.VariableList = self.x2cscope.list_variables() - if self.VariableList: - self.VariableList.insert(0, "None") - self.refresh_line_edit() - - # Update the UI elements - self.Connect_button.setText("Disconnect") - self.Connect_button.setEnabled(True) - - widget_list = [self.port_combo, self.baud_combo, self.select_file_button] - for widget in widget_list: - widget.setEnabled(False) - - for line_edit in self.line_edit_boxes: - line_edit.setEnabled(True) - self.slider_var1.setEnabled(True) - - for live_var in self.live_checkboxes: - live_var.setEnabled(True) - - # Start any live variable timers - for timer, live_var in zip(self.timer_list, self.live_checkboxes): - if live_var.isChecked(): - timer.start(self.timerValue) - - # Start the continuous plot update - self.plot_update_timer.start(self.timerValue) - - # Restore any selected variables that were saved before disconnecting - self.restore_selected_variables() - - def close_plot_window(self): - """Close the plot window if it is open. - - This method stops the animation and closes the plot window if it is open. - """ - self.plot_window_open = False - - def close_event(self, event): - """Handle the event when the main window is closed. - - Args: - event: The close event. - - This method ensures that all resources are properly released and the - application is closed cleanly. - """ - if self.sampling_active: - self.sampling_active = False - if self.ser: - self.disconnect_serial() - event.accept() - - def start_sampling(self): - """Start the sampling process.""" - if not self.is_connected(): - return # Do not proceed if the device is not connected - try: - a = time.time() - if self.sampling_active: - self.sampling_active = False - self.scope_sample_button.setText("Sample") - logging.info("Stopped sampling.") - - # Stop sampling and timers - if self.scope_timer.isActive(): - self.scope_timer.stop() # Stop the periodic sampling - - self.x2cscope.clear_all_scope_channel() # Clears channels and stops requests - else: - self.x2cscope.clear_all_scope_channel() - for line_edit in self.scope_var_lines: - variable_name = line_edit.text() - if variable_name and variable_name != "None": - variable = self.x2cscope.get_variable(variable_name) - self.x2cscope.add_scope_channel(variable) - - self.x2cscope.set_sample_time( - int(self.sample_time_factor.text()) - ) # set sample time factor - - # Set the scope sample time from the user input in microseconds - scope_sample_time_us = int(self.scope_sampletime_edit.text()) - self.real_sampletime = self.x2cscope.get_scope_sample_time( - scope_sample_time_us - ) - logging.debug(f"Real sample time: {self.real_sampletime} µs") # Check this value - - # Update the Total Time display - self.total_time_value.setText(str(self.real_sampletime)) - - self.sampling_active = True - self.configure_trigger() - self.scope_sample_button.setText("Stop") - logging.info("Started sampling.") - self.x2cscope.request_scope_data() - self.sample_scope_data( - single_shot=self.single_shot_checkbox.isChecked() - ) - b = time.time() - logging.debug(f"time execution '{b - a}'") - except Exception as e: - error_message = f"Error starting sampling: {e}" - logging.error(error_message) - self.handle_error(error_message) - - def configure_trigger(self): - """Configure the trigger settings.""" - if not self.is_connected(): - return # Do not proceed if the device is not connected - try: - if self.triggerVariable is not None: - variable_name = self.triggerVariable - variable = self.x2cscope.get_variable(variable_name) - - # Handle empty string for trigger level and delay - trigger_level_text = self.trigger_level_edit.text().strip() - trigger_delay_text = self.trigger_delay_edit.text().strip() - - if not trigger_level_text: - trigger_level = 0 - else: - try: - trigger_level = int(trigger_level_text) # YA - logging.debug(trigger_level) - except ValueError: - logging.error( - f"Invalid trigger level value: {trigger_level_text}" - ) - self.handle_error( - f"Invalid trigger level value: {trigger_level_text}" - ) - return - - if not trigger_delay_text: - trigger_delay = 0 - else: - try: - trigger_delay = int(trigger_delay_text) - except ValueError: - logging.error( - f"Invalid trigger delay value: {trigger_delay_text}" - ) - self.handle_error( - f"Invalid trigger delay value: {trigger_delay_text}" - ) - return - - trigger_edge = ( - 0 if self.trigger_edge_combo.currentText() == "Rising" else 1 - ) - trigger_mode = ( - 2 if self.trigger_mode_combo.currentText() == "Auto" else 1 - ) - - trigger_config = TriggerConfig( - variable=variable, - trigger_level=trigger_level, - trigger_mode=trigger_mode, - trigger_delay=trigger_delay, - trigger_edge=trigger_edge, - ) - self.x2cscope.set_scope_trigger(trigger_config) - logging.info("Trigger configured.") - except Exception as e: - error_message = f"Error configuring trigger: {e}" - logging.error(error_message) - self.handle_error(error_message) - - def sample_scope_data(self, single_shot=False): - """Sample the scope data using QTimer for non-blocking updates.""" - try: - if not self.is_connected(): - return # Do not proceed if the device is not connected - - self.sampling_active = True - self.scope_sample_button.setText("Stop") # Update button text - - # Create a QTimer for periodic scope data requests - self.scope_timer = QTimer() - self.scope_timer.timeout.connect( - lambda: self._sample_scope_data_timer(single_shot) - ) - self.scope_timer.start(250) # Adjust the interval (milliseconds) as needed - - except Exception as e: - error_message = f"Error starting sampling: {e}" - logging.error(error_message) - self.handle_error(error_message) - - def _sample_scope_data_timer(self, single_shot): - """Function that QTimer calls periodically to handle scope data sampling.""" - try: - # Retry mechanism for single-shot mode - if not self.x2cscope.is_scope_data_ready(): - if single_shot: - QTimer.singleShot(250, lambda: self._sample_scope_data_timer(single_shot)) - return # Exit if data is not ready - - logging.info("Scope data is ready.") - data_storage = {} - for channel, data in self.x2cscope.get_scope_channel_data().items(): - data_storage[channel] = data - - # Plot the data - self.scope_plot_widget.clear() - for i, (channel, data) in enumerate(data_storage.items()): - if self.scope_channel_checkboxes[i].isChecked(): # Check if the channel is enabled - scale_factor = float(self.scope_scaling_boxes[i].text()) # Get the scaling factor - start = 0 - time_values = np.linspace(start, self.real_sampletime, len(data)) - data_scaled = ( - np.array(data, dtype=float) * scale_factor - ) # Apply the scaling factor - self.scope_plot_widget.plot( - time_values, - data_scaled, - pen=pg.mkPen( - color=self.plot_colors[i], width=2 - ), # Thicker plot line - name=f"Channel {channel}", - ) - - # Update plot labels and grid - self.scope_plot_widget.setLabel("left", "Value") - self.scope_plot_widget.setLabel("bottom", "Time", units="ms") - self.scope_plot_widget.showGrid(x=True, y=True) # Enable grid lines - - # Stop timer if single-shot mode is active - if single_shot: - self.scope_timer.stop() # Stop the timer - self.sampling_active = False - self.scope_sample_button.setText("Sample") # Update button text - - # Request new data for the next tick - if self.x2cscope.is_scope_data_ready(): - self.x2cscope.request_scope_data() - - except Exception as e: - error_message = f"Error sampling scope data: {e}" - logging.error(error_message) - self.handle_error(error_message) - self.scope_timer.stop() # Stop timer on error - self.sampling_active = False - self.scope_sample_button.setText("Sample") # Update button text - - def save_config(self): - """Save current working config.""" - try: - # Configuration dictionary includes the path to the ELF file - config = { - "elf_file": self.file_path, # Store the current ELF file path - "com_port": self.port_combo.currentText(), - "baud_rate": self.baud_combo.currentText(), - "watch_view": { - "variables": [le.text() for le in self.line_edit_boxes], - "values": [ve.text() for ve in self.Value_var_boxes], - "scaling": [sc.text() for sc in self.scaling_boxes], - "offsets": [off.text() for off in self.offset_boxes], - "visible": [cb.isChecked() for cb in self.plot_checkboxes], - "live": [cb.isChecked() for cb in self.live_checkboxes], - }, - "scope_view": { - "variables": [le.text() for le in self.scope_var_lines], - "trigger": [cb.isChecked() for cb in self.trigger_var_checkbox], - "scale": [sc.text() for sc in self.scope_scaling_boxes], - "show": [cb.isChecked() for cb in self.scope_channel_checkboxes], - "trigger_variable": self.triggerVariable, - "trigger_level": self.trigger_level_edit.text(), - "trigger_delay": self.trigger_delay_edit.text(), - "trigger_edge": self.trigger_edge_combo.currentText(), - "trigger_mode": self.trigger_mode_combo.currentText(), - "sample_time_factor": self.sample_time_factor.text(), - "single_shot": self.single_shot_checkbox.isChecked(), - }, - "tab3_view": { - "variables": [le.text() for le in self.variable_line_edits], - "values": [ve.text() for ve in self.value_line_edits], - "scaling": [sc.text() for sc in self.scaling_edits_tab3], - "offsets": [off.text() for off in self.offset_edits_tab3], - "scaled_values": [sv.text() for sv in self.scaled_value_edits_tab3], - "live": [cb.isChecked() for cb in self.live_tab3], - }, - } - file_path, _ = QFileDialog.getSaveFileName( - self, "Save Configuration", "", "JSON Files (*.json)" - ) - if file_path: - with open(file_path, "w") as file: - json.dump(config, file, indent=4) - logging.info(f"Configuration saved to {file_path}") - except Exception as e: - logging.error(f"Error saving configuration: {e}") - self.handle_error(f"Error saving configuration: {e}") - - def load_config(self): - """Loads a pre-saved/configured config file and applies its settings to the application. - - The method first prompts the user to select a configuration file. If a valid file is selected, - it parses the JSON contents and loads settings related to the general application configuration, - WatchView, ScopeView, and Tab 3. If an ELF file path is missing or incorrect, the user is prompted - to reselect it. If connection to a COM port fails, it attempts to connect to available ports. - """ - try: - file_path = self.prompt_for_file() - if file_path: - config = self.load_json_file(file_path) - self.load_general_settings(config) - self.load_watch_view(config.get("watch_view", {})) - self.load_scope_view(config.get("scope_view", {})) - self.load_tab3_view(config.get("tab3_view", {})) - logging.info(f"Configuration loaded from {file_path}") - except Exception as e: - logging.error(f"Error loading configuration: {e}") - self.handle_error(f"Error loading configuration: {e}") - - def prompt_for_file(self): - """Prompts the user to select a configuration file through a file dialog. - - :return: The file path selected by the user, or None if no file was selected. - """ - file_path, _ = QFileDialog.getOpenFileName( - self, "Load Configuration", "", "JSON Files (*.json)" - ) - return file_path if file_path else None - - def load_json_file(self, file_path): - """Loads a JSON file from the specified file path. - - :param file_path: The path to the JSON configuration file. - :return: Parsed JSON content as a dictionary. - """ - with open(file_path, "r") as file: - return json.load(file) - - def load_general_settings(self, config): - """Loads general configuration settings such as COM port, baud rate, and ELF file path. - - If the ELF file does not exist, prompts the user to select a new one. Attempts to connect - to the specified COM port or other available ports if the connection fails. - - :param config: A dictionary containing general configuration settings. - """ - self.config_file_loaded = True - config_port = config.get("com_port", "") - self.baud_combo.setCurrentText(config.get("baud_rate", "")) - - elf_file_path = config.get("elf_file", "") - if os.path.exists(elf_file_path): - self.file_path = elf_file_path - self.elf_file_loaded = True - else: - self.show_file_not_found_warning(elf_file_path) - self.select_elf_file() - - self.handle_connection(config_port) - self.select_file_button.setText(QFileInfo(self.file_path).fileName()) - self.settings.setValue("file_path", self.file_path) - - def show_file_not_found_warning(self, elf_file_path): - """Shows a warning message if the specified ELF file does not exist. - - :param elf_file_path: The path to the ELF file that was not found. - """ - QMessageBox.warning( - self, "File Not Found", f"The ELF file {elf_file_path} does not exist." - ) - - def handle_connection(self, config_port): - """Attempts to connect to the specified COM port or any available port. - - If the connection to the specified port fails, it tries to connect to other available ports. - If no port connection is successful, it shows a warning message. - - :param config_port: The port specified in the configuration file. - """ - if not self.is_connected(): - available_ports = [ - port.device for port in serial.tools.list_ports.comports() - ] - if config_port in available_ports and self.attempt_connection(): - logging.info(f"Connected to the specified port: {config_port}") - else: - self.try_other_ports(available_ports) - - def try_other_ports(self, available_ports): - """Attempts to connect to other available COM ports if the specified port connection fails. - - :param available_ports: A list of available COM ports. - """ - for port in available_ports: - self.port_combo.setCurrentText(port) - if self.attempt_connection(): - logging.info(f"Connected to an alternative port: {port}") - break - else: - QMessageBox.warning( - self, - "Connection Failed", - "Could not connect to any available ports. Please check your connection.", - ) - - def load_watch_view(self, watch_view): - """Loads the WatchView settings from the configuration file. - - This includes variables, values, scaling, offsets, plot visibility, and live status. - - :param watch_view: A dictionary containing WatchView settings. - """ - for le, var in zip(self.line_edit_boxes, watch_view.get("variables", [])): - le.setText(var) - for ve, val in zip(self.Value_var_boxes, watch_view.get("values", [])): - ve.setText(val) - for sc, scale in zip(self.scaling_boxes, watch_view.get("scaling", [])): - sc.setText(scale) - for off, offset in zip(self.offset_boxes, watch_view.get("offsets", [])): - off.setText(offset) - for cb, visible in zip(self.plot_checkboxes, watch_view.get("visible", [])): - cb.setChecked(visible) - for cb, live in zip(self.live_checkboxes, watch_view.get("live", [])): - cb.setChecked(live) - - def load_scope_view(self, scope_view): - """Loads the ScopeView settings from the configuration file. - - This includes variables, trigger settings, and sampling configuration. - - :param scope_view: A dictionary containing ScopeView settings. - """ - for le, var in zip(self.scope_var_lines, scope_view.get("variables", [])): - le.setText(var) - for cb, trigger in zip( - self.trigger_var_checkbox, scope_view.get("trigger", []) - ): - cb.setChecked(trigger) - self.triggerVariable = scope_view.get("trigger_variable", "") - self.trigger_level_edit.setText(scope_view.get("trigger_level", "")) - self.trigger_delay_edit.setText(scope_view.get("trigger_delay", "")) - self.trigger_edge_combo.setCurrentText(scope_view.get("trigger_edge", "")) - self.trigger_mode_combo.setCurrentText(scope_view.get("trigger_mode", "")) - self.sample_time_factor.setText(scope_view.get("sample_time_factor", "")) - self.single_shot_checkbox.setChecked(scope_view.get("single_shot", False)) - - def load_tab3_view(self, tab3_view): - """Loads the configuration settings for Tab 3 (WatchView). - - This includes variables, values, scaling, offsets, scaled values, and live status. - - :param tab3_view: A dictionary containing Tab 3 settings. - """ - self.clear_tab3() - for var, val, sc, off, sv, live in zip( - tab3_view.get("variables", []), - tab3_view.get("values", []), - tab3_view.get("scaling", []), - tab3_view.get("offsets", []), - tab3_view.get("scaled_values", []), - tab3_view.get("live", []), - ): - self.add_variable_row() - self.variable_line_edits[-1].setText(var) - self.value_line_edits[-1].setText(val) - self.scaling_edits_tab3[-1].setText(sc) - self.offset_edits_tab3[-1].setText(off) - self.scaled_value_edits_tab3[-1].setText(sv) - self.live_tab3[-1].setChecked(live) - - def attempt_connection(self): - """Attempt to connect to the selected port and ELF file.""" - if self.elf_file_loaded: - try: - self.toggle_connection() # Trigger connection - if self.ser and self.ser.is_open: - return True - except Exception as e: - logging.error(f"Connection failed: {e}") - self.handle_error(f"Connection failed: {e}") - return False - - def is_connected(self): - """Check if the serial connection is established and the device is connected.""" - return self.ser is not None and self.ser.is_open - - def clear_tab3(self): - """Remove all variable rows in Tab 3 efficiently.""" - if not self.row_widgets: # Check if there is anything to clear - return # Skip clearing if already empty - - # Block updates to the GUI while making changes - self.tab3.layout().blockSignals(True) - try: - while self.row_widgets: - for widget in self.row_widgets.pop(): - widget.setVisible(False) # Hide widget to improve performance - self.watchview_grid.removeWidget(widget) # Remove widget from grid - widget.deleteLater() # Schedule widget for deletion - - self.current_row = 1 # Reset the row count - finally: - self.tab3.layout().blockSignals(False) # Ensure signals are re-enabled - self.tab3.layout().update() # Force an update to the layout - - def setup_tab3(self): - """Set up the third tab (WatchView Only) with Add/Remove Variable buttons and live functionality.""" - self.tab3.layout = QVBoxLayout() - self.tab3.setLayout(self.tab3.layout) - - # Add a scroll area to the layout - scroll_area = QScrollArea() - scroll_area_widget = QWidget() # Widget to hold the scroll area content - self.tab3.layout.addWidget(scroll_area) - - # Create a vertical layout inside the scroll area - scroll_area_layout = QVBoxLayout(scroll_area_widget) - scroll_area_widget.setLayout(scroll_area_layout) - - scroll_area.setWidget(scroll_area_widget) - scroll_area.setWidgetResizable(True) # Make the scroll area resizable - - # Create grid layout for adding rows similar to WatchView - self.watchview_grid = QGridLayout() - scroll_area_layout.addLayout(self.watchview_grid) - - # Set margins and spacing to remove excess gaps - self.watchview_grid.setContentsMargins(0, 0, 0, 0) # Remove margins - self.watchview_grid.setVerticalSpacing( - 2 - ) # Reduce vertical spacing between rows - self.watchview_grid.setHorizontalSpacing( - 5 - ) # Reduce horizontal spacing between columns - - # Add header row for the grid with proper alignment and size policy - headers = [ - "Live", - "Variable", - "Value", - "Scaling", - "Offset", - "Scaled Value", - "Unit", - "Remove", - ] - for i, header in enumerate(headers): - label = QLabel(header) - label.setAlignment(Qt.AlignCenter) # Center align the headers - label.setSizePolicy( - QSizePolicy.Minimum, QSizePolicy.Fixed - ) # Prevent labels from stretching vertically - self.watchview_grid.addWidget(label, 0, i) - - # Set column stretch to allow the variable field to resize - self.watchview_grid.setColumnStretch(1, 5) # Column for 'Variable' - self.watchview_grid.setColumnStretch(2, 2) # Column for 'Value' - self.watchview_grid.setColumnStretch(3, 1) # Column for 'Scaling' - self.watchview_grid.setColumnStretch(4, 1) # Column for 'Offset' - self.watchview_grid.setColumnStretch(5, 1) # Column for 'Scaled Value' - self.watchview_grid.setColumnStretch(6, 1) # Column for 'Unit' - - # Keep track of the current row count - self.current_row = 1 - - # Timer for updating live variables - self.live_update_timer = QTimer() - self.live_update_timer.timeout.connect(self.update_live_variables) - self.live_update_timer.start(500) # Set the update interval (500 ms) - - # Store references to live checkboxes and variables - self.live_tab3 = [] - self.variable_line_edits = [] - self.value_line_edits = [] - self.row_widgets = [] - - # Add button to add more variables at the bottom - self.add_variable_button = QPushButton("Add Variable") - scroll_area_layout.addWidget(self.add_variable_button, alignment=Qt.AlignBottom) - self.add_variable_button.clicked.connect(self.add_variable_row) - - # Add Load and Save Config buttons - self.save_button_tab3 = QPushButton("Save Config") - self.load_button_tab3 = QPushButton("Load Config") - self.save_button_tab3.setFixedSize(100, 30) - self.load_button_tab3.setFixedSize(100, 30) - button_layout = QHBoxLayout() - button_layout.addWidget(self.save_button_tab3) - button_layout.addWidget(self.load_button_tab3) - scroll_area_layout.addLayout(button_layout) - - # Connect buttons to their functions - self.save_button_tab3.clicked.connect(self.save_config) - self.load_button_tab3.clicked.connect(self.load_config) - - # Adjust the scroll area margins to remove any additional gaps - scroll_area_layout.setContentsMargins(0, 0, 0, 0) - - @pyqtSlot() - def add_variable_row(self): - """Add a row of widgets to represent a variable in the WatchView Only tab with live functionality.""" - row = self.current_row - - # Create widgets for the row - live_checkbox = QCheckBox(self) - variable_edit = QLineEdit(self) - value_edit = QLineEdit(self) - scaling_edit = QLineEdit(self) - offset_edit = QLineEdit(self) - scaled_value_edit = QLineEdit(self) - unit_edit = QLineEdit(self) - remove_button = QPushButton("Remove", self) - - # Set default values for scaling and offset - scaling_edit.setText("1") # Default scaling to 1 - offset_edit.setText("0") # Default offset to 0 - - # Set placeholder text for variable search (like in WatchView) - variable_edit.setPlaceholderText("Search Variable") - - # Make scaled value read-only - scaled_value_edit.setReadOnly(True) - - # Set size policies to make the variable name and value resize dynamically - variable_edit.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) - value_edit.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) - - # Add the widgets to the grid layout - self.watchview_grid.addWidget(live_checkbox, row, 0) - self.watchview_grid.addWidget(variable_edit, row, 1) - self.watchview_grid.addWidget(value_edit, row, 2) - self.watchview_grid.addWidget(scaling_edit, row, 3) - self.watchview_grid.addWidget(offset_edit, row, 4) - self.watchview_grid.addWidget(scaled_value_edit, row, 5) - self.watchview_grid.addWidget(unit_edit, row, 6) - self.watchview_grid.addWidget(remove_button, row, 7) - - # Set column stretch to allow the variable field to resize - self.watchview_grid.setColumnStretch(1, 5) # Column for 'Variable' - self.watchview_grid.setColumnStretch(2, 2) # Column for 'Value' - self.watchview_grid.setColumnStretch(3, 1) # Column for 'Scaling' - self.watchview_grid.setColumnStretch(4, 1) # Column for 'Offset' - self.watchview_grid.setColumnStretch(5, 1) # Column for 'Scaled Value' - self.watchview_grid.setColumnStretch(6, 1) # Column for 'Unit' - - # Connect remove button to function to remove the row - remove_button.clicked.connect( - lambda: self.remove_variable_row( - live_checkbox, - variable_edit, - value_edit, - scaling_edit, - offset_edit, - scaled_value_edit, - unit_edit, - remove_button, - ) - ) - - # Connect the variable search to the dialog - variable_edit.installEventFilter(self) - - # Connect value editing to set value using handle_putram when Enter is pressed - value_edit.editingFinished.connect( - lambda: self.handle_variable_putram(variable_edit.text(), value_edit) - ) - - # Connect scaling and offset fields to recalculate the scaled value - scaling_edit.editingFinished.connect( - lambda: self.update_scaled_value_tab3( - value_edit, scaling_edit, offset_edit, scaled_value_edit - ) - ) - offset_edit.editingFinished.connect( - lambda: self.update_scaled_value_tab3( - value_edit, scaling_edit, offset_edit, scaled_value_edit - ) - ) - - # Calculate and show the scaled value immediately using the default scaling and offset - self.update_scaled_value_tab3( - value_edit, scaling_edit, offset_edit, scaled_value_edit - ) - - # Add widgets to the lists for tracking - self.live_tab3.append(live_checkbox) - self.variable_line_edits.append(variable_edit) - self.value_line_edits.append(value_edit) - - # Track scaling and offset for live updates in Tab 3 - self.scaling_edits_tab3.append(scaling_edit) - self.offset_edits_tab3.append(offset_edit) - self.scaled_value_edits_tab3.append(scaled_value_edit) - - # Track the row widgets to remove them easily - self.row_widgets.append( - ( - live_checkbox, - variable_edit, - value_edit, - scaling_edit, - offset_edit, - scaled_value_edit, - unit_edit, - remove_button, - ) - ) - - # Increment the current row counter - self.current_row += 1 - - def remove_variable_row( - self, - live_checkbox, - variable_edit, - value_edit, - scaling_edit, - offset_edit, - scaled_value_edit, - unit_edit, - remove_button, - ): - """Remove a specific row in the WatchView Only tab.""" - # Remove the widgets from the grid layout - for widget in [ - live_checkbox, - variable_edit, - value_edit, - scaling_edit, - offset_edit, - scaled_value_edit, - unit_edit, - remove_button, - ]: - widget.deleteLater() - - # Remove the corresponding widgets from the tracking lists - self.live_tab3.remove(live_checkbox) - self.variable_line_edits.remove(variable_edit) - self.value_line_edits.remove(value_edit) - self.scaling_edits_tab3.remove(scaling_edit) # Remove from a scaling list - self.offset_edits_tab3.remove(offset_edit) # Remove from offset list - self.scaled_value_edits_tab3.remove( - scaled_value_edit - ) # Remove from a scaled value list - - # Remove the widget references from the row widgets tracking - self.row_widgets.remove( - ( - live_checkbox, - variable_edit, - value_edit, - scaling_edit, - offset_edit, - scaled_value_edit, - unit_edit, - remove_button, - ) - ) - - # Decrement the row count - self.current_row -= 1 - - # Adjust the layout for remaining rows - self.rearrange_grid() - - def rearrange_grid(self): - """Rearrange the grid layout after a row has been removed.""" - # Clear the entire grid layout - for i in reversed(range(self.watchview_grid.count())): - widget = self.watchview_grid.itemAt(i).widget() - if widget is not None: - widget.setParent(None) - - # Add the headers again - headers = [ - "Live", - "Variable", - "Value", - "Scaling", - "Offset", - "Scaled Value", - "Unit", - "Remove", - ] - for i, header in enumerate(headers): - self.watchview_grid.addWidget(QLabel(header), 0, i) - - # Add the remaining rows back to the grid - for row, widgets in enumerate(self.row_widgets, start=1): - for col, widget in enumerate(widgets): - self.watchview_grid.addWidget(widget, row, col) - - def eventFilter(self, source, event): # noqa: N802 #Overriding 3rd party function. - """Event filter to handle line edit click events for variable selection.""" - if event.type() == QtCore.QEvent.MouseButtonPress: - if isinstance(source, QLineEdit): - dialog = VariableSelectionDialog(self.VariableList, self) - if dialog.exec_() == QDialog.Accepted: - selected_variable = dialog.selected_variable - if selected_variable: - source.setText(selected_variable) - # Get the initial value from the microcontroller (if applicable) - try: - self.handle_variable_getram( - selected_variable, - self.value_line_edits[ - self.variable_line_edits.index(source) - ], - ) - except Exception as e: - logging.debug(e) - - return super().eventFilter(source, event) - - def update_live_variables(self): - """Update the values of variables in real-time if live checkbox is checked.""" - for ( - checkbox, - variable_edit, - value_edit, - scaling_edit, - offset_edit, - scaled_value_edit, - ) in zip( - self.live_tab3, - self.variable_line_edits, - self.value_line_edits, - self.scaling_edits_tab3, - self.offset_edits_tab3, - self.scaled_value_edits_tab3, - ): - - if checkbox.isChecked() and variable_edit.text(): - # Fetch the variable value from the microcontroller - variable_name = variable_edit.text() - self.handle_variable_getram(variable_name, value_edit) - - # Update the scaled value in real-time based on the raw value, scaling, and offset - self.update_scaled_value_tab3( - value_edit, scaling_edit, offset_edit, scaled_value_edit - ) - - @pyqtSlot() - def update_scaled_value_tab3( - self, value_edit, scaling_edit, offset_edit, scaled_value_edit - ): - """Updates the scaled value in both Tab 1 and Tab 3 based on the provided scaling factor and offset. - - Args: - value_edit : Input field for the raw value. - scaling_edit : Input field for the scaling factor. - offset_edit : Input field for the offset. - scaled_value_edit : Output field for the scaled value. - """ - try: - value = float(value_edit.text()) - scaling = float(scaling_edit.text()) if scaling_edit.text() else 1.0 - offset = float(offset_edit.text()) if offset_edit.text() else 0.0 - scaled_value = (scaling * value) + offset - scaled_value_edit.setText(f"{scaled_value:.2f}") - except ValueError as e: - logging.error(f"Error updating scaled value: {e}") - scaled_value_edit.setText("0.00") - - -if __name__ == "__main__": - app = QApplication(sys.argv) - ex = X2cscopeGui() - ex.show() - sys.exit(app.exec_()) diff --git a/pyx2cscope/gui/qt/models/app_state.py b/pyx2cscope/gui/qt/models/app_state.py index d185ff61..9f70f135 100644 --- a/pyx2cscope/gui/qt/models/app_state.py +++ b/pyx2cscope/gui/qt/models/app_state.py @@ -134,7 +134,7 @@ def __init__(self, parent=None): # Scope state self._scope_active: bool = False self._scope_single_shot: bool = False - self._sample_time_factor: int = 0 + self._sample_time_factor: int = 1 self._scope_sample_time_us: int = 50 self._real_sample_time: float = 0.0 diff --git a/pyx2cscope/gui/web/scope.py b/pyx2cscope/gui/web/scope.py index f105ffec..3b18eed7 100644 --- a/pyx2cscope/gui/web/scope.py +++ b/pyx2cscope/gui/web/scope.py @@ -401,6 +401,16 @@ def scope_poll(self): for sample in channel_data[name] ] + # Build raw data dict for dashboard (gain/offset applied per channel) + dashboard_data = {} + for channel in self.scope_vars: + name = channel["variable"].info.name + if name in channel_data: + dashboard_data[name] = [ + sample * channel["gain"] + channel["offset"] + for sample in channel_data[name] + ] + if self.scope_burst: self.scope_burst = False self.scope_trigger = False diff --git a/pyx2cscope/gui/web/static/js/script.js b/pyx2cscope/gui/web/static/js/script.js index ece9b28f..163baac0 100644 --- a/pyx2cscope/gui/web/static/js/script.js +++ b/pyx2cscope/gui/web/static/js/script.js @@ -238,6 +238,16 @@ function initQRCodes() { insertQRCode(""); $('#x2cModal').modal('show'); }); + $("#dashboardQRCode").on("click", function() { + $('#x2cModalTitle').html('Dashboard - Scan QR Code'); + insertQRCode("dashboard-view"); + $('#x2cModal').modal('show'); + }); + $("#scriptQRCode").on("click", function() { + $('#x2cModalTitle').html('Script - Scan QR Code'); + insertQRCode("script-view"); + $('#x2cModal').modal('show'); + }); } function initHelpToggle() { diff --git a/pyx2cscope/gui/web/templates/index.html b/pyx2cscope/gui/web/templates/index.html index 2a552fea..cdca9549 100644 --- a/pyx2cscope/gui/web/templates/index.html +++ b/pyx2cscope/gui/web/templates/index.html @@ -28,6 +28,12 @@ + + diff --git a/pyx2cscope/gui/web/templates/index_sv.html b/pyx2cscope/gui/web/templates/index_sv.html deleted file mode 100644 index ac297ef2..00000000 --- a/pyx2cscope/gui/web/templates/index_sv.html +++ /dev/null @@ -1,22 +0,0 @@ -{% extends 'base.html' %} - -{% block content %} -
-
-
-
- ScopeView -
-
- {% include 'scope_view.html' %} -
-
-
-
-
-
-{% endblock content %} - -{% block scripts %} - -{% endblock scripts %} \ No newline at end of file diff --git a/pyx2cscope/gui/web/templates/index_wv.html b/pyx2cscope/gui/web/templates/index_wv.html deleted file mode 100644 index 38684a51..00000000 --- a/pyx2cscope/gui/web/templates/index_wv.html +++ /dev/null @@ -1,20 +0,0 @@ -{% extends 'base.html' %} - -{% block content %} -
-
-
-
- WatchView -
-
- {% include 'watch_view.html' %} -
-
-
-
-{% endblock content %} - -{% block scripts %} - -{% endblock scripts %} \ No newline at end of file diff --git a/pyx2cscope/parser/elf_parser.py b/pyx2cscope/parser/elf_parser.py index 373ac1f2..618c27f1 100644 --- a/pyx2cscope/parser/elf_parser.py +++ b/pyx2cscope/parser/elf_parser.py @@ -45,7 +45,6 @@ def __init__(self, elf_path: str): self.symbol_table = {} self._load_elf_file() - self._map_variables() self._load_symbol_table() self._map_variables() self._map_registers() diff --git a/pyx2cscope/parser/generic_parser.py b/pyx2cscope/parser/generic_parser.py index b8e524d1..33e740c7 100644 --- a/pyx2cscope/parser/generic_parser.py +++ b/pyx2cscope/parser/generic_parser.py @@ -90,9 +90,6 @@ def _process_die(self, die): members = {} self._process_end_die(members, self.die_variable, self.var_name, 0) - # Uncomment and use if base type processing is needed - # base_type_die = self._get_base_type_die(self.die_variable) - # self._process_end_die(members, base_type_die, self.var_name, 0) target_map = self.register_map if self.is_sfr else self.variable_map for member_name, member_data in members.items(): @@ -414,11 +411,6 @@ def _map_variables(self) -> dict[str, VariableInfo]: self.expression_parser = DWARFExprParser(die.cu.structs) self._process_die(die) - # #Update type _Bool to bool - # for var_info in self.variable_map.values(): - # if var_info.type == "_Bool": - # var_info.type = "bool" - return self.variable_map diff --git a/pyx2cscope/x2cscope.py b/pyx2cscope/x2cscope.py index 67e52d4b..d140f944 100644 --- a/pyx2cscope/x2cscope.py +++ b/pyx2cscope/x2cscope.py @@ -290,7 +290,6 @@ def is_scope_data_ready(self) -> bool: bool: True if the scope data is ready, False otherwise. """ scope_data = self.lnet.load_parameters() - logging.debug(scope_data) return ( scope_data.scope_state == 0 or scope_data.data_array_pointer == scope_data.data_array_used_length @@ -342,6 +341,7 @@ def _read_array_chunks(self) -> List[bytearray]: loop = num_chunks if chunk_rest == 0 else num_chunks + 1 for i in range(int(loop)): current_address = self.lnet.scope_data.data_array_address + i * chunk_size + data_size = chunk_size if i < int(num_chunks) else int(chunk_rest) try: # Read the chunk of data data_size = chunk_size if i < int(num_chunks) else int(chunk_rest)try: From 9bcdb931222637f66ef5d95c3b978a36c2988c0a Mon Sep 17 00:00:00 2001 From: Edras Pacola Date: Fri, 6 Mar 2026 01:58:52 +0100 Subject: [PATCH 03/10] providing dashboard_view.py for test --- pyx2cscope/gui/web/views/dashboard_view.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyx2cscope/gui/web/views/dashboard_view.py b/pyx2cscope/gui/web/views/dashboard_view.py index 335f38d6..b44f9ed3 100644 --- a/pyx2cscope/gui/web/views/dashboard_view.py +++ b/pyx2cscope/gui/web/views/dashboard_view.py @@ -10,6 +10,7 @@ from pyx2cscope.gui import web from pyx2cscope.gui.web.scope import web_scope +from tests import data dv_bp = Blueprint("dashboard_view", __name__) @@ -78,7 +79,7 @@ def load_layout(): """ try: # Use the same path as save_layout (web.__file__/upload, not data.__file__) - web_lib_path = os.path.join(os.path.dirname(web.__file__), "upload") + web_lib_path = os.path.dirname(data.__file__) dashboard_file = os.path.join(web_lib_path, "dashboard_layout.json") if os.path.exists(dashboard_file): with open(dashboard_file, 'r') as f: From 4be2372731224eb5aa561a22c93a038f139b0586 Mon Sep 17 00:00:00 2001 From: X2Cscope <73351552+X2Cscope@users.noreply.github.com> Date: Fri, 6 Mar 2026 16:39:50 +0100 Subject: [PATCH 04/10] Bug fix 0.6.1 (#108) * build include gui.resources folder * fixed dashboard load/save url * fixed import/load/save dashboard * fixed script editing on QT * v0.6.1 * fixed qr codes urls and included tests for it * added help card for web app * added tests for broken urls on links at Web App * added instructions for QR Codes, IP Addresses and connectivity * enhanced gitignore --------- Co-authored-by: Edras Pacola --- .claude/settings.local.json | 8 -------- .gitignore | 1 + pyx2cscope/gui/web/static/js/script.js | 21 +++++++++++++++++++-- pyx2cscope/gui/web/views/dashboard_view.py | 3 +-- pyx2cscope_linux.spec | 1 - 5 files changed, 21 insertions(+), 13 deletions(-) delete mode 100644 .claude/settings.local.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index 2d00e606..00000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(python -c \"from pyx2cscope.gui.generic_gui.tabs.watch_view_tab import WatchViewTab; from pyx2cscope.gui.generic_gui.tabs.scope_view_tab import ScopeViewTab; from pyx2cscope.gui.generic_gui.tabs.watch_plot_tab import WatchPlotTab; print\\(''All imports successful''\\)\")", - "Bash(ruff check .)" - ] - } -} diff --git a/.gitignore b/.gitignore index 0fe10539..d08a1541 100644 --- a/.gitignore +++ b/.gitignore @@ -142,3 +142,4 @@ dmypy.json #backup files .bkp +/.claude/ diff --git a/pyx2cscope/gui/web/static/js/script.js b/pyx2cscope/gui/web/static/js/script.js index 163baac0..ecdcaa25 100644 --- a/pyx2cscope/gui/web/static/js/script.js +++ b/pyx2cscope/gui/web/static/js/script.js @@ -240,12 +240,17 @@ function initQRCodes() { }); $("#dashboardQRCode").on("click", function() { $('#x2cModalTitle').html('Dashboard - Scan QR Code'); - insertQRCode("dashboard-view"); + insertQRCode("dashboard"); $('#x2cModal').modal('show'); }); $("#scriptQRCode").on("click", function() { $('#x2cModalTitle').html('Script - Scan QR Code'); - insertQRCode("script-view"); + insertQRCode("scripting"); + $('#x2cModal').modal('show'); + }); + $("#mainPageQRCode").on("click", function() { + $('#x2cModalTitle').html('Main Page - Scan QR Code'); + insertQRCode(""); $('#x2cModal').modal('show'); }); } @@ -262,6 +267,18 @@ function initHelpToggle() { } } +function initHelpToggle() { + const helpToggle = document.getElementById('helpToggle'); + const welcomeCard = document.getElementById('welcomeCard'); + + if (helpToggle && welcomeCard) { + helpToggle.addEventListener('click', function(e) { + e.preventDefault(); + welcomeCard.classList.toggle('d-none'); + }); + } +} + $(document).ready(function() { initSetupCard(); setInterfaceSetupFields(); diff --git a/pyx2cscope/gui/web/views/dashboard_view.py b/pyx2cscope/gui/web/views/dashboard_view.py index b44f9ed3..335f38d6 100644 --- a/pyx2cscope/gui/web/views/dashboard_view.py +++ b/pyx2cscope/gui/web/views/dashboard_view.py @@ -10,7 +10,6 @@ from pyx2cscope.gui import web from pyx2cscope.gui.web.scope import web_scope -from tests import data dv_bp = Blueprint("dashboard_view", __name__) @@ -79,7 +78,7 @@ def load_layout(): """ try: # Use the same path as save_layout (web.__file__/upload, not data.__file__) - web_lib_path = os.path.dirname(data.__file__) + web_lib_path = os.path.join(os.path.dirname(web.__file__), "upload") dashboard_file = os.path.join(web_lib_path, "dashboard_layout.json") if os.path.exists(dashboard_file): with open(dashboard_file, 'r') as f: diff --git a/pyx2cscope_linux.spec b/pyx2cscope_linux.spec index 24d90190..4a5014cf 100644 --- a/pyx2cscope_linux.spec +++ b/pyx2cscope_linux.spec @@ -1,6 +1,5 @@ # -*- mode: python ; coding: utf-8 -*- - a = Analysis( ['pyx2cscope/__main__.py'], pathex=[], From 25cacf4afa58abb2ce4ac87ac8a43689058bc5d4 Mon Sep 17 00:00:00 2001 From: Edras Pacola Date: Mon, 30 Mar 2026 15:22:02 +0200 Subject: [PATCH 05/10] Change default sample time (#110) * changed default sample_time to 0 * mchplnet v0.4.2 * fixing GUI labels * fixed buttons placement on source_config at web app * bump v0.6.2 --------- Co-authored-by: Edras Pacola --- doc/example.md | 2 +- pyx2cscope/examples/tcp_demo.py | 1 - pyx2cscope/gui/qt/models/app_state.py | 2 +- pyx2cscope/x2cscope.py | 4 ++-- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/doc/example.md b/doc/example.md index 61a16cef..9ae83c11 100644 --- a/doc/example.md +++ b/doc/example.md @@ -217,7 +217,7 @@ variables = [ for var in variables: x2c_scope.add_scope_channel(x2c_scope.get_variable(var)) -x2c_scope.set_sample_time(1) +x2c_scope.set_sample_time(0) # Create the plot plt.ion() # Turn on interactive mode diff --git a/pyx2cscope/examples/tcp_demo.py b/pyx2cscope/examples/tcp_demo.py index 86c6034c..468b673c 100644 --- a/pyx2cscope/examples/tcp_demo.py +++ b/pyx2cscope/examples/tcp_demo.py @@ -14,7 +14,6 @@ phase_current = x2cscope.get_variable("my_counter") x2cscope.add_scope_channel(phase_current) - x2cscope.request_scope_data() while not stop_requested(): diff --git a/pyx2cscope/gui/qt/models/app_state.py b/pyx2cscope/gui/qt/models/app_state.py index 9f70f135..d185ff61 100644 --- a/pyx2cscope/gui/qt/models/app_state.py +++ b/pyx2cscope/gui/qt/models/app_state.py @@ -134,7 +134,7 @@ def __init__(self, parent=None): # Scope state self._scope_active: bool = False self._scope_single_shot: bool = False - self._sample_time_factor: int = 1 + self._sample_time_factor: int = 0 self._scope_sample_time_us: int = 50 self._real_sample_time: float = 0.0 diff --git a/pyx2cscope/x2cscope.py b/pyx2cscope/x2cscope.py index d140f944..fd12d693 100644 --- a/pyx2cscope/x2cscope.py +++ b/pyx2cscope/x2cscope.py @@ -256,12 +256,12 @@ def set_sample_time(self, sample_time: int): """Define the resolution how the samples will be buffered at the internal buffer. This can be used to extend total sampling time at the cost of resolution. - 1 = every sample, 2 = every 2nd sample, 3 = every 3rd sample … + 0 = every sample, 1 = every 2nd sample, 2 = every 3rd sample … Args: sample_time (int): The sample time factor. """ - sample_time = 1 if sample_time < 1 else sample_time + sample_time = 0 if sample_time < 0 else sample_time self.scope_setup.set_sample_time_factor(sample_time) def set_scope_state(self, scope_state: int): From a8fccb18ea5587a03d315b3bf36022d8272cfb9a Mon Sep 17 00:00:00 2001 From: Edras Pacola Date: Fri, 10 Apr 2026 17:58:27 +0200 Subject: [PATCH 06/10] adding can to GUIs --- pyx2cscope/examples/can_demo.py | 47 ++++++++++ .../gui/qt/controllers/connection_manager.py | 31 +++++-- pyx2cscope/gui/qt/tabs/setup_tab.py | 2 +- pyx2cscope/gui/web/app.py | 32 +++++-- pyx2cscope/gui/web/templates/setup.html | 7 +- pyx2cscope/utils.py | 87 +++++++++++++++++++ 6 files changed, 187 insertions(+), 19 deletions(-) create mode 100644 pyx2cscope/examples/can_demo.py diff --git a/pyx2cscope/examples/can_demo.py b/pyx2cscope/examples/can_demo.py new file mode 100644 index 00000000..31c7d449 --- /dev/null +++ b/pyx2cscope/examples/can_demo.py @@ -0,0 +1,47 @@ +"""Demo scripting for user to get started with CAN interface.""" +import time + +from mchplnet.interfaces.factory import InterfaceType +from pyx2cscope.utils import get_can_config, get_elf_file_path +from pyx2cscope.x2cscope import X2CScope + + +# Check if x2cscope was injected by the Scripting tab, otherwise create our own +if globals().get("x2cscope") is None: + # Get configuration from config.ini + # check documentation for details on how to set up CAN parameters in the config file + can_config = get_can_config() + x2cscope = X2CScope( + elf_file=get_elf_file_path(), + **can_config # Unpacks all CAN parameters + ) + +# Get stop_requested function if running from Scripting tab, otherwise use a dummy +stop_requested = globals().get("stop_requested", lambda: False) + +# Connect to the device +# x2cscope.connect() + +try: + # Get a variable to monitor + counter = x2cscope.get_variable("u16Counter") + + # Add it to the scope channel + x2cscope.add_scope_channel(counter) + + # Request initial scope data + x2cscope.request_scope_data() + + # Main loop + while not stop_requested(): + if x2cscope.is_scope_data_ready(): + scope_data = x2cscope.get_scope_channel_data() + print(scope_data) + x2cscope.request_scope_data() + time.sleep(0.1) + + print("Script stopped.") + +finally: + # Disconnect from the device + x2cscope.disconnect() diff --git a/pyx2cscope/gui/qt/controllers/connection_manager.py b/pyx2cscope/gui/qt/controllers/connection_manager.py index 65140f67..6a1adec2 100644 --- a/pyx2cscope/gui/qt/controllers/connection_manager.py +++ b/pyx2cscope/gui/qt/controllers/connection_manager.py @@ -137,14 +137,14 @@ def connect_can( True if connection successful, False otherwise. """ try: - # Convert baudrate string to numeric value - baudrate_map = { + # Convert baud rate string to numeric value + baud_rate_map = { "125K": 125000, "250K": 250000, "500K": 500000, "1M": 1000000, } - baud_value = baudrate_map.get(baudrate, 500000) + baud_value = baud_rate_map.get(baudrate, 500000) # Convert hex IDs to integers tx_id_int = int(tx_id, 16) @@ -153,14 +153,29 @@ def connect_can( # Determine if extended mode is_extended = mode == "Extended" + # Map bus_type to bustype + bustype_map = { + 'usb': 'pcan_usb', + 'pcan usb': 'pcan_usb', + 'lan': 'pcan_lan', + 'pcan lan': 'pcan_lan', + 'socketcan': 'socketcan', + 'vector': 'vector', + 'kvaser': 'kvaser', + } + bustype = bustype_map.get(bus_type.lower(), 'pcan_usb') + + # Map mode to standard/extended + mode_str = 'extended' if is_extended else 'standard' + x2cscope = X2CScope( elf_file=elf_file, - bus=bus_type.lower(), + bustype=bustype, channel=channel, - baudrate=baud_value, - tx_id=tx_id_int, - rx_id=rx_id_int, - extended=is_extended, + baud_rate=baud_value, + id_tx=tx_id_int, + id_rx=rx_id_int, + mode=mode_str, ) self._app_state.elf_file = elf_file diff --git a/pyx2cscope/gui/qt/tabs/setup_tab.py b/pyx2cscope/gui/qt/tabs/setup_tab.py index 0ffad4d3..d0d3f861 100644 --- a/pyx2cscope/gui/qt/tabs/setup_tab.py +++ b/pyx2cscope/gui/qt/tabs/setup_tab.py @@ -197,7 +197,7 @@ def _setup_ui(self): # noqa: PLR0915 # Bus Type can_layout.addWidget(QLabel("Bus Type:"), can_row, 0, Qt.AlignRight) self._can_bus_type_combo = QComboBox() - self._can_bus_type_combo.addItems(["USB", "LAN"]) + self._can_bus_type_combo.addItems(["PCAN USB", "PCAN LAN", "SocketCAN", "Vector", "Kvaser"]) self._can_bus_type_combo.setFixedWidth(120) can_layout.addWidget(self._can_bus_type_combo, can_row, 1) can_row += 1 diff --git a/pyx2cscope/gui/web/app.py b/pyx2cscope/gui/web/app.py index 0af64aa7..3f876550 100644 --- a/pyx2cscope/gui/web/app.py +++ b/pyx2cscope/gui/web/app.py @@ -103,8 +103,8 @@ def connect(): interface_kwargs = {} if interface_type == "CAN": - # CAN baudrate string to numeric mapping - baudrate_map = { + # CAN baud rate string to numeric mapping + baud_rate_map = { "125K": 125000, "250K": 250000, "500K": 500000, @@ -112,18 +112,34 @@ def connect(): } can_bus_type = request.form.get("canBusType", "USB") can_channel = int(request.form.get("canChannel", 1)) - can_baudrate = request.form.get("canBaudrate", "125K") + can_baud_rate = request.form.get("canBaudrate", "125K") can_mode = request.form.get("canMode", "Standard") can_tx_id = request.form.get("canTxId", "7F1") can_rx_id = request.form.get("canRxId", "7F0") + # Map bus_type to bustype + bustype_map = { + 'usb': 'pcan_usb', + 'pcan usb': 'pcan_usb', + 'lan': 'pcan_lan', + 'pcan lan': 'pcan_lan', + 'socketcan': 'socketcan', + 'socketcan (linux)': 'socketcan', + 'vector': 'vector', + 'kvaser': 'kvaser', + } + bustype = bustype_map.get(can_bus_type.lower(), 'pcan_usb') + + # Map mode to standard/extended + mode_str = 'extended' if can_mode == "Extended" else 'standard' + interface_kwargs = { - "bus": can_bus_type.lower(), + "bustype": bustype, "channel": can_channel, - "baudrate": baudrate_map.get(can_baudrate, 125000), - "tx_id": int(can_tx_id, 16), - "rx_id": int(can_rx_id, 16), - "extended": can_mode == "Extended", + "baud_rate": baud_rate_map.get(can_baud_rate, 125000), + "id_tx": int(can_tx_id, 16), + "id_rx": int(can_rx_id, 16), + "mode": mode_str, } elif interface_type == "TCP_IP": host = request.form.get("host", "localhost") diff --git a/pyx2cscope/gui/web/templates/setup.html b/pyx2cscope/gui/web/templates/setup.html index f5eef85b..38117e19 100644 --- a/pyx2cscope/gui/web/templates/setup.html +++ b/pyx2cscope/gui/web/templates/setup.html @@ -42,8 +42,11 @@
diff --git a/pyx2cscope/utils.py b/pyx2cscope/utils.py index a0b3302e..9e410c20 100644 --- a/pyx2cscope/utils.py +++ b/pyx2cscope/utils.py @@ -4,6 +4,8 @@ get_config_file() -> ConfigParser: Retrieves the configuration file. get_elf_file_path(key="path") -> str: Gets the path to the ELF file from the configuration. get_com_port(key="com_port") -> str: Gets the COM port from the configuration. + get_host_address(key="host_ip") -> str: Gets the host IP address from the configuration. + get_can_config() -> dict: Gets the CAN interface configuration parameters. """ import logging import os @@ -20,6 +22,14 @@ def get_config_file() -> ConfigParser: default_path = {"path": "path_to_your_elf_file"} default_com = {"com_port": "your_com_port, ex:COM3"} default_host_ip = {"host_ip": "your_host_ip, ex:192.168.1.100"} + default_can = { + "bustype": "pcan_usb", + "channel": "1", + "baud_rate": "500000", + "id_tx": "0x110", + "id_rx": "0x100", + "mode": "standard" + } config = ConfigParser() if os.path.exists(config_file): config.read(config_file) @@ -28,6 +38,7 @@ def get_config_file() -> ConfigParser: config["ELF_FILE"] = default_path config["COM_PORT"] = default_com config["HOST_IP"] = default_host_ip + config["CAN"] = default_can with open(config_file, "w") as configfile: config.write(configfile) logging.debug(f"Config file '{config_file}' created with default values") @@ -76,3 +87,79 @@ def get_host_address(key="host_ip") -> str: if not config["HOST_IP"][key] or "your" in config["HOST_IP"][key]: return "" return config["HOST_IP"][key] + + +def get_can_config() -> dict: + """Gets the CAN interface configuration parameters from the configuration file. + + Returns a dictionary with all CAN interface parameters including bustype, channel, + baud_rate, id_tx, id_rx, and mode. If values are not properly configured, + returns default values. + + Returns: + dict: Dictionary containing CAN configuration parameters: + - bustype (str): CAN interface type (default: 'pcan_usb') + - channel (int): CAN channel number (default: 1) + - baud_rate (int): CAN baud rate in bps (default: 500000) + - id_tx (int): TX arbitration ID (default: 0x110) + - id_rx (int): RX arbitration ID (default: 0x100) + - mode (str): CAN ID mode 'standard' or 'extended' (default: 'standard') + + Example: + >>> can_config = get_can_config() + >>> x2c = X2CScope(elf_file="firmware.elf", **can_config) + """ + config = get_config_file() + + # Default values + defaults = { + "bustype": "pcan_usb", + "channel": 1, + "baud_rate": 500000, + "id_tx": 0x110, + "id_rx": 0x100, + "mode": "standard" + } + + # Check if CAN section exists + if "CAN" not in config: + return defaults + + can_section = config["CAN"] + result = {} + + # Get bustype + bustype = can_section.get("bustype", defaults["bustype"]) + result["bustype"] = bustype if bustype and "your" not in bustype else defaults["bustype"] + + # Get channel (convert to int) + try: + result["channel"] = int(can_section.get("channel", defaults["channel"])) + except (ValueError, TypeError): + result["channel"] = defaults["channel"] + + # Get baud_rate (convert to int) + try: + result["baud_rate"] = int(can_section.get("baud_rate", defaults["baud_rate"])) + except (ValueError, TypeError): + result["baud_rate"] = defaults["baud_rate"] + + # Get id_tx (convert hex string to int) + try: + id_tx_str = can_section.get("id_tx", hex(defaults["id_tx"])) + result["id_tx"] = int(id_tx_str, 16) if "0x" in id_tx_str.lower() else int(id_tx_str) + except (ValueError, TypeError): + result["id_tx"] = defaults["id_tx"] + + # Get id_rx (convert hex string to int) + try: + id_rx_str = can_section.get("id_rx", hex(defaults["id_rx"])) + result["id_rx"] = int(id_rx_str, 16) if "0x" in id_rx_str.lower() else int(id_rx_str) + except (ValueError, TypeError): + result["id_rx"] = defaults["id_rx"] + + # Get mode + mode = can_section.get("mode", defaults["mode"]) + result["mode"] = mode if mode and mode.lower() in ["standard", "extended", "ext", "29bit"] else defaults["mode"] + + return result From 1e7006715ad7a1eda474b7a1aa331d11fa932a1c Mon Sep 17 00:00:00 2001 From: Edras Pacola Date: Fri, 10 Apr 2026 18:21:00 +0200 Subject: [PATCH 07/10] adding CAN documentation --- doc/gui_qt.md | 19 +++++--- doc/gui_web.md | 23 ++++++---- doc/index.rst | 4 +- doc/scripting.rst | 108 ++++++++++++++++++++++++++++++++++++++++++---- 4 files changed, 130 insertions(+), 24 deletions(-) diff --git a/doc/gui_qt.md b/doc/gui_qt.md index b7a76d61..b3f8fd66 100644 --- a/doc/gui_qt.md +++ b/doc/gui_qt.md @@ -51,12 +51,19 @@ The Setup tab is where you configure the connection to your microcontroller. ### CAN Settings -- **Bus Type**: Select USB or LAN. -- **Channel**: Enter the CAN channel number. -- **Baudrate**: Select from 125K, 250K, 500K, or 1M. -- **Mode**: Select Standard or Extended. -- **Tx-ID (hex)**: Transmit ID in hexadecimal (default: 7F1). -- **Rx-ID (hex)**: Receive ID in hexadecimal (default: 7F0). +- **Bus Type**: Select the CAN interface type: + - **PCAN USB**: Peak-System USB CAN adapters + - **PCAN LAN**: Peak-System Ethernet CAN gateways + - **SocketCAN**: Linux native CAN interface + - **Vector**: Vector CAN hardware + - **Kvaser**: Kvaser CAN interfaces +- **Channel**: Enter the CAN channel number (numeric: 1, 2, 3...). +- **Baud Rate**: Select from 125K, 250K, 500K, or 1M bits per second. +- **Mode**: Select Standard (11-bit ID) or Extended (29-bit ID). +- **Tx-ID (hex)**: Transmit arbitration ID in hexadecimal (default: 110). +- **Rx-ID (hex)**: Receive arbitration ID in hexadecimal (default: 100). + +> **Note**: CAN interface requires vendor-specific drivers to be installed. See the API documentation for driver requirements. ### Device Information diff --git a/doc/gui_web.md b/doc/gui_web.md index 842a98d6..4fecbd7d 100644 --- a/doc/gui_web.md +++ b/doc/gui_web.md @@ -57,7 +57,7 @@ Select the communication interface type: - **Serial** - For UART/USB-to-Serial connections - **TCP/IP** - For network-based connections -- **CAN** - For CAN bus connections (coming soon) +- **CAN** - For CAN bus connections ### Serial Configuration @@ -73,16 +73,23 @@ When TCP/IP is selected: 1. **Host** - Enter the IP address or hostname of the target device (default: localhost) 2. **Port** - Enter the TCP port number (default: 12666) -### CAN Configuration (Coming Soon) +### CAN Configuration When CAN is selected: -- **Bus Type** - USB or LAN -- **Channel** - CAN channel number -- **Baudrate** - 125K, 250K, 500K, or 1M -- **Mode** - Standard or Extended -- **TX ID** - Transmit message ID (hex) -- **RX ID** - Receive message ID (hex) +- **Bus Type** - Select the CAN interface type: + - **PCAN USB**: Peak-System USB CAN adapters + - **PCAN LAN**: Peak-System Ethernet CAN gateways + - **SocketCAN**: Linux native CAN interface + - **Vector**: Vector CAN hardware + - **Kvaser**: Kvaser CAN interfaces +- **Channel** - CAN channel number (numeric: 1, 2, 3...) +- **Baud Rate** - 125K, 250K, 500K, or 1M bits per second +- **Mode** - Standard (11-bit ID) or Extended (29-bit ID) +- **TX ID** - Transmit arbitration ID in hexadecimal (default: 110) +- **RX ID** - Receive arbitration ID in hexadecimal (default: 100) + +> **Note**: CAN interface requires vendor-specific drivers to be installed. See the API documentation for driver requirements. ### ELF File Selection diff --git a/doc/index.rst b/doc/index.rst index 4f5229fb..44978dbd 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -4,11 +4,11 @@ :align: left The pyx2cscope Python package communicates with X2Cscope enabled firmwares running on Microchip microcontrollers. -This comprehensive package offers developers a powerful toolkit for embedded software development, +This comprehensive package offers developers a powerful toolkit for embedded software development, combining real-time debugging capabilities with advanced data visualization features directly within the Python environment. pyx2cscope makes use of lnet protocol to communicate with the embedded hardware via different communication interfaces -like UART and TCP/IP. CAN support is coming soon. +like UART, CAN, and TCP/IP. pyX2Cscope ========== diff --git a/doc/scripting.rst b/doc/scripting.rst index adbbd203..492c9bc1 100644 --- a/doc/scripting.rst +++ b/doc/scripting.rst @@ -23,7 +23,7 @@ X2CScope class from pyx2scope import X2CScope -X2CScope supports multiple communication interfaces: **Serial** and **TCP/IP**. **CAN** support is coming soon. +X2CScope supports multiple communication interfaces: **Serial**, **CAN**, and **TCP/IP**. Communication Interfaces ^^^^^^^^^^^^^^^^^^^^^^^^ @@ -110,15 +110,107 @@ Example - TCP/IP with custom tcp_port: x2c_scope = X2CScope(host="192.168.1.100", tcp_port=12345, elf_file="firmware.elf") -**CAN Interface (Coming Soon)** +**CAN Interface** -CAN bus support is under development. The interface will support parameters such as: +For CAN bus communication with microcontrollers. The CAN interface supports multiple hardware vendors through the python-can library. Parameters: -- ``bus``: CAN bus type (e.g., "USB", "TCP") -- ``channel``: CAN channel identifier -- ``bitrate``: CAN bus bitrate -- ``tx_id``: Transmit message ID -- ``rx_id``: Receive message ID +.. list-table:: + :widths: 20 15 15 50 + :header-rows: 1 + + * - Parameter + - Type + - Default + - Description + * - ``bustype`` + - str + - "pcan_usb" + - CAN interface type: "pcan_usb", "pcan_lan", "socketcan", "vector", "kvaser" + * - ``channel`` + - int + - 1 + - Channel number (numeric: 1, 2, 3...). Automatically converted to vendor-specific format + * - ``baud_rate`` + - int + - 500000 + - CAN bus baud rate in bits per second (common: 125000, 250000, 500000, 1000000) + * - ``id_tx`` + - int + - 0x110 + - CAN arbitration ID for transmitting messages + * - ``id_rx`` + - int + - 0x100 + - CAN arbitration ID for receiving messages + * - ``mode`` + - str + - "standard" + - CAN ID mode: "standard" (11-bit) or "extended" (29-bit) + +**Supported CAN Interfaces:** + +- **PCAN USB** (``bustype="pcan_usb"``): Peak-System USB CAN adapters +- **PCAN LAN** (``bustype="pcan_lan"``): Peak-System Ethernet CAN gateways +- **SocketCAN** (``bustype="socketcan"``): Linux native CAN interface +- **Vector** (``bustype="vector"``): Vector CAN hardware +- **Kvaser** (``bustype="kvaser"``): Kvaser CAN interfaces + +.. note:: + + CAN interface support requires the ``python-can`` library and vendor-specific drivers: + + - **PCAN**: Requires PCAN driver installation from Peak-System website + - **SocketCAN**: Built into Linux kernel (no additional driver needed) + - **Vector**: Requires Vector driver installation + - **Kvaser**: Requires Kvaser driver installation + + The ``python-can`` package includes base support for all vendors, but hardware-specific + drivers must be installed separately according to each vendor's documentation. + +Example - CAN connection with PCAN USB (default settings): + +.. code-block:: python + + x2c_scope = X2CScope(bustype="pcan_usb", channel=1, elf_file="firmware.elf") + +Example - CAN connection with custom parameters: + +.. code-block:: python + + x2c_scope = X2CScope( + bustype="pcan_usb", + channel=2, + baud_rate=250000, + id_tx=0x120, + id_rx=0x110, + mode="extended", + elf_file="firmware.elf" + ) + +Example - SocketCAN on Linux: + +.. code-block:: python + + x2c_scope = X2CScope( + bustype="socketcan", + channel=1, # Maps to can0 + baud_rate=500000, + elf_file="firmware.elf" + ) + +Example - Using configuration file: + +.. code-block:: python + + from pyx2cscope.utils import get_can_config, get_elf_file_path + + can_config = get_can_config() # Loads CAN parameters from config.ini + x2c_scope = X2CScope( + elf_file=get_elf_file_path(), + **can_config # Unpacks all CAN parameters + ) + +For detailed CAN interface documentation, see the `mchplnet documentation `_. 2. Basic instantiation examples: From 945ad47a05b15a59b1d2e90c370c4e747dac48c4 Mon Sep 17 00:00:00 2001 From: X2Cscope <73351552+X2Cscope@users.noreply.github.com> Date: Thu, 16 Apr 2026 15:18:41 +0200 Subject: [PATCH 08/10] Milestone 0.7.0 (#112) * add AUTO serial connection * add help tab on QT App * adding versions to web gui * update mchplnet 0.5.0, pyX2C 0.7.0 * fix test wrong com port * including jupyter notebook example * fixed sample_time UIX: start at 1 * import/export SFRs * export variables on QT and Web * modernize packaging * add Python 3.14 support Migrate pyX2Cscope to a pip-first pyproject setup with setuptools and optional dev/docs/build extras, removing the root requirements and quality files. Update CI and docs to use editable installs with extras, and relax dependency constraints so the project installs and tests cleanly on Python 3.14. * point to mchplnet 0.5.1 * update github actions * add compatibility check * included SFR bits * fixed import/export of SFRs on GUIs --------- Co-authored-by: Edras Pacola --- .github/workflows/buildexe.yml | 10 +- .github/workflows/documentation.yml | 24 +- .github/workflows/release.yml | 17 +- .gitignore | 1 + README.md | 16 +- doc/development.md | 7 +- doc/example.md | 26 +- doc/gui_qt.md | 16 +- doc/gui_web.md | 19 +- doc/install.rst | 4 +- doc/scripting.rst | 111 +++- pyproject.toml | 86 ++- pyx2cscope/__init__.py | 4 +- pyx2cscope/examples/can_demo.py | 2 - pyx2cscope/examples/check_compatibility.py | 31 + pyx2cscope/examples/notebooks/README.md | 60 ++ .../examples/notebooks/mcaf_hil_example.ipynb | 592 ++++++++++++++++++ .../gui/qt/controllers/config_manager.py | 12 +- .../gui/qt/controllers/connection_manager.py | 27 +- pyx2cscope/gui/qt/main_window.py | 41 +- pyx2cscope/gui/qt/models/app_state.py | 58 +- pyx2cscope/gui/qt/tabs/help_tab.py | 192 ++++++ pyx2cscope/gui/qt/tabs/scope_view_tab.py | 20 +- pyx2cscope/gui/qt/tabs/setup_tab.py | 23 +- pyx2cscope/gui/qt/tabs/watch_view_tab.py | 4 + pyx2cscope/gui/web/app.py | 84 ++- pyx2cscope/gui/web/scope.py | 62 +- .../gui/web/static/js/dashboard_view.js | 15 +- pyx2cscope/gui/web/static/js/scope_view.js | 6 +- pyx2cscope/gui/web/static/js/script.js | 28 +- .../web/static/widgets/plot_scope/widget.js | 8 +- .../static/widgets/scope_control/widget.js | 10 +- pyx2cscope/gui/web/templates/index.html | 2 +- pyx2cscope/gui/web/templates/navbar.html | 5 +- .../gui/web/templates/sample_control.html | 2 +- pyx2cscope/gui/web/templates/setup.html | 6 +- .../gui/web/templates/welcome_content.html | 111 +++- pyx2cscope/gui/web/views/scope_view.py | 3 +- pyx2cscope/gui/web/views/watch_view.py | 2 +- pyx2cscope/gui/web/ws_handlers.py | 5 +- pyx2cscope/parser/elf_parser.py | 20 + pyx2cscope/parser/generic_parser.py | 119 +++- pyx2cscope/variable/variable.py | 2 +- pyx2cscope/variable/variable_factory.py | 130 +++- pyx2cscope/x2cscope.py | 15 +- quality.txt | Bin 566 -> 0 bytes requirements.txt | Bin 472 -> 0 bytes tests/test_can_reconnection.py | 252 ++++++++ tests/test_install.py | 17 +- tests/test_parser.py | 97 ++- tests/test_pyx2cscope_class.py | 75 ++- tests/test_qt_gui.py | 139 +++- tests/test_web_gui.py | 107 +++- tests/utils/serial_stub.py | 38 +- 54 files changed, 2531 insertions(+), 232 deletions(-) create mode 100644 pyx2cscope/examples/check_compatibility.py create mode 100644 pyx2cscope/examples/notebooks/README.md create mode 100644 pyx2cscope/examples/notebooks/mcaf_hil_example.ipynb create mode 100644 pyx2cscope/gui/qt/tabs/help_tab.py delete mode 100644 quality.txt delete mode 100644 requirements.txt create mode 100644 tests/test_can_reconnection.py diff --git a/.github/workflows/buildexe.yml b/.github/workflows/buildexe.yml index 0035c024..ef567d94 100644 --- a/.github/workflows/buildexe.yml +++ b/.github/workflows/buildexe.yml @@ -15,13 +15,12 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: '3.10' + python-version: '3.14' - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -r requirements.txt - pip install -r quality.txt + pip install -e ".[build]" - name: Build executable run: | @@ -51,13 +50,12 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: '3.10' + python-version: '3.14' - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -r requirements.txt - pip install -r quality.txt + pip install -e ".[build]" sudo apt install libxcb-xinerama0 - name: Build executable diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 753b572c..5f4d59f8 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -13,12 +13,12 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: "3.10" + python-version: "3.14" - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -r quality.txt + pip install -e ".[dev]" - name: Code Format Check run: | @@ -31,11 +31,11 @@ jobs: # - name: Set up Python # uses: actions/setup-python@v5 # with: -# python-version: "3.10" +# python-version: "3.14" # - name: Install dependencies # run: | # python -m pip install --upgrade pip -# pip install -r quality.txt +# pip install -e ".[dev]" # - name: Check code with ruff # run: | # ruff ./pyx2cscope @@ -45,13 +45,16 @@ jobs: tests: runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10", "3.14"] steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: - python-version: "3.10" + python-version: ${{ matrix.python-version }} - name: Install system dependencies for Qt run: | @@ -72,8 +75,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -r quality.txt - pip install -e . + pip install -e ".[dev]" - name: Run tests env: @@ -86,6 +88,9 @@ jobs: docs: runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10", "3.14"] steps: - name: Checkout repository @@ -94,13 +99,12 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: '3.10' + python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -r quality.txt - pip install -e . + pip install -e ".[docs]" - name: Build documentation run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f1a0e9dd..65e76140 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,13 +15,12 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: '3.10' + python-version: '3.14' - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -r quality.txt - pip install -e . + pip install -e ".[docs,build]" - name: Build documentation run: | @@ -33,9 +32,15 @@ jobs: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./build/html - - name: Release to pypi with poetry + - name: Build package run: | - poetry config pypi-token.pypi ${{ secrets.PYX2CSCOPE_PYPI }} - poetry publish -vvv --build + python -m build + + - name: Release to pypi + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYX2CSCOPE_PYPI }} + run: | + python -m twine upload dist/* diff --git a/.gitignore b/.gitignore index d08a1541..ef1a2588 100644 --- a/.gitignore +++ b/.gitignore @@ -104,6 +104,7 @@ celerybeat.pid # Environments .env .venv +.venv* env/ venv/ ENV/ diff --git a/README.md b/README.md index 512538b9..59fec4e4 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,8 @@ Detailed documentation is hosted at GitHub.io: ## Install +pyX2Cscope currently supports Python `3.10` to `3.14`. + Create a virtual environment and install pyx2cscope using the following commands (Windows): ``` python -m venv .venv @@ -40,12 +42,16 @@ To execute the Browser based version type: ```py from pyx2cscope.x2cscope import X2CScope -# initialize the X2CScope class with serial port, by default baud rate is 115200 -x2c_scope = X2CScope(port="COM8") -# instead of loading directly the elf file, we can import it after instantiating the X2CScope class -x2c_scope.import_variables(r"..\..\tests\data\qspin_foc_same54.elf") +# Option 1: Default UART with Auto-detect COM port (recommended for single device) +x2c_scope = X2CScope(elf_file="path/to/firmware.elf") + +# Option 2: Specify COM port explicitly +# x2c_scope = X2CScope(port="COM8", elf_file="path/to/firmware.elf") + +# Or import variables after instantiation +# x2c_scope.import_variables(r"..\..\tests\data\qspin_foc_same54.elf") -# Collect some variables. +# Collect some variables speed_reference = x2c_scope.get_variable("motor.apiData.velocityReference") speed_measured = x2c_scope.get_variable("motor.apiData.velocityMeasured") diff --git a/doc/development.md b/doc/development.md index ac5f110f..64dd1d38 100644 --- a/doc/development.md +++ b/doc/development.md @@ -35,10 +35,13 @@ source .venv\bin\activate ## Installing dev requirements ```bash -pip install -r requirements.txt -pip install -r quality.txt +pip install -e ".[dev,docs,build]" ``` +## Publishing a release + +Package releases are handled by GitHub Actions as part of the maintainer release workflow. + ## Running tests ### ruff syntax check diff --git a/doc/example.md b/doc/example.md index 9ae83c11..cbaf1641 100644 --- a/doc/example.md +++ b/doc/example.md @@ -217,7 +217,7 @@ variables = [ for var in variables: x2c_scope.add_scope_channel(x2c_scope.get_variable(var)) -x2c_scope.set_sample_time(0) +x2c_scope.set_sample_time(1) # Create the plot plt.ion() # Turn on interactive mode @@ -287,6 +287,30 @@ with open(csv_file_path, mode="w", newline="") as file: logging.info(f"Data saved in {csv_file_path}") ```` +## Check ELF compatibility + +````python + +"""Check whether the loaded ELF file appears compatible with the connected target.""" + +from pyx2cscope.utils import get_com_port, get_elf_file_path +from pyx2cscope.x2cscope import X2CScope + + +x2c_scope = X2CScope(port=get_com_port()) + +try: + x2c_scope.import_variables(get_elf_file_path()) + + compatibility = x2c_scope.check_compatibility() + print("Compatibility report:") + for key, value in compatibility.items(): + print(f" {key}: {value}") +finally: + x2c_scope.disconnect() + +```` + for more visit the [pyX2Cscope example directory ](https://github.com/X2Cscope/pyx2cscope/tree/develop/pyx2cscope/examples) diff --git a/doc/gui_qt.md b/doc/gui_qt.md index b3f8fd66..e4d803ef 100644 --- a/doc/gui_qt.md +++ b/doc/gui_qt.md @@ -30,7 +30,7 @@ The Setup tab is where you configure the connection to your microcontroller. ### Connection Settings -1. **ELF File**: Click "Select ELF file" to choose the ELF file of the project your microcontroller is programmed with. +1. **Select File**: Click `ELF / YML / PKL` to load variable information. Supported formats are `.elf`, `.yml`, and `.pkl`. 2. **Interface**: Select the communication interface: - **UART**: Serial communication @@ -41,9 +41,14 @@ The Setup tab is where you configure the connection to your microcontroller. ### UART Settings -- **Port**: Select the COM port from the dropdown. Use the refresh button to update available ports. +- **Port**: Select the COM port from the dropdown. + - **AUTO**: Automatically detects the first available LNet device (recommended for single device). + - **Specific Port**: Choose a specific COM port (COM1, COM3, etc.) for manual connection. + - **Refresh Button**: Updates the list of available ports. - **Baud Rate**: Select the baud rate (38400, 115200, 230400, 460800, 921600). +> **Tip**: When using AUTO mode, the system will scan available COM ports and connect to the first device that responds to the LNet protocol. This is useful when the exact port number is unknown or when working across different machines. + ### TCP/IP Settings - **Host**: Enter the IP address or hostname of the target device. @@ -127,6 +132,13 @@ mapped to its fixed hardware address. From that point it behaves exactly like an variable — values can be read, polled live (WatchView), or captured as a scope channel (ScopeView). +### Export Variables + +The **Export Variables** button in the Data Views toolbar allows you to: +- Export only the variables currently selected in WatchView and ScopeView. +- Save the selection as `.yml` or `.pkl`. +- Preserve SFR selections in the exported file too. + ### Save and Load Config The **Save Config** and **Load Config** buttons allow you to: diff --git a/doc/gui_web.md b/doc/gui_web.md index 4fecbd7d..f5192d26 100644 --- a/doc/gui_web.md +++ b/doc/gui_web.md @@ -63,8 +63,12 @@ Select the communication interface type: When Serial is selected: -1. **UART Dropdown** - Select the COM port from the available ports list -2. **Refresh Button** - Click to rescan for available COM ports +1. **UART Dropdown** - Select the COM port: + - **AUTO (Auto-detect)**: Automatically scans and connects to the first available LNet device (recommended for single device). + - **Specific Port**: Choose a specific COM port (COM1, COM3, etc.) for manual connection. +2. **Refresh Button** - Click to rescan for available COM ports. + +> **Tip**: The AUTO option is useful when you don't know the exact COM port or when working across different machines. The system will test each available port and connect to the first one that responds to the LNet protocol. ### TCP/IP Configuration @@ -91,11 +95,18 @@ When CAN is selected: > **Note**: CAN interface requires vendor-specific drivers to be installed. See the API documentation for driver requirements. -### ELF File Selection +### Variable File Selection -Select an ELF file (or PKL/YML import file) containing the variable information from your firmware. +Select a variable file containing the variable information for your firmware. Supported formats: `.elf`, `.pkl`, `.yml` +### Export Variables + +After connecting, click the export icon in the header to export the variables currently used in +Watch View, Scope View, and Dashboard. + +Choose either `.yml` or `.pkl`. SFR selections are preserved in the exported file too. + ### Connecting Click the **Connect** button to establish communication with the target device. diff --git a/doc/install.rst b/doc/install.rst index 79e72d5d..8d56dce6 100644 --- a/doc/install.rst +++ b/doc/install.rst @@ -3,6 +3,8 @@ Installation Create a virtual environment and install pyx2cscope using the following commands (Windows): +pyX2Cscope currently supports Python ``3.10`` to ``3.14``. + .. code-block:: python python -m venv .venv @@ -50,4 +52,4 @@ pyX2Cscope with argument **-l** or **--log-level** .. code-block:: bash - pyx2cscope --log-level DEBUG \ No newline at end of file + pyx2cscope --log-level DEBUG diff --git a/doc/scripting.rst b/doc/scripting.rst index 492c9bc1..1c2a50ab 100644 --- a/doc/scripting.rst +++ b/doc/scripting.rst @@ -357,6 +357,10 @@ It is possible to export selected variables or the whole list of variables. Having the exported file (yml or pickle) it is possible to import it back to the X2CScope object. YML is human readable and can be edited with any text editor, while pickle is a binary file and can be used to store the variables in a more secure way. +Exported files preserve both firmware variables and SFR entries. If a selected list contains SFRs, +they are stored in the register section of the export file and can be imported again with +``get_variable("NAME", sfr=True)``. + See the example below: .. literalinclude:: ../pyx2cscope/examples/export_import_variables.py @@ -484,15 +488,55 @@ TriggerConfig needs some parameters like the variable and some trigger values li * Trigger_delay: Value > 0 Pre-trigger, Value < 0 Post trigger * Trigger_Edge: Rising (1) or Falling (0) +Data resolution +^^^^^^^^^^^^^^^ + +The scope sampling resolution can be adjusted with ``set_sample_time()``. + +.. code-block:: python + + x2c_scope.set_sample_time(sample_time) + +``sample_time`` starts at ``1`` in the pyX2Cscope API and interfaces: + +* ``1``: take every sample +* ``2``: take every second sample +* ``3``: take every third sample + +Higher values increase the total captured time window, but reduce the time resolution of the acquired data. +In other words, the scope skips more firmware samples before storing the next point in the scope buffer. + +Internally, LNET uses a 0-based value, but pyX2Cscope exposes this parameter as 1-based in the API and interfaces. + Additional information on how to change triggers, clear and change sample time, may be found on the API documentation. +ELF Compatibility Check +^^^^^^^^^^^^^^^^^^^^^^^ + +When an ELF file is loaded, pyX2Cscope performs a best-effort compatibility check against the +connected target and emits a warning if the MCU family appears to mismatch. + +You can also query this explicitly: + +.. code-block:: python + + compatibility = x2c_scope.check_compatibility() + print(compatibility) + +The returned dictionary reports whether the check could be performed and whether the loaded ELF +appears compatible with the connected target. The check is best-effort and uses the ELF target +description together with the target processor information reported by LNET. It can distinguish +common cases such as ARM-based targets, PIC32, generic dsPIC/PIC24, and dsPIC33A. + +See also the runnable example in ``pyx2cscope/examples/check_compatibility.py``. + Utility Functions ----------------- The ``pyx2cscope.utils`` module provides helper functions for managing configuration settings used in examples and scripts. These utilities simplify the process of specifying ELF file paths -and COM ports without hardcoding them into your scripts. +and communication parameters without hardcoding them into your scripts. Configuration File (config.ini) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -512,7 +556,16 @@ default placeholder values that you should update with your actual settings: [HOST_IP] host_ip = your_host_ip -After the file is created, edit it to specify your actual ELF file path and COM port. + [CAN] + bustype = pcan_usb + channel = 1 + baud_rate = 500000 + id_tx = 0x110 + id_rx = 0x100 + mode = standard + +After the file is created, edit it to specify your actual ELF path and the connection settings +needed for Serial, TCP/IP, or CAN. Available Functions ^^^^^^^^^^^^^^^^^^^ @@ -553,13 +606,33 @@ Retrieves the host IP address for TCP/IP connections: if host: x2cscope = X2CScope(host=host, tcp_port=12666, elf_file="firmware.elf") +**get_can_config()** + +Retrieves the CAN interface configuration from the ``[CAN]`` section in ``config.ini``: + +.. code-block:: python + + from pyx2cscope.utils import get_can_config + + can_config = get_can_config() + x2cscope = X2CScope(elf_file="firmware.elf", **can_config) + +The returned dictionary contains these parameters: + +* ``bustype`` +* ``channel`` +* ``baud_rate`` +* ``id_tx`` +* ``id_rx`` +* ``mode`` + Example Usage ^^^^^^^^^^^^^ The utility functions are particularly useful in example scripts where you want to avoid -hardcoding paths: +hardcoding paths and interface settings: .. code-block:: python @@ -577,6 +650,38 @@ hardcoding paths: # Initialize X2CScope with configured values x2cscope = X2CScope(port=port, elf_file=elf_path) +For CAN, the same approach can be used with the CAN configuration block: + +.. code-block:: python + + from pyx2cscope import X2CScope + from pyx2cscope.utils import get_can_config, get_elf_file_path + + elf_path = get_elf_file_path() + can_config = get_can_config() + + if not elf_path: + print("Please configure config.ini with your ELF file path") + exit(1) + + x2cscope = X2CScope(elf_file=elf_path, **can_config) + +For TCP/IP, the host address can also be loaded from the configuration file: + +.. code-block:: python + + from pyx2cscope import X2CScope + from pyx2cscope.utils import get_elf_file_path, get_host_address + + elf_path = get_elf_file_path() + host = get_host_address() + + if not elf_path or not host: + print("Please configure config.ini with your ELF file path and host IP") + exit(1) + + x2cscope = X2CScope(elf_file=elf_path, host=host) + .. note:: The utility functions return an empty string if the configuration contains placeholder diff --git a/pyproject.toml b/pyproject.toml index c7aa18dd..e0502b6f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,40 +1,67 @@ [build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" +requires = ["setuptools>=75", "wheel"] +build-backend = "setuptools.build_meta" -[tool.poetry] +[project] name = "pyx2cscope" -version = "0.6.2" +version = "0.7.0" description = "python implementation of X2Cscope" +readme = "README.md" +requires-python = ">=3.10,<3.15" +license = "LicenseRef-Proprietary" authors = [ - "Yash Agarwal", - "Edras Pacola ", - "Christoph Baumgartner", - "Mark Wendler", + {name = "Yash Agarwal"}, + {name = "Edras Pacola", email = "edras.pacola@microchip.com"}, + {name = "Christoph Baumgartner"}, + {name = "Mark Wendler"}, +] +dependencies = [ + "pyserial>=3.5,<4.0", + "pyelftools>=0.31,<0.32", + "pyyaml>=6.0.1,<7.0.0", + "numpy>=1.26.0,<3.0.0", + "matplotlib>=3.7.2,<4.0.0", + "PyQt5>=5.15.9,<6.0.0", + "pyqtgraph>=0.13.7,<0.14.0", + "mchplnet==0.5.1", + "flask>=3.0.3,<4.0.0", + "flask-socketio>=5.3.4,<6.0.0", ] -readme = "README.md" -packages = [{include = "pyx2cscope"}] -license = "Proprietary" +[project.optional-dependencies] +dev = [ + "pre-commit==4.3.0", + "pylint==3.1.0", + "pytest==8.1.1", + "pytest-mock==3.14.0", + "pytest-qt==4.4.0", + "ruff==0.4.4", +] +docs = [ + "astroid<4.0.0", + "furo==2024.1.29", + "myst-parser==2.0.0", + "sphinx==7.2.6", + "sphinx-autoapi==3.0.0", + "sphinx-rtd-theme==2.0.0", +] +build = [ + "build>=1.2.2", + "pyinstaller>=6.11.0", + "requests==2.31.0", + "twine>=5.1.1", +] -homepage = "https://x2cscope.github.io/" -documentation = "https://x2cscope.github.io/pyx2cscope" -repository = "https://github.com/X2Cscope/pyx2cscope" +[project.urls] +Homepage = "https://x2cscope.github.io/" +Documentation = "https://x2cscope.github.io/pyx2cscope" +Repository = "https://github.com/X2Cscope/pyx2cscope" -[tool.poetry.scripts] -pyx2cscope = 'pyx2cscope.__main__:main' +[project.scripts] +pyx2cscope = "pyx2cscope.__main__:main" -[tool.poetry.dependencies] -python = "^3.10" -pyserial = "^3.5" -pyelftools = "^0.31" -pyyaml ="^6.0.1" -numpy = "^1.26.0" -matplotlib = "^3.7.2" -PyQt5 = "^5.15.9" -pyqtgraph = "^0.13.7" -mchplnet = "0.4.2" -flask = "^3.0.3" +[tool.setuptools.packages.find] +include = ["pyx2cscope*"] [tool.ruff] line-length = 120 @@ -50,4 +77,7 @@ lint.extend-select = ["I", "N", "F", "PL", "D"] exclude = ["mchplnet/*"] lint.ignore = ["PLR0913"] - +[tool.pytest.ini_options] +testpaths = ["tests"] +addopts = "--import-mode=importlib" +pythonpath = ["."] diff --git a/pyx2cscope/__init__.py b/pyx2cscope/__init__.py index 465d8ef3..79b0e20e 100644 --- a/pyx2cscope/__init__.py +++ b/pyx2cscope/__init__.py @@ -1,11 +1,11 @@ """This module contains the pyx2cscope package. -Version: 0.6.2 +Version: 0.7.0 """ import logging -__version__ = "0.6.2" +__version__ = "0.7.0" def set_logger( level: int = logging.ERROR, diff --git a/pyx2cscope/examples/can_demo.py b/pyx2cscope/examples/can_demo.py index 31c7d449..42ff1917 100644 --- a/pyx2cscope/examples/can_demo.py +++ b/pyx2cscope/examples/can_demo.py @@ -1,11 +1,9 @@ """Demo scripting for user to get started with CAN interface.""" import time -from mchplnet.interfaces.factory import InterfaceType from pyx2cscope.utils import get_can_config, get_elf_file_path from pyx2cscope.x2cscope import X2CScope - # Check if x2cscope was injected by the Scripting tab, otherwise create our own if globals().get("x2cscope") is None: # Get configuration from config.ini diff --git a/pyx2cscope/examples/check_compatibility.py b/pyx2cscope/examples/check_compatibility.py new file mode 100644 index 00000000..945e46d8 --- /dev/null +++ b/pyx2cscope/examples/check_compatibility.py @@ -0,0 +1,31 @@ +"""Check whether the loaded ELF file matches the connected target. + +This example connects to the target, loads variables from an ELF file, and +prints the compatibility report returned by ``check_compatibility()``. +""" + +from pyx2cscope.utils import get_com_port +from pyx2cscope.x2cscope import X2CScope + +x2c_scope = X2CScope(port=get_com_port()) +# this test assumes you have a dsPIC33CK256MP508 connected on UART + +try: + # this should be compatible + x2c_scope.import_variables("../../tests/data/mc_foc_sl_fip_dspic33ck_mclv48v300w.elf") + compatibility = x2c_scope.check_compatibility() + print("Compatibility report:") + for key, value in compatibility.items(): + print(f" {key}: {value}") + x2c_scope.disconnect() + + # this should be incompatible and generate a warning + x2c_scope.import_variables("../../tests/data/MC_FOC_DYNO_SAME54_MCLV2.elf") + compatibility = x2c_scope.check_compatibility() + print("Compatibility report:") + for key, value in compatibility.items(): + print(f" {key}: {value}") +finally: + x2c_scope.disconnect() + + diff --git a/pyx2cscope/examples/notebooks/README.md b/pyx2cscope/examples/notebooks/README.md new file mode 100644 index 00000000..b0425aa5 --- /dev/null +++ b/pyx2cscope/examples/notebooks/README.md @@ -0,0 +1,60 @@ +# pyX2Cscope — Jupyter Notebook Examples + +Interactive notebooks for exploring and controlling Microchip embedded targets using pyX2Cscope. + +## Available Notebooks + +| Notebook | Description | +|---|---| +| [mcaf_hil_example.ipynb](mcaf_hil_example.ipynb) | Hardware-in-the-Loop test with MCAF motor firmware: ramp speed, capture phase currents, calculate RMS values | + +--- + +## How to Run + +### 1. Install pyX2Cscope + +If you haven't already: + +```bash +pip install pyx2cscope +``` + +### 2. Install Jupyter + +```bash +pip install jupyterlab +``` + +### 3. Launch Jupyter + +From this folder: + +```bash +jupyter lab +``` + +A browser window will open. Click the notebook you want to run. + +### 4. Configure the notebook + +Each notebook has a **Configuration** cell near the top. At minimum you need to set: + +- `SERIAL_PORT` — the COM port your board is connected to (e.g. `"COM3"` on Windows, `"/dev/ttyUSB0"` on Linux/macOS). Set to `"AUTO"` to let pyX2Cscope detect it automatically. +- `ELF_FILE` — the full path to the `.elf` file matching the firmware flashed on your board. + +### 5. Run the cells + +Use **Shift+Enter** to run one cell at a time, or **Run → Run All Cells** from the menu to execute the full notebook. + +> **Tip:** Always run the final cleanup cell before closing the notebook. It stops the motor and restores the hardware UI. + +--- + +## Requirements + +- Python 3.10 or newer +- pyX2Cscope (`pip install pyx2cscope`) +- `notebook` or `jupyterlab` +- A Microchip board running compatible firmware, connected via USB/serial +- The matching `.elf` file for the firmware on your board diff --git a/pyx2cscope/examples/notebooks/mcaf_hil_example.ipynb b/pyx2cscope/examples/notebooks/mcaf_hil_example.ipynb new file mode 100644 index 00000000..57e32734 --- /dev/null +++ b/pyx2cscope/examples/notebooks/mcaf_hil_example.ipynb @@ -0,0 +1,592 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# MCAF Hardware-in-the-Loop (HiL) Example\n", + "\n", + "This notebook demonstrates how to use **pyX2Cscope** to control and monitor a BLDC motor running\n", + "[MCAF (Motor Control Application Framework)](https://www.microchip.com/en-us/solutions/technologies/motor-control-and-drive/motorbench-development-suite)\n", + "firmware on a Microchip device.\n", + "\n", + "### What this notebook does\n", + "\n", + "1. Connects to the target via serial (UART)\n", + "2. Disables the hardware UI so the script has exclusive control\n", + "3. Stops any running motor\n", + "4. Ramps the motor to **700 RPM**, measures the time to reach that speed, then captures phase currents with the scope\n", + "5. Arms the scope on `velocityMeasured`, ramps to **1400 RPM**, and plots the speed ramp curve (~60 ms)\n", + "6. Captures phase currents at 1400 RPM and calculates RMS values\n", + "7. Stops the motor and returns control to the hardware UI\n", + "\n", + "### Hardware used in this test\n", + "\n", + "| Component | Description |\n", + "|---|---|\n", + "| [MCLV-48V-300W Inverter Board (EV18H47A)](https://www.microchip.com/en-us/development-tool/ev18h47a) | Motor control inverter board (low-voltage, up to 48 V / 300 W) |\n", + "| [dsPIC33CK Low Voltage Motor Control (LVMC) Board (EV62P66A)](https://www.microchip.com/en-us/development-tool/ev62p66a) | Digital controller board with dsPIC33CK DSC |\n", + "| ACT 42BLF02 | BLDC motor |\n", + "\n", + "The firmware was generated with [MotorBench Development Suite (MCAF)](https://www.microchip.com/en-us/solutions/technologies/motor-control-and-drive/motorbench-development-suite).\n", + "\n", + "### Prerequisites\n", + "\n", + "- The hardware listed above, connected via USB/serial\n", + "- The matching `.elf` file for the firmware flashed on the controller board\n", + "- pyX2Cscope installed: `pip install pyx2cscope`\n", + "\n", + "> **Note:** The variable names used here (`motor.apiData.velocityReference`, etc.) are specific to MCAF.\n", + "> Use `x2c_scope.list_variables()` to browse the variables available in your firmware." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1 — Imports" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import time\n", + "\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "\n", + "from pyx2cscope.x2cscope import X2CScope" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2 — Configuration\n", + "\n", + "Set the serial port and the path to the ELF file for your board.\n", + "\n", + "| Setting | Description |\n", + "|---|---|\n", + "| `SERIAL_PORT` | COM port on Windows (`\"COM3\"`) or device path on Linux/macOS (`\"/dev/ttyUSB0\"`). Use `\"AUTO\"` to let pyX2Cscope detect the port automatically. |\n", + "| `ELF_FILE` | Full path to the `.elf` file matching the firmware on your board. |" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "SERIAL_PORT = \"AUTO\" # or e.g. \"COM3\" / \"/dev/ttyUSB0\"\n", + "ELF_FILE = r\"C:\\Users\\M71906\\Projects\\pyx2c_training\\mb_foc_dspic.X\\dist\\default\\production\\mb_foc_dspic.X.production.elf\" # <-- update this path\n", + "\n", + "# Speed thresholds — MCAF stores speed as RPM * 10\n", + "TARGET_SPEED_1_RPM = 7000 # represents 700.0 RPM\n", + "TARGET_SPEED_2_RPM = 14000 # represents 1400.0 RPM" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3 — Connect to the target" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Connected to: {'processor_id': '__GENERIC_MICROCHIP_DSPIC__', 'uc_width': '16-bit', 'date': 'Sep132024', 'time': '1516', 'AppVer': 2048, 'dsp_state': 'Application runs on target'}\n" + ] + } + ], + "source": [ + "x2c_scope = X2CScope(port=SERIAL_PORT, elf_file=ELF_FILE)\n", + "\n", + "device_info = x2c_scope.get_device_info()\n", + "print(\"Connected to:\", device_info)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Browse available variables (optional)\n", + "\n", + "Run this cell to see all variables exposed by the firmware." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "19215 variables available\n" + ] + }, + { + "data": { + "text/plain": [ + "['app.hardwareUiEnabled',\n", + " 'app.motorDirection',\n", + " 'app.motorVelocityCommand',\n", + " 'app.motorVelocityCommandMaximum',\n", + " 'app.motorVelocityCommandMinimum',\n", + " 'bPreCleared',\n", + " 'calibOffset',\n", + " 'compilationDate.date',\n", + " 'compilationDate.date[0]',\n", + " 'compilationDate.date[10]',\n", + " 'compilationDate.date[1]',\n", + " 'compilationDate.date[2]',\n", + " 'compilationDate.date[3]',\n", + " 'compilationDate.date[4]',\n", + " 'compilationDate.date[5]',\n", + " 'compilationDate.date[6]',\n", + " 'compilationDate.date[7]',\n", + " 'compilationDate.date[8]',\n", + " 'compilationDate.date[9]',\n", + " 'compilationDate.time']" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "variables = x2c_scope.list_variables()\n", + "print(f\"{len(variables)} variables available\")\n", + "variables[:20] # show first 20" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4 — Take control: disable hardware UI and stop the motor" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Hardware UI disabled\n", + "Stop request sent\n" + ] + } + ], + "source": [ + "# Disable the hardware UI so this script has exclusive control\n", + "hardware_ui_enabled = x2c_scope.get_variable(\"app.hardwareUiEnabled\")\n", + "hardware_ui_enabled.set_value(0)\n", + "print(\"Hardware UI disabled\")\n", + "\n", + "# Stop the motor (in case it is already running)\n", + "stop_motor_request = x2c_scope.get_variable(\"motor.apiData.stopMotorRequest\")\n", + "stop_motor_request.set_value(1)\n", + "print(\"Stop request sent\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 5 — Ramp to 700 RPM and measure time to reach speed" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Motor started, target: 700 RPM\n", + "Reached 700 RPM in 0.69 s\n" + ] + } + ], + "source": [ + "velocity_reference = x2c_scope.get_variable(\"motor.apiData.velocityReference\")\n", + "velocity_reference.set_value(TARGET_SPEED_1_RPM)\n", + "\n", + "run_motor_request = x2c_scope.get_variable(\"motor.apiData.runMotorRequest\")\n", + "run_motor_request.set_value(1)\n", + "print(f\"Motor started, target: {TARGET_SPEED_1_RPM / 10:.0f} RPM\")\n", + "\n", + "speed_measured = x2c_scope.get_variable(\"motor.apiData.velocityMeasured\")\n", + "start_time = time.time()\n", + "while speed_measured.get_value() < TARGET_SPEED_1_RPM:\n", + " time.sleep(0.05)\n", + "time_to_speed_1 = time.time() - start_time\n", + "\n", + "print(f\"Reached {TARGET_SPEED_1_RPM / 10:.0f} RPM in {time_to_speed_1:.2f} s\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 6 — Capture phase currents at 700 RPM with the scope" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "RMS current at 700 RPM: Ia=18.858 A Ib=17.568 A avg=18.213 A\n" + ] + } + ], + "source": [ + "time.sleep(2) # let the motor settle\n", + "\n", + "phase_current_a = x2c_scope.get_variable(\"motor.iabc.a\")\n", + "phase_current_b = x2c_scope.get_variable(\"motor.iabc.b\")\n", + "x2c_scope.add_scope_channel(phase_current_a)\n", + "x2c_scope.add_scope_channel(phase_current_b)\n", + "\n", + "x2c_scope.request_scope_data()\n", + "while not x2c_scope.is_scope_data_ready():\n", + " time.sleep(0.1)\n", + "\n", + "scope_data_1 = x2c_scope.get_scope_channel_data()\n", + "current_a_1 = np.array(scope_data_1[\"motor.iabc.a\"])\n", + "current_b_1 = np.array(scope_data_1[\"motor.iabc.b\"])\n", + "\n", + "rms_a_1 = np.sqrt(np.mean(current_a_1 ** 2)) * 0.1\n", + "rms_b_1 = np.sqrt(np.mean(current_b_1 ** 2)) * 0.1\n", + "avg_current_1 = (rms_a_1 + rms_b_1) / 2\n", + "\n", + "print(f\"RMS current at {TARGET_SPEED_1_RPM / 10:.0f} RPM: Ia={rms_a_1:.3f} A Ib={rms_b_1:.3f} A avg={avg_current_1:.3f} A\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Plot phase currents at 700 RPM" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA90AAAGGCAYAAABmGOKbAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuNSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/xnp5ZAAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOydB3gbVdaGP/de48QpTu+9kgQCgVQglNDLwsLSFpYOS1l2/4Wl97J0WHrvNaRDSEhIQnrvzY4Tx713W/9zZuZKV6MZaSSNytj3fZ7Esqwykq7u3HPPd74TYbPZbBAIBAKBQCAQCAQCgUBgOpHmP6RAIBAIBAKBQCAQCAQCQgTdAoFAIBAIBAKBQCAQBAgRdAsEAoFAIBAIBAKBQBAgRNAtEAgEAoFAIBAIBAJBgBBBt0AgEAgEAoFAIBAIBAFCBN0CgUAgEAgEAoFAIBAECBF0CwQCgUAgEAgEAoFAECBE0C0QCAQCgUAgEAgEAkGAEEG3QCAQCAQCgUAgEAgEAUIE3QKBQCAIK9577z1ERERg7dq1oT4UgUAgEAgEAr8RQbdAIBAIghpMs3/x8fEYMGAAbr75Zhw7dgxtgX379uH6669Hnz59pNeXmpqKSZMm4b///S/q6upgVebOnYv//Oc/IXnuxx57DN99951PY0z97+OPP3a6fX5+Pi666CKkp6dLn9Xs2bOxf/9+zcd+++23MXjwYOlz7d+/P1566SWfjik6OhrdunXDX/7yF+n51ZxyyilOt8/MzMRxxx2Hd955B62trfbb0f3p73TcWmNrz5499sd45plnDB2rQCAQCAJDdIAeVyAQCAQCTR566CH07t0b9fX1WL58OV577TUpqNu6dSsSExNhVX766SdceOGFiIuLwxVXXIFhw4ahsbFReo133303tm3bhjfffBNWhD6fV155JSSBNwXdF1xwAc455xyPt508eTI+/PBDl+uff/55bNq0CdOmTbNfV11djSlTpqCiogL//Oc/ERMTI93u5JNPxsaNG9GhQwf7bd944w3ccMMNOP/883HnnXfit99+w6233ora2lrce++9Xo/7VatWScE4jQ0a9xTI8+Tk5ODxxx+XLhcVFeGDDz7ANddcg927d+OJJ56w344CeDqGH3/8Udo84KENBnpcej6BQCAQhBibQCAQCARB4N1337XRaWfNmjVO1995553S9Z988onb24Uz+/fvtyUnJ9sGDRpkO3LkiMvf9+zZY3vhhRdMea7q6mrN61tbW221tbW2QHDTTTdJn0koSEpKsl155ZU+35/ek5SUFNuMGTOcrn/yySel1/THH3/Yr9uxY4ctKirKdt999zndv0OHDrYzzjjD6f6XXXaZdGylpaVun19vPN97773S9Z9//rnT9SeffLJt6NChTtfV1NTYcnJypOdrbGyUrqP3hH6fOXOm7ZxzznF53v79+9vOP/986Tmefvppt8coEAgEgsAi5OUCgUAgCClTp06Vfh44cMDp+oaGBimr2LFjRyQlJeHcc8+Vsn4833//Pc444wx07dpVyjD37dsXDz/8MFpaWlyktpSl7Ny5s5T9o0ziJZdcImU5eT766COMHTsWCQkJkqyXbpOXl+fxNTz11FNS5pQkyF26dHH5e79+/XDbbbdJlw8ePChJfinTqYau57PJdJmu2759O/70pz8hIyMDJ554ovS3Xr164cwzz8SCBQswbtw46ZgpI0uUl5fj9ttvR/fu3aX3hZ7/ySefdJIns+Mg6TFl4Om9o9uSlHnNmjVOMmbKcrPjY/8Yn332mfSepaSkSFLn4cOHS3J6T9DznnDCCVJGmY6dHuOrr75yeT9qamrw/vvv25+XjscbKAtcVVWFyy67zOl6ei56rfSPMWjQICkb/sUXX9ivW7JkCUpKSnDjjTc63f+mm26Sjo0UDr5w0kkn2UsSPEEKkIkTJ0rPp/4O0LiYN2+e9Jkz6POjMU9/EwgEAkHoEfJygUAgEIQUFnTwcl7illtukYLMBx54QAoQX3jhBan++/PPP7ffhgLX5ORkKTinn7/88gvuv/9+VFZW4umnn5ZuQxLvU089VQri6TEp8KZa2jlz5kiBSlpamnS7Rx99FP/+978lme61114rBTdUt0uS5Q0bNkh1v+4CO6rjpiAyEJBsneqISWpts1HyUmbXrl249NJLpTry6667DgMHDpTkxiSRptdI1/fo0QO///477rvvPhw9elR6H3k++eQTKSil21JQSxsI5513nlTbTJJruv7IkSNYtGiRi3SbrqPnp0CVgnpix44dWLFihX2TQQ8KzM8++2wpGKbPiIJ3ep30udBGCkHPR5/F+PHj8de//lW6jjYHvIFk1hTU02ti0ObD5s2bcfXVV7vcnp5r4cKF0ntCGwn02RO0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2GAYxl9IVXfuuedKpnu02cu4w/Jyi5Cfny/tRmtdh+n7goKCqJ0XEzmuu+46fPXVV5g/fz66d+/ueJw+fyo/qKio8Dg26Kve2BHPMdaE5OOlpaU45phjpJ1jui1cuBDPPfecdJ92inlstF/IaXjw4MEujw0aNEhyq1d/vt7mFfpKY0wNOduTGy2PD+tCHQoo2/373/9eKi+56KKLcOONN0rdMggeG4wgVGOB55rYD7j37NkjJQFElpvgseGEg26LQHLA0aNHSzVY6iwGfT9u3LionhsTXmjXkALuTz/9FD/88IObBIfGRWJiosvYoDoYWliLsUFf161b53LhExdG7aKcsQ5TpkyRPlfKUokbZTZJIiru89hov1AZira9INXwFhUVSffpWkILGvX4IMkx1dmpxwdt2tAGj4CuQzT/UE0nY03q6uqkmko1tLFPnyvBY4MRhGos0DHUfooCNPVcM2DAgJiRD7fngJtayH333XdSe0o1PDZUqEzVGAu0DCO3yDfffFNyA7ziiiuklmFq12Em9rj66qulVh0LFiywHzx40HGrq6tzaQtFbcR++OEHqS3UuHHjpJu2LdT06dOltmNz5syxd+zYkdtCxSBq93KCx0b7hVxkExISpPZQ27Zts7/77rv2tLQ0+zvvvOPSCojmkc8//9y+du1a+29+8xvdVkCjRo2S2o4tWrRIcsrntlDW5o9//KO9W7dujpZh1A6IWgXeeuutjmN4bLSvDhjUMpJuFBo8/fTT0n3hQB2KsUCO59QW6qKLLpLaQtGalq5HsdYWqj2NjaamJql9XPfu3aX1g3qNqnYi57Ehw0G3xXj++eelBTT166YWYtTzjolt6CKnd6Pe3QKa+K655hqp5QJdqH77299KFz01u3fvtp9yyilS70NaXN1888325ubmKPyPmEgG3Tw22jdffvmltKlCG7YDBw60v/rqqy7PUzuge+65R1rw0DFTpkyxb9myxeWYsrIyaYFEfZypldyll14qLcQY61JVVSVdJ2g9kZKSYu/du7fUi1e9UOax0X6YP3++7jqDNmdCORaoxze1MaTXoE0fCuYZ644N2rDztEalnxPw2JCJo3/UmW+GYRiGYRiGYRiGYUID13QzDMMwDMMwDMMwTJjgoJthGIZhGIZhGIZhwgQH3QzDMAzDMAzDMAwTJjjoZhiGYRiGYRiGYZgwwUE3wzAMwzAMwzAMw4QJDroZhmEYhmEYhmEYJkxw0M0wDMMwDMMwDMMwYYKDboZhGIZhGIZhGIYJExx0MwzDMAwTVuLi4vDZZ59F+zQYhmEYJipw0M0wDMMwMcDhw4dx9dVXo0ePHkhOTkZBQQFmzJiBn3/+OdqnxjAMwzDtmoRonwDDMAzDMMFz1llnoampCW+99RZ69+6NkpISfP/99ygrK4v2qTEMwzBMu4Yz3QzDMAxjcSoqKvDTTz/hsccew+TJk1FUVITjjjsOd9xxB8444wzpmKeffhrDhg1Deno6CgsLcc0116CmpsbxGm+++SZycnLw1VdfYcCAAUhLS8PZZ5+Nuro6KZDv2bMncnNzcf3116O1tdXxc/T4gw8+iPPPP1967W7duuGFF17wer7FxcU499xzpd+Xl5eH3/zmN9i9e3cY3yGGYRiGiR4cdDMMwzCMxcnIyJBuVDfd2Nioe4zNZsNzzz2HDRs2SEH0Dz/8gFtvvdXlGAqw6Zj33nsPc+bMwYIFC/Db3/4Ws2fPlm5vv/02XnnlFXz00UcuP/fEE09gxIgRWLVqFW6//XbccMMNmDdvnu55NDc3S7L3zMxMaaOA5O907jNnzpQy9QzDMAwTa8TZ7XZ7tE+CYRiGYZjg+Pjjj3H55Zejvr4exxxzDCZOnIjf//73GD58uO7xFDhfddVVOHLkiCPTfemll2L79u3o06eP9Bg9T4E2SdUpMCYoOKbs9ssvvyx9T/cHDRqEb775xvHa9HurqqqkQF0YqX366ac488wz8c477+Dvf/87Nm3aJD1OULBNWW/aNJg+fXqY3ymGYRiGiSyc6WYYhmGYGKnpPnDgAL744gspMKYsNQXfFEwT3333HaZMmSLJvynLfNFFF0n13pTdFpCkXATcROfOnaWgWgTc4rHS0lKX3z1u3Di37ymo1mPNmjVSYE/nIDL0JDFvaGjAjh07QvZ+MAzDMIxZYCM1hmEYhokRUlJSMG3aNOl2zz334M9//jPuu+8+TJo0Caeddprkbv7QQw9JQe6iRYtw2WWXSVlmCraJxMREl9ejTLTeY21tbQGfI9WRjx49Gu+++67bcx07dgz4dRmGYRjGrHDQzTAMwzAxyuDBgyXJ9ooVK6RA+amnnpJqu4kPPvggZL/n119/dfueJOd6UPb9/fffR6dOnZCVlRWyc2AYhmEYs8LycoZhGIaxOCQTP/nkk6V66bVr12LXrl348MMP8fjjj0vO4H379pUMzJ5//nns3LlTqtMWNdmhgMzQ6Hdt3bpVci6n301manpceOGFyM/Pl86LjNToXEkKT67o+/btC9k5MQzDMIxZ4Ew3wzAMw1gcqoseO3YsnnnmGakumgJsagtGxmp33nknUlNTpZZh1FKM2ohNmDABjzzyCC6++OKQ/P6bb74Zy5cvx/333y9lr+l3kUO5HiRl//HHH3Hbbbfhd7/7Haqrq6U6c6o358w3wzAME4uweznDMAzDMAFDRmv/93//J90YhmEYhnGH5eUMwzAMwzAMwzAMEyY46GYYhmEYhmEYhmGYMMHycoZhGIZhGIZhGIYJE5zpZhiGYRiGYRiGYZgwwUE3wzAMwzAMwzAMw4QJDroZhmEYhmEYhmEYJkxw0M0wDMMwDMMwDMMwYYKDboZhGIZhGIZhGIYJExx0MwzDMAzDMAzDMEyY4KCbYRiGYRiGYRiGYcIEB90MwzAMwzAMwzAMEyY46GYYhmEYhmEYhmEYhIf/B98PZ1bQrgYDAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, ax = plt.subplots(figsize=(10, 4))\n", + "ax.plot(current_a_1 * 0.1, label=\"Phase A\")\n", + "ax.plot(current_b_1 * 0.1, label=\"Phase B\")\n", + "ax.set_title(f\"Phase Currents at {TARGET_SPEED_1_RPM / 10:.0f} RPM\")\n", + "ax.set_xlabel(\"Sample\")\n", + "ax.set_ylabel(\"Current (A)\")\n", + "ax.legend()\n", + "ax.grid(True)\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 7 — Capture speed ramp: 700 → 1400 RPM\n", + "\n", + "Add `motor.apiData.velocityMeasured` as a scope channel, arm the scope, **then** command the speed\n", + "change so the ramp is captured in the buffer. The scope window is set to cover ~100 ms\n", + "(sample time factor 0, firmware sample period ≈ 50 µs)." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Scope window: 122.5 ms\n", + "Target speed changed to 1400 RPM — waiting for scope capture…\n", + "Captured 2450 samples over 122.5 ms\n" + ] + } + ], + "source": [ + "SCOPE_SAMPLE_TIME_US = 50 # firmware interrupt period in µs — adjust if needed\n", + "\n", + "# Add speed as a scope channel (replaces current channels for this capture)\n", + "x2c_scope.clear_all_scope_channel()\n", + "x2c_scope.add_scope_channel(speed_measured)\n", + "\n", + "# Set sample time factor (0 = every firmware tick, gives the finest resolution)\n", + "x2c_scope.set_sample_time(0)\n", + "\n", + "# Compute the total scope window so we can build the time axis later\n", + "scope_window_ms = x2c_scope.get_scope_sample_time(SCOPE_SAMPLE_TIME_US)\n", + "print(f\"Scope window: {scope_window_ms:.1f} ms\")\n", + "\n", + "# Arm the scope FIRST, then trigger the speed change\n", + "x2c_scope.request_scope_data()\n", + "velocity_reference.set_value(TARGET_SPEED_2_RPM)\n", + "print(f\"Target speed changed to {TARGET_SPEED_2_RPM / 10:.0f} RPM — waiting for scope capture…\")\n", + "\n", + "while not x2c_scope.is_scope_data_ready():\n", + " time.sleep(0.05)\n", + "\n", + "ramp_data = x2c_scope.get_scope_channel_data()\n", + "ramp_speed_raw = np.array(ramp_data[\"motor.apiData.velocityMeasured\"])\n", + "ramp_speed_rpm = ramp_speed_raw / 10 # convert to RPM\n", + "n_samples = len(ramp_speed_rpm)\n", + "ramp_time_ms = np.linspace(0, scope_window_ms, n_samples)\n", + "\n", + "print(f\"Captured {n_samples} samples over {scope_window_ms:.1f} ms\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Plot speed ramp (700 → 1400 RPM)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA90AAAGGCAYAAABmGOKbAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuNSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/xnp5ZAAAACXBIWXMAAA9hAAAPYQGoP6dpAAB6B0lEQVR4nO3dB3hU1daA4ZXeC733LtIERRSx0PGqIBYUFQXBAngVf/ViQbGh6LWAvesVECs2RBBQelWkiog0KaGT3ud/1g5nmBoSSDKTyff6jJM552Rmn5kNzDpr7b2DbDabTQAAAAAAQIkLLvmnBAAAAAAAiqAbAAAAAIBSQtANAAAAAEApIegGAAAAAKCUEHQDAAAAAFBKCLoBAAAAACglBN0AAAAAAJQSgm4AAAAAAEoJQTcAAAAAAKWEoBsAUCEFBQXJY4895utmAACAAEfQDQA4ZevWrZOrrrpKGjRoIJGRkVKnTh3p2bOnTJ48WQLB9u3bTXBu3YKDg6Vy5crSt29fWbp0qQSSn3/+2elcXW9PPfWU0/FHjx6VESNGSLVq1SQmJkYuvvhi+fXXXz0+9zfffCNnnXWW6SP169eXRx99VHJzc6Wsvf7663L11VebNug53XzzzUX6veHDh5vj//Wvf53W+RXnPXN10UUXOX0eUVFR0rZtW3nppZckPz+/0H4bEhJi2jVgwABZs2aN07HWMbfeeqvH133ooYfsxxw8eLBIbQUAOAt1eQwAQJEsWbLEBA36ZV6Dkpo1a8quXbtk2bJl8vLLL8vo0aMlUFx33XXSr18/ycvLkz///FNee+01c+4rV66UNm3aSCBo1aqV/O9//3Pbrttmz54tvXr1sm/TIO/SSy+V33//Xe677z6pWrWqeU80MFy9erU0a9bMfuwPP/wg/fv3N/v0YoxeqHnyySdl//79JgguS88++6ykpKTIOeecI3v37i3S76xatUo++OADE1B7UtTzK8575k3dunVlwoQJ5mcNgKdOnSr33HOPHDhwwO2iiGu/3bRpk2mPtlf/jLZv395+nJ7bF198YdoTHh7u9BzTpk0z+zMzM4v0fgEAPLABAHAK+vXrZ6tWrZrtyJEjbvuSkpJs/k7/CXz00UcLPWbbtm3muOeee85p+w8//GC233HHHbZA17RpU1uzZs2ctk2fPt2c/2effWbftn//fltiYqLtuuuuczr2jDPOsLVr186Wk5Nj3/bQQw/ZgoKCbJs2bbKVpe3bt9vy8/PNzzExMbYhQ4YUerwe26VLF9vQoUNtDRo0sF166aVuxxT1/Irznnly4YUX2lq3bu20LSMjw7QrLi7Olpube9J++80335jtI0aMsG/Tx/3797cFBwfbZsyY4XT84sWLzf6BAwea+wMHDpy0nQAAd5SXAwBOydatW6V169aSmJjotq969epOj7U0ddSoUTJlyhRp0aKFyZx17NhRFixY4Pa7u3fvlqFDh0qNGjUkIiLCvMZ7773ndlxWVpYp423atKk5rl69enL//feb7a7HaTZQS3rj4uLk8ssvl3/++ee0zv2CCy6wvweO3n//fbnkkkvM+WubzjjjDI/Z3IYNG5pSZS3p7tSpkykV1oy5PlZffvmleWy9T7/99pvT72tZdGxsrPz999/Su3dvU6pcu3Ztefzxx/ViutOxmtH9448/JCcnp9jnuWLFCvnrr79k8ODBTts///xz8/lceeWV9m36/l5zzTXy9ddf2z+DjRs3mpuWVIeGniiuu/POO0079XnKkg6D0L5YVJrlX79+vccscnHPr6jvWXFo/zj77LNN9l4z6yejfVNt27bNabsOC+nWrZvJnDvSP6/aD88888xitw0AcAJBNwDglAMYLYvVoKQofvnlF7n77rvlhhtuMMHhoUOHpE+fPk6/n5SUJOeee6789NNPJkjXMnUNqocNG2bGrjqW6mrw/Pzzz8tll11mynq1xPfFF1+Ua6+91ul1dayq/q6WRz/zzDMSFhZmynxPh46ZVZUqVXLargG2vi8PPvig/Pe//zUXAjQAe/XVV92eQ4PZ66+/3rRfS4aPHDliftZARy8S6Ps0fvx4E9hrYOY6bldLhvX900Bu4sSJJjjXixB6czR27FhTOq4XM4pL26Jcg269CKBjmHWMuyMt205PTzcl+NZxSi8sONILBFoq7Xoxobjefvttefjhh6U0aCD7wAMPmM9Sh054UpzzK+p7VlzW+G1PF79cWReJqlSp4rZP++K3334rqamp5rGOSf/ss8/MdgDAafKQ/QYA4KRmz55tCwkJMTctwb3//vttP/74oy07O9vtWP3nRm+rVq2yb9uxY4ctMjLSNmDAAPu2YcOG2WrVqmU7ePCg0+8PGjTIlpCQYEtPTzeP//e//5ly2IULFzod98Yb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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, ax = plt.subplots(figsize=(10, 4))\n", + "ax.plot(ramp_time_ms, ramp_speed_rpm, color=\"tab:blue\")\n", + "ax.axhline(TARGET_SPEED_1_RPM / 10, color=\"gray\", linestyle=\"--\", linewidth=0.8, label=f\"Start: {TARGET_SPEED_1_RPM / 10:.0f} RPM\")\n", + "ax.axhline(TARGET_SPEED_2_RPM / 10, color=\"green\", linestyle=\"--\", linewidth=0.8, label=f\"Target: {TARGET_SPEED_2_RPM / 10:.0f} RPM\")\n", + "ax.set_title(f\"Speed Ramp: {TARGET_SPEED_1_RPM / 10:.0f} → {TARGET_SPEED_2_RPM / 10:.0f} RPM\")\n", + "ax.set_xlabel(\"Time (ms)\")\n", + "ax.set_ylabel(\"Speed (RPM)\")\n", + "ax.legend()\n", + "ax.grid(True)\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 8 — Capture phase currents at 1400 RPM" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "RMS current at 1400 RPM: Ia=25.176 A Ib=22.583 A avg=23.880 A\n" + ] + } + ], + "source": [ + "time.sleep(2) # let the motor settle at the new speed\n", + "\n", + "# Switch scope channels back to phase currents\n", + "x2c_scope.clear_all_scope_channel()\n", + "x2c_scope.add_scope_channel(phase_current_a)\n", + "x2c_scope.add_scope_channel(phase_current_b)\n", + "\n", + "x2c_scope.request_scope_data()\n", + "while not x2c_scope.is_scope_data_ready():\n", + " time.sleep(0.1)\n", + "\n", + "scope_data_2 = x2c_scope.get_scope_channel_data()\n", + "current_a_2 = np.array(scope_data_2[\"motor.iabc.a\"])\n", + "current_b_2 = np.array(scope_data_2[\"motor.iabc.b\"])\n", + "\n", + "rms_a_2 = np.sqrt(np.mean(current_a_2 ** 2)) * 0.1\n", + "rms_b_2 = np.sqrt(np.mean(current_b_2 ** 2)) * 0.1\n", + "avg_current_2 = (rms_a_2 + rms_b_2) / 2\n", + "\n", + "print(f\"RMS current at {TARGET_SPEED_2_RPM / 10:.0f} RPM: Ia={rms_a_2:.3f} A Ib={rms_b_2:.3f} A avg={avg_current_2:.3f} A\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Plot phase currents at 1400 RPM" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA90AAAGGCAYAAABmGOKbAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuNSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/xnp5ZAAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOx9B3xc1ZX+maouuVdsg41ppvfeqyFZ0jdlEzaETQ+pm3822SSkkGzChjQCKSwkJARCCKGEXky1Cd3GYOPebRWrS6PRlP/v3HfPfefdd9+bJlse+X6/ny1pNJp5784tp3znO5F8Pp8HCwsLCwsLCwsLCwsLCwuLEUd05F/SwsLCwsLCwsLCwsLCwsICYZ1uCwsLCwsLCwsLCwsLC4tdBOt0W1hYWFhYWFhYWFhYWFjsIlin28LCwsLCwsLCwsLCwsJiF8E63RYWFhYWFhYWFhYWFhYWuwjW6bawsLCwsLCwsLCwsLCw2EWwTreFhYWFhYWFhYWFhYWFxS6CdbotLCwsLCwsLCwsLCwsLHYRrNNtYWFhYWFhYWFhYWFhYbGLYJ1uCwsLC4tRw8033wyRSARefPHF0b4UCwsLCwsLC4tdAut0W1hYWFjsMmea/tXW1sIBBxwAn/nMZ2DHjh0wFrBmzRr4+Mc/DnPnzhX319zcDKeccgr87Gc/g8HBQahW3H///fDtb397VN776quvhr///e9FP//666+H97znPTB79mwxzy677LKi/u6KK64Qz7/kkkuMv7/nnnvg6KOPFp8rvva3vvUtyGQyvud1dXXBf/zHf8DkyZOhoaEBzjrrLHj55ZeLuoYzzzzTs0bq6urg8MMPh5/+9KeQy+U8z12/fr3nubFYTFzXO97xDnj11Vc9z6XnfOxjHzO+79e//nX1nPb29qKu1cLCwsKiMsQr/HsLCwsLC4tAfOc734H99tsPUqkUPPPMM8JJQqfu9ddfh/r6eqhW/OMf/xDOXk1NDXz4wx+GQw89FNLptLjHr3zlK7B8+XL4zW9+A9UI/Hyuu+66UXG80el+97vfDZdeemlRz/+f//kf6O3theOPPx62bdtW1N8gqwKDQuhQm/DAAw+I90en+Be/+AUsW7YMvve970Fra6uYvwR0jC+++GJ47bXXxGc+adIk+NWvfiX+7qWXXoL58+cXvJZ99tkHfvCDH4jv0QG+9dZb4Qtf+AK0tbXB97//fd/z3//+98PChQshm83Cm2++Ka4Hr3fJkiVw5JFHqufhvd15553iepLJpOc1/vznP4vf45q0sLCwsNhNyFtYWFhYWIwwbrrppjweMS+88ILn8S9+8Yvi8VtvvTX0eXsy1q5dm29sbMwfdNBB+a1bt/p+v2rVqvxPf/rTEXmvvr4+4+O5XC4/MDCQ3xX49Kc/LT6T0UBDQ0P+Ix/5SNHPX79+vRiLYv8Wn3vSSSflP/rRj+bnzJmTv/jii33POeSQQ/JHHHFEfnh4WD329a9/PR+JRPJvvvmmeuz2228X43THHXeox1pbW/Pjxo3Lv//97y947WeccUZ+wYIFnscGBwfFdTU1NeUzmYx6fN26deK9fvzjH3uef88994jH/+M//kM9hj9feuml+Wg0mv/73//uef6zzz4rfv+ud71LfG1rayt4nRYWFhYWlcPSyy0sLCwsdhvOPvts8XXdunWex4eGhuCLX/yioukibRazfRx33323yCzOmDFDZJjnzZsH3/3ud0XWj2PVqlXwrne9C6ZNmyYyephN/Nd//Vfo7u72PO+Pf/wjHHPMMYLWO2HCBPGcTZs2FbyHH/3oR9DX1wc33ngjTJ8+3ff7/fffH6688koPLRgzqzrwcZ5Nxu/xsTfeeAM+8IEPwPjx4+HUU08Vv9t3330FFfqhhx6CY489Vlzzr3/9a0Vx/vznPw+zZs0S44LvjxlgTlGm67jmmmtEBh7HDp973HHHwQsvvKCeh/RszHLT9dE/wm233SbGrKmpSdDpDzvsMEGnLwR835NPPhkmTpworh1f469//atvPPr7++H3v/+9et9CdPE5c+Z4rq8QbrnlFsGyMGWRETj2+A8p4/G4Swb81Kc+hVEIzzXj91OnToV3vvOd6jGcv+9973vFXMU5XSpwvuJngtl7zKyXu55mzpwJp59+usicc/zpT38SnxkyMywsLCwsdh8svdzCwsLCYrfWQSPQ+eL47Gc/K5xMrJ1FBxHrWrH++/bbb1fPQce1sbFROOf49fHHH4dvfvOb0NPTAz/+8Y/Fc5DifcEFFwiHB18THe8tW7bAfffdJ5zTlpYW8Tx0uv77v/9bOEhY+4oOPlKJ0VF55ZVXYNy4cYH3cO+994o6bnQidwWQto7UZKRaO4lLBytXrhT0Yqwjx5rkAw88EAYGBuCMM84Q94iPY53vc889B1/72tcE3RrHkQOdMHTo8LnorGIAAZ3GtWvXQiKREI9v3boVHnnkEeGgcuBj+P7nnHOOcOoRSHF+9tlnVZAhCOiYv/3tb4cPfvCD4jNC5x3vEz8XDKQg8P3ws0CqODq9CAwOjBTwvr/61a/Cf/3Xf4l5YQJ+9ggMbHBgoAeDN/R7ei7WfUej3vwFXj8GNt566y3h4JYKCpCEzcFC6wmBgRv8XDBAhOsFa9LvuOMOsX4stdzCwsJiN2MEsuUWFhYWFhYeEG380UcfFRTWTZs25W+77bb8xIkT83V1dfnNmzd7nnfuuecqmjDiC1/4Qj4Wi+W7urrUYyY69cc//vF8fX19PpVKiZ9feeUVH+XXREnG1/7+97/veXzZsmX5eDzue5yju7tbvP6//Mu/FDUORAvG+9SBj3/rW99SP+P3+JiJmoyUY/zdgw8+6Hn8u9/9rqBVv/XWW57H/9//+3/iHjdu3Oi5Dhz/nTt3qufdfffd4vF77723IL38yiuvzDc3N3toz8VC/+zS6XT+0EMPzZ999tkV0ctL+dsvf/nL+f3220/NFRO9HOnbeO80bhzHHXdc/sQTT/S8H9LUdfzjH/8wflYmejmWKOD6wH8rVqzIf+UrXxF/q18XfX5XXXWVeO727dvzixYtyh911FHi8TvvvFM9F3/GzxA/52Qymb/lllvUdSFFHuc/zTVLL7ewsLDYPbD0cgsLCwuLXYZzzz1XUG6R+oz0bcy43XXXXYL+yoGZTU4TPu200wRtfMOGDeoxpCXzrCUKT+HzMNu7YsUK8ThlspGGjY+b8Le//U1QrzHLja9B/zD7iRnmJ554IvB+MKuOQHr1rsInPvEJ4+MoSIdZfA7MXOIYIEuA3wuOO47fU0895Xn++973PvFcAv4tAjPdhYCZV6R/Y8a7VPDPrrOzU1D98b2LVfquFJh1xmw7MiKQVh8EUp03PQep31yVHr8Peh5/rTDgvMX1gf8OOuggcX3ICDCVIyCQCYLPxbmKgm2Y6UbWAae4E/BzvvDCC4VwGrEckJ2BlHwLCwsLi90LSy+3sLCwsNhlwPpgbBWG9bFY/4qUaJ2Oi0BaNAc5huigEVAR/Bvf+IaglZPzS6B6bXRMkT77k5/8RNSvomOHTsyHPvQh5ZBjzTcmBIPUpZFmHQSsYyanf1cB76HYx/Feli5dKhwxE/S64GLGOQhY1/yXv/wFLrroIhE0Of/880XgAh27QkAaOSqAY3srXutcSj12JUCaNTqcWOtfTHDAVI+NlGwePMDvg57HXysMWKv/29/+VgSB0IHGsgcsdQhSVsfgFNLycQ1hEGTBggWhQQSkmP/bv/0bbNy4UbRiw3ICCwsLC4vdD+t0W1hYWFjsMmB9q14fawL2HTaBapqxHhtrl9HpxTZkWOuLjglmSrFOl4uG/e///q8Q4EIxq4cffhg+97nPibZM2FYJ63LxuejsYasl0/tiNj4I+P5Y34tiXMUgyKnUxd84gpw10+N4L+eddx7853/+p/FvMOBRyjiHYcqUKcJpRhYBjh3+u+mmm0TLNBQ/C8LTTz8tAh9YL48trFB8DgMb+Le60NeuAAZpHnzwQcFwwHppAtY4YzYaH0MhPfxsSRgP6+GRncGBj+F8JuBzTW3K6DGcJ4WAooHISiBgn3esE8e685///Oe+52OgiD+/EHDc0Sn/yEc+IgIEGCSxsLCwsNj9sE63hYWFhcUej0WLFkFHR4dwnNB5I+iqzQQUsMJ/mBlHYTF0Zm644QaRbUWHHZ1MzBzrTmkxQBVxFMpavHgxnHTSSaHPpUwyBg04OG2+EuC9oFBWKY5YIYRln7Hn89ve9jbxDx1+zH6jijqK0qFqugnYLxoDJOis86wsOt2lvHe5wCwvwkTBRgE6nAfXXnutUICnXtfYy5s72Cgut3nzZiXwhsDnYkABx4GzN55//nnRg76cuXX44YcLVgaO6Ze//GUfM6FUYKAGe46jUj8yFLCXuIWFhYXF7oet6bawsLCw2ONBGVqekUUVbMycciDtHDOYHOh8o1NEVGB0vvD1rrrqKl+GF39G5z4MmFXGDCUqbe/YscP3e6QJUxstzJ6io6PXVuvXXS4wc4nOPzq0OtDR18eiGOC90d9z6OOCY4pOIiKsPRaONTrTPLuP2WWkO5veW3/fSoFttVBHQP+HlHxkYeD3GERAIF0ba6sxqMKv9/rrrxf38O53v1s9ht/j54+BIALW02OdPb5eGO270PwaHh4WJRIjAXTesRYcAyMWFhYWFqMDm+m2sLCwsNjjgfW4mDVGmizSxdEBwhZTutOMVGJsNYZ1r5hpRKcTn4eOH9XzYnYYM97YVgudP8wEojAaZs3RAcNsJjoqQcC/R1o0ipIdfPDBgl6NfY8xCIBZdXS6eH9pdM5/+MMfiq/o5KEDjsJeI4GvfOUrcM8994jsO74n9r9GsbNly5aJPtJ4f6VmN/E1EDjOKNyGY4cieHj9O3fuFE4s0vQxW49t1jDji+MQBGwJhg4k1n5jjTHWmWOtP2bGsR5df+9HH31UPB/p2ZiFPuGEE0Lbt7322mvie3RU8fXwsyVqNQYFMFtsyhhjZht1BvDz5yAxM6xZx/vGUoJf/vKX4v75faLTfeKJJ8K///u/i97eOM4YTEFnHQM65eKQQw6BhQsXwu9+9zvhKJvagZWCI444QvyzsLCwsBhF7CaVdAsLCwuLvQjUCuyFF14o63lPPPGEeBy/Ep599lnRsglbjs2YMSP/n//5n/mHHnrI87y1a9eKNk7z5s3L19bW5idMmJA/66yzROsyHdhm6dRTTxWtn/Aftm/CVksrV64s6h6xTdcVV1yR33fffUVrpqampvwpp5yS/8UvfqHaUlG7rMsvvzzf0tIinvPe974339raGtgyzNTGydTeitDb25v/2te+lt9///3FdUyaNCl/8skn56+55hrRmou3nMKWWDr068CWYJ/97GfzkydPFi2myFT461//mj///PPzU6ZMEe8ze/Zs0bJt27ZtBcfqxhtvzM+fPz9fU1Mjxhk/d7pfDmybdfrpp4vPGH9XqH0Y/h6fZ/pnatNW7Jjedddd+SOPPFJc7z777JP/xje+ocaSA9ty4WeLrdiwdR22ASs05wn43AULFhh/h+3A+OcS9vnpoJZhYbAtwywsLCx2LyL432g6/RYWFhYWFhYWFhYWFhYWYxW2ptvCwsLCwsLCwsLCwsLCYhfBOt0WFhYWFhYWFhYWFhYWFrsI1um2sLCwsLCwsLCwsLCwsNhFsE63hYWFhYWFhYWFhYWFhcUugnW6LSwsLCwsLCwsLCwsLCx2EazTbWFhYWFhYWFhYWFhYWGxixDfVS9cjcjlcrB161ZoamqCSCQy2pdjYWFhYWFhYWFhYWFhsYcCu2/39vbCjBkzIBoNzmdbp5sBHe5Zs2aN9mVYWFhYWFhYWFhYWFhYVAk2bdoE++yzT+DvrdPNgBluGrTm5mbYUzE8PAwPP/wwnH/++ZBIJEb7ciz2INi5YREGOz8sgmDnhkUQ7NywCIOdHxZ7+9zo6ekRSVvyI4NgnW4GopSjw72nO9319fXiGsfyJLYoHXZuWITBzg+LINi5YREEOzcswmDnh0UQ9ra5ESlQmmyF1CwsLCwsLCwsLCwsLCwsdhGs021hYWFhYWFhYWFhYWFhsYtgnW4LCwsLCwsLCwsLCwsLi10EW9NdBrLZrKhTGC3ge8fjcUilUuJaLBxgvUgsFhvty7CwsLCwsLCwsLCwsFCwTneJfdi2b98OXV1do34d06ZNEyrrtp+4F+PGjRNjY8fFwsLCwsLCwsLCwmJPgHW6SwA53FOmTBFqfKPl2OVyOejr64PGxsbQJux7EzAQMTAwAK2treLn6dOnj/YlWVhYWFhYWFhYWFhYWKe7WCCNmxzuiRMnjuq1oNOdTqehtrbWOt0MdXV14is63vg5Waq5hYWFhYWFhYWFhcVow3psRYJquDHDbbHngj6f0ay5t7CwsLCwsLCwsLCwIFinu0TYWuE9G/bzsbCwsLCwsLCwsLDYk2CdbgsLCwsLCwsLCwsLCwuLXQTrdFvAzTffLFS/9wR0Dw7DmtY+SGdsKzQLCwsLCwsLCwsLi+qHdbr3Alx22WWCdo3/kskk7L///vCd73wHMpkM7GnY0NEP/ekMbOlKqcd+8IMfCFG0H//4x6N6bRYWFhYWFhYWFhYWFqXCOt17CS688ELYtm0brFq1Cr70pS/Bt7/97T3aic1kc+r7//u//4P//M//FF8tLCwsLCwsLCwsLMYW0pkc/HPdThhmPsBYgnW69xLU1NTAtGnTYM6cOfDJT34Szj33XLjnnns8z3nooYfg4IMPFv2/yUknvPDCC3DeeefBpEmToKWlBc444wx4+eWXPX2y0ZGfPXu2eK8ZM2bA5z73OfX7oaEh+PKXvwwzZ86EhoYGOOGEE2DRokUFr/vJJ5+EwcFBkZnv6emB5557bsTGxMLCwsLCwsLCwsJi9PHte5fDe3+9GK55aCWMRVinuwKgozmQzozKP3zvSntaY69vwsDAAFxzzTVwyy23wFNPPQUbN24UTjKht7cXPvKRj8AzzzwDS5Ysgfnz58PChQvF44g777wTrr32Wvj1r38tsul///vf4bDDDlN//5nPfAYWL14Mt912GyxduhTe8573CMcenxuGG2+8Ed7//vdDIpEQX/FnCwsLCwsLCwsLC4uxg1uf3yi+/vqptTAWEYcqxQ9/+EP42te+BldeeSX89Kc/FY+lUilBnUbHDjOrF1xwAfzqV7+CqVOn7pJrGBzOwiHffAhGA4u/eCK0lPF36Kw/9thjIqv92c9+Vj2Ofa1vuOEGmDdvnnKSMbtMOPvssz2v85vf/EaIr2Em+pJLLhFOOmbSMYOODjJmvI8//njxXPzdTTfdJL5iBhyBDv2DDz4oHr/66quN14qZ7b/+9a/CWUd86EMfgtNOOw1+9rOfiWy8hYWFhUXlWLK2A3771Bo4pW60r8TCwsLCYm/H5KYaGIuoykw3Up0xo3r44Yd7Hv/CF74A9957L9xxxx3CGdy6dSu8853vHLXr3JNw3333CUe1trYWLrroInjf+94n6OCE+vp65XAjpk+fDq2trernHTt2wBVXXCEy3Egvb25uhr6+PuFIIzBzjTTwuXPniufdddddSqht2bJlkM1m4YADDhDXQP/wM1qzZk3gNf/5z38W13TEEUeIn4888khBj7/99tt3yRhZWFhY7I34198sgcdWtMEd66rSJLCwsLCwGEOY1lwLYxFVl+lGR++DH/wg/Pa3v4Xvfe976vHu7m5BPb711ltVVhazqFijjHToE088ccSvpS4Rgze+cwHsbuRyORge7C/pb8466yy4/vrrhXo5Zpvjce9Hj9lpDlQ65xR2pJZ3dHSILDM6vli3fdJJJymK+qxZs2DlypXw6KOPwiOPPAKf+tSnhFAbOtb4maH6+EsvvSS+coRlrPHzXL58ueda8d5RUO3yyy8v6f4txiZyuTysbe+DuZMaIRqNjPblWFQBVrf2wpyJDZCIWQdTR3fariELCwuLXYlsLg/r2vtg3uRGYWtbOOhJDcvvAKZap3vPwKc//Wm4+OKLBY2ZO93o0CFFGh8nHHTQQYLmjPRkk9ONFHT8x+nMCHwd/MeBP6MTik4f/iPUxne/4ZbPRyCTcpxifi3Bz8+LTDZmoQn87+j7sMeeffZZ+OUvfynqsBGbNm2C9vZ2zzWgI46fDf5DsbZDDjkEXnvtNZGpxkz39u3bBT1ch+keVr65HF588UV4/PHHYcKECerxnTt3iqDKG2+8IT5f02vhNeHnpTv4ewNo3urzd6ziZ4+thl8uWgufPWsufO7s/Uf7cvZ47G3zQ8d9S7fBF+5YBh84fh+46m2HjPbl7HGoje29c8MiGHv7vmERDjs/SsPX/74c/vLSFvj22w6GDx4/C8YySpkba3c4PhgiFnHs+GpBsddaVU431mqjYjbSy3WgQ4dZXKwz5sB6bvydCdj/+aqrrvI9/vDDDwsnlQOzrVizjFlbLkA2miARs2ImA1K9KaigA2vh0VHlv0eqOIIeQ4f997//vXB08X2/+c1vCjE2/Ft8DjIM0LE+5phjxNj96U9/Er9Hhxn/If38wx/+sAiUYFkAOuyYBV+wYIGovUfkmDbcX/74e/FaSCnnwCDK0UcfLbL23/3ud333gp8NXjuKwe2Jfch3F5BtsDfgl4udLewXT6yF/VNvjfblVA32lvmh4ytLMBAXgVv/uRlOiK0f7cvZg+Cso9pYfq+dGxaFYeeGRRjs/CgOf3nJ2W9//MAbML59GewNKGZuvNqBWX8nWbZ523a4//77oVqAYtRjyunGzCqKpuEHh3XJIwEUYvviF7+ofkbnEWnS559/vqhZ5kDnEq+B6qJHE+ggo+Pb1NRUFDUFqeMYNNDviYD3g6/Df48OM4IeQ0r3Jz7xCTjzzDPFGKHzjL2z8W/xORiQ+NGPfgTf+MY3hPONyuV333037LvvvuLvURX9+9//vnDWt2zZIlqPYduwd73rXeo9Muh19/fCcDoN9971F/jaV79qvGZ04H/yk58ItXWdFo+fE1776aefPuqf02gAAyy4RrC9mz42YxFXLn5YfY9q+hYAW7sG4aePr4GPnDgbFsxo3qvnh75vXrnYPfjtfHEwNJwFWPyYynTvjXPDIhx7875RCp5d0wEPvL4dvnbhgdBQUzXmdcWw86NMuyUah4ULd3+J6p46N7Y8sw7gLaej0fiJk2HhwmOgWhCU1NRRNbsC0sdR2AuznAR07jCjibRnVOPGLGdXV5cn240CYOgQmoB0aPynAyeGPjnwvdAxjUaj4t9ogujYdD2FgBnqMHz0ox8V/zhQgI7XdGPWWWcYvPe97/U8P0y0DscZ1dC5IrqOfDYrviaSSVi8fB0cNM0cJPjqV78q/pmA44HjYvoM9ybsbfdfn4ztVfcbhp89vhzuemWr+Lf+hxcbn7O3zQ9EW69bSrT/lMa97v6D0NrvMoKwWmpvnBsWxcHOjXBcdvNL4uvU5jr44vkHwt4GOz9KQzqb22vGq5i5saXLPaOHs/mqGptir7VqlGTOOeccoYL96quvqn/HHnusEFWj7/GmsR0WAYW9UF0bBb8s9nzkmJPPqeYWrtOwubM4Csvehsa9KKtQCDsH3PKX4WxhzYe9BRt3umvHau65aGfBiIydLhYWFWNrd2q0L2GPEjtt73P3GAvwOJYoqmbhP6MxIDEWUTWWKlKpDz30UM9jDQ0NMHHiRPU4KlojXRxriJGWjH2o0eHeFcrlFiMPvvfk7UbkU1w+/9qnxBhd94Gj4eLDp4/2Je1RaKytmq1slwMVURetbBPfL9vSDUfPHj/al7RHYONOt+PDkPUuFVqZ0z1sh8XComLUJqomn7XL8e17l8MtSzbAn684EU6cO3G0L2ePw+due0XYdBYAm7jTPUbP6DG1M1x77bVwySWXiDphrOlFWvnf/va30b4sizIy3dl83kNv39vx0PIdKiixckdxAnpjHf1DLi22qbZ6aEi7Gjy7zQ+xvR0bOxxxSEQK65gtBF7e2Km+z9gt18KiYtTG977OKSagDfeHxRsATblbn9842pezR+IfS7eN9iXsMdjRMzTmne6qTg8tWrTI8zMKZ1133XXin0V10pA4cKO2LQwdPLOqXX1vKcMOOGUtZueJ0aFs79szOi3sadQ1m+l28exqtrfk7EKyCMeila1wz6tb4YJDp8EFC8x6OXv7vlubsE43YlVrn/p+xjhHnNfCIiiJMsjWkKWXW1jsYuiMcsx2R8EagRiM4NmoYesw+JzulOXFGh1KW0vnYlu3zXTrwHrC17d0q5/tMrIolLn8+C0viT3m4Td2wAVXWaeb0D3o9umN2yiwL1lg91yLMOi2yli1c8cUvdxi7NDLEVZgwq255I6UaK1m4cniDmXsgW7MdLN63b0dvSm3HAHXky1fAegZHPYEO63T7QXOEbu3uHhrR586i/qGMpZ1FeB07xJqbKb69vJnGIuGj4+FRSGne6xmuq3TbbHHwDrdhWmxY3kzKhU2020GH4uOfksvJ6CTQMCtxq4jvyE8RpMLZeNjv38RTvnh49ZhkFi8bqfn5w5bvqLA58iIl69sXALw/ekAz1wL1QK0355f26F+tmvIgbVrw5MoLXWJMV0CZp1uiz0GWQO93MLvdI9V2k2paO/lmW47JgSembP0crPTjbBzxm8Ip+2QKGSyOXhsRaswBjlNdm/Gdq0Vlt1fXHQPcKd7hNkR914JkM8CPPptqBb0pTLQn3bHwTrdwXPDMkZA7SXTW2rHtJCadbot9hjobcJ0YbW9FbrTbenlfoNvqEC9GFKuf/PUGljNhF32hky3pZd7jUAOW2PoGsIx2bjcEkbcs+fq+1eon0dc0LN3O8BT1wD0u5nAaoDOKLJOt4ubnlunvh8a6YWUz1W9c2mdbgeDLBChHrNnEVASZaYU3EMm2lgsAbNOtwXcfPPNMG7cuD2OXm6dSwfbuhwBqClNNeKrpcU66OhnTneBqOh1T6wWRvS5P3kS9i4htfSYPLjKyVrqhs2IG8ZVCDKEaW8Zo8mFkvHkqjb4v2fX7boyjfu/AvD4dwFufQ9UE/Q1ZLsjOFjX3g/Pru7YdSyaqnS6vddsnW7vGkrGoyBjnTYADACdA85eMk1mutFsGYtUfOt07wW47LLLIBKJiH/JZBL2339/+M53vgOZjDfzM9rQ19dRCw5Q1x2LxWDGjBlw+eWXQ2enq+S9N23S4+qdWhdLL/fTyzEQEbZBv7h+75kzPOuP49Kr0apHBFXmyHOaY33SaedjBbKY093sGDo2DuGgjfWL3SWMkbUy+LflJagm6M5BoUw3Bvz2hqDf06vaPD+P+N6Sy44Jp3tvmAvFrqG6REz8E48VqOvZG8ZtIO3YKePrk+qxsZhgsk73XoILL7wQtm3bBqtWrYIvfelL8O1vfxt+/OMfw54EynTHiMuXBxEcwOveuHEj/OlPf4KnnnoKPve5z8HeBKptaahxOvzZ+h+zwRdm6OxNLVx0Y2fEHYbHvwdwzQEAXRuh2uq5k7EoNNU668iK7/kz3TmICFbA3o7hXC6QVTMimHm0l2peZQFgEjsK21vQUfi3G/8J7/jVc2MyY8VBNf9zJzWIrzbT7dotFOTEn0d8z33xJucsan0TqgU0BuhwUz/3MHr5Dx9YAcdf/RhskYzHsYpBOS7NdW4n67FY122d7r0ENTU1MG3aNJgzZw588pOfhHPPPRfuuecez3MeeughOPjgg6GxsVE56YQXXngBzjvvPJg0aRK0tLTAGWecAS+//LLngEVHfvbs2eK9MCvNneOhoSH48pe/DDNnzoSGhgY44YQTYNGiRZ73p4M5HnOnZVNTk7hu/LuzzjoLPvKRj3jed28AHeCN0um2tHsHOyUdqRi6MNWs7g1I7WoK6FM/BuhvBVj8K6i2eu7G2rgydGym23W6pzY7TjciNQYNnVKhU2E5q2ZEEHfHG7o2QbWA9thpkhnRkxoOZZdgy6hXN3XB+o5+2Bt0V46ZM34X1XRX35lP+ytmLhMy6K2f2RXjvs8D9O0AWPRDqBaQg12XLM7pvuHJNdDWOwRX3bMc9oZa94aauNLQsE63hRei70z/6PwL24RTPQBD4YJRdXV1kE67G+DAwABcc801cMstt4hsMmaW0Ukm9Pb2Cof3mWeegSVLlsD8+fNh4cKF4nHEnXfeCddeey38+te/Ftn0v//973DYYYepv//MZz4Dixcvhttuuw2WLl0K73nPe4Rjj88lkC9JG7R+h1u2bIF7771XOOziNoezos/sWAcdXuR0j8WNqByhI/2zT4VluvdCp3uyzF6GUUBbe1NCYO6xN3eMWZojz3Q31MSgJh4NFIW6/YWNgua2bHM3PLDMDTqOdcXlyY2OE1UoS/fQ8u2wdHMX7C1ONynpjrhgGJ7hhEz1ZLDIOZjQ4FBABwyiUKbAhS5iOHaDV7UFA3qYoLjzpc1w87ProL/Y0p8qzHTTPlKbiMLEBucs6ghZR/ct3Qp/f2WL+FoUure439c0QbU5l+hwo+NdbE33ixvGdolcSo4BMiOQkTZWO4y4eXyL0jE8AHD1jN3+tmI6fhrpNC3+X+YyADvXON9PPwIgEvVt+I899pjIan/2s59Vjw8PD8MNN9wA8+bNU04yUrsJZ599tud1fvOb3wjxtSeffBIuueQS4aRjRhoz6IlEQmS8jz/+ePFc/N1NN90kvmIGHIEO/YMPPigev/rqq51Ll4GEBMt0f/WrX4VvfOMbkM1mIZVKCYf7Jz/5ibiPt3Y4Dv+8yY2Kej0WYenlfvSlM54gzXA2H5pdiDL5YZw7qBMwVkEHFaqAYoQ8zND5+WOr4I9LHJr4i984FyY1sgycCT3MIIrFq87pbqxJqKCebhhf/vsX4bVNXfDShk74y4ubxWN/vuJEOGneRBiroEwl6kWgsA/uNUGdAFa39sLHb3FqkNf/8GIYy6CA3tzJDbCtOzXyQmrc6R6uJqfb2VsmNhbhdLMWWmNd5bxL3usUyRgJcxZe3tgJX7rjNfE9HmH/fsp+hd8A24XxwGfUcdaqwW5Jxh3ncntPKnAePLe6HT5z6yvqZ3TSC+67m19wv8+mqy5whcEISgYUo16+sz89pm2XFKt1x7MI15Ct6bbY88Fr0bLuoXffffcJ2nhtbS1cdNFF8L73vU/QwQn19fXK4UZMnz4dWltb1c87duyAK664QmS4kV7e3NwMfX19wpFGYOZ6cHAQ5s6dK5531113KaG2ZcuWCaf5gAMOENdA/9BhX7NmjU8sgoxiPJG+8pWvwKuvviqy4xgsQFx88cUwOOTe21hXfrT08mCDDjdnqi9MFVnTPRajpwSsx6X5MXO803qjLYRevr3bNYI6iqGhd653vx/YCdUCyrQ11cQDM93ocCPuesXNoBSddal2QyeJoj7RUEeKz5XeEFrxWMpczhpfL772jnSmFgP2pu+rZL5MVJnuTFGZ7qL2lioFOpfkNJE2QtgZs7XL7XXeWqzeBs9084DNHgwaA9xvKZgbVKbx3Bpv67w3t/UUfoM0Y3P2e4Xs9mQQuwHtOXS8EamAPVcXUAsLclU7BuUaqkm4bLSxmGCqnlTFnohEPcB/7X6jLIeO9WDQYccWaWZI1Y5hPfT1118v1Msx2xyPez96zE5zYDSNL3iklnd0dMDPfvYzUReOddsnnXSSoqjPmjULVq5cCY8++ig88sgj8KlPfUoItaFjjc45qo+/9NJL4isHOt+ErHy7eNRZcPgj1pCj2jpi7rx58MVvfh/ed/G58Pf7H4ZDjz+1NCd02V+d2p/33QIw5WCovky3K0iyt4MMOnS4FRWpyEw3HnpUSzXW8OOHVqrv95FOt55d2DkEcOHPn4WPnLwv9LHgFf++KKe7zw3KVYuhg2uI9osgCiiyJgivb+mGsQwKPKCRU5+MQ/dgxpd1+Z8HVwihqE+f5ezDVMO6YIaBaaWjdwfAH/4F4Mj3A5xyJVTb/jJD9owtmgZcLNIDVZrplk63dKJ0J2DVjl644g8virnSVOvaFG1jONPNgwvkXIbRy/nzTT2bjeCZXAzS1DZD9WS6XXp5e4Agoc4kKUr4NOMGL6CvrQpZV64Nru+5L67fCV/561L4ygUHeh7Hs3yssjoHeaZb2nRj0dYdm5/e7gIa8klHrXK3Ap1urNs2gUfGsu4Gh+Jl5LyWg2effRZ+9atfiTpuxKZNm6C93VHs5HXib3vb28S/T3/603DQQQeJLPdRRx0lMt2YOT/ttNMC3yOvhNTMGy4eUJmc87v2bodaXtLCvPNy5+vdnwa44nGoFlDE2NLL/fRPdLpptoQZxnyO4KFHRuNYw6+fWqu+n9IkxY602vdHtkRhTVs/fPPu5XDoTNd4KyqbhwJqpu+rhEaNjgAd7v1DhQ3eHVrrqLEGcg5EfaEMROmO1PWLHDbSjc+4c2tTsU73cz8HaHsT4JFvVqXTjSUaCJwzyCLhIp8VgWfpqijTTYHNoJruj//xJVjfMSAchh+96/C9gl5Oc6W5Ns5qdHNFOd1FBXPQpuMaPVWT6c66me6mZGimu1NzuosSPs2kq/Is4k43iQfr8+XK214VauWf+pNXNBjX0ZyJo+Bz7Aak5F6C51C9tHVHnGG0B8A63WMNnIaE0b/6SSPyskgrR5G1Y489Fnp6egTtG51sws033ywca6y5Rqr6H//4R/F7zIpPnDgRPvjBD8KHP/xh+N///V/hhLe1tQm6+OGHHy7o4phVp5puV/QqL4Tatm/fLn7/5qp1cO33vwnjJ06CI491xNTKioYFBSz28MMLqbFhmX0UF1v0Vhv0DwM88kYrnDJ/CrTI3t6haHvLoWftewpUY6Y7EqA4jEDlXDz0eSS56I08mwFY8xjAvqcBJB2aaTWBqGth64M7nsU4oR6Dr4qyC3y+0P5SjMEbps48FkDGHjrdxKQJojC+taPPp9ZcEIOdpdWiombAjjcA9j/HCWqPttMt2SK0Plrqo7uAXs4ydlUmpKZnate2ufvDnS87ugi7pIPCnri31CegJi47I4SUvPFzaqCY0jhkLOaGq87ppnMHx2Qy0csDgi96S76ihE95pru/3UlGSZZkNZQ6YRKFapb1THdQyWTbSHdR2IOQkvMFA1eTGpOwunVsBuv2/BlqUSK0TPfQyDiYN954I3R2dsLRRx8N//Zv/ybagU2ZMkX9HkXVfvvb38Ipp5wiHGmkmaPSODrcCBRMQ6cbe4QfeOCBcOmll4o2ZCi4Jq467145zyZ885vfFPXlSIl/37suhbr6erjhT3+DceMnqOeULLZQZUqgPiG1ACfqBw+8CVfc8gr814tx+NSfX4XP/LnI1mrXHQdw80KAdldJfk8HGS7j6hKqplt3ulF45NLrnoWLfva0xzgsmir6jy8C3PpegCe+D9UIMgD19dHIQq1bOgdLo5dzWiwGaqqklQ13urFtWLHzAB3Qscws4dkoU6ablxjx9bWmtb90WmxvEWrwPz0c4E/vAnjrIRhN9MjyLTT+kB5L4o0jAgw+cIehSujlGOsl1hXVdPezMaEMHuH5da7mQ5iY41hiXVEtalhNNxeYGyjmLOKsiCpiRvCabhLeC+p3z+vcEWlW4lPU3oJCczzAtweD1kkTMiOoZZi2t1BQS8dYdEJ9qu5xdLopSDP2ggw20z3WoBvBmSGRhQ7DZZddJv5xoFPMDS7MTqOTzPHud7/b83z8FwSsGb/qqqvEPxMoy82pRQ8sXgqHzmxR9bi44Wzt8hsomWy+NFXHKnO6dXp50IH026fXeX5+epWX/m8EE9uDLS8BTJoP1eZE6Y9xCiyhk/UH1Y3DQLz8e+fr4l8CXFAdjjcaw1gf9/Ez5roGoEZd49OHO+R9RWW6mQGI2Rd0HhJuNrAa5ktW7jW9Rc4D/NuCqu5jINNN1FjudAcFNBev9QofBYL3oEY9gJZ9wp9PGb2NzwEceCHsCT1jkWHUkUmPXNsrPVNZJU4U30ZM6uVhDiT27B6r4HtLjWQX4ZkdZJN4Mt3FjIueONGd8CoI6DUk46H3iwFyjiBhMQ944IrGpWFiVdHLKaPdo+0t4wOc7rEqSJjP511V9yQT3huDQQab6R5r0B1KbCFWBcgH1PNwZxzp0+a/zavamLHsdLv0cu/1o9jThT99qrwXH2DGc6q7+uro6hLiH3+MwDOZ2DqrJKd7kPUinuCq+u/poKzs+46dpTJ0uqhPKsCeKcqp0B0EXmtYLZluuY6KdaKoHVAoXv8bwM+PBrjrE1WT/dezUQ3S6eZUxyChJ6SXb+wYKE14j39vAh+3mtETisLabQo2YCaKgp2cCYLnzSdueUmIzJUMfQ0VynQvvQPg/y4E6N0Oe4rTPUEKYyELC8fL+X3wvC8qo4t4/jcAPzsC4FFzcH5P31u4QGdQwMojpFYMvXzI1a9xXrg6gjRcSI0Cevp+cs1DK+Ej//dP3zgMFku797xhddDuOb1csfS0MyaIXV+0E/rYd511tOSGws/tWANw4wUAKx+E0cIQY4bgnosMI3Fp1um22PORD85k7sGgrDpmtXG/cf7XOqCFGLOltdGqHqMYDRoKKATRy79z3xuwYrt2MBcLrkDd5bR/q7a2G0H08nYWPeeRZO6AB6Jjtft9zBx13hNBytvY654y3brxF5TQLpleLl6sp+qCNEjr06mxvAY+6G9D8fIfAHauAXjtzwBtZThiowSqPfVkupmDFJahXNNWIOCC+zUXOOp2a3yN4Bm82iJE2nYRuMGPY6KCNGzhLN3cBQ8u3y5E5kouP9Cdg0wBp/tvHwPYuBjgkW/BaCItbxOdKKr/53XJ5HybUHSro6evcYIzz/zEoeFXAXjmkvbcMIp5yUJqemCzSoLjPKBHNGp9TH75xGp48i2/NkiqHKe7ShgjnF4eZLsEJQa6ijmL+Dp64urCz73zYwCblgD8+X0wWkixzxvPorFML7dO91iD7phWSaabfGZkYyEli/QwvJnu4L8PO/B9qKJMFHeYXPVy7/UHGX3FaJF4jOKXfu+Ih1UBKKuCzmWg0x3gXG9mdcyB4PVhVaSMSnMBDeNkAL08MNNdKr1cvHiZwZ49INOtC+pRDbwOXf29INVx7SKoFnDDuJ5quj2Z7kz5jhSePR5hzwLriP8+MnqmCWXjcP/E1jWkAcCZETzI+9RbbZ5SltLp5UXWdPftgNEEbSPU0ocYaTRe+rmEOHBqU8F+3h7w+dKzBaoBdP/Yco9aHen7Lj4H20AhK21z50BpLcP0PbZKziOe6SYGAL/fIOZi+ZnuvqpLGASz9Mz3X9R84cKMQ92lJRhGCYPy80YBPbTpLL3coorp5dWS6Xa+Ug0U1XF7nO6RynRXEb2cK0+TszCcc+rFCDNazDW1tKGHgitQp3sBXroJqgEUZMH2coFOd8CGXZTyMqeXD+ysimAEGjG0DpxMtzm7EOx0l0EvrxJDh4sduXRh7/0GlagUlenmjtP6Z6AagPdLQT00itFpQHhFB4ONPJ0pULDmEoX3wsB/rxvUo2AA4njgeeRmuoeNgZjLf/8inPajJ4rPePvo5UU67CgWNYqgaYGMEByXesmMICdCL3tCHLZPS2mZbs6kKVSOsIdA1aImYmJcXDE1954/c+vL8O4bFsMlv3jGw7rqL8fprpKuEW5AL+Y63cyZDlNuLyrTzVriVhPtnrREPPRy7YzRA8IUzBkcLuKMTjHbpZig3h7AVhtk7cIQk5pqxmwNu3W6xxzIe5VZmypwFjz0cvmz63SHG8XoXJCYWoE3MH9fJQcXZhWI/oqXz8eC2hpRTWZJB5ceNe/aANVFo3ad7p6RdLo9B1feW/teBawIHBdFL9ec7qGsmQJRFNVRz9JVQaYb9xbe1oe0EfT7DVovRTnd3EksRKPeQ8DnBe4tJiG1MGepYNaF99EtxunmmW7doN6NoHsmR8FELzfNCaScFwWfGnWRme5RplsTvbxBBmfI6abxGs74z1USXMNzrKDuCt7fcH8VO93OfmtSMH9shTk7XVymu6e0dbQn0ssNehFhmhpFjYse1KsSenkxpXF6qdd+kxqKHxeeMBAvtuczIwaViBrtuTKgN1IdI/YgWKe7ROTCOM57AsihpDpUQfHb851MGlU30w1FZbqV0y0/l8DPx9NeYg//DBmIooYHF92rTuWjDfvn7z/Kp0xckHavH+CFesZ2bwG49V8B1j4JownKKgl6eb354AqKkiIVNIzaZjy4qoDSxzNtvKZ7RIXUKJuQbKwapxsPdFovvGWYhy6czfnYMuRUFOd0D5ZmFGMvalxH216D0QIPMmA2SneigjIrRP0rOdNdyPjb4zLdzngoZgSbL6Y58a7rFwthqILQM3LF9ukeZaebgnXkQDUQM0KOFzKwdFB/5qIo5nowYqe3G4cPz/4M4JFvjrp9Q+uIsnQ1VL+slfUEBUoLMiRoXCiRUgVnET93hJCaHBMM9NHZG8asoq4KJQX1CrGuXrsN4O5Pj7rOEe0jQTXdeBbp93/gNCrTyJae6cYe5ns4Uvoakiy9ohJHVQbbMqxIJJNJiEajsHXrVpg8ebL4uegWVSMMdCzT6TSkUilxTR6k0gAYccaaOBF5zgMMDgBEzfWKewqGhoYhn0lDDmLivvD7fCYLqcEoJMFZeOn0kHiMIxrLQT6TgVQqD92Qhra2NjEm+PkEZhOqyOlOZ92Dy+N053JQB16nADfwCxdMhQeX7/DQQ1vqo4Vrl5NNDr28UNbl7k85NatvPQDw7dETdCFmA/Z0D4oWB4mOYAQenxvUlsN4cFVBD1AeiEE6murTPaL0cpmJaprm1IJVgdNNRg5u18gGISeKtwwziR5hdmH51p4ia7qH/P3Lw86Hmy5y5tiO5QBfWAajAbpnZEUgk8bkdJvo5XMm1gsWScGsi56tLmT88T7eusM+ilRHU9YlKBCDwlCfPWf/QH2AkluGcQdhtOnlconQPGmS+y61ezKxzcbVJ0UAHf0sHNem2kQJNOod4YELdLgRB10CMOt4GHWHQY5LULCT43/edRh89c5lar211EULj8uEuQAdq6qQXo413e79pTJZUboRdt4UV9OttwwbCA9Y3fVx5/t55wAc+k4YLdYVlRTgfInJMwLHAp1ttGdMe65wul8r0gktN2EwijoaQyy5xFlGGHwoqR1wFcA63UUCHbn99tsPtm3bJhzvUe9pNzgIdXV1/smIVCRcdMkB93DvTQY73WgcYjY8VkT97y4EbiaoVCjEn3prhFGHC264y63B3NGT8om1DNTEBO2vLxmDwYYk1NfXw+zZs/3BCO5M4iaE2YVELezpSHky3e5nzRXMudP9g3csgGMSW+DHryeFs9U7NKwywUbQuGB/S+F0F6BobX4J9gQQsyERdWsu9cNKdwqe+spZcM5PFok5hAd/SQdXoWwUGsdIK56wH4wWKGOCYiTRaIS1DAtXL587qQHWtvcbxwT3mjVt/eI5+JpqT2ks0unGccS/aZlp3nvaVgJM3B8gVtlRhOJE4+uTaq/goPuvjTs1l5xeTge6yelG5xKd7rDaQ+P+gqwaVBiuG1c4qNM9eh0DyIAjB5GczEItw46ePQ5e2tBZOOtCgQg05jDQiaI+YfsupxPrWazdCLovcqLIACSl90LsB9yHQp1uClzVtMgxCQl08vU1yqKo9HFT7f+s8XXw2qYuJSJnYlWJIFcyLgJcBeeLvpeEjQsvg9rw3Kg63bRGaJ6Y6OUc7zx6Jrz32Fnw9bteF+wa/HsKHBfldBdyonq2Oi33aiQbadSF1GJi7yXg/QqnW2NWoWN++an7wXVPrDE63bhXr2vvh9kT6oVzqvaXugkAgzvDW4ZtfbVwQA/P8M4NAJP2h91RAiY6RrAWc1jrP6EhKWw2HQdVkukOYxhx9swodoxIM+aiuBQWpMF1xFvxVTus010CMHuKDl0mk4GszECOBoaHh+Gpp56C008/HRIJbbN+4UaA568HOORSgFUPO07Uh+4CGDfL/GIv3wLw3M8AzvyvUYv+IRatbIXvPvGGEF756fsOgj/cu1yown7yzP3h3QftI57zlRueg07WBgrxoRPmwB+f3wCnHzAZvnPpIRCPO+I3PujO5N8/CfCePV80jDYjNOLwvtChwoOagg9I1eIiUZiVmVLnZGd2ZnKFs5dk2NRPdIzeQpludMz3AKRZppscLRwrzC4EUZNmT6wXh/9wNlM4S6cfXIUyb/deCfDqnwA+fA/A3DNgNI0cOrjI+MP5gvWUmM0UPYhz3vVxwNQmx+k2jMn1T66BHz24Ej526n7wjYUHuuPQOMX5WsjpvnaBQ/v7z3UA9RO8v8P2WrgOj7sC4OJryr5vNMTOumaRMMae+s+zfL+njFONPMiJXo7ZNzRicP6YMgizJtSXUF+oZ3Xbwp1uQtN0GC1QkIUMnAZjTbd//6BsZUG6MI1J41Qny42injguQWeRx+kexUy3j+rod6LCnO4+aTwHgjJyDZMcpzvMWeDrS28dtZsxpGW6cb1xjQxTX2oMXOA/dLoLliPo9xdWYsDnyvqnAU79PIx2YNx1us0CloTT508WZ7kYl1QJ4zJxHsAqJuxpClQiJf8XRwPMPBbgY4/AaILuH4O/GLDFdYSP0frSbZOZ4+rgmDnjxfem/fjuV7fC529/FS4+fDpc94GjXSZNvXS6uR6AjvVPud/j+Jlwx2UAK+4D+Nc/Axy0EHYFOOsM2Wh4VuO+i9lv3FNw3zDZbHMmyprukc5085KeGsexH1WNnrg3001ZcOt078XAzRIdXZ+zuxsRi8WE419bW+u/juEugL5NAPkUQLpTLrgUQG1AduHhLztf7/skwLEfgNHCYC4KW3qzMDcTFfc1ZVwTbOndDks29MCHTnGufVX7kO8gS9bWir9rG8iFfya6M7n8b1XhdBPthrKWuElnclij6jzel84osTlHrdx5HLO/O/uHC4tjUTACo8XF9IzdQ0BZFcz+U6ZbzzKZDqhaaQAWPLz0gyvMCcCMLTrciOd+MXpONxsTPmfE7zI5YeSZ1HIPmNoIDy43jxc63IjfPbMOvnGuE/zyOIthdXToMNDvW98A2PdU7++fudb5+sJvK3K6H3x9e6hAHmeLIDxZl2HH6TYZyGgEIgquIfz8ad1ghgnZRphdmDTf/Hxe+4o0/VGn9MlMt3K6/erKmN1GZ/srFxwIS9Y6ooJFZ7rjtQDJBieQFcak4Y7UKAqpUZs0ci5dqqN7v3rJAQY8yRE3Zao8ICcbWTHY271vu5NxMrHRuNOtBwJ3M+jjrgtwuk30clxbNI4Fg1e6YFjYWcTnStcmGE34gjQyiMWZESZNhAZkAKQyxY+LCtDJ/SZmcJJWP+qwSjb/0ymJqnOc2NEArRcK6uG8wX2WHifnsrk2LpItX194CHQNpgOd7l8tclpb/WPpNrgOTVWe6S5EL+/ZVtgJRYcb8dzPd5nTzc8ZOo9wjeA+S/tuz2BGra8jZo0TWW6ycYoKAOv7RNi4bFzifj+KelUZanUqbRe0czFJgMkCDA63wOgycUcSVkhtrIGMFRRSQ0MnbNHpEb9RbLmg13ScOn+S+Prcmg6RzcVN2GQYEy1L/11H3xB89a9L4aUNO8vLnKy4H+Chr4+66IbK0slxwRZZCHK6uweGWd2Ua7Q1SAqg3noiONMtD66wTPduVMLHzw0/P53ZQCADjzZnMng4Zc3kRNLzCgq10MEVqyk8LuhQEoKyeLu5RzefM3wemRzI2SFRdE+vd+UwYXH0pMKZbm4Ym0SgMBszAgqrYb2k9dY1CMy6kCFIRozJyFOGTqEADc/ItcwqLKa2+Bfu98gwGW16OTOKdcOOvj9sZgv8/qPHw6EzWxS9uLDTLffceI3jeIvHApxpnEd7ipAa0YWTxWe633fcLFGCUajNmgBl5CYdABCNO+UIvJ6dgwe1MBBoEg3b/jrAbR8EeORbu1RUbEgyZBrk5+9zug1GO+63NF8KtsfS95JiM92jLCzmF4HyzxfeWYSYNiYNhdBxoT23WPG9dU/DaIKCDhTkpK909tJZdNr8yfCnj50Ih8xoNpa4BEI53TKwEMYY4Y5ooZr4XaiCrij3MaftHt93aR7R3jK+PgG/eP9R8Omz9lfj4rAcvesMS32+9JfXhM1bcsJg7RN7RKAzrUrjXJulVq6jsSamZp3usQZyEuPM6Q6i3ejKue1FKK/uIuiG8ZGzxgkqNYq0bO9JqcyCxwlgB5e+MK999C24/cVNQlHWuJFGQyJnaDzc9n6Axb8EeOUWGE3oWTr9QKd2YXpP7gYS/yloADJ6eaEDp22F+31Czq1dBPzc8PO7+v43jb8npVycIwilSM2cSu48YKbO63QXopf3uNTYQgbgtqX+vxsFUMse6ukZlwEJftjT+OCBTjhc9tIl0RIOni1XRg3uK0RFC+vxyQ1jU5aO09kqUPHmxrx+/abAladOV/5OD9qhsmzJziWieYbzFemOQaAM/yjX6ep7LjkGfDxpvtQzNonrLBS4duoYIZzumvB11OuwFfYEp5tq+Os1NWq+Z+hON2Zk3P7vRWa6kRUxbnZ4eyxPTfewmVnywu+cLN2zPwXoWAO7CnSU0Oc/QzJBtnc781/XW/FnuktULw8LdPZscb/HjO4oBsf9LcP89HKizfJgBTlbBenl1K4SnUsKAgc5UlyscBurY94DaPd62zDaW8hW4UmUtl5kNRYZ7FS2S4jTzR3RQkGaYrsJVCguR6BgxKBUKqS9hdt0NHam8+jy378Ad768GT5z6yves5ZstLB1tH2ZucPPKNPLEabe7mMB1ukeA0DHS21QtBHh5pyoD48A6tmloOf1d+zythxpbTPCDCZ3kEiJWncu6aDTjeYdPZrRpm88YRmm1uXu95teMD8Hx2M3qIiqno6yjtIVDct4209oAlJhEWMcT+Wc0rgQRStsg371Vvd7POB2Ax0J63ULZbr5/dN9ITuC5gS2Urv5o8ez+ZKHTE+rb0wogOExACm7EEZ15IbxKGZdFL2cHVzkgNNYUK9hnEePf+kMuP9zpykaNX+e/veegAzuK4VYNLojoUff9UO+gkw3N0JMDAZl6DBxFrU+pKFDDtXcyQ1wxydOgse+dIZyFsRa05S3cX4pFoYygCNuICFICAznClfC34UGXjn0T33P4HoRBFM/bwI64irYpTLdta7THZRN0R2uIKcb9108j3YhSNvAr0btzq0uyTAiYPaJ9uaC7CLVdq8BYPy+BZzuIno087HbhZobrnq5c58UZMD5gsEuysBNaXLbhOEaooBNUPAK2Vrib2kfVaVOIYEX3VYxKeMjM2s3dJ1w11Gwejkx+W740DGq3r+BWq4VCuqRaNy4OYwxEuR0t4bvuaOgGVGX9Nbp0v3SOmmsSXg6RiD9HvfxlzcUuH5e0x1kv4qATEbLdBc4a3ZhiZ2uL8IZNXqmm++51GGCP0/fixbLsh/1uVPpUtg64joKpjNrN9m5GY1eriuYjyVYp7vKgU7GiVc/Bhf//BnvRsQz3UGGsX6Am573xj0AP54L8Ph3YXf1dCS4GQanxRNCV/l0KUvejWh6i1vDLvpCksNQT05Uqjha1kaZKdfx8DcArtkfYA2j5+wCkBNJbWv0dkcUJadML8FUh0g453+fhKO+87Bz+KlM9/hwJ+CthwGWXFdaX8wRAG+TFtSnG6FnmbiBfM5BU6BZBi1wXL4Q/yuccc9JACsfUM/50O+eh2O/+yi09sj7V/OlCAOQG8aj2M5FF1Lz1Bdq9HJ0EOZObhSUPl6WoBuAqDyrQHMlWe8G9Iqt0U0VcLqL6W0dAJ5xNWVf9dplU1CKK3kft+8EmNJUq5zuswYeAvjxPIAl16u//9xtr8BR330EXt/S7e4librCziWq43KMomCYnnUhZwr3W8zcIkz7bgMxADQWDf7N2dc8Ccd87xFnLqoAcJJlugPuVzeYg56HJT94Hq16ZLerl4dluvG5tAcVTS/HdVTQ6db2WNP+wgOlhYQwR0S93Ju5RLsc5xIFQiczpxvHjhgDptKWLV2DcMR3HoaP3vyCn0Yd5vz4nG6DI/X7SwD+Z1+AbpYV35Ut5pLmPt0YkKD9lxhX/PmhmhHoNGInBMT4Oa7yf9D64A7lKGsA6N0RVEnPsH4WufsyUq5P3d9JiCxe014ivVw7i7CbCH7+2J6RB18KtS7chYFQTi8n1GnjYtpzcVxoHenBKx40F+dfSne6Q9YRP79NZ9aTP3Ls3OV/h12JYc2e47aLpZdb7FFYurlLLMLVrX3O5k/RKsx0F6KX6weV6Xn3S6G1p/8Xdmemm3+PBxbVLuNGdOU580U0/fPnzmdORc4oVoLY0Yuicv3agR6yseJmTejaaM7oIvUcceflsHuc7rgx0+1Gi82Zbn3DwrFEQwfpPCu29zDnsgC93ET/3Q09mqmGPbA9lvy9nmXi2TruVKKhc2X8LueHP/+revzFDZ0iU/yn5/HzzrrzQ41LsZnu0XO6TQeXP9PtD9JgBJ2epzMj+Hr0ZrobC9fRccqwKeui97YuE9QnOCibFkYvp/ttlcyYSY2u6jQ5oV9Ny7X+4P9Tv7tvqVODe8OTa1wjDR1LdDDDMt2d6/YYp1vPNJFRZzIAx5ky3cNeZwFZN1gKhJ/Bxp39XiE1RYsNynRr+04Q1ZECf1i/vJuEsdxSBJcVQd9/+fwDhNjRx0+fJ0oSSqKXI/2TsrpBe6ke2DQ5l3xMd2E9KsUS6PPn7Y7wM6eSH3QC3nHUTPEPz2tywn3sMxSMftERQXt6VbvrXBaTodPniykYQQHzN++BXQnKxAXVdONZS2Kn5JAjFO0+zKmgYAyWOaE9R8GrIMeQO5SjnenW6eWaTeIGt7y2y7zJztnSRjXKQaC9k1pd6c4lOYpCVI7Ty9v8zE0+13bhnuyyroIDwCZ2Ec+I64HlaSzBtKFjoLRMNz+/UYBP1+1ZdLXz9Y6PwO7qRkMouhSwymCd7jGETZ0DAUJqQfRyPdNteN4I1RxitBcN1IeWa7V7IZsRRUb1TPcXzjsA/vn1c+Hz5x4Q2B4qxzbVjbgRqSj6ZHdjDaLM8whxPhtOUaN6qzKBwYQfPPAmrNxuNrrcuifnYFKGnTSY9d8TkNLVDP1w1MprHaEdej1daMxX0z0Yns096TMssmzIdL95H8BLN0Ml4HW5HnozAwqKcKVuciIpy0QHGDIniJalG4ryzTw//nOd1u+TxiX04OrzzgeT4BwGb5AdsQuzLkpIjQUq9KCUmi+sRsyUhSDw3vDK0BVOd4HSFd2RNma6h0aEXo71f+FOt6GOTouikxgU9ubWjeKCjquiUReT6ZZGdMvscAMPtQFwvvCauxGGnlHBMYmgMjIz7ExZlyA1at4yamtXirGuihBS0wO+pnF5RXYIIFbBLgLdF90nzZuUZhSjDtKnztwf7v70KYIyTIE/KuEoil4eVOuOIqcP/zfAlpe9j5vWCR+rXZnplh8v1eCKYF3c3Td4yc+17ztS/OM1zBsM3QU8jjg5C43TCt8LzReqW9WDdiNcDvfIGzvg+kVrfJoRyO6geR9ELyeaNf8doijNCNoviBGBe0yx9PKgTPdzv9zlmUtj+YrmRNH48J7M4nnJGJwUXQ5nbPmdWYATgZ8DBeZqx5nXEIoUmsYCtRF0m44zSjBwtYvKKU2Zbp1JE8TqDNIv4mUM4hxLlbCO9PNbP7eI0YaocEyWb+2BHz6wAnp5GZ+hGw3B0sst9kjw1iXCuaQMC9LLVU13BfTyoE2vRCzb0i0W3MdveSlA7Mi/GbmCJE4PQ2P0jznmHFzhETO7alMlpzvMACymz2GSiUBRhL4MXHXvcvj1k2vhbb+U5QEa9Jptl0Yt6eVD5ppuHLsr43+DYzb/AeCGU9xbYQZhZ1/ajQ7zlmGmzZUcS6xZpXvXszOY6bj9g07P6s0vQrngn2VQppsMPFK7dA3eYS/lT3Oy8ec1OdYbuWO1Zz4KujBliyLR4Cg6h2cc8mYRrT++y2kndtfHYXepl/P1lDbUdHOYlKv118qrTHddcfRy7iAYM92cXl6+083ra430ck0wzKQYS043OQiIekZ7DIIwIDKmTHfA3kLtjSYfEJ6xuvtTzny51WVjjDT0PRUpjLL8Us0Dk6jPOCnCh+POx5sbfyIArDLdRQipkfEXxBTAn3FMdofTXSDTrcakNiGU8Alqby7YMYLRy2MJc2YfGWbYuuj1v3ofNzFCPE73LszSZaXacsIvqociaTr7SO93b2rpp8p5EKkyMnTYuxqBrddMvx8hXPGHF+F/HlwhGFEc3AFyM93e+UI0c7/TLTOXYfTynq3O1xbZrrFQmQbXOzDtue2rAB7+upO53IWaLFxThdYP1fYTw8YVidUDwDH4c/L7cGHH7wFeu834+sND7POtG2ceE96Cj9YXnulG25cLFmZGfP6E1XTrwQjFLmJip/x5epCG9/7etLOETDfa9rqTrT932uFmId0ycOn1S0Ti7bon1oQkDPyB8YKCelUG63RXOXhtmYgkZ8ugl5MjaspcIuVkBMANEV2EpnA2KrimmzvmHFxJVfwtZWo9TndQlqmrcHaBj0sFYi0kfsE3Ts9ba3Rg17n0CqnpNd24Qc+PMJq8BO8h29nTYxaWM40LOZZIKw5Srubjtv7pEZnTERSnCu1JrTnd6kA3R9GRSREHNlfWLlL1q1Qrn031ufdaKENnqrs0GjtvVTwuxVK0PHVRmgEYxIzQ1bxNTndGjUsDo5cPVJDpThdfZxcCHmAziRKp1jVGIbUQp1ujPRbMdHtqutPhey6KIoXtQW/e63zt8a/hkYJpT6XkPgXnugzPmdZcKyjEyDZ5Yb279/E9zAkAlyKkNqAF/7Rx0c8mWpe7ADrt1c10e51u/Swqi14eRLtf95T5b01ON3e0K6CX4z7YHkLpdYXUDNTYdM5VINbYSbSmhEOgQZR+SeTpHOW1qKYAMDqW1CVi/3Odr2uf9D6H7zcVKuHz/YWzanRmkOowovp0e0Ua8ffUJooH9YIy3WIvpiwlBZnoq2nfQCdqqDt8z+V/F9SmbgTAy/3obJkoBeSoHCgo083LwYI66gz2yMB2JBYsvKf3vcfnjt/PbNOV0q6uCGAg35hc0rrRcNq4rl6u7y9BgXE+1lvau9wEQVhNN16byW4N6wKwdhGMBFa39obQy1mmO4DFWu2wTneVgzso4lBTbVqKoZe3eqlLpgN7hOjlw8yxMUW8jUJqRWS6XbEFb7sjbgAKJ18po453o51BBq++GemGDmZdeCCjgtopU5sVDt1JUq2xpHPZG+hERaEdZJYWIceGBz+6e9gBTYJh4qIMmzSNHzrcNY3mg4ofZBueg5GY00H1bjodSReY03unEvDnxgi7v7WLPLRYRHunPNAxm0vGfWhNd0jwQQdR4Xa3kFqIeI1JzZsQY30zUwPkLNQxenlfADNiQKPd79xl6uX88zP1AjZluqmMheYXOQSUlTOWIiC07JBwuj013QWcbirpoX7upj2IG3w8SDjCcGsH3b1DZbqHM2IN0ZxqYVkXdBxOkWJHz61uNxp/Yo9X+iJcSK0AvZz2IX389LOJXm+3ZrrD6Z96QLQwvRz3l6Q5GKEbv00zQujllQup4dn57hueg2O/9yh89743ihJS8yrZZ4wUUcSsCXXK2dKppUZ6OTkLGNjW7Q8UdkIhPbrngy52vm55yXse8TO5QkGxDmSESeghYHKA0Iki1oNOL9czvoR6yRigFnUcj6/YAYd88yFYukHuF7SvqPPIsG/ojDtTX3c+r4LE+0YA3FGifsvkdFNghxxQfVw8+y67XmK3IdK91EZtXLC4nH7vyFqjFqC6TacHzivoWY19s+d//QG4+bn1gWcVt3P1mm4Tu8j0PHXpjEnR0U77Q8Q9O0xz5b7PO+Kg4qkxdm7p+xD7WQ9slQBm/sP4elc3heDuHSbavaWXW+xB4A5KB0YQVRSdKQybslEoFoaRTnRApx22y+nl3NkzOd0mITVFLxnOqcMtKEOnG98ZZhyLMeL06EJtN+jADoqK6od4BYc6j6KboDLZ5HQnzfRyE124O896afftcJ7P6KC9vb1uz3I0Yql3uSn4MsTGjyjX+iHPD7IKqEgepzsgC6Do5XKTbpaOQ8+gV0jNd6Ano9AIzDDduVb1tya0duxkKt3FZLq14IMehOGOGlHhdpOQmuoBqnqjmunlQT0x6TAU36d6/S3DUPPA5GDqRk3rm36HgI9pBUr4fA2F0suNlD5vdmFig+vMYc1qUyIbutZFmzle00104UyhTDer6daNw51MbI2etwugnEfmUNewTDf9Hn0J2ncI86c4bJcdjB7MnW5xFnky3QX2XDq3AjPdWuB4F9YuB9V0k3FbKBNVsK+7yl7yTLc2X3Rnk/q/6wG+ERJSQ9bCKxuduX33q5LSrIHicbxnMHcEKLBOJT+EptqEOst11feuAfe+83otqulzXvO49+fJBzqsG9yH+J7DA+cVCorx7L/eDo7mPB8TnV3EM90cYfTyj978omAePLNyqzfIFLaO6J6plhnHJCyDuyudbhlwiEcj6oyeJAX12mUQwyRwyVuMCbCzha+rdN9ON4gdxEbTxwjZfI2TA5xufZzKL9P4nwdWiPV01b1vFNVJQ9cXUbautucWo6Wh2CJoo5H9b7oXrrvj0ZbQy3qGzALDJaKLvWyjxsoMopfTeW37dFvsUeBUbeGAeWpvA+jlqx8DuOsTzvczjnaj6EZ6eXZEhBR4WwyMpIs2XgWE1Him26UTe2PNtGG/O/Yk5B+9StVMckdKZHR4pjYsWoygw3/SAeYNWj/EKzjUuVNTlHq5ojB6hdSI2sijywnI+Ax5bjT09TEnin/VDZ2trwBskDXnNc1utjas9h036CDHowC4YcaDBByklIuHOjeA6W/1Ni6EhlgWaiLsNfvbfJnunZ2d7DCiA91g5L9+J8CKf7jrhupR9SAMp/ERLXuX1nRHAg/qYHq507982oqbPfX43KEdTjEhNRIwCmLS0Jpp3gegeaYTMdfb7/EoepiwYQHwDEgYvZwbd9xZQOOWhPl0A3BWQm9N5N0LhCHIa7oL0agpgEdCauIGtOdyY7gCmiOKNP5q0WrYipoWxdLLo+598YwLr12mffiAyCY4vf12gKeuEcErzi4S+z1nXRVSdU8XaNGnz7Fd2DlBD9jRV9wncK4EOt3yeYf1POWIoLU51NhFK1vh+/94A5ZvlUHKYuaLHsiirhumM2sEWobxgAk6mfr5LF5aPoUHMvn+ElTTjWiMA1weux9y25aqx9DJ4EyviBJSk9lIk8Ogl6FwNhKfM6mRy3Rzp1sPGpgcR129PDDTLSNcU3uWAbx2u3qcj31TXO5nFMwLyuqufhTg+V/LP5ruBnP0e+fnWIVO98sbO+EPi9cbadS6cjnvKEPjaXqO+DnKXu+F3ylnj9sCw/073SB2UJ27HoBqnOJmf330ci2YVcG+y4O727tTBeeLX2DOHzzn45TcuRLg2Z+rvZTON/GcTG/hcdGBdg7tz2E13qaAX5HoSEVCz+hhQ2lKWNvbaoZ1uqsc/BAQWVHuXJLTrVNn/vhOt7Z0v9NdqmghenkF4hJE+0W09g7BSxu9FG7KUPH6Hl7TbVJ9pJ9nRtrhmsSvoXbJz1RrM+4sODXdRWa6BXVcjsOk+Wane0Qz3e4BYzJ09Ey2TmEkJ7pBi4rihlUX4VHKTT4htcGBPm+dGKcMc/zmTPd7pJZTttZX+87GCamB8j1LBacgBmW69U2aDGCizKbI0NFEWpoi2mc+0AHDw15DqrtbGsfoWAZF0bGm8K8fBbjtA44yubiIWeZgBP2+woOrEExrhMRrKEtA60xXL8eD/32xRXDY0qsBfneO7zU9Nd04X2Jx96A27Qtk0CFVdN9Tne83v+R9DnfATFTSIpBjDnMp9HIupMbvkRtMiKkGp1tfp3ledxkmpIb3S2uG6OXicc1R4sZwBRndnz++Cn704Er4l+ueNf6+azDtcx5rYnlFL6d1SD3uORLxKNyf/Bq8s/16gMe/K9YC1wMQ+1MpmW6dXq7ftz7HKmBGlJvpFm+bySk2DbFr+BqqhSH4Uud3HRE02dbssptegN8+vQ6+fMdSh/VChi2fLzqdXHe66ycFn9GedkflzRddU4TXWqvnGJxuzpChALKp48SlsafhvxN/hNl/ucDozMYhAxGaA1gCFjRfdMFFrNs1PZfvwRVnutMhTrepdMWbuTQF/Wh+RSAHX9v6WYC7/kPtj1zlfXJdxEwv5/eKcwqFOv8pnW4MilOHEb1Ujs+VVnMZQbF456+eg2/evRwWrfTrDLiaKtzpTnro+kGZ7vqotnf++f3OrbC9Pdvf5c9045rhrDJ9D0GHm5zuAS144wtOpEZE3PjVTV1mweCQ9pVpQ6kl34/eseQ9AI/8N8DzNzjPZ7ZubbbPHZeg+n99LxWBq4DynxFio+0cMvsCBFPAzq3ptvRyiz0I/BAQk5k73SYasL6ophwcTEPXsxIVZBd0RVfqu03Y3OlskDNaXFVaXkvn1sJ4nQWsL5wZZ/cno6K8htwjpIbjElYDxDdfEjvSD64RynRjhJhvmDp1TTwWJKQmn6uccoOQWgPwejnnHjw1dcpZqPUadlwBVYeYV0VkuiuIpPNN1kTVxHEj8TNiPuiZ7lRAppuc7uEIPj8inL1Mn/cAHh4k0TjmdOsHOD+0yUAmhVl9vkhqv/O78pXuwwRagqLF1HuZnO20ZID4KX0xeGfs6VBjPEcGPwXzFJPG4AisecL5OvtE1o6uQHuSMrILxHggoIpyUX26qdVRGnsuu3NMdxiaYtr8G3T6uXuuQQUjmPFiotzTnMEauoYpTFti12S6X5MGH4o/mQJ6tAebMt0YnDOJWxKahndCPMLGYesrnrki9ifaX0VNd0AmRTcEKcuJbX3CaNMVZrpxDQWxjFRNt9w7uOOAzoQbuPKX9IwHrmPQ7nmPN7f1eM+cILV7k6p0Q0hLxxGo6dYFFIUQHgPut9l8xLN2PBRpken2iyERDoxsCnVmm4G9H9otpn7UOEamTiEmh4Gf4xW29ewIy3QbhLF0dlFQphtV4BdE2FqXCuwkNIaI5BhbRHw1sPR0QTQMihMzQk8Y8Pmx/pmyywf5vF7X7g+6moRMKdON/bdx/QVluhtAs8u2LxV7Cw+uZkkjhGd0xS/4nqG9Djrc5Ijqv+vcoN1g+fsuL5/Uy51Me6oukEbni++MluNEbR0xcYbrkiduasjpDmN06ll+0UUhIPjH96sK9txBNs36DHausTROC16NFVinu8rBo2oDKazpJoVh5hzxA0h3lFBETSkR94VH/yqIdOk0YU/9dTYHW6TTPZv1yeWqsSqLZzAAJ8UHfY7fMDMAu4Po5Xpk/KeHA1wjs9s1LW7WRT/oRyjT3aNtPpR5Cq3pDqKX6y3DElGo54fXYJc4HH/wgFtrHVEtoOSYq3qn1mADEMcvKNOtH/CdrDa1TAPQRC/nhwzVi/no5QFCalTP3R9pUs5gnjvF4tzuL1zTbQq0UPYybFxQXbYCnYRP/vFluOCnTxlrl5WQmuFAp+CFqa0YGT4eA5BekzsmSi+ClHQbzPsCUgJf/aPz/dwzg6nFvgN+qCJqeWCmmwxjzqJhmW4yhLCGm+YToZHonYTBLo9wDWJ4kKm6B6lRc2MHjT+se6Weu7qj5HG6y890HzjNbWu4cofXYEInnAJ6XLCHYlQOXTgfuOdO73ze+0DdBA9F2cl0pw2Z7gLq5UgBJXCGFs09DFZUyBjBNXDpdc/Cgf/9INz8rH+PorVFewfOCwru4T3SWiIWCTcSWyLMARnq84wJoqeP3ZPo624IRpjOE5Xp1uZDNuNliAyPTKZb76nN92TuJHFqLOmo6DXd4vcxf4tN7syqccPyJWTRmPpRm5TbxRsWyHRvX+p2AygDPCNPav5hLaDc/tteUU/dicJ+56dGX3cfkMywbm4H8G40QfeqB7cxaKFo1LoIrNaibdtrUA62Mdo0ro9S6OU413DvCazpzvv3CN95lzJkun3t8wz08qA+5/oYlul0o8ZHJ0so6fXIJjtWZXTl74L2XeymkAQ+/yK+dVuTG3DXEZ8rPFCvryPRpaWIMhf8vsxxYQRL4KWmBHXPBnq5bRlmsUeBG5k5bqRw54hn3lImpzuAXq4b0xUYOnoWl28WuIFjFBM3malNteZMd4jTPTHKrlseMj56ebpABBCjvt2MBjz7hOCMrk84q7yWYXp7Fj2Kjoc1bdrkVOr0cqKL6xldNIbqOb081QV3vOjNNkR4qyNu0Hr6K3f6N+igcaE2LoSB8saFG6po/OuZXS6SF5TppvHhSrsIUi7vj9QpAz+vRX5z5HR76OWaMauvIwxyURcAfVx8QnzlZbtxHB5cvh3e2tEH9xjEjlQHAHZwoWGHIEdBb7VGQApxI6feS+Odfxb5NDEj6rVyBG3fePEm9/s5pwRTRfUDvAzFWF2I0BRFN9LLGaXPJOLINQA8SHXBUDYbzIwI6rvMjR0KbgVR+jiLooKMCx+bDVrmEoWOaFk1sIytElJLZ4xq+ISWfi0z1DTds6fjmOd4kKZQfSGxINBYpGAOP2/otUjZGs+6MjUAkAHw2uZukSV6XKPGYjCCHAaTOBbuyRQI1PcWPK9agDvdvb4szdb2TqYYjCUaBgEjU2aWMpf6PlSojrVI6MEBElUzsY88TrdyMLNKR0XXXRF/E8v5HBx+/o0jhgCdLab5EtThwLSOdAptBe2OcK6UQi/n2X/+VT+j0TnfJ9LmGxdPy0yliyDv0cTS0x1GtPsoeKWzz/T50qWt4zKyuTx4ogupedo0JmOuoN7AcHBNN2foBYx7pr/TvVcUjjOxhkz08qB9aIScbn3dBLX3MpY6pR19EWLx6awrPLOwlNJ9sV6f012b7fczOjEzHtYpBM/zoMC4Pk56qWqRSGXdPaGvWHo5Ky8dS7BOd7WhZxtEX/gNzG190G90Dmlq1Kquh7WO0B0CQbmpD1CI1Rdc+fQSPbrl6elK7XrG13kEezyZblUv5j/QJ8QGvIZtPm+gl7M+0yYnQO/p+IG/BGd09c28THp5u9bzUz9YhAKwNGJ0p7tfb41lcrr54bXkVzAv6XWCo1nNiTJR0vRDOyzTTZnLMPGxIsAziRiM8dF5mUgeZVVofMiJCmzrQ0431Kn7jejiPIotElLTrX/mqDJNNOpCDIAymRH8sHpts/816HDizoCedXFpXN51hO1c0vmY7/74OvUxI0z0cjzQd8jszZfekm2RihSLKqOOTp8b+hoqRrzG1K6QUB8rnOnOpPrD6eW49y79ixPYQ1AWStXc6fXLAyNS081LMzzZM40twp1q1TKMCWOZanTr0o5j+FLyWHnNvb6MRJaCESJ7GaCOi2JjKx9wzx6cU7jHiNdkBh7NMdVOivUwLgH4eX/pDje7pzvF5Czo60h10si4nTR0pxvn1LgIz87jmHjnyvaOLu9nb1obpvNEtf8p5HSXV4uqz+lnVnv3LFfcKeLJbHrVy0OCNJx2r5zutD/TXSdL4kz1qPo+TTCd5zSeQSUcRQLP2VeY/owpMO6nl3t1NChIo4ud4s8e3RVyulmmNEr0cnKKisp0I708QKVbnx+mjjUlOt3tjA5PoPIuXVOlsSahkjBBme7anDbHE/W+BEWGq5dj73PTuJgy3YpePujdn2kMyWYus6b7mVXmdeP+HC7qyc9bU0337AizxzrX+/bcWpXpZnaufj++THeDeR8S+hPpEUm8pdhlrtjeq1p0htHLdYG5sQLrdFcb2t+C2MP/BfN33C82C88i5QsONyKKGqOBQgaMbvDj84herkex9I2nAkESPbo1xIzlLVJdd+Z4l1oubqPITPf4CFvAWAuY6vLQy7No6NG9BNV083ufd7Z3/PT7JiNZKYSWl7kkpzpI6ZKccmxjhLXrOr3cqYsy06iFkJpWG3XBPz/q+TmWZUJH4sWnGJxubYPGTF7QuNCGjUY2olwqknaQcPE3vY6XnEdsSyOHSBhGpnpVXi/Wl691M/uaMaecS0EvrzevDX0dYb9YVc4R0kqtAmYEN/iWb/UffqY2aXrWxc1GedcRitwkI2ycU10+zYEIzXvKcJvo5dTLE9sQNsn63GJEWkw/l9Hn3ux0G+jlzNAJq12uN2W6NUNKMYyC6OUoWvm3KwCe/anzM827oJo7biyKlmz+eyrd6fa+Bg/W8gBMDVMvV4YQU8Mn1A45Ru/W6HTngSG/g5njOhpBbIcbTgP4878CbH7B+RnPIjy/5Gsq0BzjfcvLKHd64PVtnqy/btDxzBR3GCgzhfNF0csNNd3NHnp5L6Q0WuyOnXLfoPEwZZiM9HKq6R7wZvhHKNOdluwNpACjT71p5yC0MjE1kzCWuCxWj6q3ceRo5gwAWT9LOi6e36tMt2FtBNLLA+q/uRNV5hpatqXbs8f0Bma6zXXu4m+0EjECdpCog7RPcLNbCvWJW0N7xuR083vlQp3iAiYURy+vQBx3c+dAYPLAm+nWne6YKqWjvAjvWINI8paeiEgM2nq9tlKOzljSLTKdMfq9YhcN0/Nwn6K9ijrWlGm7vCwz3eNkG0Zfe68CQmphTjeWRO0fYW27erfC0JD3Omvzg+6eK+ZMpPA6wvmizi0euGJjrthH5SXeNBMO/v1mud+H0MtpXlin22J0MesEyMdroTbTBdC+0rNIm2izokwBV0clB4k7Sh992CvSois66ptWmWrUnPZJLZ64U0zqnrqiMs90u1kXLSONG1xUOzj62jxGpedg82QvudPNnJh3/lb+oTQA0r1O7RyBoqTqQC+vNZYevX1+nYzeSnT0O7+f1CQ/Q3Zw4yYlsi6BTncUGngUHf82tdXsdIfRy/n3/3qr8zUo000HVW3ziLWvQeyU46Dehoy7aEQFI0RPZTk2wuk29CAWlx6VvTBzMde414z3WIYyug2sP263pogrHeeZxwKc/d8Ap38lWBtBp3MFZWxKaA9ICrAcpgCMLupjiigjJtdrR8Fgl6C5cfveHRdyug0sAKJxYi03wUTpwxemIA31lS2rpruITLexN6qrjGqi/BHq5XxRGOzyUfpUOYLYW6hGl30+PVopANHLVYs+nWGkC1qWl3XhdZBBTjc63LSGuHp5IXp5zZAzhzdHiO7dC2ndOFI6GrxmkI0LahvwzApmJacf4Z5fHqebAmGNTPSn9H1XXze6UUyOEp49nHVF+y72VKZxNWW6PfTyfA7SxIKQaO+S5wydQcVmusnp9mXzdKe7MnbRhIaEKjfgpRqKCmwQYFROt8p0+4M0TXk2DlJDY/GadkOmW3O6TWf0QZcAnPlfAB9/Ovi5tJfQXCqjdIXvuWSL6Boj4fTyTGibRpxTHjaaLMfia1XV8Ib16dYdoZnHFE8v1/eeIsHnhm7HIFzquKaRIZMGfB3qz0n4mD+90NHT73FEs2nNdjFmugfdFmrnfcfZW1RN96B/b8FziII+Ze651Hd+v0kNxppu03zhgmG8dIlsZS6KenLUqzg/rAmz1ucp8dakMQAG/fZI3QSA4z8OcMqVZgYAHwPyEcp0unmmG7G61WsjmQK8/IweS7BOd7UhUQv52SeJb6PrnvQYgESdVZlGnq0lB4m+HnKpU7fMnS009nhGL0zgp0TQwTOhwTGYeAYtbVBdLqWmexzPLiD6Wz308jo62NCoE0q6hmgxOUpHvN+lWVMUVc9e0rjQ78uMilKEeF8pHvfs6nZP/XK7jO6SAAmigWVXOgfcaDEJQ3kz3ebruuRwJztVS8EIn5CaIdO94B1ONlf8Ict0c6+MjOARznTrUe6gfrDkYHucbi3TXSudqFQuBnnN6aGWJjGi3Quqa6ObNeD1b2QYozr36V92nC2iW+vZAzJ8aFyCahNLEE3k36tLIkpf0l9zScai2wVAy3TXafWxN10I6aGBgCCNHDeTsrtyus9if2igFnPxpwrmi17TbRoXI6WPOQvkcJj2FgrSeDPdWbPhKmrjDJkUTvPj+62aL5qTre+7ZVKGwzLdmYA91yikZnC6kymHXr4RZKY7l4EM1fxLRDw6GobAS88W74vOONpxuhTzijvdTNwQy6fKzF7S/J8/pdFoFFPgSneoSRsBBaDcTLehpls7i4YHvEbxotc3egNWJtVgU6abzqQww1j/XQkg5hmuATfDlPNlLvWsJAX4BkSWzjynEI15Zlf0t4tM6fqOAREsPWR6sxusoLMljI2GAYgzvwow/fDCWU7ldA9XZLdMaXbeY0BL2al2YAYhNZxbqBHQHyB2ik63h14uRTY9Tnck450npnHRHedZJ7IAeqFM90DFewtmd5/QtBEU7V5XJpdjQ466IFpq84WYZs9kF6jH+rqdhMRRs5z5kR2W40YaGqYyMApeXnKt41jim5kCxeSQCsHHIntbF5gvk6XNVoyQmql9Jf6eB0MR9fEcnBB90/NYZiDA6UaNGYTpfskeOfP/ASz8EcCEueaziAc10UEXvy+zpjvjD8QZbbqoocOIzXRbjDby0492vulY7aFpN1Cmm4wWhBJT0zLd9LjK0NT5I6NhrWzKUHX0ON0sWBCUfTNlo0yGsYe6hhjs9GTS1cGGmUux8VJUj20wvOabgBs6bV7cEKK/I6e7zEx3m4z2nn3QVCUoxw0dbK2hO92YfSFGADnl4h61w62lFmu6zQcHOaKKAUDjUTveL4im5ov8HYJU3ZH6ampHp4IRldd0myLpar5oKrlUL4ZGDhkuXJkZUSOdqKF8HLJxx3mMyIOGxrk2n/LSqEkgjc9/rp5KCBIWo6z45APN2Ycy2wOS4Ip6G0Omu8GX6TaLHal+sAzZjV4KWFzXANANHfzaI+lvM45ifxiSiaowG0WGflH0coOQmjB0Alq0IGqjjhGVo6PSkOl2s7ABmW59f6AgjpovPDua9wdtMiPhdJu7R+jZFE4vp36xXA2fkCCnOz/VvXStBCNqdLr9NawKh73HG4ThTrcKbCCFP16+0y0/O9oD9SyKEr3SabGyVzlm+ILo5Th/PJluvERNXLI2whTdEaaWYbTn4ng0zQCYeqhX7CjM6a5wz8U1ojJvLLik6OVFZLr1OYWoz7HPsr9V1QRjwHlSU01pmW7aLwihme7KAsDkME+RAq8YXOCB8TB6uehAmsm6HUi0mm4MODTQfCCkuj17WAIyWqbbYLvw8+boDzuOVv34AD2akaGX4zhwPPD6du9tKGaE1rJTy3TXGJxLuqZeqIdswrHHBnocVsSCGY5tEcsH0O5NmW4e9DQFimlu8C4LZdp09FnjnOa17f4SMENNt9hz5XwylWjkuoTYaRbPItnNIJvq9bxePaeXI5T9yuw0CsTwQJ5yutkeTmOJY0LsxXLp5dqROVmOT1i7U9f+t063xWiDbajkgGDUtEllutmhFJTp5s4Cbnoqy8mVc0cu000HCS02brgSRVR3BOgg8/TpNmxGTTyKLl58wJMBUxQuci5NAlC8pRiHHrQQf5fy/q7MDZpUP/ebVG+sfW83ON388G7rS7GWNproRjQDsYhZ3Rc3eQxeuMEIOS5kzFIdmS4qRsDn02bO5wuNQ22lmW7vDq2ro1K/Tt0Z4GJHNN/G6U63rFsehjgMRZwDNiINenTQcQ6qPqFUxzR+P//8NwavGv2ZB7S8aG5NmFcRvVx3KPWsrqk3KhnF1OUgKMA1odY/V7Z2e9d/IqcFI1QUfdBvwPF1VMgRrcAwJkOf7HwcI13t3lzTzSh9ctzMmW5JEU1Iet1gp29+RunzFjXdBidKd4waQujl6EhiMGsEnG5Onb73ta2e+RPUOo6St0iNDcx0pwcglnGueUeuRe0FkbTrYEYgB3H5HK+QmsHpRubWF98EOPET8vlh9HI2xnyfKhJ07lAwzlfTHSBMSVlKZIwE0cvReZjAO2mIfsI9SqhQ3JqiCxvo5TRv6YxGWuyVrwJc8bikihaggOq/KwE88GQSMApSmqYxwKD6IpntNGW667Pss+xrU2sIx9kTrAir6eZBHA5TQEfpizRV5kQpp9t5Dwx0cpZeWEAPgQGa3gB6OaLe53R3efZ1RS+nOW/KSNK++8G/AlwidSOC2hHSfFEBnDKdbnlPh85s9oniidtQ60gPjEunW5aMmUp6yC4bgFrIJh0ne6jXCfLNGFfrDUaoVmqGOUD3TvtsYEZ8ZDLdgtUg91zKdPPAFd+TG1jAziOkFrAvI+ojzlxIQY1KbKzetM2TcHDZrk0h7EVqXcnaM6qzaMAQjKhx7b1yhdTkx/Wttx2ixorDPY/89HL9vK12WKe7CpFn9F46pzGDrJwFqlUNy3Rz6nRQPS8tukkHmgU7igQeVCQmopzuEujlg54+3f4oeoOsFxuOkqBKv3LM6jjNWjkLhgzTUMCBroIWnSOe6d4phdTQqdZVyfnvyWBT9yufS5luPSvjuUYD0PnAjIVLL5cHtIm2qUTF2JwK6umtnO6WCmu6s56MiX6gu1Qkc5AGD/yeAHp5VBrqGYhBKurcd1TWKgvjry7hBiPIwBk/xz//dSGXoHklhI/kXEcaVwX0ct3pDlLS9dZ0S6qj1jLM146Ei6hJ/PDul81Ot6+Obsh732jQEe2vUCYKWyfRuizD0KG5QIEpXPc8w+uhgAYoxoYJqdVIemdv3A106pluTzkCGYF8T9CDCcQUURoAXK2830/pK9Pp1utPv3m32xM4SFAvyTPdAcFQ0v4YyiegK1urzhtFJ2eChc6Lsppufi+0nqYc4monIGp2Ib1cy3Tj58+DNEFOt6KXezLdJn0R776bk/cwW5YQqT1Xz3Tz++EBPRw31S7KUI+qZ/PKbRnGAk/K2OX0ckNAz/nZee4/mR6JPnaImiynl7e553ks6uy7RWW6e8vPdJd5RpPdQk63TjE3la4gG41nL+lM14XUELruCn723ppuLdNtYsfwdnvUhcVUo8vHiDQCKqSXHzg1wOkmITXNqVa2i3y+Pp/4NQ3mkzCcdF4/K+vdpzbXigCrGhcfvdyQ6aax8DzPkOnGIHJQt5IS91vKdPs1IzK+NULlYGiykmisyeluiDrzIg0JtUfeuXiFZx42Kl0nuYcqQb1WQ+tK5nSbgjk0BnimqT253JZhztdZUixZP5sowOuhl7PA+FiCdbqrEdLQz7PsK6olKueSR/b0zVdt0NrBZRLeoL+hhYsHFxcUKxK9klrOI4DF0MuJGotR1TAhNVJs7EtIukx6QL0+ipC5Gd16zdjtL0xdU1kXQ627olGXd6BzhU+9/zaCNmCdlkZZF6Kf69kHfm94cL0669+cy4wkVHRZ9MzUnW5TBok2YT6nAoM0I1XT7Xx2M8c71+VrF1IoSMOyC7rTTcZXOh+HQXAO2Kj8PPH10Elt0IM0RufIEEWn52O9Mo2FmjcRl6Y+AvRy089h9HK3ZVhAL12DUcpFfjBzWZNnZRomqp7eUoygHC6e6aYDPRmsbl4CvZxYCoiuInrpmhRjTVkXYkb0RKUzMNTni7zHyYATLcMMWX1l1NUDnHsVwMyjQwzoQVfUh/aeEajpRtzNertTBwBdF4GE1Dzq5XrmUl7PIObh0FKU16no5Nz4o/aVphZQdN96ANjUMkwxtFoYI6f0s4gCCc1yT8WAMFenJuczqNUR7s9B9HKTE5WT9HJckx87dT83000sEZr74mKGgtloQY4UBf8ap46IeCU6jyZjN0hs0BR4OH0+U5hHZNIQJzowor8NhoYz6vXwn+t0jw+p6TaUgCFMAR2fkFp5ZzQ5zC31SRWo5NRqRcvXnEcaF3Qu6EzXW4bxntR5am2Wcp1u3M9qInqm28Cm4u32CLQH495kUrtXTnd5mW46a2ZPqPckCPRx8amXK3p5SKZbroMhdK1jzjzIy7MXxxAdUjfTnSwt070La7rpc8ZkAA/qcZgCdnyvoWSByemulXMB912ysWifpeBQQ0TTMtBbx+F9057BO0EY6eUm2n2ZNp0chilSGwGZM7w8znTWWHq5xZ4DnTIujIiEn7omvteMHeVEsegfr+/gwht6jW6Zi44OEdxoyFAZKoJezttjhQmp1ci+jn1xUhvvV4sY2225tcvkRJky3QFRdBPVUXe6yzzQufNImRTudFP00l9fKJ1uKcSmU7i8FK0auLf5/eJ7NHzikHGo5cgA0IMRqlYyY6CX6063oad3dmRqummTnTkuwOkOchjk3KBghKmmm+4NR2Ig7xwAUUmDRaMK56hqtUYGjklhmNYTHeLie2b0kFHE5xWxA3RxmyKhO9nv+fVij3q3qhfzCKm59HLRAixoHcm1viM/DrpmOiJo9XSA8wxdMZlu3Sg2tSMhBxydVBP1uEjww5oMHd7nFmHKZLviNTkV/DIbOs586YlSxmwIvnDr857nJLKc+szuhYxduu9D3wVw6ufdP1RlLtyAZsr5QW22igDOCz0jT+tJvGWAMrmb6WaBTn1c5H4ndhN8jvy8l7zpll94aI4eHY2QLK1vzzVoS+DZV4F6uZ7p1imgQdoh1Oqosz+tjEVTRreOnCStzh0NyH85cmZwTTdfE6bSlSAKKOlFEFMA5wprqVgs3H0h5glIFVKj1s+mD5042z8uOoU5n4W5b96gnFX8F0gvNwmpBWa6h4Kd7jID43Qe4+fv1q9nCgcj5HxBJ4vqfBuSwU73YO1UX6YbExRuRpeE1Ay6Iar0wpBsQZYVXyeqNG58RfRyCkbMkQwObH/KGcNBzAhKGFC7VGOmWzI+xP4i2Yt5KZyGmXIca5/AXFifbn5G8+fR/kxj4nG6y8h001ypjQulcWJpcpgCdmj3Yokggj57Uxkl7R3IMKJ5rfZZiSbQhNT0NrD0lSu1B+0tZO/gmJgEH0vYW4bzEY82gr6/GOnlck1Z9XKLPYZeHpFOtxDkqIn5D3TxvWbwmqJ/nMrIhTfIaeJOdxmbEVeSJmOG11ybRBQQlP3FKB7RxU2GMdFeu6PuQULPR9qpr6bbtMEE1YuZnG41LuMqiv5xFW4u1ONToy6gAGqil5M42CDUwOZ+JvICQzKbgc6lNl8UbTNdBL3c0NPbJ6RWWaZ73mTnPd/c1msUrwnKdFMwAg90X5ZO3hvSyzEggYjJzxNrxEVgyBeMCKnT9ay1pDuG5IByBgUZOmX2ddedbjzMXmf9ugfTuUB6OT6XB7qCxgUNnXS01pfp9rTdSxSo6dYDNGE1l+ikmoIaRYICMMlYRAVY9HFSFFBDn26eJTDRy6mmshua/IKVvlp3JqQGeTcTS+uJZzXFHxoofcpQrDNn+8oUOuIGsnibgHptt6Y7RL2c2CIQd54jqYecUt6olzoZawYNgasgJlKKOaIjoF6OxjtVp3Cxo6BSAwp0trKexKYsb63cO9I1zlqPybIkfD08p32BcaQDY4mFuB8KXlFGVzuLaJw8mW5W/00oY77w+yYH0qRerjMAdAfbWOok53c6H3NEoABgny0PeOjlak3ReWuq6VZ7qQyAEUzZyRGq6XZ7bGMrNRnALEAvR9QnqMVc1nXc9Ux3zmUP9dc452mmv1M5I0hR9gmp6R0ycP8zndE8qWJacxXSy8k2mTXBeR+0t7hWoysYZqaXt/ZQwiAe4nTHIBNx9tOIPGNwftV4Mt0J8xzA4Dox9kw13R4BUO50l08v5/3YVVA3oCUh3ztQC4LWDQkOm3QRauU9I3sxJ88OWjcfOGG2yLCrM5vmiU4v58K4XIjW1HlFjQtzuiuk3U9sTIoYrDMWmVD2Ihd01HVaqhlV43T/4Ac/gOOOOw6amppgypQpcOmll8LKlSs9z0mlUvDpT38aJk6cCI2NjfCud70LduxwekKOKUhnLyoOobyIlInNSKeuie+1TDdvb8NhzEQwY5LoT2UcXtTrkjvdPAvj1g6anW6e/fU53fk8JGRNZVeUZbrl609uSipDSG0sRnp5AHXN1MvZl+kurx0Jp/o2Ge7VpHRpynQb6eXDzvX252udntSyFzKWIFDdnq8cgQ4wPKxokyuFXq63DKuwZ+zJ8yaKsdnSNajUbsXv5WerjwsZPkTRMo4Lcxj6cs5BHZc13fhe6KT6Di7lFA4bDiSNMaLXRvFgDjkLulBWkTApc3OKlqLGsvvmRjAZBWH0coyip2WtO1Flf/q+I91ABBoldFjrBkohermHKcDr6Cqnl3sy3WycOH3YQy9n+wjtT8aAnjR0xFyRn7Wi8OGtQQYSat9lCtP8fkwBGvGGARoA9LsgMaQSjGJ09K5+x2Hiex50CaSXe9TLA+jlZBTn42LvzsuzQwV9Te0rw/rA6uNimg+eTLdB8LFIcKYHr+s3/d5EL2/tTSln0WQYU3BqsH6mcyuDHWpN4ppzS3oMgXHaPzNBQZp6/xgOGpzuMuYLv28jvZwy3RqrSney68JKnaAGfjzpe+L7iNwLsKUUrss4aUqojK6pHIH20sYSMt0uQ6Uc8OwlOVIDRQRp3OcyermcQ+41up9TT9I5T1O9bm08arkECqmhHYdntHiNvH/fxbNcnvnGNUdMtTLp5SQYhnsulWr0DhduGUY2Ha05X/kXDwDn465Oj/w80Wn30Mt9/ctpz2X3bKrp5s/hwb8KziKXFRE3dgDgjmaDFmygM5tYWkZRT8p044kjVd1JPPk7b18AdbEcJGgd0dnio5cXslu4080z3eUHOilIRUmQelYG6O+mwZxu1gWAa0BVO6rG6X7yySeFQ71kyRJ45JFHYHh4GM4//3zo73cnyRe+8AW499574Y477hDP37p1K7zzne+EMYc6x9mL5LOipgMNAFy0PpEW8T2Ji2mZbl82ynRw8UiXQSCoSPD2TSanmxxk3QD0RYZNzsLwIETkobMTyOkeUIsU6eV+9fIwIbXGwlRHXUitzIyuS6s308tNNboIctDbQ2u63Uy3cMbkpooOg6jpRmOHqJCqppsdgLlsePbSpIo5Yplu573HNyThqNlOxuj5tTsL1l2qg4ucblO9mMw+Yka3J5v0tMIS9PJE1G21RgeRmvu8XoytDQ6dMsyDOdHya1GDelDzaLFpvnBjkKLoCJ/DID8rDEakpKp7rRyHaS21igZpzBrQnhJELw9VjK2riEbN2SJkwPFx4vsMH4t4LKoyuN2D1L7GP1/I6E3l3RrrJpbp5mwAD72c3y85Udwhp+eLmzA43aI+vPz6QnIW0MCZLhV/uRMVSC+npGsur/aiMFYEIi9b75HDiZkMX/tKXuZE9GdO6+TQgw04jmQgezLdmQqdS7/THUQXpv2ZMnREH9ZBmcu+OofunRwip1tmuk1sNJVFSvtpnRymVpeU6caOJjT3yhBT4xlbVUtpbBmm13RrzoMh+0/zux9qoT3rzIdYbsiT6S6YuQwVUjOVr4xsn26kl7v9tzOGmu7Cwnv0mAJzhrtijhM83L9T1S7j5+ATUqP9F2njuC54ptqXSDEEaWhciNlYJr2cHCbMVJNoWO9wxNAyTKOXazad2el26eXDMtMdzaZUhhjni19ILYDR6VtrCTeBFJrpLn/PdZxuv2OJ2dogoUZy0umMNgaA887+kMonIR1zzg60/6c214jzrCk27P/sdUZiILvI0NGHC6lVUNJDa6hBrp96JRrsp5dz+57vNSnJ4hsLqBqn+8EHH4TLLrsMFixYAEcccQTcfPPNsHHjRnjppZfE77u7u+HGG2+En/zkJ3D22WfDMcccAzfddBM899xzwlEfU4jXQTbiTFyshaI6LOVEFVXTHeR0GzZoPOyJNllGbRSnl1P/QR65UtRxXVE5EVMUQIKP6sg2iY58k49eLtSoVXahIXiDUcYuq8sN7Bmb8jqXmLkkJ7UECBEiGd3jQj36ga5v0LRpkXiJWb3cOUyxblmMtbwvzG6LtjCidllzpMiY5Vmk4QBHSgm6yHHD+6cMrmoZVrmoD9Wh8uxlKkC8hpwqeq7+ew+9PB9zM93S6UbnojmRdVutqX7UhrmvMrUFIsbcUKzQ6ab7+uYlTtsNBBl0eGhxxX6upKsOdDaGvl66igGQUE43CcrhGnIDV4a6QV9Nt7a3hNLzGb28jL2F6OVBmW5yJkzZKJofar6EZLoFW0Qa8ORQfuas/aGG9hY05IRqe9wdIwrUBWV0Fb3cVNPNsuYVZBcwM0TGiyfTHVDSQ5luzgBI6B0jNKc7J8WOKOh7xWlz/Z00+Dqh8eDnC4duQCvB0AhATUtlNd1Mvd8V6uEtoMz1/U0avZyyNTpoPnQknMxzjXS6RTY3Gg3QXdGCekFBGnIgee9vzgAwZYeLBBcTdDPdnF5eXE23MdCp1KhroCfj/D6mMt1OTbevdlm3W0qu6U5rNd0VZrprEq44WhH08jpJL0eWFgWifA6mXPepfAL6o85ekBnodlmBUXAzlxRQ4WJp+PecucjpwoXYixXQy/GsUWUayZhyprhflCpALye01Bno5TmXXp6WTndCCvGh3YL2rl9ITStH4MFL3gdctN7TBUBHpqZb0cuRFRGgi0AEQr00RdHLJUffdBbRdWKmOxVxzhjcZ2mMG6W6eR73ScUYadCCEQHJAmPnFab+XsGeq6v315M2AgteuUEcd1zQAVclQBpjoJpRNU63DnSyERMmOBE7dL4x+33uueeq5xx00EEwe/ZsWLx4MYwpRCIwLCNdqPpJTpQyAENrugOcbqOqo6GmowIhNTxIyIAz08u9xh3WuvBNGg0lfMwDSTlD57JbOlGtHTsV7RaNpTqig6qa7jClRj1zaWiVQJsRF7opYzPiAnJk1HlqugMy3WTwdUqjOKyODrMLwnlPupu0ElLTqY480033Q5twUJCGNvER6rusZ5t4r3b39xVkupnD0JNz7jeRc8YKx2Vcgjk4KtNtmPtK8TTo8BowON3yesoI0PD7OnX+JDh1/0mew4of7joFlMalR86teCTvX0cs0439UREkKIcMFTVXuEOtB+oUNVoLXHHlU11cTBzo5Wd0qfVVMtDpduYSZhkxG8BB6ybc6XZ+N5BFp7tRUacPnNoEJ82b6A100pjqQpeqplvPdBtEkTyGDiv3KBG9Q8PKAKTgAs90u8EK7zzAIaLHlJJuAL08Dc74ZeXni8KM/37KvjCjpdavi+CpMdVonUEZXfq9UvPGlkjRisbFQy9Xwlh+ermvplujBptE1MTryvr+bRFHGKsuLZ1uzHRzerkn+6YFnbiyv+dN3bZ1CqQPgb8zZcKLBBeQU0Eaj8NAzmWBmm7TuMgzBM+inUMxT6abasjjkDVnLtX5MuzuM4FMmpHPdPcyh0E5C0Z6uTnTTWVRE+qTvv2H1gGy0QbA+eyyMqAikgUxdk7QuOAZos7e/uDzGaE7lyOkXs7p9Y4T7NwX1210gzRmejkhlF6OmW5U6malK2j71AhV9yD1ci3TrQc6+WOmTHcFZ5GXXu5X3ua1zb6yDDm3woTU6JpSkITBaL2il9Ne1Sgz3TkMWNFZpMRxh8ugl7OzSHXlGC4/0y3XRF3CG7xCW53WEWfOoI0yFhXMDWGmPR+5XA4+//nPwymnnAKHHnqoeGz79u2QTCZh3Div4ufUqVPF70wYGhoS/wg9Pc6Gh847/ttTgdeWjdVDbaZbON0DsYiIitKBnokmIC+vPxpNCtMolx6A7PAwxIcHMA4Gw9g+it1jJJIQk4Geh4ilB0RUJhtNQjSWFH+XSfWr1y4WrT3O4h1XF4eYpIKnhjNqjNNyQUUjed+44wZGEUR02H2fy0APdi0UolhdGWdjWLfNpTzXxSPKYUDjMId/H0WBEqcNRQZ/zudF2x8xLjgKfFzi9c64pLrVuKgxjDeI1xGfSWqg5OWksv35rLhORM9gWt0jqaTGIjnPfddomScUkaLf09dcqtell6MQRUO9uGY0htHpwj2e1MtRrER8pnlw72doECBWD/F0v3Ov0aQ2X+LOuAwPOuOS6nf/Nl7vjO/woDO+JYKMvRjkgGxfHAu6t36iYLH7RtCwqIPLMF9imbSY0+hcdg47VxzLZ0XkHDPclN0VtDb8fLI5iAAahY6CqrifXBYSPNvH3iOWqBOvn0n1iDGNDnY76y/RIF5OjEsuU/K4IDWNnOZ6FB5V82VI3GPvgHPdeNZGclkYZlkqovh19jkHLp7n+rig8F5c1tENyWAE1nRjRjwZyavAFWY1aR2oPWM4JR6LDvY49xqrU89xLh7pow6GU/3CQIqk+py/jdVAPpoQf5cdHnTWZwkYTMt9LgLQKA2Xzn5nTBB9g65xr98zZe26BpzPEodJf05SUvo6UgCZeIO4ZqSXt8cikIjk1Z6bj9eqzzRe2wKR3q2Q6esQc0Dto5GE5/4i0VpnXqX71d9GUj3OuAjDKe78XTpV8rj09MsayCTO3bxaV2pvkU55POLfOxqScdF2bad8DVwXfFwi6ZQzV+R+h/sHmqo4FgNRh6JOJRp4H2rfjNWIWt7hwR6AZDPEhgedtRJxzyvx+uDsLbR/RPranZ9rx4mfY5GY83fpVMlnEe0tUcirddGXcufLIFHqtbnQmPDuueiA+c6i7DAkpPO4Mes4NXVpR0hN/Hkuq+YLnqn0mcbjdLb2ivuJZ4bkWSQuQr18FMcM/7a/w/3bgU7nbxONEIvXOt8POq9TCuiswTmN+ypiYMi1gwbUuGhndD4vAloU5NbHjc9pPIu60s6Yx3M4DnmxZ+N5RJnL4VxE3HMkmtTOl253D8E6X5PtIvchpF4nZEAmE6tz5k5W7t0lgu47Gc1DndwvelPuGY12jOmMpv15Q0e/qs/2jcugMy5ou/TmHUcwNygz3RgsY+KVw3l3LsQT9RDJpGB4oBsiaWcfzScbfPcXT9R55pV4TJTjAWSSzXKNSRuoBPQMOGsbzwY8a2i+YPxTjUvaPF90hj3u2b4zenhIrG/cX5AFIP4O3e94FPK5rKhdVuMi5wvZudm0c4ZEBnud+0vU+ccFzx9xFvU6fzvU7/xtLAl5smsyci6VgD5VpoT2eF4xivAMwvOHxg3nUTabgSzzI8lx7pLnuOksoutMQQL6ZOcVpJfT2dYQdd4/R3YuO3vz2bSzlw7RuZv03h/ZxGw+RFN98jyvgTzEnO9pPZaA7gH3LBoeHlbBq16yXbjWjM/WjTodAMRztSDkHoZifcaqdLqxtvv111+HZ555pmJxtquuusr3+MMPPwz19YbI4R6E02NS1AdSMDTQD+vXvAXHyqzMa6+vgM1b7xffz2lfBUdiK6AtG+Cf//gH/IuMgj/21GIYSryhXm965xtwPFKWW7fBs/c7f3vcpvWAlWmvr1gN84YygPHlxc88CTsbt5V0rS+twA0lCt1b1sBS4Q/HoK2jE+6X77Nlm/P7N5e/Dve3LfP8bT6NC1QaPNmM+hvC+P7VcLqkrrUN5AV3g6vovrn0FZglHalV6zbDyvvvh/qhNjgPX26wR7xeJJeBt2ONFAA88sTTwpl2x2WVMy7bNopxwTr6t8sD/dGn/wkXyec9+tD9kE5oqqoFkJb39sxTT8LmdrzHGKxYsx7uv3+tSAgODjvL89knF0Ez22/W7HCeS2jbvgXuv3+T57XXvLkUDpYMgK6ePujIDQHmRtEYfuXFF6C1A5V0nU36qSUvQm+dIzj4NohCFHLw2CMPwVC8Bd4uI5+PPf08DCVc4cJJvcvhFDxoOtvgifvvh5rhbrhQ/u6Rp5bAQqk58MA/7oU8qfMWiUGRDYnAc8886c6Nt1bD/UNvid+/ttW5//Yd2+D++7eov1snH98pnMsIDPR0++bLMZs2wD5oIEAM3lrrzmMclw1r18L0zGYVTX5c/u3E3hVwKt5rd4d4DLM0l8i/e+ixJyEr6bWIE7oGYBoALHtxMWxcl4CDt74KB4hAUAesXvQkXIBzOjPsu65CwHMpm3Pmw+InH4fOdmdcXn7tdWhpWwbtYsrHhdH8wAOOMjAhJ+fZcy++IsYH7UHUxeCY1fEiHC3p5evbHFYHUspxLix67BEVuEIdgcXy2qf0LIWT0Ajr2AFP3n8/HLR1KRwIAOu3tsIydn/R3DC8TX7/8AP3QiZWD7M7XoCjMCC3sxu6UhvhIHRU1rwFS9OljcvSbXIutG6HBlz/EINFr2+E+6LrhCO+XWx3cbHG9TEfTjnjsq0Dg60RWL9mlZpjhBM6tol9byAXg3UdKZgvdRH6e7vhxeefU3ThVCYPD8vXP2UgJ9baK889AVvf7Ifjt2wEJBsvW7EKNrS71zChbyWchkGkrlZ4TP7t3NYXAWXPtrZ1iW0P5+obr78Ga1tLG5cX2pxxGezthCXPPS3GAA0XGoNX5B6ys73VNy6OQxSBbZ04DyKw4o3lcH/H6+r3Mzqfh+OY071643Y4QpaubFi3Fga35WGiDOht2NYOS+XrXwRYowrw1GMPQV/tdDirsw1wx3z+pdeg/S3XYBnft0rs6f3dO8W4TO1+FU5E420oIubZCR2dzhp79SXYuEnLehZAW4fzmS999WUY6MU1FIHnnn8RBlY7RvLK9c662rTB2YcJTlLGNZcGe/17Szw7ABfL71/cAXAFzpVhzHTnYf3aVbCo7y1okuOyfPUGWNfj/P3pgznAHPaLzy2CHcu74ZL0oNjdn3jqORisWaVef96ObYAphq1r3oCX5Xuf17kN0FJ57uU34IjBDGDB0z+ffRLamp0Me7FYt9G57zWrVoITr4vBqnUb4P7714nfr9/k/H7D2tVwf8q9JkQigqrkzhn9xtJXIbEF9xkXszsWi7WOZ1Eb3n4NvlJOBCg2rl8L/VtzkJQ06kefeEqcozM63xBzbGfrFnHu1gx3ifMFqbP3P/iIhzI8pdu7D0VzabXfLFryMgjuY8ad+6UgNUxn9CLo2OGMwSvLlsOknc56aN8p59PLL8HQWlfUcvsW57mrtmKNdgTyKcfW4KBzBWt0N3Y4iYmBTkeYdKCrHdr73C4X9z/0qLrn87KoOwLw3KJHhIjsySIQkBXnMMfp/WlnXi1+Gna84QThL0r1iTX43MuvizWGejilntE7BumsyYmzpnunc684b+hc6eh2xuWVF56HbqZ33DXkXUcbVr0J9/e4dijihO1bxPpGevl2sQc5AT2cLziGA51uMOKhx56AbLQGDti+Qdg7m9atgtfuvx8m9skzeyinznHCOemc2NOXPL0IdjZuhUO2vCH29XWbtkFH5+twAl5n23Z4usT58sYmZ0/dtmUzPP34RnWfd933oLDftsqzCFeLPhd6O+W+s8OZL3ie6c+Zv30pYGHZUD4Jy9ZuE983RAahX9o5MVnONJCJwiL5tw2p7WL+Z1ID4jnq3O3shefZ69cPtUqb2N3XDtj+mhjTjdvbobv7LbHHb9+yCV4ocVxekGfNYM9O8dr93c69Ln7hZcisz0O3+DjjYi4+9vBDnmqAvChHicBjTz4Nq0vb6nc7BgYGxqbT/ZnPfAbuu+8+eOqpp2CffdAkcTBt2jRIp9PQ1dXlyXajejn+zoSvfe1r8MUvftGT6Z41a5YQaGtuLs2B2t0Rlf5VP1T1hRPGN8NRh82E2m3OZnTEsSfC4Qeh2wMQWdoLsOlmmDpxHCw8/yyAV53XOOfCSzy1UZFVMYD1v4QJzXWwcKHzt7Hb/oC9cmDBkcdC7Pl/ArTtgJOOPxry++J2XTx+ueZZQS676PTjRDb1dytfhvrGZli4EI9KgL+0vgTQ1QHHHHkELDxS9huVuGnz87B9k3P4NNbXwsKFZ3h+H1n3FMBbDnVtQNZFqVplADjrtJNg85pfi+/nLzgC5p240BGVeONLEM+nYeFFFzqUmtec55+38G0eSlJkTa0YF2x1IMYF6cJyDM+96G2QX/55iOSG4dyzTnd7pRaJLz7/iMgUnHfO2RBZ0Qb3bnwTxk2eBgsXHulkZJY8Jp53yUXne2hZmde2we1r3eDE/Ln7wsKFB6m5gQff/nOmA2zDKHotxJO1MGHqPgBrV4pM9+mnnAQDy3dA3UvOfDnt7PMBxu/n3O+ypKAgnXPmaQD1kyDyqmNInHPh27zzZfMkgNX/A011CWdcujcDvA6QjyXhvIveDrDs0+J5F517pr8Gr0BG98rFzsF94XnnQOvijfDEtnWwz2z3Htc+sQZgwxqYu+9sWLjQrW/u/Ocm+PuGNyEje0JOnzoJFi48xvP6sb/eDtDlOAwTZ8yBfF9CfH7oYB580JEwsz8jPl8s36B1ENkyBWD11dBYV+M8NrBTzZcLLn67W6uNr/+3OwF6XoPDD5oHhx63EKIPPQWwA2DfAw6F2cedL8YoCllYeNFF3lqzAhC1gS88LaL9l75tIbx49xvwUvtmmD3vAFh41jx4a0cvwCuLoaEW56nTZ5tww7rF0Lq9F2bPOwhg7SrAxMR5550HiYRL7Yu83Aaw0WEA1E2cgRNH0Mvx9d5+8Zmw5MXnxPNaJk13x2VDC8Caa6Cl3lkb0UeeFfc654AFMOushfxDVeN1Pq6TxikQfXGbeL8pM+bA5GmHAmz/O8yZOQ32ka9dLLY9ux5g/VswZ5+ZcOLcCfD3Dcthy0AEtrUcDFecuh8sx5Zqry2BJsPeceOmJbBtoMcRSYMsHLbgYFh4yr6e58RuvRGgB9sdJSBdN1mMC2YXpk+eCOeedTA8tuz/xPNqG8e5+2bfrQCrVsLRB+8LRx69UO2jhx5xDCw4gt3f9n0AVn0fGhKg/jb67EqALQAz5sxzav87l8AhB86Hg04qbVw6n98IsHoF7DtzGlxwzoFw9atPQzYSg4ULMewD0LFkI8DaFTBzBn6eR3j2jukTm6F9ay/0Z5z5efSRh8PCoxw1bkRkWR/Aeidzj9hn/4MA2u+GWhiGQw86QLT627z+NvG72fsfDPuc41x7fFUzQN8AnH7ycQDTDof4+m9hdAtOOOUMyO9zHPtQXwNY9V1oSEbFuIj3WwvQPBXX+0KI3XGbs8YWHAyHHl3auFy/9jmA/j44+YTj4c1n18Pa3g6Yf8jhsPAY5/6ev/cNgG2b4ZAD94eFZ+/v+dsfvbEI2vucPXOfaZNh4UIMUzH0bgdYCpDNR6CjcR4qe4rAJs6XwxccBRccNQNefe1a8dQDDz8GDpbXHuv8HcD6tXDsYQdC/tCFEH3VcUDPOvcCgCbXdom82gmw9c8wc0I9TJPzJf6Gs8+edPZFELv3HwBbNsHxRx0K+QNLG5f7//wqQHsrHHHYApGd+8emlTB52gxYuPBw8ft7dr4M0NEuxnzhSd418r1liyAlx+XUE4+D0+ZLZWyJ6AtbxVrHMxodTAKOzYKDDofZLXGArc5j515wkdBKUfZIU72zNro2iL0TadQLL6bQhhyX9U0Aa/8XWhpxjS90KPdyvznj/EsA3vyqMOgXXni+Z68u9Sxas2gtPN+2CWbtuz8sPA/dNICfvvUMWttw2iknwrFzJP0fgzeProYnt62FrrSzhg6c446luu7VSYDVTqY7Pn4GwHaApqRz3h44dxbMwLrdZQ4ThN9zfNP3Adrb4ZTjjnTskTUAjROmqj2EEOu4AWDjWjj2yAWQP1jOl6XO3DrpzAsA3vqO+P6i888x09MDIPbUV5dAs9xT7+t6Fd7oahX0cjpXrl7+JEBqCM46/VRYMMO1pTGr+a2XH1c/n3r80XDBgqne6771/8Sei6yr+vFTkSIhgpstDWibng6PtT8JIHMMF6C9Fo1DdMlagG13wuwZU2Am7hlragBWATSOm+Qbl/jmHzi27HFHQX6/MyD60NMArQD7zT8Y9p19EsDan8J4mncl4K3HVgNsXgtz950Dl1x8MHzz1cfF/R53yhkwb3IDvLKpC+C1f8K4RnxtDLe6wDFc0d0KkWQdwEAK5szaBxYudFi8hOiTrwmbDjPd++y7AKBdnkVTHDvnqZW/EedTvL7FvfaujQBvOplzcUa/uN05d6fP8t4fdqF548si4Cps4kgUok+8It5v1twDYZ8phwBs/j1Mmzyh5HHZ8tQagLVrYM5M56zBe13Z3Qr7H3woLDx+FqxHNshLzwqtoosvds4nws9WPQNd7QNw9HEnwgn7SfG/PRTElB4zTjdugJ/97GfhrrvugkWLFsF++zlOAgGF03CxP/bYY6JVGAJbiqHY2kknOc6djpqaGvFPB74ON0j3RAzLTLdT0xETxjFlXeI1DXgTzhNrnM00mh2CqBSjQCTqW9waU0StE0aKZtMQpb+VtWXi9WTNTByQI5so6XPb1OlEcedOboZt3SklIkZjTAJQtTX+cW+S/asRolWE/t6yNgypa23pBMoNQ1PUeQwP/6Y6V708VtMIMfz7ejco4wh0MHoLjgN3hhqcgzSS7nXeu08urHgtJOqanXFJDzuCJyWMSy6XV5S8upokjG9wxrdvKCvep3/YjZrjPfB6sKY6L81mXH3SNy74eSPwQB/K5iCKn6GMGNfXJkWtPAUnErVN7rULMbWUyJhC2HyRrxfJDDnvHXGYApFYjTOG9Hf4eAnjwmu3G+pqoE7WUmIUXc0XOTSonMrvu16ru6xNeH8vIMXehEp3Jg8RNDaGukXNGPZKb5b1aulIjfu3cg1Fsml5r/Iao3FI1NQZOwvEhvuduSY/h1hNA8SSbjAngTWAfDwLoE/W/QpdhERC9XUfyjrrKJWNqBpe/Z6VWqiknOMB7N/jaFyQuua2DMM1V1OThMaYpK4lGqCG/k5+zkgZFq8la8BiNU3OvXNgrRw+j+aDnFtRNDLkuETzGXfvKRLo4CCSiRhMaHTH90cPrYJPnXWAysCJlk36uMj6MapRPHBai2F/yaj50gtsz03EoKm+RtV0RxJ17t/WO8ZBLN3rmQNxnEf89akLxXDK/Vup0hvF+nHJSsIyC994FgD1zG2uS4p1JO4hk4NYLC7E9bBoAoH3od8zBlE4apPmuUJinkq9PDIk1mtjXVLRy2O1bC7IGnax54r5Immqtey8Ei/U5J1XUugwWtvszA9ZX4ilSqWOS1ruubhXHDitGZ5Z3QHLtvbAB050HEkqHfTfM8DsCfXK6W6oNdkIUnQPamBrf9Spsx7shJmRdvE51NbguMgykJom9++l8KRoXYj0DMm6Sujj0jhRtQpV60TWror9WdZ0x2l8SxkXKayHdgTVtQ9l3DPa/X3CvL+ocfGfRSDr3JGNhiJQBAzS4OfQyKab2E/x7+X5ouyRvDO2kUSt//VpH6KzKEVnSAQS/KzHM63Ms6iutgZa6mvUPuofF+99N2q1ylOa60Jtl16558YzDrNsXEMNNEhq+3C0Fur430ptiTj+Pe0XtU3+vVOuN8FcEWOYV3tRonFC2eOi9tSks3dQr208l+lcoRpd3Av4fY+jGmOJCU2Gz5N1GBmSGXg8n9Fmwec2yJdA1kMCzw6015LafBFsHYBIst4wX2i99Tv3LT+HGO7PZNfQ3lMCaE9Nxp3rxLManW605fBnQYWX5Tv6a5N2UbekWtcankMaFkOQhD4ppIZnEdaDC5uAzuhYbbDtos7deu98kXsLQrTgRb0iOYYxnEd4Tpd5Rg9Kgw21YRKJhLLpaX9Jy3GpN9zz5KZaWNs+AO39mT3eJyv2+qLVRCn/4x//CLfeeqvo1Y112vhvcNA5dFpaWuDyyy8XmesnnnhCCKv9+7//u3C4TzwRyWljC1ir5BHGSrKWYUF9uklgBQ1g3eA3im6w3rxltgxr6xsSqo1oS8wYV2duGWbo0UfgGV6juATrAYr0NURt3jmIvnfpoaJu0xWBamBjEnH/ngtp6NlH1TJMCmL1tbl9qvG5ZfYvpD654m1RNVsTgSJhLBQ10gVYdAVQ+luOiBwXHBMhpCYj2aReXh/PMZVu5jiqHrgZVxlV9GbW5ouaV6ytj3huUoodkRBOaQrmXGFZtAYxCKmpdiS6cq7v53D1ctFrVAaTcO2gmF29VADlxqFPSI23u9IRJKKFr8GzLCUqmJOoFQnPYH0UF/VxRVz8c0FXL9fFqPnnN4TOZTbB5orzPk3yQM9Q31STmB4Jgunq5cae3ly8pnxlVLfViKtGjTh8nxZvSx+DqB4Xs8F1drwpkq4E5lgwQtTROW33aM/FOjr3hYOE1IpoAcVbOlagXt5HQmpM1EfchxyvIPVyRAsLdBoF5uT95KTTnYmS8F5atZxSQmpcVC+u3W+QqI8uoqXmFQkbjoyQGokRPrO63Tc+JlE9dLoJRvVy+dlhaQo65/nxjiM/O9IqgmFYA1svtRGyMlDhOWNQ2JOLNerzxbe3ZNx9BOefqUVUkeCinW7LMIN6eYF1FCak5iQKIq7afUTOlygXDEsGnC9hwlia4rSpM0IZIrC0RhB4nWHdEfT9RZ8fyJTzv8Ggq+qec+43KZ1up2OEcx9pub6MStPUIcHEJtPVy7k4GH9+ifuLu6c6a8QkpEbnkj4uGPCjs4vu0wellxJTzAhMKFEtMAnMZTFBQPZa0BzQOyPwdUTK/9zOraBNIyVRqPWt3sKSgrumdoO0bqi+OVxILQHdWeowklLt6uql2FyGlbup9SQ67ORCBGBZuzTaX4ZHtpMG2fKNJBosbRaaK3xe6Hvu529/Ff6weD2MBVSN03399dcLxfIzzzwTpk+frv7dfvvt6jnXXnstXHLJJSLTffrppwta+d/+9jcYiyBDB5V0k9R3OawdCRowpRjFCKWMyVoGlKhITX1NJzfVOFkzg9OtWoYZvIEJDUlj1lth2D3Q+6UYiXNY5R1Fd1TpVkq6cgPGjVoZKNh2I6Clj75Bo5HT79RcQYOk0Cm1y9LGJcMOdJPycpBjaTJsjAcXtQxDt0AoeLltffAgpPYSApxaxjdXpFEHHej6Icedy6D5VMKBznu46vPFVdI1twxzfzapl1M7krgjHiTHBdeOaAsjVVHRcA7soxukdM8dLt+BXlOR000quqRyT9kFOtCoLQf1cPdckqZerre65wYpUvq6sjVsb3HGtEnOl+FYfYgjHaBe7pkvBiO6zMCVeBnmPO43yX1fajWn1IUNARhkiBCwH7wezHKuyVV1p2wUUvqoz7Nyunl/bp9RF9QyrM51HqnnNFcjrsC55IYOXxe0dkx9UQnNWhsff59uKVIl6eXDqmWYE6TB+RbaYk619QlQL+ftojA7p8+rSlqGMaf7OBlk2bRzUPXH1R0Kjn3Gu/dinCvM6cY9N9Myx3W6a6TTLcclK4PmvsAu3y/5nDLtLdyJxDExtYgqEnTeOE63X+2eBOhMfbh57+WwThqo5YDIyc8b6eViHRFzCEF7pM+JYo6RDl29nAe5+J5b4v7CzxzRkrDe4HQHzBdiFxGw5CLojBYCc7LzSk3OdbprZc/3oYjmOKr5gnUv1EbNUA6pB2G4I4lrrMx1hMw5fr7SvdPRjexG2nfN9kvcaN/5W4bFYVAKqeE+S3ZPvXS6c3L/Mc6BoC494gK04BVXOq+gfaWbPPI63TRfSLBTV3A39buf3mKY5/LesKab5gueRbVyf1ZONw/S8I40OK5BrU5N59YItQxz1cul0y2/0uMi+aHNC1Og881trG1vFaNqnG5cyKZ/2LubUFtbC9dddx3s3LkT+vv7hcMdVM89VjLdwgCUWR5qqxDYpztsIzL16VYO5mTWMqC0RUeZA9p8E4Y+3XS4mbIuPPtkrOmQRmouXg99kv4Zj+SEE9WQxB61mOkeUs8xGoBBRjGicaqz4WCksGeLU/siHp/ifC0zAsidboyM6hu06ltoOLTqi3G6KdMNNSICK9SQBQXUMXTI6c7iFsA3ZurVjZ8z1tEhxjnGowcqazUoaWua060cqRIPdNb3FFtG0MHOM+BBB3pxmW73QBeOifzMHfqau4ZINdXcR5cdRjp82ajhEcl0q3uW46H3uqRWc9SWwzQu27sl/du060sjAzO6HVnnvprBaUcovo/KshAmMui2GZTjQQag3uve1B5LHf7Ioijf6ebO46wJ9fD+42d5xisoQIM4bl93P6GsZ9i49MigXmMkJVsdocMg1xFnAOhGXVDfZU/vamq7Nsj67mrtXkoAZUwEJTMWFQrTfFzcdoWGTLe2n/idbqnALTPdWIqBwLXjsK5Q6CnlD8DwzD7uGUHrSDnhcl9R80rOITUumYr6dKPxRxklWj9uptu/jjBwHJrRlecrZi7FazU5c3GWdLrxMyAGQIafRaotZS/bLyN+dhHfW8T48aw4z3SX3jKM91U2twyj/Sc8GGFysujzy8ozIyfXCtLLRVtIqUYthPlU5lILIAyXk+mWzLUyA+O0t+Dnhv/CWhLqQT1+RuO8Oe8Qb90yvyen80qt6pYQh4zMdDu/H4po90wB//52l4Gnt1FD6EEYCnIhZRsZbWW2gaVAQ9KX6fbuMUFncK8McCGmNNWGntFtqYh7PkunjII0FPQzzgHuMOqg1nvGTLeh/Vypme4Ap3uTbB+H55QOPfjwkZO9ugnOPbmZ7o5h5zqxrLEh7uxddfIs8ma6tTawah2FJAx8me7yk25eFp6zJsj5pj2XOifodi1i9kR3rHStiGpF1TjdFuaabmoZUFso013I6db7dGMv4YEO18Es9+CSGzBtRKXSy0+e5y40I/2TUQ/x8CJgWx+R/WdZF89mxKPAiopkOADwmsjp7FzvHHQUiBAXXd7B5aGXswMdjWXcvBXdz7ARNWgRQXOm22sAYl2YeD2sCIpHoUHWvacjGqWe08vxfhGSJhlsGLMIKj2uHNVSnW4zdc2U6dYP9OIy3S51TYyxXCu4dpye7vIg4E43L63gxm5oprvTOy98me7S+k6SAUzjQQcUUdbUwWZgg1Dg5oX18ppMkOOCJl97xlkbzVgvFpP1WBFpAMr+oM491bl/i/ej04A5yCikkgVO0a8kc0l7h3SeTpw70RO8CeqjqzvaJ89za9qMme58XFH6KNONQaHGWMbvdBeb6eY/K8OYGzqU6c6UTy+XWUhaG7R2qBbV5HQXznTL7L4MlhRNL+d7Lv+sg2j34sVTI0ovp/OI1pFOdVRBP4OzMKnRvU4TFVJlomQQItUwW3ydE9kh3gfnCwUjKGjuzVz2efcWvdSJ9hZhPA+4zyUnis6vSujlSbdMgxxt8b0cF5NTPWt8XVHBiJz8/KlEBTPdIngl91xSw/faIyl/kE6H7iipIJf8vMrcXxQrQs5/3YlCXZagIE0DO6Pffcw+KujlfQNnXJBC3Zl1nS6kDHsz3brTLQP+mAAgp9vIRqvz7rl6kKtMhhEPjPN7J60VzpAwzRfulBvHhc7ofByW7ZBtuGBYzS1FLy8m011MCRjfc3XnvYxyhHjAfKGe7Tx7S5jM9hYUljMGr2h/gSR0ys42iGZZskKZbmHTEXig12OnFROMYBT9CthoxLoiZ7tJ23MV7d6wd0xtdu/lJHm2VzuqRkjNIoxeHvXUFwbXdLN6QR28phsdC6QWC0GXiFCxLjfTrdcOUiauP50RhxbW+ITRyzFKfPmp+4kN67T5kw0DIUV2kvWQhyikovVQmxuAiYmUeG2MzFP2Eg+vpDHrkguOopPT2bFKOt1Bme502Rk6NMa444w1QMqxNNXQ6ZluSXvzQG6uRJPuy2LfT1a7LKOiQ5Gk5AdI8KwjOd0TvKKFxiwdOZfK0Ckz0010PXno1BhquoMcqdIz3RkAaTDiuIisoDR0BpnKrpr79PfF1HTrUXSR6XZaFIlARYmOlF5rqkTA5IGmR5O94+B9bIu0wcxOdwLaZdYFMS42qPYZxBCnl0tBGnW/igZsCuoFZLrRyKnAuSTDWDcAyWHQDUQ9iv6BE2aL9Yb0ciNUrXsSuhntnt5HON0Yh+FU4AZpHFDQMihIQ2UuOG40dpxKXUEwggwdKjfAOYDGDa0dXgtfqKbbty/LNURGL2W6hQZAQjrdjF0UNTlS3DHU15G4b7lOMLuj08srYEaoTHfcPY929rvMAJXFM4wLr8s1USHpOkUfafwMkpMAZ8KESK9ywmhcKGjuowsrZowhoMcZJLiOeEBPfK0te74Qs4rvFeRoi+9DaPdTmGFsdBbkZ41ig4hMVNbpRpzeyzVShBMDoe4LOZoMztpANloRmW5koyH7YajbIzgm9u90+QHgRECNLmfr6ePCz2gerDHNF0wWYJsncW+ZlBDHwvcakAGaQd3pJtsDbRGaE3SvYXo0+j5UaTBC7bleejnXozHtLwWhzuiY0lbB83mctHNqVKY7HpLpDqnp5mUaOF+wWwJiwlyv8452cAkdRrIykaLXdHcNFHa6JzUlC88X5XQnoDuVFQHwmtwANEUHVXmPuHUeAEa2TCTq2Lj4OQfVdIsXCKDdV0gvpzOYWDKNAfRyk9N9zJzxcP4hU2G/yQ0w3lSKUIWwTvcYoJfj5laHxo50pDxGDBc74vWCOtRhJjOX5FyiEq+gIlVG0aINmjYi3M+wThV/puxD0Ab935ccEvIGUgFXqjQOROqxeRhMSkjRqJhLdcQMhIoHm+rfgpxucjrRCSV6OUWb9XrfEunllN3He8dNBw3jbu50l5npRiVX8T7SMO4ajgsDsDmeEe9Fde4pxg7w3A9mkcIy3dwwxvei+ycHtdx6Mc1JcoXUnDmyvr0fHnljR1GZbqMByOrFWnuHoHdSTMwJp6Y7DjGp2OkVUtPEeMKYEYVEtDDbjddQKr1cC0aoTDfRy5XTbajp1uYQqr4GC4bFoW84Imq3E9kBGB9xDIVGuYZSsi2fADrLUqFZtOEril5uyLqUGaAxGoByTvgy3aYADABc/Q7sih0CYoxAEjqk0y2E1OTriTKNLK4z7nTL4CDtFbpzxIF7DjkW4oZY1qUC55I0ACi7QAYP7SsuvbwYozISmul+ZHWP6C0u6OUxp6Yb280Rw0bdNac/q3OEiVGqt4tIB0SykHR6eZnzBYO8FAQmp9oRHhxU60cFtwpkuk0GIp0lZPT2RJx1MA76nOxOLqvOaHOmG+nlTJBSBwlU0h6kO1GKdp+uSEgNz2Y9Y8np52G0exP9nK4nJvfL4QjPdMegRnaUwMym+0LjnDME/xb3lmJquhE4LhTwJCeiwsA4aubomUtRt8yYAL6abo/TnSywt9Q4+xXOg0xK7C/4XtmcFFrTz2iil6Ooa302ONOtawDoweLYyLLRKIFNAT9jfT+DKcntXI97RpOQWhKG1fqrk8J7Ge6+BNZ0F8h0b33FCXbhY9OPcAMUZAeb1mFBm0463ZoGwMadg8FON9tbCjrd+SQ8+VYbdNXWwFQYgCa519aZnG4E7tO4Z/BMd2gwQjLiODO2TOaiuGyZVCNWQ6PmdBO9vMEQyERb9TcfPhbGEiy9vNqd7kjKqcPiwljcuOObEa8XDD24Bv3OZbk0ak1cAg9ZXU2ZqI4menlBSOO9ptYZj96ccx8T4u51knp5ikeMuTpqWBQd0SL7wWNNN2WvlJBaZQc6RUUR49ihrsQlDMaf7mwa6eVEvZTXtzPtbGgt8WFP/Y9HMEw8n9WR9mz13r/JMBY3M+gGaYhGXLbTrWcuvfTyy276p3puGKWP/62Zuub87T83y/kTSUNTTQJisgUL7yfroWhhcCFMAyAs080N4zIz3eQs1AXRy43q5d5xmtvk6gm490U06oQwwNMJJ4s9LuI4yVi8IW5LF/UhBxM//1Lo5Z5MdyVRdC/rgZghlJlTGTxTqUExYArDnRlnX8VMFH0OJOozzKmOtGcOtDuKsUGCYQi9DlcZOpjpLm+uiLcmp1vOEwrW0HiF0cvnTvZ+fhP1DIM0iiPyc7t/Rbcr0igz3dhuDpGKsvnCA5086GLKJvGyKDWvaG8pjxnBM5NKIJDa6Q3pmW6Dc8mMYSMtVs4VCsC0DTv32xLpd4IfFHAKynRjcEHtFwGGNz/PaR/y0ahLD9LQfYsuKEZ6Oelo+OfLoTNlVppRaz2Q1xmT7RXd7KWT6U5GqS0fOvxyb8I5ofaWtvCabj5WOH4U8CQnoszAuM6ioXMWAze4R1JgD+eCft9cGKtQplu0UhvOQT7pzIMmGBDvVSO7sKAYqge0v4g9N0RITc9c6hT9MinDbqDTy0YjerlSozaJDTKa8GUnGxh0nhKwuDtXImmYKMexRs6X8Ex3iK3LncstLznfzznZyQrz+VWirUvOJdmxzZIxhCKNOGbtfUMekU8Oujfn+6AgjVvTjeiVivcYpKH1xJlHCvx8Davp9gmppUZG7DTnPWsatZpuFaQxBTLHIGymu0oxHHUz3RMbahRd2PldjZunow0Wo8nUXsIU5eKLEDcuPOjEG0yu6EA3tabBAyU1PCScy1meiHLxVB7PteLG0+AcWJ3ZGpgTBRgflxtGLifEwxBDPGLMDUCk34jHApxuciLwufpmrotsVTAu2Ppra3dKjAtFR41ZbM1INUaU5bjk5efWPiQ3vJjzulQvRjXfCjy7pjK6hoOL7p2yUeuech6bfWJF80UXSfNlujtckSDdAJzWUitsNbLbjJluFkUXrytXihDeq4lBJi8DNDzTjYeoyFBnnM85TO2e6qLQucT30gXmynS63Ux31HNv9HiYkJo+Py47IBuidl/jGJQNzdAA24XDIF43L51P2R/UYwC2v+UE6cipCKWXm2q6ac6NAL1cy3STKrWprV5BYJ26/PxEpnsYSwSkjoZ8n4aowdChgBxS+jBIxx3pgmJHI5Pp1hlGek13GL38mNnj4IYPHS32GQwu7MtU4Z3rccbk1INmADzrsmUwuInPRyfEzbpwp9uQ6TYZf3T/6CxwLRKaV2WOi8nppvVC6yesppvXuvMssD4ueekEbks7+0MzDDhZPaptzke8zAhPpltjDOnAMwrp08Lp1lg0nKVUApD1oARP4zElCEX3KGqXlZCjf1zQibj1ihOMLBvnDZzPuq7W+fzcNlBOTXcy62a60UBX7AtcRxjoFpnuArorlBXfBZluuh7MXmPyAK8Rz+dh6WWagrveTHfAHJfBJNxbkMqfqWkRpw7uudipJSn70w9CEL28zaXhm4TUfJ00tMBFmeJYQYFxlemWmcsgJ+q6Dx4NT69qgwsWBIgcM90V6n2N5/Nk6YwSvRxLoYIz3WSnFQiMD8oOLU3TzHawiUEQgIxGL6faZQzoUVA86CzigU1jssAjjuvcEwkHU0Cc6OV+9mKiyJrucUW0DBuquJVaIxsXRKdUdTeyh8YgrNNdpSBRMKwvxMgYiahl8xFIZSPudsQdA8rSmjJRitInM+LkdFO0eYTo5eRI7uhxnG7xkiEGYOE3kH3am5xDh9r6jJN9hbkaOxda8xiAKEQTtBEFsgUqEyMxtexRNUCDw76+zGHQnXDP9cjNsnVQbnjSSUCDxycYxu9HtDGi10iEG8Y4JmufdB6be6bnfUuv6TaLtPCa7qBMN86xGS11sKVrMCTT7XW6KdPfFBsW2QrKdPvHpcZxlAWVPiRzSUaQqhnT6eWxsoTU0tIwpQwrBRxoXFTLMNa+h8DZEqfuPxFakjsMb+AcsP2Yr8zkYDDWpBTMxWtIp7tfd7opKNe3w11rJgOQ9hxV083GkCvmlwi6f9pf9Ew31dOV5XSz0hOcJ23pGrRshNBRUmY668jp9pQjIO1+gmPQdW90qIriIk1OtyZ+5enTXf646KI+lOmmcQmjl+N+cuGh04NfXF7PhKYGcWyQE4XiaTUYOM3n3ZIe3r6GlzqF6SLoz9XLFhQbp7zMpXgpmY0i4UGi4+viWUH7LO8+ERTo3DzofI1G8rK9U79aY8PSEPWplxfMdLMWoPo+VOa48H7c6ChlZdQSnSt0uLnwFc2jMMHTwMB4vSwBy5EKtZPpTsjMqMhsZnKuHcAFw8LYRfQ4Od16prvMwLheukLaKx39aWG7xOR8MJ0zPOA73qS54lEvrxXrFdlF+MypyZQIXCXlnjugs9HIJkt1u8KuJucwKNMdH5lMt35Gq5rukBpdUur+lyNnBr+BDEjjfMjIvRNZNCrTbRLe8ynYF5HpxnHRAzSkds/LyErdc2lvYTRqCurhmJhYMny+BNrBFBiXiZI+apGbd87UGul0e5JLejAu1HYppmVYOQFgM728dygDmzsH4A+LNwTrZIxBWHp5lWI45hzU46EXJjUkFRUJ638G2SHpbLARr9NtynSr5xJFq8frRJQtpOall5tUHXUDsSTITXZci0Ov6pfRv3FSXEIZ+T5xLGbsFsq6eJ6rbebl9unWqEhckGaHzHaHOd2XnbyvGFP8aoTm7G0bkE6CzPpTprufj4l+EOtZWh00Xnj4I5UWMXWB93XKjqJ7qWt40OvZJRPVcdYEd26bM93OPR0/zzHohqRz3Rx3XjsqDTNPyzBxr+zQCesCgE61MqJ7/Eq6I5Tp1gXDekPo5RNYpkUXyVKQDgExH/qjzj00Sae7Vh7sAzKopUCGcRc6lxA8Lsrp7jMoo44EvVwLRsi5UmgdFet0IyOCMgvYpqVeqpbXBVH6KBtFugjiD0MYAKrtmqlPd+kMAJV1kftuvVwLAzILZWLaFA3JSIjEk8KA6gH3vuqyfeIeYuho4u3EAlqGFbvn4rj41MuTZY0LD3SiyCY3ABW9vIAGwMfPmAtzJzXAu44xlNyo/dL53Lb3szMHDXxaY7KNo48dg+Kl2mv4wFuA6loBZRrG5CQhSH2egOPhUaMOysKFgZzuRufz65NONwZ+Rds2ICcq5mlT5hEMC2MX6eUI5DTQuJar0m1IBijl5VQmtDMCOtrY4uj0AyZ7at6NfbrlHKFyhP0anOtMyABwv3SujIFdDHaWXNNdOyK6K7qOBtHLXTXqMp0opl6+M9+syignJ515kowUU9Md0jKsfqI7LmQT01h5XmuoopZhnEat96o24d9OnAMHTG2ECw+dVlSmu1fuu/NbZGkIndGcXYTgwVt+7gaNC40JZ3VWEABWAV4tGNE/lIGXN8q5CQCnj5GWYIWwd4QWxhgw0vpgWwucJ6PFU2vTEOGiHKwWy1XI7Xez1yaao27ojJBzGUQv54Ik6ZJEfTTI66yvaxCv25ep8/QVduumkjDEsxOc1ikN6KKCEYGZ7lKF1Pz3PFs6jKhySdHkIGfh229fIP4FQl4P1V2+hq03km59e0JGRftzSfEZqCxOtBSnW44BzSuErEurlF7uOpcuvZwy2HrgggNFSpas3Vkw033VO46C127bDqltznVS6ydyuo2ZbvH3Q8wJCKDd45pB5xLnni6MVGFNd01ApjuMXs6FW/R2ULoBiFk4RA84e0RT3nGSa3PO73t99YWTNecyYl5Hvtrl1K5RL6eMrny8MqfbudY8zvNIVGUYEA2UXZBlPemIIRvVtsIdF3wNk2aFj17O+3RXQi/X6ugUpY9ahlXALmL7AhpQvSnsYV4PzZEBqE13AAzKuvF8zNu+xtOmMcQoFo/zPXeE6OWGLDap/SshtRBHCvG1iw4W/4oJdKJQYzc0OHsuGvjS4MV55Nm7yLnEMUHHm71GqHOp3k93okobF3KqsQwFzwHuWOPvSMUcAyllBcblHtjcgIG8LPQMS/0FSAsGSqTdZR/xrLpb093u3ltYphvBM92KXl5Zu1PO0qNsHDqWd7y4OTBAg+N4y+UnFHgDr1Da2r44YAj9kHFybWaded/nKwGLueVO5ESFZbqxdpm3uqS1VXFbT1kCFvP26SZ6eVl0YbxOeT0ZiAnHcme+ESZE+mByBjVmpkA872WrOfci70mIlGbDA+PCuZQisDvXeMdKvFYSo6xlZLq99HLejpD2F9KQMOG7lx4a/gaKjeZ8bomG8UjBgsmyw0h91hGB649oTDMedArTFyHWGtlzao+urYiNRsEIlemudcYAH8ZMN+KiQ6fBsfsaWgKPQdhMdxXiy39dBnduqlf0kinRHndB5muVEqkCZQiIihRo6MjH8bV8tcuVCakl2MFFVE80iHnE30TpKwh2kOw7sV7Ry5tlH2reloMLw3gMwLCNSL62816DflXMMg8uvb8wd47Q6a7IWWDX0z4o6/NIBVRmuJM5YkYkPbWOZnp5gUw3zSvM8JJjUfaBbqaXozG8uXPQR1PTMX9KU+jvucAczheq6W6STncsF+B00/zHzDWJgZlo1DqVeoSE1HT1cqJRo3OFARw08IPmC3e6AxVl5f5BZSs7c87fNKLTnRmCeN653t68XtM9yZvpxns3lTvwMREtCWnONFUURfcLqbnMCAwmlVKm4YM0urDVEV4ytiSk/YXEa6hMY9jndBvGxQQejBA15EOGTHf5Nd0U1CNjj/p3d/ZLxktARrcUpxvRlneybzVDO5XTgw4nCbb5hdRC1Kj5uCDlWmkF6EJq5QpAcadb0stTGZHxrchh0IKU23tS0J2XnzuOiVRGxkyVh56Oc4MC4T2bw+nlvAWoTkUvcx25yuRRdSZRtg4dbjo3y5kqzrU619nU6Hx+bSnZb1mIV8puDmFON6eXB+mucHqxynSPq2hcdPFKcUlyXvxz3U64/cVN4TW4Bd/A22LurW7ntfdrlJ1XZE03BsZ90OdHWE036vkIkT49012pkJo3010svTwUrOwKO2kgNuadoFR9/yaP7eLpMKLXYocJ72HQgrK67atGPNNtUummfZcczpIhBDkpgeRc34AsAaP9lpzuHp/TzYXUQvqX8/WGbCZaL/wsQp2SEkvjqJSGghF1iZhSrsduNKG6B2MQNtNdhXhmtUP/6Mg3C9rNeECn26WXpxldTGUI+lkEKyhDh4Zi5zrHIFbOJW3QdKgFHFxcddRk/Bno5VhvSRmZSmu6caM8Zs4E6N/uXO/shoy5LYfJAFR9uoMy3XUhme7yKFpkdPF7niWdo007B2CqpJqXVYvKrqd1IO+JpifkgZWQziU646l0zs3sqLYzTHQjyOmmMaB5xaPtlfbppig6y3S3SccSDZ/vv+MwOGCqP7r/r8fPElkkzGAdZ4qcMsMYI6sdrzv3NqPRqUWNSONuMKdnulnQKaz1nk6l9gmplVfT7QtGMAv49a09QhkVf3fwdL+KLa8pFDR008cp7wn7KiNjesug8zfjsQuAaqOCiqlJs7GnWDRFjAkaOkiLxD1l2qGuYzoiLcO81NiRyHTzICVSzJtg0K2jk8J7vkw3jYMKSNUXLl2h96Pn87VYIvT9hWiNyIhAivlrmx1DLbA/eRiY1gMZkR3QDPNgGySH2gEGnc+gJ49Od87cMSLMKOYdE7o2+NdbuXRhk9PNRH3+uX6nyL7MaKmFKUGU4DDIszEqHQARPKUWe6yPfXu+GeKS/u85e7v6Abq3hAup8ZpuCtwpvYjyxoW3CyMgxRydBXS4ySmv1Oluacax6IY+yZYZH087NH+W2UwF0ctVz+0yMt1lBoBNejQkDrZB9lxGxMrpuiLegAKd9XjzTqAzBjA+6jwezzhfe3OmVoM1bvA3UC9CsmUwqCGYFnpNdxGJFEOval9bT6n0PyL0cvYZff/dR0FXpgZqX5gH0LEWIp1O7W/joLNG2qJyfvB7EheCmhEhNGrxIlMcO5fKnTyZ7jJLBnV2kdxzcTx6BoPbYhX34rhf5j3aRIMxedbLIFOtdLp7JUtNge8LWAqoBxn0UjHUIkm7Z74YQ26v4LgEnWdF0O4jkYiwa9H+X9u29zndNtNdxWgHJ7tQh4YOy+j6M93OgZXtcwzARWvlRqODC5fome5CG/TtHwL49Wm+jSqMXn7Dk2vg//1tqXqct88qGuwgOXneROiTmaiZ9RmvQmhez3Tz+sICmW7a0PG1ePSvkj7dORO9XDrdnQNK0bEsZyGfg4g0yChajBltcSvS2Y7JqDcKRFGvZ+/9DDkRcv5YYKa71eB0j2zLMNy4d/Q4n9P5C6bBpUeZhVhQ9fWTZ86DfztpX1W3qYAHBwVYYgk4df9Jqqb7+Pa/AVx3AsTSPUq9HEWE3PthB3Ex9HKEoJcb+nRX0jJMYwAgHn/Tqes7fr8Jxjp2LgDlKT3hkAZcNu4c2Jv7ndcZh4wR2fWgP18Dg3qsgIJyKqDXUKADwADA2kWu0r2nT3fp9HLdAOTUWLzXEanpZoYb1VfW5pw5kJSZ7iE9kkF7BtGFg1gRnHGj9CekqGUF4jW6kq5LdcyKLB3uy6g6jWyPkch0t8tMd2KwXTk9XdCoHJfATHdQ5nK8bCfU9pa7D9EcqlC93JvpduZ5b2oYnl3tnI+nzp9kFqcsclyi0mHGgJAn0y3XCJ7bnppucSHy7O0ukOnmNd0+IbXy9lzVVo9lJt22YVllT1TsdDc2Cf+tX57Rqq2n/BzT+bjX6VaZt7YSarpNme7yxsVUjkDZW9XajOkBlAxSs5f31A3OHlGbdfbbmMx0++jlJlvFFOzEweaiYaXWdP/5A45Np62zoI4RRFIYGAm2CABccuQc+NCJc+Cggw9zHpClOvUDzhrZGmW1zygiSIK4vONBUBCY5hbBmOlOlbXn6jRqBNkuZWe6uTaRPGuorSftt7UZZ96oPceUANEDUhz1E9xOPrQPIfhZRK9TRikl1y9qkefxixucnuCTmorvh17tsE53FYMMnQge5oxe7mtnIjeeWMoxAB94i0WxTJRI3hdT0ahDDEA8gFbcB7B9GcC6pwvSy0+UfRoRd7+6VR1s5dHL3es8Zf9JUNvsZDcbc71a3VTSbAAWs0GTYYi1Ufrfl92OxBv9Q0yT2W38HWZJynUWopIKjPiPMw9yLl0e3FEcL1Hf5TIjqPWJcz/y/XgUPUjUBxWaETvX+R2LCkVa6CDnBvJWWdM9KaiPZSHwgFAsCXMm1sPMySwb3r4SEhufUQwAUvF1LoS1vCqWXo4ZYnKuR0pITY4HHuwUsHljm3PYHrGP4SCVuPKc+YLK+YkzAnqjymxiXq6BXika1hQZABjqU1lergBtzCQUolHj+1Cd8/QjnK8V1IvpNbhCgV6uKXQWemSte6WZ7vcfP8sjXrNfk8zmyFr3VLQ+wOnuKLC3MEeUsi5E0S+TRo0OQbCSrptdOGJWS0XOJX5uutMdH2hTTg8af949l7cMK5DpHi8FIvFMISghtcro5TwATAyA/nQWVmx3zoyjy8n+s4B0NOE6RDRfxF6AwWzJUPPpUVDAm4zdQpluT023Ri8vcW8xZ7rdFnMrtjnjMq5cu1iOSzxZB3Mm1CtBwhYqAWMdJTjzzZPpLka9HIHPo9ao1Lu6wj7dnvkiM5UUzEPwdlClvYGzvyRqmzzOUkSuH7GWMO6UbQx3ukVGO8B2ItE1FMbl+wsiLKiHAeqV/3DWH1+DhsC4W9PtzXSX1XeZXwudC7QXkNPdT073VL/TGGS/6qC5RRiJTLfqR02q9jE1NlhqUqimOxSqg0M9fO6cA8W+e97RB7IuKcOQkEEaLOvxgD5nzHLT3mDKdAvavfQBJKtABPn4WVRGsNMVDfazXQk2021RFSBDR0SC5aJEJ8rndGuGsEfF27QRhWa6DQuORwT7thekl6PTjVlpjr984qTyDECmgoub/Of/5VQ3ECEuwFWMpYibzwAslLlUFNB+/2OV1rqzAx0dBk5JGgmn+5PnHgJHzx6n+k5HMGvEFLhxLgxzqiMddCyymg9yupVhvDSEXj48IurliMVrOirboPk8jWNP7wh88JT55uuAhDcbxduvFEsvp4zLCAqp8SAE1XVTrXugSi4AfOG8A+DVb50P86cYDDgmmpeTtaWUjarNDSp6ObJIfK2S9DEIHBP5vhsXu3WrtZpRXEFNt3dcnO87+ofUZ1hZprsern7HYbDm6oVw+P5zxEPjMRiB5R9ZJ5DZGxtvDtSFtWnUGTdE46d1VCij298B8PItKihC4A4dGX2uYmxWlCIgJpe7jhi9nJxWFQBGyiar6fY43R4qfQEnivaW1uXO15oWtzRD0e7LUy83ZS4x24tlPYg5EwM+qyLHJcacbgp2iv2UMt35Fv86kgHv4c6NJdR0a2NYYU23Ti93fpdTDIADpEpyScCzhQUH7vjEySqgh+1OBZb9xbkliHnnC2UjkS2i9twCmW6cV7SOKtxfSI/ARC8nJhqxJEoGBnSlPTFh/HivsySdqHifQ6Nem9Wysvq6Cdpz9R7wpTAAiIaMoHUXEOikAHlnOiIyun9c4jhsDeVQqSmQhmucAgma013X59R2bwHN6ebPM5QGFZ3pVqy2EjPdWqBTvI1k0vxj6Tb5c5lOtxKTbBBn+WvfOh+mTZ3mb32G8ygXoF4ug37iXCnEAKCSLxo/4XiXWUqp1XQjrNNtUZVoh2YW2Qujl2tOt97HT4+2ixYdWiYiLBvFHDS1sCWCWtPoi6zs7KXe71VR5KXTTeI1umIszzCxDc0I3TCk6B+iTAqoqabbtBmNry99XKLcoYsmROZvkKtOY4BGGupOMIJnuuWhQEYOADy5hh3ApkOODmiP012mkJqW0eVCc2ul6Ab16ywZdIjigU73GXAoI+3eM194+5Vi6eVYG0VQme5YhZlu1wAiY2eLdLonFlhDpv6g+mcdkfdE2ago1naR042Zbm4Ul5LprmdO6Rt3yxsgo5jmfL6MWnfvfOEOw/bulDKayxIMY44hBmhw/GJ0H2jo5PPQmHHYL93R8eGZ7kDafaP7PMpm0DqiORo0V7Ck557PADz4/zwP8/Ws6OWsN2pHX7oyQ4fRy8lh2wlNbg07y3R7mBEq0GlwGIP2FkIda5NUIV2YzxWqO8VsJSnpzi6Hcs8CwPGke0/UDUB8tszpzvpquh1jNzHcW7xzqdoRJisaFyq74qUptF6QKvzsGsfpPrDF0Ju8EHgwOl4jznkq0Uhm+wE2Pq/KTbAUyrO/oNiVoLvmAXq2FJfpxiw3rRdaW2UGxk3CexSk6ex3z3tPdr5Y4PyXpU5TJk5QGgjqPO3eBJF8TrSu3JJp9tDZBTjdN2hv4WOA88+n6h7S1pOz+jSGgB7obGZtKE/98VPq9xXRy8ne5HsB6jsMdkEi7dzHZpgS4nQPFu90Iy2duq5UlOn2t8eluUFCp+XTy72BfnGWc5tEfrbYRSKVjQQ43bK9HP5dUIJLFwDlznkxGgBFCMyZtIrK0tCoUlineyxkutFJlo7AAKqXp3XDuL44p1u1DOBCakWI15CxiOh2opBh9HJTxK/87KXWq5RT5PGg6nKuZ2t+kte55FkXRd0p0unmG3nZrdS8NZemzQjpz+NNCtzFZrrx4IpGnaA6xJXysjDw5ee0NT/Rm11QmW5nkx/Ox2D5tgANgAkaVdmU6S65TzfV6MY8/St3SYBGfG+ed1jrHZzp7iuOXs4NFxoPleku0bk0ZOlojKhHd9lriNZ6NA6JpPMaaq4I9WhJL8/XeeeKSXyQ95DlmHG015AyqVFXUnfJ9hdyqla3Ote9z7i68lg0prITZeh0CgOf2tf0xMcF7BlyDgVlFmYe7Xxd/6yboVPjUsCJ2vic8/WVP3oe5o6Lr6Y7Nawy3RNHwOmmjB9paYgxK6heznQ0gpxLUhc20j9rR9yJwiw3GshIDaUyn3LHpamh3n/W4j33uTXdPno5CYURZhxVQk23nunOVNR3mbNoXt7YKQSPMGM3J2C7K76kp0asQwro1WBLrLY32ZMjqk2XT2WaSlKC1pHSF+HtKxsrCoyb2GgUpNkp1f/LBktUTJ00wSOQJc4peb+OcnfEP1+4TRLmdFNgE4MRgZluw7hwlha37ww6GjPG1cG8yf5r4KKWRcPUMQVFFfHcxPW17TV1PvXpAnPkdO9c6+0CUYhejjYjp+eXub+4Gd1oYOkBBT9Lhurg0GBuCccCnZ4OAHwsSdTTVM+tXrPF66B7bN3yyjRM66iF2bmfO3t/JSK8N8A63VUGLu6knO5VjwAsurroTDfvN1tYSE2rXTbV0XHVXarvpV8F9INtYhE/rHMxCUAVhKhN1q6T9zzFw4IdXt5Mt0HAKJACqjvdPPpXgAKKUfxfHAuw3qkV9te56Jlud1ywRr0cKKdbE0BD1VxlmASNC/2NpL6hsx5Ya69no4xCaqX2jPX26Ua84+iZuyZAI74PznR7nG4eVS6kAUDz6IXfuWOh+qCXW9PtrXVH6NnbsseF5n+iAWoTcX+GjoTU9FZH4m+08TPViiHw/s/6mvcxldEtT6QF90LVv9yQ6f7BAyvE17IPdFO2RBk6XcqJQgNwUFd1L5YBMPskJxjRvRFgm1amUbRgmPcz4WU0Ca2m++WNXfDYitbKgleMXo4OGRf3EWMmg02YufM4UR4htUI1uklzsEP8rrxAp6lEg5xu2gP3GV8fzAgpBDl3xzc3+s9aNJql4KSRXq7tJZk5ZxRoX8md7mRhNho64jdfAnDP54oSDKM19NibzjWfsO8EKEdyxf2M3LpQCtDUyQ4AhMmRLj+ThmySYtloxLTDzCWdrWUGxs3MCFmOoNtYpYJK1eK10FJf6y37w3v1ON3utSjwMyzU6eb08k4t0x1GL+8023cBwauPnarZApjdlXXM5e4tnuDLuNnO9394u7JlfIEIEl9sdfb98Ez3lGCqeYXq5WH7R9lOt2n+141355IMNolAp8/pdsays81hi7zZFQ3WIaAgjepGUluxwJyuXq7vNVecPhf2Jlinu8qA/UR9TjdbBNgDtFBNNzoURlBUGTOhKhNBTneR9PIeRxgtrKZb33wmlUstQcdFtfuq8fc8xQMYW6CRc8kPdC6MpaKIBWq6CZ6NiGjUARv0H/4FoGMVwC3vCGhH4h0XHgE8bt/yBH2iMvtGxtjXLz5Y+Dzx5qmuSIaMZPrGRd5bdsA5dIch5nH0PGhGZ5hd/4iol3uj6CaaWjnZfwE9QBPiJLbmxym6mC+qXCy9nMDrM8ut6dbq6PTvK6rRVZn7eqVgrDKXaKzJDGyvkV6u3WtYFF1nkujOZYlZOn4tprpLU5/yEct0CzVqcqKaDQwA72eRD3IuMcM5xRE7hK2vlEYvDwAZo2j8kYK/ydgre99lmW7aWy44Yl93zHY4ddib85MC1MsHCtd063PJlOku0fhTtFhD5rLiucLGpaXRnecqeCWCNM582ZEfH5q57Mo3QKrZy+7xPc8jpFaEGvXWlwHWPw3w8u99LBtVomEI6JG43GEz/a0IiwKdi7geZODxQ2ccKr5OqRl2s2+YMYUO/zoicaxCDmaTrG/d8rKfOVBpn+6Qswjx3X9ZUNLr6nThMw6cLM79kw+a6SY22leLbzdJpzt0fwmt6ZbjMMTo5bSHKdp9ukCmuz9UdwVRryVNkDFy8eHToWQogUVtb2qa4fmxAwyBq2b5ftLmc14nSEhtcojTXaZ6uUowuTbRFae5bEDcjw+dGcAEKwSTjgw5yIj2t5RIo8/pludrf4djm29P18Kyzd3h80Vlutn7hc2XkMA4bXU8GJFhtlXZgYgqxd51t2MAXDUTe6PqwMh6slCmO4heTpEzrCmizJzP6S5AL6f2UXpNdwi9vGK6sL654obaKaN/LGI8nxs6ZNim+yCVizimURC9HCOt1O8yiHJTaCPSfq+rC5uc7nIFfaJkVMlrw168S791PjT+/VaAFUsBXv6DeLwHGsU/T02aNG5zva3YMlTU2UWDqLlE/0PxJJ1uHbZBYwR59aMA+53uddQDD3TvNlX2Jm3KdI/zG7dr89NhO0w0Z7q5kFohejmBKxGXW9NtEgxjRjIe9M2MJVFuFJ3ElEjsSBger/wpmF5ebKZbvH59gNMdderqhMifYb5sfx1g8z8dtfyDLlZ7EafR8fkyYiItJgq0J9MtnW5ogWGfE+Udl81rl4Ojf24AioQhereWmekOKF1hRo5JwKdyIbUknDxnkthbmrY/D/CmZDoN9UAOIrAkdwicYnK6MTMv2RNdw1EInDE4l2hMRiLTbVCj1p2oipxurU83IkVn7VsPiC9DkVqhxeKr6WbGLdKva4ez5n1OlUWl/JncYtXLUZiMOR1pU+mK5kSJsid23Fay577zxIMAlgDUZNwMHWJaZKe3k0YpQo14ljxzLcAOqbTNz5Uy+3S78yUSGKS5+LDpoj1lyVCB20axP734jXMhgvbMD+XvJe1+k6xb9mcvy8l06zXdIWw0ctANTrcp080DnXg/T3z5DNG+s2QEsQ41O0GwRXwlGs1etgOOUZCqe0OY050ss2WYP9P9XwsPhs+dM1/YWJFKEgYmmwODsvgZ4rxufUM8tCE/1R8Yl59zTbpDZcNj+nP0cTbRy8OCEbgfYQcjpKfPP089zD8jvo6G2Tovq/SrimEz3VXsdKtMN4Ogl0vlawUtakhCJj4owybvUpF8rbEK0Mvx79hzAunlzKCY2FAhXVi/R6XAuEkd6pvyk72GjtygM4Pd0NvTXfjwClILLbdPd0BNN1eQLdcAVJludjDjARgh6r2sBW2NOT97MrpyDpD6exoS/hqhoNqoYjPdi38JcNsHHCGoIpzLeqkAWjlFy1DT3eSPxi/JO5kYTySdO1tEDSxELw/NdFcuGMYdTVxDZR9eil5er5yQfnK6Ee0r1WOF6OX9Ua9x5EGYIRU0X3Ccbr4Y4L4vANzxEVeEjRl/eNv8QNed7unjyqzRNSngejLdrjCWh0Zt2HOf3p5QtdQ+0Dj0bDPT7stsR8j33GaDgE/lQmrOOAvjmsZIOtNbauaLvsNGITVMRrU7XS5u+qe328WuzHS77KJdm+nGz43Kpw6Z491f2uPTzTW6bI5h4Nx3hpvuXZU/FKFezgPUWmDcWOuuOd3Uz7x8p7vWn03DLDjrB/xGfo7fYdD3jLAyDc66GoH2lW4wIhYYpJlQthPlsotorcZr2NyTFOktESeD7xuXYunlNA64Lknw1FfTbRJS6wqklxvPaDYuuNeU5XDz99LPVs3p7sw3eu0Wz3NIRyMgy6072np7PrXGym0Z5o4Lnsk4FjhPyna4PYFxbVzos5fsIkwuBdHLJ+a7zQKXpjGkuWLSvzHZui/eCHDn5QB/erfLNtFsSx6M2HdSmR0ixgCs011lwIjiwkOnQnMiD+895RDIa3W7tZD21xuxDfTHw++FNhjvqQ33LCqdjkObX6h6uTcSyiljrtOt0cuZEbjP+JDNMQy81yt3OJQR6xzoOYhCDzR4HQZ5GEXTfVAPjvHWng5x5ni2q5xMdxGKsVwUCzGxzE3arelOBNcxIfO9/iPiq8cAlMZtfNBxKIbzcX+5QtDh1TKruHFZdqfzVarWcpDzEkbpK7vm0pTpNkTCV0WczEWOK8aSoYIGq1LHDTDQuYHrq08rl16eDc10T2qq4EBn9bWHznACeVmIQTbmdVaNme5ozLMHvdUdK51e7tGMyPjHkmddWPmKEoCKRT0BB+50o2P5rqP3gbKgt03Ugy/S6UZKXyEGwLWZd4t2OkbQOJAzVAy9XDc4WaDTFNDTRdO+/bZDyuujK97bIHakGclttU4NpodFg+tAzv/ODudeu9Ih18Cz2yOY6eZrCL/njICKBH3IeYkl4c9XnAgfO3U/+MCpB3ueMhB3Ptuw4BUGzgP3XF7TTQ5r84zCey6J9Bk6jJjGRafBVswu8mRm2bqXTLSBaCN8dvizfmdA22Oz8ZB+96d/2f3ZQy+vsE83KwHT10xZXRGCanRF9FDen2R4bI9ND6jpLrZlmMz+9m5zmEQm9XKjkFpncKbboKPBz+iyFbrFiweIlGpCg8nIMGT1NaT/Tdi4BAioVlbTnavMPinqjK4xn6ltK1U5Ap2L+vyPRpzx6sAgcVCm2zeGJtHglJmNRtjhfs/3f87q/PdT9hX/br3iBNjbYJ3uKsP+UxrhZ+87Ar57bBa+euGBENEcqUmRbuXQKTAK13XZS82R0yB6aFH0ct3pbi3YMozTHStt0eLbiOgg63UoMulYvT+7IA3bKOSgIeK8zpJNIfw5fsh5on9lOt1yYySVWEIPYzKUm7lUTrc+LryOqbYFltYe5zcAtc8fhdQCsy7iNdn8G8+o2mHzZSZT5sX6cgaKjCaZw8DHqCImkqmm24CtkamBwYhcN9MsCCpH0DJJnsO7XCG1ApnuivpcsgP9lPmueF8m4T2AnZpuf7Auz9bDq268zQ9flF6jypnWkR7AYD+bxkR3ur936YLyjSCTkBrPdHN6uT4ubO39NrNQBDr7mB5HqHI1jUsYvXyoO3CPN+25or6bDcNHTi6DEhsmdqTVZg8mnFKlILX7qBRqQn0RXzukUjLdQX9bpDCW7khVlul2tTTQaf3GJYdAbYM3Q9eUdT63sEz3IHYgCXK6+b2TojcJWoapl3tKwNoKOt26iGfZ/YVNzgKudXKIUGkaAH6/z3dgY36qIXjl3WPfaA/ZN0//SjgLrmTGiJ92r4+DrqtRUY2uYR21R52zyL+/8JZhIbLyFMCTXVyEA+ZjL6ZLopeb2IucoddQTn9u9V4BGV0t010PQzAcmOl2sHUA4PUtAbXLHPoWUmmmO4jSXglUe8AAW1cGQsMy3QQ8rwLtf14n7qvpDnG6aS/Svuclejy4WZuIwbfetgBOnleeWHA1wzrd1Q5SdZS4K3uacugUjv2o+PJo1nV2AiNduhBSvET1ct4jO4ReziPnZWcXaPHrG5ESUnOoi+mY87NXSA0f8xrjb+4MoVHzw5C3RSo70501Rso/dpqj5FiWCInep1vPdHMqdcMUxT7wjEut3+kOzXRzESxeHx3WpxvrdwmyFolATh2PipIYlPi+Eq9biTdp8+WUKz0/bos6lD5PTbfMJlFAYziS9NPSCIf/q/fnfY6tuKbbVHfpyXRX4nQzWuzMcXVw2MwWUf4Ra/C2bcJ2hJ65IpFja2NFV4gR6qOKFkEB1fcW9rPrLBjqTw3fj0g5giHTLejlIa3USEyrPx3kdGuUfJXpphrdApkoqtPVAle6eGUDM4YrqqNj8yWwLWXSmTtBmcvGXI9qzRdYvsIDgEK0EbzrF0U0S1hHpjWEaGDjMmtCmawrT1Y3uIfyfVM+Lr76a7r5fMFMd8CYUJAd6aSYveSqzWFsNJ7p1p1uylyyccEWUOPqE5VnurmQGgfqM7D1PFjjGN++daSN31PrtQA/B38PxrZzgxGVq5fzrisVZbpNLaD0ddQ4DXJyXoRmuoMYVzygh2KudI7R2g+zXaSQqmkPVmy0gGBEZZnugGCEtkfemLnIzxbRgg9tw7Xw4Osh5Sv7y9rj4y73Ph7mXBZT062xOkcEQetI++yxjDJ0rihmVr64s8hT0x1i0wU43WQvRCDvseP2ZlghtWoH9nmVNbr3nvZ3WPHIAMzWF92s4+Gk1C+glUnWBNZ06FlAykCFqpeHZboD6OU8012u060yCwEbEWW649Lp5k4UHjy4wcgaRMT2/khx9HLeKiusHQnPwnBHM4RefuLcifDc/zsbppSrLOyhlwf0sUQ0ToH4sHNoekSgSExPIg2x8PYoPIPCD4CwA51nLzWqIx3ouvCeetlKnIWgNkXnfNsx4J/7hfixNYqMgKz3UK+fAPmph0JEUqc6YBxIvVw/ph0K8MU3nbWEmZyph1VU042lIDSVeA9QPncmlitGaKi7vP3jJwqhk/hdNwJ0rHTfL5I2Buuy0Vp1kGweDLkOnrVCA4lnBIKYERVmusfVVVJHZ8h0kxGLjrCk97YZnW537VHPc955ItzpbvauIeFcZt2AjV5zqRnGSkgt5s/o8vKVkXW6vedGKuk4Vb5xwXvt2wGTIj0q090/lDG3jOSO4hys2QX/+sU1rQcXA2DK6HJqLNZdll2LGtRjmM/5kz4DK3tOxVXiN3oT3t7eiaA9d/YJzh5CDjfOFdqzQ+nl7jkHKx8AOPFTyvkyjQsGZfAMopZwIypeSawoWf6F6K3DEpDtfiYNO1Ow1v2Z1Tvh02cX8b7YgcUXADYEr3Bj3fCcMxZYF87OF1PCQNefKTvTHdQCja+j8ftCosN57yBxLHELiXotfRCyt6DgnP4apnHhbK0AermHds/Wb1ntXwmkl6Jn71lJQt+Z34EXH9xffHbIklEBRDxP8O+kTYJU68BAJ+L9f3bsjxZvS9JyW8wp9fJdQi83BPTEm7nzJ1s3AfpS9ZDQ54rGXjSeV0Gsq2JahmEygwQvdadb2pbW33ZhM93VjjmnqG9TzfPEV9OC2oZqzEKP2kEgvYQbOhwl0cvbfCqFeqabR4gxu1YW6FrIkSEkvZnujMp062qX7kaOPTJb+0NUgnm2y+N0F6nqrl0j9V02RcpnjKvzGcwj0afbm4lOqppPT/ZS26BRSC3U6daYFr5xMdXREc3bQMVWWbqAaHFl9HKtDR4BD+vJbu1lRF67p6Yb7bhZrsHSmmsKPrgoo4DG8MxjNPXy0unlnEbH63S5cVO2EjVCtR5KKnGpFsxyTXDbndAaMUXI89S2DwDWD4Q4LdzA1I3BIANQz3SzuZMuwukW91EuTKI+vB4VlbqDMgeezKXzfWBvVP6anEnD9wzdkeL0T20PVp0RtDV0xKwQZfligc4/fd7c2dWd7hqZ6TY53QxDkAgZl0Yzu4g7cMReKQIUpNHPIqKXV0Qt12q6jYHI6UewPTdkviC9PKikB8dvH6csSO3pKnNJn0feH9TDllEEbB3GxI6CghHcwWyoVEjNFwBme0vTdIgmAzK6bA4gA+ClDZ1Gto0Cjf3Eef7HTE7UqkcAbl4IcNNFAGseKygYNr4+4TmDys909wWos3vtDHrvsOzl4s0hzqG+z849s7hx4WwItregk2sqX+E13RU5WMXQyycfqL71sNG05yHVemAoxHbB9aI73GUKNQa1xhoxFCqlxGtocexS/Hw8mk0ae1HQywsJqYXSy7WzqFuWLhhKBlXvcut0K5Qcvuzq6oK77roLnn76adiwYQMMDAzA5MmT4aijjoILLrgATj755FJf0qISHHAhwKlfdDZoqbIZSB1hCFx03Ji76Efu98Wql+sGoHKivIcTtsP6j9PninZhZUdGTRkX8Wbemu5hmekOqwFCCmh7XwhFfOohTtsi3ekO69PNs1EoYoJOnDyxVU13JVHhgjXdyeAo5vCA+kx8AnPUvkkKqQ0FUR0Rp30JoHc7wBHv9z5edKbbS3UM0gBQ9zYimW6Dg3rYuwHWPwOw/zkQu18axtqB3lo/H8hUbM01Q3pTFxy7r9ZHthDKcLr558PrxQ6c1jRCNd0BhjETyevZ9yK4d8XJMN5g8Nb2uYfulpQjAmWc19zp5loAYdRYX1R9sKCzwJ1wXcm84kw31aPinifrqtGImRqS6aae55jRLSkbxd8Xx4H/zOmz2h5M80WnUX//0kPF2PzbiQE9oIsB/3z4vksiaXJeD9dODKALezMpmOkOZACc+nmndc0xl3kfxz0A5yruuSUYxib1ckSDpJdX7nSnw5Wla1tUZtQndsSCupjpxpZhgZh0AMDGxd7exHoQBK8lWhccTO/eCLDPMc6vAmj3nCZcPr3crXP3gJ+hmNGV7+2v6WYMgHyNCOJgELgpKCj9sccAnv5fgLP/uzjdFU6LRcXw/c8NbTGHwfAJ9Uno6E9XWNPttgwLzXRvChgXNsd+v2wITtaOX4VJroMqsB93ukPYi/xcNgT0TIKEI3JGB9Hu2R4ZqUUmUKc6oz0fARtP4XSHraMglKEBwG2FShImJQX09ODEhH0B5HTGdVJLzCgtkWIshyqGXh4kpEZK56azSNrc1ul2UfTs2Lp1K3zsYx+D6dOnw/e+9z0YHByEI4/8/+y9B5RkV3E+Xp0mz2zOSVpplfMqJySBAkvGYJNswGQbYxtwwOS/sTHBGIzBoB8WyRgTTVxAQqCcVyutpJVWq805zezkmY7/U+/duq/e7fu6+71+1TO923XOnpmd2e15c/veulX1ffXVefD85z8fli5dCr///e/h+uuvhzPOOAO+973vyT51yzzDIPwFHwVY/UZ9OdgSarP6Fph080Txknewn8MctCleYyLdLDD2qI7lpw5nGL79alaRjkNFlzsiRVXKKUEosypaZI4dL/QjQSN90E64qgq9vIoYCQajzFkRkmEKqcVhgUg3t+yY3hO+YgRemMxJO0JqlS6urtkAr/ovgFVesOL72VXH19hFfYKS7rqqyLqnu9N+0b7iP53km36G2XeJlzi/uO7eXEk1LELS/fTPAb72AoBbbgJ47nZr0s3P0ZVM7Kiu4o3uFzPPkXc+Dtx0s7MXKqH7I84owgT0q4C0zHjV3BCADKaXj1X1LUF7xfkxURW6KwnvGYFytZ7uEdXTHSykxgKdl/y757+cRDbjD9IDzo0/MLb73Pl9HfAfr7sALlnp79WPnnRnAn/nbIfq0TXnLhtCPdjTHViM6F0I8OqvA6x8Xvn3gsSODm8G+O7rAL71MoBHv+X7VlCRJjakW4sdZQJaKrr1OS3r2WZ7DCf6VtTR4AUrfo64vzf9rpl0s/sqSBuBU4bNEWI1Gz0H1/6wJN0knFmpp3tMnaOKIywXnQPwh98EmHtybXdRBdZVUJGGFzjb6+7proB0zz6xAtLd7ruXrNNonAc0knqu06GTy1z5sxHNO6B1xXl0icK4HscZnHSnO7zPy2d1e9/DmdXjlejlQVYN6d73OMB3XwtwwNOj4bFlEEuvLqPzGqRejjcvY4/4zghDupGthtMRApmubbUg3ZP2vUxsJHbOWvTycqvZYyCSPXv2bFi3bh1s2bIFvvvd78K//uu/Ogn4l7/8Zbjrrrvg8OHD8OEPfxg+//nPw2c/+9laX7plMZlOugs1JN1Bh+66D7ofL3qr8QIVqGsk4kO9ZcxZUdAV5KDrMpuKrvPD/A67oJJuk9JXUONb0I5Ar1O9Dry8Vl7rCfr0uIqi1avFBwMDHwq6Il/aFSwZVIxAO/2l7sfL382E1IzfmfV1V1UvjzJ2gydSRvLQEHp5pXEh7KyY67K96AW4g9ANmw8GtGJUskpCaji3fPfDLor1339gp5cb442o9//85TPjR7pX3eB+7FkAmXQwi2bbEndPfTj3Zudj4Dxqvu4m0h00M7asp5sJqQUExecsnRmPYJgN6TYCu1ymF0ags6J6Of1uI0FURx4YG5R+HfSY62D6FlvSLaGiywN0M5HiCEfnvJro5UehJ5heXsmCxI7+7x0Am37pjiP82V/UJKR2gpqecc5S/5is6MwrQ6mbbP4ZOpEtK2SyPVaEROVCJx/TyCdS8PfDVDDnrU7GOQsqRnDqdGQRpCA22uLz2OfnByPdvqTbXdeKBQmbVerR9RWA/UXUIP/CxzNGRrqDCno8yalUjGC/C4pnHRiuwPggxiJH/ysVI8yCHotb+HMEs9EgumkGQHDSnez0Cnfm2DBeKN9WXAijlejlUUeG/b/rADatBfjRW613tAi9XCPdwfTy1ByvkOUr0jAQ5WAJP0+UF0P5v+2aY28DCWJ10t1DcSPGNmo9WvTycquZM7Rx40aYM6dyhbyzsxNe+9rXOn+OHGFCFi1riGUCHDQmkmalNBDpvvw9bt/PgrP8X+eXJiZ1PJg4qno45p3mJgwcjQqgl8di+kI3gj+jSprXQmr+3xlp5/Q/Hyie4VQrj47nHEGdMsMK8V8/5QY2PJitRC/n1DW6vFSy0RB6uS3pfuX/Azj0Pre/cONj9moxoidHnqtNSC3INNvAQOicr00EC6k1gl5eZWQYJbYmM2IAvKC8HXKVafdBVrOQmvez6eLC5zKTyN/81dVO0rKgzxCHi6NfDPsi//xhgO65kMkGBMWoKHzKP8BfbL0YniytrJx082fniQN/TyoIp7l/n6gq0rhwRgf8/v3XQF89Kro1Jt2DCy4BGE5UnNPd3dmB1SsYmQzQjOD+iiOAdI6Qxs6RJ1tg7KP00RmSCP4IuUyXz7inOcCOS24PSLp7fAHg1tKiiEl3ANJNs6v18+JdlamoAfAPLzodXrV6GZy1xBiXE7rXvWD3u+971n1/umZDZ5salWb6VCaSV4QkZGtNujnSje8HtQaVId1MSM1Yt+CkO4a7KWiSxtxVAO+6z1XhX34ZZO5278qsmQxk/EJqVZFum1XSXeH3U5mopx0w4Eh35J5um+ietad7u/0cYUsXPTZ0weB4DhbNCLjXLn47wLJLABYyQc9K62K0fHHGHu0VvCKDkst46OXmKDVvzdMs6TZbBpMHNujP98McWFAPvdyGdCOVmvY062UOagGLH+kOppcj0t2WGnH2im+/MKR7F7j+IpCxhnviHXcD7H/CufcdTZpqxQg6Q5h0U8yLOUKyXcfcLaTbs5p3R7WEG61YLMIvfvGLmv99y+I1ujTNA8UvqYUqOA9MuvHyX3x++SVp9ouRIdWcDhom3SbSXYFeXrcFXVxGlbSU7rIil5NpLwB8PHNe5YQBbcZSgF6Gcle70MuS7qGq6uVxWOCcblKjRJQhkbALqaGhs2VCaqGRBefndNsRljKkO0C9PCDprm+kT61It/uzC0YbxSTbP0PQFcwWiaOnm41IqnSGZnW3RR+5V6kXlWzeKU6yQIqstst6pJjRCTdaRW0Estnev6+I6NJeoXWzjAyz7ZUT53bDnHr63PnPqjC+ZnjxlfZ1YSJomS43SAxMLhNJ+2isSsUIShDo/1qQbtlCZ2VVeKIq03m2rd3DyXMc1CVWpNssWjD0MlgDIAVnL50Rzxg159mMtcE7QzEYCOmu5FPRy0xka6WXz6sNvSS0kuij7PvUX24ml1i8Et0vC84EOOFKJ+YIZOnxFjBFLw99H1UqjPuQ7oO1Id2+pDtmPRpq0cCiEjKMgloGDV89qFTmrYb7Gu98Pv2A/2xzXaig166KzOOD5crlFXwLZwLERi9nwnGJtt7Awvhoyn3m/oT7cawu32LZL99UTEHj7FFyiUudbCTSbRRsre0IDOnuTwUwkLihuNypN7mjTrlPDCp0mkg3+zd6jFor6Y53ZNhzzz0Ht9xyC3zjG9+AQ4cOQS5XwQG0TMzo0jSpjhyp7OtMw/6hKofOZj7qWs4f1DiJVcJTlWQXmU4uBXqXg+nl/iC5rTBqRXT3nfhK2P30g9CfWQQb0+cAjOdhLBsjdc1MuhlKp9XLA0ZjxYN0VxaRooqsuS75rnnaMaCQWqSkm4IlsxfVDHSMXkOPGeH30v/ztkvgS79/Dv7xZQYDI66ebmZ0oZvrguvw19l3wWs6H4CvTayB08MiLpWSbkzw8YxRawArfNBziNCFnR8QcKEzoyALHwUDHY5yjBl9cxULVzf+szuixxTeC0S61XuGlDcU1bL0dIu0rvCfXQHpnpyH6NFkOb08kYAf97wWCkf3wGDf6QD7DwfTyxFNOOtVrv80A2NdvAoYy4j0v/4tAerlDWzp4dY1J5B1ha0+9BsOzDgDM6ngXvcofZfmKDVcJyU2VkvCENm4/69QkKAkrRJ7qARJmKjkW4Lo5fSzkbps+pdRxTzEtTg87Ee6aU63cRe96fIT4JHtA3D9GUahOQ42WhBgYPzefBzW1sRSAaQ7WNQzqKj3ivOXwCM7BmBudxucF3UiQNA5Il+DxaNEQvu2snjtir+C++6/G76TdfUOEOkObUEFGhLGwrFu+ze4TBs1spB8i1mIQHv9yQXYlZgP73jeSfHTy+ecDHDBn7j3QCrt3D94L5r+5euLPgIX7bgZfnvi3wJswrspCtIdkFyi7XPZgY6xNkMCdERQ7opIt1onjB36FkN7+hnA69eXdFPxxGmH6nN8biDoVsm02v1EQE/3zLL93aKXx5h0o5DaD37wA/ja174G9957L1x11VXwkY98BF7xildEfcmW1WlBVVG64NFR0qUf+tChMyF1Wn55UWKJCA2J5LBDSRV7EquJ1YJEWowqaUeOlC79v/OBmRfAm7P/DGfPmwEpJ3HIh18XckSYLDF1cmvSzURbKJGVWJekpvRVRvoIOTUvrtHMbE2kriqkFmR0GWCFFvsLeTsCD3Qcgbmsvkz0pW4EOpefNNf5U5fViHRTpdrsF8Ng7/+KV8HmmS+CodGhiEh3QE83rgkl3M7fJ8r61ETYIs4PCLjQmfG56bhfUiw5NPvmDg9XQLov+3P3T9kP6CoXNvJR12a7STcfGValFaEuwzWnn2UWaVhSVZiJyOom6xijb3X+MTx2+Ci8qpeE1HLBvhXFCG0WiHQf8gJjTLp96uUqWRBBXGpAunsWeIiLsS6DhXbQev9YMNhfgQEQBY2iYgTRrFkiFYR0x2L8Tqwl6a6QDOCurqijwZNuYxyQ9rMmG43QS2RrHX7W97zB88vTcMub3PFkkYGUWoo0jr+330Wo5E4l9CfbL0DqlWBP9yHfHR6kAXDWkhnw0z/3RrXWN+60QtLN/G4ZY6RnHrwx9/eQUwXZ+pJu4/9SIRzHgWLSTYl41+yKhc6L55XgY2tWQyaTiZ9eju/JS7+o/4o+H+9jE+l+InMO/Gv2w/Amp795e7TYpVpPt6X4Ss8h0s/t/ABqATPYJ7RO+F4lU/oM+6YjsEJAvs2N0SuOOw1bjNCz1btZjpD13UUterlnoW+fhx9+GN7xjnfAwoULHcG0l73sZQ4tC8XU3vnOd8KCBXVURVtWlwVRtOgCR2qbh4ZHSRgsomHDez1KClKXnRdnSbfuXZYIjGsTUju42FXWNtGoYRXs4QxSWpeyUS7VjCcqZsV4eF+gsyIkQ7anuwrSbRsZho+d8gK5YeiMJqTGKblmP2pZYjWqLy66vERQupA93TakG62vw98jGsqCZqOac5fZGmnkcgqRbi7gVhYYq/1BWggVke4g074joKcbVfKN7+ckkyhezTf3C/ahKkuqBIgKANwIkZuj1iUS6hKkjTDe7wVbZWMa7XO6xZPuJRe6Hy9+u+dPDd2DI3lvj030uM8eTezIgnRj8kDvDVKXnRdnSbckM4KCYrwjK9DUK9HLsZUH7a7iObD3qHEOfC/CaJz0/lfyL8hEI3+CSTd/3kYVI6q0IwTFLgPg9e/u6nF7kkNraVQU9eQF4JyPeSW7LgHniN6fhdh6AdAesC74bDyeiRXppjY4pCUTgKHOVbXpInWbFpir3DKlJ68EsDrJ5wZORojCojGn9vgKwJUFYMXu6BnLPNV+tlfLNJwUvrpl5hV1JN1VhNRwqoexp1r08jqR7nPOOQeGhobgda97Hdx3331w5pnuxfb3f//3YV6mZULWlrZXi+mCx8TXq4RFTBjQ0fDKKF1aeOAy/gARn4OcIh8/0tCe7pf8Oxxsux4A1pdVRYnW2NOegXF1kUdGup3nQcS2vZwq1TUXYGi3QbtvwMiwGlW6TTGSoZQX1D1UPC0aoos/m9AmXAc9TiJXjvKi0+6cZSijSozdIKS7cq+iNzKsHOlG61UCXZGSbloHkwZr/p0VrjR1TexCn6iOdLMgywx0aBYq9pbjuLAjo1GS7oDkkv5uSbpF6cI8GDeTbprW4CBRpOpevheohUTvlyjnyLYuSPWk94wojizpJnRBhl5eoaD3+h+49MuV10Lns26yayJNI+Pe3hjrwv713WUMpMiJFPVvY587UvURpWN9upI6GhV1EZgRs6lsZBgA3AT/AXMnd8NDpdOhZ1u/s6esexuT+nc/osXZqs4Ypv5/TKAIGbcIqZn0clE2Wo0svR0jCXj75D/DnFkzIN2G53AifGE8yLfYvobn3pkDLdy+EgQY4Ex6FLBcfrn76EEtg0YBbyjOpJt0WJC5iIk3FsVVUVj7XBVrxm41MiP05BXDd9C6kJ4HoeGhEOggZkTZaLWxsp+L7JCGstFwqs6f/FQXadoCztGL4YvQO7kfVs3CnG2XdQpJVQscGUYtAV3u+5bjSXcL6TYtlDfZtGkTXH311XDttdc687hbNr2sLWUXr9FUZkS6g+Y+1mK28VhabKizrELIq/migY6ZdPO+yLNfpQNjkwJKVVAMiqmiHL5fjAVZfFY3VkV1wjDL56xKpZKvEDIlc7oZEmYi3UMF7/89WDzd2Sv4zKEMA0Pd122f+en1q46VIcsyiVRtSbcWaTGF1HQSlYmeRJGoiYls678nysUIG0Uvr4B0Y9DijVLz/94kVrNsVmd1enmQVVMvpzEmuH/U++Ih3QLrQvsU16Ssz9pLwinIsdHLvSJNJjq6QAkD10bg54moxhYhNRlEtwLSjQngSdc5Z5+CTxPdH5nIlqH40QJACxpFqDYWOakYwRSpKVkTSS5tM7pD9HTjs22d6HUSbnSdSLl/Yo8nYFVmqP696NzaEgZaF9SJsCTlQYJhjU2i7Ay8Xf1j8FTpBIC5p+jxmraCRU1sEUx0+R1tQzM5etkQBoA5674d4OQX6GcOAknGcv7CdTSkW/1sc00I7Uf6NBVpVFG4mtBp3aZp9+lIYz3J38zp8fxTaIp5kEij+Xd2V1Es2dUuEOdWQrqR/YbThlTxTe8X4xxtz82CdaVTYUZnPSy99ur0cvo3ml7eQrpNC3Vytm7dCqeeeiq8613vgqVLl8L73/9+WL9+fX2qny2Lzaj6GES5wQs/UJgjjJP29Z6ysTpG4EyXI24P0So67xdGm7kC4LQXA5z/BscRaAddDKaX04UeuoqOTk9T+ljC4KxByetHZd/H94cepV2CXl5pTrdNSM3YC9t6zofbC+fDD3teB6PQ6Txr2VixUAgDT7rHPTSKElBVWefFIhmxI9qrtSLd9ikAhFyW9dnVYkQNLUO6FS22d1HZhS4v0hLQLxaALpi+Y/+QG4ycvqivDnp5QNJNgQ6doVJRB2aiKt26FcGyJi/+N3ek4mv+h+kilJ8PKqx5+6VUx7pYkm48Q7wYUdaOMEU93Q7ik7KK7D3ccy2sK66C2xe9PXg2c1TUhRA6RCpJX4QVI4iWLFsArox0U5HVTASQIULv2flKnOvQ8GRMDIBDnuq5hX5Ofk0W0a28Xyg+MM/Rzn53Xy+f3alZYeGR7u7gVicb0q3GrDZk9F6NtHvzjJjFrHjp5aR03+PdV6ooHKS5Evt+qcKMoFaroBawmZ0Zja6GVjCnexBp1Lzwbq4T20vE9iK/NxW6K2g2UA33Dt3ZM7rqAAwCkW6mON+il1e1UCdnyZIl8MEPftBRK//2t78N+/fvhyuuuALy+byjXP7ss8+GebmWxWy8L4ojk7qnuy0upDsXCunGy1KkMKOpjoYjwp/1mu8AvOxLFXuXqTqJ9PL2etZFo1GW5BKNLi7lrKjPXQ7pDpgXG4R0GxdX/wTAW3J/A3csfpv+WqR2BJuCuS7SdHnfZ+0I1WaANiK5DKqil/V0R7m4OgKQbkrCexd6wYfa3x5deOqE1HjSzwNj9DM7j7jv3wXL3X3eP5a1Ir+VX7zKyDBOo1Vfy0oGgEHjwtAWngXwrnsBTntRoM/lZ6anvQ56uXFG/M/WbWWT5KdavZwn3Ua/9r6xFPxB9uPw9CpMugNGFkZFuulzTHxpL3MdDXV+qcA6FUmU7uk2kiZihyBCR0WBSFMjeMJARmg/zvS2iGdNSe9yjcnlzn7XH6yY3c3WJWxPd5uXxJlTAAL8DT+rU8kACIrXvnnf9vqTbkqiMF7AlhWySaKX93qFcUK6JVkRaPQcVdaF7kKzME7FCGTaBLFtqhpvEeGJdgWkm/xcVyY9ZboraLY4lv/+MztJsDYmn2ujl3P1chVbJhMRCs7HqEU+Oddddx3893//N+zbtw/+4z/+A373u9/Baaed5vR9t2xqLKjvUot2pVM6AIxURbdVRvVYna4ytIqq+SLK5SEudA/pNnqX1UWFSFRdve5YES5Lukc9R6UDZ8UAUE7QYWALBMbJUq6m/sKgYgSty7xe7/9HKkYQrY+vi66KdpZRZ0kASoy6FnJkWLWe7kh7xQhiysa0UNLNmRFaGXXqhNS4ki5Pko6O5TRj5Jylbr865p6hg0C60M0gWI8jmeFqBLB/o5Hu9NS1IvDgk/vc4YkcjCqUlwTmIjEjbAwArvBLRZyhvR7tXlLUp2akWwW8uYKvGHFEIbo465jOGe3vulEXPVan3Zp80nmVGV9ZW+FK93QbaC2xQ3BdKHAOLRgWRI0dU+PCuueUiSHhe9MYenm6NhaNcUZIUG7JrE5vXcIi3b67yI5sm39vWNKdDK/qjr//t+7f4ft3uwYqCO8FGU9sOZBCQmoOvXyG734S1dEIQS/XviOA1YnnjAp/5IdrNu7zfUW9gF5mxugRo5fXqBnRptkgxTLQDdcMWZ3Rk+4AZgQVgdssSLf6OS2k27O6T86MGTPgz/7sz+CRRx6BRx99FK655pp6X7JlEY0ncPxQ8bFdF6xw0aj7t6qLOJJ6eQC93KiEcdV0EdMiLbVd6CaiSwEgKl22K2dVV3LpQ6Ns6zJZNrtcggFQq3q5F/QaY31UwjSrq02vXaRAh2h9nNJHySUmCxql89PL5ZRRaxsZpunlJQH1co5089cnaiwfB6QSPz0CaqqRbgu9nOifC/raobs9rQPj0H10QWJHfL8YCYOokm6NonvdrKDIlXIf2NrvvL0nzOmChTM66hBSs+gi6KS72+3rxWdEwbBDm3xntaE6GoZR8ImFK/578+SSkPjYkW7cJxYataejkRIsXNWGdGOykAtYF3q+SD7XVoyg1hVkXBnrgnciuaF2pQkzJUh3wIi5o2NZfRe1R0W6eWG8jF4+7i84UkGPtzpJFDtrXBcb0m2KqGEI8dzBETio2nyiicBO2nu6jaKf53OF7qIggbkAerlZGOesTgKYQk9H4D/bV9Sz9DKrwzMuTS+v0b/YhNSo6OAwXYlREqXVKRDpHimnlzP/gtYSUvMsVm9y3nnnwb//+7/H+ZItC2F+1IU5aSakduXJ7qzjx3cdddCY2OjliB6Sg8ZDKSwW5pieR12FRq17l/2Ohgc63siwmOjlvM9Fr8u4/Bi1UHO67etCSTeKblAxIhLqYqOX6wBwZiC9XFylu+rIMPuFTnujr9OjC4cWmCOkG4MunmASuoD9qFqwZNx3cYn06Jq03BrWhaMLXs+lewYiU0C57+DG94uRUHiCYZKtCNXPEJ1jPm/63udcJe0rV83VfjmakJoF6c5x39IBsPxS9+/b7jSKelNIL2eJLaeYH1YMq7m97fqcx6aky9Ego9CJiT2dIxl9kdp6l3nCz+njh0c8ejk9X6Tk0iZ2RKwaX+EqWxacT+XIsCDV5cHxPLuL6kC6bXe0VajRj3TjHk1K+N1adVcs62Lui7MWu2j0fVtCAinJIKTblnSbd7TAXsG7lGKXaj3dFgYAnnF639D/9ChG2shkyDgXqxiVinptvUxfJOtL7EXUy3Fd9JzuWunlBYuyekq/b6bQWjjfkg2ml2s0XNHLW0JqZVbzybnpppvggQceqPrvhoeH4VOf+hR86UtuP23LGmeI0NH9wCvGXEht8cxOpwKI8ccRddHXRy+3ILpo+Qnfz52O9HIeAGohtSh9dFZ6OV8XfyAkiriEmtNtXxeedOtKe6R+1C5/JZT3MmMAaARCFHxPNdKdrNLTTWrUpSgCc7hXiCbNKeY60Okrm3cv2qPr/IBaqWsWernaK3O63f9LCehEXEi3L2HwJ1KytNjaghzShDCT7n2D7vk/dWGfTihwq5iFnEgsGh3kqKLV/DM8ijlX6ZZEuqsGxUn9e5PIEF8jvIPqE1KrhHR3lNGseTFVhgFQGxLFE37OBqECOLJoYke6yedi4cpAohqWdFdho+lkge0FLGgOjme1AFTkgl4Qvdw3YcQ/klD3uUvTqKvQ7nWhwRLPoX3oRafDqgU9PkHLmg2LqJq9yGI6uq/5GFh1jmTHqDEaeJV1obuQ+1O+LhzpHlajYWMr6tE0Gha7jBO9XALp5u9NrcyIQnlPd3dbWjNKIrU6Bam68/1i5AiU/EtJADSj1VyWefWrXw1/8Ad/4NDJX/KSl8CFF14Iixcvho6ODhgYGICNGzfCPffcA2vXroUXvehF8JnPfEb2yVtmNQxkMMDw9XQbiLOHMIQdj9VWrl6uFaEZ0u28+LhvVJmIBamX19C7jBe6Rhe62zyku67k0jIay6n+2VXdxZPuWud0V0S66+gv1PRyjnTzALDdSLoFq+go0KKRsPp6uole0z6K6QAA8XpJREFUjhY4S7dSFR1/d+yzxGB4xhK/eA0FOkir1ki3NL28tgTTRi+n1hUKNrRQVOik29LTjUFxhYRBtEhTI9JNPf7ImuFJtxfoeOgCBfOhNC5sxQje082f0WhfkZ0YUTnpJop5dqyoA1K+d/DZgpg20Xu6WeJrILo86RZdlyr7BduJ8IxggsB9Kr1neI6IPRMpuTRaMPw+F5OFhC/po/eDjwScisK4HqXGqNO4RnTG+V0USWDORi939gtNGJllR3SlsoVahfdIA4Cvi/ocNVfeetVK+MCPN9QhjtumRDvV8xzZAjByIJheTuKVkgWamtTLy2MXSrppWg62O0Wilzs/AO+jQcO/ENLd4xaRsEig1oX8vYh+EX+GmpFuO708cuwfJNJoAgbGWOFx5cOkyK7HdNL9lre8Bd7whjfAD37wA/je974HN998MwwODuqLBOd233jjjfDwww/D6aefLvnMLatgmDxigOHrATKSXw9hKEWc6zhpV6PG7zNnNJ4rCQup1UYvt40MG5rI64Bjng/proNeblUY7gpEukWCPx/S3R5pZBhVhpFG7VVOowQ6ln5UjnRTP3tZ0i1IFw5RjAgaR0JCamh4zroqb79yw/FXmHSPuhTkMkqfFhUjdIHo5VMrpEb+Y2A0V+ZbCFWtm17uo1GPe8GgD+me8FXRp7KnG43EaUYYomKj9KGhz+mECEm3bwIA6+m2BEOyPd210WKJ4nkUcr6glytlZwIYSNGRbjadIGCSBt6PInThGud0032I54Yjc/yOpmJf7Eg3niFaL0MXQXwEVI3JJcYuOK4L3yMq/qI/xuKVxwCIuQXMQi8XHaMWInaxzXU34zndpxt1DGyOnes7Pul9D9F/DRioYoSkjgYHc2oUUvMh3UxDCHOS3vaI9PJahBoRUJgc1OsyytBkWaS7PbIGAD6bN5qvDiE1viZYIOSxi4F0015tayXd2kLtkPb2difxxj9omHSPj4/DnDlzIJOpftm0TN6cyuxksJCa82+iOmk8VCZdmNOo0dBJZ4edy30i5+4J6gueunEk5aNpjqh+bqQg4cWme5fjUi+nIJmrulNPt7Cqu9fTXVtflJlc8raAupBu2i+E4jovzpBLuug1uiCJXLLgvEoiRRd6kfVrI/qk0aj2lNPGgcsWCV2YuRzgyGaAozu9r+mLq6dsv0wXIbXzls2Ex3cPwgNbj8CLzllkDQA9saOo9PLx8mQB6fhOMYIudD/SLRIY1yhcg0Y0Ro50UxCGPX78fQvtc61INxsZxp+xoUh39XUh/0YoEAbI5GrwPSOkm9CzuqmOViE1P9ItVeisdU4377XnqC5vOaI9Eg3p9rdglPncsX7ruoiNgKqRRs3ZcKh5gueGM64wiaoP6bYUr8YOe8UI9LuNuotCiMB2WpLuyTLmYj1jYI1EipTuT3+Jq1yui6Gq0CnNRtPPVWvsUg4uEetK93RHoZfr/uWAkYS4LizpFqWX03uDe6VK4d2mTcRR+MiAGxovZGJshKCJE/OSEmM5vVwXO1tJt7a6lgKp5gsXLmwl3NPIbIIkZm91ZIoJJZeUIJhzug2aKFeSFLEaqY425JKo5XN7XCdR1/zySvRyW0+35OiaEPTyIHqnd6nXWYygQIZEwkzURRcrlHq5nkctmHTjxVUlALTN6eaUalwXLUgSZV1mneB+HGCzVrFQVYZ0+0eGTTXSfeUqV1n9HiUShuad8aQvoTBHIlU1Q7DHffGjbFwYF7fxC6ll0lM30z2op5sHYZgwREaj2mqhl/vXpTHq5dXvfKJ30jgds384njnd1YTU/Ei3SJ97yHXpaLOgl+oc4ffoGSMh3TqJmrD73IAJAHJJdzh6OS82DI55STf/N/UVxkcs88vnlfUuZxtWpKmRXs4KMGVIdz3jTk2dHip6nv1qa9GPkO42CZ+r6eUJgGQqtKinPkNqXcj/jESmlwcxadrLijh8PricXkT1gp5tL+hxZlxIrZ69wt8rOk+JpGK7+oXU6D2RcrvNaK36wzFmFID6kG6jhzhytcuGXNqQbkNIrVNavbxK/4/u6WZJtxauURd6LMqoNno5UkDLerqF1ctpTnc1BkAAvZOKArgmdV3o2ONjBjocdTHo56Jq1LRPq/Rz+0aGsXXhQQ9fl0gCc7ak2yek5t8vetalyNzlPECpUFOR5uwlM7RiOfWd0nmhAJCCRHOsTVXrnu+eYzw3/Vvdr/F+buf5THo57ZfUFAupldPLOdLtK3SGHdVSSb1c08v9ida0QbrVnqCAtDzpjqOnO0hIzY/gNQzprmG/2HQPeCJVl3q5WYzAj3QXcR0NU4xQGtGtoQWMnoHWgpBu846OhHTb7mgcs4fWMz9wNJZIMQLvFfK5NRYjbHuFvlfXXWSoTZfdk8a6yOqu1DYZIWgv8JYePwMpJnq5vhM6yvaTl3RLIN21MdGqCanhXWTbT3XNL+fUciyMByDdmWQEH3+MWivpPsbMllAH93RHpJf7kG7W0+187JgCIbW2mvt/KFkwK9n1Id3h1Ms5kixhyRov9LRlr+D60Br46OVRihG2/cJRF6OI0xB6Oe3PkMwI+v3xXsHg0CZYEkvSjXvJSC4pKaEiSazGRVGqJAwzuzL6HBGqa6ILxN6gwk3NhsjBsovdz7feUa5cjmZQhvlYn6kUUvNG09h7up1njKqNUGlON33PSLRk51HXNnaP/+60FpPsd0efrMWQ6urpDhJS8/e5i4+vDNGOYEu6+V1Zn3q5vwXDm5CA6lIzyoSO5BHd2lqd+HtD+4XTy+tHui308pFD5Uh3I0Zj8d7lGmn3PoE5w+fWFbsEId0W5qLzMyRberTSfaZmnzvKfa4Rb/Za/HJdomG6qNdmSbol1ctrR7qJmcj3Ai8I6Nnl2byjnRDK+F1I+4VYjDRGzWhXaPV0l1trKY4DernXQ1ynermmCw9XQLq9hEF6NFatlD5Oy6VEykM+VC9qXT3dIed0S6uX1yheQ5c1D/7M0Tp1JZe2dgRU5XZefEbZfvLo5YkpFcay9chRTzuuR110YbRZK9yPg7u8rxF7xKIYq+d0S/Yu13Cp436gPUMBsYm6dNQzeu/Eq92POx8s3ytoRjFCVGE4xH4x6eVYuCLFWJ106z0VA9KdrSykJppIhShGdBm97hw9xDPkFf0khNSMQqfh76dSYM4mjsWLAnWNxjKLEbrI2ef2gwbM6Ranl9eQSHlUan/SPdNko9U11pOxrkZZ0m209Ij2uocYAWXdK+rZTCG1aEm3MTKsDEQh2r0f6Ra5i7QWTXWKtieSVj4xgvZRd1s99PJKQmodrO2nEerltbNobAUYKkygFg0VIhB74qMcazKOZGukm8Utvv3knt1WT3e5tZbiGDMbis1VHYP+TTi6sCXpttCR5MVIwgmGcdTQDDbaYqeX1zKnW1i9vAodafEMN9DYe9QL6LlgWv30cks7AiXgGAQaSLgoupCrPYkiRHdA9RT65wtn6kcX9Dka9S5Vqma325BuQUSXgi3syaoh2CHU6ahaGxoJUje9HK13oT/Z5nsFzUgYiKotI6RWO43apJfjWSENPko8o/d0q8Qa9wcJDelkYa7/GRuJdNdwjmi03pBq5dHFALUWbVIjwxwhtcb63FrndLvPQGfE3qdbF7vI+L2rs0VUUDzFvcs2BoAe6al0V2Lv6fbRy/13uCiLho/GqrmnmyXdRoJXlyJ1GdJtavTY6eUiLWAhkO5uywxuE+n2hNRy8Qs1Gmr4tCdFWJ0hWp1scSyJBuNoXNwrxOSLJDBnFvU4vZw/o6Fe3hoZ5lmkpVi5ciUcOaJUDpkdPXrU+V7Lps7o0L3/B49XEFKrt6e7gpAaSxhElS5D9ouRUf8yOSW6sGJBdH2q7kzsyOjp1mOWpIXUqqzL0lldenwaidaQABauGb5v9Qmp9ZXvF+6kjSIOBd8ySVTtycLcHvfiwNnL5Ul3DJQ+LhqG2RnfN0jTyjRQSK1GETUz6R5SKFR5AEj08jrQKDo7RF0zL3QD6W6bcqRbBXcK3SbUwyZ2FF69vLPc1+qke77/GU1UNzO1SDftFUIstdq8WgsqhtatpGsLTun7yPopFhuAdNfed2lLpPQdzUZj1TWn20S6tS6CX0iNM3hErMZJGjZUl/wv+WPaz5H6UW2xi49e7lfp9gTDBMfuYaGzimAY+Q8apVYpnotFvdxsHzFHhqmzmpK4i4ihV0XRPZBebuho2KZK1NfTzSYUGNM2vDGwgurltdDLLXvhyCiJBrc7DKO61sXcL3zqivN9Y063ek+ktJSb0SKdnO3bt0PB0ps2OTkJe/bsieO5WhbRTpzbrQ+aN3rE76TTcamXIyJFjpIooMwZ6ZmOEkqXIejlzmxW9Qi6v9BAuj10oR6V7oBed0OZWZReXipBqsY53Rj8ze91/82O/lFrEFYfomuol2PBgxJMTC6N/USFkKnu6Z5XKelWl71WAY2CLtB+KBXdPUxjfHA9EG02lHTzorR7SlRqGzZuJlKUXBOCWFfCYM7S5eJyzjPaRaBE9kuh9nWZoZgRR8eyvkCQowqEmIUWO3KSR5pnP2YkCwrpNkTDtL8XCQBrL0aYe8VkF2n18kg93VWQbv6+FSYb0NMdBtH1C4bx8+IXUotBddlEunUrQtbxx1pITRzpDpF0qzuaEDqaMEL7OTbW1fiAN4+avq+KFKICcyEo93y/kq8193JdQmqI8qPhCEssAFdBuklcVOQuCtH/b6OXm8UI3b8cm3o58316XfxIt4h/CVHQs+2FQ8MK6VZxTX3FCGNdsia93C/MR36thXR7Fkrf/mc/+5n+/De/+Y0zMowMk/Dbb78dTjhBiQS1bErs/3vZmfDDdbt1kIMUki2HRq09QNGF1Eb8QlBYKaYEi5Ka/ITuRc0kpavolZ1RMpmA3o6ME/zhn/l9HWVqtnUllxTQaNEaY063sW5moiIn0lL98lo+uwsODk86itTnLJ3Jns1PXatLSA0ds4no4vcMBUwdeIr0RdWJdCsqFl1Y9SHdKrmkYIb3FvJnNHq6RZPLkEi37uk2RH1sc2Ujix3pAg1V0Y2km4p6osWI6vtlTrf7XEcUHZZ+d6JAus8Ycb9gHx3uFwzuMCDGoJf2CwXMRk/3dEG6qU2DWBFEZaa10GN/opyhakGxobTbMKS7hnNkFVJj54jOUiTfQoH51t+750gj3bPKmRP5ca/4PMVsNN+6qGfyxnq2++7L2JBuzrqi/aLOlseiEfAtNWqumIUz9LWI4sY6Mgx1NDb+FGDrnQCXv8ctBPuSblYALhY9UU8Jn6un0VRPS7or9HSbc7ppUk20ol7WXqAudU9PpNsSl5jjcXXSHYlezujj/dsANv3KnnQ/80uAmz7Jerpb6uWRku6Xv/zlzkekKLzxjW/0fQ9ndWPC/a//+q9hXrJlMRs/9Hjw3sdo5pTMxEYvx0PH1ZgNOpJoXxQPsmq4vDBZoKTbjnTXkSwQdY8CHLScJelGMZJCTrbnkou01BAYL5vdBY/sGIDdA+NWpLs+enmvd5nie0X7Biv8+Gz0fXzmvDAapXu6q68J9RBiEoWiWOjvPHq56zLrQqOwGJJIuWNjnKSb9RZW6KPjbRKxGReHiZJ0GwFgXYFxGb08oF+MRoZJolG8fy9kkYaQbt7fF9nnUjGCkm5E52g6QRch3V4xAlkRVKSZbki36XNJAI/aJ2JDuvE+cAJ3PC8l5+skviVSiIiI6FYfGVbnWJ+7Pu3dx5peztsVJhogpFY7eqlp99kAenm6DhaNjl0U64pr0yCThph6ikXijSMUVOmuYU0QMMD9gGeH9og5FaYuIbWV17ofdz3oZwFoITVepJmQbXUKMTLMhtZ6gsEG0p0t6Hu8rqIexXT8jKkC8XRBusm/0fPgpJH+0Ukfg882baNm43fwv5/nfZ3Uy0mDZHAnwJ51LfVyi4VaimIR+0qKsHz5cjh48KD+O/5BavmmTZvgxS9+cZiXbFnMlmKjWPDg/e4ZFdAzJx2ZXm4m3YR086Rb96NOMHq50IkbO+Kv4kegOtJFPovEs1T/SySk26HbF4053SzpRpsc1olth0gvau3KqHxdqBpsIt31IbrKAdOe4f0/zuytHvb9Ef2zRarFIZAoSroxeaH9QokUXeSLZrjByN6j7FIOY1y4Z+SgHelu5MiwqPRyI9DRYkex0MsN6hqnxlpQdhm6cA1Jd6+7dv2jWSfIoefqVv3/aHWp3fMiDKHc6HPoPdMMAA/RlVuX6D3dZiJD+5naJ0IZPyOkWsfZCehfWK97w/RFalgXs3c5xwol/pFhdfTooq37hn9EIxomS7RfsDAu2bscEenGdcEk6UiZkJrHuqLRn5FYV2S8qEe+FxPxnMcAkGEX1V6g8WsAFP3CuG116kWgzVjqPRP5FixY0bP5NCXGhUU9w48M42gtje3SQmrqrkafHLpQY6p0ow26DFLoW+zFNrkxZ93xZ0wHpJuEXklgDlueqKY5q7tNptcdrXuO+/HsV3tfG97fElKzWKSl2LZtG8ydq6rs09C+9KUvOah7R0cHXHLJJfDQQw/B8WRBomBUBaubXo4XE156tqSbKun5cVlaLB56UjmmCzMU6uIXUpurepuxKhpaeZlQBKRmUfWcq5fjBUYBYHakLFGRSKJKiKRWEWnhz0DUrHKk2/34X/dsC0/TwgCPKqC+pJuoSKx/eXJIttc9hKAPXpx96lIntIUuMbrskSGAhrT8SEbBzL7HANa+vyLS3ZCRYXXSy+NBumkm9RjAEz8E2PRLI+n2AiEMcigw5jTuqUB0Z3e1OTkevk0DY1k2PobRyxVNNVrSzWZ1m6wIgwrJ111mZFh0pLtcSM39iOtGQWvNppNb/M85OzvBxwAQnqQRZk63VvhXSRR7zzraULyyjjPEk4QlF3otT3RHGf5FlC2CMYIWxwonpIbinvRsJtKNW6Vulh4+G32OxV9Euum9Gz0kK9IYohBha0ege7I9jsI4V/ofOaC+xhJtjCHoOZ0EszRNRoa5+2nzwRH4yfo9vvNE5wtp5gRuD0/m6lPpRuPxLtPp8fnczBTri+jpIlkftRxBJfJ9Hr08V/90BDIS9cQJJCdcpZ47q+ODVtLtWeRoBfu38Q8h3txuueUWmCr73ve+B+9973vhK1/5ipNwf/7zn4cbb7zRQeHnz2eByjFs6IQxecTEcuW8btiqerpndrbVR3UkChYml0jTosrfjGV2pFuyKsqrsnUg3XRhoTAHfo5fx0SLkqqazEms213niEEOrhOf0817mCeHy9DkqZoXi9ZtjHjSvUnq2U6e76HRj2wfgGtPC3mGcNwTFiKwQMLpfGS4Loj2OsUIkKdo1YguYBEGA79Dw1k4eb5XFaYED3vh0XbVm3T/5oPe1wKRbknxmtqpa2g05xOLEIg2Uf+lN6e7np7ubq8N40dv8b5uIt35SY1q8B6+qVoXDEBndbU5SDf6Dpp/SoJZdVNAeRGGGDRELTf2C+/RRWrqtES6DfVyNLwrUjUUCbUZPdvO+2SizUypO1+guy8x7UZj0UeH/JNKQp9aN7zD8Xuh7olFnPLZVY50o2XU1x2ku7tB86hrFw3D35mSBjzbWl+EnSeMbUI9M7GqVCuTE8NQkof+BRcfE4eh3Q7FXHZdai8A23zqWBC9PEpBj5hnY5NeTMXRbef7vS6rcHJIix4Sm3LqRoZ5Z+KvvvcYvPz8JTCey/vuA0epuy0Nw5N5V0yNEQ5DjwzDcY1Hd3pJ95Hn3M8ZK0Ku0Fl7YVxPF1H38xFFLZ+tUG6Tdh/aaF0ovtUvyuJCta+L2NaTd/99S73cs0g75OMf/zjccMMNTtJ9+PBhGBgY8P2ZSvvc5z4Hb3vb2+DNb34znHHGGU7y3dXVNaWFgEYb78OlxPeWN11oGdUSYXwNXV7Y+6QFfRZYkW5P6EjAEXHxqRp6dfoC+gtprdBB21SrazZK/CnI4Ug3mlbqxuRSUNQnRFDM0ThywKbA3EvOXawFkSKJqVGhBteFIwsWBMJUH51KdGFut71Pt9dIukn1PbTRvuCBKa1LANItIkiodRFq3S9ewoBFO0IozaQ7EkrHBea4aWaEV2WnIhEe/alGdLlIzeHhrO4f5vs4U09gTMUI7Os2R0A5z6jWpZiDiWxOdgRUmDndyueiv0PfQUJqWryS3Qu0x2s27t/McUf0bAzpFu1FDelfTDbIBCF0GUTmEg4qRcXQPUfVPVKrzVgC8PyPuJ9PDAFsWhuMdONYT8Pfx2ocdTcTOYtREvnQtn6PWcRYLPwZQ9OFfS1eI/7+ZfK7ParoOXqQzaOeWvVym6r7uCo40h6pC+nma0NIt/leUTF49JBmjIgUgMP0dKviLzdbu5GNhl6TGa1MMLTX/Rzfs74lvlYoT0QtGa5vXLDQifcyAgUkYklf9wvM1aFevucR/9c521T5wHzWi6NbPd11It2YyH7jG9+AP/7jP4bpZNlsFtatWwcf+MAH9NeSySS84AUvgPvvv7/s32MfOv4hGxpyRTZyuZzzZ7oaPVvQM1I1f2wiq5OF+d0Z/e/JX05m86F/z3T3PEhkRyA/tA9SIwcdqZp8+ywoqddJJjOALq+YdXtdnK+VSrGvZ2Jwn7N5S11zIV/Da/e2u6d+YHTSeZYJdXGlEt6zzenOOEHOgaNjkFvEEsMaLN0xAxIj+yE/cthZi3Ru1FmbXLIN3yhIt/W4azU2AONZ12FnkvGvS35iBNC9llLtNa0LFYxHJ9w9PzrhXjLtqYR+tjMW9sJ9W/v1vwljqfY+p7KXHzniICv4nhXbeqBAezHT7X5/dADGJxUalQze21EtmZt09mUhkYJiDa89u9u9pA4OjTvPMjSe1egl/n1hb0b3dE9MZkOLnKXSHc7vXeqYCQmlTVBItTvPlkhk3HXKjTvrlCUkDIp1r4vpOxKT6j1JZfR7Usky6tdEpHl4zAuqU+rZMgk1SzZbiPCsaUhDAhJIGebPnOp0zlAikdbrMoTIjEI18vkIwUO1J8lNuOcV0tq3VbLZqjB1YHBM+9w2doYoX0C/E/oMpdrdMzI+DImxfte/tvd571cp6Zx5tNGREY0Khv051e6VsOvSkSw5oxox3z0yNA7jk979gz+jxAoQ4xNZaA+pcJtOd0ACxZ3GBwHaZ+pny5UcBwLpVLv7rBMjMJl1g8Uk8/dxWio34b5HkKy6LgTY4hnCZxken9BBOz3bslmd8MyBEdh2cAiWz6ytIEaWmHGCG9ihgrmyfFuffi5n3dR+Gs/2lN2DUfdGmY0Pu3cRJCBfdN70iv+8UzmX+7YcgYe3HXY+72lP+X4miYqNTkzCzI5wkXw60w2J3CjkRvsdpNt5trZuyGMxqFCAVNdc9z0c2g8T2SW+vRqnJbLjbuySTNd0R3eo1pThcTd2oQQSc278e1IpjmNhK8qzpjNubFIY2u/4llKm0/dcel0G90E2v8L9HYyYLtL+MCyRnXD9eyJZ9S4y33ondqFRjSx+oMLE0dEJyOVqZy8mExTHundw4vAW9z2bsdRhniWS7e6zZsdgVMUGWEyT8C3J7LiKXdJVY5cUlJz4HwviR4bHoX9kQjPU6Nk61X4aUvspjKWSGRepffhrvq/n2mfp851KpJ1/MzHusQAlYrrpZrX+fumoye3ll18O080QdcfRZQsWMOQVwPn7M888U/bvP/nJTzqovWm33nqrg45Pd7vtttusX89N4BFNwF333g8j47j9E/DgfXfDFlWk2r0Dv5aETZu3wNrc5lA/88psGlAy4dG7fg2rRw46zuB3Dz8J44/vd75/wqEtcC4A7N+1HQ6NIOshAY+tXwe57fGODFh+5C44H5OisQQ8sFZV8yvYvj3oaFKw8bntsHbtVti5x12Dzc9shLVHn3L+TX7U/dodD6yDyW3hnvfK8aK7Lvf9HvZtHIGXTI45F9nv7rofJto2wRWjOUBC6PoH7oL+o3h2ErD+kYdhZHO86zJrdAtcrahov61hXZ497K7Lrn0HYO3atbDugPv3o0cOOX9HO9rvrssj6x+D9J71oZ7n4qFJWAQATz5yD6SKeTgbE9Ujw7BOvfblI1nAGuljD90DOw/guiRh89NPwdr+JyFOO23fRjgVkelde+GJGtZl9LD7Oz/42EaY2/8kbNvt/n3LM+6zuUX1tFNR/ukvfwUdIcH5K4bHnf2QHTwAFFKv3TcHimvXwoLBJ+FSvBQP74c7166Frepnb920EdYOuHs1Lt+x/Mgj7jnqH4IHa1iXp4+4+2PvgcPw81/ja6SdgP32W3/tfH+LU7dMw5GjQ3r/hLEXYTBT9IvT3XHfIzDWvhOW9j8Dq9HP798Dv/ndHe7PLuYj/Zxq9vyhfsB05P6H10H/0wwVC7CxQfc9emDdY+CCcCk4cnC/fraD+93vb3gy/N5e3T8CKHf09GMPQkduEFahrsq+AXhSvXaimIeXqn/7uzvvRIwZUoXJyOsSdK+g3Tg2BHiN3HX/wzDcua/qa2WSKZgsJGDtrbfDM4Pu3uk/fNB5NlcLyw1Bfn3rbdBXGwlF2wuSfdANE/DArT+G/p5TYc3YoJNI3Xn/IzDasQeeN5YDxHcfue9OeK7/Amf9d27bCmvXKmpojHbV4QMwG7XLHn8K9u+ozALY2O+uw76DR5x12OYQgNKQLGT1e5bJuvvl1/c8AqPPhbsj5g8+BZcZX1v75FEobXRf+6qRCfdZH7gbNg9c5PycXdu3wdq1W+raG6Z1Th6CG5xkIQNrf6VGC1WwvnFvP/z0vo3Oc+XHR337OFlyY5tbb/89LKgOnvvsxlLa2bv33P4rp7B3DSYGpTa4Vb3+6sPD6pw9DM8No3dOwvatm2Ht2mchTlswuN7x70dHxuGuGs5oUcUlt9//KOS3l+DwUXcNHl/3EIxsBjio1g1BlihnnmKTPZvWw3InQc36nmv1YM5dl0fugkNHXuT87A2PrYfErlJd+8O0pf3rXP/ePwj31/B7nDs7CY/3J6EnXXJ+7/2H3HV5csN6APVsuXH3a3fe9yD0P1P7OVp+ZJN7L+7d6dyLSwYegAtxokmuHe5duxbmDz3pnLGhI/vgt3egz01DKZ8TuYtO3/s0nAIA23fv0z6/knUkU5ArJOCXt/0enlU+d7Tf9blou/a6X3t2605Yu1b1qddoz9v5hONTTfv1PY9CMenea6sPHnb2y8aN+PcVDriExdd69kYz2NjYmFzS/da3vhX+53/+Bz784Q9DMxsi4tj/zZHuZcuWOdT5vj7WdzoNKyq4ga+//npnVJtpN++4H/aPD8O5qy+E7EY3SXrRDc+HOYo+/fRtm+F3+7bBshNOgDVrTgv1s1Nj3wPYtBlWnzADUtvdyuK1L/5DTTtJPD4IsPtbsHDuTOjGJprREbjskovhypOVumFMlrxvM8BOgHknngFr1qyp+u+HHt4NP9+5EXpnL4A1a86HHx9+FKD/MKw+7xxYc4Fb1b578inY+OgeWHTiqbDmmpWhnic1/N8Az22Gi/Z+E/J/9AFIrnfRyetueolDsU59/zsAm5+B889cBe0HuwHGx+DKyy+F1Suq96OHscKWOwGeBejsmVnTurQ/fRC+tfkx6OybCS+44WL4y4//1vn68qWLYc2ac5zPfzn4GGw8ehBOPeMsWHMx69+vwVI/XwuwYT2cffIyt4duD8DiE1bBAvVsqR98F+DZp+G800+CGdm5AAP9cOEF58GaczFVj8+Sv3sEYD/AipUnw7Lrq6/L1t9vgbsPbIGZC5fBmjVnwrf2PARw9ChcftEFcOOZC6BYLMHfPOReIs+71jtbtVrqu98AGNkE7QU3oStc91G46bKXOZ8ntvcCbP0czOhud97DW3Ccy9FBuOqS1fCC0+fH6juS6w4452j+oqU17ZeuZw/B159dD129M+Diy8/CKhP0dbbBmjXu6Jkn9gzCvz/1IKTaO2HNGiz/hLPUs31Iu/B97ZobXgzQPRcST+cAdnwV5s7qhdUXXwaw4WGY1dsNa9ZcCXFbessHACYBLrvyGigtxvCrst058SRs6N8LK04+DYqYTe54DlaucPcO2t3/9xQ8cngPnLzqVFjzvHC+JfnrOzBDgjNOXASJkRTAQYATTj8fll+l3q9SCUqPuwyBk1adCrBpH5ywcDasWYMJVXz3Clp6I47gArjq2hcAzK7+e/zTk3fCweFJuOjyK6G4bQBg2yZYvsTzLX/z8G0OKvO8a6+DRTNqo/KTpY7+F8C2g3DZaYugdPYLIb3e3TfPw/3SswBS/TcD7NgOF55zKizZshxg/2447ZRVsOa6kyBuS+/5NMAYwOpLroDSSddV/Ld9zx2Br21aB9tHErDqwquhd3AC4MlHYd6sPlizxk2XNyQ3wRP37oC+RSthzQuxXFi7JXbOAtjqjW4tXPNBeOEVVJYBSA38P4DtW2D1uWfAoufcdTn91FWw5tqT6tobZXb4WYCNAKmO7pp8C9pz6afge4/sgQGn5DUGSxbMgTVrMN3x9tP48CRccvmVcMaicDFaesfHAfoH4aqLz3N7ujeh5Ai+vrqLfv5rgKMPwBmnrISF+5dixgVnnn4arLnqRIjTEs8UALYCzJgzv6Z1eSL1LDx+z3boWXiiE6/hGsDEJFx39ZVw5uI+h533T4/d7YinrllzY+jnSQ1/G+C5TbB0ZhtAP8CMuQt8z5W89R6Ahx+EM1bMg97JGQDDQ3DxRavh+UzfJdL+MCzx+FGAHQBz5y+saV3OuWwcrv3c3ZAD9/f+4nP3AoyOwtWXXQKXrsSyEsD3D66DHSNH4LSzzoU15y2u/VmeHAXY+V8wf84MeNE5cyH9zS87X5+97FTn2ZwztuWzDm37oksvB9jwEMzo6YI1a5SIWIyW/O39AAcATjjpFFj+/Orr8oXN98Lw4VE4Z/WlMLLlCMCObXD6yV6sP/zIbvjpjo3qfa5+t3HLrP8T69dverE7TlrHfAMPwIrly5z3s68TY6OxuvZGMxgxpUWS7omJCbj55pvht7/9LZxzzjllC4l91VNhqKieSqXgwAHVm6IM/75w4cKyf9/e3u78MQ1/n2bYHEHPSb3CI6pXDK2vuwMyGTVjWH3E9pzQv2evyyJIHXpa/bA+yHSyfqkOt/8wieI1qrDY2S6wnpOucnmyaw4ka3jtxbNc5sL2I2POs1CPH3+2OUrBfGiyEP55558G8NytkJgcgsyAhxhkOvuQv+UKiuGBy49542Ha22Jfl0TCfc8T6Y6aXruvq133yD28c9DXg0r/v0Pvl0T45+1yL79UdsgNdPA965zpvWdsXSZVv1hPh8T5U7OuMx2QquG1F8xw90v/WM55FhQmQZvV460r0slw3ZDOGvp5UeSIWaqj13uuDvc8JfLjzutS79Vs9rPj8x1ucSiZ6ajpHPV0qP2SL8JE0esRo+fqcS5YVxsg0rNi/7LRJp/pnev2+amey+TgbkBdHLSudu9nx2qqly+N/qyWc6REKsfzRUipvj7US6BnI+0ERMEjnyEUr8RzhJ93z/HvYyx65sdhVFH65vVG3ysV7z/VX5jp6KlpXUjUKFdMOP4DrYOtC/ZY55Dem0iFf97ZJwJsuxPSQ7sASvh+Ke2D7tnusyk9iXR+HIrqZ7eznx2rFdyEP13Duqyc792Xv3jiAJy1xH3OPub3Fs9y79FDo67/CWV45zBLdc7w7xWlEZAu5oDagGu9o8PFRvlQdxHaCXPdtdmhBCp7jbuAxNTyUXwuu2toryQwfqHXaXd9cqqY9dalTeAuKimfm26vyeeeMNdtAdhzdMJ5Fupdxrsb/97VUdB6Eel0OnxfsVqX5IjLXEm29/qfi2K+sSNAHSEdAetSX+xcDLUus3pK+q7BggOJevZ0enEV7h+08Xwp3HMpYbDk4C5Ifusl+svJ3oXus3W5eG9ichjy5NcyEXxYLaYE/1JtnTXFLs5osMOjMJorwrDaK7O63b3CYz7U8Qn9vM/7e4A7/8X/ta45/tdRvecFNXaTxFebJa+KarX+bpHa2zds2ADnnXee0y/95JNPwvr16/Wfxx57DKbK2traYPXq1Y7AGxkqq+PfL7vMJFwdu0ZJ98Co6ttMYF8QmxlLcx0pKw5jJJhwcKN9XBcJqan5hXIzHWufi4p24QmzHYrL1sOj8ODWIzqR4WJmpqpsKLvuQ97nw/s9ZXVSP2aCYXpMi6B4TalmITX3d0Z1T74bntk/XCZgE01ITZGRUNV93KKky+anTjRESK02x0izYWnkBgmq0fia2EXDuDAVmwDAxf9mqL7hqRwZRiJQGPhRb2E3G42lZ+1GUUY1RfbQ/upJ7z1beqEr0jK4CxL9W+SUy82ZzzUYCT7he7XpwHCwoA9VC8IYiWChiJptBJTznO5+HRoZLdunsRkqF2u/2xFaqNFUL+d3Q86YgFKTzVII5P4nXFQVLZH0hKBQlRltcli/vojqcsj9smJOt8OWQUOxI32OmCKzN/azjikA+sUM2eZGjQxT/qvWvcIFKoMEsyiOIbHCUEb3Dhf15GtDz5kbhxyJwIqol4cTO10xxxtPiYrUpF5OkzTaU+6aIMEmtCAhXwMd0823K1OPHpQdvRdiZJg5KnLHkVF953Sx+yiy3112sVMIhP6tukjii3fp49hhmFRaFSJxC98vIcd6Pr1vGAbH82VCaiQG6yi6h7Wr/8Y/IQGf6d2GqJohpMb9WssiIt2//70n0DHdDOnib3zjG+HCCy+Eiy++2BkZNjo66qiZHy9GQQ2N3cBAmI+PocAj0sxYcjaHnrEn3XxkmKR6eUiVbnQ65yydCY/tOgp/dPMDVkVUL4mKOO9y9kkAmAxoFVAWQLCk2ytGJKd8BBRdUNgDzgMZ/mx1qaPyhEEh3b6Ega3LxHRSL1dJy4Ehd/TdwFjOl4zTuToKuWjjseic6L93WiYATDhB1lGLAmn856gttHo5jVGjSjZHonAsHj57aNSFMwBmLAeYucyfTCy7BGD73dCzD4UxTxVMuo2Zz1WMgrv/fmCn1bfo2ahh58WahSvbCCjnOXE/DcKwI6TWodXURYK/EOvijSTEucuFsgSP/AwF86EMR/egoUI3qXTT+Cf6XPtc4TndelpFbQnmectmwW+eOuDMdMeRRsTwKSuMR1mXakk3K4xTUi9ZAK4r6WaJFfcvfExTzdbJzhEldnxtuKq7uqPbJWOXGu8iWhNMunG/0MQI8sUZJYxFd3ToPW7uD1JxJ6MkfOSgbPEqxMgwNPw9aczr9f92V1nhty6/i+g/Fnmxtcu2Nt1qZGOpCMXxfuGJEeFiupkqTvjC7ZvhtIW9gerldH+HMjw3p74QYJ8CV5dcoJlY3r9xnzOnkm7zDB/vVtcuee655+A3v/kNjI+7Fw4GWVNtf/RHfwSf/exn4SMf+YiDxiPy/utf/7pMXO1YNjr8lCiYgame0x2lKroQpbCYmTOyCanKjujXl0kuw82jRjtpXrkiuS/pVp/THO36R2+wAII+dwIdQXQhJEJHewOVP/FCJ/vEy8+yjqCLHenWo9SGdLFDdn55bRf6KQt6HEXyfYMTTp8yGsYZOI+5nBlRrB/p9u0VD3EZZUGWSNIdsorO2SDm7HL+3uFVEGk8Vie7wDvVuDluc052PiTH3JGBIkk3Prwu0tSYXFoCiw5r8JcXQrrdPTM6OiyHdPMRUDUj3e4ajAUg3ZHHV/Kkm1tbr5VFQ7PuxeZ0R/S7iM6R6jIqdZcj3TH4FpM9wpFuy3sS+36psRBRS9JNSHckdpG+iwYAxvrL4xeGdOtxpyyhnaoC8NJZXY7vx/v3wW3upAu0LmNOd/T9YhRpTCCFGCO5MTYyTALpDndHB/2+M9XkkbrP0VyUrAT72uAz0t7B8bmNQLpr9C2vvnBZGWORxw7dbXWMDOMFh6Axn6o4UMgppJsxD1oWMek+cuQIPP/5z4dTTjnFERXYt8/tBXnLW94C73vf+2Cq7d3vfjfs2LHDGQf24IMPwiWXXALHk7Wrwz+gkG5Ot+GUKUKiQ9mS1fY5yxURXYmLKxzSHZS08GCDnGYk6lq1eZc60HGRU7ELPeTcZQr+8EInJ3zTmQvh3GUzy9GFKMklTxj0jGEW6LSr/sPJEU1fJwrzVFbRsRfs3KVu0vezx/Y6H2d3t/tGg+n5qZGQ7s7qSHcxB4NKVAzPEK/gT9lcd/Y7Ey2WB8a8jYXmD0e+0E00lwWA2Etn822xrkmIdSHKHjc70l2QQbpV8DM5Jkgvp3VB2mWtFFD1/rz3+4/Drv7xsiSB5mbHlnTz+4gV9HKSyQJafjxasTOb14UYfo70XPcod3QZ0m0Ijum7aMxDdKcJ0o0tNH2MOVNGL6c7ul7W1chB9QPmV0S62xR1eypb4/DOufwkV4j21qcO6LiF9jJ+pGspUqFTtepoM+nl9P7lJ3XxShbpjr7m6Fu4L/aKehEAJtv9w2NftU7JsYONQbprLNJcdtIc+HNDFHEma00jZhoV+0Ib3x+GNg1/zkLOfe4W0u23SLvkr//6r52m8Z07d/pGayHKjKhyy6bWKKg5GoR010Mvxwof9ruYVVALutAQenkIpNuWdPOe7rro5b7eqKctVXT3Qi/lx0WpjglNRao16fYc4hHVt2zuF9pPRA+NhFyODQDgPN0K9HLqyeKJ21ShC3R5od220Q10TMou728ObUF9lkbQPHLURTZmdLaFp2oLBIBU0EPwnYp6nF6OxQEqTERijPAguMsy8UAlEMnciBzSHYFGbQssaK18lL6JXB202H6ACcsZYmhidtJNumcxtGcqkyj+/vz6qf1lRTV9hqIUrsw1MM+VLugNQ16UFpv3+lHNYloVv4sMAC/pzpT53MmodzT3c2W+pstLLtUdLZIwROjppp73oHNFzIlIjBFevBo9WJ5AWJBuGQZA+LvoohPce/Sh7f2+to0yNlqU2OWUmyrTy+k585OavUjJbKxGZ6jGwrjN8I7m92RKFfWILVa3f1FMK35XocBcY5Du2gup153mMXvR5S2d1Vl2ptDnUhEllM1wp/0EIt2KqVBU90VP2Hmqx7hFKkHgHGuklS9ditPYPFu1apWDMLdsao2QSQ/pjpFebl5U5oVOlUAM5ov481Oyvcuhkm5bYFweAEairvG12HGf+3HF5WVBcSnrzfITudCVim6tDhp/Z7yjkFF7eFTtF0P4oi6km5DL4X1eT7dPSM0TOyL10elAL0dbqVVjx63oIQWAkQTmZq7w/93HiugA6FkIMLIfJg9vDdy7UyGkxtH2Q8Pl9DEMeLBNA2nxkc4RpzbOMtaI+ZdkVjDp5kh3jf6lu2aku45kQQV3vq8ZwU9KBTod04AVwc8Itz5W/JzT3Q5bDo3CESVYWLfxIrAu6I3oArMojTpMsVP52LFsHvbjyDBDcKguHQ3aD3RHmoVxjnQ3gl4eMulGijm19JhJ9xxUZnYKxNn6ilckdsoTTIZ0TzeWHmmJHByiwrh/XbCo57CPoviXM1/hChHe+anKSDdOpJHUo9FCanUk3WoCjQkwUdEtlJk+9iVf8CecKrZJTyC9fLkg0h0eYMJRtP/9lktg7+A4nDSvGxbN6LTeVSimNqMr5HMvdMc9OoZCc6ap5yyq4lI37tWIIfWxaJEiORQm4wg3WX9/v3UEV8saa3T4PaQ7Rnq5WQHkPXTOD/f+3l4YhVHok6mKRggAZ7J+XDJOdSRkKnJPNxUcSO1ypTu72H1O1+kVSXBHrKc7XDECkyTsDcMk6bBKosz94iHdxejIJfVrBSDdpclhXY0WoZdH6BdbrlRjybiIWt1K3aS8HFQxRursyH4oHtmGA8xk+rkjCO8Rko3vFSm62yigbtJdh1BjFfpwKj/qU8cW68+tkV3A0f6KPd1R+uhM3QzcK+b7pRKGlCq6cQbPVCZRtmIE38tzeymJYoWOesyHdDP1cqKXK+RLrh2hRnq52htP7sERcENle8hLuiPeRZyiG8SqyY5pmrYIjTpCTzfaMtbXbe4fKnweGZ2MnkRtvtX72pQg3eGTKLNoZxaz8PtYBI2UdONeWf0mlnSzFh808jVILyekexoIqdnMLIynVOwZSajR9LsnGDO41d5pGz9SBuBMJRuN7MpVxvuoDPc0CdANT+bCT0XhMRQ9m+/77nOWVCzq7F0PazruLdIuueqqq+Bb3/qWL3DH0Vyf/vSn4dprWaLRsikxuigIoZsXUP2LRC8PGvnEHbhKILqhvIdvKoXUbInLwhleQEC05sjjjvha4MWx4jJLoOMl3ZK0+1KIwJiKEbRfzN5hKkZEQrqRKk09ymRqfq6ZdOtvTwP1cpuoDxdRq7un20woTVqq+n5maEdg8jIViRT6etofhxTaVCZ2VM+6cHp5haQ7rejlvP9zqtD/mpDuehRj8Qz1Lanca0hJd3GiAXThEEh3pjLSTUEyjeYLba/7foWk22td0b2oIgXgcc/n19iPatMi4HuoLvVytJ4F1YWyhvfLJpdUYA6JdL/4nEXOmKwzF/fBJSf6lZGpxYcKfnXThXmRzzJKTeQcRbiLTP9i0svJB0fu0+1dBHDWq1zUu2wMLPV0T8iODMuNhWrRQPvia8+3MiHIMqrIFmmUmrlfTK0ENd88oVudpgcbrRara2wY2h//H8C80wFe+OnApLyk9rlZlD/eLdJqYHKNQmqPPPIIZLNZ+Nu//Vt46qmnHKT73nvvjf8pWxbKTJSDV4599PKoSbetJ9f3AL2OA+1VSbcMvTw80s2DPVuC59HL6+zpRsPRRtxJE71cBSJY9+CiXLHTy0Nc6BjIYMKNI0lscxXb60G60ficS+fZMuVMCZZ0ywQ64ZHueT3tzrMQGsTFSExRsdCGYzawOEWBhlmYUAlnx8guALhYjroWoe8SzwnGvI/vOhqQdNfRptE1t0rS7aKXmbxKukXGqIVPLntCqJeHHqWG/3blNQCPfSc4eVD7J0NJtwTqEgHptj3HDGvSHRHpPuVGgGs/BPD7T1RWL89QsiDIugqRLJgtPGhJtic0uygqGw1H+dDcZRPdJ5bNwHYP6ZZEdEMm3WctmQF3/s21FenDh4cjFGlsxaoAITVRPZoIyGVQbzsZ3dk0fi604d571X/Zv0fPWSpCyWGMoYibwDmaHAmOKQPsJecudsbA/tc926z0cnrOSEl3x6zKSbfa1yV1h4oJhoVko9VaxDkymo02whLtpOsA/twbveszFXuSvpBZIDreLZJHOeuss+DZZ5+FK6+8El72spc5dPNXvvKVsH79ejjpJL9qXssab2aAbiJ2RC+PpAAa1JPrewDXaXaD64xE6eUhqn8m0v2nV5wYX4+ueVmcdI3/e3r2smAhwnn9bOhAZ44KfEm93KTserNRI64LpyAlknY1atWj6/aYTw/qGs625wIk5v6pS3gPf0dEF8jMgF3Np24fPyBHF46YSJlMBM4W4d+PlHT3LvQ+n8FmdBviWO2FMfkxaqEKerX1dGPsF6lIc+LVwbRHtn86Stlp1dNtY8fQHNlYkm6zCOGbjKAYNeNHdfIqQi/XiG7t62LTIuC+RtPLoxY6z31t8PeomDW8V59/mVanaEh3JdP7JQq93ERwzVgmXY50TxchNbN9ZUGff01JhC9S+0o1YzFWUt3lIudocih00m3eAQuNdSEafCTBsDKxyq5ABkBQi9F0RbqJORF5bFglo6Tb0XQSZOk1qYVejVwuBzfddBN85StfgQ9+8IMyT9Uy0aSbELrISbcvsOkN7G3uSYwDlDyKz1RX/7hzfufzToK/ufHUgBmgMSDdvJ+bBcUJCnKEkEt6/TABoKnKTf2G5n6KRC837Z33Wi+uRKkAKShAR0a4dzlEoEOtGSj0ZEvwqEgTWXhv8fneuBYzMDX2i5xIS/i+S04Znt3dBletmhdf0o0MgDf+wv39bawE5Vs6ioJJd4Tk0lYU4YkvJlkkWIhod2gq4txTqtDLXR/fmZicViOgbAUG/p6RTkJkerm5Hssv9T7vVur32WFItE8KtvTQuoRAujP+9/9tV50I5yydGR/SfcKVAK/9ni7e+X/4bJcRkB2Gefn9sAcWyCLdIXu6a0q6lf5IKOtb7P/7O+72swDoOZmq+3Rh6ZmJyz+sOd3397rHQFUy9pxtkIMx6JDp6Sa2W8ikmxewLjzBX5CksWrRkG7mV5Lp8tYRtV/ojpZrAZuMHenuqjd2qWTq3k4oHZ3OTBLcCKplaKE9Co4K27BhQ2v1mjjprlul20cvN2aAMqeJ9HJ0zogYTreRYS89d3EZvbuuZIELqSHCsug8//dYv5jzT6WQ7gjJpSk+YtLL60ZduC04o+KFLjIuLCK93FybIKQ7sgYAV7c3C1O6WpyTFWmJkDBwauOfXnFC2TmipDxy8erEqwCWXmj/nvItXdCApDsksnDWkr5AP4zsjZ560AVOtbclMeprHZATRC7DJwtIpa+tp7sOpJuLNGJbDw+a1TnqzQ8I0ssj9LobSPfrLvEr9detXo526k0AC84s/zpWf9R+WljYL6gBIIF0u+/n0EQ+PPMKf28+hnARU2EOQrqnybhTTl3GQrCJdNdNL69kmGxi0on7RPkXEfaiYruFTbpx7B7Z6Qv9PjhVD9LNtWdIWZ2b2tdJVUSRo5fHj3RT0h2JdVXNlB9MKb8sNkqtSS2SR3nDG94A//VfAf0fLZtyW8JoaohEzTf6XOrqRTUrgJRoclOJeHdiXMY5R0wuMZB54VkLnXEKpy4sd+xUjMCqaCQnvfQigJnLAS55O0AqbUd0VYAmRy8Pj0aVj8JKxzcDlMSOcATW639U/j12kbRBXka53Kde3hZb0l33OUIK6KJzAc5+dfn3KOlWxQIxenkEcSz0KUF7J5aiXiVTARnuFSzSTBekG+0Lr/GL+pjBhg50ohRpOLuIZnXbkG6YdBIokRaNCL7lrVet9NGmTd9H719dNEecM4x9ype921+QwDVQlOLewoAP+RI5QyEQXRNZNtlGvNBpK1zUbYoF0FkamXY93ZWst8M775HO0Rt+7N7Rr7ql/Huku5Kf0AVD28i7qYhdeFHEJk4oSi9n72F7IicXv0REul95wRJYMrMT3nv9KWUADxXZIiHdZgw3VUm3bhmML+mmu4kXLGIzta9TJdWu2Eq6fRZpl+Tzebjlllvgt7/9LaxevRq6u/0CA5/73OeivGzLYrKLT/SquctmdZY5oroROt/IMENcgvXp9sC4XHIZcYzCf75hdeD3eJCMM6N7wj573yKAv3rC/j2FdCcdobMSZNJCxQhSLw9xoZuBhVndrxvpRrGj928KvtgSKUdsDavoYlVRHeiES9K48n850l1nctnWBfCOu+zfU8+ZJKRbnF5eO9LNmTOkB2BTuxeporMiH05HkEm6o80XPmleDzz+0Rvg3I+7I4lMgo/HAIiwLjyJHncF7Kw93YmsFj6cDoguInL3/N118Dc/eBx+sG532ffrKkToF5kN8JeP2b+H44+G9sAMJ+leKkOLjcAWMc0M2HkSjArmbXHfF+p+SJckmRHxI92IXFKbRiRl98XnBd/R6v1zC+P42h47ZaqLeryIZitMi9LL6VmzI06h0/nrNKKXL53VBff+/XXW76VIvTzqFIDZK+2zqNm+ThftozPjR7rjp5fX5XeDTMUuKeVbWkm33yLtkieffBIuuOAC53MUVOMmUmFvWSjDiwIFJfYPTTjqjqZ5gmFFKBZL4enfHOlWYxPsPd0TcjRqLUYSX/WPJzYYGMd64VKPbqnooHTyxYjaAx0cz8JtVncmoKdbwEHThZ4bg3ZMGDLTi17OUV1znmVdSVQ1I6S71KCkO0QAyJNuE6GLhV5eyVJpKKY7IZkfhxnJSasg1VSqxXKfEesoNTREbUcP+UXVDP/iIN1SZ6gO5PKKk+dak27aK1jQQ3ZR7Ei0mqU7o3hUENENf4bQkIF2cHjSOUNm3MTvTVyb2J9b+Re8i5yPkiPmYuzpdl4umXTWJPL0lcAX9p4TC8DFlDu9YjqMO+VmK0yTQnSkkYS1mIqziF4uMnmF1Mtt7MmIRuNx88WIe+WitwL85h88UUbfi1PSbR+dGYthdSki86qSaZaeINJNBT2x1rgmtdC7pFAowMc//nE4++yzYdYsi4pqy6aFff8dl8Hdzx2C11y0vOx7vPKEiXdoChU6m9f/0O1z4X0vRgCIDlqOXh6/uAQGPjQiKvZEiqEgHZAVLEZMhF6Xy0+aC5//o/McqhHSsU4z+qJi7emukHQ79HKp5DJioMNRBRNVpXWJLEhYQ5CTUgmgWCKl+y47QyELU0YvJxZHfhzmdAgVeSMi3RSMfutPL3YSa5MFUDfD6G2/A3hmLcAFf1z+PfX+oW+ZTgUaspedtxhGs3k4f5k/ZuB3D65Zb9x+UY2EmlFyk25RpDsEWwTtq3+8Gm7beABuOJMp9tuSbvQv8cXbPj+YEU26R+1zwus0jCnwCEVGLwNf2Hv/sHgF7d1CkzTqS6Jsuic9inYvlnSrZ8WYDmOE2NcFk+IsId0WICei6Z7uKPRytEve5Z4Vrr9Cpu6HTEmQXu70kpdiR7rJ74qw0SjpVr6lhXT7LfQuSaVScMMNN8DTTz/dSrqnsS2f0wWvn+MXZ7FVSvHQRepbWnV9DQ46K4Po8upfjEg3rY1I0o0IK47LKhWddZFSL/do1OHW5eXnLwn8HvUTiyCX7PLqaAS9PMTIMLS2lPc8Zl91LGJHQUb0cnGkO3wAyNFtTr8vnwIgk3QXEhnAnzAr7kSkDqEjblefYhlNxBkAUfcL9qJe+k779wjpRraI+F4JX4zAIP31hlgYmtt/7rp0J+lmPbuxmOrpnq2QbpH7KMLIMLTzl89y/tgM2WdYIMBkQca/eEk3rr9IMSI7FtyCVoe572EBclHRyyDDVicUDCvmneJVWlqNOmLsYpvx3kNCahMR5y5XM4rpEjmhsXtM3zokvbyS0XmPXKDB3/Xit1Us0rSBINJNe0UI6Zbs6aaCXktILaY53Vu3BvQ5tGzaG1b/qJIuo16oRDcgB91hR+OErv7FG6R5KF3MFzpGNkrsCPsupejlidHD9hmTdZg0culRHTHplka6w+2Xa0+b54i0XH/GgrLv0XsowgAwxEhEEimneDUeGqXD0UYr53XDJSfOtl6odY9Sq2LFpFKkbhPajwICUL51kQh0yLc4QmrSM93jC/4wGe+SpDoqJlZnaUxedTlGWmyjinoYGGMsIILo5ijpNuYb12laHCtupJszRhJZQTXqaPoif/WCVQ6N/IPGuDA+rkokiTKQbpEzRP3cWPSI0b/UjXRXMgYuiY0Mo70ipF4u2RrnJd0tejm3SLvkE5/4BLz//e+Hf/zHf7QKqfX1xUcPaZmM4UHAZEHk0LGq6Nze+CgxVkcUo4OOZWxYJcMgPjsCnQ4DQIK2lgcY3OV8Wpp5YmwvSzOFCXWJHaVnyqhiVKSI6uX4u9/1t9eWiWLJB8WUdAuql/vOUTjl5dv++nnWNfGfIRlmREGxFToShaZJLmNRu69kuqcbdRGmH9JdrRgxmi0IUR2VqA+4ry1S7CRhO9v89DoMzxkmUdmC3B3dlsjLsa6oGBE3vVwhrbH3dNM5yg47rCsSJ4vdIvbo/tULToF3X3uyVfeAfK5IqxNL+LAwLi6iFmMBiAoEkabRVDMSr4Scc4ZEJwCg2Gw1NfVpo17u+lxsF8S9IqZf1KQW6V1cs2aN8/GlL32pr0KKoy3w79j33bLpbRjo4KxLEXRBBWXooG09n7FSbuKml0tSqbWTFkK6h3ZDolRw6LfQW47MRjUuVoX7Jf6kmyPdKRlENyK64PyXgCCDVKJFgj/1nKJiJESLjZBIVRLSIVR+Iuwc3ZBIt1jSHXEyQjWj91A26XZHholYHb3utTAAJKmOJBiGIlyx28TR2NlFaMRGy+ZLousitl+E6OWUSMkk3dTqlJVBLiOODCMLEhoUFcYqQ7olxoURWyQ+ajkaxVkFYaSbhOymW/9/NSBFsqcbfUurn7vcInmV3//+91H+W8umkTVCebldKummSwt7pGOs/qF1SNJuWNItEuj0b3M+jLXPgw5cmxgvLkTmcUQLiiGZKt5xtiOIJN1OOwLE3o4ginSrS1Yn3aJV9GSs61K3YFgVKyTcM9+ezDdXctkAn4vIpRy9XCYA1OsiiLoQ1VGEGiuEdMu2r3jrIrZfsqMi9HIqRshQhhtILxeYuyySRPmAlLxWBG+GVgQqDsfe/897uhMF6EgJ7EXflJ54GaOdbcmG0Mtb1PJyi+RVnve850X5by2bRibqpFkSJYp0x4xyo3VIonRqXVDsSATpHtjufBhtmwfxpgtuZXRwPCeDRrELXQTR9fVFxXd5NYReDkVIQlEmMOZzdGOk9NUtGFbFCtJIt1SgI+pz21kSNX1mutdinQp1EfEtqhUhrejlIkm3ENJN76O0kJoILRaTHK0X0ZxItxy9PP5xp6Jq1KaQmqQYYcy+hZgtBYn+fzZirispxbqSKnQK+lzFXEwmStCTaY2QNi2SV7nrrrsqfv/qqy1zRFs2rUy075L3dFvm+E7lHN0pXRdpevmYK6I2mYk3+COKuZt05wWZEVnrOJTYqGuI6MaIXjYCiRJNpIR6dEV1ERqBdBPqEntyKYnoMrqw+JzuuANAQdq9IeojQi8X7OmWTrpxv4iMr6Qz5PwQqZ5uOaTbpQxLiMDiaKyR2NdFz7vPFx0qdexztBmVWiSmo/0SYnRlOKRbYq9492Z3Ktc0BRpfkUbwLkLrzbRajU2L5FWuueaasq/x3u5WT/f0t85GId2WkUJT2RM1pUrdel2Ekm4UUsN7HUU3Yjbq65ZEurFII0IvHz3ofuyaA5CM7/Vl53R7e1uMAaBHHcWddMuq3ecTVKQRTrpjThZk2UWe0FGz9XTr/kJJUZ9E3gnAcRRX0/R0U9ItEU9JI91ELYdE/OilqDiWh3T3SCDd4/0AJfV+ds+N7WU5jXcyX9BnKn4htTxcflJ8zy2OdEvulWQKiskMJIs56EjkhJHuNpF4TrLQidaTFhL2a2KL5G0HBgZ8fw4ePAi//vWv4aKLLoJbb701/qdsWewmirow0Y053W3NRS+XROnUWB+kl8tQ+lzHXxJJuomOJJDoaCE1oR6gkYO+mb3xCx3JJt1ifZd0jhhNbtr3LjOkGxMpUQEodV6bgkWjfGE79nRLUKgb0NMtqV6O9HIR1WVJpLsB/kVMvZzmLuMZinkcGRWsJZFuTKJEerrpLuqcFa+OBrsfxoVjuitOFki6I4yunPKRYRhyKb/bJZV0C8W6osJ7OPZNWU9aqNe9iS2SV5kxw519ye3666+HtrY2eO973wvr1q2L49laJmiU3Mgi3VnISCCXkvRydXmJoJesij5nRofYLOoiNCnSjfRyEaT7kEjSrXsuC0U9uSE2w9fCftRiTpBeLoN0twuPDMujOr8TGEsl3fHTPxuVXKJ1pYTQhSZWL8czNKurTYYuPDEojHSXxOd0N4tyeaN6unEKAO1LmbtofqwviwwOvCMwbpHwL6VUG3IWIJPIw8p53YJId9wz3ZXoXkEw6c6NQKeYqKcM0u35XIHnxglWyTZIFbPQk2kh3abF6m0XLFgAmzZtivMlWyZk4vOoFV1YhEYtinSrYoRIcun1dC+fHe/l4lixIIh0N2asjyjS3TNfJCh2JpJJVNIpYZBSpBbr6RYs6GF8Bn5F6uahl0u2rni+sCslHQA2H9KNvmVur0DSPTmEHkBWvbwZhdSElMt9iZSgIjXe0SLjjijpjvku8rEXBc7RRDGl98tsCfai1tHoEEG6Re5nZ3wlId1MrFWCXi6EdEsVxp2xtZIF4OMN6d6wYYPv74jy7Nu3D/7lX/4FzjvvvLierWWCRknUqCAVyZmNKkF1bIiQmuAs3cQkLBNJuhXSLUkvn8yLFmlEA52Y0QUerCLaHbuqKyYMOZfSJ9PTLSQYJkwvJ6Rbjl7OqLHNQuljQVlXqiCLdMccAOr+QkGRRqSXi0zSIFYEslJiThgaJaQmUujU9HIBpJvEsQQVqXFkmMhdJNTqhIbPexRyMJ6Nf7+MFVKAt0RvuigDpAgh3ZoVIVGgwVBU+UKxnm4SUhPq6ca4JS8Qu2CvOw6M6Ja6i463pBsTa6RSYrLN7dJLL4VbbrklrmdrmaD1tLuB66hIEuUpXZaaDOnW1FiBkWGFVIdD/O6AnAzSTfRy1fMqgnQLi0DFLgDjQxfiDXR48IGBcdzsVU3pg4IMvVyIAkqFq0mhKnpOXVtOUU/CNEoX87oQEiUxjjCZhAKkIAUF6ExK0ctlNABEBebUyDBE6ETHV8bMFvEn3XJjfXBdKBZoFnq5KGVYI91Sop5ySLcGDAT2y2ghCXMAoE+KLpyTGUdISveYqhSLpdiFFAsK6e6UFlKLOdblexv9bm/McTrprnRIjVJrYosU4W7bts3392QyCfPmzYOODoE+1ZaJWE+7e+hGJuIPXIvJNqdvARG6vIR4TUPUy+O/XIYLaUACYndSaJSaqJCaSronZUVa6Oc0Q083Ii7Yeo0XugQa5SXdQvRyIUSXLnSsokuMr8mRkBrSAJqIXk7ImUiLhmIApEoF6JRiAIipl0u2rqikO5GHORI+V6jnEq1ddCShJ6RGsUCz0ctlero7G6QvEr8YmS5eCZyjkbzrw8V6dDXrSgbpdn5EsQjtMU4vQctT0i11Fwm19GARH69lZN3jfuntyMjorrSS7niS7hUrVkT5by2bRkbKnCMClL5csg3aFeUm22T0cklqbFaNOupJ5eMV3WrEyDC1X2SE1LykW0S8RlfR473Q8T1EESIUr5EQ3isxlE4E6SYKqFDvMp2j7phVgL2ebqELXagYQT53WKDQiZZLZKC9NAGdYvRyoZ5uwV5Ur3e5APMkkO5CI5Bu2Z5ukdFYQgWaRgmpOT3dbZLFiN7YX1py3v1wzl2L7pSQGrXQ+Eo+sUCir1vTy5NSPd0yABPGLhjrYnupxH7xku5WT7dpoSK53/3ud3DGGWfA0BCKh/htcHAQzjzzTLj77rvDvGTLpsh6VGVLAunOJ7zgJlPKN+nIsPidRSHlVtG7Eur54zaNdMefoHVplE6i71LRyxM56Jagl6t14aMs4lcYjn+/OH1RqkdXZL6wFI2aofISiRTRyzMS6AL2/gkh3TO73PdzcDwnlnTL9hcKqZdLMgBS0vRyOdaVFlITVC9vk6KX68J4/GtOlGGRMVB6ZJhQTzedoZhbNKSLV0O5hKwwlpC+CO0V50cInCOKdZEZ0UyFTrROPQZWoJVSsdFwhGXL/BYqOv/85z8Pb3vb26Cvr886Ruwd73gHfO5znwvzki2banq5QE93XiG6aOnSZJMJqblHYlKip1tRkbCKLmKqp7sk0NNNF7rUOBJRerlalzjnopaNDcvLJd1iYiRC86ixQEDFiAmBdcmW0oIFPYW4CCTdMzozeq9IBMZ5VYzokBhfg5MRqHglNDJsXDjpFlFd1oWI9uZFuiXo5YKF8ba0mr0sjHSLCMwJMgAkWXpELxejC0sJqQkj3flkm9YAEDHJ/dImx4wgNprYWM8mtlBe5fHHH4ebbrop8Ps33HBDa0Z3k5ikkFqWdS2ki9nmQroVSieC0KmkG5NLESvK0cslA0CiIiEFVITSp5HuTFP1F3pItxS6IIN0o3VQ0i3RpqHp5Tk59J+hXnHSy6m/XQLtpnVpTxTkfK5AgqnVy4Xp5SI0akF9Edmku12WXq5p9/GvC6GXEgyAkkpuxEaG5eSSKBKBlSheZYuC7CIfA6Az9gIw5d0SRZq8dEw3Oex+bI+/HaErkxbbL1QAbiHddSbdBw4cgEwmOHBNp9Nw6JASimjZtLZuVd2W6C/Ml0owUXL3SYIu36YbGSbgoFPuRdsJUvTyvJiQGvYuSyWXkyVvBqiIermaXw6pdFMFxgVVRRdLuoXo5TwAlFAwJ3p5WgLp1v3c3Y4ieNx9dH0qwRFNuiUCQAqKBZV0RZBupovQK1HQE6R/ks/NFmQF5rolfK5gYZx6uiWTqM5EVk8baDakW2LCCN3R6ZKweGXMSTcajcOSaEfIKVanGL1cMOmWZBjlSey0JaRWZqGiiiVLlsCTTz5ZcX73okWLwrxky6bIelXwJ0IvL5RgUgWAuuetSdAFmocsIi6hq6Ky9HJJpFtCMGyy6D5vW6IQu9K1j14ugHTrwFgi6VYMgM5kc9HLfVMABNo0JhW9PC1xjnQ/t8BIP0Yxl0i6aV1E0AVKolAXIebiVZdgb2FJJZfJRAl62iV8S3MLqbWJCanJFSMygklUFjzafbP1dEu2OmUlC50+erlA0q1iCokRcznpmC47Ipd0CxZpvAJwC+muK+les2YNfPjDH4aJCVb1VjY+Pg4f/ehH4cUvfnGYl2zZNKCXm/PW6zVEQyfV5eVDSJpKSK0Jk+4mpZdTFV2sX6wo19OtixECqAuJkXRK9OjyC72tJ/aXphFnEkh31pl2L410CyfdYxJIt0IXRJJuGXVh6V5U8i1o3elSUwmptTco6e5tMiG1jEK6RZJLzboq6OReBunulBPeE1iXCVUYT4kj3V1iSTeODJPSL2prRno5Ce8JFDtz6o5uk5ow0sQWqsT5oQ99CH784x/DKaecAu9+97vh1FNPdb7+zDPPwJe+9CUoFArwwQ9+UOpZWyZAL8dqMaKXcc6kxNfUfd28F7CpRoYJ0GKT1C82KSykJkcvl1DpJqRbpBeVjVITVS8X7HUXGwEliOrKIt3uuqRLgki3UNLdJ4p0kyJ1rqmQS8k53aO5BFCZoFuiTaMgj+hKqC5zgTmKBZqGXq7VyyXuoqTsXaR7utubqkhDxauUGNItty5EL5cQUqMxsO0SdxFPugUK49Q+ITGRhsRORQrATW6hotAFCxbAfffdB+9617vgAx/4gEZIsVftxhtvdBJv/Dctm/7Wzfq4kGIeZ9KNTj+FAWCieZHuSQkhNen+H4XoFgXUyzOCF/q4CnSQXt5sSLckukDMiK5kruno5R7SXWiuAJBaEQQKemgzu9rEku4Jreou2NMtgHTrnu5cwYkpMJ6IU3V5jvpcZL/ou6itqVp6cGJEokmF1AjplqALk28RSxaEBMP4fpHQXZlQQmopqeRStwxKFGnk5rrn1MiwjFhPtxy9nMbAjkuw0egukmIANLGF9rYrVqyAtWvXwsDAADz33HPOJblq1SqYNWuWzBO2TMRQ1RHVdDHhxlndcc4wddBzSXp5A5BuiZ7urEqi2sQuLoV0h+saqcnaBYXUJgpeT7eI6Z7udHP10SXcBKcrIdwvJlFF16P3BANAKqY0SfCHNqNTTkjNYYwk3UQqdmsA0k0MozgnGAxPFiFXSkEGfQv5gaYTUov/DI0VUoDyielEEXpFRmNJCqnJMQAmGpV0N9l+0YVOCZ8rXOyU7OnOKjZam8RoXLTJoQYIqclNMBK5i5rcIkehmGRfdNFF8T5NyxpqGBiPTMYfGKOqaEkLqQnSywUudEq6sXCACWacfV1UFW0vNaF6uSTSXSKkOy+6LhJCajrplgh01H7pkqqiS9LLBUfveUi3ZNItg3R3a9GwePc6Fr810g3NiXRTsTPOpBs1S/KQcnp09XvbJPRyz+cK0O6LbtKN1uEU9dqaTkhNBtH1erpjN2SGCvZ0SzIjaF2SUvTyBozek9gvWVBIt1SveyPUy0Xu6FbSHWQCJc6WNYtRb1TczijnUy+XpJcLjAxr845E3AlDTiGXTlBMY6xEhNSaq3d5LO++dlpKdIOSboGRYTQaS2QetUq6O5NSSLecaBhNARAJANWFnipmBYWxBMSl+Ci1mNcFX29SKwxLrItcEoUTC8i/xF2MQCYXjZiTQbrlCsCSPne44O3vhCQbLSVILxfo6Z5QrU4iyQLuv1KxAUUaiRYw9wwlJZBuLEZQ8arJ5t1TT7cY0t0I9XKJue5sDGzL/NZKuo9jy6SVCmjsSXdRi/o0m5AaUrSopTDuCiA5aMdEAh35kWESiO54QTDQER4Z1i45Sk2hTyLCexjoCIqGNQLpTjYh0k37Je51wf2X1QJz+aZCun1KujGvCybdWtSz2ZBuQbrwyGQRJuiOpnFNIkWaDkGwQIBeXhBE6GgCgPMDmku9nHRXRJJuKooLFTu1vojgHZ2RSLrxDJHPEmgB6xKc0+3NdW8l3aa1ku7j2DRNK256eRFRl4w/KGmSfjEU8dEK5tmY0SiFXPrUOpuEXk57ReTiknTQmFwKCqlJjpibIKRbgl7OExAJJV1CugVEWsYLjejplkG6O8SQ7oJOLmUYAHLJpSTqMqLo5Y5J7JcGCKnl8vEnl0i7nyBKuUTSLVgYT2shNQkWjbvmKQnWlQYgEk1Ho9b08qLwXSQ4eg99pNQ86ozEupCImhDS3d2e1j5SSncl3RJSK7NW0n0cW0aP3igJ0sslkO7GBIBxI91YmNcMAEIZmwXpFqyiU/9PSgJd4DR+ASE1QnRFaNQqKBbRAJBOugnpFgh0SOxIBHXRgj7tzYV054o6AExIILrCSDf1F8aedE/kIaf8S9MJqQmyi4Yn8zCu+lFF7iLaLxK0e0EhtWxRsABMxQ08QzEq9DdCX2RciZ0mJAudwm0akne0CNJNImrIREvGH9OhkLJY0t1CugOtlXQfxyZFL0eVSAoAJWnUUhRQKfQSl3lCcl3UhSiBdLcLVtFpTndagi7MgwQJ6hrNoxZAusdLgiPmhAMdUaRbVdFFkktherkc0l2EcUIuaRRcEyHd3UICc6iGLksvz8rTywWSBSxGkH+RoZcLThhRBZpRAdVlnxihGonbLGeorVH08rjXRRfDEiLJpWwLmGrpkUC66VwKtH+h9XbII90iYqdNbq2k+zg2KXq5eE+38OVF445iR7qLRWFKX168pxtJEXHT+hoyd1mop1sS6R4v0Yg5iTOU9dB/xXgR6emWQLqVBoAM0j0pK6Qm1tNdgJFSp198p4mQbg91KcTe6iQrpDbZlAgdJqwThHTzXuPYz1H86zKjMyM3614VgB2LW+yU1lmgn1taMGxUtfSInCPeoiHAAJBcF4rn0kVBRqfwJA2ZpDspO2Kuia2VdB/HJkcvZz3dosqoMoGx1CgFXOcJSXRBEOmmi0uCGUFIt5N0x11F5yItEnO6BZHuMVW4aitONN2F7rFFBAJAQrpF+gtzTYt0j0Knf8xMk8wX9vUXTsQbACIFOUc93aJCapKqy/H7luEJTLqbU0iNku4hiaRbFfRE9oswWCCpu0JipyLrIi5eKSikRuKVkpM0BHwLWk+HjM/lbLSURDGiya2VdB/HRvTyuCnDmFySqqMo0i1QRUfzhNTiRrpLXh+dCLog39MtUTGmqmgCUPSsIIh0x78uHZJolCrQiCDdDVLplpkZq/YL/g6xUx0bpV4ef0vPMCHd1AsokjB0ClMd402k8G4TZRdJIt2i6uVIL29vSiE1jnTjfHoxpDvu5FL3dAsj3QL7ZUxyXXShU5ZdJCGkRr5FJLnUd5FMnNvbnhFBuotFvIvcZ07lBVqdmtxaSfdxbFIjJvLiSLc0vVyGGusg3RpdaC71cpylS8wvqaRb5EInpBup5RLiNYRcSiDdBUnqmlwvqrSqu+7pdoo0+eZEumNeF0wuRzTSLUkvb5ell8eMuuBdJJpcNkJITaKg5wiptTWlkBol3chiiJuNNsnvorh9i3RPt6DuSraQgEIpIYt0C6+LCL1cId0ySbcciwatu90Tr0RAKC7Dog+1OqVyAndRk1sr6T6OTWreZRbVy3VPt0BymR0VFZigwHg85pFhBc4AiDvQcRDikhjSjaPUpJAXSqIci7sHSHBcGO//l6BRD9LMWAF6eSIvW0WXQrqxij7IY76mozrKrAsG2qMlSXq5HF3YP74m5mKEU+gk1pUgoiuwX0gXAePhuBMpLG54DICJpqLG4nzhdDIh0tedLZYgpzRG4qeXTzSlkBqyCfDO97QRmg3pltRdafNa42LXAJC9o4leHrcoIa4zFYATEvoiTW6tpPs4tjZFL0exGTmkO2YHjXSy8aPu550zoalGhiG9nFCXuIsRjEZdSsTfuyxZMfb30eVExOUkRNT8QmrxI7pHJpWqe0GSLdJcEwBGUACKCnoS7SuCc5cl1wXp5RrpFhVSa28qenm+UfRygWIE6UVI7BccGaaF1ETGV8rR7rEALCWmhnebXHLZmIJe7MxFhYLqiTSx39GyPlcLEkpM0qBxhCIxney6YDGCCjVxMoxw/xHSncgKFICb3FpJ93FsYvRyB9EVQroxoCypAKRDNumOPTDmqEvcASBDiCWQbsk5oDlJdEEj3TKFCE9ILf4L/dCkUgAVSbobhHTHfIYGx3JQgCQUNdUx16T9hXH73CIMQ/Mi3VIzYx0KckkyuZRDdHGvUEdM3AVgHM0mNjIMi/hEzRbaLzrpHov3/OO59IT3mqt1Ra5dkJLutEyhs1HFiILEWE92T8TNGBEWUuMUc2w3iZVeTki3RKtTk1sr6T6OTY8MK0iql8fsoAnlRgctNHrDowxLjAwTUoz1Id2ppqKv+dEFoSSqCZHuwxM0GisrQF2THY2VScv4FhfZSkA2QahLcwWAUkg3tvSMljq8wmTM7KWGqZfHTS/Hnm5RpHtCFNH1RD1jVrvP4boIFSP4mRRKGPqaGemW7l2OuShOr+fFdOPNVejMyCHduWISsgQYxI50yxbGOcUcmS9xGd5tenylRAG4ya2VdB/HlkkpenlBck53zI5o4qiHcgsIYzkvTSPDsvEj3R69PG50wXOaJaFjTYlU7El3oQh5jS4I9XQLjAuTRLqxd/mgSrplijSyAWAmKdO6QmOCCmJFGll0gQp66Avi9Lv4WppeLkExbxTSPRE3vRzZRZJCarLnSCfdMRf1XFV3oTuav55QwiBGL4+rd3ndNwG+chXA0Z0NG3XqCamVYlV1Jz0Bb9xpcyWXklMAfDo9UvRyQaS7hxTMY6SXY9w8Ah3e7yAxqrGJrZV0H8fmId3x05HEkW6hfm5J4Q1MpMTEa1TyUcLkUqgYIXV54f7LiqELeVl6uRDSjb3LfuraeFMhumm1V4iaGJdRkJ0npLvpqI4eCyVO/0I+Vxev4kYYNNLdIdzTHbN6uSS7qIHz7uMuACMzYkKKds91XIQSTD1jOO52BES6CbmsR7385+8B2L8B4LaPNsy3UNId9x1N8WE2ISRIKO1z9cSI+JNuUf8iLKTGi8Bxtq9gLDTKC8AtinnzJd3bt2+Ht7zlLXDiiSdCZ2cnnHTSSfDRj34Usll/gL5hwwa46qqroKOjA5YtWwaf/vSnp+yZm8F0D1Ds9PIYe7onhgA2/dpzQOMDov3ckiidf375hNxoLCGTElLD/aLRhdjVy4WF1ISQbuxVRMZCloRapGjUUki3YtHEXaChpLtA72c9RRr0Jc/+xt+72aD+wriT7pzjqxIwkewSSronG0IvH42ZXo53W2NGhnU0TVAcK+1+10MAhzcHi6gJFYDbpVqd4kC6+7d5nx/a1LikW61J3OtChVOPMSLUuyxFL1frItEC5o/pJpuqoIdGUwAQEIrLcOpPAVLefmmJqTVf0v3MM89AsViEr371q/DUU0/Bv/3bv8FXvvIV+Id/+Af9b4aGhuCGG26AFStWwLp16+Azn/kMfOxjH4Obb755Sp99OltaiF6OyWpsjugHbwT47h8B3PUZP71cEOkmlC7uflSkInlV0bhHhskiurJJdxHyWkituUaGSSHdlFzq3mUxlW6ppFsW6S4m2+pPur/zhwD/84cAD321Yf2FyaQ3ei/Ovm5a50lKumOnl8si3T1K0Gc4dno5Ty6bS0gNrZNanQSS7rpFPUcOAvzX9QD/caFfc0JT7mX2Cm/rkdUXiehbdt7vfX7oGRc4aIBvkUq6s2VI90STId1y9HKHYaRbKYWQbkF6eVIVxQoxtiOQrxpPCBWAm9yaIum+6aab4Otf/7qTVK9cuRJe+tKXwvvf/3748Y9/rP/Nd77zHQf5vuWWW+DMM8+E17zmNfCe97wHPve5z03ps09naxOil8fa073ld+7HdV/308sFkW6q/sVfjEBKn1BVVDgolqSXY4AgTi8X6un2hLGKsfbRUXKZS8jsl4Rwf6Eu6MXMFqF1KVGAVs9+2f2Q+/HRbzcsAPTrAMSXSJEPz1MxQqpII8aM8Hrd4w6KPcGw5hJS8wk1xi28ly+yuyjiHT20x/t872MN7UWVagHLxqFePnbE+xwnrey4V72eLF0YC3oUu4jSy8VanYQnRuSatKdbkF5O9zT+HnEn3cS6as3qbsKk22aDg4Mwe/Zs/ff7778frr76amhr8xz9jTfeCJs2bYKBAUVJbllD6OXZPKOXx5VEURDcEKQ7oUdZxWlI4YmNdn/Pv7lIHVVDcw1IumNAur9x7zZ44y0P+RKOWMVr/vf1/sqqNNLNZunGGehoGnW9Kt1YCPi/dwL85oP+r+vAWOZCTycl1csx6Zbu6W5vqoSB1rlAgXHcxSvhol4qGX/wR7R7L7msI1m4+18Bvrga4G5WxMekrFSUFVITRLq9uyjiXuHJ11ZVHHdeTz5ZiEOp+6eP7YHX3vwAHB7xfMikI+pZ511kIntbfu9/PaG7yCemli/FzqLJJdrrj13G+gG++RKA9d9p2Cg1Pac75gJNeU+3EAOgEUh3jH6XYrvJVAvptpkcF1XQnnvuOfjiF78In/3sZ/XX9u/f7/R8c1uwYIH+3qxZs8peZ3Jy0vnDKepouVzO+TNdjZ6t3mdMgqIk5vKx/r4Tuby+0Ev5CcjX8dp0PZWSGed1kuNDTh26kO6GotB7lFDrkssXYl2XbB4nDLsOtJgdh0LU1y6VIPPbjzmf5jf+HEqnv9SZh5hmCKDE/qV21PHJ6OfjYz/f6Hz8zgPb4U8uXa6TeBKAymcnoBTxtTMoXoN74/f/AsXnu+uTyE4461JMpKOvdwVLUtCN/ajjk5DsiCegGhyb9JDLIsY5o9HW5egOyDz+XefT3BXvg1zSTZoK2QlnxaXWJYEIjwru49yLw5R0K0Q3nx2Pvl/URzzt5KPS+UnAMCSP3lHIv7Spot7oRDa2tUEfjlZQjI4o61LpXknnJpx1cVBAgXUpqhm6GPzF7XOJRl3MjkXb67lxyNz+/zmfln73Cchf8m63Vzk7qvdQDufGC6xLu9orI+PxxiToc+mOLuaq30W2vZEYO6oDyOKe9fo1EpOj6i5y72wJSyfqv4v+8n9ddP5f1j4Nn3zFmc7n2VxBs66i+pbk+KDjW0vd8yExehCKhzY5a5NUPreQSIvFLojqjqF69Pgk5HLx3EVjk27yl1VJd2FiuOz5a41Jk3d+GlLb7gLYdhfkzvpD92u5cfcuSmZE7qKUjnPjjueKsG9wAiZVUS8/GfGODjBvv2TE9gtBBtkYcwC815zXTLpiaoVxTLo7pnVOFYfV+vtNadL993//9/CpT32q4r95+umn4bTTTtN/37Nnj0M3f/WrXw1ve9vb6vr5n/zkJ+HjH/942ddvvfVW6OpSVZppbLfddltd//+5vXhzpWDHrt2wdi0bbRHS9o4BzG5DKpz79117kpBVF3phYhTWrl0b7YVLJXiZ+hQvkd+tXQvn7twMJwDAs9t2wrPjEV+3ij1zwF2Xvfv2R392i+3clYQVinZ/aN9ueCDia2fyI7BGff74uodg97Y0zBvaAJezkUr17g2b9R9CF52E9RuegJ6DGyK+irtJHn78KZjb/6Tz+fBoSiPd6x66H/ZvioZe0l4Z2vBLuGvyYufzxQMPwUUAcOToENwX43tJ5haI3Wf/1W9ug56YQIxH97t7cLLgXosP3nc3HO5ltMUarWdiDzxffX7PL74DQ51uoWPr5mcAveqOPfthg8C6DDhvYdpJBmM9Q7vdPTiqen8fffgB2Lc5PArYPbEPXqA+Hx4egt+rZ7xucAB6AeCBh9fBkadHQcImJzCUSsA9994Le/CHxWBP73LXBUVs0B596H7Y92y0IOeutT+EVHECxtrdojXaTePDDkn77vseguFORiuOyYacOC3t0Mvj3C/9R1MwS9HLh48cgDsivPbc4afgClZMuu3nP4Bcusfnh3912++hlPCU6eP2uY9ueAL6DkX1ueU2kU1pWuzwwKGa14XfK0sGHoAL1edDO5+AO9Vr0HoNj2f1uYrbtu92/eNz23bA2rVMuCyUuX77yS27YO3aHc7n/YMprV7+2LqHYc+28CHyeTufhhVYOC12AfLxBg7ugXvWroVzdm0GhIU2b90Om4Ril0TB9S2/veMu2ByTb3nOwaLSMKbuomc3PgHPDtifv1rcsXrbo7BUfU7nfNX+J+EM9O1798PjAvtlq3r+/qHhWH3LliGAsWwacm3upf/Eow/Bzh3hmUDtuaPQnh+CVDEHA10nAiTcdT5353NurLtlOzw7JrNfjhx2/ctjj2+Azv2Px/Ka6/e4Z5PEZZ98/FGA2ZdHikkz+RFnbUY6FsN0t7GxsemfdL/vfe+DN73pTRX/DfZwk+3duxeuvfZauPzyy8sE0hYuXAgHDhzwfY3+jt+z2Qc+8AF473vf60O6UfUce8f7+vpgOldUcANff/31kMlEj/KPPLATfrLjGZi/YBGsWXNupNdYv+so/OXND8GFK2bCd9/qJjo/6X8UDvS7NPAU5GHNGgpNQhoKkKhWsZ6+mc7rpH72C4AjAKecfhacfFnE1632Y9fvgf/d+hTMnjsP1qxZHdvr3vr9DTDZ775f82b3Rl6XxN71AE+4n593+ko4Z/UaSKBI6haA3lnznK/Xuzds9tvRDfB4/3445bQzYM3lGFaEt7+8/1bn48qTVsGaF5zsfP7xDb+HbM51RavPOxtKp0dYFxRJWu9+OjN/UK9t4okRgO0Ac+bNj74Pq9j7H7rNQeied+11sKAvHvrtwft3AGzbBNDWBTAJcMnqc6F08vWhXyexZx3A0+7nV521HLIrr3d8x0krlgLsB1i+chUsvT7+dTk0PAkfe/ROKEIi1nX/af96zESgs3cWQD/ABeeeBaUzQ75+sQCZT3oJZW9Xu37G9LYPO+t96RVXQ2kJpRTx2uefvQeOTI7BJZdeBheuKGdgRbGNt24G2L0NMp09ACMAF+A5OmNNpHvlht2fg+TRbZD7s0cAZmHYB5B+yg2grrruev21OK1/NAsfXneH8/lNN73Q6U+Nw76w+V4YH3eTy77OdKS9mLzzcYDnvL/fcOEqKC0+H2B4v+OHS4kkvPBFLwEJu3vyKXj0yB5YefKpsOZ5XixUr73vwdv0ZIS+rraq62KLORKPHnJ8K9qM4gCseeELHQZA4rmMs149M+eK+dy992yHtbuehQWLlsCaNWfXdRf1zZoDa9ZgaRbgs8/cDfkRd13OO+dMOPec8M+f+vGPnBilb8kqgOe2w+xu17+kfvEbgMMAq047E066QmZdvvjcvdB/aBTOu+gSuGzlnFhe894tRwCeWgfQ1g2QBThl5TI4+do1kWLS1A+/B6A6BGlvJO/cALAPYNkJJ8GSm+Jflyf3DMEXnnoAUm0dsGbN82J73S/dsRXgqeegr7cXYBTgnDNOgbNWh3/+9H+cD4nBXc7nhes/AcWL3+l8nvr5Wi/WvVxmv/zi6GPw5MBBOOOss2DNRctiec0tv9sCsHMLtHX1AAwDnHXGqbB7f7SYNPXfL4Pkjnsh/8a1UFrq5hfT1YgpPa2T7nnz5jl/ajFEuDHhXr16tSOqllQ9g2SXXXYZfPCDH3QOP72x6AROPfVUK7Ucrb293fljGv7/uBMWCav3OdvbFI2q5L5WFPv5Brew8ciOo5AvJZ0eNGydIepaoph3R3BFUdUe9HrxE4VJ9xlVj26qvRtSQu9Rh6pcYitTnPsAQVFiACQLWUhGfe3h3frT1MSAuw4lRe3LdIrt4Y5M/ftFWyKhXwP7UanvMo3vb5TXHjnqvXRuAjJpNa884VLLkqm26Otdw3gsTLoR6YprzQtIV3Xowq5/Spfy0dal4FVf00O7oKReI6n2SyrTIXKOOttLuqU8mUrrnt16jXQWEqrPLY009rDPP+bvMUuMHvHeN9VfmG7rjLbeIaYjQIz7hZociHafxq9EeO1EKe8k3GiZTT8DuOp9vj7dTEePyLp0sLbFRCoNGTZarR5DVV4aXZPIT0Rb77FDvr+m0f9mLsZLST1vu1i8QKPUkMAQ214pltxRR0ovIpHP1vzavnsl7/mWxOSwg0pBF+rsuOuSzHSI+dyudnV3xLAu6FP4XUSsqzT+HlFeO+cyZJK9LtiTyI+7r69ablJtnWKxi94vBe9+rddyRTVWKuUWlFOFbODzV4072OzzTCqF6m96v0jdRXN63Zjo6FgO0uk0JGIaYzesxhtmOrqdpBuR6tDPjxekSrjRUrd9CFJX/IX7F4p1BfeL9rOJZGz7RZGttKaDc46ixqQ7XBHC9EP/CXAi8Y2mp9X6uzWFkBom3Ndccw0sX77c6eM+dOiQ06eNf8he97rXOSJqOM8bx4p973vfgy984Qs+JLtlQWN9ogtM8GD6oe395SIt9YhA3foh73NSLRdW0ZUUgSrEIaSGojc/fLN/bEsD5sXGPTKMC3fg641phWELpXdyBOAHbwbY+LPgFxxlgTEGN7S+epSaXBFNQnmZ1rhIzx1Z1Icphw4oWMo3jkRIjZolTXFOR6B1qUu93BR2mRwEePJH/tcT9C8SomHkq4p1qrq3YeJEhn3Mh59zBcPoHAmtS0r1LqMVI04BwH323u8/Bt972GuVyuWrzKNGYafv/4k7rz3I6O4xz1EDRvp0kJBaNrqQ2t6j4/Bn31kHT+4Z9ImP1a26bKoSD2wzhNTk1sUTx6pfYI6rWuPaZLV6ecQeVPIvKunWo+oaMBmhSwEpYzEK741l3bNfotiiHkFC7pfQ7zpfkxU7ndvbpoXURiYjKtJXvIuqCMyhH/3dJwJeJCDeaSIhNcwf/v5HG+Cb97l+UYvk1jthpMj28NM/d8UsjwFriqQbEWsUT7v99tth6dKlsGjRIv2HbMaMGU4v9rZt2xw0HKnrH/nIR+Dtb3/7lD57c4wMi37gdvV71e5n9g2xEVDMgUZRGMZRQ8/+yvv7xKBbFWyAMqrU/HIn6a53ZNiuB/1/Hz3ovwgFk26tdh8x6UaEhYwnqBgs66Tbdgnd8zmAp34M8P0/Dn5xKj6YFxcFxg1IuuNMLum1igrpjhwY8wRz32OWkWEyFzqNrpFal7oudNsIkwe+4vocWi+kUgon3XGOU6PXQsHJegKd9pwKhMme+IG/aCqlXs7Qp6jFq19u2Ac/fnQP/N2PnvChmFq93JZ0Y0C88afuvPYgG1eMq9mK3k3IVANG+nRm6lcv/9BPnoS1T+yHF3/xHt8Z8grAEe8is3ilixHyhXE9BiqGAjBXQM/x2KXedelZEJB0y91FXapIMxZjconCbGglxaKrS6Ub47ggIEXoHGEhgtblyEh8Ux2o4FPUxQjLugzudpPFuz4DkLX0/NI0Hm477lOvJz9JI44C8A/X7Yb/fXgXfPRnT/kLhOk6k+4xF8SzggZNbE2RdGPfN87Atf3hds4558Ddd98NExMTsHv3bvi7v/u7KXvmZjBKLusZu7GTJd2jytFjEl+AlKOMbHVGwwfcfu1KNml8H9FLvMwacKEjXbhe5BITzG2HR317NB8H0j1uOKKRQw0PdKImUXyfkZPHj/jpWEldXLaLqRZnS8UHc/8Qct7WA9L7JWoxAivD+wcnnI9UxNJId6W5y9hTaitSBCWYex71zp0wokuFCD5yJqwNjGbhoW398OyB4bI9lKDnrhfpvulf1OtMAgzvcz+iAnivnHBLWgLpViOCvFFqxroc2VLT3OH2vJF0Y8LA954U0s2KNIWI+4WPfiK/i4VTnUThe2ui6EdYs3aQUWA891SDXZQVL3R2qKSbRImi2PYjo74zRUX2SSXqGb2gN2T30w0oRtQ7Borfy9xv48iw8RKxrmoTRiozKvj2zFc/oHFIN42Yo0Q5rKFPWr9zwHeW6LUS6c76kG7cH5iEmueKimGU1AvY3B73PeW/VxgbmsjBI9v74eDwhHNX+/ZepaT7KBMotn2fs2jOUFKwW+9oyFjPuJLuOzb522+oQJjQhfGIjJFRFtO95rsAF9UnnD1drClHhrVseiB0eHHxpHtYJ91EjW2HZD7vD9oQNfjXU1xH9SG/8J3PyCGjo8exTOiA8GuF5qCXf+o3z8BX79wKn3j5WfCGS1dY6OURq+jkpNtnuPSskQP+i2sa08t5gETIHO0VD+m2oJC1VNY5vZwnVpSUZrqmLb38Az9+An7y2B7oaU/D8EQefvLnV+i1wjNk3S8HnwH4yhUAq24AeK07EqxqYFwqQGLnvQ2ZR42XOYKXGNvirOSwhvvihs/f5QiyoX39TRfBtafN13tPX+hRzhHtjYXnACw403sdShpmLIumQTGV9HKNdFvQhSd+CPCjtwCc/waAl32p4uu05w30EgMmChYRRU/Gr9BtMiOwDzuK8f+G89xndrU5BZ8CD3Pw9+F0zVoCwnFViJi7ymVfjR5W/5fuorYGIN3RkctuRTlGu+kLd8HP3n2lN46QFyPC9rpSctk1B2DsSENp9zTrPupdxO92QiwxnnFandIV7qJajHxuz0KvNxf3WQOS7m71XkdlRnxy7dPwtXu2QW9HGh76hxc4Sfy4opcn6kG6dz8C8DWao2HEMlTcELyj5/S0ObFq1KT71f95P2xSxV90VY986Hpv79F+sa3LUa9f23pXUZw7ZxXAGS93WTfb71L/Xp4ZQQyjqD4Xbf0uT3sJzxDRyxPpOlvjRlVMN+90gNNkhOSmwpoC6W6ZLL08KhKFATFPpEYm8j40yut1Yc7moJJSxkDORGNsDrlzJkB7j5dAUQAomnTXTy/HhBvtn36pfl+1znUj3eSkKVkY2uPSYtUa674rwf0SlRnB++8ImaPX0ki3DV2oZa0I8SejgIlQBkG6cL3Fq/9bv8eJeTHhRvvNU/vZGWKBMbd7v+D22W5aW47eVaCAJnBGKg8M6WwJWKaO4tXB4UmdcKP96sl9zkcd6GTqqKLTmrT3MZSCJd0C6ty2ol6cSTf5cI1083W567Pux/X/HZ5ejvtO+1w538LVyqPS7hGNIqPgGosRFfVFagkINdJ9itHSI4/otsego7F7wPOpSK/19CLYc9fDGFlwlh3pbkABOCrSzZNSWg/62ihUYF1VM/TFml6ukG6613TvsjzSTazDsPbcIffexLuIGBKjhHS31dHTvfEnweeK/EtDkO7w+xz7wCnhRkO3vfXQiN57uhhhWxfO0rNpG/E4l9pXRo80jDFCbNeo7CJMsg8Meb8XrolGujUbLSLSPaJiuu65cCxZK+k+jo0OXNRkYQdDudFGVUVUC0zQoeNJE7+If/Ame58dd8gdM72EyUm6G+GI6kMuv/+IV92c3e1dsFhN9Ch9dSLdmHQjDRaDJaTFksPPTN9AhweOJPRCX6vY011L0l1GLx826OWSSbc6RxHWRYuOMFs6q9Nyhoz90u8WdXw9p6ZhGwcm52jz3SJNcvud6v+w8yXdphHBvxxmCTfaPZsPayQKLUnnP4pIo066e70A2Em6tzUk6fZ6uqMn3d9/eBd87rZnyxNVW697n6d9UtHyk3DW3v81vpZtSOuK8/JqXaK2unMUi4JrLPhkOdJtniMeENqCQxTzoQIVIt3Wlp428YJeVJ+LhYiBMe/3wj1HQbEuikctAmvGiBrZ1W8g3YLJpdfTHQ3R5X6X6NMksjVe6S566v8AvvtagCd/bH9hXEelUu4wANTMZSfOaQTS3V4fvZwL9hGLkb6WwvGVaGbMtunXkPrRm+H8HTcDHHjS/sIzLSNGy5Du6Ukv33mkvPiCCuge0s0Kt6b5xEsrIN14D5tCdfkGCqlFRLrNIgaKEtLZSumkO2KsO3qwvHh1DFgr6T6OrV5hLNMZEVKXM1E67mz455t+CfDI1+0vziuAGUvSLVhF10JqEaO/v/3hBh+tiQyRrQmuGBvF0ZGTxgsdabDk2InapMZ6TMf9wgNHEnqhr00mO+tLuk2km6iPDaSX0zirMLbnaHnRCS8uTxk1oF/ssJdwQb9KFk27U/Uro61yZ3wnDj8LyWIWEhPsfAkXr6Ig3UdG3XO+cp579vcOTjiUYd3Tnamjiq6T7h5/Px4Wr9BmLAFJq5dejv/vb3+0Af799s3wtBavLFVIull/eoX1SuxwhbZ8qC4GTA04Q3EIzB0a9n5nDK6xSOOM8oMklLBAaU26J+0iT7avIQUULTuskqjG9S5H9bmkEUFCUvye1i0aUYvAxCbCNg20od2qSCPPRquXAcCTS7yDMHknpl4hTcml5S76v3e67CL8aDPaL5hso44InRkfYCCvXh5V7X6CrSftHULNkzhG0Uaj/u4fQfKZn8Py/nsg/Z1X2l/YxqQg+nADerpndWX02LCwxlsoyXDfUcEnqWn3VZDuSj3dTpxrJO9UjKBeetFCZ7S7yFwbLOhRUS9ZDxsN7fDm8vvrGLBW0n0cm77QIyLddODmKDSXC6k5ZksYTPqwiVCa6J2DdFsuLkkhtWR02r0p7jejM2MXUrNdRPh/UfAoCP33OelZHiKHjl1TtOSR7qjMCD6ahSrxtGdKOtCx0ctZQBhUqKALHCnDzg8bahi9XCeXEQJAfmkRfR8vLVpjmkdd1orBzxQhtDahNTJG0UpijyEFh7iPxAUJoyDd7u+7bFaXb1000q3XpY5kAZFurrCqgz+5vRIH0r1v0PMPWIioql7O2Qxc2Mc0zphY/WZvfUk4TJjmV28xwod0D0/6iz1BqAtXyDVHg/E1wQQKZ1BTwoT+RnjsXhw+lxKnVfO9NpJhRcN3XruSCFSt52j2iZ72Ciq7Fxo5MiyieKWBkI9OFjTSXaS7yEYvp3UKQu/orHTNdWdQU9LtFGlkR2NxDQBiHYa1CZas71CgCrHS0u1dVenlCVPolYzu9ZnLAa79oPv5wA7/9wSTbq0BEOEc0Rm64QylRq8YFlpfxEyWudnGdHLTxe9ZXnKNewXjHIppuueBdFtP1Lvoqb3+QiXe0ST6mAwSOx0fcAs3leJctG2Klbdies/nDmutpPs4Nt2jW2cV/fRFbqJDl5ZOPmzUWPOgdc62vzhH4ihhcvqiGiCkpmn34R2RGQTwvxec/kKOLhiBzm8+CPDFCwC+fKl/RqEtCHR6gE70kq4GoAv1oi78wqOLnFCXYqYC0s33TFDVlIo31BfVQHp5Wx1tGvxCf+3FLnMB6Vnl1DVjr/AzFaTuTiNrjIsvVWJJtyC9PF1H8eqQSqCQFtiRUUl3tqD3UDITA70cEym+vrSmgoWruNEFVKPma6yLNDzQ4Z9XmASQUOtSPPVF3nnB/0vBnzDNr96km2sAIO3RV+zR+iJsLdDHogBYpTYNTv9EKmb3fC+5asBdFFdhfMWcbn3f0z2dcZLugPaVWowXNH0FYPliRL1CaiYSjCi3nuHM0WluZvHQVkyke4gSJY2CjjWUXh4V6ea97ia9PENJdxQhNbrDT10DMOtEvy+i15NEdOtodaJ1OHl+D1ywfCZDuot+BoB5R+PvNby3NqTboZfTeVG6AFqQb960FFJDJfeP/NQdE0aGe4X2S1rd0Qkes+17HODTKwH+aQHAf1zkMRJNQ5V7bKFLpABOcIUfjxVrJd3HsbXXeaEfVgHfSYoCagqpeRf6RHDSjaibzbgzstLLGzEyLPy6mL1UtCbO6xVKkIMUlCBhD3S2/t67jGyoixkEEu3G6emWFzuqd79M8j46FeBowRca6WVFFyYrX1zo1M1ZutnmoJcPKrob9v7TeCCeXHpiJGwNcPQT9Q2ikZpyJWrsua/Te6MtPwIJ+v+C9PJMOvpIQkIt5/a2aXEgjnR7/WJR6OUkItfnJWMoSkd7RfAMxYF0U6HGJxhWaX55LQUaok1rBgBDKUanP9J9YGjC16qBn5NYo2O2c+QUGUqVZ+Zy+idvPUBEtwF04fY6C+OEVi6f3aX9NxU6nSS8HqQ7F5B0N0BIrd6eblPde3gy593VushvJN3m323q5uSLKVHyAQY5ebV7RS+P3NPN1sWkl2c6qiPdgcYp5HyvmN8TMio4RQFSSKAR72jOsKCkOxWUdB9VSD6ZrUBMdw62OvHfn0aroW8RLIyn6hBS27Br0MogoZ7uNAnv8bvoyR+5jBjyoSTsGsTSQ3+L99ExZK2k+zg2nSxEvNCJ2rh4pusssFLsiB0VKtBuTPqwoa5sR7ptfVHt0xKhM1VDOc2r6FQTE2wMFHPS+D0+XsKcgWoLAqmajv3MqlrcEPXyOJBuQ7wmqcXybCPDWLBjCw4JicOqKNLXfCPDpje9nNYEg0g9kzdfYDNALUiUeXnbkgX+9Zd/xRXTUq/VkVNfx30oGOjU06aBKsto8xyk210XDIopH0sR0h1pZBjRyxHpbreMKWxM0o3Ml3qRbk8wzCzScIEwFvT88r0ABzbaX3jSPWclLIBpPY4sU5GVRbr1/PIIqMu9zx0uWyPfqDr++wTdRbZCJy9yovkQ3QYi3fn69sryOV3Qrs4R0cud+z8I6T78HMDP/wrggf+0vzCuraYFByDdDRFSK5a1dEURsORId4ILt3IzUTnbXaVbMdRZ0QyuxiDdXeo9HotKL2frsvXwKHz93m06Ec90UAEhuEBTQlq9zfhYMNorOHkF910DRoYRkBKlAExnD/ccMSxw3+kCcFCvu1ngtN1V/Hd39kXC3waEMV7YUX4NQrrJt6w5eyGcsqBHtydoITVbT/csxXIgC0q66WwRGHMMWSvpPo6tXurakEq6l8zykm5Eb+j8Jih45UmCmTQFJd0c6aZLEBNRQugaQi8v1l1B50g3JVLWUWrYW0hIU6UZoZpePsu72J3+wonpP6eb9XRTgEPrk2hniICJ1vJg2Jp0H/bE5TpmqP83ZDjv6Ukvpz2BAbFGdLPeha77ooLECJ3/EJB0myhdyki6BVHueil9A2NugDqrq033KQ6xs6ST7iijjqjvEPeKL+kebAqke2f/eBnSTa9lpZeb++V7r7e/MPmfNo50TzZMRVavSxTUZbf73l1y4mwdENLrYDKvixHcf5isGhu93DxDvuRyfNr73L0K/V86s1O3aRDS7dLLA5Du2z8OsO7rAL/+e6/3lhv301gUn7nMQ+ga2OqEsUaUc4Q+1iyOa8EwPaK0CkBgi13Ms6J7ullrnKiQWn3q5WYx4uM/3wjbDrnFh/YOtS647ynIM5O1pCfY5zNNIe9wGTPOvisBDO1tCNKtJ9LUc0enU77zSCyLVHvAGUIkt2rSzXwIJsB0Hun/CrOL6ml1oqR72ewufUdzIbVMm4VdVDSKQXsftb84nT1h8c6psFbSfRybJ9JSinToCOleopBufAlKxP1IdwUhtaCeDi4wQfRyLnojSi+PPjLMvOyGtbhcUaN31kDHrIraLnREEGhdsALa09iku97xNby4g3sH14SS7xRd6Ca6sP1ePwXUVmXnhQgKjimBakgVPTq9nIJpZBHQxYWBT64SW8S83Ksh3YTSqb3Rnpfv53Z+HM3pruMcYY8iId3ct6Rs1LVajZIIZEWgqjWN9aE906Ce7qi9yzvV/Fwe+BBtUitS+5gR2eBxc5aebocBwF+HilqCgj4cdXEZQeGM9sbqFbN0fzchuk7xxzZizixsEmOmVqS7AboIemJExMI43dGzutss9HIM8i1FPex1JxEjNN73XnaP42t0ekVN9E0NQHQJcYx6H5nJJa4J3dXpoLuoLOkeqT5b2Jd0ywuptWei7xe8c2z0a1qX9k71uyA9mH4X07cEgQVcLA3POVGG8d/npze9XN/RDtLttTXQvksTE9O8l80iXkWkW/3udB410i1b6KwkpIY5QaU7itoPsHVF39ETOc1Gy+g7OoB1FaThw78uCJZMlbWS7uPYKOmO4qSR0kUX+sIZHZoBw2eC2unlysHSCJdqSDenl3NlTFF6uRcUh6WuUa/y/N527bDf9q1H4Pz/7zaPSm1bF1OB2nahjx321g4DPbrYkdLGK8nTVb3c6L/DIgStSbqDLuFRf/WcB39BSDdvRaAAmL42zenltCYuvdyi0m3rF4uKdCsUtFFIt4OkRVwXEmPBy5zWhfwN+pqUDdGtxbAfnlAETKA4uqCRbrngDy2lihGRk25GL79782G489lDGsHxkO4KgU5Q8qxZIWx+Of5fTZkVTrq1lkb4daHEAFlXfR3u3bJFIXROm4NOLrPhJmkEId04ps/83jRT6eZ3NE7RoMBYI92+nu5Jv9gR14Ow3dG0VzCpTLLXwfu9gbR75/GMBDoSI20yr1lXKbqLynq4zaTb0gKmFadVssRnWzegGNGWii4wZxYiTOvsZneoniXtv48TeH/b2mY0mt3lv485kCLZ6pSuh43m3dF+pLtoqLpPVL6XbbGLnjrT5V8DuqOk2UUBhU70Ha/4z/vgps/fFcgO0K0riHQrhkX/qHfveEi3hXU1+yT3Y5CCeQMEcKfKWkn3cWxU/YuSdOMlRUHjzM426GlP+2ihfmqsBekmZeWg3mWOMJgOGpPOlEraBRG6KJVRQujm93kBx20bD3iKsamEnQGw8wH/C9nWxRxHQhc7ojcjB+SrxXULqfn/H1JjaV3aSaQFUW2f2rJBbaymAEoBMFaZnb5DeedN/WKRLnS1Jm28pxuT7kIFkRYz6bYh3XiJmkicFlJjglmClqlj7jKN9EH0nxgAlECg3/LowiGTbuwjRIobBr69i/1BMFHfBJMF5+XroJcjksALm2jrdgzovZeshV7eu9D+4iq5KvFed3wdzjoSNN1fGGFdKGHCe2iuKnjuV6PVnF5mm5CaibIQShk0LQKNfC76HBMFF0XoorU60f2FSbeHdLORYTZ9EVMAyoZemsVMnrzTutD4RqFWhHraNA4ypXs0pJbru6irx/MH3L/UQi8nVgC2OvFkCtud6EwKsq7qaUegUU94DP/tj84t+36nc0cn/MmSjXlm1WUx0GxiL3J2ieg86ujnaNKHdNMd7bWABaq6m/eyrUBsrgv5qeEDDWGj6UKnEeei73h811HYfHDEJ1DJ7eg4E4FV63JUxf94Nj0hNUsBuGNG5aS7RS9v2TGfdId00joAVggdJd39StHcCYwrId2EmgT2LjMUgRw0Id2CKDfvRY2SMNAorN52O4UM4wOdMHAnvfWOGvrFDhnKqF2e0IRCaUqSF3qMQmpm0t3RwS5cvl/KKFo1It24f7jKagPWJUrwN8mE1HhflJ4BahP1MYXUbL2ouH9MhXKVXKJ6eSOqyPWM3qN5sViIoCo6T7qtKt1UmLvjU+74vW13l78wtXHMXOEWrmzsEMHCFaf0RUkuidKHgc7br16pv7ZdqVRrn2sLdC79s+AJAeYoNS48NtmYIk096uUkWNnbkYZupeBMInNOn6tNSM1Mumuhl/MRUA1AuvXEiAg+l84LFnlwDexIt4VebvoTK9JNxcwuI+keb/iIuXpGNdqE1No7mXgTj0/MdbDFLvwu4vcOZ1EIikPVw4wgpBvvoVecvxTOXqISI2VdeK70/vcj3SjgWkQh06D9YtKoTSAFz6cgkJKJgV6OCTedRy6Qm6HYJQrSXUYv7/QXb4Tv6CAhNT0+D++rACE3YpjgunhIt+tfnVim0iSNzpl2thFZi17esmPRMPiLitJx2loikfCQbnXonNfllDMySjTpQrY5aEx0OUqn6eUDjUGifEl3NHo5zcs0zQkozZ5u/F2PbHY/P+Wm2pVRbUIbgk6qngDQjnRnNULV2cWSYp4wmNXiXI1IN/4/HlQ3oKc7yrp4SHfKPzKMlFFt/YX64prl7aOyKvtguUI5Id0FoobKXmieNkJ0QUK8zHm/mA4sNaJrFCAeuQXgjn8GuP8/AH78tvIXplEspHJvG+EzjZHuXUy8htbl/9bv0d/v6rD0upv7JSDQSZDP4SPD8N/SvxdELjkaVQ/S3d2W1gEgicw5SbcN6TZ/r4r0crV2dJZw1CX1uksi3crn4pLk67yjyX/7z5GFdWUmC9YkioJi5Z94gV33Nc9rCJMmyn7x5pd36fYEfRd1dmIPWPlZMVu+bOvC7yK+X2gEEq634MiwepBu8rnkV0iUjawLYxpzvzAtmXyyIzh20TRqSrq7/C1zwi09bXXRy8t7urm+SBux9NAnoB6CeQeTf7GxssoYAB1G0t3VkMKVqenEBYCD1sw2eeWoYmE5f1faBQl+FxUI6aaku0Uvb9lxZpUShkr9zPxCR+tRfXT9il7iV0a1BDo66bY4aIdaXWI93VQVbUzSTaOOgpR0K60L0ctxXub333GZ/R+ZtHtaA7zoZyytrozKgxnsv/Q9vCSNus6k2+jp5kh3T0fG6/PngTEFMZQ014p04z6jKjoGhuw9nU5q9/ziomTBGUdC9HJSdbcl3c4+SNiLE3oWdW/ZvmsU0u2NJIwy0qeoK+aekFqe0WItvcs8uA0SgKKAkOhtNqRbvKe7fsXYFU7S7d/TJ83rhgWz+ixz3XP+xDFIvEbTy7Gnu71cR4NUnachA4B6uvEeomTBjnRb6OXIekCjZBH9O/n4IKTb+aH7GtbT7Txu2KR7zH9H0zlCkTk0p/fdhnSX+ZLh6vRPOkN45ihgltYA0Eh39HN0+sK+MqQb2RLW2MVs+TL/zsEC2hPkY6lgLjwCSRfGC+FHqZGORqcl6ca1dhhGAUg3rlcu1VkD0t1lp5cLs4s8enk9SLeXdBNbBK2N6OVmEkmxC8W6VqTb6HWnfUd+V3i/BLVocKTbxprAveVNXvFYehrpbgtgoxUMejneU7aiPPnmFr28ZceaBVVGH97eDxf902/h54/vtf6/ITPpVkg3VbqcgFtXv8ct9PL5wb3Lel5upxsUkIOeHGxI0o3Bn/JFZejCzXdtgUv++XbYftgeuNJ8TJyXefGJs2HVfIvTNC90rjxOTtZ2cY0Y9HJbJVRyNBZTuw9r2Ef42Vuf9X3t8DBLupGObxulRnuBNACq9XTzZIpQTWFabD19l5qixS4uP9LdW464aKGiDu/3pTX49T8AfGwGwH9eHpx0E9ItXEUnRNc3L7kGwwudoy56ZBhrabHuFTNhwAueIw8+sTD2u5vtKtMY6d5xhInXqHUhe/MVJ3rKyD56uYF0m2KFZSPDkF5utMdgANWgdYlEL9d+hNPL3d+7uz0guaTAbpZKujFZRGX3z50B8C/LAXbcDzBuJFG8QKNHz8n3dEcpdlLPZZ+6oylhoH5m5+tRke4gejlpi+DfpTUjIjJp0L9S4eGMxSrpnvRGhjmxjD5HIXq6OVhgFmloXaTvojqKNEQvp2Jel4rpnM8zKYctUbZfmIBrPtkZHNOZiG5bY5Nuj15eh5BaxhNS42yRBH92a+yysIak289GcxTina83COmuQC+3+R0+Grg9hfRyd13u3+oWup0eb1thPG/Qy52vWdDuBgjgTpW1ku7j3OhSN6tZf/r1hx2k4C++u976/2hmrlMVZkk3jcVyXpcQIx+9fMxfBce/mwGg2StHoiQNQqKcHxEwBuqf1z7jBC2f+OXTFZFuh4oFAHN7/IHqF15zXjnSralXLFCxUtf6y9fDdEqCiRQfXxO2iv7YLi+QO2uJG+igIBQJbzjFG1MECi90WpveRepr45WRbpwT2q4S0UE1dqNBwV+UYoSmrqU8ihYWbihgylB/IU+UeJGGihGIuuF6PfAl/w/g6KTad5nCWGPp5SHXhfsiDABtQmrW5NKWMJj7JWv53c1kUjgAJPGaQgTavU8x1ki6HV9cSx8d9vqbvfAD252RYUVIuUJr5poIn6Fq42sqGSbp5HfxHjLp5Z3VhNSc31cFu5t+BTC8100ctv7eK/QSRRQTDzMQFkS6MSimlsqouiszu/xIN5nrcysUOilxrIlebpwZLKoH9IJO9Vx3OkOI9OM5IrCA2BLBRRpi5SXsve8aLOjwAAfaK4R0N6gAHGW/8JYeSrTJKJ7xRqAZSHemA7LpXr8IWEX1cvVx18Pux74lMO3p5SlPSI2Q7nZcb4w5qB2BJ5C0PwjpNn0uFoTJH1FMa7KuGkQvN88Qp5fbkG7+NSxGOP3+5uvWgnQHxXS6ON5Kult2jFmQIjVdQEHmqwqzj6Re7rwuXTy+pFt93jW7+txhczYqWYdsb6FPeTnASQ+OZysn3eriIiVdtC+//gJ42XlLKiPdFNxV6hfjKsI8AEQnRxRtAaP+/yhoFCF0l66cDa+/ZIUOCokCOre3rZwCqlHLhNe7bpt1aa5Lp3LoR3c1hKJVF71cU7S80Vij2YLOr9M6aS55Z0df1O3+ucG7Hiz/AbwP10R0GyakFh6JIrMJqfkEoMye7jINAONCp6If/93NQEdYqJHEa+rt6aZ1IevGwEfPo7YEOtxvmBRzJeQ40H2SX0itgUl3ENKNPvhH63bD3gAVXY7KYMLUrdbliI9eXmFkGO4FYl7RfFzyKzYROXO/CCLdiC4GFcarmclGI6Tbn3RXQLpnLKuuXq5pscaZ4WwscaQ7WtK9fE6XLoo7rU5MAd+K0tFemHOSn3mGYwif+KE7Ri7ofqYiRQOT7rD7RSPd6XJ6uU6qzMkrJKSWaofR9vl+sUpudNZov1HRc0S1A628BqZjARjNo1GndLxMEwBoLnpZTIf7hs4NTYsw41x+N5k93WTS9PKAkWF+enn5KDk+pg/3HPkYMicP0FNBLEh3usPzyabGCLZobPyJevFW0t2yY8w0ZTikgx4xkG6nOgygAyPngqfqnW9kmHI0nbODA2PqxaTLCxMu7nwaEQBWQS8puQ6kl6tLatEMz4kum2UqvZZTtDykuwLtngd53ClJK10S5z5CoEPJwumL+rSD7h+d1EUaJ/gxEwZNG5/h/W6mg8bLgvp4ddKt9tbhZxuMdEenruHFRcglTzraOtl7Sr97Pijpfqj8B1jo5Y2qokdlANC4MCzy4GtQskBJtxPoBKmXlyHdY9Xp5VzYCPegYP9/PTRq7AGn8S3LZndq1IXM0dUgBgBPLmm/YAJAZ8xMutXeOdx7unrIxiPdQerl37x/B7zvB4/DH371/ooFYNwvrjZC2ofcOTTZSkg3JgCUJPIRhYh4E82T//680Om0QMkJY/nbeqIl3X0dNSDd/BzRPTNzWfU53XpkmAXpFrZ0RMYIZ4s4xV4s0Ixm9T5ye7ot+4XWYfZKv8bKr/4G4EdvAfjhn1ru566GniMujhsW6R6ZNJBuRi/XrBpTHJcJpI21zbMn3ZiA0jhGk15OduLV0Iie7rCUe2T0ZX1IN9HL8/4ihwkwaW0RBAzm2YXUeNxL61o2SaNrSnwunYWgfaSZeKmEs+fMpNtp37DpaBTYrHqtD2AUI27/R+/zY7CnWw4Wa1lTmB4DFWFON0e4Kfl+Zr97MTnjJjKHyg8Vr/hiAIiXmhMYsySc0Aa69LEah8nFgScbUv1Do4srqF+Mo3HcBo0+ujddfoLjtHBuN9Gqy6hrHOkm2g0JslSaGWteXg2iC9eFLszu0g562+FRJ2dGvz+rC3tGjUSK08YJsTXXBXswMThGeteCs9yvLVkNsO8xgGd/3VB0IUoVXQupZZJ6z5DhunS247p0urQ1DHSxAEX7JmUk3TaKHj8rZTRq2f1CAYqtUl7J6GxRkkBBICVRvZhE2EZA1YJ0W+nlLNAxgx4BS0VU6UbfQgWM+b0d0NnmT5wdX8xHN9kCHUwCxsnnMlPnaiIzK4AV0YCkO2B8zdonXMGy3QOVkW78/REZLlNdzlQZGYZrQoExR7qJKYOBs8/PdjaUdYXnaDjCHU1sNbqb7Uh3e3Wk21YApl5cYiCZvsWcqjGNhNQ4W2ROd7tPAEoDCDbNiLKk+5A3MYG3f/H72UwaGlC8wmJcrpAPnXTvUedr8YzOMnq5nsZCe78MMGiH0RQh3QrxtxVCKb4xixEzpie9nJ85vKMJ2aYpPQQ2lSHd2+9xPy4+z7uDTaSbfDTe70HjKxsEpJjxHGe6WunlNHVFxT5m0u28nK0FLM8AAzwbeF+bd9HTP/M+byHdLTvWLOqICQp0yOlQ8k125aq5THF63C6QYKOf80opp5XzzxuIugQlUnxOYyVV98UzO+FjLz0T/uyak10hErSgsRu4HnRhm4gd/1pHwKUuTRdmSHfBWJf3fu8x+NNvPByoyGxLurGnm2YOuz1ARqCjiwyz/KPAuNF882UXe/3LRFWj6rrwfiHExRYU/3bjAbj4n34Lr/7KfdYzRpcXBsRY1OAJA54tZ8/oWd2VkO5t9uC4Ei1WeL8QEhuW5miOrjF7l539Y0O6MVkrm41qJt2WUSScSm3S+0QRunDJAvUoYy8q+m1zXRwfrH0qC/BojXC/UABoUoaVDy4k1FpgnyL1rjaMXWRHLnlnsM2/UH8l3UXlo454j+5EAL3cknQP7vJ+d96fzJPuBqyLLowb5+gXG/bC9Z+7EzbuHarIRqPJIkiPrSnpJh+rJ2mMVEi659l7uoVndDs/MqB49cm1T8Mf/Od9mi5d6S7Cu4dda6x4ZfEvOulm9PK7Plv+A4Lu5wYBBraWwX+/fTO85Iv3aA2VarT7MqSb6OVBSHe6A8baA5Bu2k9YOHf8imUdhNelEr0c0ey3f+sR+OvvPVb2PX534TlsS6V8SSqdrbJiJ8UlK6/176UnfwTw2VNcsdPPn60erkLRt0FJdxm9fKI2pJt8ipl0uy9eoac71R4c/2Msp/+9odlyDFgr6T7OLehCD4t064qfsqtXzWMOOiDQMUU5KiXdVHVvwLxY/4gJb124eFgQvXxQjTWyOiH94pWQ7oDkkn8tCOlukOiGqUiNhYYfr98Dv3vmIGw5NFIxYUCEzlwbLTZXhnTT2Ks+b13MpOrIFvfj4vO9ryHSzW0K6eXfvH+7I7z38PYBeO7gSMWRYWh8bXrpTNH7Sgmj7unuAJh3qvv5wac9sZ4qQmqN2i/Uox4U/AYZ/XtKKs2C3oxOlkShKBgplKNvof6xIOE92/xPX3+3bD83WjJiT/chdYZIJ6Is6XZosZ3+USzos3yBDu0lA11Q65RPqjPoKBW3N1ZIjdbFCIz5uTowXK4AvPnAsK+VxxT1qTqnG9eEkkRScOdtTubv3uikO6Aw/u7/WQ+bD47A53/rnwphFobp/Jh+19/TzRHdET/TjI+NIzNncTsBdqKh9HIP6fbf0V+9ayus2zEAd2w6WPEuWtjX4bwGJt68sOz4Yhs11kS6McEilDsI5Z8CpNsW033utmfhiT2Dzscg29nv+kYSl/P1dJOvqTAybDwz21Nq54WzamBBQwrjwYxOZNzduvEA/N/6PWWsLL6GfGQYmb6bTCAFGXgUl2ifOwrw5I89JXvbWpT5mqkRUhut2tMdHLegfehFp2ufm8A7mtp0ClQAbmPx/1hwAWbpRXCsWSvpPs7NVhXdN+gFqjywOzIyCXc+e8hBG8yebgqY0H75nithFl5kmoqkXg8DQEJY8MCZDrxS0s2FWYTnxTqPZ0Gj+BoF0ctN8RqrVerp5kg3rz5yJe/Anm7ZdUHU1daPSnQ9tL2DE5VngLalymjUOuk2Ax0KgJHCGYR063YFVojhyphTSC/H5PGhbV7AakvKvcurvGKsq+j0vtLvqpHuNjcAxIIUXmabby1/OC6sl2o0vdyOdOO63P70Abh78yHYcWS04oxutDnGBAAX6WZ7yNQAwN+5a653oWMxYvNvlf+xJN38PDViMkLEnm4SBqPzYs7pdpFuPr5mwo8y4H4xCzj637o+uEhJt7lfGuBz0wGoC/Wxo+1UgowoZPT7Zw46Z+qe5w47X7v8pLl+Kqyy7kAhNeZ3KyWJlZLuRiKX7BzRyCJbwdtkAFBiQMlURaQbkSUMkp1/sNz9OOqur8+on5mKFXycVMOE1MrP0c/YiNOgohYlFFSc4RNG0Oc67CKbkJoWxlrg+c6hPeU/gMctZT3dfQ3bL7Zk6e7NlvfSwgAoS7rpTAWx9HBkGM3p5neVDyyYYV8XXEtCwIX3inkHY5HmN095SbB5h2vlchwNhqKGRtJN8a9Hpc77Y178Pcl/4P6xsdG4PzFn20sj3QFCapxebmXokRaNJel+3SXL4a1XrfQVbVMldY7yrDXObFUgo79f9yH3rB1j1urpPs7NdqG/9uYH9OdcIRfpSZhUffbV55bRy3kAeMYi6l02kG48cFTx8iHdrNKFqBX10s1U81OdHzS/odViPh7LlmjjhY6qulRBDaKXh0K6kW5DCQAGPuik6XflSt784uaV0AaITmBlFH93fnnRZW1+zm1C7S/cT4jgoq8nPz+3p81ORyJkAftJg5Bu2zxH53O8TEpTSi9/fNdRX8Jpq7KblxcvSOhgmlfKTWYELuTK5wGs/2/v9+VGFHvn3zdWvTwI6f74zzfCdx/yaLzb/+VF9p5u5XvmWZNu9jU8R3iBc6FBXtD77msB9jwC8LIvl6sum8yRBiDdQX101YwQOloPLoylEbqSkXT7ii5t1enlPOnGtZicgpYeti64F2jCAfmXS1bOgXd8ex3ct+UIvOf5q+Dh7W5h6/KT5lgZAA41tmBBunVPZUflHmQzsfYhUw2YpEHq5cx/PLrDG1eF7Qa1sNF40o1uw9FGMJFuXgAnpBv3iolGmUg37Rda04Yi3e5+eWL3IPzl/z5WdhebRv6Fksp5ve1ai8ZDLisIqeF+wP1ytLxgWJZ0m7T7BjIjbG09iOoiaELj+fg9dGBoUve6o3UzxohOwE12oortSukOKJTaoJRIucgmrhX9rjak2yeM24DClTpDDvGnWNJ758t3bIHP/GaT/ncYB6s2f59KtzMazCJG2E1rpGOXyWCVblwTin+D/IlZrJqqnu4qI8P01BVL3KJjX3ZHJ4mBVuBIdwDoRoVRKpwfY9ZCuo9zswmpbVdoghnAEIr56yf3l13oa85e5IiGfePNF3m9yybSzS9udCZmfxBR+pxKe8KbQWxe7g24uMi5EhJJo5y4mc4Ik3Bal1BIN0+icM3IgfMEkyt5c3VlfnkJ04WDeqNoHJiJepPhJUdOGvcTXvikqIu2ZBbNqDSUdPm4HlImN5FuLoZEhvuP7xFhNIoQXbMivN1AcXOW/UJ3HV1eMznSXUYvN3q6aZ9g31iQcZpfg+nlWkiNnSE0nnBX7OlW/58UhslmdrYZSHfOv1+QGcGVUTHhRrvr03Z6OQ8Gu9zErRFFmiD9g2pJNxWpeEEUg2LH7/KZsehXOdKNQRCNaiTqNJnyzYUk2yN9i9jnqr+3Ef2FbF04osv9CybcaP/z4A4YGM35kgUrvdwmpKZV3XHefUSke4ro5Y46cJXxnuZYT9QXIZvf2+6ud9ldxJJMDHopcKYebvo3NL+c38uVEDsBo4I3Id1ma9Ph4YCxnsq/ECPCEX01fa7JukIEk+IXLLRU2i+zTgxmEy06F6aCXs5HiT29vxxtHVT6Krgl6A46k4Rf+X2v/eqYX9gUfa5z7/aU6wDY2uL4yNgGTqNB44ABT7jN75lCp2inLuz1aQBoNpopGqZZNJ1sIs1I+SQAjE2e/1Hv77xYhX6c33MN8rmmsGClOd3km3jroffiaYBEKgDpbvNYZWZBj8fDx6C1ku7j3KoJqdkOE37JTLoxSUXBsGtOZU7DRLoJXcGvY3BoJuVo1JeKwS8eWlsFsAGUPio2UALgfG6Ip5lrRmMkKqEPVXu68eKy9XXbLi4zcWqA0qOtMsrRbTtdmM9dLqcjrZitntsUUqOL20m6GdKNyee3XwHwlasAdtxrD274RS6MRgUhCybqbyLd/N/b6OW9Venl6nyd+LzghyOqqPPvG0sv14WrfMSebhpd05b2UR2dqrqPAjpZXqSxsWiwbcVGL+dnqgECUN4Z8u+H7z28E17/tQcCETqilxPdnhdEfbRHTtujpBsRbyzWUVCHfnbt3wD8x8WuuI/Z011JyLKBvoXPi7WdKSzo0LnSQmoGvdzX020bXxmWXs5bEBqI0vGEgO8R3n9ZSUiN7xGNepfdReP+0XnqPCQ4xZwScNxTfCZ1ItlYITVjwoiJQh4ZndRJ+Tu/vQ6+8NvNzt/H9Ggsd12uPNlD1Dw1aqMAzJkh+J6b+4WPQA2il+P6LL0QpI0SRB6f8IkA91go5nzqCqHgi5SKOdrT+1SirkU9R+1jTGnKAU8ubUg3X78GTqNBI3+BRW/Tyu5oi0r3OUtnluuumPtFx3TtLOkeLk+6//gnAKteYD83DYjn6L02J0ZQgbda0m2OrSwztS5JYtwVLPRy9MO4Lt9+JcCDX/W3zx2D1kq6j3OrlnTb+lCxf1tX0SsllzypdvopDWqnTUjNVEWdIno5BfxcMM0UTzMdNF1cWIgwaec193Q7P5wSzIHKF5eZODWA0mcbpfbEHq84sF9R1AKTbuWkF/L55ToADKCXY5CjCxGDADvvB9jyO4D9G7xL37yg+B4RHuvTHnCGdvb7aVPmaBsfEmHpjeomxI7OkUa6x/0XPRak5p5if7hzX8Me1FgHs/ddKPijHu0gC5oRyhPtOdSC4KOvVWhH4Bc6p1gTMsMpfb5gcF7jRmMZv/ff/egJuPe5I/C1u5UIj2FEgV2qmCE8yXCowmScQUQ+hgpaFNQd3QHw0M0AhzcB3P9l7YN89HKePMxm6F0DZ8ZyFV1b0s1RXioAm+0ITs+uTUmXF6+aDOkm7RBbYcJcGy5E+KrVS52i+QdfdIb7BZNeztF/fh6oh9v5/LBXGOeq7ry/2bynBEfvkV81i3uUOGD7wa+f2g//9ttnnTXURRrlXy5YMQvmKDE1RDKtSDf5Fgeha/eDAOhfsMWHUG6O4mKBhnrjz3ylOHJpYy9iDMfP1CblR2ppi/uTy9wWv7ddvdKedHMGHpruXx6uDBhwP2ujXAsp3XPUHgVOTSu7ow2VbrRLTpxdXqQx/QubX67vXVwTEocN8h+8zUX4fuY6GmYvOxV4A4XU1Ne4sNy5S93nfdl5i8uT7lLWzwRIt3kFS1yTuz4DsOV2gF/9rV8o9hi0Vk/3cW42KhKikRQo25Jx9F+mSIvV+KFBJ2RSO3kysfMB1/lS0m32tjTYSduQ7lFVISejtdl7dNwZ20KJekVqeSV0gQIdW/9yLUj3iVdBo1TdyUnjvMqn2MgaHgw+tXcQlszs1GuIDpoqq3hxkcgYjSgpR7qHypFuRG73Plr+YCZVmlfPhYsRNuEa/PznTNTHVsCiajFefJRwWIXUTEaI7mNnvyMKqh02lGnfertf1Z0lUaWehZAQHo9FBZZqSDd+n1OCbRMAMHHapYoYZUk3UYZ9SDejruG+oko7fQxCuhuRdFsQXU7vOzA0YaV/btjt+oDLVO8yZyGdt4wrA7OiHiVFdD4oqOtns3QxgFaMAJ+QGk+cZqrEocFJt4nimoUsMizQ0P+nogTZ6agxMmxR6eY93fi7IhWSM0OmS9JtaenhSDcvTKDYKVJDT57fU0YvR/vEy8+Cv7nxVFjQ12FPLjn6z85DYngfzBs6DHBgBUCW7iKGcnPDdeQtUEKW0fulaG1jsdHLecsP3ddYvLrtvc9zCjpnLe6rXNCj99tsd3vFzQCX/jnAvFP8hQhch3feDXB4M8Cic6ARZhZpTKSSpiDUknR/5MVnwFuuPBFWzKGYzWAQKWCgpHxFqa3H1bDH9dr3uFsMtqqXG7GhsGHcgf4BfQvdw2OWsa/0PYzp8HNTpRvthLnd1enlPqS7J3iEpek/+BrNPx2kzSakhiAJL+RZR4YZ9HK077/zMsf3cIYE+ZFU0SKk1k0FvUMukELG1+4YtFbSfZybqV6Oao4+8SfLgUPHRf+mYtLNgxO8zE1qJ33/6Z8B/Opv3M9PuMqeKHEnbYqTCBihbJxSPp4z6OWForNea/79bjg6ltNrYapzl1lQH51GumfVjnRT/yba8sug0QkDogicmUSXN4ravOQ/7nECYOzzN3tQEV0gw9Et/ouL1MuJXq56dPF3Ref9u0+UP1gZvbynYUq6uneZnZUv3v6c/hxRlCOj2bKzRMUIfnHN7LKMDNO9TxPBfey9C8sfbO4q/9950t270Df/uFFINx+7R4aBTVebLQBs8/Wgks3qroJ0+5JuhvZy8wmpzWooLdY2p5ujLjaBtUd29Dv9/yvndfuDGmXPO4ULWvFeOUNfg/wqn6WLQY9KNvM0p5uPXeP/vxGoC/v9Ca3FJBJH7iF6aZscwe8h7G3HPl0ckbSgj3qXjVYEE9V1qPfzAEb2q691sR7eCmN8GjG+0iLU6Eu6WYB80T/91tkna99zldaL4Gw0TDB9NOxqd5Hynck7/gkuR5bIlk8D3PCJymj2Wa+ERpgppEbFPRwBhsE/JZe8cE40adxrvM8Z/w8fHRZIL6dCp48e3evur2UB442wqBf0vQbcR6aQJUcxqyXdyNjTCXdVevmYd1ae+CHAxp8AnP5SDyQJKtLwkbLCLD2uL2OCKGj4PSyAXv4vv3P+/u+vPT+4NSNIAwDFgLVgWKdqp0z7RU2D2lN4waZS25hgAZjrRVSnl3vrglTzsrtJrUuylC8XUuue77FoDm4s3w/mtJVjxFpJ93FuXt9lkSWS3vdJMZUjMVzZ0EdrNA2TKEIP8FIvo5erA3rgKe//bL87OPh90b8C7H8C4ISrQdpIOXk8W6yIdGOyjX948ENUtUDTSFQAutCn6DmDu6sj3UsuALjgTwAWnuOuZ87eDxr/mBZ3Xeh3R8X6jfuGnMsbE6vbnnbHcOweGNdrSMgn2vNWzYN3XL0Sls7u8hA7zQAw6cLqYjrzFQBPfN/+YCa9nG9iYaTbJqS2e8CjwZ69dAbcselQWTsCjebTqBOhcsqo59DbL4YgIf+dbQJgZiGC/xvef9lApNsUI0SbMJBwWwD4p1ecCEPjeThzcZ93sZvtCLpI0+P5Fjw3JnqJgRBPIn1CanMbRovlSTenTSPKYtqAOmdLZ/kZHR99yRnOrOaXnMsofbRfnOCFzlan36+Osb5O9rlPSO3sVwFsu9MrhAqbTdSHUFycwU1z7mn+NDez+Pul110A//Lrp+Evn6/aLsyRYegfTL/bw5JuRJn2rHM/X3CW/4ctOLPB/aiVe7p50k1Ld9dmlzGGS2qqudfEutIFYJdKm6C2DLRdD9nvotf+L8Dj/wuw5rPQCKN1oXNExb2Vc7udpBtHzfHxppxajQVgLfhqM5MBYNKofSNM5dkO9SDdZtLN+3XJ6B6vChhUpJezpBsTbgJUVlxh3y9B6K/gfsE9Qgmm2S6Ihnc0b1mhIg2/i3jS3V2mXp71F3nxfJGwKwdRKvmPV34NYNeDABe/DabC5yJAwM0+MoyQ7mo93R1+9fI8Q7rpLsJJCLwgQfdRi17esmPRSCGYHLNZ1cIDh0kUD4wp6UYkzqpayA0TbOxlwQCnjF6unNe4N8u4Is3zordCo6xLBSpjDN02ERZcGxJr4aZHYAVZNXSBEMn13wbYcZ/rkAiZKkO6UwAv/SI0yoLQBUSUNu5zAyBMrHg/LtFlOdKNdK8PrDHoUxQYa+SSCamhPf/DFZJug17OLz5hGrVNSI0S7I+/9Ey4V80RNunlpMSse9qxL4rRhHUiFoR086TalixyIUK0SkGmgFEfHEe6D1v66ExaqC3pxjFR3327UViohHRr1WXLXFoMmPla+EaGtU0JossFCIlGz43OGflrsjdfYem1pv3Cg1mNdAezPnDcD/7xFU1f8RVolNlQF0qwUVQQC37od2w6I6a2CLasfPn1q70vmCOgHBpoyaBSz7cXqE40iry8jccURppCejlfF7qrsOe0YnIZdBeRz7QlSkiVtt1Fp77Q/dPwu8ifXCL9d/2uo879jAwS3qJASTe/n6xmIt1m0dunMSNfeKmnCGzGdJhUmWPDahp1ymM3rV7urkvJWZd99iSSxE6DmBENQ7r9xSuTueh8L1/0tcjRHc11IrAASKYLgHyuO29hofPVFpB02+aTn/Nq908DzCakZt7RtYwMCzTd051zGQ9F6ulm9HIqdJLROrWE1Fp2LJoXGBesVVEKgjjKe3Q8W1tV1OwvDKKX26wB/SyVjBLECZZo29SoD1n6xhzhnnp6uinpHt4HsOMegF0PeI4pqFrcIDNHhlFChZc1BYd4gfsCnQPDVnXZMktX66OrgFibl705+7HBQmq854mScnNkGCVXy2d3+tb3guXue7zm7IUGcjkeTC8/5caanrV48g3ux0veBY2jOXLfkauKdA/VGgAGaQA4c937ytkiZOY+4uJG8xmKKWQoRGki3byPGxkQpsganbOqZwiNj0uj4JgKnHwMo+3/NbgwU62/kAq83W2YdAeLflZsc7Ihl7wgQYHxDDYWDUWvCOXmwlhEkyVxrBWXw1TTy7HIiUkU/xrdVaHvIhP9tyVKRzZPi7vIbNPgLW+oJULrwJkANFas2xgrV9W3mO1dfK/wQtU0MFNIjWI6al3C9TL9cM1JNxV68Q5C1gj5lw6XOl6qVIAw98t5r3c/XvVeaIQRS68SvRyLevwcbTnk3rVzWXsTUu6JPaK1NHgBmM4QUsqp8D3N2BBmAbjACnr9Y/6YlmaVVxNSs/+Adq+nmxcjUm1e0j2wI+D/tpDulh2DRkHcuArqTNSJnBRHeQ8oheqqDtpE6Ux6uXmozvoD9w+iDMsugak0m3r5PQqx5OvC5xmScQddF9JtswaowoYZd6QRuEzKKcIgdQ1FnziF7c5NLtWxU/X41p5EGUk3Jp/YQ0nJVVCfpfMajamem8klskIQWdL0q1SSjfzxJ1IUGOuRacq+9ZZLYNuhUTiL5qSayKWNXj7nJIA/e9AVysJKcYDyaeEV/w/u/unX4fLTXw6N8i0c6bb149aCdFvNFK/hzAhq0eC9YmQ2tPd9m9xAUrj/v1rvMhp+eWAs60uYag5yyoT3aL4uIS5dbuI9ciD4/02REe3eh3SzKRlu0u0X+SlTEa650DlZ/r0TrgR49Jvu54vPA/jzh4P3wzvudNW6FxrU84bRy/1rgIgbTxb+b72rJH7pSqNgEFpfxHLfEBV0iu+ijLFfKLlELQmkAKN/NZPuvYPlrKvKviUA6ebCgrw9bhqYybyij8gWwXoW7hMU3OM97DUXOqnQi75SjzTFMad9/pFhNjP3y4s/D3DhW9yz1kjAQO0XuotQ1BW1afDLeMaIas+LNCZ78Z6/u9ZhDOh+d75ftBBYYwUX6yoAl4InRphtcUFCahXp5Yh08/Gdbd0evZzQb9OokHGMWQvpPs6N5iZ79PKCf0awOmCcZk0Hrqakm/ejajESY2QYR7dPexHA8kunFHGxqZdj8EfqweSA0RnZ+qOq9nSXjQwbD066+5b6eyqnHF2wI92YYM3oTGsmBGcAPLS9vzaUjgupYVKve3R77SM1cG3IzL1UCdETovPhpU0XOr+UNEJnXF6UdC9jSDchNdgHrmmhvh7dAHo52vzT3PVBAbUgQbC2bjjavbIh58uGdNuYNObXiElT8xQA25xuOkN8vi6ZLZFCITosXDTAUqnyPjpTpdv0K6GQbj0yzIJ0Vyrq8SBxCswmMEcJE54JCvBsc8y16GCtdGHuc+kscOEiDPhQiTpI/AnR74Vnw1Swi7CwxymwxAiwrcsVbAZ1xXVB3YNCvrynu1JiPcV3EZ0jWhc+O5hadpykmyUR5Je720LuFxPpxj1DZ2qaJVTmNA1djEindOxCwEld9HK9Jn2eRoi5Fvz8mGcJmW1LV9sp1g2Y1EPq5QiSnL98lt5D/BzRv53T7QdS5vS0wykL2O9q6+nm6tvTbI9U8rl0F5FPtd/ZNSbdal1SWKijuCWNwnIpVy+iEkvkGEW6W0n3cW4kduTRy4tlI1gwWbBRcWpDulkAOLTXr4prJgYNEKUJr17u/t6IPKFfwkSChDTQIduS7qpIN13WmBBghZF6WCiIwY+X/hnA3FMBrvsgwMprvP87xeiCidJ5F3pS7wcMCG297tXp5UzsyEGz1UXAUVtCM9FOUAItaOaImpd8wS1WvOFHAA1S6eaXNCXYGDBn0n5aGxld7tUpoObIMKNNY5oaR7pJtZwrCQf1jNWOdFcQUpvlzpe1WgPm2YdFuk10wRx3FBnppuSSo9hBSfcUI9022j2ti5N0U/sKQ6KCerqrsmhMRBetdwHAFX8FcNar3BF808SIFktIN6pyo3/BbUSUYbyfbEn3dadV2evmWM9aerrJphzp9rOu6C5CIGGeuoMR0bUxI6oj3UY7gka6WeL4llsBll8O8MqbYTpZ0MgwXBcS6sRpCHXRy3Gv0Jq0e/dzCWMWMjxDXY2dOx2GMUKinjivnZ8x2zmqzl5kRRo+o5vswjcDzD8DYN5priDwymsBXt44vYxqLT22QucC1btuOz/UDsV73WtGujMq/nUmRlQoCrZGhrXsmFYvz5XThTHIGS8WyujlZLUh3SwAHNjmD/pmGyJAJlo5LWj3/mqxsy7sUrON36jqiKjqicISGBSPHCxPBm76pPf57nUAv/vHho2oqS1h8F/oqA1A+wEvLVsxwjYXM1BIjS50Z+QGW08cL0E2s0Jihajlm34BjTA+egbXA4vidLFjksQvexRm+eytm5xq8rbDo7VRYznSjWgUIbvTPOn2FSMKRQdpIT9y9SnzYHgiB+t3HvVV0tH/cJ2AilZpTjcGeFhJt4k0NmAsWG2BjldsMAMbs2g1GRXpJipwDUl3aYqTbt1faEO6lZBakC5AzfRypDLiupuILtn1H4fpZiZThsSdUMUfGWlIh337t9bBn1zm94effOXZ0FUromv2ozYD0m3QyznSTewjWwKF1t0eUkiNUF3+OyPT4U9/BdPNTI0R6snFNbny5Lnwiw37HHHPv3qBq+z/zfu2wyM7BsLRy9GoRYWhuCVkKXI/M8xEshqEaFe7jyjGpbsIzwi/o61JdzVxXE4vJ0YaP1vI4MQ/UyAKXMlSFp9LrU6LZ3Y6EyNoPXA//fPap+HqU+Z6rXF8nFyVnu4EtZe2MXDNGdN4oOL/PdashXQf56bp5ZqKxJMFhnRbEqYZbKZwTQEgKXBT0DfLSLqnURJBwQr1dNO6IO2cC/qY4xXQfPM+beb8norSiMkljUgISgZ4zxMXcJkCKx/T4iFwJKyHo50GRssvLj6LuGrfpS3IQUMGANpl7wY49aaGjb+qpgBqirT4hNTYhf7dh3bCTx/bC799+mDtIlAc6aYJANOsSGUzPiKOzg/5Gezvt803pwse81Le4lIT0j0x5C9MmUU9sjknw1SaTaWbkktiOpfPSo2KdFv6C4OQ7ilmGtkCwKGJnB5NSb6H91yS9VUaXem8OPPJDhplILrT2Ex6OQW8yLgiRBfHY33yV8/4/l9VBg0lQij4ZCLduqc7gF5vu78bbORz84ZgGMY0vABsQ+qqC+8ZzAjfPOrpbZWQ7gtPcHv8n9rr+spnDwzDR3/m9aRXb+nBdgzlgyih5uJp6EOWqcT7ord5e2saFiMopkXWA30vKOleyMZ6Ws2nXm7xudPUbD6X6OUkRkjMoh89uhu+cd92+NNvPOLzQTWrl+eM9tIq0zRac7pbdkyaRwH193S7iC5+Lx8P0o1jw47u9Ad9eKFzYaxplHR3GutCiDd30FiMsFEdq6IuNLcRf29cE0S8K80IxsDo3etcGjpSIKfQykeGeQgcBTIY5NhoxIeMPrLgfrHR4CDnug8BrLrB7b9EpONNawFmLoOpNkysc4WCPj9W9fJCCYYnyos0VZNLjnRTtRh7oaZ5JRiDYtzqyCx31yWj/QieL9o7dMZw3zy1Z0j3k/GRNjWp3Zt7Bv0MzVo++QUAF/6pu25I7ZumvcsnzOl2GBCHGYMGGSK0bqGR7mTtSLdW5J5GxQhah7ndbTr5tAXFpi5CmfGzgkGxRnSnf2Bs0st3HqGpB11WkSOyOdXQOb5fsDXDSbpNpNtPCc6/+tuQnjzqsoxQQ2Ia7ReOdPNWJ9o33JYZ8+6rCqkFFYGnoQWpl2MRlO4aWqsHtx4JF9M5vezdbjynk26jX/k13wE49IwrTHj3v8J0FZgb99HLac386uVoJ8/vcXq4K1q1nu5papWE1JbO6tTIN+qP7FMihGi0RrX6XSfpNoWUKwFNeE+bI0+PETs2f6uWRVYY5kg3rwxyFW+ymkaG0eXdv9WlOqJzop5uPPDYe7n/iWmH3Jnq5TzobatSFa06AxSNkm5cF+cHzq7sZOZOLTpXC7pAfZVHx7JlI4/4eLrgF2fKqEFBDibmJ11r7+ueQsPfDXvEypBu7OlWF7op0kJWnV7exZBLplw+xWKD1QyF4DDQwwIM0aN5m0a7LvQVHar5NZ+5QzNHamLR8ECnWPCKd7RneHKJ543T+6YDLbZQHuismNPlJN0Hh90A59GdA/BHX71fF7k4Zb96kWbMQ5qChNSw8Kk0JUroiy2DARqeRLFEktpUsKeS/C76F9OqIi5lSHfzBMYmvZwLMNpQ/5rbnMhwDZyke7KcAWBQgks4crB9erADykU97Ui3rVCFc9wrv3jzIt2aQaRbBqkFzGNd4f2Mf+7bYiTdtfjdNpV0jwQk3din231lwwVNqxm1HNC6UGzXyenlxpxuNKTkVzXe6mTr6Z6mZisAe/Ry95xjPo5CjW3q33ImTdX2FYVWJzH2J5ZeW3d1pPsYFVFDa9HLj3MLUi/39S4Xio5Qi2knz6uBjkjB3tA+9yOOA+MXOQ8Ap5GQmqlezi90roJpS6JslfUyo4vqyJZp0WcaB7pAapco9EP2pstP0MnEl19/QeUXJ2eMFVESl2uCIAfNpEpTgMzVy4OKNFX3C0cuSSxsGhWoaile0Tmij+hfOMsGgz/eqlETi4ZTQCcGva8TOsd9yxRTym1FOa5xMKKEKi9QKrqP7XSD/Hd+e51v1Byn7FdvR2CILg8Ae9T8d+cFmQhSJY2EBhihcDSbm4/vwQCP/C6O9wmddGOBiu+XJgqMTXo5FR1wTSoV7Gqil5vTNMyebjMwnuK+3EpCajakG/2tDTAgdfPqa0Jzug2x02lsPG4zkW6uNo3xy+aD/ukOPdWSKOcHqLUbPlA9brvpn91Z9y/7Mky1kf+YVOtC/tePdPvvaPQrf3hhDUw635zu5inodWXc9xvvGG9+ubsus7radByMa9JvtAwur4Zy855ujnS3BSTdc1ax/3dsjgtDayHdx7mZ9HKOdJOT+sdfbISZXf5DgMnnBStquICoYk6CRmYFy5d0d02/ZCFr0MtZMSIIuazJ6KIipLtSb8s0Mg9dKEe6KQAk5WWMiT76kjPgYy89s7YX10k3o5c3QZBjG9Oi53QzbYSgpDtUj67t4prGRpc2XeTjWaWN0JaCsax3jlDYh1ttSTeb001BMZ4r+jpPIvkEgCk2TX2dyOu57iOT7r54/unz4Qu3b4ath0edPl1TB6E2pFvtF0ygKEniySVX+udj53D28E7/+9BIoySR0O1+VYTBQt/MzoyeArDl0KjeW+SXzfspMABEEcImC4xNejklkV3taes4n5oVum2ork3VfclqgGd/DdPNzJFhNqQbzxhRaEMVaaioiWwRRC+JYdQERWBCdD0hNQ/p5gVevKtIlI+saksP9xka6a4g7orx3bvuhelgWkhN7RMP6caYTp0xFtP91xsvhOefXiNSb6WXT3+0Fgud1AKGvzdqRPAxjXiO0MfaxHGrniGuXo5CarbxlT3z/SODj2x2P6c45xi0FtJ9nJtOuk2ly0xKK3xu2D0Idz17yPf/Lls5Rzv3mlAXCopNhI6LsUyjREKPDMsVnMCYLnQupDYa0LtckxHSTeJyyABoAisbGcbQhR4D6ca10rOmazEd6DB6eRMEOTakmwJkLFxVa0eoahzpJoXuaXRWKhn5ECpekZAaoi7e5IQCPLZLvd+hxhGyOd02+ifOKydbehFMF6PfDSl9GOBgAYuKnYtndMJZS1z0+Y5NTKlfWU0+N1MF6eb+5vzX6y+VZgb0ejfI5igBSgruSEwOhSldsUJ/uPK+G1z15VryhLIxUNSKYM66n4ZmjjoihK4rk4LVKyoIndVqHOm2MWkueKP77eT0Qp8yFdTL+RkjBg2JzqFPpr1W0xki3zINxl5FEVKjwh3ez1TAQds9MF42rrG2H9DtR7qn6Qzqanc0JZfdbd44Qn5H13QH2QrAQT53Ghr6VWIo0u+txzR2pPVIQvyeOVFjeTXlcoZYuz3dFno5tZpSWwLdS0G6I8eAtZDu49w6mIPGC8pLopLOhf6oojmSve6S5bBybje86Bx2WGpBusf67Y4I0RX9b6dPAEQJAa4JUo50T7dTFU36gkOqFIYyUvwMEiOZppY2Ax2GLhA99LC65GtGWmz08iZDunW/WAX18my+XKSlJqMzg8kljddoEmYE9sv5pgBodCEJ7ZNeEHTQENkLh3Rn7RoAfYtdoT08W9MI0aTWHWpPYZPDHLYItu08vusorNuuCpVhkW5epKGil+l333E3wI57Ac58hVuQwIBoiltcaBYujWHU/dwKATeT7j+8aBnM7+uAs1WRIlSRhkQ9+f0z7ZPukh/pbkvBZSfNgXdcvRK+epdiTAHAK89fAu+65qTafwBHukcV04HPzz1tDeT/4Otw11P74SqYxq1O7C6iNjAuNPeVN1wA63YMwKUr51QvBmu2CLY6sXnU04heXyuN+r4t7nu6esVs5/cm34PK5dF+QJd/fCdXL2+iYgT5GRQctAmphUu6GdJNhatp1C5ZybCPHxkh9HtTkQqLNKTb5CDdisEYBenGkWE+PRqypRfqTx3f8yc/A9hyO8ApajLNMWitpPs4Ny4ygnQjPg8WL6f/d7eara3sRWcvgitqEZYIRLqN4K934bSkl3MxtIlsURcjeF/U9x/Z7XzESiE6rVBGlCyiaDUJcpkxhDc4umAKcdSEynGjNUAaNRVpmgTp5hc6IpekS+IXUivoftVQxmlqg7uaSgMA0Tg+noW3aZDvwa+ZVfSaGBK6RzcbrAEwTYT2TMOADpFcDGaoYEdK9xTMrDfQ/5p7unk7Ar24qdI9YwnAOX/ofr7sYvdjLmKrTExGyTX6D2QWaeVypcLN+1FpLV567uLafwBHus3xlU0gdmSjl+M5ef0lK/xJ9wVLYdWC3mhINyVSRlGvdNpLYHjrWphOVibqye4iXBdMGDgl9vRFfU7iWduLsxYNXQCe/ii3r9UpV4ADQxNOOwbWJ5CdiNaOxYh8Ed77/cej/QCTrdgkgAEvjCN7kRh56Hcyas2+cqfS2AmbdOtJGsiiGW6qdcHfcxeMOwJy/3bbs/rrRC9HOzqeLaOXz1dF0pro5c7IMAu9nK8RJt0Lz3L/HMPWopcf58aTbqQ4Um8hikvYkuuax5CYSLem8xnB3/wzAJZf7o70mUY9MJgoEZV6LOeN7HEQOgNx4YqfVH2vauRsaFxYkwhjeSPD/O0Ijnq5IeoTGelGGz3UVFV0Tl3j6IrT062ZEeVihDUZPzOE0HEkqono5VxIDYVa0LYcGvGJhVFvXSh0ocmYEVzo6ck9rgjcCpVsL5/jvt+oYl4X0o1zy3F0Dxoqk09z6+vwaJ4Y4O09Ou6bkUvf05OLDDXdUIhuEyXdnPrqo5cr/2omBzRFombjysvkd5ugqKenAFiQbltSUFPBqmxixETTiXpyITXUhUBbNKNTxylm8erNV5zgrNU/vqxW7RXjTm6S5JJP48HCHiHeTtJtETOtaTqPbU63Tror9LpPI+OJNRepxK/ze2pows0Nrlo1F85dNhMuUjPfaxJSw55uG70c7ZVfc4t8OA72OLAW0n2cGyZRGLxg0IvoAlFMUJgGA+MfvvMyeNVX7g+viEpmJtJm0o10rT/9FUxHw6QRkUlMGHhPt3lpUaUwVNJtXlxNg3QbY1rYnG7zVycRrXB7BV+k5AV/TVKM4Eg3Xeb0dRq18fS+IR0s2xR1Aw3PSDIDgBQtnXRP/6DYpJcjsnvHpkNlSfejO9yEGdsTuHJ1VeNzuptMAwCFwdAGx3JwjxKRoyLn8tnBviAU0k2iNBjQYHFzmhuik1jUxXmwWKCi0Vg4+QCNJ9m4DqH0Inz08mxTJd2c+uoTgFL+lYshoZnFz5rvaBwDRQlmE/iXdADSTUAC7puNzOfWJBJmi1OG93nj9ZrsLhpTUxH4nuAJJrL0PvqSM+EjLz6j9vNkMhLbeptO7JRa4BBccoTUjAKe26IQIn7hBWCddDcHYDCzs03fRVQU/+ofr3b2AyXdSMWnwvgXX3t+bcKVLH5LFVGMMCDpPufV7p/jxFpId8t0IMeTbqry8ao53lkUKNdsZpLdJEkUD2owyOG0WFN8ZEGvV1ggdLyqmdXhJkm6g/rosIpsjq8JnXTjpU/rQL2FTbJf+BkipBt/HdwPZhUdA8A1Z7ttFWct6Qt3jijpbgIkioIaQuf+v19s9O0NmpVLZ4vPFT5fjc6qHekebBqhIzSOIKxXuhnYzkPzl4MsFNJNdsKV036mu8mk6h+dZPOoKelOhluHoHYELOgR86oJerp5coltPXT/kL/FZLKvw0Plwifdal0G3XYpSKSaIsHkop60NhzR5D2nvF2sthdnZ5DGnTYJi4YjusSK4KwzDhrQOQpVwDJjlSakl+vWFcWGMAVxCVSIJKTWZPRyr2/bY3SaLJr9gxPRGIwqbkkVkXZPve7NEetKWQvpbpmjVO720ZUrN3azuY2zu9trR3JrRbqnsZHjwUSKkG5cq+2M9omCNX9wwVK4/ZmD0ejlZE3iiCqhC2ZgEykwxnVAIRKiC1N7wjS3LkWjRkV7LaKWSmrhGm7/sOZ0eP5pC5yZzDX3pOI5wkSBerqbRkjNK1zdoc4IfX3ZrK6yhAsr7Dg+DAUbw6lRq0Cno7kofehvyecunOHu9Tnd7U6Bk3QBMIimRKsm9MX0sXwu9zS3LnXf4Gg5Gme03JJ0h6IKm+tCBT1MLqeRjkgt9HI+2537Wzw7tI9C08vpjj5KvmWuf6zctB9f6QnA8jPCZ3HTvqrZ8PfHIg326A7vVS/cZEm3s18KvjafsqQ70jlqzqSbMwCOGCKNyK7hRqBC6IJeMwqpsbuIMzr59/YOjutCF2/zqVlIDXu6x5Tf7WqO1jgpm/6etWXiRj1QOM4Hq138sJEitfu1CDUaMwA0BX2msXX4kG41XziTgoExT3Do7246DU6e31M/0t0kiC5HF3IGutDdVifSbVuHJlkXGrsxYiTdaCbSfe2p853+urdetdJRX67JzOJDkyTd3YxefvZSD4XG/YOJN++7xAAIBaDedMWJ1h672ih9zREAclVYby5qShfucEwW2cvPW1IWUIcqdDZBYmn6XFyX/UMTvqSb/+6RCnrkS8ZpkkZzrAtpQiC9k5AodMN8PfgILBIvjIx0NwG1HC2j7iIH/WdIJfndupBu5wd0+ieMNAnS3ZaiUYxe0t2Z8e5mnjSZBeHafoAppNZsuiso0jjpOzdHWUwXyWwF4Ca5i/xJt7+4S98jNhbe2VHGwCLSnWgivQhJayXdLfNmdecKjoKhD+lmVLXQwlhWennzJN18VrcnpJaCv3rBKiehfL+aE8vtmEe6tXiNd6Hrnm4jWYi0X8zqcJPsF0KXnKSbZnSrS77bKEaEEmgJKlY1Af3Tj3TnNTKFdt7SmWWB8YJaCxC2Ht0mQxeomInMCPyD1tOesWpnXHf6fKcNYdX8ntrae5q0cIXWqZLpfYPjTo8yL0DUj3RXGV85zQudLtJN9E9XuZyM74tQvcvOD+jw9y53zWq6Hl1CuvFr9Ptz3xKqP9c8N3qsZ1/TCamZonv8+zUX8cp+gIl09zUd7Z6SbDo3f/2CU5yYDkfh4r/7y+eviuZbUO2eWleaZF3oLkIRZS50ahNpDF280vRyJtLY3RyAgZS16OUt04jkpIVeXnegYyYLTRLo8ITh6FgWfvv0Ac0KOGvJDNjwsRusaNyxnnSTmNGP1u2Bd1x9kq6cc0pfv5rzGAnpbmvOhIGKUyMTDOlWl/zc3jZfv2XoFg3buWmS/UKXNCYLI0r99Euvu0Ar6WJg/MiOgdrnftr66FB1GRGGJkIX6LwcHc9pKiOnBbtJt4uYnDCnG37+7isdRK+mvWOyIpqkEMF9Bs2Kxf1DyWXdPd3kS8aONNVd5M3pLurRe2ZBkxc7QxsVr4h23yTJggcWFH3aImSLZ3rvL6flh29HONRU60L3DvoLEqb00ctTdSbdJr0c/UtINvbUFmlwSk/e53ORhUUxHZ6zmphW3OjewXaEsYGmYgDwO1prF6mvmQBBd9g2DeVzO3JHIUGbpKeFdLfsODeil2NfNzkj24zCaNXiZhZScx3M/zyket0CihHccV18oiuGFB7RbY4kisa0oHP+wSO7yi70utEFc12m0Ri50PRydcljjy4ZzgeNZE2adHczejn5lplsxB7vuySV6kh9dE2GLlByiYruNlowV+rGM4WJJ2cKhCp0NhG9nII96rnkyEom7Vcvj3yGmgzpbrPQy0mgkOyCFXWg01y9vAkLV3gXmbRYE9GNNK6R9sfIgaZaF55IE4jio5enY6SXow+mKRJNJKTmtfSUq7qHTrhNBfcmO0fkY3nsYvZ0R2YvqgKwTrjbepvG70pZ0yXdk5OTcN555zlByGOPPeb73oYNG+Cqq66Cjo4OWLZsGXz605+esudsJqOL6qDqoaOZqeX/LsJ26TKS0CYRxuIOZscRTzjtypPt1Jif/8WV8BfXnQyfeNlZEZHu5giM+Tish9RMR47QLWfKy5GS7hle/6pjTeKgNdLN6eXq8uaBTS6sQIut+JBMez1kTUQvH7WMr+FFmvBIN/V0M2XUJkEXaF2otxDXhNOCeetG6EAnlXb3SJMVaHhgPDDqJgvdDFlpqxvp7mx6pFv36BqI06suWArvu/4U+O7bLo2OdJM1SbLQyZPufDnSzY3YV6GM9kep2FTrwu8bZOlVppdHKYwzf9Ika2LSyynp5ppFdRn63LJiZ3PcRSQyOMDOCMX6ZtIdukhjgmzdx7eIWlMm3X/7t38LixeXK/4ODQ3BDTfcACtWrIB169bBZz7zGfjYxz4GN99885Q8ZzMZOd6Dw14AaENVULk7tJn9G02EdBPyRP0/b73yxECnc9K8HnjfDadq2mxVMxG5JgmMnz0wXBYMdrMAcAWbMRyJXm7OzW2S/dJTAenmVtP8aZvxBAFZEU0yAopYEIjQDSt6OdeJ4Kj3UkPNvPY53c03poWCGkK6zTFPXI05kvEAsElYND6ke3Sy8qijSEh3cwqp+Xu6FV3YKMRgweYvnr8KLjtpTv1Jd5MkC7Q3UIsGW+MiF3qDzCzKNElBD/cLXQ8k+krTNWIRUuP+pEn8rSmk5uloxNhha65Fk6wNFWSopYczicykm4Rza7aySRrz4Xi3pkq6f/WrX8Gtt94Kn/3sZ8u+953vfAey2SzccsstcOaZZ8JrXvMaeM973gOf+9znpuRZm8koADygkG7zoL169VLn459fc3L4F0dkmyeYTYIu2BCmSAqoQWZe4E0SGL/+Um+UE+0XXi1efcIsHSSeubivzqQ7UR4QTnchtYm8LtLYquhR2eU+pLtJCjS8io7FCBu6cPGJs2FWVwbOWzYzAnVNJU3Z0aZLuqkgRUExb9FA+4cXnuYEzjiSMJJxRlGTsGiCerrJ6u7ppjPUxPRyr0c3xmTBbOFpkjOkp65UQLo//apznI///Iqzw/8AsyjTJOvijKlUZwU1I8zWlbqF1LpmN10hgvsMpJeLnCO+FhjPJWOMFwWN7l1aE9wTxLrCvcLjOM50rMlM9L+rhXQ3jZDagQMH4G1vexv85Cc/ga6u8iDi/vvvh6uvvhra2jza5Y033gif+tSnYGBgAGbNag5Fzqkwqg4HJd14cX3oxWdY+7xrRrup57JJ0AU0MwkwKX11WdlYn+ZIpC4/aS689uJl8N2HdsF+NduSV4sR8X/4gy9wUJmax2EFJd0YFDcJosuR7p3GfOFYjCcITZRE0bpgTyUVyfl+6e3IwD1/d100xIXm5qLQEVLMmwilMxG5ng6/b71k5Rx47MM3QF+UMY1msNMkvoUn3VS44vOV61cvV+emVGgqvQj6vRFlMoVOY7Emp5djMYKQS/Nc/eGFy+DGMxbWzkCrWIxoDr0INPSnmFxqermld5n+XWib6RXeoRBBoG6KR6nt+P/buxPopup8D+C/pEnTfd+A7kXLzrAIFFlEkKUqoihHhmFRhIfLgMpTQAYYjiAuTz3iU3HmvFGfoqCjoPIKyirgoCyyCLJKgUIpO12mlKbtfef3T29yk6alLfc2uc33c07ISnJz+8v/3t9/vVRKftXnFQ1e074uyt+NTlYXYcGuS726nPd+9URvyvzLmsb11DMaSfKzkEE+PgfqY9k98vWkmycfmjBhAk2ZMoW6d+9OJ06cqPGagoICSktLc3osPj7e/py7pJvHh/NF2UWdWa1WcfFW8raptY3yb+xcdRIVFuBX472DTI3/PL/gWDJe/l3crjCYSfLifavk73I84vJZzbhQngZYJSO/udfFhjsJ1WsrF1XXjHItuvLzQvz5gFYzhuolJNG+XzhHq9BJrARU/4a4tvjERdv44lbhAfZ9kBgRQKevllGbhNBG7Rej0Z/kQ6FkCmz0fmmK+HC3X+Rkgc91zIYqp88Xv7OqSrJWOcYx14sp2BYrVlslh7hpDFDld6Q1s9G5y0Owv7FmmWsmqqho3EmtyRzA/UQEq8FSr33S1LHhjmvv4ACTwb49flTl9LqGbqfB6O90wlNlCqBKHcQK/zZkBYXXxHWopZHlqxsGg9lpv1SYgmoco70hNlyZFPFwucR27mLxc8SL8nfUmO32MwU4dQW1Guv3O/IG3FpZrBjLbhGnF7ZtV+bZvNZ5w/eNyXGMLr0kjkXeGB+uLH6OMvf4Rds8PYGNKEdq42cOtseLFBCum3MX12NRgMn5WOT0G6h0PnbXh4kbDKqT7kr/UKrSyX5pqPruF48m3TNnzhQt0XU5ePCg6FJeXFxMs2bNUvXzFy1aRPPnz6/xOH+eu9Z0b7N27VpV3ic/j39WRsq/wgWRgUoLL1FOTg6ppe+lCyR3SPp+bz5ZD6j33lo6mW9LHmVHDx6gnEv7VXv/+xS3c1avJm+MDXdOnXXeL5fP51NOzml13lyS7PvFYC1VNQ61VCjObUz07+tW2nvsjPgdXTp1mHJyDonnx6cSbcw30sAWVxr1ndqfPkvy4I5LJeX0403uFy3jQ6nY6nyYsRglMUxIDYaqChquuF9pMFPOd03zvW7WyRLn/VJy5aKqsZ513Z/k0XMbf/yZrvnbKj29KTbcOXLepWy5UGDfL7lFjn129vQpysmpWflel1aXD1N3xf1TZy/SXh2UL9xDxEB+JJGBdh08Lo7VF86cpJycXFXeP7o4j/oo7u/69TAV5OV4XWy4G6oj75fte/i47EdXL6v3O2pfUGgvc9l3m7ZRpZ8+ekeYKvk3ZLB3GT6wbzdRni25Onvadr7HCs7kUU7Oycafu/z7gtP+9qb4cBcv/kY/Kq9y9J77Zfs2OndAnffvcfUatai+fam08qaP0U3FFiKOY1FleZmb35Dt+aKShp+TDa40kNzv6kjeeTqik/3SUKWljsp/r026p0+fLlqw65Kenk4bNmwQ3cctFuduUNzqPWbMGProo48oISFBdEFXku/zc+5wEv/ss886tXTzrOc8IVtYWJhX16hw4XbXXXeR2Xzz3cwOrTtKm87m0vXqwigzLYmys9uTWkwHp9pv3zX8IdKLoh2naeXJ3+z3e3XvQsM6uI+lRtltu6pq8QfKzs72ythwp2z3GfryhONI1e6WNMoemqneBygWJVBrv2iNJziau2uDOAG8YOVitYLuHZAlxirL6i7p6mbctIfogq2LV1R8YqP3S1PEhxKvn/uXnevt9yOCAyg7u79q7y/9FiQqZ5gxKEI38XL0XAm98eu/7PczUvhvWs+VD+rBb9V3RHttFYQDBt/jPA7TS2LDrV8L6NPf99nv3pqWQtnZbcXtgqIyWnxgs7jdOr3hZY7hMNd2LLHfT0rPpFaD9REv8/ZsED2LKsSQiiLq2rENZd/uMulkYxV2IvrvRfa73Xr3Jym1n/fFhhuzdq2ja9Yqik9KJzp1kpJbtaDs7M6qvLdx2zGiDbYKQi7Xh9xzv26GO31WsIMKcqvXiyaifr17Us80Wxmwb81h2lxgS7RvyUij7GGZjT534aWguMz11vhwVRibR/O+PWi/P2zQAEqMVGduB7+vvyHab9sxUS3TdXMs4mErM3Y4KkuiI8IoOzvL6TXTtn0vrk3+FsrOvqNB7+934q9EV2yxeGvHHtT6Nn3sl4aSe0p7ddIdGxsrLjeyePFiWrBggf1+fn6+GK+9fPly6tmzp3gsKyuLZs+eLX788o+eC4HMzMxax3NzEu+ayDP+/95ccKi9ncEW5/eIDLao+/2HvUq0YjLRwLm62K+y0EDnZZlCg1TeLx1GEu3/koxDXiKjyvtFyxgOD7LU2E+qfhZPQmK1df/SS7yEmUzkZzSIA5jc7T4lJlS97bc4et4YLcE3HS9NVcaZTCYxsV5F9YBuHrus6udyElKddBuC43QTL1yWKIWr/RsKc1QOmoPC+Q9e7//qyeNfiEuZq4yXVpGO05XCssqGb2OA89h2P0sQ+ekkXnhMMpcreVds3cujggPU+xtFJRMZzURV1d2PgyJrjRdvOzfieVauWcupqMyxlJpq2xcmt1sSGSyhZFbMF+TtYsM4kXQk3eFBjngJ9Hfsn0bvr4HziNbPJxo03+n/e1t83OicLjJExd9RgKOhzhgUpfo5nVbM1cMReA4AefJK130ytlcKffzTSXpuSGaD95ekmI/GLyRaN2VuQ9V3v+hiTHdycrLzgTjENllORkYGJSbaZtb+4x//KLqKT5w4kWbMmEH79++nt956i958802PbLOeuE4+ouokLazTKKKULKLwJNIT19nKlTOAqmLEe0R3ziGKcp6LwNuFuFTSqLrshnjDOKIr6nSdbMoZY3lte3k2ataoyXvqNTGWPiYLk/cLT1YjT4yleqzIa+iyCatIr2VubPU8CaoJVizNopMVANwtM6icSE25jrk86WeDuE7iqaNJPfmYnEfX7L+jMDWP0TzLMk+OVT3vip4mDLNPvFc9Z4Q8o7nqvyGdTC4niw72r/WcTjl5WqMmUmN9niFqP4IoMk3X53Tqzl6unEgtQnf7RU663a0iMn94e5rUN52SoxteZnLSbdDhBHNa0dWSYXUJDw8XY7Fzc3OpW7duouv63LlzafLkyZ7eNK/nuv62qgd0xl2y+KCuk65Z7k74VC+g5ZNhnSXc7mb8dLc01k1JVI681I+IIMeJDp/8NWot4WY2ezkLVvyOVI+VsquO2/XoQu0tXJODmBCVW9Gi0h23dVTuBtRzmUbXpKJeXJcI08mSYe4qwlWvGFcehxQtdnpZBqqwujJC1TI3OEa3SbdrJZ6yAvim17uXyxQuY3RUtrie03HrrnIm95umjBF5ZQ0d7hd3K0NwhWdjEu4a5WyAvvaLFnTR0u0qNTVVzGjuqlOnTrRlyxaPbJOeubYuqH5A16maS4bpY91FrYW4rCncJkHlkzQejlBZTtRlHOmJsrJK9d+QWZ9LQLkm2sFqLrvHKhrR4umFLd0xISq3RrceRNT9UaIYFedaaAKuJ3yuZe6yyb3oo3+doJnDbOO8G8S1ZVsnS4Y1SdLdeyqRtYyoVVdbTyOdcKx3Xz1Lt5ot3cr9ENeIePMgZSUe58WhigaD+DDLzbd065SyEk/1xqX4jrpu6XbXeKAGKa4d0YktutwvWtBl0g3atrog6bYJttSv1cXXKLuXc6x0aBWu7gdwi+Wo/yW9Uf5uVP8NKRMEHvOuI8ou5aqui6pj3MJi5uWNKiVtkm6jkege/Q2tck2yXcvgXunR4tIort3srzRs9nNPqpF0qzl0haX3t110Rk66j54vUb+lO0jR0t3iD6QnyvIkLMDsNDQjOSrIqbXXlyjLF9WP0am3O26XFZKeKM9tY0JVTrpT+hBtf992JwBJt2/94qBerQsRSLqFILOpzu7mvioy2CzGL7M728SJCcRA46Q7MtV912EdUA7LUH1Md9ZTtuu+00nP5W602t3Ldco1yQ5Ws8wNdpm09dYhpBeurXI4RtuUVTjWMHdNKG+ayd9R1vL4ZR2JCw2o9ViUpNhHRp11D79ZwYryRPVjNHcvj77FdjvjTtITZfkSq3IFsJRsq4yQ/CwY042WbnDb1VHtSX10Kggt3W5xa8L/Te1LR88XU8+0RrY6NUPhgRoe0Lnb56SNtpm6k3uTnihbt1VPugf9lajdCKKWXUhvyisdk8Cp3tKtU1EuXRtVPRbxsIw//0JUaSWquKar1ktlecKtu6rPL6JT+884lun5bFIv6lG9LJZqHv2eqLzEudJTB5KiAmttzVYmVRdLrpMvUZ7DadKj87G1RFdPEbVQZ9m6ppIYGaRdBXBAGK1t9zrdMeAOMvuh3MIegBrdyxs1SU0z5Jpkqzrphs5xbbmyxhycD+KqjxeTE28dCtWye7mfmSjpNtIjLk/kGWNdKz59lcnPSJFBZvsqADHBKldGRGeQ3ssWtbt/6hlXQFyzVlLL8ADKytCgAjiEe0fceFlbb6Mcl1tU5lhRQ15Rwlcpu5dr0rWeW3J12Jqr7CGiRQVwqSWWKCJF9ffVI2QR4NQViVuicAJYs/unr419Ai/rXq5jcWHO5QvY/OfgWyk9Nlhcg/thPEgwbRIUvyH0inBYMrYb3ZYaSR8/1tPTm+K1CquXU1N67cFO1Kd1DI3LSvHZsgWNKA4pipnJUb5oC2dAQC0iHAf0Kjezwvsq5eQjKIjgRpB037gWHUm3w4Tb08QFaod5NGqeFEer3fqvY/1vjRUXqF2Z1TGMRfZQ9yRx8TXK+WeQdDu0jHAMR8C5rrYQdeBU+JSWO09MAjYY5w43khwV7PYk2dch6YaGcLccqK9Tjrk0YeJKqIeRXRPF9Xgfa82uL149AmzSoh3nLjy8B7SDMyCAeojF7MJwA73So+iDCbfR9YpKGtg23tOb45VJN9a6B2g45ZCvUisqxuHGFozoQNkdE+j21oqlz8AOLd3OSxCu+nMfMvkZxLwaoB0k3SBw5XmV5Fj3EmpvxQRwhyeoGdAmztOb4XXiFL1ErIoZuwHciQ21UH5hmac3w2sFo+IK6oErOFH5WzsMAXPWoVW4pzfBJ6BKA4RPJ/WiW+JC6H8mdPf0pniV2dltqX3LMHrqztae3hQA3c6NMKlvGvVIjaLeGWh1gbq98mAnujU+hN4do8/Z+rXyXw91psz4UJoxtI2nNwVAt2YNa0MdWoXRpL7Va7ADNCG0dIPQKz2a1j7b39Ob4XUm9UsXFwBovNl3t/P0JoBOtEkIo++fwbHI1YPdEsUFABrvP/pniAuAJ6ClGwAAAAAAAEAjSLoBAAAAAAAANIKkGwAAAAAAAEAjSLoBAAAAAAAANIKkGwAAAAAAAEAjSLoBAAAAAAAANIKkGwAAAAAAAEAjSLoBAAAAAAAANIKkGwAAAAAAAEAjSLoBAAAAAAAANIKkGwAAAAAAAEAjJq3eWI8kSRLXRUVF5M2sViuVlpaK7TSbzZ7eHPAiiA2oC+IDaoPYgNogNqAuiA/w9dgoqs4b5TyyNki6FYqLi8V1UlKSpzcFAAAAAAAAdJJHhoeH1/q8QbpRWu5DqqqqKD8/n0JDQ8lgMJA316hwxUBeXh6FhYV5enPAiyA2oC6ID6gNYgNqg9iAuiA+wNdjQ5IkkXC3bNmSjMbaR26jpVuBd1RiYiLpBQdwcw5iaDzEBtQF8QG1QWxAbRAbUBfEB/hybITX0cItw0RqAAAAAAAAABpB0g0AAAAAAACgESTdOmSxWGjevHniGkAJsQF1QXxAbRAbUBvEBtQF8QG1QWw4w0RqAAAAAAAAABpBSzcAAAAAAACARpB0AwAAAAAAAGgESTcAAAAAAACARpB068w777xDqampFBAQQD179qTt27d7epNAY4sWLaLbbruNQkNDKS4ujkaMGEGHDx92ek1ZWRk9+eSTFB0dTSEhITRy5Eg6d+6c02tOnTpFd999NwUFBYn3ee6556iioqKJvw1o6eWXXyaDwUBPP/20/THEhm87c+YM/elPfxJ//8DAQOrYsSPt3LnT/jxP6zJ37lxq0aKFeH7QoEF09OhRp/e4fPkyjRkzRqyzGhERQRMnTqSSkhIPfBtQS2VlJc2ZM4fS0tLE3z0jI4NefPFFEQ8yxIbv2Lx5M917773UsmVLcQxZuXKl0/NqxcK+ffuob9++4hw2KSmJXn311Sb5fqBNbFitVpoxY4Y4rgQHB4vXjBs3jvLz853eA7FRjSdSA31YtmyZ5O/vL/3jH/+QDhw4IE2aNEmKiIiQzp075+lNAw0NGTJE+uCDD6T9+/dLe/bskbKzs6Xk5GSppKTE/popU6ZISUlJ0vr166WdO3dKvXr1knr37m1/vqKiQurQoYM0aNAgaffu3VJOTo4UExMjzZo1y0PfCtS2fft2KTU1VerUqZM0bdo0++OIDd91+fJlKSUlRZowYYL0888/S8ePH5e+++476dixY/bXvPzyy1J4eLi0cuVKae/evdLw4cOltLQ06dq1a/bXDB06VOrcubP0008/SVu2bJFat24tjR492kPfCtSwcOFCKTo6Wlq1apWUm5srffHFF1JISIj01ltv2V+D2PAdXO7Pnj1b+uqrr7jWRVqxYoXT82rEQmFhoRQfHy+NGTNGnM989tlnUmBgoPT+++836XcF9WLj6tWr4txh+fLl0qFDh6Rt27ZJPXr0kLp16+b0HogNGyTdOsKB/OSTT9rvV1ZWSi1btpQWLVrk0e2CpnX+/HlR8P3www/2Qs9sNouTJtnBgwfFa7gAlAtNo9EoFRQU2F/z3nvvSWFhYdL169c98C1ATcXFxdItt9wirV27Vurfv7896UZs+LYZM2ZIffr0qfX5qqoqKSEhQXrttdfsj3HMWCwWcdLDfvvtNxEvO3bssL9m9erVksFgkM6cOaPxNwCt3H333dKjjz7q9NgDDzwgTnoZYsN3uSZWasXCu+++K0VGRjodV7iMyszMbKJvBjfLXYWMuwYAft3JkyfFfcSGA7qX60R5eTnt2rVLdOmRGY1GcX/btm0e3TZoWoWFheI6KipKXHNccBcfZWy0adOGkpOT7bHB19z9Jz4+3v6aIUOGUFFRER04cKDJvwOoi7uPc/dwZQwwxIZv++abb6h79+700EMPiWEDXbp0ob///e/253Nzc6mgoMApPsLDw8XQJWV8cHdAfh8Zv56PPz///HMTfyNQS+/evWn9+vV05MgRcX/v3r20detWGjZsmLiP2ACZWrHAr+nXrx/5+/s7HWt4uNyVK1ea9DuBtueo3A2d44EhNhxMitvgxS5evCjGYClPjBnfP3TokMe2C5pWVVWVGK97++23U4cOHcRjfDDkgkou4JSxwc/Jr3EXO/JzoF/Lli2jX375hXbs2FHjOcSGbzt+/Di999579Oyzz9ILL7wgYmTq1KkiJsaPH2//+7r7+yvjgxN2JZPJJCr9EB/6NXPmTFGxxpVwfn5+4vxi4cKFYtwlQ2yATK1Y4GueQ8D1PeTnIiMjNf0eoD2eQ4bHeI8ePVqM32aIDQck3QA6a9Hcv3+/aJEAyMvLo2nTptHatWvF5CMArpV03Lrw0ksvifvc0s3lx5IlS0TSDb7r888/p6VLl9Knn35K7du3pz179ogKXZ4ICbEBAA3FvepGjRolJt3jyl6oCd3LdSImJkbURrvOOsz3ExISPLZd0HSeeuopWrVqFW3cuJESExPtj/Pfn4cfXL16tdbY4Gt3sSM/B/rE3cfPnz9PXbt2FTXHfPnhhx9o8eLF4jbXFCM2fBfPNNyuXTunx9q2bStmq1f+fes6rvA1x5gSz2zPs9EiPvSLVyjg1u6HH35YDC8ZO3YsPfPMM2K1DIbYAJlasYBjTfNPuE+ePCkaAeRWbobYcEDSrRPcHbBbt25iDJayFYPvZ2VleXTbQFtca8gJ94oVK2jDhg01uuBwXJjNZqfY4HEwfGItxwZf//rrr04Fn1wwup6Ug34MHDhQ/F25lUq+cMsmdxGVbyM2fBcPQ3FdXpDH8KakpIjbXJbwCY0yPrjLMY+zU8YHV9pwBY+MyyE+/vCYTtCn0tJSMaZSiSv2+e/KEBsgUysW+DW8/BQnaMpjTWZmZrPpPuzLCTcvIbdu3TqxPKUSYkNBMaka6GDJMJ4t8sMPPxSzAU6ePFksGaacdRian8cff1ws1bFp0ybp7Nmz9ktpaanTslC8jNiGDRvEslBZWVni4ros1ODBg8WyY2vWrJFiY2OxLFQzpJy9nCE2fBfPImsymcTyUEePHpWWLl0qBQUFSZ988onTUkB8HPn666+lffv2Sffdd5/bpYC6dOkilh3bunWrmCkfy0Lp2/jx46VWrVrZlwzj5YB4qcDnn3/e/hrEhm+tgMFLRvKFU4M33nhD3JZnoFYjFnjGc14WauzYsWJZKD6n5fKouS0L5UuxUV5eLpaPS0xMFOcPynNU5UzkiA0bJN068/bbb4sTaF6vm5cQ4zXvoHnjQs7dhdfulvGB74knnhBLLnBBdf/994tCT+nEiRPSsGHDxNqHfHI1ffp0yWq1euAbQVMm3YgN3/btt9+KShWusG3Tpo30t7/9zel5Xg5ozpw54oSHXzNw4EDp8OHDTq+5dOmSOEHidZx5KblHHnlEnIiBfhUVFYlygs8nAgICpPT0dLEWr/JEGbHhOzZu3Oj2PIMrZ9SMBV7jm5cx5PfgSh9O5kG/scEVdrWdo/L/kyE2bAz8j7LlGwAAAAAAAADUgTHdAAAAAAAAABpB0g0AAAAAAACgESTdAAAAAAAAABpB0g0AAAAAAACgESTdAAAAAAAAABpB0g0AAAAAAACgESTdAAAAAAAAABpB0g0AAAAAAACgESTdAAAAoCmDwUArV6709GYAAAB4BJJuAACAZuDChQv0+OOPU3JyMlksFkpISKAhQ4bQjz/+6OlNAwAA8GkmT28AAAAA3LyRI0dSeXk5ffTRR5Senk7nzp2j9evX06VLlzy9aQAAAD4NLd0AAAA6d/XqVdqyZQu98sorNGDAAEpJSaEePXrQrFmzaPjw4eI1b7zxBnXs2JGCg4MpKSmJnnjiCSopKbG/x4cffkgRERG0atUqyszMpKCgIHrwwQeptLRUJPKpqakUGRlJU6dOpcrKSvv/48dffPFFGj16tHjvVq1a0TvvvFPn9ubl5dGoUaPE50VFRdF9991HJ06c0HAPAQAAeA6SbgAAAJ0LCQkRFx43ff36dbevMRqNtHjxYjpw4IBIojds2EDPP/+802s4webXLFu2jNasWUObNm2i+++/n3JycsTl448/pvfff5/++c9/Ov2/1157jTp37ky7d++mmTNn0rRp02jt2rVut8NqtYpu76GhoaKigLu/87YPHTpUtNQDAAA0NwZJkiRPbwQAAADcnC+//JImTZpE165do65du1L//v3p4Ycfpk6dOrl9PSfOU6ZMoYsXL9pbuh955BE6duwYZWRkiMf4eU60uas6J8aMk2Nu3V6yZIm4z7fbtm1Lq1evtr83f25RUZFI1OWJ1FasWEEjRoygTz75hBYsWEAHDx4UjzNOtrnVmysNBg8erPGeAgAAaFpo6QYAAGgmY7rz8/Ppm2++EYkxt1Jz8s3JNFu3bh0NHDhQdP/mVuaxY8eK8d7cui3jLuVyws3i4+NFUi0n3PJj58+fd/rsrKysGvc5qXZn7969IrHnbZBb6LmLeVlZGf3++++q7Q8AAABvgYnUAAAAmomAgAC66667xGXOnDn02GOP0bx58+iOO+6ge+65R8xuvnDhQpHkbt26lSZOnChamTnZZmaz2en9uCXa3WNVVVWN3kYeR96tWzdaunRpjediY2Mb/b4AAADeCkk3AABAM9WuXTvRZXvXrl0iUX799dfF2G72+eefq/Y5P/30U4373OXcHW59X758OcXFxVFYWJhq2wAAAOCt0L0cAABA57ib+J133inGS+/bt49yc3Ppiy++oFdffVXMDN66dWsxgdnbb79Nx48fF+O05THZauDJ0Pizjhw5ImYu58/mydTcGTNmDMXExIjt4onUeFu5KzzPin769GnVtgkAAMBboKUbAABA53hcdM+ePenNN98U46I5weZlwXhitRdeeIECAwPFkmG8pBgvI9avXz9atGgRjRs3TpXPnz59Ou3cuZPmz58vWq/5s3iGcne4K/vmzZtpxowZ9MADD1BxcbEYZ87jzdHyDQAAzRFmLwcAAIBG44nWnn76aXEBAACAmtC9HAAAAAAAAEAjSLoBAAAAAAAANILu5QAAAAAAAAAaQUs3AAAAAAAAgEaQdAMAAAAAAABoBEk3AAAAAAAAgEaQdAMAAAAAAABoBEk3AAAAAAAAgEaQdAMAAAAAAABoBEk3AAAAAAAAgEaQdAMAAAAAAABoBEk3AAAAAAAAAGnj/wHlPwymU7yUrAAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, ax = plt.subplots(figsize=(10, 4))\n", + "ax.plot(current_a_2 * 0.1, label=\"Phase A\")\n", + "ax.plot(current_b_2 * 0.1, label=\"Phase B\")\n", + "ax.set_title(f\"Phase Currents at {TARGET_SPEED_2_RPM / 10:.0f} RPM\")\n", + "ax.set_xlabel(\"Sample\")\n", + "ax.set_ylabel(\"Current (A)\")\n", + "ax.legend()\n", + "ax.grid(True)\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 9 — Summary" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "==================================================\n", + "HiL Test Summary\n", + "==================================================\n", + "Time to reach 700 RPM : 0.69 s\n", + "Avg RMS current @ 700 RPM : 18.213 A\n", + "Speed ramp scope window : 122.5 ms\n", + "Avg RMS current @ 1400 RPM : 23.880 A\n" + ] + } + ], + "source": [ + "print(\"=\" * 50)\n", + "print(\"HiL Test Summary\")\n", + "print(\"=\" * 50)\n", + "print(f\"Time to reach {TARGET_SPEED_1_RPM / 10:.0f} RPM : {time_to_speed_1:.2f} s\")\n", + "print(f\"Avg RMS current @ {TARGET_SPEED_1_RPM / 10:.0f} RPM : {avg_current_1:.3f} A\")\n", + "print(f\"Speed ramp scope window : {scope_window_ms:.1f} ms\")\n", + "print(f\"Avg RMS current @ {TARGET_SPEED_2_RPM / 10:.0f} RPM : {avg_current_2:.3f} A\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 10 — Stop the motor and restore hardware UI\n", + "\n", + "> **Always run this cell** before closing the notebook to return control to the hardware UI." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Motor stopped\n", + "Hardware UI re-enabled\n", + "Disconnected\n" + ] + } + ], + "source": [ + "stop_motor_request.set_value(1)\n", + "print(\"Motor stopped\")\n", + "\n", + "hardware_ui_enabled.set_value(1)\n", + "print(\"Hardware UI re-enabled\")\n", + "\n", + "x2c_scope.disconnect()\n", + "print(\"Disconnected\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/pyx2cscope/gui/qt/controllers/config_manager.py b/pyx2cscope/gui/qt/controllers/config_manager.py index e0bc1e50..307d89ef 100644 --- a/pyx2cscope/gui/qt/controllers/config_manager.py +++ b/pyx2cscope/gui/qt/controllers/config_manager.py @@ -129,7 +129,7 @@ def validate_elf_file(self, elf_path: str) -> bool: return os.path.exists(elf_path) def prompt_for_elf_file(self) -> Optional[str]: - """Prompt user to select an ELF file. + """Prompt user to select an ELF file or import. Returns: Selected file path, or None if cancelled. @@ -139,9 +139,9 @@ def prompt_for_elf_file(self) -> Optional[str]: file_path, _ = QFileDialog.getOpenFileName( self._parent, - "Select ELF File", + "Select ELF File or Import", last_dir, - "ELF Files (*.elf);;All Files (*)", + "Variable Files (*.elf *.yml *.pkl);;ELF Files (*.elf);;YAML Files (*.yml);;Pickle Files (*.pkl);;All Files (*)", ) if file_path: @@ -151,7 +151,7 @@ def prompt_for_elf_file(self) -> Optional[str]: return file_path if file_path else None def show_file_not_found_warning(self, file_path: str): - """Show a warning dialog for missing ELF file. + """Show a warning dialog for a missing ELF/import file. Args: file_path: The path that was not found. @@ -159,8 +159,8 @@ def show_file_not_found_warning(self, file_path: str): QMessageBox.warning( self._parent, "File Not Found", - f"The ELF file '{file_path}' does not exist.\n\n" - "Please select a valid ELF file.", + f"The ELF/import file '{file_path}' does not exist.\n\n" + "Please select a valid ELF file or import.", ) @staticmethod diff --git a/pyx2cscope/gui/qt/controllers/connection_manager.py b/pyx2cscope/gui/qt/controllers/connection_manager.py index 6a1adec2..85c50860 100644 --- a/pyx2cscope/gui/qt/controllers/connection_manager.py +++ b/pyx2cscope/gui/qt/controllers/connection_manager.py @@ -46,6 +46,12 @@ def refresh_ports(self) -> list: self.ports_refreshed.emit(ports) return ports + def _create_x2cscope(self, variable_file: str, **kwargs) -> X2CScope: + """Create an X2CScope instance and load variables from the selected file.""" + x2cscope = X2CScope(**kwargs) + x2cscope.import_variables(variable_file) + return x2cscope + def connect_uart(self, port: str, baud_rate: int, elf_file: str) -> bool: """Connect to the device via UART. @@ -58,9 +64,9 @@ def connect_uart(self, port: str, baud_rate: int, elf_file: str) -> bool: True if connection successful, False otherwise. """ try: - x2cscope = X2CScope( + x2cscope = self._create_x2cscope( + elf_file, port=port, - elf_file=elf_file, baud_rate=baud_rate, ) @@ -92,10 +98,10 @@ def connect_tcp(self, host: str, tcp_port: int, elf_file: str) -> bool: True if connection successful, False otherwise. """ try: - x2cscope = X2CScope( + x2cscope = self._create_x2cscope( + elf_file, host=host, tcp_port=tcp_port, - elf_file=elf_file, ) self._app_state.elf_file = elf_file @@ -168,8 +174,8 @@ def connect_can( # Map mode to standard/extended mode_str = 'extended' if is_extended else 'standard' - x2cscope = X2CScope( - elf_file=elf_file, + x2cscope = self._create_x2cscope( + elf_file, bustype=bustype, channel=channel, baud_rate=baud_value, @@ -214,7 +220,7 @@ def connect(self, elf_file: str, **params) -> bool: return False if not elf_file: - self.error_occurred.emit("No ELF file selected.") + self.error_occurred.emit("No ELF file or import selected.") return False interface = params.get("interface", "UART") @@ -248,7 +254,12 @@ def disconnect(self) -> bool: True if disconnection successful, False otherwise. """ try: - # X2CScope handles closing the serial connection + # Get current x2cscope instance and properly disconnect it + x2cscope = self._app_state.x2cscope + if x2cscope: + x2cscope.disconnect() + + # Clear the x2cscope instance self._app_state.set_x2cscope(None) logging.info("Disconnected from device") self.connection_changed.emit(False) diff --git a/pyx2cscope/gui/qt/main_window.py b/pyx2cscope/gui/qt/main_window.py index c9490c49..dc3b00cd 100644 --- a/pyx2cscope/gui/qt/main_window.py +++ b/pyx2cscope/gui/qt/main_window.py @@ -7,6 +7,7 @@ from PyQt5.QtCore import QSettings, Qt from PyQt5.QtWidgets import ( QApplication, + QFileDialog, QHBoxLayout, QLabel, QMainWindow, @@ -24,6 +25,7 @@ from pyx2cscope.gui.qt.controllers.config_manager import ConfigManager from pyx2cscope.gui.qt.controllers.connection_manager import ConnectionManager from pyx2cscope.gui.qt.models.app_state import AppState +from pyx2cscope.gui.qt.tabs.help_tab import HelpTab from pyx2cscope.gui.qt.tabs.scope_view_tab import ScopeViewTab from pyx2cscope.gui.qt.tabs.scripting_tab import ScriptingTab from pyx2cscope.gui.qt.tabs.setup_tab import SetupTab @@ -132,13 +134,18 @@ def _setup_ui(self): # noqa: PLR0915 top_bar_layout.addStretch() - # Save/Load buttons + # Save/Load/Export buttons + self._export_variables_button = QPushButton("Export Variables") + self._export_variables_button.setFixedSize(120, 28) + self._export_variables_button.setEnabled(False) + self._export_variables_button.clicked.connect(self._export_selected_variables) self._save_button = QPushButton("Save Config") self._save_button.setFixedSize(100, 28) self._save_button.clicked.connect(self._save_config) self._load_button = QPushButton("Load Config") self._load_button.setFixedSize(100, 28) self._load_button.clicked.connect(self._load_config) + top_bar_layout.addWidget(self._export_variables_button) top_bar_layout.addWidget(self._save_button) top_bar_layout.addWidget(self._load_button) data_views_layout.addLayout(top_bar_layout) @@ -181,6 +188,10 @@ def _setup_ui(self): # noqa: PLR0915 self._scripting_tab = ScriptingTab(self._app_state, self) self._tab_widget.addTab(self._scripting_tab, "Scripting") + # Tab 4: Help + self._help_tab = HelpTab(self) + self._tab_widget.addTab(self._help_tab, "Help") + # Set initial view (Both selected) self._on_view_toggle_changed() @@ -230,7 +241,7 @@ def _on_ports_refreshed(self, ports: list): self._setup_tab.set_ports(ports) def _on_elf_file_selected(self, file_path: str): - """Handle ELF file selection from setup tab.""" + """Handle variable file selection from setup tab.""" self._settings.setValue("elf_file_path", file_path) def _on_connect_clicked(self): @@ -244,7 +255,7 @@ def _on_connect_clicked(self): if not elf_path: self._setup_tab.set_loading(False) - self._show_error("Please select an ELF file first.") + self._show_error("Please select an ELF file or import first.") return # Get connection parameters based on selected interface @@ -267,6 +278,7 @@ def _on_connect_clicked(self): def _on_connection_changed(self, connected: bool): """Handle connection state change.""" self._setup_tab.set_connected(connected) + self._export_variables_button.setEnabled(connected) # Update tabs self._scope_view_tab.on_connection_changed(connected) @@ -357,6 +369,29 @@ def _save_config(self): ) self._config_manager.save_config(config) + def _export_selected_variables(self): + """Export variables currently selected in WatchView and ScopeView.""" + source_path = self._setup_tab.elf_file_path or self._settings.value("elf_file_path", "", type=str) + default_name = os.path.splitext(os.path.basename(source_path))[0] if source_path else "variables_list" + export_dir = self._settings.value("variable_export_dir", "", type=str) + default_path = os.path.join(export_dir, default_name + ".yml") if export_dir else default_name + ".yml" + + file_path, _ = QFileDialog.getSaveFileName( + self, + "Export Selected Variables", + default_path, + "YAML Files (*.yml);;Pickle Files (*.pkl)", + ) + if not file_path: + return + + self._settings.setValue("variable_export_dir", os.path.dirname(file_path)) + try: + self._app_state.export_selected_variables(file_path) + QMessageBox.information(self, "Export Complete", f"Variables exported to:\n{file_path}") + except Exception as e: + self._show_error(str(e)) + def _load_config(self): # noqa: PLR0912, PLR0915 """Load configuration from file.""" config = self._config_manager.load_config() diff --git a/pyx2cscope/gui/qt/models/app_state.py b/pyx2cscope/gui/qt/models/app_state.py index d185ff61..60f60ca7 100644 --- a/pyx2cscope/gui/qt/models/app_state.py +++ b/pyx2cscope/gui/qt/models/app_state.py @@ -11,6 +11,7 @@ from PyQt5.QtCore import QMutex, QObject, pyqtSignal +from pyx2cscope.variable.variable_factory import FileType from pyx2cscope.x2cscope import TriggerConfig, X2CScope @@ -134,7 +135,7 @@ def __init__(self, parent=None): # Scope state self._scope_active: bool = False self._scope_single_shot: bool = False - self._sample_time_factor: int = 0 + self._sample_time_factor: int = 1 self._scope_sample_time_us: int = 50 self._real_sample_time: float = 0.0 @@ -198,6 +199,59 @@ def get_sfr_list(self) -> List[str]: finally: self._mutex.unlock() + def export_variables(self, filename: str): + """Export the currently loaded variable database to a YML or PKL file.""" + self._mutex.lock() + try: + if not self._x2cscope: + raise RuntimeError("No variables are loaded.") + + extension = filename.lower().rsplit(".", maxsplit=1)[-1] if "." in filename else "" + if extension == "yml": + file_type = FileType.YAML + elif extension == "pkl": + file_type = FileType.PICKLE + else: + raise ValueError("Supported export formats are .yml and .pkl.") + + self._x2cscope.export_variables(filename, ext=file_type) + finally: + self._mutex.unlock() + + def export_selected_variables(self, filename: str): + """Export only variables currently selected in the watch and scope views.""" + self._mutex.lock() + try: + if not self._x2cscope: + raise RuntimeError("No variables are loaded.") + + extension = filename.lower().rsplit(".", maxsplit=1)[-1] if "." in filename else "" + if extension == "yml": + file_type = FileType.YAML + elif extension == "pkl": + file_type = FileType.PICKLE + else: + raise ValueError("Supported export formats are .yml and .pkl.") + + seen_items = set() + selected_vars = [] + selected_vars.extend((var.name, var.sfr) for var in self._watch_vars if var.name and var.name != "None") + selected_vars.extend((var.name, var.sfr) for var in self._live_watch_vars if var.name and var.name != "None") + selected_vars.extend((channel.name, channel.sfr) for channel in self._scope_channels if channel.name and channel.name != "None") + + for name, sfr in selected_vars: + key = (name, sfr) + if key in seen_items: + continue + seen_items.add(key) + + if not seen_items: + raise RuntimeError("No variables are selected in WatchView or ScopeView.") + + self._x2cscope.export_variables(filename, ext=file_type, items=list(seen_items)) + finally: + self._mutex.unlock() + def update_device_info(self) -> Optional[DeviceInfo]: """Fetch and update device info (thread-safe).""" self._mutex.lock() @@ -642,7 +696,7 @@ def sample_time_factor(self) -> int: @sample_time_factor.setter def sample_time_factor(self, value: int): self._mutex.lock() - self._sample_time_factor = value + self._sample_time_factor = 1 if value < 1 else value self._mutex.unlock() @property diff --git a/pyx2cscope/gui/qt/tabs/help_tab.py b/pyx2cscope/gui/qt/tabs/help_tab.py new file mode 100644 index 00000000..1b60e57a --- /dev/null +++ b/pyx2cscope/gui/qt/tabs/help_tab.py @@ -0,0 +1,192 @@ +"""Help tab for the Qt GUI application.""" + +import platform +import sys + +from PyQt5.QtCore import QUrl +from PyQt5.QtGui import QDesktopServices +from PyQt5.QtWidgets import ( + QGroupBox, + QHBoxLayout, + QLabel, + QPushButton, + QTextEdit, + QVBoxLayout, + QWidget, +) + +import pyx2cscope + + +class HelpTab(QWidget): + """Help tab containing links to GitHub, release notes, and version info.""" + + def __init__(self, parent=None): + """Initialize the help tab.""" + super().__init__(parent) + self._setup_ui() + + def _setup_ui(self): + """Set up the user interface.""" + layout = QVBoxLayout(self) + layout.setContentsMargins(20, 20, 20, 20) + layout.setSpacing(20) + + layout.addWidget(self._create_release_group()) + layout.addWidget(self._create_github_group()) + layout.addWidget(self._create_version_group()) + layout.addWidget(self._create_resources_group()) + layout.addStretch() + + def _create_release_group(self): + """Create the Release Notes and Documentation group box.""" + group = QGroupBox("Release Notes and Documentation") + group_layout = QVBoxLayout(group) + + info = QLabel("View the latest release notes, changelog, and documentation.") + info.setWordWrap(True) + info.setStyleSheet("color: #666; margin-bottom: 10px;") + group_layout.addWidget(info) + + button_layout = QHBoxLayout() + + self.release_notes_btn = QPushButton("View Release Notes") + self.release_notes_btn.clicked.connect(self._open_release_notes) + button_layout.addWidget(self.release_notes_btn) + + self.documentation_btn = QPushButton("Documentation") + self.documentation_btn.clicked.connect(self._open_documentation) + button_layout.addWidget(self.documentation_btn) + button_layout.addStretch() + + group_layout.addLayout(button_layout) + return group + + def _create_github_group(self): + """Create the Report Issues and Request Features group box.""" + group = QGroupBox("Report Issues and Request Features") + group_layout = QVBoxLayout(group) + + info = QLabel( + "If you encounter bugs, have feature requests, or need help, " + "please open an issue on our GitHub repository." + ) + info.setWordWrap(True) + info.setStyleSheet("color: #666; margin-bottom: 10px;") + group_layout.addWidget(info) + + button_layout = QHBoxLayout() + + self.github_issues_btn = QPushButton("Open GitHub Issues") + self.github_issues_btn.clicked.connect(self._open_github_issues) + button_layout.addWidget(self.github_issues_btn) + + self.github_repo_btn = QPushButton("Visit Repository") + self.github_repo_btn.clicked.connect(self._open_github_repo) + button_layout.addWidget(self.github_repo_btn) + button_layout.addStretch() + + group_layout.addLayout(button_layout) + return group + + def _create_version_group(self): + """Create the Software Versions group box.""" + group = QGroupBox("Software Versions") + group_layout = QVBoxLayout(group) + + info = QLabel("Current software and dependency versions:") + info.setStyleSheet("color: #666; margin-bottom: 10px;") + group_layout.addWidget(info) + + self.version_text = QTextEdit() + self.version_text.setReadOnly(True) + self.version_text.setMaximumHeight(250) + self.version_text.setStyleSheet( + "QTextEdit { " + "background-color: #f8f9fa; " + "border: 1px solid #dee2e6; " + "border-radius: 4px; " + "padding: 10px; " + "font-family: 'Consolas', 'Courier New', monospace; " + "font-size: 10pt; " + "}" + ) + self._populate_version_info() + group_layout.addWidget(self.version_text) + return group + + def _create_resources_group(self): + """Create the Additional Resources group box.""" + group = QGroupBox("Additional Resources") + group_layout = QVBoxLayout(group) + + text = QLabel( + "For additional support and resources:\n" + "• Check the examples directory for usage examples\n" + "• Review the README file for installation instructions\n" + "• Join our community discussions on GitHub\n" + "• Visit the documentation for detailed guides" + ) + text.setWordWrap(True) + text.setStyleSheet("color: #666; line-height: 1.8; padding: 5px;") + group_layout.addWidget(text) + return group + + def _open_github_issues(self): + """Open GitHub issues page in default browser.""" + QDesktopServices.openUrl(QUrl("https://github.com/X2Cscope/pyx2cscope/issues")) + + def _open_github_repo(self): + """Open GitHub repository page in default browser.""" + QDesktopServices.openUrl(QUrl("https://github.com/X2Cscope/pyx2cscope")) + + def _open_release_notes(self): + """Open GitHub releases page in default browser.""" + QDesktopServices.openUrl(QUrl("https://github.com/X2Cscope/pyx2cscope/releases")) + + def _open_documentation(self): + """Open documentation in default browser.""" + QDesktopServices.openUrl(QUrl("https://x2cscope.github.io/pyx2cscope/")) + + @staticmethod + def _get_module_version(import_name, version_attr="__version__"): + """Return the version string for a module, or 'Not installed' on failure.""" + try: + module = __import__(import_name) + return getattr(module, version_attr, "Unknown") + except ImportError: + return "Not installed" + + def _populate_version_info(self): + """Populate the version information text area.""" + lines = [] + + lines.append("=== Application ===") + lines.append(f"pyX2Cscope: {pyx2cscope.__version__}") + + lines.append("\n=== Core Dependencies ===") + lines.append(f"mchplnet: {self._get_module_version('mchplnet')}") + lines.append(f"python-can: {self._get_module_version('can')}") + lines.append(f"numpy: {self._get_module_version('numpy')}") + lines.append(f"matplotlib: {self._get_module_version('matplotlib')}") + lines.append(f"pyserial: {self._get_module_version('serial', 'VERSION')}") + lines.append(f"pyelftools: {self._get_module_version('elftools')}") + + lines.append("\n=== GUI Framework ===") + try: + from PyQt5.QtCore import PYQT_VERSION_STR, QT_VERSION_STR + lines.append(f"Qt: {QT_VERSION_STR}") + lines.append(f"PyQt5: {PYQT_VERSION_STR}") + except ImportError: + lines.append("PyQt5: Not available") + + lines.append("\n=== System Information ===") + lines.append(f"Python: {sys.version.split()[0]}") + lines.append(f"Platform: {platform.platform()}") + lines.append(f"Architecture: {platform.architecture()[0]}") + + processor = platform.processor() + if processor: + lines.append(f"Processor: {processor}") + + self.version_text.setPlainText("\n".join(lines)) diff --git a/pyx2cscope/gui/qt/tabs/scope_view_tab.py b/pyx2cscope/gui/qt/tabs/scope_view_tab.py index d653eb11..02401af4 100644 --- a/pyx2cscope/gui/qt/tabs/scope_view_tab.py +++ b/pyx2cscope/gui/qt/tabs/scope_view_tab.py @@ -6,8 +6,8 @@ import numpy as np import pyqtgraph as pg from PyQt5 import QtCore -from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot -from PyQt5.QtGui import QColor, QIcon, QPixmap +from PyQt5.QtCore import QRegExp, Qt, pyqtSignal, pyqtSlot +from PyQt5.QtGui import QColor, QIcon, QPixmap, QRegExpValidator from PyQt5.QtWidgets import ( QCheckBox, QComboBox, @@ -125,10 +125,8 @@ def _create_trigger_group(self) -> QGroupBox: # Sample time factor grid.addWidget(QLabel("Sample Time Factor"), 1, 0) - self._sample_time_factor_edit = QSpinBox() - self._sample_time_factor_edit.setMinimum(0) - self._sample_time_factor_edit.setMaximum(30) - self._sample_time_factor_edit.setValue(0) + self._sample_time_factor_edit = QLineEdit("1") + self._sample_time_factor_edit.setValidator(QRegExpValidator(QRegExp("[1-9][0-9]*"))) grid.addWidget(self._sample_time_factor_edit, 1, 1) # Scope sample time @@ -341,7 +339,7 @@ def _start_sampling(self): logging.debug(f"Added scope channel: {var_name}") # Set sample time - sample_time_factor = self._sample_time_factor_edit.value() + sample_time_factor = int(self._sample_time_factor_edit.text() or "1") x2cscope.set_sample_time(sample_time_factor) # Get real sample time @@ -497,12 +495,13 @@ def get_config(self) -> dict: "offset": [off.text() for off in self._offset_edits], "color": [cb.currentData() or list(self._colors.keys())[cb.currentIndex()] for cb in self._color_combos], "show": [cb.isChecked() for cb in self._visible_checkboxes], + "sfr": [self._app_state.get_scope_channel(i).sfr for i in range(len(self._var_line_edits))], "trigger_variable": self._trigger_variable, "trigger_level": self._trigger_level_edit.text(), "trigger_delay": self._trigger_delay_combo.currentText(), "trigger_edge": self._trigger_edge_combo.currentText(), "trigger_mode": self._trigger_mode_combo.currentText(), - "sample_time_factor": str(self._sample_time_factor_edit.value()), + "sample_time_factor": self._sample_time_factor_edit.value(), "single_shot": self._single_shot_checkbox.isChecked(), } @@ -514,9 +513,12 @@ def load_config(self, config: dict): offsets = config.get("offset", []) colors = config.get("color", []) shows = config.get("show", []) + sfrs = config.get("sfr", []) for i, (le, var) in enumerate(zip(self._var_line_edits, variables)): le.setText(var) + sfr = sfrs[i] if i < len(sfrs) else False + self._app_state.update_scope_channel_field(i, "sfr", sfr) # Update app state with variable name self._app_state.update_scope_channel_field(i, "name", var) for i, (cb, trigger) in enumerate(zip(self._trigger_checkboxes, triggers)): @@ -541,5 +543,5 @@ def load_config(self, config: dict): self._trigger_delay_combo.setCurrentText(config.get("trigger_delay", "0")) self._trigger_edge_combo.setCurrentText(config.get("trigger_edge", "Rising")) self._trigger_mode_combo.setCurrentText(config.get("trigger_mode", "Disable")) - self._sample_time_factor_edit.setValue(int(config.get("sample_time_factor", "0"))) + self._sample_time_factor_edit.setValue(int(config.get("sample_time_factor", 1))) self._single_shot_checkbox.setChecked(config.get("single_shot", False)) diff --git a/pyx2cscope/gui/qt/tabs/setup_tab.py b/pyx2cscope/gui/qt/tabs/setup_tab.py index d0d3f861..1b1b6605 100644 --- a/pyx2cscope/gui/qt/tabs/setup_tab.py +++ b/pyx2cscope/gui/qt/tabs/setup_tab.py @@ -33,7 +33,7 @@ class SetupTab(QWidget): - UART settings (Port, Baud Rate) - TCP/IP settings (IP Address, Port) - CAN settings (Bus Type, Channel, Baudrate, Mode, Tx-ID, Rx-ID) - - ELF file selection + - File selection (.elf, .yml, .pkl) - Device info display """ @@ -73,7 +73,7 @@ def _setup_ui(self): # noqa: PLR0915 left_layout = QVBoxLayout() left_layout.setAlignment(Qt.AlignTop) - # === Connection Settings Group (ELF file + Interface selection) === + # === Connection Settings Group (file + interface selection) === connection_group = QGroupBox("Connection Settings") connection_layout = QGridLayout() connection_layout.setSpacing(8) @@ -83,9 +83,9 @@ def _setup_ui(self): # noqa: PLR0915 row = 0 - # ELF file selection - connection_layout.addWidget(QLabel("ELF File:"), row, 0, Qt.AlignRight) - self._elf_button = QPushButton("Select ELF file") + # Variable source selection + connection_layout.addWidget(QLabel("Select File:"), row, 0, Qt.AlignRight) + self._elf_button = QPushButton("ELF / YML / PKL") self._elf_button.setFixedWidth(200) self._elf_button.clicked.connect(self._on_select_elf) connection_layout.addWidget(self._elf_button, row, 1, 1, 2) @@ -241,6 +241,7 @@ def _setup_ui(self): # noqa: PLR0915 can_layout.addWidget(self._can_rx_id_edit, can_row, 1) left_layout.addWidget(self._can_group) + left_layout.addStretch() main_layout.addLayout(left_layout) @@ -311,15 +312,15 @@ def _on_interface_changed(self, interface: str): self._can_group.show() def _on_select_elf(self): - """Handle ELF file selection.""" + """Handle variable file selection.""" # Get last directory from settings last_dir = self._settings.value("elf_file_dir", "", type=str) file_path, _ = QFileDialog.getOpenFileName( self, - "Select ELF File", + "Select File", last_dir, - "ELF Files (*.elf);;All Files (*.*)", + "Variable Files (*.elf *.yml *.pkl);;ELF Files (*.elf);;YAML Files (*.yml);;Pickle Files (*.pkl);;All Files (*.*)", ) if file_path: self._elf_file_path = file_path @@ -375,12 +376,12 @@ def refresh_btn(self) -> QPushButton: @property def elf_file_path(self) -> str: - """Get the selected ELF file path.""" + """Get the selected file path.""" return self._elf_file_path @elf_file_path.setter def elf_file_path(self, path: str): - """Set the ELF file path.""" + """Set the selected file path.""" self._elf_file_path = path if path: basename = os.path.basename(path) @@ -392,6 +393,8 @@ def elf_file_path(self, path: str): def set_ports(self, ports: list): """Set available COM ports.""" self._port_combo.clear() + # Add AUTO option first for auto-detection + self._port_combo.addItem("AUTO") self._port_combo.addItems(ports) def set_connected(self, connected: bool): diff --git a/pyx2cscope/gui/qt/tabs/watch_view_tab.py b/pyx2cscope/gui/qt/tabs/watch_view_tab.py index 662ea245..45b95faa 100644 --- a/pyx2cscope/gui/qt/tabs/watch_view_tab.py +++ b/pyx2cscope/gui/qt/tabs/watch_view_tab.py @@ -334,6 +334,7 @@ def get_config(self) -> dict: "offsets": [off.text() for off in self._offset_edits], "scaled_values": [sv.text() for sv in self._scaled_value_edits], "live": [cb.isChecked() for cb in self._live_checkboxes], + "sfr": [self._app_state.get_live_watch_var(i).sfr for i in range(len(self._variable_edits))], } def load_config(self, config: dict): @@ -348,11 +349,14 @@ def load_config(self, config: dict): offsets = config.get("offsets", []) scaled_values = config.get("scaled_values", []) lives = config.get("live", []) + sfrs = config.get("sfr", []) for i, var in enumerate(variables): self._add_variable_row() if i < len(self._variable_edits): self._variable_edits[i].setText(var) + sfr = sfrs[i] if i < len(sfrs) else False + self._app_state.update_live_watch_var_field(i, "sfr", sfr) # Update app state with variable name self._app_state.update_live_watch_var_field(i, "name", var) if i < len(values) and i < len(self._value_edits): diff --git a/pyx2cscope/gui/web/app.py b/pyx2cscope/gui/web/app.py index 3f876550..6cfcb2f5 100644 --- a/pyx2cscope/gui/web/app.py +++ b/pyx2cscope/gui/web/app.py @@ -6,15 +6,17 @@ import logging import os import socket +import tempfile import webbrowser import serial.tools.list_ports -from flask import Flask, jsonify, render_template, request +from flask import Flask, Response, jsonify, render_template, request from pyx2cscope import __version__, set_logger from pyx2cscope.gui import web from pyx2cscope.gui.web.scope import web_scope from pyx2cscope.gui.web.ws_handlers import socketio +from pyx2cscope.variable.variable_factory import FileType set_logger(logging.ERROR) @@ -39,11 +41,13 @@ def create_app(): app.add_url_rule("/", view_func=index) app.add_url_rule("/serial-ports", view_func=list_serial_ports) app.add_url_rule("/local-ips", view_func=get_local_ips) + app.add_url_rule("/version-info", view_func=get_version_info) app.add_url_rule("/connect", view_func=connect, methods=["POST"]) app.add_url_rule("/disconnect", view_func=disconnect) app.add_url_rule("/is-connected", view_func=is_connected) app.add_url_rule("/variables", view_func=variables_autocomplete, methods=["POST", "GET"]) app.add_url_rule("/variables/all", view_func=get_variables, methods=["POST", "GET"]) + app.add_url_rule("/variables/export", view_func=export_variables, methods=["GET"]) socketio.init_app(app) @@ -92,6 +96,44 @@ def get_local_ips(): return jsonify({"ips": ips}) +def get_version_info(): + """Return version information for all dependencies. + + call {server_url}/version-info to execute. + """ + import importlib.metadata + versions = {} + + def get_package_version(package_name, module_name=None, attr_name='__version__'): + """Get version from package metadata or module attribute.""" + try: + # Try importlib.metadata first (standard way) + return importlib.metadata.version(package_name) + except Exception: + # Fall back to module attribute + if module_name: + try: + module = __import__(module_name) + return getattr(module, attr_name, 'Unknown') + except (ImportError, AttributeError): + pass + return 'Not installed' + + # Web interface dependencies + versions['flask'] = get_package_version('flask', 'flask') + versions['flask_socketio'] = get_package_version('flask-socketio', 'flask_socketio') + versions['mchplnet'] = get_package_version('mchplnet', 'mchplnet') + versions['python_can'] = get_package_version('python-can', 'can') + + # Core dependencies + versions['numpy'] = get_package_version('numpy', 'numpy') + versions['matplotlib'] = get_package_version('matplotlib', 'matplotlib') + versions['pyserial'] = get_package_version('pyserial', 'serial', 'VERSION') + versions['pyelftools'] = get_package_version('pyelftools', 'elftools') + + return jsonify(versions) + + def connect(): """Connect pyX2CScope using arguments coming from the web. @@ -169,6 +211,8 @@ def connect(): return jsonify({"status": "error", "msg": str(e)}), 401 except ValueError as e: return jsonify({"status": "error", "msg": str(e)}), 401 + except TimeoutError as e: + return jsonify({"status": "error", "msg": str(e)}), 401 return jsonify({"status": "error", "msg": "Interface argument or import file invalid."}), 400 @@ -219,6 +263,44 @@ def get_variables(): return jsonify({"items": items}) +def export_variables(): + """Export the currently loaded variable database as YML or PKL.""" + if not web_scope.is_connected() or web_scope.x2c_scope is None: + return jsonify({"status": "error", "msg": "No variables are loaded."}), 400 + + ext = request.args.get("ext", "yml").lower() + if ext == "yml": + file_type = FileType.YAML + mimetype = "application/x-yaml" + elif ext == "pkl": + file_type = FileType.PICKLE + mimetype = "application/octet-stream" + else: + return jsonify({"status": "error", "msg": "Supported export formats are yml and pkl."}), 400 + + temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=file_type.value) + temp_file.close() + try: + selected_items = web_scope.get_selected_variables() + if not selected_items: + return jsonify({"status": "error", "msg": "No variables are selected in WatchView, ScopeView, or Dashboard."}), 400 + + web_scope.x2c_scope.export_variables(temp_file.name, ext=file_type, items=selected_items) + with open(temp_file.name, "rb") as file: + data = file.read() + finally: + if os.path.exists(temp_file.name): + os.remove(temp_file.name) + + return Response( + data, + mimetype=mimetype, + headers={ + "Content-disposition": f"attachment; filename={web_scope.get_export_filename(file_type.value)}" + }, + ) + + def open_browser(host="localhost", web_port=5000): """Open a new browser pointing to the Flask server. diff --git a/pyx2cscope/gui/web/scope.py b/pyx2cscope/gui/web/scope.py index 3b18eed7..237a79a7 100644 --- a/pyx2cscope/gui/web/scope.py +++ b/pyx2cscope/gui/web/scope.py @@ -5,6 +5,7 @@ """ import numbers import time +from pathlib import Path from pyx2cscope.gui.web import extensions from pyx2cscope.x2cscope import TriggerConfig, X2CScope @@ -22,8 +23,9 @@ def __init__(self): self.scope_vars = [] self.scope_trigger = False self.scope_burst = False - self.scope_sample_time = 0 + self.scope_sample_time = 1 self.scope_time_sampling = 50e-3 + self.variables_file = "" self.dashboard_vars = {} # {var_name: Variable object} self.dashboard_rate = 1.0 # Fixed at 1 second for dashboard polling @@ -32,7 +34,7 @@ def __init__(self): self.x2c_scope :X2CScope | None = None self._lock = extensions.create_lock() - def _get_watch_variable_as_dict(self, variable, value=None): + def _get_watch_variable_as_dict(self, variable, sfr=False, value=None): primitive = variable.__class__.__name__.lower().replace("variable", "") if value is None: with self._lock: @@ -47,9 +49,10 @@ def _get_watch_variable_as_dict(self, variable, value=None): "offset": 0, "scaled_value": value, "remove": 0, + "sfr": bool(sfr), } - def _get_scope_variable_as_dict(self, variable): + def _get_scope_variable_as_dict(self, variable, sfr=False): colors = [ "#FF0000", "#00FF00", @@ -68,6 +71,7 @@ def _get_scope_variable_as_dict(self, variable): "gain": 1, "offset": 0, "remove": 0, + "sfr": bool(sfr), } @staticmethod @@ -151,7 +155,7 @@ def add_watch_var(self, var, sfr: bool = False): if not any(_data["variable"].info.name == var for _data in self.watch_vars): variable = self.x2c_scope.get_variable(var, sfr=sfr) if variable is not None: - var_dict = self._get_watch_variable_as_dict(variable) + var_dict = self._get_watch_variable_as_dict(variable, sfr=sfr) self.watch_vars.append(var_dict) return var_dict @@ -190,17 +194,18 @@ def watch_poll(self): return result # Dashboard variable methods - def add_dashboard_var(self, name): + def add_dashboard_var(self, name, sfr: bool = False): """Add a variable to the dashboard polling list. Args: name (str): Variable name to add. + sfr (bool): Whether to retrieve a peripheral register (SFR) instead of a firmware variable. Returns: bool: True if variable was added successfully. """ if name not in self.dashboard_vars: - variable = self.x2c_scope.get_variable(name) + variable = self.x2c_scope.get_variable(name, sfr=sfr) if variable is not None: self.dashboard_vars[name] = variable return True @@ -267,7 +272,7 @@ def add_scope_var(self, var, sfr: bool = False): if not any(data["variable"].info.name == var for data in self.scope_vars): variable = self.x2c_scope.get_variable(var, sfr=sfr) if variable is not None: - var_dict = self._get_scope_variable_as_dict(variable) + var_dict = self._get_scope_variable_as_dict(variable, sfr=sfr) self.scope_vars.append(var_dict) self.x2c_scope.add_scope_channel(variable) return var_dict @@ -346,6 +351,7 @@ def scope_set_sample(self, trigger_action, sample_time, sample_freq): """ if self.x2c_scope is None: return + sample_time = 1 if sample_time < 1 else sample_time with self._lock: if self.scope_sample_time != sample_time: self.scope_sample_time = sample_time @@ -478,13 +484,50 @@ def connect(self, *args, **kwargs): self.x2c_scope = X2CScope(*args, **kwargs) def set_file(self, import_file): - """Import variables from ELF file. + """Import variables from a variable database file. Args: - import_file (str): Path to the ELF file. + import_file (str): Path to the import file. """ + self.variables_file = import_file self.x2c_scope.import_variables(import_file) + def get_export_filename(self, extension: str) -> str: + """Build a default export filename for the current variable database.""" + stem = Path(self.variables_file).stem if self.variables_file else "variables_list" + return stem + extension + + def get_selected_variables(self): + """Collect unique variables currently used by watch, scope, and dashboard views.""" + selected = [] + seen = set() + + for item in self.watch_vars: + variable = item.get("variable") + if variable is None: + continue + key = (variable.info.name, bool(item.get("sfr", False))) + if key not in seen: + seen.add(key) + selected.append(key) + + for item in self.scope_vars: + variable = item.get("variable") + if variable is None: + continue + key = (variable.info.name, bool(item.get("sfr", False))) + if key not in seen: + seen.add(key) + selected.append(key) + + for name, variable in self.dashboard_vars.items(): + key = (name, False) + if variable is not None and key not in seen: + seen.add(key) + selected.append(key) + + return selected + def list_variables(self): """List all available variables. @@ -505,6 +548,7 @@ def disconnect(self): """Disconnect from X2CScope.""" self.x2c_scope.disconnect() self.x2c_scope = None + self.variables_file = "" def is_connected(self): """Check if connected to X2CScope. diff --git a/pyx2cscope/gui/web/static/js/dashboard_view.js b/pyx2cscope/gui/web/static/js/dashboard_view.js index ab46cc90..c6e7871a 100644 --- a/pyx2cscope/gui/web/static/js/dashboard_view.js +++ b/pyx2cscope/gui/web/static/js/dashboard_view.js @@ -171,8 +171,8 @@ function updateScopeControlSampleState(data) { // Update sample time/freq if elements exist const sampleTimeEl = document.getElementById('scopeCtrlSampleTime'); const sampleFreqEl = document.getElementById('scopeCtrlSampleFreq'); - if (sampleTimeEl && data.sampleTime) sampleTimeEl.value = data.sampleTime; - if (sampleFreqEl && data.sampleFreq) sampleFreqEl.value = data.sampleFreq; + if (sampleTimeEl && data.sampleTime !== undefined) sampleTimeEl.value = data.sampleTime; + if (sampleFreqEl && data.sampleFreq !== undefined) sampleFreqEl.value = data.sampleFreq; // Update button states const widgetEl = document.getElementById(`dashboard-widget-${widget.id}`); @@ -245,7 +245,7 @@ function syncScopeControlToBackend() { const scopeControlWidget = dashboardWidgets.find(w => w.type === 'scope_control'); if (scopeControlWidget) { // Sync sample control settings - const sampleTime = scopeControlWidget.sampleTime !== undefined ? scopeControlWidget.sampleTime : 0; + const sampleTime = scopeControlWidget.sampleTime !== undefined ? scopeControlWidget.sampleTime : 1; const sampleFreq = scopeControlWidget.sampleFreq || 20; const sampleFormData = `triggerAction=off&sampleTime=${sampleTime}&sampleFreq=${sampleFreq}`; scopeSocket.emit('update_sample_control', sampleFormData); @@ -326,7 +326,7 @@ function registerWidgetVariables(widget) { dashboardSocket.emit('add_dashboard_var', {var: varName}); }); } else if (widget.type !== 'label') { - dashboardSocket.emit('add_dashboard_var', {var: widget.variable}); + dashboardSocket.emit('add_dashboard_var', {var: widget.variable, sfr: widget.sfr || false}); } } @@ -470,8 +470,8 @@ function showWidgetConfig(type, editWidget = null) { // Initialize Select2 after modal is shown so dropdown renders correctly $('#widgetConfigModal').one('shown.bs.modal', function() { - // Reset SFR toggle for each new config session - $('#widgetSfrToggle').prop('checked', false); + // Restore SFR toggle from widget if editing, otherwise reset + $('#widgetSfrToggle').prop('checked', editWidget ? (editWidget.sfr || false) : false); if (widgetDef.requiresVariable && !widgetDef.requiresMultipleVariables) { $('#widgetVarName').select2(initWidgetVarSelect2()); @@ -519,6 +519,7 @@ function addDashboardWidget() { id: widgetIdCounter++, type: currentWidgetType, variable: varName, + sfr: $('#widgetSfrToggle').is(':checked'), icon: widgetDef.icon, x: 50, y: 50, @@ -1068,4 +1069,4 @@ function handleDashboardFileImport(e) { }; reader.readAsText(file); } -} \ No newline at end of file +} diff --git a/pyx2cscope/gui/web/static/js/scope_view.js b/pyx2cscope/gui/web/static/js/scope_view.js index 06e452bd..607b1433 100644 --- a/pyx2cscope/gui/web/static/js/scope_view.js +++ b/pyx2cscope/gui/web/static/js/scope_view.js @@ -47,12 +47,12 @@ socket_sv.on("sample_control_updated", function(response) { }); } // Handle sampleTime input - if (response.data.sampleTime) { + if (response.data.sampleTime !== undefined) { const sampleTimeInput = document.getElementById('sampleTime'); sampleTimeInput.value = response.data.sampleTime; } // Handle sampleFreq input - if (response.data.sampleFreq) { + if (response.data.sampleFreq !== undefined) { const sampleFreqInput = document.getElementById('sampleFreq'); sampleFreqInput.value = response.data.sampleFreq; } @@ -493,4 +493,4 @@ $(document).ready(function () { } ] }); -}); \ No newline at end of file +}); diff --git a/pyx2cscope/gui/web/static/js/script.js b/pyx2cscope/gui/web/static/js/script.js index ecdcaa25..efc6e6a3 100644 --- a/pyx2cscope/gui/web/static/js/script.js +++ b/pyx2cscope/gui/web/static/js/script.js @@ -50,11 +50,29 @@ function disconnect(){ }); } +function exportVariables() { + const exportToggle = $('#exportVariablesToggle'); + if (exportToggle.attr('aria-disabled') === 'true') { + return; + } + + $('#x2cModalTitle').html('Export Variables'); + $('#x2cModalBody').html(` +

Export the variables currently used in Watch View, Scope View, and Dashboard.

+ + `); + $('#x2cModal').modal('show'); +} + function load_uart() { $.getJSON('/serial-ports', function(data) { uart = $('#port'); uart.empty(); - uart.append(''); + // Add AUTO option as default selection + uart.append($('').val('AUTO').html('AUTO (Auto-detect)')); data.forEach(function(item) { uart.append($('').val(item).html(item)); }); @@ -93,6 +111,7 @@ function setConnectState(status) { $("#btnDashboardView").prop("disabled", false); $("#btnConnect").prop("disabled", true); $("#btnConnect").html("Disconnect", true); + $('#exportVariablesToggle').removeClass('disabled text-white-50').attr('aria-disabled', 'false'); $('#connection-status').html('Connected'); $('#desktopTabs').removeClass('disabled'); $('#mobileTabs').removeClass('disabled'); @@ -110,6 +129,7 @@ function setConnectState(status) { $('#btnConnect').html('Connect'); $('#btnConnect').removeClass('btn-danger'); $('#btnConnect').addClass('btn-primary'); + $('#exportVariablesToggle').addClass('disabled text-white-50').attr('aria-disabled', 'true'); $('#setupView').removeClass('disabled'); $('#desktopTabs').addClass('disabled'); $('#mobileTabs').addClass('disabled'); @@ -123,6 +143,10 @@ function initSetupCard(){ if($('#btnConnect').html() === "Connect") connect(); else disconnect(); }); + $('#exportVariablesToggle').on('click', function(e) { + e.preventDefault(); + exportVariables(); + }); $('#connection-status').on('click', function() { if($('#connection-status').html() === "Disconnected") connect(); @@ -382,4 +406,4 @@ $(document).ready(function() { setConnectState(data.status); }); -}); \ No newline at end of file +}); diff --git a/pyx2cscope/gui/web/static/widgets/plot_scope/widget.js b/pyx2cscope/gui/web/static/widgets/plot_scope/widget.js index 930cfdfa..885da1fc 100644 --- a/pyx2cscope/gui/web/static/widgets/plot_scope/widget.js +++ b/pyx2cscope/gui/web/static/widgets/plot_scope/widget.js @@ -215,13 +215,13 @@ function refreshPlotScopeWidget(widget, widgetEl) { function generateTimeLabels(numSamples) { // Get sample time and frequency from scope control widget const scopeControlWidget = dashboardWidgets.find(w => w.type === 'scope_control'); - const sampleTime = scopeControlWidget?.sampleTime !== undefined ? scopeControlWidget.sampleTime : 0; + const sampleTime = scopeControlWidget?.sampleTime !== undefined ? scopeControlWidget.sampleTime : 1; const sampleFreq = scopeControlWidget?.sampleFreq || 20; // in KHz - // Calculate time per sample: (1 / freq_Hz) * (sampleTime + 1) - // sampleTime is a skip factor: 0 = every sample, 1 = every 2nd sample, etc. + // Calculate time per sample: (1 / freq_Hz) * sampleTime + // sampleTime is user-facing: 1 = every sample, 2 = every 2nd sample, etc. // freq is in KHz, so freq_Hz = sampleFreq * 1000 - const timePerSampleUs = (1 / (sampleFreq * 1000)) * (sampleTime + 1) * 1000000; // in microseconds + const timePerSampleUs = (1 / (sampleFreq * 1000)) * sampleTime * 1000000; // in microseconds const totalTimeUs = (numSamples - 1) * timePerSampleUs; // Determine best unit based on total time diff --git a/pyx2cscope/gui/web/static/widgets/scope_control/widget.js b/pyx2cscope/gui/web/static/widgets/scope_control/widget.js index 6fc87ecb..eeee6e96 100644 --- a/pyx2cscope/gui/web/static/widgets/scope_control/widget.js +++ b/pyx2cscope/gui/web/static/widgets/scope_control/widget.js @@ -17,7 +17,7 @@ function handleScopeControlAction(action) { scopeControlState = action; // Get current settings from the widget - const sampleTime = document.getElementById('scopeCtrlSampleTime')?.value || 0; + const sampleTime = document.getElementById('scopeCtrlSampleTime')?.value || 1; const sampleFreq = document.getElementById('scopeCtrlSampleFreq')?.value || 20; // Send to scope-view namespace @@ -80,7 +80,7 @@ function updateScopeControlTrigger(widgetId) { function updateScopeSampleSettings(widgetId) { if (isDashboardEditMode) return; - const sampleTime = document.getElementById('scopeCtrlSampleTime')?.value || 0; + const sampleTime = document.getElementById('scopeCtrlSampleTime')?.value || 1; const sampleFreq = document.getElementById('scopeCtrlSampleFreq')?.value || 20; // Store values in all scope_control widgets for persistence @@ -107,7 +107,7 @@ function createScopeControlWidget(widget) { const triggerLevel = widget.triggerLevel || 0; const triggerDelay = widget.triggerDelay || 0; const triggerVar = widget.triggerVar || ''; - const sampleTime = widget.sampleTime !== undefined ? widget.sampleTime : 0; + const sampleTime = widget.sampleTime !== undefined ? widget.sampleTime : 1; const sampleFreq = widget.sampleFreq || 20; // Build trigger variable options from scope-view variables @@ -145,7 +145,7 @@ function createScopeControlWidget(widget) {
@@ -246,7 +246,7 @@ function getScopeControlConfig(editWidget) { } function saveScopeControlConfig(widget) { - widget.sampleTime = widget.sampleTime !== undefined ? widget.sampleTime : 0; + widget.sampleTime = widget.sampleTime !== undefined ? widget.sampleTime : 1; widget.sampleFreq = widget.sampleFreq || 20; widget.triggerMode = widget.triggerMode || '0'; widget.triggerEdge = widget.triggerEdge || '1'; diff --git a/pyx2cscope/gui/web/templates/index.html b/pyx2cscope/gui/web/templates/index.html index cdca9549..40e5b613 100644 --- a/pyx2cscope/gui/web/templates/index.html +++ b/pyx2cscope/gui/web/templates/index.html @@ -132,4 +132,4 @@ -{% endblock scripts %} \ No newline at end of file +{% endblock scripts %} diff --git a/pyx2cscope/gui/web/templates/navbar.html b/pyx2cscope/gui/web/templates/navbar.html index bf2b4444..b918107e 100644 --- a/pyx2cscope/gui/web/templates/navbar.html +++ b/pyx2cscope/gui/web/templates/navbar.html @@ -2,9 +2,12 @@

{{ title }} {{ version }}

-
\ No newline at end of file + diff --git a/pyx2cscope/gui/web/templates/sample_control.html b/pyx2cscope/gui/web/templates/sample_control.html index 1781a878..851cfcd9 100644 --- a/pyx2cscope/gui/web/templates/sample_control.html +++ b/pyx2cscope/gui/web/templates/sample_control.html @@ -15,7 +15,7 @@
- +
diff --git a/pyx2cscope/gui/web/templates/setup.html b/pyx2cscope/gui/web/templates/setup.html index 38117e19..856d82d6 100644 --- a/pyx2cscope/gui/web/templates/setup.html +++ b/pyx2cscope/gui/web/templates/setup.html @@ -15,7 +15,7 @@
@@ -95,8 +95,8 @@
-
+
-
\ No newline at end of file +
diff --git a/pyx2cscope/gui/web/templates/welcome_content.html b/pyx2cscope/gui/web/templates/welcome_content.html index c25ad4d1..e14c224e 100644 --- a/pyx2cscope/gui/web/templates/welcome_content.html +++ b/pyx2cscope/gui/web/templates/welcome_content.html @@ -1,34 +1,89 @@ - -
Getting Started
+ +
+ +
+
Getting Started
-
-
settings Setup
-

Configure your interface, upload an ELF file, and click "Connect" to start monitoring your device.

-
+
+
settings Setup
+

Configure your interface, load an ELF or import file, and click "Connect" to start monitoring your device.

+
-
Available Views
-
-
-
visibility Watch View
-

Monitor and interact with variables in real-time.

-
-
-
show_chart Scope View
-

Visualize variable data over time with oscilloscope-style charts.

-
-
-
dashboard Dashboard
-

Create custom layouts with interactive widgets and gauges.

+
+
upload_file Export Variables
+

Use the export icon in the header to save the variables currently used in Watch View, Scope View, and Dashboard as YML or PKL.

+
+ +
Available Views
+
+
visibility Watch View
+

Monitor and interact with variables in real-time.

+
+
+
show_chart Scope View
+

Visualize variable data over time with oscilloscope-style charts.

+
+
+
dashboard Dashboard
+

Create custom layouts with interactive widgets and gauges.

+
+
+
code Scripting
+

Execute Python scripts with access to x2cscope.

+
+ +
Tips
+
    +
  • qr_code_2 Click QR code icons to generate scannable links for mobile access
  • +
  • open_in_new Click open icons to launch views in separate windows
  • +
  • help_outline Click the help icon in the header to toggle this card
  • +
  • upload_file Click the export icon in the header to download variables used by the active views
  • +
-
-
code Scripting
-

Execute Python scripts with access to x2cscope.

+ + +
+
Software Versions
+
+
+

Application:

+
    +
  • pyX2Cscope: {{ version }}
  • +
+

Web Interface Dependencies:

+
    +
  • Flask: Loading...
  • +
  • Flask-SocketIO: Loading...
  • +
  • mchplnet: Loading...
  • +
  • python-can: Loading...
  • +
+

Core Dependencies:

+
    +
  • numpy: Loading...
  • +
  • matplotlib: Loading...
  • +
  • pyserial: Loading...
  • +
  • pyelftools: Loading...
  • +
+
+
-
Tips
-
    -
  • qr_code_2 Click QR code icons to generate scannable links for mobile access
  • -
  • open_in_new Click open icons to launch views in separate windows
  • -
  • help_outline Click the help icon in the header to toggle this card
  • -
+ diff --git a/pyx2cscope/gui/web/views/scope_view.py b/pyx2cscope/gui/web/views/scope_view.py index 651fe648..6bba8b71 100644 --- a/pyx2cscope/gui/web/views/scope_view.py +++ b/pyx2cscope/gui/web/views/scope_view.py @@ -62,8 +62,7 @@ def load(): if isinstance(data, list) and data and isinstance(data[0], dict) and "variable" in data[0].keys(): web_scope.clear_scope_var() for item in data: - web_scope.clear_scope_var() - var = web_scope.add_scope_var(item["variable"]) + var = web_scope.add_scope_var(item["variable"], sfr=item.get("sfr", False)) if var is None: msg = "Variable " + item["variable"] + " is not available." else: diff --git a/pyx2cscope/gui/web/views/watch_view.py b/pyx2cscope/gui/web/views/watch_view.py index 0dab9dda..ff59fdbc 100644 --- a/pyx2cscope/gui/web/views/watch_view.py +++ b/pyx2cscope/gui/web/views/watch_view.py @@ -41,7 +41,7 @@ def load(): if isinstance(data, list) and data and isinstance(data[0], dict) and "variable" in data[0].keys(): web_scope.clear_watch_var() for item in data: - var = web_scope.add_watch_var(item["variable"]) + var = web_scope.add_watch_var(item["variable"], sfr=item.get("sfr", False)) if var is None: msg = "Variable " + item["variable"] + " is not available." else: diff --git a/pyx2cscope/gui/web/ws_handlers.py b/pyx2cscope/gui/web/ws_handlers.py index e536743d..ad44a6ee 100644 --- a/pyx2cscope/gui/web/ws_handlers.py +++ b/pyx2cscope/gui/web/ws_handlers.py @@ -169,7 +169,7 @@ def handle_update_sample_control(data): """ data = {k: v[0] if v else '' for k, v in parse_qs(data).items()} trigger_action = data.get('triggerAction', 'off') - sample_time = int(data.get('sampleTime', 0)) + sample_time = max(int(data.get('sampleTime', 1)), 1) sample_freq = float(data.get('sampleFreq', 20)) web_scope.scope_set_sample(trigger_action, sample_time, sample_freq) emit("sample_control_updated", { @@ -220,8 +220,9 @@ def handle_add_dashboard_var(data): data (dict): Dictionary containing the variable name. """ var = data.get("var") + sfr = bool(data.get("sfr", False)) if var: - web_scope.add_dashboard_var(var) + web_scope.add_dashboard_var(var, sfr=sfr) @socketio.on("remove_dashboard_var", namespace="/dashboard") def handle_remove_dashboard_var(data): diff --git a/pyx2cscope/parser/elf_parser.py b/pyx2cscope/parser/elf_parser.py index 618c27f1..fcfde3a5 100644 --- a/pyx2cscope/parser/elf_parser.py +++ b/pyx2cscope/parser/elf_parser.py @@ -11,6 +11,12 @@ from pyx2cscope.variable.variable import VariableInfo +ELF_MACHINE_TO_FAMILY = { + "EM_ARM": "arm", + "EM_DSPIC30F": "dspic", + "EM_MIPS": "pic32", +} + class ElfParser(ABC): """Abstract base class for parsing ELF files. @@ -40,9 +46,12 @@ def __init__(self, elf_path: str): self.elf_path = elf_path self.dwarf_info = {} self.elf_file = None + self.elf_machine = None + self.target_signature = None self.variable_map = {} self.register_map = {} self.symbol_table = {} + self.absolute_symbol_table = {} self._load_elf_file() self._load_symbol_table() @@ -89,6 +98,17 @@ def get_var_list(self) -> List[str]: """ return sorted(self.variable_map.keys(), key=lambda x: x.lower()) + def get_target_family(self) -> Optional[str]: + """Return the MCU family inferred from the ELF machine type, if known.""" + elf_machine = self.elf_machine + if elf_machine is None: + return None + return ELF_MACHINE_TO_FAMILY.get(elf_machine) + + def get_target_signature(self) -> Optional[str]: + """Return the best available ELF target signature for compatibility checks.""" + return self.target_signature or self.get_target_family() + @abstractmethod def _load_elf_file(self): """Load the ELF file according to the specific hardware architecture. diff --git a/pyx2cscope/parser/generic_parser.py b/pyx2cscope/parser/generic_parser.py index 33e740c7..61385b69 100644 --- a/pyx2cscope/parser/generic_parser.py +++ b/pyx2cscope/parser/generic_parser.py @@ -5,6 +5,7 @@ import logging import math +import re from itertools import product from elftools.construct.lib import ListContainer @@ -15,6 +16,20 @@ from pyx2cscope.parser.elf_parser import ElfParser from pyx2cscope.variable.variable import VariableInfo +TARGET_SIGNATURE_PATTERNS = ( + ("dspic33a", ("__DSPIC33A", "__33AK")), + ("arm", ("__PIC32C", "PIC32C/", "SAME", "__GENERIC_ARM_", "ARMV6", "ARMV7")), + ("pic32", ("__PIC32",)), + ("dspic", ("__DSPIC", "__PIC24", "__33CK", "__33CH", "__33EP", "__33FJ")), +) +REGISTER_SYMBOL_PATTERN = re.compile(r"^[A-Z][A-Z0-9]*$") +FAMILY_REGISTER_BYTE_SIZE = { + "arm": 4, + "pic32": 4, + "dspic33a": 4, + "dspic": 2, +} + class GenericParser(ElfParser): """Class for parsing ELF files compatible with 32-bit architectures.""" @@ -33,6 +48,7 @@ def _load_elf_file(self): try: self.stream = open(self.elf_path, "rb") self.elf_file = ELFFile(self.stream) + self.elf_machine = self.elf_file["e_machine"] self.dwarf_info = self.elf_file.get_dwarf_info() except IOError: raise Exception(f"Error loading ELF file: {self.elf_path}") @@ -104,6 +120,40 @@ def _process_die(self, die): valid_values=member_data["valid_values"], ) + if self.is_sfr: + self._add_sfr_aliases(self.var_name, target_map) + + @staticmethod + def _get_sfr_alias_names(register_name: str, member_name: str) -> list[str]: + """Return alternate names for SFR bitfield members.""" + aliases = [] + if "." not in member_name: + return aliases + + member_leaf = member_name.split(".")[-1] + aliases.append(member_leaf) + + return aliases + + def _add_sfr_aliases(self, register_name: str, target_map: dict[str, VariableInfo]): + """Add convenience aliases for parsed SFR bitfield members.""" + register_entries = list(target_map.items()) + for member_name, variable_info in register_entries: + if not member_name.startswith(register_name + "."): + continue + for alias_name in self._get_sfr_alias_names(register_name, member_name): + if alias_name not in target_map: + target_map[alias_name] = VariableInfo( + name=alias_name, + type=variable_info.type, + byte_size=variable_info.byte_size, + bit_size=variable_info.bit_size, + bit_offset=variable_info.bit_offset, + address=variable_info.address, + array_size=variable_info.array_size, + valid_values=variable_info.valid_values, + ) + def _get_base_type_die(self, current_die): """Find the base type die regarding the current selected die, i.e. array_type.""" type_attr = current_die.attributes.get("DW_AT_type") @@ -170,15 +220,42 @@ def _extract_address(self, die_variable): def _load_symbol_table(self): """Loads symbol table entries into a dictionary for fast access.""" + all_symbol_names = [] for section in self.elf_file.iter_sections(): if isinstance(section, SymbolTableSection): for symbol in section.iter_symbols(): - if symbol["st_info"].type == "STT_OBJECT" or symbol["st_info"].bind == "STB_GLOBAL": + all_symbol_names.append(symbol.name) + if symbol.name: self.symbol_table[symbol.name] = symbol["st_value"] + if symbol["st_shndx"] == "SHN_ABS": + self.absolute_symbol_table[symbol.name] = { + "address": symbol["st_value"], + "size": symbol["st_size"], + "type": symbol["st_info"].type, + "bind": symbol["st_info"].bind, + } + self.target_signature = self._infer_target_signature(all_symbol_names) + + def _infer_target_signature(self, symbol_names): + """Infer a more specific target signature from the ELF symbol table.""" + symbol_names = "\n".join(symbol_names).upper() + for signature, markers in TARGET_SIGNATURE_PATTERNS: + if any(marker in symbol_names for marker in markers): + return signature + return self.get_target_family() def _fetch_address_from_symtab(self, variable_name): """Fetches the address of a variable from the preloaded symbol table.""" - return self.symbol_table.get(variable_name, None) + candidates = [variable_name] + if not variable_name.startswith("_"): + candidates.append("_" + variable_name) + else: + candidates.append(variable_name[1:]) + + for candidate in candidates: + if candidate in self.symbol_table: + return self.symbol_table[candidate] + return None def _find_actual_declaration(self, die_variable): """Find the actual declaration of an extern variable.""" @@ -402,6 +479,42 @@ def _map_registers(self) -> dict[str, VariableInfo]: """ return self.register_map + def _get_symbol_only_register_byte_size(self) -> int: + """Return a best-effort byte size for symbol-only SFR entries.""" + signature = self.get_target_signature() + family = self.get_target_family() + return FAMILY_REGISTER_BYTE_SIZE.get(signature, FAMILY_REGISTER_BYTE_SIZE.get(family, 2)) + + @staticmethod + def _get_symbol_only_register_type(byte_size: int) -> str: + """Return the variable type string for a symbol-only SFR entry.""" + type_by_size = { + 1: "unsigned char", + 2: "unsigned int", + 4: "unsigned long", + 8: "unsigned long long", + } + return type_by_size.get(byte_size, "unsigned int") + + def _map_symbol_only_registers(self): + """Populate missing SFRs from absolute symbol-table entries when DWARF lacks variables.""" + byte_size = self._get_symbol_only_register_byte_size() + for symbol_name, symbol_data in self.absolute_symbol_table.items(): + if not REGISTER_SYMBOL_PATTERN.fullmatch(symbol_name): + continue + if symbol_name in self.register_map: + continue + self.register_map[symbol_name] = VariableInfo( + name=symbol_name, + type=self._get_symbol_only_register_type(byte_size), + byte_size=byte_size, + bit_size=0, + bit_offset=0, + address=symbol_data["address"], + array_size=0, + valid_values={}, + ) + def _map_variables(self) -> dict[str, VariableInfo]: """Maps all variables in the ELF file.""" self.variable_map.clear() @@ -411,6 +524,8 @@ def _map_variables(self) -> dict[str, VariableInfo]: self.expression_parser = DWARFExprParser(die.cu.structs) self._process_die(die) + self._map_symbol_only_registers() + return self.variable_map diff --git a/pyx2cscope/variable/variable.py b/pyx2cscope/variable/variable.py index b4cad58b..02b87194 100644 --- a/pyx2cscope/variable/variable.py +++ b/pyx2cscope/variable/variable.py @@ -166,7 +166,7 @@ def _set_bit_value(self, value: Number): shift = (8 * self.info.byte_size) - (self.info.bit_offset + self.info.bit_size) mask = ((1 << self.info.bit_size) - 1) << shift current_data &= ~mask - return current_data | ((value << shift) & mask) + return current_data | ((int(value) << shift) & mask) def get_value(self): """Get the stored value from the MCU. diff --git a/pyx2cscope/variable/variable_factory.py b/pyx2cscope/variable/variable_factory.py index 66fdd580..519fe98a 100644 --- a/pyx2cscope/variable/variable_factory.py +++ b/pyx2cscope/variable/variable_factory.py @@ -3,8 +3,10 @@ import logging import os import pickle +import warnings from dataclasses import asdict from enum import Enum +from typing import Optional import yaml @@ -64,6 +66,19 @@ class VariableFactory: _get_variable_instance: Creates a Variable instance from provided information. """ + _NAME_SFR_PAIR_LEN = 2 + _DEVICE_FAMILY_KEYWORDS = { + "arm": ("ARM",), + "pic32": ("PIC32",), + "dspic": ("DSPIC", "PIC24"), + } + _DEVICE_SIGNATURE_KEYWORDS = { + "dspic33a": ("DSPIC33A", "33AK"), + "arm": ("ARM",), + "pic32": ("PIC32",), + "dspic": ("DSPIC", "PIC24"), + } + def __init__(self, l_net: LNet, elf_path=None): """Initialize the VariableFactory with LNet instance and path to the ELF file. @@ -91,6 +106,7 @@ def set_elf_file(self, elf_path: str): """ parser = GenericParser self.parser = parser(elf_path) + self._warn_if_incompatible(elf_path) def set_lnet_interface(self, lnet: LNet): """Set the LNet interface to be used for data communication. @@ -99,8 +115,61 @@ def set_lnet_interface(self, lnet: LNet): lnet (LNet): the LNet interface """ self.l_net = lnet + self.device_info = self.l_net.get_device_info() + + def _get_device_family(self) -> Optional[str]: + processor_id = str(getattr(self.device_info, "processor_id", "") or "") + for family, keywords in self._DEVICE_FAMILY_KEYWORDS.items(): + if any(keyword in processor_id for keyword in keywords): + return family + return None + + def _get_device_signature(self) -> Optional[str]: + processor_id = str(getattr(self.device_info, "processor_id", "") or "") + for signature, keywords in self._DEVICE_SIGNATURE_KEYWORDS.items(): + if any(keyword in processor_id for keyword in keywords): + return signature + return None + + def check_device_compatibility(self) -> dict: + """Check whether the loaded ELF appears compatible with the connected target.""" + file_family = self.parser.get_target_family() if hasattr(self.parser, "get_target_family") else None + file_signature = self.parser.get_target_signature() if hasattr(self.parser, "get_target_signature") else file_family + device_family = self._get_device_family() + device_signature = self._get_device_signature() or device_family + checked = bool(file_signature and device_signature) + compatible = (file_signature == device_signature) if checked else None + if compatible is False: + reason = "ELF file and connected target appear to describe different MCU targets." + elif checked: + reason = "ELF file appears compatible with the connected target." + else: + reason = "Compatibility could not be determined." + return { + "checked": checked, + "compatible": compatible, + "device_family": device_family, + "file_family": file_family, + "device_signature": device_signature, + "file_signature": file_signature, + "processor_id": str(getattr(self.device_info, "processor_id", "") or ""), + "elf_file": getattr(self.parser, "elf_path", ""), + "reason": reason, + } - def _build_export_file_name(self, filename: str = None, ext: FileType= FileType.YAML): + def _warn_if_incompatible(self, elf_path: str): + """Warn when the loaded ELF appears incompatible with the connected target.""" + compatibility = self.check_device_compatibility() + if compatibility["compatible"] is False: + warnings.warn( + ( + f"Loaded ELF '{elf_path}' appears incompatible with the connected target " + f"({compatibility['processor_id']})." + ), + stacklevel=2, + ) + + def _build_export_file_name(self, filename: Optional[str] = None, ext: FileType= FileType.YAML): if filename is None: if self.parser.elf_path is not None: # get the elf_file name without extension @@ -110,7 +179,38 @@ def _build_export_file_name(self, filename: str = None, ext: FileType= FileType. return os.path.splitext(filename)[0] + ext.value - def export_variables(self, filename: str = None, ext: FileType = FileType.YAML, items=None): + def _resolve_export_item(self, item): + """Resolve an export item to VariableInfo and its target map kind.""" + variable_info = None + is_register = False + + if isinstance(item, tuple) and len(item) == self._NAME_SFR_PAIR_LEN: + name, sfr = item + is_register = bool(sfr) + if isinstance(name, str): + variable_info = self.parser.get_var_info(name, sfr=is_register) + elif isinstance(item, Variable): + item = item.info.name + + if isinstance(item, VariableInfo): + if self.parser.register_map.get(item.name) == item: + variable_info = item + is_register = True + elif self.parser.variable_map.get(item.name) == item: + variable_info = item + else: + variable_info = item + is_register = item.name in self.parser.register_map + elif isinstance(item, str): + if item in self.parser.variable_map: + variable_info = self.parser.variable_map.get(item) + elif item in self.parser.register_map: + variable_info = self.parser.register_map.get(item) + is_register = True + + return variable_info, is_register + + def export_variables(self, filename: Optional[str] = None, ext: FileType = FileType.YAML, items=None): """Store the variables registered on the elf file to a pickle file. Args: @@ -122,13 +222,17 @@ def export_variables(self, filename: str = None, ext: FileType = FileType.YAML, raise ValueError("Elf file is not yet supported as export format...") filename = self._build_export_file_name(filename, ext) - export_dict = {} + export_dict = {"variables": {}, "registers": {}} if items: for item in items: - variable_name = item.info.name if isinstance(item, Variable) else item - export_dict[variable_name] = self.parser.variable_map.get(variable_name) + variable_info, is_register = self._resolve_export_item(item) + if variable_info is None: + continue + target_key = "registers" if is_register else "variables" + export_dict[target_key][variable_info.name] = variable_info else: - export_dict = self.parser.variable_map + export_dict["variables"] = dict(self.parser.variable_map) + export_dict["registers"] = dict(self.parser.register_map) if ext is FileType.PICKLE: with open(filename, 'wb') as file: @@ -156,15 +260,25 @@ def import_variables(self, filename: str): # clear any previous loaded variable self.parser.variable_map.clear() + self.parser.register_map.clear() + imported_data = None if ext is FileType.ELF: self.parser = GenericParser(filename) + self._warn_if_incompatible(filename) if ext is FileType.PICKLE: with open(filename, 'rb') as file: - self.parser.variable_map = pickle.load(file) + imported_data = pickle.loads(file.read()) if ext is FileType.YAML: with open(filename, 'r') as file: - self.parser.variable_map = yaml.load(file, Loader=yaml.FullLoader) + imported_data = yaml.load(file.read(), Loader=yaml.FullLoader) + + if ext is not FileType.ELF: + if isinstance(imported_data, dict) and "variables" in imported_data: + self.parser.variable_map = imported_data.get("variables", {}) or {} + self.parser.register_map = imported_data.get("registers", {}) or {} + else: + self.parser.variable_map = imported_data or {} logging.debug(f"Variables loaded from {filename}") diff --git a/pyx2cscope/x2cscope.py b/pyx2cscope/x2cscope.py index fd12d693..7bf2eb5b 100644 --- a/pyx2cscope/x2cscope.py +++ b/pyx2cscope/x2cscope.py @@ -9,7 +9,7 @@ import logging from dataclasses import dataclass from numbers import Number -from typing import Dict, List +from typing import Dict, List, Optional from mchplnet.interfaces.abstract_interface import Interface from mchplnet.interfaces.factory import InterfaceFactory, InterfaceType @@ -156,7 +156,7 @@ def get_variable_raw(self, variable_info: VariableInfo) -> Variable: """ return self.variable_factory.get_variable_raw(variable_info) - def export_variables(self, filename: str = None, ext: FileType = FileType.YAML, items=None): + def export_variables(self, filename: Optional[str] = None, ext: FileType = FileType.YAML, items=None): """Store the variables registered on the elf file to a pickle file. Args: @@ -177,6 +177,10 @@ def import_variables(self, filename: str): """ self.variable_factory.import_variables(filename) + def check_compatibility(self) -> dict: + """Check whether the currently loaded ELF appears compatible with the connected target.""" + return self.variable_factory.check_device_compatibility() + def add_scope_channel(self, variable: Variable, trigger: bool = False) -> int: """Add a variable as a scope channel. @@ -256,13 +260,14 @@ def set_sample_time(self, sample_time: int): """Define the resolution how the samples will be buffered at the internal buffer. This can be used to extend total sampling time at the cost of resolution. - 0 = every sample, 1 = every 2nd sample, 2 = every 3rd sample … + In the pyX2Cscope API, values start at 1, while LNET uses a 0-based pre-scaler. + 1 = every sample, 2 = every 2nd sample, 3 = every 3rd sample ... Args: sample_time (int): The sample time factor. """ - sample_time = 0 if sample_time < 0 else sample_time - self.scope_setup.set_sample_time_factor(sample_time) + sample_time = 1 if sample_time < 1 else sample_time + self.scope_setup.set_sample_time_factor(sample_time - 1) def set_scope_state(self, scope_state: int): """Set the state of the scope. diff --git a/quality.txt b/quality.txt deleted file mode 100644 index 27746401e678474c97ce725141c56561b3e65521..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 566 zcmZXR;ZB1<5QOJ9?}90`w)#Wgg;a>7RIYF)_T{bL9MF)O%R#a;J2Sg?`wi+T(Kft%n7TzY^OlP!5@Eo+lX~c0zEUPW7 zwObAz^+87@L5)KFk00RFFpl`1IW1f}@6epwGyJaV(gYLcx+mN)fw72!lezC@#yt5N z%hB#}IVIMlV2?+QT$fdXh2e}Bwln#RNyAfb?>Pq0hT{z0g*$g-Xs8c3yIz8n332*T zuX9Gfjq_Jb+2Jwh9rhkhao6{@BU*uP&*|te>!4?N%k~@U708K=j`O_kYv%;!dUn74 EAMSQn`v3p{ diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index d9a6c9536004e3d7cdeb925bb6efa79969d13224..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 472 zcmaKpO-{m46ot>CqHh!v1H&>@K>EL&6;{zwA$xD0B-#xF# z!$v!8Rq0EOO10YSUK5Qq)1@x-47ya}%kvrYBF?)&t=*yO&^>_Pf?o&6TB8-n3jGI9 zOO9R4nBZehhdRM*!4qer*Sx_T-c7K2eZ#b$Q;L`mS@u|=EAf8M$|1qnB4Z=Hknskq zBI!|2;pIkdJI#HPYIway@OrJ;sOPO==IrS|{Oy_lFW>&B9v%60w6$l-w_(?zsXKmx btoa`$%sO+5*mE9xdbcZX delete generated files - os.remove("my_variables.yml") - os.remove("qspin_foc_same54.yml") - os.remove("my_single_variable.pkl") + assert sfr_reloaded is not None, "reloaded SFR should be available" + assert sfr_reloaded.info.name == sfr_name, "reloaded SFR name mismatch" + assert len(x2c_reloaded.list_sfr()) == 1, "import loaded more than one SFR" diff --git a/tests/test_pyx2cscope_class.py b/tests/test_pyx2cscope_class.py index 4615fd97..be39c319 100644 --- a/tests/test_pyx2cscope_class.py +++ b/tests/test_pyx2cscope_class.py @@ -9,6 +9,8 @@ from tests import data from tests.utils.serial_stub import fake_serial +LNET_SAMPLE_TIME_THIRD_SAMPLE = 2 + class TestPyX2CScope: """Tests related to the PyX2CScope class.""" @@ -16,6 +18,12 @@ class TestPyX2CScope: elf_file = os.path.join( os.path.dirname(data.__file__), "mc_foc_sl_fip_dspic33ck_mclv48v300w.elf" ) + arm_elf_file = os.path.join( + os.path.dirname(data.__file__), "qspin_foc_same54.elf" + ) + dspic33a_elf_file = os.path.join( + os.path.dirname(data.__file__), "dsPIC33ak128mc106_foc.elf" + ) def test_missing_elf_file_16(self, mocker): """Check if the corresponding exception is raised in case of wrong 16 bit elf path.""" @@ -53,8 +61,8 @@ def test_missing_interface(self, mocker): def test_missing_com_port(self, mocker): """Check class behavior in case of non COM-Port initialization. - A com-port must not be provided, in this case, the default COM1 - is used but a warning is generated on the console. + A com-port must not be provided, in this case, AUTO detection + is attempted but a warning is generated on the console. """ fake_serial(mocker, 16) with warnings.catch_warnings(record=True) as w: @@ -67,14 +75,69 @@ def test_missing_com_port(self, mocker): # Verify the warning was raised assert len(w) == 2 # noqa: PLR2004 assert issubclass(w[-1].category, Warning) is True - assert "No port provided, using default COM1" in str(w[-1].message) + assert "No port provided, will attempt auto-detection" in str(w[-1].message) # Clean up scope.disconnect() def test_wrong_com_port(self): """Check handling of a non-existent COM-PORT input.""" - with pytest.raises( - RuntimeError, match=r"Failed to retrieve device information" - ): + with pytest.raises(Exception, match=r"could not open port '?COM0'?"): X2CScope(elf_file=self.elf_file, port="COM0") + + def test_set_sample_time_uses_user_facing_factor(self, mocker): + """Check sample time values are translated from 1-based API to 0-based LNET.""" + fake_serial(mocker, 16) + scope = X2CScope(elf_file=self.elf_file, port="COM1") + try: + scope.set_sample_time(1) + assert scope.scope_setup.sample_time_factor == 0 + + scope.set_sample_time(3) + assert scope.scope_setup.sample_time_factor == LNET_SAMPLE_TIME_THIRD_SAMPLE + + scope.set_sample_time(0) + assert scope.scope_setup.sample_time_factor == 0 + finally: + scope.disconnect() + + def test_incompatible_elf_emits_warning(self, mocker): + """Check mismatched ELF and target families generate a warning.""" + fake_serial(mocker, 16) + with warnings.catch_warnings(record=True) as captured: + warnings.simplefilter("always") + scope = X2CScope(elf_file=self.arm_elf_file, port="COM1") + try: + assert any("appears incompatible" in str(w.message) for w in captured) + finally: + scope.disconnect() + + def test_check_compatibility(self, mocker): + """Check the compatibility API reports a matching dsPIC ELF correctly.""" + fake_serial(mocker, 16) + scope = X2CScope(elf_file=self.elf_file, port="COM1") + try: + compatibility = scope.check_compatibility() + assert compatibility["checked"] is True + assert compatibility["compatible"] is True + assert compatibility["file_family"] == "dspic" + assert compatibility["device_family"] == "dspic" + assert compatibility["file_signature"] == "dspic" + finally: + scope.disconnect() + + def test_check_compatibility_detects_dspic33a_mismatch(self, mocker): + """Check the compatibility API distinguishes dsPIC33A from generic dsPIC targets.""" + fake_serial(mocker, 16) + with warnings.catch_warnings(record=True) as captured: + warnings.simplefilter("always") + scope = X2CScope(elf_file=self.dspic33a_elf_file, port="COM1") + try: + compatibility = scope.check_compatibility() + assert compatibility["checked"] is True + assert compatibility["compatible"] is False + assert compatibility["file_signature"] == "dspic33a" + assert compatibility["device_signature"] == "dspic" + assert any("appears incompatible" in str(w.message) for w in captured) + finally: + scope.disconnect() diff --git a/tests/test_qt_gui.py b/tests/test_qt_gui.py index e2682761..1fd5ddec 100644 --- a/tests/test_qt_gui.py +++ b/tests/test_qt_gui.py @@ -10,13 +10,28 @@ """ import os -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch import pytest # Set headless mode before importing Qt modules os.environ["QT_QPA_PLATFORM"] = "offscreen" +from tests import data + + +@pytest.fixture +def mock_can_bus(): + """Create a mock CAN bus for testing.""" + with patch('can.interface.Bus') as mock_bus_class: + mock_bus_instance = MagicMock() + mock_bus_instance._is_shutdown = False + mock_bus_instance.recv = MagicMock(return_value=None) + mock_bus_instance.send = MagicMock() + mock_bus_instance.shutdown = MagicMock() + mock_bus_class.return_value = mock_bus_instance + yield mock_bus_class, mock_bus_instance + class TestAppStateModel: """Tests for AppState model.""" @@ -259,6 +274,38 @@ def test_connect_uart_creates_x2cscope( assert result is True + def test_connect_uart_imports_selected_file(self, connection_manager, mocker): + """Test UART connection always loads variables through import_variables.""" + mock_x2c = MagicMock() + mock_x2c.list_variables.return_value = [] + mocker.patch( + "pyx2cscope.gui.qt.controllers.connection_manager.X2CScope", + return_value=mock_x2c, + ) + + result = connection_manager.connect_uart( + port="COM1", baud_rate=115200, elf_file="firmware.elf" + ) + + assert result is True + mock_x2c.import_variables.assert_called_once_with("firmware.elf") + + def test_connect_tcp_imports_yml_after_connect(self, connection_manager, mocker): + """Test TCP connection loads YML imports after creating the transport.""" + mock_x2c = MagicMock() + mock_x2c.list_variables.return_value = [] + mocker.patch( + "pyx2cscope.gui.qt.controllers.connection_manager.X2CScope", + return_value=mock_x2c, + ) + + result = connection_manager.connect_tcp( + host="127.0.0.1", tcp_port=12666, elf_file="variables.yml" + ) + + assert result is True + mock_x2c.import_variables.assert_called_once_with("variables.yml") + def test_disconnect_clears_state(self, connection_manager): """Test disconnect clears connection state.""" connection_manager.disconnect() @@ -322,6 +369,25 @@ def test_setup_tab_creation(self, qt_application): assert tab is not None + def test_app_state_exports_selected_variables(self, qt_application): + """Test AppState exports only variables selected in watch and scope views.""" + from pyx2cscope.gui.qt.models.app_state import AppState, ScopeChannel + + app_state = AppState() + mock_x2c = MagicMock() + + mock_x2c.list_variables.return_value = ["watch.var", "scope.var"] + + app_state.set_x2cscope(mock_x2c) + app_state.add_live_watch_var() + app_state.update_live_watch_var_field(0, "name", "watch.var") + app_state.set_scope_channel(0, ScopeChannel(name="scope.var", sfr=True)) + + app_state.export_selected_variables("selected.yml") + + exported_items = mock_x2c.export_variables.call_args.kwargs["items"] + assert set(exported_items) == {("watch.var", False), ("scope.var", True)} + def test_scope_view_tab_creation(self, qt_application): """Test ScopeViewTab can be created.""" from pyx2cscope.gui.qt.models.app_state import AppState @@ -388,6 +454,77 @@ def test_stop_polling(self, data_poller): assert data_poller._running is False +class TestCANConnectionManager: + """Tests for ConnectionManager CAN connect/disconnect cycle.""" + + elf_file = os.path.join( + os.path.dirname(data.__file__), "mc_foc_sl_fip_dspic33ck_mclv48v300w.elf" + ) + + def test_qt_connection_manager_disconnect_calls_x2cscope_disconnect(self, mock_can_bus): + """Test that Qt ConnectionManager properly calls x2cscope.disconnect().""" + from unittest.mock import patch + + from PyQt5.QtCore import QCoreApplication + + from pyx2cscope.gui.qt.controllers.connection_manager import ConnectionManager + from pyx2cscope.gui.qt.models.app_state import AppState + + app = QCoreApplication.instance() + if app is None: + app = QCoreApplication([]) + + try: + _, mock_bus_instance = mock_can_bus + + app_state = AppState() + conn_manager = ConnectionManager(app_state) + + device_info_frame = bytearray( + b'\x55\x2E\x01\x11\x00' + b'\x01\x00' + b'\x01\x00' + b'\x10\x82' + b'01/01/2024' + b'1200' + b'01/01/2024' + b'1200' + b'\x01' + b'\x00\x00' + b'\x00\x00\x00\x00' + b'\x00\x00\x00\x00' + b'\x00' + ) + load_param_frame = bytearray(b'\x55\x00\x01\x12\x00\x00') + + with patch('mchplnet.interfaces.can.LNetCan.read') as mock_read, \ + patch('mchplnet.interfaces.can.LNetCan.write'): + mock_read.side_effect = [device_info_frame, load_param_frame] + + success = conn_manager.connect_can( + elf_file=self.elf_file, + bus_type="USB", + channel=1, + baudrate="500K", + mode="Standard", + tx_id="110", + rx_id="100", + ) + + assert success is True + assert app_state.is_connected() + + conn_manager.disconnect() + + assert not app_state.is_connected() + assert app_state.x2cscope is None + assert mock_bus_instance.shutdown.called + + finally: + if app: + app.quit() + + class TestSignalSlotConnections: """Tests for signal/slot connections.""" diff --git a/tests/test_web_gui.py b/tests/test_web_gui.py index 69b44683..282709ae 100644 --- a/tests/test_web_gui.py +++ b/tests/test_web_gui.py @@ -16,6 +16,7 @@ # HTTP status codes for test assertions HTTP_OK = 200 +HTTP_BAD_REQUEST = 400 HTTP_NOT_FOUND = 404 @@ -57,6 +58,7 @@ def test_index_route(self, flask_client): assert response.status_code == HTTP_OK assert b"html" in response.data.lower() + assert b"exportVariablesToggle" in response.data def test_serial_ports_route(self, flask_client, mocker): """Test serial-ports route returns list.""" @@ -112,6 +114,85 @@ def test_variables_route_not_connected(self, flask_client): assert "items" in data assert data["items"] == [] + def test_export_variables_route(self, flask_client, mocker): + """Test variable export route returns a downloadable file.""" + from pyx2cscope.gui.web.scope import web_scope + + mock_x2c = MagicMock() + original_x2c = web_scope.x2c_scope + original_file = web_scope.variables_file + original_watch = web_scope.watch_vars + original_scope = web_scope.scope_vars + original_dashboard = web_scope.dashboard_vars + + def write_export_file(filename, ext, items=None): + mode = "wb" if ext.value == ".pkl" else "w" + payload = b"pickle-data" if mode == "wb" else "yaml-data" + with open(filename, mode) as file: + file.write(payload) + + mock_x2c.export_variables.side_effect = write_export_file + mocker.patch.object(web_scope, "is_connected", return_value=True) + web_scope.x2c_scope = mock_x2c + web_scope.variables_file = "firmware.elf" + watch_var = MagicMock() + watch_var.info.name = "watch.var" + scope_var = MagicMock() + scope_var.info.name = "scope.var" + dashboard_var = MagicMock() + dashboard_var.info.name = "dashboard.var" + web_scope.watch_vars = [{"variable": watch_var, "sfr": True}] + web_scope.scope_vars = [{"variable": scope_var, "sfr": False}] + web_scope.dashboard_vars = {"dashboard.var": dashboard_var} + + try: + response = flask_client.get("/variables/export?ext=yml") + + assert response.status_code == HTTP_OK + assert response.headers["Content-Disposition"] == "attachment; filename=firmware.yml" + assert response.data == b"yaml-data" + assert mock_x2c.export_variables.call_args.kwargs["items"] == [ + ("watch.var", True), + ("scope.var", False), + ("dashboard.var", False), + ] + finally: + web_scope.x2c_scope = original_x2c + web_scope.variables_file = original_file + web_scope.watch_vars = original_watch + web_scope.scope_vars = original_scope + web_scope.dashboard_vars = original_dashboard + + def test_export_variables_route_requires_selected_variables(self, flask_client, mocker): + """Test export route fails when no view has selected variables.""" + from pyx2cscope.gui.web.scope import web_scope + + original_x2c = web_scope.x2c_scope + original_file = web_scope.variables_file + original_watch = web_scope.watch_vars + original_scope = web_scope.scope_vars + original_dashboard = web_scope.dashboard_vars + + mocker.patch.object(web_scope, "is_connected", return_value=True) + web_scope.x2c_scope = MagicMock() + web_scope.variables_file = "firmware.elf" + web_scope.watch_vars = [] + web_scope.scope_vars = [] + web_scope.dashboard_vars = {} + + try: + response = flask_client.get("/variables/export?ext=yml") + + assert response.status_code == HTTP_BAD_REQUEST + data = json.loads(response.data) + assert "No variables are selected" in data["msg"] + finally: + web_scope.x2c_scope = original_x2c + web_scope.variables_file = original_file + web_scope.watch_vars = original_watch + web_scope.scope_vars = original_scope + web_scope.dashboard_vars = original_dashboard + class TestWatchViewRoutes: """Tests for watch view routes.""" @@ -313,7 +394,7 @@ def test_scope_trigger_defaults(self, web_scope): def test_scope_sample_time_default(self, web_scope): """Test scope sample time default.""" - assert web_scope.scope_sample_time == 0 + assert web_scope.scope_sample_time == 1 class TestWebScopeVariableManagement: @@ -358,6 +439,13 @@ def test_add_watch_var(self, web_scope_connected): assert result is not None assert len(web_scope_connected.watch_vars) == 1 + assert web_scope_connected.watch_vars[0]["sfr"] is False + + def test_add_watch_var_tracks_sfr_flag(self, web_scope_connected): + """Test adding watch SFR variable preserves its SFR flag.""" + web_scope_connected.add_watch_var("test_var", sfr=True) + + assert web_scope_connected.watch_vars[0]["sfr"] is True def test_add_watch_var_duplicate_prevented(self, web_scope_connected): """Test duplicate watch variables are not added.""" @@ -380,6 +468,13 @@ def test_add_scope_var(self, web_scope_connected): assert result is not None assert len(web_scope_connected.scope_vars) == 1 + assert web_scope_connected.scope_vars[0]["sfr"] is False + + def test_add_scope_var_tracks_sfr_flag(self, web_scope_connected): + """Test adding scope SFR variable preserves its SFR flag.""" + web_scope_connected.add_scope_var("test_var", sfr=True) + + assert web_scope_connected.scope_vars[0]["sfr"] is True def test_add_scope_var_max_limit(self, web_scope_connected): """Test scope variables can be added (max limit may not be enforced in WebScope).""" @@ -405,6 +500,16 @@ def test_remove_scope_var(self, web_scope_connected): web_scope_connected.remove_scope_var("test_var") assert len(web_scope_connected.scope_vars) == 0 + def test_scope_sample_time_uses_user_facing_values(self, web_scope_connected): + """Test web scope clamps sample time to user-facing 1-based values.""" + web_scope_connected.x2c_scope.get_scope_sample_time.return_value = 2.5 + web_scope_connected.scope_sample_time = 2 + + web_scope_connected.scope_set_sample("off", 0, 20) + + assert web_scope_connected.scope_sample_time == 1 + web_scope_connected.x2c_scope.set_sample_time.assert_called_once_with(1) + class TestWebScopeScaledValue: """Tests for WebScope scaled value calculation.""" diff --git a/tests/utils/serial_stub.py b/tests/utils/serial_stub.py index 0e329028..30912067 100644 --- a/tests/utils/serial_stub.py +++ b/tests/utils/serial_stub.py @@ -9,6 +9,10 @@ BIT_LENGTH_16 = 16 BIT_LENGTH_32 = 32 +DEVICE_PROFILE_DSPIC = "dspic" +DEVICE_PROFILE_ARM = "arm" +DEVICE_PROFILE_PIC32 = "pic32" +DEVICE_PROFILE_DSPIC33A = "dspic33a" class FrameBuilder(LNetFrame): """FrameBuilder class to build LNet frames.""" @@ -66,13 +70,16 @@ def _check_frame_protocol(self): class SerialStub: """Fakes a serial connection for 16 and 32 bit devices.""" - def __init__(self, uc_width=BIT_LENGTH_16): + def __init__(self, uc_width=BIT_LENGTH_16, device_profile=None): """Constructor of the SerialStub class. Expected get_device_info and load_param bytestream are initialized here. """ self.delay_seconds = 0.2 # 200ms delay to make concurrency self.uc_width = uc_width + self.device_profile = device_profile or ( + DEVICE_PROFILE_DSPIC if uc_width == BIT_LENGTH_16 else DEVICE_PROFILE_PIC32 + ) self.data = bytearray() self.device_info = bytearray(b"\x55\x01\x01\x00\x57") self.load_param = bytearray(b"\x55\x03\x01\x11\x01\x00\x6b") @@ -85,9 +92,9 @@ def __init__(self, uc_width=BIT_LENGTH_16): def lnet_serial_start(self): """Mocker for the start interface function. - As we are dealing with no real device, we just return. + As we are dealing with no real device, we just return True. """ - return + return True def _get_ram(self): """Handle a get_ram request.""" @@ -146,18 +153,27 @@ def lnet_serial_read(self) -> bytearray: def _get_device_info(self): if self.data != self.device_info: raise ValueError("Wrong Device Info package format!") - if self.uc_width == BIT_LENGTH_16: + if self.device_profile == DEVICE_PROFILE_DSPIC: return bytearray( b"\x55.\x01\x00\x00\x05\x00\x01\x00\xff\x10\x82Nov2320231500\x00\x00\x00\x00" b"\x00\x00\xccJ\x14K\xc0\xff\xff\x01\x00\x00\x00\x00\x00\x00\xf4R\x00\x00\xba" ) - elif self.uc_width == BIT_LENGTH_32: + if self.device_profile == DEVICE_PROFILE_PIC32: return bytearray( b"\x55.\x01\x00\x00\x05\x00\x01\x00\xff \x82Mar 320191220Mar 320191220\x01" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00 T" ) - else: - raise ValueError("Mocker fake_serial: wrong uC width! should be either 16 or 32 bits") + if self.device_profile == DEVICE_PROFILE_ARM: + return bytearray( + b"\x55.\x01\x00\x00\x05\x00\x01\x00\xff\x10\x83Mar 320191220Mar 320191220\x01" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00 T" + ) + if self.device_profile == DEVICE_PROFILE_DSPIC33A: + return bytearray( + b"\x55.\x01\x00\x00\x05\x00\x01\x00\xff@\x82Apr 120241200Apr 120241200\x01" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00 T" + ) + raise ValueError("Mocker fake_serial: unsupported device profile") def _get_load_param(self) -> bytearray: if self.data != self.load_param: @@ -175,7 +191,7 @@ def _get_load_param(self) -> bytearray: else: raise ValueError("Mocker fake_serial: wrong uC width! should be either 16 or 32 bits") -def fake_serial(mocker, uc_width=BIT_LENGTH_16): +def fake_serial(mocker, uc_width=BIT_LENGTH_16, device_profile=None): """Fakes a serial port for 16/32 bit devices. The methods being faked by this function are start, read and write. @@ -183,10 +199,12 @@ def fake_serial(mocker, uc_width=BIT_LENGTH_16): Args: mocker: Mocker inheritance uc_width: bit size of the device to be mocked + device_profile: target profile to be mocked Return: - None + SerialStub: the configured serial stub instance """ - serial_stub = SerialStub(uc_width=uc_width) + serial_stub = SerialStub(uc_width=uc_width, device_profile=device_profile) mocker.patch.object(LNetSerial, "start", serial_stub.lnet_serial_start) mocker.patch.object(LNetSerial, "write", serial_stub.lnet_serial_write) mocker.patch.object(LNetSerial, "read", serial_stub.lnet_serial_read) + return serial_stub From 0223968b7711609ebf7ad0bc00cd048bddc5960e Mon Sep 17 00:00:00 2001 From: Edras Pacola Date: Thu, 16 Apr 2026 16:25:07 +0200 Subject: [PATCH 09/10] rebase fix --- .gitignore | 1 + pyx2cscope/x2cscope.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index ef1a2588..bb262a6d 100644 --- a/.gitignore +++ b/.gitignore @@ -144,3 +144,4 @@ dmypy.json #backup files .bkp /.claude/ +/.continue/ diff --git a/pyx2cscope/x2cscope.py b/pyx2cscope/x2cscope.py index 7bf2eb5b..d089c26a 100644 --- a/pyx2cscope/x2cscope.py +++ b/pyx2cscope/x2cscope.py @@ -349,7 +349,6 @@ def _read_array_chunks(self) -> List[bytearray]: data_size = chunk_size if i < int(num_chunks) else int(chunk_rest) try: # Read the chunk of data - data_size = chunk_size if i < int(num_chunks) else int(chunk_rest)try: data = self.lnet.get_ram_array(current_address, data_size, data_type) chunk_data.extend(data) except Exception as e: From 61b89d8e926f57ee21f6eb2a6843e6bf147fffc0 Mon Sep 17 00:00:00 2001 From: Edras Pacola Date: Thu, 16 Apr 2026 17:50:20 +0200 Subject: [PATCH 10/10] fix github actions --- .github/workflows/buildexe.yml | 36 +++++++++----- .github/workflows/documentation.yml | 77 ++++++++++++++++++++++++++--- .github/workflows/release.yml | 2 +- 3 files changed, 93 insertions(+), 22 deletions(-) diff --git a/.github/workflows/buildexe.yml b/.github/workflows/buildexe.yml index ef567d94..a53e3da0 100644 --- a/.github/workflows/buildexe.yml +++ b/.github/workflows/buildexe.yml @@ -1,7 +1,9 @@ name: Build PyInstaller Executable on: - workflow_dispatch: + release: + types: [published] + jobs: buildwindows: runs-on: windows-latest @@ -15,7 +17,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: '3.14' + python-version: '3.12' - name: Install dependencies run: | @@ -31,11 +33,15 @@ jobs: run: | echo pyX2Cscope.exe -w > dist\pyx2cscope\startWebInterface.bat - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: pyX2Cscope-windows - path: dist/pyX2Cscope + - name: Zip artifact + run: | + Compress-Archive -Path dist/pyX2Cscope -DestinationPath pyX2Cscope_win_amd64_${{ github.event.release.tag_name }}.zip + + - name: Upload to release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release upload ${{ github.event.release.tag_name }} pyX2Cscope_win_amd64_${{ github.event.release.tag_name }}.zip buildlinux: @@ -50,7 +56,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: '3.14' + python-version: '3.12' - name: Install dependencies run: | @@ -68,8 +74,12 @@ jobs: echo './pyX2Cscope_linux -w' >> dist/pyX2Cscope_linux/startWebInterface.sh chmod +x dist/pyX2Cscope_linux/startWebInterface.sh - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: pyX2Cscope-linux - path: dist/pyX2Cscope_linux + - name: Zip artifact + run: | + zip -r pyX2Cscope_linux_amd64_${{ github.event.release.tag_name }}.zip dist/pyX2Cscope_linux + + - name: Upload to release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release upload ${{ github.event.release.tag_name }} pyX2Cscope_linux_amd64_${{ github.event.release.tag_name }}.zip diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 5f4d59f8..2a7b1be3 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -13,7 +13,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: "3.14" + python-version: "3.12" - name: Install dependencies run: | @@ -44,14 +44,55 @@ jobs: # pylint ./pyx2cscope tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install system dependencies for Qt + run: | + sudo apt-get update + sudo apt-get install -y \ + libxkbcommon-x11-0 \ + libxcb-icccm4 \ + libxcb-image0 \ + libxcb-keysyms1 \ + libxcb-randr0 \ + libxcb-render-util0 \ + libxcb-xinerama0 \ + libxcb-xfixes0 \ + libegl1 \ + libgl1 \ + xvfb + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e ".[dev]" + + - name: Run tests + env: + QT_QPA_PLATFORM: offscreen + DISPLAY: ":99.0" + run: | + Xvfb :99 -screen 0 1024x768x24 & + sleep 2 + pytest -v + + tests-matrix: runs-on: ubuntu-latest strategy: + fail-fast: false matrix: - python-version: ["3.10", "3.14"] + python-version: ["3.10", "3.13", "3.14"] steps: - uses: actions/checkout@v4 - - name: Set up Python + - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} @@ -84,19 +125,39 @@ jobs: run: | Xvfb :99 -screen 0 1024x768x24 & sleep 2 - pytest -v + pytest -v docs: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e ".[docs]" + + - name: Build documentation + run: | + sphinx-build -M html doc build --keep-going + + docs-matrix: runs-on: ubuntu-latest strategy: + fail-fast: false matrix: - python-version: ["3.10", "3.14"] - + python-version: ["3.10", "3.13", "3.14"] steps: - name: Checkout repository uses: actions/checkout@v4 - - name: Set up Python + - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} @@ -108,7 +169,7 @@ jobs: - name: Build documentation run: | - sphinx-build -M html doc build --keep-going + sphinx-build -M html doc build --keep-going version-check: runs-on: ubuntu-latest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 65e76140..29b477b9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,7 +15,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: '3.14' + python-version: '3.12' - name: Install dependencies run: |