diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..9c9dc6b3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,138 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*.pyc +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# Virtual environments +venv/ +ENV/ +env/ +.venv/ +.ENV/ +.env/ +.venv/ + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ +docsrc/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.env.* +.venv +.venv.* +env/ +env.bak/ +venv/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ +*.pyc \ No newline at end of file diff --git a/parameters.yaml b/parameters.yaml new file mode 100644 index 00000000..4950435f --- /dev/null +++ b/parameters.yaml @@ -0,0 +1,96 @@ +bedTemperature: '120' +chamberTemperature: '170' +d: '0.5' +dosingHeight: '0.1' +heatedBufferHeight: '1' +heatedBufferRecoatingSequence: 'G91 + + G0 Z{layerHeight} Y-{dosingHeight} + + G90 + + M400 + + recoat + + M400 + + ' +i: '0.01' +initialLevellingHeight: '0' +initialLevellingRecoatingSequence: 'G91 + + G0 Y-{dosingHeight} + + G90 + + M400 + + recoat + + M400 + + ' +layerHeight: '0.1' +moveToStartingSequence: 'G28 Z Y + + M400 + + G0 Z0 Y{powderLoadingHeight} + + M400' +p: '10' +partHeight: '75' +powderLoadingExtraHeightGap: '20' +powderLoadingSequence: 'G28 Z Y + + G0 Y{powderLoadingHeight} F600 + + M400 + + G91 + + G0 Y{powderLoadingExtraHeightGap} F600 + + G90 + + M400 + + goDown + + goDown + + goDown + + goDown + + ' +prepareForPartRemovalSequence: 'G91 + + G0 Z{powderLoadingExtraHeightGap} Y{powderLoadingExtraHeightGap} + + G90 + + M400 + + goDown + + goDown + + goDown + + goDown' +printingRecoatingSequence: 'G91 + + G0 Z{layerHeight} Y-{dosingHeight} + + G90 + + M400 + + recoat + + M400 + + ' +volumeTemperature: '120' diff --git a/requirements.txt b/requirements.txt index b9235944..12818127 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,14 @@ PyQt5 requests -python-octoprint -Pillow \ No newline at end of file +Pillow +moonrakerpy +opencv-python +numpy +flask +crcmod +serial +pyserial +cmapy +simple_pid +pyyaml +pyqtgraph diff --git a/src/Feeltek/3.1.png b/src/Feeltek/3.1.png new file mode 100644 index 00000000..44c8b321 Binary files /dev/null and b/src/Feeltek/3.1.png differ diff --git a/src/Feeltek/Feektek API Doccumentation (edited).pdf b/src/Feeltek/Feektek API Doccumentation (edited).pdf new file mode 100644 index 00000000..ef59abd5 Binary files /dev/null and b/src/Feeltek/Feektek API Doccumentation (edited).pdf differ diff --git a/src/Feeltek/Feektek API Doccumentation.pdf b/src/Feeltek/Feektek API Doccumentation.pdf new file mode 100644 index 00000000..70a5d3bc Binary files /dev/null and b/src/Feeltek/Feektek API Doccumentation.pdf differ diff --git a/src/Feeltek/Feektek_API_Documentation.md b/src/Feeltek/Feektek_API_Documentation.md new file mode 100644 index 00000000..7fc1b30b --- /dev/null +++ b/src/Feeltek/Feektek_API_Documentation.md @@ -0,0 +1,1814 @@ +# LenMark Marking Software Manual + +## 3DS Secondary Development - Network Port Communication + +### Suzhou FEELTEK Laser Technology Co., Ltd. +**Address:** +NO.36, Hua Da Road 3rd-5th floor, +Building I, Zhangjiagang Free Trade Zone, +Zhangjiagang, Jiangsu, China + +**Chinese Address:** +苏州菲镭泰克激光技术有限公司 +地址:中国江苏张家港保税区科创园 I 栋 3-5 楼 + +--- + +## 1. Document Properties + +| Document Item | LenMark marking software manual | +|----------------|--------------------------------| +| Document Name | 3DS secondary development - network port communication | +| Document State | Finish | + +## 2. Document Change Process + +| Version Number | Revision Date | +|---------------|---------------| +| 2022.1.0 | 2022.4.27 | +| 2022.1.1 | 2022.7.19 | +| 2022.1.2 | 2022.10.21 | +| 2023.1.0 | 2023.9.6 | + +*(Version number definition rules: year. major version number. minor version number)* + +--- + +## Catalog + +### 1. Overview + +### 2. Interface Description +- **2.1** File Operation Class +- **2.2** Scanhead Operation +- **2.3** Parameter Operation +- **2.4** Primitive Operation Function +- **2.5** Input and Output Operation Functions +- **2.6** Alarm Operation +- **2.7** Visual Function +- **2.8** Stepper Motor Function + +### 3. Interface Calling Process +- **3.1** Schematic Diagram of the Overall Software Workflow +- **3.2** CCD Calling Process Diagram +- **3.3** Motor Shaft Control Diagram + +--- + +## 1. Overview + +1. Based on TCP and IP protocols, the data message takes the form of a JSON data packet format string. +2. This interface serves as a TCP server, listening on port **50000**. +3. Each function is completed through a **Request** and a **Return**. + +--- + +## 2. Interface Description + +### 2.1 File Operation Class + +1. Open File + +| Function | open file | +|-----------|-----------| +| Parameter | Path; file path | +| Returned | ret | +| Value | 1: Successfully executed 0: Not executed -1: Failed to open -2: File does not exist | + +**Request:** +```json +{ + "sid": 0, + "cmd": "open_file", + "data": { + "path": "/document.emd" + } +} +``` + +**Return:** +```json +{ + "sid": 0, + "cmd": "open_file", + "ret": 1 +} +``` + +2. Close File + +| Function | Close file without saving | +|-----------|---------------------------| +| Parameter | none | +| Returned | ret | +| Value | 1: Execution successful 0: Not executed | + +**Request:** +```json +{ + "sid": 0, + "cmd": "close_file" +} +``` + +**Return:** +```json +{ + "sid": 0, + "cmd": "close_file", + "ret": 1 +} +``` + +3. Save Document + +| Function | Save document | +|-----------|---------------| +| Parameter | Path; file path, canCover 0: Uncoverable 1: Coverable | +| Returned | ret | +| Value | 1: Saved successfully 0: Not executed -1: The file already exists, the operation is not allowed to overwrite -2: The file name is wrong | + +**Request:** +```json +{ + "sid": 0, + "cmd": "save_file", + "data": { + "path": "./document1.emd", + "cover": 0 + } +} +``` + +**Return:** +```json +{ + "sid": 0, + "cmd": "save_file", + "ret": 1 +} +``` + +### 2.2 Scanhead Operation + +1. Software Running Status + +| Function | Get software running status | +|-----------|-----------------------------| +| Parameter | None | +| Returned | ret | +| Value | 0: Waiting 1: Marking 2: Previewing 3: Already working, marking or previewing not started yet | + +**Request:** +```json +{ + "sid": 0, + "cmd": "get_working_status" +} +``` + +**Return:** +```json +{ + "sid": 0, + "cmd": "get_working_status", + "ret": 1 +} +``` + +2. Start Marking + +| Function | Start marking | +|-----------|---------------| +| Parameter | None | +| Returned | ret | +| Value | 1: Execution successful 0: Not executed | + +**Request:** +```json +{ + "sid": 0, + "cmd": "start_mark" +} +``` + +**Return:** +```json +{ + "sid": 0, + "cmd": "start_mark", + "ret": 1 +} +``` + +3. Stop Marking + +| Function | Stop marking | +|-----------|--------------| +| Parameter | None | +| Returned | ret | +| Value | 1: Execution successful 0: Not executed | + +**Request:** +```json +{ + "sid": 0, + "cmd": "stop_mark" +} +``` + +**Return:** +```json +{ + "sid": 0, + "cmd": "stop_mark", + "ret": 1 +} +``` + +4. Start Preview + +| Function | Start preview | +|-----------|---------------| +| Parameter | None | +| Returned | ret | +| Value | 1: Execution successful 0: Not executed | + +**Request:** +```json +{ + "sid": 0, + "cmd": "start_preview" +} +``` + +**Return:** +```json +{ + "sid": 0, + "cmd": "start_preview", + "ret": 1 +} +``` + +5. Stop Preview + +| Function | Stop preview | +|-----------|--------------| +| Parameter | None | +| Returned | ret | +| Value | 1: Execution successful 0: Not executed | + +**Request:** +```json +{ + "sid": 0, + "cmd": "stop_preview" +} +``` + +**Return:** +```json +{ + "sid": 0, + "cmd": "stop_preview", + "ret": 1 +} +``` + +### 2.3 Parameter Operation + +1. Get the Marking Parameter Based on the Layer Number + +| Function | Get the marking Parameter based on the layer number | +|-----------|-----------------------------------------------------| +| Parameter | layer_id Layer number | +| Returned | ret | +| Value | Layer index 0 — 254, Marking speed, Jump speed, Jump delay, Light on delay, Polygon delay, Laser off delay, Polygon killer time, Laser frequency, YAG, SPI current (A) IPG energy (%), AG first pulse suppression pulse width, Pulse width(us), CO2 first pulse width (%), CO2 first pulse suppression increment step size (%) | + +**Request:** +```json +{ + "sid": 0, + "cmd": "get_markParameters_by_layer", + "data": { + "layer_id": 1 + } +} +``` + +**Return:** +```json +{ + "sid": 0, + "cmd": "get_markParameters_by_layer", + "ret": 1, + "data": { + "layer_id": 1, + "markSpeed": 3000, + "jumpSpeed": 5000, + "jumpDelay": 100, + "laserOnDelay": 100, + "polygonDelay": 100, + "laserOffDelay": 100, + "polygonKillerTime": 100, + "laserFrequency": 100, + "current": 100, + "firstPulseKillerLength": 100, + "pulseWidth": 100, + "firstPulseWidth": 100, + "incrementStep": 100 + } +} +``` + +2. Modify the Marking Parameter According to the Layer Number + +| Function | Modify the marking Parameter according to the layer number | +|-----------|------------------------------------------------------------| +| Parameter | layer_id layer number | +| Returned | ret | +| Value | 1: Execution successful 0: Not executed | + +**Request:** +```json +{ + "sid": 0, + "cmd": "set_markParameters_by_layer", + "data": { + "layer_id": 1, + "markSpeed": 3000, + "jumpSpeed": 5000, + "jumpDelay": 100, + "laserOnDelay": 100, + "polygonDelay": 100, + "laserOffDelay": 100, + "polygonKillerTime": 100, + "laserFrequency": 100, + "current": 100, + "firstPulseKillerLength": 100, + "pulseWidth": 100, + "firstPulseWidth": 100, + "incrementStep": 100 + } +} +``` + +**Return:** +```json +{ + "sid": 0, + "ret": 1, + "cmd": "set_markParameters_by_layer" +} +``` + +3. Get the Marking Parameter Based on the Index + +| Function | Get the marking Parameter based on the index | +|-----------|---------------------------------------------| +| Parameter | index | +| Returned | ret | +| Value | Index, Internal index: -1 outline, 0, Marking speed, Jump speed, Jump delay, Laser on delay, Polygon delay, Laser off delay, Polygon killer time, Laser frequency, YAG, SPI current (A) IPG energy (%), AG first pulse suppression pulse width, Pulse width(us), CO2 first pulse width (%), CO2 first pulse suppression increment step size (%) | + +**Request:** +```json +{ + "sid": 0, + "cmd": "get_markParameters_by_index", + "data": { + "index": 1, + "in_index": -1 + } +} +``` + +**Return:** +```json +{ + "sid": 0, + "ret": 1, + "data": { + "index": 1, + "in_index": -1, + "markSpeed": 3000, + "jumpSpeed": 5000, + "jumpDelay": 100, + "laserOnDelay": 100, + "polygonDelay": 100, + "laserOffDelay": 100, + "polygonKillerTime": 100, + "laserFrequency": 100, + "current": 100, + "firstPulseKillerLength": 100, + "pulseWidth": 100, + "firstPulseWidth": 100, + "incrementStep": 100 + } +} +``` + +4. Modify Marking Parameter Based on Index + +| Function | Modify marking Parameter based on index | +|-----------|----------------------------------------| +| Parameter | index | +| Returned | ret | +| Value | 1: Execution successful 0: Not executed | + +**Request:** +```json +{ + "sid": 0, + "cmd": "set_markParameters_by_index", + "data": { + "index": 1, + "in_index": -1, + "markSpeed": 3000, + "jumpSpeed": 5000, + "jumpDelay": 100, + "laserOnDelay": 100, + "polygonDelay": 100, + "laserOffDelay": 100, + "polygonKillerTime": 100, + "laserFrequency": 100, + "current": 100, + "firstPulseKillerLength": 100, + "pulseWidth": 100, + "firstPulseWidth": 100, + "incrementStep": 100 + } +} +``` + +**Return:** +```json +{ + "sid": 0, + "ret": 1, + "data": { + "index": 1, + "in_index": -1, + "markSpeed": 3000, + "jumpSpeed": 5000, + "jumpDelay": 100, + "laserOnDelay": 100, + "polygonDelay": 100, + "laserOffDelay": 100, + "polygonKillerTime": 100, + "laserFrequency": 100, + "current": 100, + "firstPulseKillerLength": 100, + "pulseWidth": 100, + "firstPulseWidth": 100, + "incrementStep": 100 + } +} +``` + +5. Download Marking Parameter + +| Function | Download marking Parameter | +|-----------|----------------------------| +| Parameter | None | +| Returned | ret | +| Value | 1: Successfully executed 0: Not executed | + +**Request:** +```json +{ + "sid": 0, + "cmd": "download_Parameters" +} +``` + +**Return:** +```json +{ + "sid": 0, + "cmd": "download_Parameters", + "ret": 1 +} +``` + +6. Get the Populated Parameter Based on the Index + +| Function | Get the populated Parameter based on the index | +|-----------|-----------------------------------------------| +| Parameter | index, in_index | +| Returned | ret | +| Value | Object index, Internal indexes 0, 1, and 2 are three-layer filling Parameters respectively, Fill type 0: No filling, 1: One-way filling, 2: Two-way filling, 3: Bow-shaped filling, 4: Back-shaped filling, Evenly distribute fill lines, Whether to enable second padding, Automatic rotation angle, Objects are calculated as a whole, Optimize two-way filling for more complete borders, Use triangle fill mode, Number of boundary rings, Number of markings for the current angle, Current number of markings, Fill in pen number, Fill line space, Fill angle, Fill edge offset, Fill end offset, Fill line reduction, Loop space, The fill angle of the second fill, The angle of each increment | + +**Request:** +```json +{ + "sid": 0, + "cmd": "get_entity_fill_property_by_index", + "data": { + "index": 1, + "in_index": 1 + } +} +``` + +**Return:** +```json +{ + "sid": 0, + "cmd": "get_entity_fill_property_by_index", + "ret": 1, + "data": { + "index": 1, + "in_index": 1, + "fill_mode": 1, + "bEqualDistance": false, + "bSecondFill": false, + "bRotateAngle": false, + "bFillAsOne": false, + "bMoreIntact": false, + "bFill3D": false, + "loopNum": 1, + "iFillMarkTimes": 1, + "iCurMarkTimes": 12, + "layerId": 1, + "fillSpace": 100, + "fillAngle": 100, + "fillEdgeOffset": 100, + "fillStartOffset": 100, + "fillEndOffset": 100, + "fillLineReduction": 100, + "loopSpace": 100, + "secondAngle": 100, + "dRotateAngle": 100 + } +} +``` + +7. Populate Parameter Based on Index Modification + +| Function | Populate Parameter based on index modification | +|-----------|-----------------------------------------------| +| Parameter | index, in_index, fillMode, bEqualDistance, bSecondFill, bRotateAngle, bFillAsOne, bMoreIntact, bFill3D, loopNum, iFillMarkTimes, iCurMarkTimes, layerId, fillSpace, fillAngle, fillEdgeOffset, fillEndOffset, fillLineReduction, loopSpace, secondAngle, dRotateAngle | +| Returned | ret | +| Value | 1: Execution successful 0: Not executed | + +**Request:** +```json +{ + "sid": 0, + "cmd": "set_entity_fill_property_by_index", + "data": { + "index": 1, + "in_index": 1, + "fill_mode": 1, + "bEqualDistance": false, + "bSecondFill": false, + "bRotateAngle": false, + "bFillAsOne": false, + "bMoreIntact": false, + "bFill3D": false, + "loopNum": 1, + "iFillMarkTimes": 1, + "iCurMarkTimes": 12, + "layerId": 1, + "fillSpace": 100, + "fillAngle": 100, + "fillEdgeOffset": 100, + "fillStartOffset": 100, + "fillEndOffset": 100, + "fillLineReduction": 100, + "loopSpace": 100, + "secondAngle": 100, + "dRotateAngle": 100 + } +} +``` + +**Return:** +```json +{ + "sid": 0, + "cmd": "set_entity_fill_property_by_index", + "ret": 1 +} +``` + +### 2.4 Primitive Operation Function + +1. Get the Number of Processing Objects + +| Function | Get the number of objects processed | +|-----------|-------------------------------------| +| Parameter | None | +| Returned | ret | +| Value | 1: Execution successful 0: Not executed, count The number of objects obtained | + +**Request:** +```json +{ + "sid": 0, + "cmd": "get_entity_count" +} +``` + +**Return:** +```json +{ + "sid": 0, + "cmd": "get_entity_count", + "ret": 1, + "data": { + "count": 1 + } +} +``` + +2. Pan Template + +| Function | Translate all objects in the template | +|-----------|--------------------------------------| +| Parameter | dx: x offset, dy: y offset | +| Returned | ret | +| Value | 1: Execution successful 0: Not executed | + +**Request:** +```json +{ + "sid": 0, + "cmd": "translate_entity", + "data": { + "dx": 0, + "dy": 0 + } +} +``` + +**Return:** +```json +{ + "sid": 0, + "cmd": "translate_entity", + "ret": 1 +} +``` + +3. Rotate Template + +| Function | Rotate all objects in template | +|-----------|-------------------------------| +| Parameter | cx: rotation center point x, cy: rotation center point y, fAngle: rotation angle (radians value) | +| Returned | ret | +| Value | 1: Execution successful 0: Not executed | + +**Request:** +```json +{ + "sid": 0, + "cmd": "rotate_entity", + "data": { + "cx": 0, + "cy": 0, + "fAngle": 0 + } +} +``` + +**Return:** +```json +{ + "sid": 0, + "cmd": "rotate_entity", + "ret": 1 +} +``` + +4. Translate Object Based on Index + +| Function | Translate object based on index | +|-----------|--------------------------------| +| Parameter | index: object index, dx: x offset, dy: y offset | +| Returned | ret | +| Value | 1: Execution successful 0: Not executed | + +**Request:** +```json +{ + "sid": 0, + "cmd": "translate_entity_by_index", + "data": { + "index": 0, + "dx": 0, + "dy": 0 + } +} +``` + +**Return:** +```json +{ + "sid": 0, + "cmd": "translate_entity_by_index", + "ret": 1 +} +``` + +5. Rotate Object Based on Index + +| Function | Rotate object based on index | +|-----------|------------------------------| +| Parameter | index: object index, cx: spin center storex, cy: rotating center store y, fAngle: rotation angle (radians value) | +| Returned | ret | +| Value | 1: Execution successful 0: Not executed | + +**Request:** +```json +{ + "sid": 0, + "cmd": "rotate_entity_by_index", + "data": { + "index": 0, + "cx": 0, + "cy": 0, + "fAngle": 1.75 + } +} +``` + +**Return:** +```json +{ + "sid": 0, + "cmd": "rotate_entity_by_index", + "ret": 1 +} +``` + +6. Model Transformation (Translation, Rotation, Scaling) + +| Function | Model transformation (translation, rotation, scaling) | +|-----------|-------------------------------------------------------| +| Parameter | dx: X value of translation target point, dy: Y value of translation target point, dz: Z value of translation target point, axis: platform rotation axis ("X", "Y", "Z"), fAngle: rotation angle (°), fScale: unified scaling | +| Returned | ret | +| Value | 1: Execution successful 0: Not executed | + +**Request:** +```json +{ + "sid": 0, + "cmd": "TransByModel", + "data": { + "dx": 0, + "dy": 0, + "dz": 0, + "axis": "X", + "fAngle": 90, + "fScale": 1 + } +} +``` + +**Return:** +```json +{ + "sid": 0, + "cmd": "TransByModel", + "ret": 1 +} +``` + +7. Get Name Based on Index + +| Function | Get name based on index | +|-----------|-------------------------| +| Parameter | index: object index | +| Returned | ret | +| Value | 1: Execution successful 0: Not executed, name | + +**Request:** +```json +{ + "sid": 0, + "cmd": "get_name_by_index", + "data": { + "index": 0 + } +} +``` + +**Return:** +```json +{ + "sid": 0, + "cmd": "get_name_by_index", + "ret": 1, + "data": { + "name": "name" + } +} +``` + +8. Set Name Based on Index + +| Function | Set name based on index | +|-----------|-------------------------| +| Parameter | index: object index, name: name | +| Returned | ret | +| Value | 1: Execution successful 0: Not executed | + +**Request:** +```json +{ + "sid": 0, + "cmd": "set_name_by_index", + "data": { + "index": 0, + "name": "name" + } +} +``` + +**Return:** +```json +{ + "sid": 0, + "cmd": "set_name_by_index", + "ret": 1 +} +``` + +9. Get Content Based on Index + +| Function | Get content based on index | +|-----------|----------------------------| +| Parameter | index: object index | +| Returned | ret | +| Value | 1: Execution successful 0: Not executed, content | + +**Request:** +```json +{ + "sid": 0, + "cmd": "get_content_by_index", + "data": { + "index": 0 + } +} +``` + +**Return:** +```json +{ + "sid": 0, + "cmd": "get_content_by_index", + "ret": 1, + "data": { + "content": "0" + } +} +``` + +10. Set Content Based on Index + +| Function | Set content based on index | +|-----------|----------------------------| +| Parameter | index: object index, content: content | +| Returned | ret | +| Value | 1: Execution successful 0: Not executed | + +**Request:** +```json +{ + "sid": 0, + "cmd": "set_content_by_index", + "data": { + "index": 0, + "content": "content" + } +} +``` + +**Return:** +```json +{ + "sid": 0, + "cmd": "set_content_by_index", + "ret": 1 +} +``` + +11. Get Object Size and Position Based on Index + +| Function | Get object size and position based on index | +|-----------|--------------------------------------------| +| Parameter | index: object index | +| Returned | ret | +| Value | 1: Execution successful 0: Not executed, xPos, yPos, zPos, xSize, ySize, zSize | + +**Request:** +```json +{ + "sid": 0, + "cmd": "get_pos_size_by_index", + "data": { + "index": 0 + } +} +``` + +**Return:** +```json +{ + "sid": 0, + "cmd": "get_pos_size_by_index", + "ret": 1, + "data": { + "xPos": 3.816241979598999, + "yPos": 12.085675239562988, + "zPos": 0, + "xSize": 43.233749389648438, + "ySize": 6.869999885559082, + "zSize": 0 + } +} +``` + +12. Set Object Size and Position Based on Index + +| Function | Set object size and position based on index | +|-----------|--------------------------------------------| +| Parameter | index: object index, xPos, yPos, zPos, xSize, ySize, zSize | +| Returned | ret | +| Value | 1: Execution successful 0: Not executed | + +**Request:** +```json +{ + "sid": 0, + "cmd": "set_pos_size_by_index", + "data": { + "index": 0, + "xPos": 3, + "yPos": 12, + "zPos": 0, + "xSize": 43, + "ySize": 6, + "zSize": 0 + } +} +``` + +**Return:** +```json +{ + "sid": 0, + "cmd": "set_pos_size_by_index", + "ret": 1 +} +``` + +13. Get Content Based on Name + +| Function | Get content based on name | +|-----------|---------------------------| +| Parameter | name: name | +| Returned | ret | +| Value | 1: Execution successful 0: Not executed, content | + +**Request:** +```json +{ + "sid": 0, + "cmd": "get_content_by_name", + "data": { + "name": "name" + } +} +``` + +**Return:** +```json +{ + "sid": 0, + "cmd": "get_content_by_name", + "ret": 1, + "data": { + "content": "xxxyyy" + } +} +``` + +14. Set Content Based on Name + +| Function | Set content based on name | +|-----------|---------------------------| +| Parameter | name: name, content: content | +| Returned | ret | +| Value | 1: Execution successful 0: Not executed | + +**Request:** +```json +{ + "sid": 0, + "cmd": "set_content_by_name", + "data": { + "name": "xx", + "content": "xxyyzz" + } +} +``` + +**Return:** +```json +{ + "sid": 0, + "cmd": "set_content_by_name", + "ret": 1 +} +``` + +15. Delete Objects Based on Index + +| Function | Delete objects based on index | +|-----------|-------------------------------| +| Parameter | index: object index | +| Returned | ret | +| Value | 1: Execution successful 0: Not executed | + +**Request:** +```json +{ + "sid": 0, + "cmd": "delete_by_index", + "data": { + "index": 0 + } +} +``` + +**Return:** +```json +{ + "sid": 0, + "cmd": "delete_by_index", + "ret": 1 +} +``` + +16. Copy Objects Based on Index + +| Function | Copy objects based on index | +|-----------|-----------------------------| +| Parameter | index: object index | +| Returned | ret | +| Value | 1: Execution successful 0: Not executed | + +**Request:** +```json +{ + "sid": 0, + "cmd": "copy_by_index", + "data": { + "index": 0 + } +} +``` + +**Return:** +```json +{ + "sid": 0, + "cmd": "copy_by_index", + "ret": 1 +} +``` + +17. Mark Objects by Index + +| Function | Mark objects by index | +|-----------|-----------------------| +| Parameter | index: object index | +| Returned | ret | +| Value | 1: Execution successful 0: Not executed | + +**Request:** +```json +{ + "sid": 0, + "cmd": "mark_by_index", + "data": { + "index": 0 + } +} +``` + +**Return:** +```json +{ + "sid": 0, + "cmd": "mark_by_index", + "ret": 1 +} +``` + +### 2.5 Input and Output Operation Functions + +1. Read Input + +| Function | Read input Function | +|-----------|---------------------| +| Parameter | None | +| Returned | ret | +| Value | 1: Execution successful 0: Not executed, input | + +**Request:** +```json +{ + "sid": 0, + "cmd": "read_input", + "data": { + "data": 0xff + } +} +``` + +**Return:** +```json +{ + "sid": 0, + "cmd": "read_input", + "ret": 1, + "data": { + "input": 0xFE + } +} +``` + +2. Set Output + +| Function | Set output Function | +|-----------|---------------------| +| Parameter | output: The io status that needs to be output | +| Returned | ret | +| Value | 1: Execution successful 0: Not executed, output | + +**Request:** +```json +{ + "sid": 0, + "cmd": "write_output", + "data": { + "output": 1 + } +} +``` + +**Return:** +```json +{ + "sid": 0, + "cmd": "write_output", + "ret": 1, + "data": { + "output": 1 + } +} +``` + +### 2.6 Alarm Operation + +1. Get Run Error + +| Function | clear errors | +|-----------|--------------| +| Parameter | None | +| Returned | ret | +| Value | 1: Execution successful 0: Not executed | + +**Request:** +```json +{ + "sid": 0, + "cmd": "clear_error" +} +``` + +**Return:** +```json +{ + "sid": 0, + "cmd": "clear_error", + "ret": 1 +} +``` + +2. Get Error + +| Function | Get error | +|-----------|-----------| +| Parameter | None | +| Returned | ret | +| Value | 1: Execution successful 0: Not executed, describe | + +**Request:** +```json +{ + "sid": 0, + "cmd": "get_error" +} +``` + +**Return:** +```json +{ + "sid": 0, + "cmd": "get_error", + "ret": 1, + "data": { + "describe": "Failed to open board" + } +} +``` + +Returned Value the Corresponding Error Content is Described as Follows: + +| Returned Value | Illustrate | +|----------------|------------| +| -1 | Dongle not found | +| 0 | success | +| 1 | Failed to open board | +| 2 | The USB interface is not a 2.0 interface | +| 3 | Failed to open cache area | +| 4 | The time in the board is greater than the current computer time | +| 5 | Authorization expires | +| 6 | Failed to load authorization (the ID.txt authorization file in the Lincense folder in the current directory needs to be updated) | +| 7 | Failed to load FPGA driver | +| 8 | Failed to set system Parameter | +| 9 | Setting calibration failed | +| 10 | Setting up stepper motor failed | +| 11 | Failed to set up laser | +| 12 | Failed to download marking Parameter | +| 13 | Marking object does not exist | +| 14 | Marking Parameter is invalid | +| 15 | Laser status error | +| 16 | scanhead status error | +| 17 | Failed to obtain scanhead or laser status | +| 18 | Initialization failed before starting marking | +| 19 | Failed to start the number sending thread | +| 20 | Object content update failed before decomposing data (automatic variable update failed) | +| 21 | The object exceeds the marking range | +| 22 | Failed to update the content of the data decomposition end object. | +| 23 | File read error | +| 24 | File save error | +| 25 | The object does not exist and the move and rotate command cannot be executed. | +| 26 | The object is not text or barcode, and the content replacement operation cannot be performed. | +| 27 | Object with specified name not found | +| 28 | Marking cannot be started while marking is in progress. | +| 29 | Invalid scope of work | +| 30 | No control card connected | +| 31 | Object content update failed | +| 32 | file does not exist | +| 33 | Index Parameter is out of range | +| 34 | Object does not exist | +| 35 | The input Parameter pointer is null | +| 36 | Failed to modify object name | +| 37 | The layer number where the object is located does not exist | +| 38 | Preview cannot be started while marking is in progress | +| 39 | While marking is in progress, the preview cannot be started repeatedly. | +| 40 | Preview cannot be stopped while marking. | +| 41 | No preview object exists | +| 42 | Preparing for preview failed | +| 43 | Hardware stop signal, external emergency stop | +| 44 | Failed to enable the visual positioning module (the dongle does not contain its Function) | + +### 2.7 Visual Function + +1. Start the Visual Positioning Function + +| Function | Start the visual positioning function | +|-----------|--------------------------------------| +| Parameter | Is bEnVision enabled? | +| Returned | ret | +| Value | 1: Execution successful 0: Not executed | +| Notice | Visual Function related interfaces can only be used after authorization. | + +**Request:** +```json +{ + "sid": 0, + "cmd": "enable_vision", + "data": { + "bEnVision": true + } +} +``` + +**Return:** +```json +{ + "sid": 0, + "cmd": "enable_vision", + "ret": 1 +} +``` + +2. Visual Positioning Translation + +| Function | Set offset function | +|-----------|---------------------| +| Parameter | dX: abscissa offset, dY: vertical coordinate offset | +| Returned | ret | +| Value | 1: Execution successful 0: Not executed | + +**Request:** +```json +{ + "sid": 0, + "cmd": "vision_translate", + "data": { + "dX": 0.0, + "dY": 0.0 + } +} +``` + +**Return:** +```json +{ + "sid": 0, + "cmd": "vision_translate", + "ret": 1 +} +``` + +3. Visual Orientation Rotation + +| Function | Set rotation, rotate the radian fAngle around the position specified by cx and cy | +|-----------|----------------------------------------------------------------------------------| +| Parameter | cX: Center abscissa, cY: center ordinate, fAngle: Radian value of rotation, positive value: counterclockwise rotation, negative value: clockwise rotation | +| Returned | ret | +| Value | 1: Execution successful 0: Not executed | + +**Request:** +```json +{ + "sid": 0, + "cmd": "vision_rotate", + "data": { + "cX": 0.0, + "cY": 0.0, + "fAngle": 0.0 + } +} +``` + +**Return:** +```json +{ + "sid": 0, + "cmd": "vision_rotate", + "ret": 1 +} +``` + +### 2.8 Stepper Motor Function + +1. Stepper Motor Zero Return Function + +| Function | Stepper motor zero Return function | +|-----------|-----------------------------------| +| Parameter | axis motor axis 0: x axis, 1: y axis, 2: z axis, 3: r axis | +| Returned | ret | +| Value | 1: Execution successful 0: Not executed | + +**Request:** +```json +{ + "sid": 0, + "cmd": "step_motor_home", + "data": { + "axis": 0 + } +} +``` + +**Return:** +```json +{ + "sid": 0, + "cmd": "step_motor_home", + "ret": 1 +} +``` + +2. Stepper Motor Relative Position Movement + +| Function | Stepper motor relative position movement function | +|-----------|---------------------------------------------------| +| Parameter | axis motor axis 0: x axis, 1: y axis, 2: z axis, 3: r axis, offset: offset position | +| Returned | ret | +| Value | 1: Execution successful 0: Not executed | + +**Request:** +```json +{ + "sid": 0, + "cmd": "step_motor_move", + "data": { + "axis": 0, + "offset": 0 + } +} +``` + +**Return:** +```json +{ + "sid": 0, + "cmd": "step_motor_move", + "ret": 1 +} +``` + +3. Stepper Motor Absolute Position Movement + +| Function | Stepper motor absolute position movement Function | +|-----------|---------------------------------------------------| +| Parameter | axis motor axis 0: x axis, 1: y axis, 2: z axis, 3: r axis, pos: absolute position | +| Returned | ret | +| Value | 1: Execution successful 0: Not executed | + +**Request:** +```json +{ + "sid": 0, + "cmd": "step_motor_move_to", + "data": { + "axis": 0, + "pos": 0 + } +} +``` + +**Return:** +```json +{ + "sid": 0, + "cmd": "step_motor_move_to", + "ret": 1 +} +``` + +4. Stepper Motor Gets Current Position + +| Function | Stepper motor gets current position | +|-----------|------------------------------------| +| Parameter | axis motor axis 0: x axis, 1: y axis, 2: z axis, 3: r axis | +| Returned | ret | +| Value | 1: Execution successful 0: Not executed, axis: query axis number, pos: query axis position | + +**Request:** +```json +{ + "sid": 0, + "cmd": "step_motor_get_position" +} +``` + +**Return:** +```json +{ + "sid": 0, + "cmd": "step_motor_get_position", + "ret": 1, + "data": { + "axis": 0, + "pos": 0 + } +} +``` + +5. Stepper Motor Gets Current Status + +| Function | Stepper motor gets current status | +|-----------|----------------------------------| +| Parameter | None | +| Returned | ret | +| Value | 1: Execution successful 0: Not executed, xState: x-axis status 1 moving 0 stopped, yState: y-axis status 1 moving 0 stopped, zState: z-axis status 1 moving 0 stopped, rState: r axis status 1 moving 0 stopped | + +**Request:** +```json +{ + "sid": 0, + "cmd": "step_motor_get_state" +} +``` + +**Return:** +```json +{ + "sid": 0, + "cmd": "step_motor_get_state", + "ret": 1, + "data": { + "xState": 0, + "yState": 0, + "zState": 0, + "rState": 0 + } +} +``` + +6. Stepper Motor Obtains Zero Position Status + +| Function | Stepper motor obtains zero position status | +|-----------|-------------------------------------------| +| Parameter | axis motor axis 0: x axis, 1: y axis, 2: z axis, 3: r axis | +| Returned | ret | +| Value | 1: Execution successful 0: Not executed, xHome: Whether the x-axis is at the zero position, 1 is at the zero position, 0 is not at the zero position, yHome: Whether the y-axis is at the zero position, 1 is at the zero position, 0 is not at the zero position, zHome: Whether the z-axis is at the zero position, 1 is at the zero position, 0 is not at the zero position, rHome: Whether the r-axis is at the zero position, 1 is at the zero position, 0 is not at the zero position | + +**Request:** +```json +{ + "sid": 0, + "cmd": "step_motor_get_home_state" +} +``` + +**Return:** +```json +{ + "sid": 0, + "cmd": "step_motor_get_home_state", + "ret": 1, + "data": { + "xHome": 0, + "yHome": 0, + "zHome": 0, + "rHome": 0 + } +} +``` + +7. Stepper Motor Count Clear + +| Function | Stepper motor gets current position | +|-----------|------------------------------------| +| Parameter | axis motor axis 0: x axis, 1: y axis, 2: z axis, 3: r axis | +| Returned | ret | +| Value | 1: Execution successful 0: Not executed | + +**Request:** +```json +{ + "sid": 0, + "cmd": "step_motor_zero_counter", + "data": { + "axis": 0 + } +} +``` + +**Return:** +```json +{ + "sid": 0, + "cmd": "step_motor_zero_counter", + "ret": 1 +} +``` + +8. Stepper Motor Obtains Limit Switch Status + +| Function | Stepper motor obtains limit switch status | +|-----------|------------------------------------------| +| Parameter | axis motor axis 0: x axis, 1: y axis, 2: z axis, 3: r axis | +| Returned | ret | +| Value | 1: Execution successful 0: Not executed, xLimit: Whether the x-axis limit is triggered 1 triggered 0 not triggered, yLimit: Whether the y-axis limit is triggered. 1 is triggered. 0 is not triggered, zLimit: Whether the z-axis limit is triggered. 1 is triggered. 0 is not triggered, rLimit: Whether the r axis triggers the limit 1 triggers 0 not triggers | + +**Request:** +```json +{ + "sid": 0, + "cmd": "step_motor_limit_state" +} +``` + +**Return:** +```json +{ + "sid": 0, + "cmd": "step_motor_limit_state", + "ret": 1, + "data": { + "xLimit": 0, + "yLimit": 0, + "zLimit": 0, + "rLimit": 0 + } +} +``` + +9. Stepper Motor Stops + +| Function | Stepper motor stops | +|-----------|---------------------| +| Parameter | axis motor axis 0: x axis, 1: y axis, 2: z axis, 3: r axis | +| Returned | ret | +| Value | 1: Execution successful 0: Not executed | + +**Request:** +```json +{ + "sid": 0, + "cmd": "step_motor_stop", + "data": { + "axis": 0 + } +} +``` + +**Return:** +```json +{ + "sid": 0, + "cmd": "step_motor_stop", + "ret": 1 +} +``` + +10. Stepper Motor Gets Running Parameter + +| Function | Stepper motor gets running Parameter | +|-----------|--------------------------------------| +| Parameter | axis motor axis 0: x axis, 1: y axis, 2: z axis, 3: r axis, offset: offset position | +| Returned | ret | +| Value | 1: Execution successful 0: Not executed, axis: axis number, fSSpeed: starting speed, fSpeed: speed, fSpeedUpTime: acceleration time | + +**Request:** +```json +{ + "sid": 0, + "cmd": "step_motor_get_move_para", + "data": { + "axis": 0 + } +} +``` + +**Return:** +```json +{ + "sid": 0, + "cmd": "step_motor_get_move_para", + "ret": 1, + "data": { + "axis": 0, + "fSSpeed": 0, + "fSpeed": 0, + "fSpeedUpTime": 0 + } +} +``` + +11. Stepper Motor Settings Run Parameter + +| Function | Stepper motor settings run Parameter | +|-----------|--------------------------------------| +| Parameter | axis motor axis 0: x axis, 1: y axis, 2: z axis, 3: r axis, fSSpeed: starting speed, fSpeed: speed, fSpeedUpTime: acceleration time | +| Returned | ret | +| Value | 1: Execution successful 0: Not executed | + +**Request:** +```json +{ + "sid": 0, + "cmd": "step_motor_set_move_para", + "data": { + "axis": 0, + "fSSpeed": 0, + "fSpeed": 0, + "fSpeedUpTime": 0 + } +} +``` + +**Return:** +```json +{ + "sid": 0, + "cmd": "step_motor_set_move_para", + "ret": 1 +} +``` + +12. Stepper Motor Gets Back to Zero Parameter + +| Function | Stepper motor gets back to zero Parameter | +|-----------|------------------------------------------| +| Parameter | axis motor axis 0: x axis, 1: y axis, 2: z axis, 3: r axis | +| Returned | ret | +| Value | 1: Execution successful 0: Not executed, axis: axis number, dHomeSpeed: home speed | + +**Request:** +```json +{ + "sid": 0, + "cmd": "step_motor_get_home_para", + "data": { + "axis": 0 + } +} +``` + +**Return:** +```json +{ + "sid": 0, + "cmd": "step_motor_get_home_para", + "ret": 1, + "data": { + "axis": 0, + "dHomeSpeed": 100 + } +} +``` + +13. Stepper Motor Setting Back to Zero Parameter + +| Function | Stepper motor setting back to zero Parameter | +|-----------|---------------------------------------------| +| Parameter | axis motor axis 0: x axis, 1: y axis, 2: z axis, 3: r axis | +| Returned | ret | +| Value | 1: Execution successful 0: Not executed, axis: axis number, dHomeSpeed: home speed | + +**Request:** +```json +{ + "sid": 0, + "cmd": "step_motor_set_home_para", + "data": { + "axis": 0, + "dHomeSpeed": 100 + } +} +``` + +**Return:** +```json +{ + "sid": 0, + "cmd": "step_motor_set_home_para", + "ret": 1 +} +``` + +--- + +## 3. Interface Calling Process + +### 3.1 Schematic Diagram of the Overall Software Workflow +![alt text](3.1.png) +Connect to LenMark3Ds software via TCP + +1. Use the “open_file” command to open the marking file path. +2. Use the “set_markparameters_by_layer” command to set marking parameters. +3. Use the “download_parameters” command to download the marking parameters. +4. Use the “start_mark” command to execute the start marking command. +5. Use the “get_working_status” command to determine whether marking is completed. +6. Use the “stop_mark” command to execute the end marking command. + +### 3.2 CCD Calling Process Diagram + +Connect to LenMark3Ds software via TCP + +### 3.3 Motor Shaft Control Diagram + +1. Motor Returns to Zero + +Connect to LenMark_3DS software via TCP + +1. Use the “step_motor_get_home_para” command to get the stepper motor zero return parameters. +2. Do the current parameters meet the requirements? +3. Use the “step_motor_set_home_para” command to set the stepper motor zero return parameters. +4. Use the “step_motor_home” command to start the motor to return to zero. +5. Use the “step_motor_get_state” command to determine whether the motor stops. +6. Use the “step_motor_get_home_state” command to determine whether the motor is at zero position. +7. Does it need to stop the motor manually? +8. Use the “step_motor_stop” command to stop the stepper motor movement. +9. Return to zero successfully. +10. Return to zero failed. + +2. Motor Movement + +Connect to LenMark_3DS software via TCP + +1. Use the “step_motor_get_move_para” command to get the current parameters of the stepper motor. +2. Do the current parameters meet the requirements? +3. Use the “step_motor_move” command to move the stepper motor. +4. Use the “step_motor_set_move_para” command to set the stepper motor parameters. +5. Use the “step_motor_get_state” command to determine whether the motor stops. +6. Does it need to stop the motor manually? +7. Use the “step_motor_get_limit_state” command to read whether the stepper motor limit switch is triggered. +8. Use the “step_motor_stop” command to stop the stepper motor movement. +9. Valid movement. +10. Invalid move. \ No newline at end of file diff --git a/src/Feeltek/laserErrorLogging.py b/src/Feeltek/laserErrorLogging.py new file mode 100644 index 00000000..bdeee55e --- /dev/null +++ b/src/Feeltek/laserErrorLogging.py @@ -0,0 +1,56 @@ +""" +This code contains the error logger class to log errors wrt feeltek scancard. +The logging level is set to 'INFO' and the logs are stored in a separate log file automatically. +It makes use of python logging library to implement this. + +""" + +# import neccessary libraries +import os +import logging +from datetime import datetime + +class LaserErrorLogger: + """ + A class for logging laser errors. + Attributes: + logger (logging.Logger): The custom logger for logging laser errors. + formatter (logging.Formatter): The formatter for formatting log messages. + file_handler (logging.FileHandler): The file handler for saving logs to a file. + Methods: + __init__(): Initializes the LaserErrorLogger class. + """ + def __init__(self): + + # create custom logger and set a format for it + self.logger = logging.getLogger(__name__) + self.logger.setLevel(logging.DEBUG) + + self.formatter = logging.Formatter( + "{asctime} - {levelname} - {message}", + style="{", + datefmt="%d-%m-%Y %H:%M:%S", + ) + + # if the logger has any handlers already present, delete them + if self.logger.hasHandlers(): + self.logger.handlers.clear() + + # this is to avoid duplication when called in parent class + self.logger.propagate = False + + # Create 'laser_logs' directory if it doesn't exist in current working directory + if not os.path.exists('laser_logs'): + os.makedirs('laser_logs') + + # get the current timestamp and add it in log filename + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + fixed_file_name = f"laser_log_{timestamp}" + + # set the log file handler to save the logs in desired log file + self.file_handler = logging.FileHandler(f"./laser_logs/{fixed_file_name}.log", mode="a", encoding="utf-8") + self.file_handler.setFormatter(self.formatter) + + # add this file handler to our custom logger + self.logger.addHandler(self.file_handler) + diff --git a/src/Feeltek/scanCard.py b/src/Feeltek/scanCard.py new file mode 100644 index 00000000..20dff983 --- /dev/null +++ b/src/Feeltek/scanCard.py @@ -0,0 +1,358 @@ +import threading +import socket +import json +import time +from typing import Optional, Dict, Any +from concurrent.futures import ThreadPoolExecutor, Future +from PyQt5.QtCore import QMutex + +class Scancard: + """ + A class representing a Scancard. + Attributes: + parent: The parent object. + input_file_path: The path to the .emd file created. + input_file: The name of the .emd file created. + input_cli: The input CLI. + HOST: The host address. + PORT: The port number. + timeout: The timeout value. + file: The file. + req: The request. + function: The function name. + file_path: The file path. + formatted_response: The formatted response. + layer_id: The layer ID. + Methods: + __init__(self, parent=None): Initializes the Scancard object. + api(self): Sends an API request to localhost:50000 and prints out the response. + get_working_status(self): Gets the working status of the Scancard. + set_markparameters_by_index(self): Updates mark parameters by index. + set_markparameters_by_layer(self): Updates mark parameters by layer. + get_markparameters_by_index(self): Gets a list of mark parameter values by index. + get_markparameters_by_layer(self): Gets a list of mark parameter values by layer. + get_log(self): Gets the log. + open_file(self): Opens an .emd file on the Scancard. + close_file(self): Closes a file on the Scancard. + start_mark(self): Starts marking. + stop_mark(self): Stops marking. + save_file(self): Saves a file on the Scancard. + start_preview(self): Starts previewing. + stop_preview(self): Stops previewing. + get_markParameters_by_layer(self, layer_id): Gets the marking parameters based on the layer number. + set_markParameters_by_layer(self, layer_id, params): Sets the marking parameters based on the layer number. + get_markParameters_by_index(self, index, in_index): Gets the marking parameters based on the index. + set_markParameters_by_index(self, index, in_index, params): Sets the marking parameters based on the index. + download_parameters(self): Downloads the marking parameters. + get_entity_fill_property_by_index(self, index, in_index): Gets the populated parameters based on the index. + set_entity_fill_property_by_index(self, index, in_index, params): Sets the populated parameters based on the index. + get_entity_count(self): Gets the number of objects processed. + translate_entity(self, dx, dy): Translates all objects in the template. + rotate_entity(self, cx, cy, fAngle): Rotates all objects in the template. + translate_entity_by_index(self, index, dx, dy): Translates object based on index. + rotate_entity_by_index(self, index, cx, cy, fAngle): Rotates object based on index. + trans_by_model(self, dx, dy, dz, axis, fAngle, fScale): Model transformation (translation, rotation, scaling). + get_name_by_index(self, index): Gets name based on index. + set_name_by_index(self, index, name): Sets name based on index. + get_content_by_index(self, index): Gets content based on index. + set_content_by_index(self, index, content): Sets content based on index. + get_pos_size_by_index(self, index): Gets object size and position based on index. + set_pos_size_by_index(self, index, xPos, yPos, zPos, xSize, ySize, zSize): Sets object size and position based on index. + get_content_by_name(self, name): Gets content based on name. + set_content_by_name(self, name, content): Sets content based on name. + delete_by_index(self, index): Deletes objects based on index. + copy_by_index(self, index): Copies objects based on index. + mark_by_index(self, index): Marks objects by index. + read_input(self): Reads input. + set_output(self, output): Sets output. + """ + + ERROR_DESCRIPTIONS = { + -1: "Dongle not found", + 0: "Success", + 1: "Failed to open board", + 2: "The USB interface is not a 2.0 interface", + 3: "Failed to open cache area", + 4: "The time in the board is greater than the current computer time", + 5: "Authorization expires", + 6: "Failed to load authorization (the ID.txt authorization file in the License folder in the current directory needs to be updated)", + 7: "Failed to load FPGA driver", + 8: "Failed to set system Parameter", + 9: "Setting calibration failed", + 10: "Setting up stepper motor failed", + 11: "Failed to set up laser", + 12: "Failed to download marking Parameter", + 13: "Marking object does not exist", + 14: "Marking Parameter is invalid", + 15: "Laser status error", + 16: "Scanhead status error", + 17: "Failed to obtain scanhead or laser status", + 18: "Initialization failed before starting marking", + 19: "Failed to start the number sending thread", + 20: "Object content update failed before decomposing data (automatic variable update failed)", + 21: "The object exceeds the marking range", + 22: "Failed to update the content of the data decomposition end object", + 23: "File read error", + 24: "File save error", + 25: "The object does not exist and the move and rotate command cannot be executed", + 26: "The object is not text or barcode, and the content replacement operation cannot be performed", + 27: "Object with specified name not found", + 28: "Marking cannot be started while marking is in progress", + 29: "Invalid scope of work", + 30: "No control card connected", + 31: "Object content update failed", + 32: "File does not exist", + 33: "Index Parameter is out of range", + 34: "Object does not exist", + 35: "The input Parameter pointer is null", + 36: "Failed to modify object name", + 37: "The layer number where the object is located does not exist", + 38: "Preview cannot be started while marking is in progress", + 39: "While marking is in progress, the preview cannot be started repeatedly", + 40: "Preview cannot be stopped while marking", + 41: "No preview object exists", + 42: "Preparing for preview failed", + 43: "Hardware stop signal, external emergency stop", + 44: "Failed to enable the visual positioning module (the dongle does not contain its Function)" + } + + def __init__(self, parent=None): + try: + self.parent = parent + self.input_file_path = "" # path to .emd file created + self.input_file = "" # name of the .emd file created + self.input_cli = "" + self.HOST = "localhost" + self.PORT = 50000 + self.timeout = 5 + self.file = "" + self.ret_value = 1 + + self.req = {} + self.function = "" + self.file_path = "" + self.formatted_response = {} + self.layer_id = 0 + + self.executor = ThreadPoolExecutor(max_workers=1) + self.mutex = QMutex() + + except Exception as e: + print(f"E1: Variable initialization failed. {e}") + + def api(self): + try: + json_string = json.dumps(self.req) + with socket.create_connection((self.HOST, self.PORT), timeout=self.timeout) as sock: + self.log_info(f"{self.function}-> Connecting to {self.HOST}:{self.PORT}...") + sock.sendall(json_string.encode()) + self.log_info(f"{self.function}-> Sending {self.req} to {self.HOST}:{self.PORT} with timeout of {self.timeout}s") + ret = sock.recv(1024) + if ret: + self.handle_response(ret) + else: + self.log_error(f"E203 - {self.function} not successful - Request {self.req} TIMED OUT!!") + except (socket.timeout, socket.error, json.JSONDecodeError) as e: + self.log_error(f"E200 - {self.function} not successful \n {e}") + + def handle_response(self, response: bytes): + try: + ret_decoded = response.decode('GB18030', errors='replace') + json_end_index = ret_decoded.rfind('}') + 1 + json_content = ret_decoded[:json_end_index] + response_data = json.loads(json_content) + formatted_json = json.dumps(response_data, indent=4, ensure_ascii=False) + self.ret_value = response_data.get("ret") + self.log_info(f"{self.function}-> Response received from {self.HOST}:{self.PORT} - {formatted_json}") + except json.JSONDecodeError as e: + self.log_error(f"E202 - {self.function} not successful \n {e}") + + def log_info(self, message: str): + # print({"info": message}) + pass + + def log_error(self, message: str): + # print({"error": message}) + pass + + def create_request(self, cmd: str, data: Optional[Dict[str, Any]] = None): + self.req = {"sid": 0, "cmd": cmd} + if data: + self.req["data"] = data + + def execute_command(self, cmd: str, data: Optional[Dict[str, Any]] = None) -> Future: + def task(): + try: + json_string = json.dumps({"sid": 0, "cmd": cmd, "data": data}) + with socket.create_connection((self.HOST, self.PORT), timeout=self.timeout) as sock: + sock.sendall(json_string.encode()) + ret = sock.recv(1024) + if ret: + response_data = json.loads(ret.decode('GB18030', errors='replace')) + return {"ret_value": response_data.get("ret")} + else: + return {"ret_value": -1} # Simulated error response + except (socket.timeout, socket.error, json.JSONDecodeError) as e: + return {"ret_value": -1} # Simulated error response + + self.mutex.lock() + future = self.executor.submit(task) + future.add_done_callback(lambda f: self.mutex.unlock()) + return future + + def get_working_status(self): + status_map = { + 0: "Waiting", + 1: "Marking", + 2: "Previewing", + 3: "Already working" + } + + def task(): + self.create_request("get_working_status") + self.function = "Getting working status" + try: + json_string = json.dumps(self.req) + with socket.create_connection((self.HOST, self.PORT), timeout=self.timeout) as sock: + self.log_info(f"{self.function}-> Connecting to {self.HOST}:{self.PORT}...") + sock.sendall(json_string.encode()) + self.log_info(f"{self.function}-> Sending {self.req} to {self.HOST}:{self.PORT} with timeout of {self.timeout}s") + ret = sock.recv(1024) + if ret: + ret_decoded = ret.decode('GB18030', errors='replace') + json_end_index = ret_decoded.rfind('}') + 1 + json_content = ret_decoded[:json_end_index] + response_data = json.loads(json_content) + connection_status = response_data.get("ret") + status_text = status_map.get(connection_status, "Unknown") + self.log_info(f"{self.function}-> Response received from {self.HOST}:{self.PORT} - {status_text}") + return status_text + else: + self.log_error(f"E203 - {self.function} not successful - Request {self.req} TIMED OUT!!") + return "No response received." + except (socket.timeout, socket.error, json.JSONDecodeError) as e: + self.log_error(f"E200 - {self.function} not successful \n {e}") + return f"Connection to {self.HOST}:{self.PORT} failed: {e}" + + self.mutex.lock() + future = self.executor.submit(task) + future.add_done_callback(lambda f: self.mutex.unlock()) + return future + + def open_file(self, file_path: str): + return self.execute_command("open_file", {"path": file_path}) + + def close_file(self): + return self.execute_command("close_file") + + def save_file(self, file_path: str, cover: bool): + return self.execute_command("save_file", {"path": file_path, "cover": cover}) + + def start_mark(self): + future = self.execute_command("start_mark") + return future + + def stop_mark(self): + future = self.execute_command("stop_mark") + return future + + def start_preview(self): + return self.execute_command("start_preview") + + def stop_preview(self): + return self.execute_command("stop_preview") + + def get_markParameters_by_layer(self, layer_id: int): + return self.execute_command("get_markParameters_by_layer", {"layer_id": layer_id}) + + def set_markParameters_by_layer(self, layer_id: int, params: Dict[str, Any]): + return self.execute_command("set_markParameters_by_layer", {"layer_id": layer_id, **params}) + + def get_markParameters_by_index(self, index: int, in_index: int): + return self.execute_command("get_markParameters_by_index", {"index": index, "in_index": in_index}) + + def set_markParameters_by_index(self, index: int, in_index: int, params: Dict[str, Any]): + return self.execute_command("set_markParameters_by_index", {"index": index, "in_index": in_index, **params}) + + def download_parameters(self): + return self.execute_command("download_Parameters") + + def get_entity_fill_property_by_index(self, index: int, in_index: int): + return self.execute_command("get_entity_fill_property_by_index", {"index": index, "in_index": in_index}) + + def set_entity_fill_property_by_index(self, index: int, in_index: int, params: Dict[str, Any]): + return self.execute_command("set_entity_fill_property_by_index", {"index": index, "in_index": in_index, **params}) + + def get_entity_count(self): + return self.execute_command("get_entity_count") + + def translate_entity(self, dx: float, dy: float): + return self.execute_command("translate_entity", {"dx": dx, "dy": dy}) + + def rotate_entity(self, cx: float, cy: float, fAngle: float): + return self.execute_command("rotate_entity", {"cx": cx, "cy": cy, "fAngle": fAngle}) + + def translate_entity_by_index(self, index: int, dx: float, dy: float): + return self.execute_command("translate_entity_by_index", {"index": index, "dx": dx, "dy": dy}) + + def rotate_entity_by_index(self, index: int, cx: float, cy: float, fAngle: float): + return self.execute_command("rotate_entity_by_index", {"index": index, "cx": cx, "cy": cy, "fAngle": fAngle}) + + def trans_by_model(self, dx: float, dy: float, dz: float, axis: str, fAngle: float, fScale: float): + return self.execute_command("TransByModel", {"dx": dx, "dy": dy, "dz": dz, "axis": axis, "fAngle": fAngle, "fScale": fScale}) + + def get_name_by_index(self, index: int): + return self.execute_command("get_name_by_index", {"index": index}) + + def set_name_by_index(self, index: int, name: str): + return self.execute_command("set_name_by_index", {"index": index, "name": name}) + + def get_content_by_index(self, index: int): + return self.execute_command("get_content_by_index", {"index": index}) + + def set_content_by_index(self, index: int, content: str): + return self.execute_command("set_content_by_index", {"index": index, "content": content}) + + def get_pos_size_by_index(self, index: int): + return self.execute_command("get_pos_size_by_index", {"index": index}) + + def set_pos_size_by_index(self, index: int, xPos: float, yPos: float, zPos: float, xSize: float, ySize: float, zSize: float): + return self.execute_command("set_pos_size_by_index", {"index": index, "xPos": xPos, "yPos": yPos, "zPos": zPos, "xSize": xSize, "ySize": ySize, "zSize": zSize}) + + def get_content_by_name(self, name: str): + return self.execute_command("get_content_by_name", {"name": name}) + + def set_content_by_name(self, name: str, content: str): + return self.execute_command("set_content_by_name", {"name": name, "content": content}) + + def delete_by_index(self, index: int): + return self.execute_command("delete_by_index", {"index": index}) + + def copy_by_index(self, index: int): + return self.execute_command("copy_by_index", {"index": index}) + + def mark_by_index(self, index: int): + return self.execute_command("mark_by_index", {"index": index}) + + def read_input(self): + return self.execute_command("read_input", {"data": 0xff}) + + def set_output(self, output: int): + return self.execute_command("write_output", {"output": output}) + + def clear_error(self): + return self.execute_command("clear_error") + + def get_error(self): + future = self.execute_command("get_error") + future.add_done_callback(lambda f: self.log_info(f"Error description: {self.ERROR_DESCRIPTIONS.get(self.ret_value, 'Unknown error')}")) + return future + + def enable_vision(self, bEnVision: bool): + return self.execute_command("enable_vision", {"bEnVision": bEnVision}) + + def vision_translate(self, dX: float, dY: float): + return self.execute_command("vision_translate", {"dX": dX, "dY": dY}) + + def vision_rotate(self, cX: float, cY: float, fAngle: float): + return self.execute_command("vision_rotate", {"cX": cX, "cY": cY, "fAngle": fAngle}) \ No newline at end of file diff --git a/src/Feeltek/testConnection.py b/src/Feeltek/testConnection.py new file mode 100644 index 00000000..0c274e54 --- /dev/null +++ b/src/Feeltek/testConnection.py @@ -0,0 +1,38 @@ +import socket +import json + +def check_connection(host, port): + request = { + "sid": 0, + "cmd": "get_working_status", + } + status_map = { + 0: "Waiting", + 1: "Marking", + 2: "Previewing", + 3: "Already working" + } + try: + json_string = json.dumps(request) + with socket.create_connection((host, port), timeout=2) as sock: + sock.sendall(json_string.encode()) + ret = sock.recv(1024) + if ret: + ret_str = ret.decode('GB2312') + ret_json = json.loads(ret_str) + connection_status = ret_json.get("ret") + status_text = status_map.get(connection_status, "Unknown") + print(f"Connection status: {status_text}") + return status_text + else: + print("No response received.") + return None + except (socket.timeout, socket.error, json.JSONDecodeError) as e: + print(f"Connection to {host}:{port} failed: {e}") + return None + +if __name__ == "__main__": + HOST = "localhost" + PORT = 50000 + status = check_connection(HOST, PORT) + print(f"Host and port status: {status}") \ No newline at end of file diff --git a/src/config.py b/src/config.py new file mode 100644 index 00000000..4594b90b --- /dev/null +++ b/src/config.py @@ -0,0 +1,2 @@ +class Config: + DEVELOPMENT_MODE = False # Set to False in production \ No newline at end of file diff --git a/src/main.py b/src/main.py index 7192da1e..d131586d 100644 --- a/src/main.py +++ b/src/main.py @@ -1,13 +1,30 @@ import sys +import gc +import logging from PyQt5.QtWidgets import QApplication from PyQt5.QtCore import QTimer from ui.main_window import MainWindow +# Set up logging +logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s') + def main(): + logging.debug("Starting application") app = QApplication(sys.argv) window = MainWindow() window.show() - sys.exit(app.exec_()) + logging.debug("Main window shown") + + # Set up a QTimer to trigger garbage collection periodically + gc_timer = QTimer() + gc_timer.timeout.connect(gc.collect) + gc_timer.start(60000) # Trigger garbage collection every 60 seconds + logging.debug("Garbage collection timer started") + + try: + sys.exit(app.exec_()) + except Exception as e: + logging.error("Exception occurred", exc_info=True) if __name__ == "__main__": main() \ No newline at end of file diff --git a/src/models/printer_status.py b/src/models/printer_status.py index c1fda70f..1b5b9474 100644 --- a/src/models/printer_status.py +++ b/src/models/printer_status.py @@ -1,14 +1,114 @@ -class PrinterStatus: +from dataclasses import dataclass, field +from typing import Any, Dict, Optional +from PyQt5.QtCore import QObject, pyqtSignal +import numpy as np +import time # Add this import + +class PrinterStatus(QObject): + temperatures_updated = pyqtSignal(np.ndarray, dict) + rgb_frame_updated = pyqtSignal(np.ndarray) + maxtemp_updated = pyqtSignal(float) # Add the maxtemp_updated signal + scancard_status_updated = pyqtSignal(str) # Add the scancard_status_updated signal + def __init__(self): - self.is_printing = False - self.progress = 0.0 - - def update_status(self, data): - if 'state' in data: - self.is_printing = data['state'] == 'Printing' - if 'progress' in data: - self.progress = data['progress'].get('completion', 0.0) - - def reset_status(self): - self.is_printing = False - self.progress = 0.0 \ No newline at end of file + super().__init__() + self.frame: Optional[Any] = None + self.chamberTemperatures: Dict[str, float] = {} + self.chamberTemperatureSetpoint = 0 + self.chamberHeatingStarted = False + self.rgb_frame: Optional[Any] = None + self.layerHeight = 0.0 + self.initialLevellingHeight = 0.0 + self.heatedBufferHeight = 0.0 + self.powderLoadingExtraHeightGap = 0.0 + self.bedTemperature = 0.0 + self.volumeTemperature = 0.0 + self.chamberTemperature = 0.0 + self.p = 0.0 + self.i = 0.0 + self.d = 0.0 + self.powderLoadingSequence = "" + self.moveToStartingSequence = "" + self.prepareForPartRemovalSequence = "" + self.initialLevellingRecoatingSequence = "" + self.heatedBufferRecoatingSequence = "" + self.printingRecoatingSequence = "" + self.partHeight = 0.0 + self.dosingHeight = 0.0 # Add dosingHeight + self.maxTemp = 0.0 + self.last_update_time = time.time() # Add this attribute + self.scancard_status = "Unknown" + self.printing = False + + def updateTemperatures(self, frame: Any, chamberTemperatures: Dict[str, float]): + """Update the model with a new frame and temperature values.""" + self.frame = frame + self.chamberTemperatures = chamberTemperatures.copy() + self.temperatures_updated.emit(frame, chamberTemperatures) + self.last_update_time = time.time() # Update the last update time + + def updateRGBFrame(self, frame: Any): + """Update the model with a new RGB frame.""" + self.rgb_frame = frame + self.rgb_frame_updated.emit(frame) + + def setLayerHeight(self, value: float): + self.layerHeight = value + + def setInitialLevellingHeight(self, value: float): + self.initialLevellingHeight = value + + def setHeatedBufferHeight(self, value: float): + self.heatedBufferHeight = value + + def setPowderLoadingExtraHeightGap(self, value: float): + self.powderLoadingExtraHeightGap = value + + def setBedTemperature(self, value: float): + self.bedTemperature = value + + def setVolumeTemperature(self, value: float): + self.volumeTemperature = value + + def setChamberTemperature(self, value: float): + self.chamberTemperature = value + + def setP(self, value: float): + self.p = value + + def setI(self, value: float): + self.i = value + + def setD(self, value: float): + self.d = value + + def setPowderLoadingSequence(self, value: str): + self.powderLoadingSequence = value + + def setMoveToStartingSequence(self, value: str): + self.moveToStartingSequence = value + + def setPrepareForPartRemovalSequence(self, value: str): + self.prepareForPartRemovalSequence = value + + def setInitialLevellingRecoatingSequence(self, value: str): + self.initialLevellingRecoatingSequence = value + + def setHeatedBufferRecoatingSequence(self, value: str): + self.heatedBufferRecoatingSequence = value + + def setPrintingRecoatingSequence(self, value: str): + self.printingRecoatingSequence = value + + def setPartHeight(self, value: float): + self.partHeight = value + + def setDosingHeight(self, value: float): # Add setDosingHeight method + self.dosingHeight = value + + def updateMaxTemp(self, value: float): + self.maxTemp = value + self.maxtemp_updated.emit(value) # Emit the maxtemp_updated signal + + def updateScancardStatus(self, status: str): + self.scancard_status = status \ No newline at end of file diff --git a/src/octoprint_client/__init__.py b/src/moonrakerClient/__init__.py similarity index 100% rename from src/octoprint_client/__init__.py rename to src/moonrakerClient/__init__.py diff --git a/src/moonrakerClient/moonrakerClient.py b/src/moonrakerClient/moonrakerClient.py new file mode 100644 index 00000000..365a5133 --- /dev/null +++ b/src/moonrakerClient/moonrakerClient.py @@ -0,0 +1,107 @@ +import requests +import logging +from threading import Lock + +class MoonrakerAPI: + def __init__(self, base_url): + self.base_url = base_url + self.api_mutex = Lock() + + # Configure the logger + self.logger = logging.getLogger(__name__) + self.logger.setLevel(logging.INFO) + + # Create a file handler + file_handler = logging.FileHandler('moonraker.log') + file_handler.setLevel(logging.INFO) + + # Create a formatter and set it for the handler + formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + file_handler.setFormatter(formatter) + + # Remove all existing handlers + self.logger.handlers.clear() + + # Add the file handler to the logger + self.logger.addHandler(file_handler) + + def reconnect(self): + """ + Attempt to reconnect to the Moonraker server. + """ + try: + # No specific action needed for reconnecting in this context + self.logger.info("Reconnected to Moonraker server.") + except Exception as e: + self.logger.error(f"Failed to reconnect: {e}") + + def send_gcode(self, cmd): + self.logger.info(f"Sending G-code command: {cmd}") + try: + self.api_mutex.acquire() + response = requests.post( + url=f"{self.base_url}/printer/gcode/script", + json={"script": cmd}, + timeout=10 + ) + response.raise_for_status() + response_data = response.json() + self.logger.info(f"Response from Moonraker: {response_data}") + return response_data + except requests.exceptions.Timeout: + self.logger.error("Request to Moonraker timed out.") + self.reconnect() + return "Timeout" + except requests.exceptions.RequestException as e: + self.logger.error(f"Error sending G-code: {e}") + self.reconnect() + return str(e) + finally: + self.api_mutex.release() + + def query_status(self): + self.logger.info("Querying printer status.") + try: + self.api_mutex.acquire() + response = requests.get( + url=f"{self.base_url}/printer/objects/query?status", + timeout=10 + ) + response.raise_for_status() + response_data = response.json() + self.logger.info(f"Response from Moonraker: {response_data}") + return response_data + except requests.exceptions.Timeout: + self.logger.error("Request to Moonraker timed out.") + self.reconnect() + return "Timeout" + except requests.exceptions.RequestException as e: + self.logger.error(f"Error querying status: {e}") + self.reconnect() + return str(e) + finally: + self.api_mutex.release() + + def query_temperatures(self): + self.logger.info("Querying printer temperatures.") + try: + self.api_mutex.acquire() + response = requests.get( + url=f"{self.base_url}/printer/objects/query?temperature", + timeout=10 + ) + response.raise_for_status() + response_data = response.json() + self.logger.info(f"Response from Moonraker: {response_data}") + return response_data + except requests.exceptions.Timeout: + self.logger.error("Request to Moonraker timed out.") + self.reconnect() + return "Timeout" + except requests.exceptions.RequestException as e: + self.logger.error(f"Error querying temperatures: {e}") + self.reconnect() + return str(e) + finally: + self.api_mutex.release() + diff --git a/src/octoprint_client/octoprintAPI.py b/src/octoprint_client/octoprintAPI.py deleted file mode 100644 index 034798b9..00000000 --- a/src/octoprint_client/octoprintAPI.py +++ /dev/null @@ -1,679 +0,0 @@ -from contextlib import contextmanager -import os -import requests -import json -import base64 - - -#TODO: response codes for important functions -#TODO:Check header content types in the GET/POST requests - - -class octoprintAPI: - def __init__(self, ip=None, apiKey=None): - """ - Initialize the object with URL and API key - - If a session is provided, it will be used (mostly for testing) - """ - if not ip: - raise TypeError('Required argument \'ip\' not found or emtpy') - if not apiKey: - raise TypeError('Required argument \'apiKey\' not found or emtpy') - self.ip = ip - self.apiKey = apiKey - # Try a simple request to see if the API key works - # Keep the info, in case we need it later - self.version = self.version() - - # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - # ++++++++++++++++++++++++ File Handling ++++++++++++++++++++++++++++++++++++++ - # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - - @classmethod - def _prepend_local(cls, location): - if location.split('/')[0] not in ('local', 'sdcard'): - return 'local/' + location - return location - - def retrieveFileInformation(self, location=None, force="false", recursive="false"): - """ - Retrieve information regarding all files currently available and - regarding the disk space still available locally in the system - - If location is used, retrieve information regarding the files currently - available on the selected location and - if targeting the local - location - regarding the disk space still available locally in the - system - - If location is a file, retrieves the selected file''s information - """ - headers = {'X-Api-Key': self.apiKey} - if location: - location = self._prepend_local(location) - url = 'http://' + self.ip + '/api/files/{}'.format(location) - else: - url = 'http://' + self.ip + '/api/files' - payload = {"recursive": recursive, "force": force} - response = requests.get(url, headers=headers, params=payload) - temp = response.json() - return temp - - @contextmanager - def _file_tuple(self, file): - """ - Yields a tuple with filename and file object - - Expects the same thing or a path as input - """ - mime = 'application/octet-stream' - - try: - exists = os.path.exists(file) - except: - exists = False - - if exists: - filename = os.path.basename(file) - with open(file, 'rb') as f: - yield f - else: - yield open(file, 'rb') - - def uploadGcode(self, file, location='local', select=False, prnt=False): - """ - Upload a given file - It can be a path or a tuple with a filename and a file-like object - :param file: path to file, eg /media/usb0/file1.gcode - :param location: location to upload to on the server - :param select: bool, selecting the file after uploading - :param prnt: bool, start print after uploading - :return: json response, with success of the upload and location - """ - with self._file_tuple(file) as file_tuple: - files = {'file': file_tuple} - payload = {'select': str(select).lower(), 'print': str(prnt).lower()} - url = 'http://' + self.ip + '/api/files/{}'.format(location) - headers = {'X-Api-Key': self.apiKey} - response = requests.post(url, files=files, data=payload, headers=headers) - temp = response.json() - return temp - - - - # Should add error/status cheking in the response - - @contextmanager - def _file_tuple_png(self, file): - """ - Yields a tuple with filename and file object - - Expects the same thing or a path as input - """ - # mime = 'application/octet-stream' - mime = 'image/png' - try: - exists = os.path.exists(file) - except: - exists = False - - if exists: - filename = os.path.basename(file) - with open(file, 'rb') as f: - yield filename, f, mime - else: - yield file + (mime,) - - def uploadImage(self, file, location='local'): - """ - Upload a given file - It can be a path or a tuple with a filename and a file-like object - :param file: path to file, eg /media/usb0/file1.gcode - :param location: location to upload to on the server - :return: json response, with success of the upload and location - """ - - with self._file_tuple_png(file) as file_tuple: - files = {'file': file_tuple} - url = 'http://' + self.ip + '/api/files/{}'.format(location) - headers = {'X-Api-Key': self.apiKey} - response = requests.post(url, files=files, headers=headers) - temp = response.json() - return temp - - def deleteFile(self, location): - """ - Delete the selected filename on the selected target - - Location is target/filename, defaults to local/filename - """ - location = self._prepend_local(location) - url = 'http://' + self.ip + '/api/files/{}'.format(location) - headers = {'content-type': 'application/json', 'X-Api-Key': self.apiKey} - requests.delete(url, headers=headers) - - def selectFile(self, location, prnt=False): - """ - Selects a file for printing - - Location is target/filename, defaults to local/filename - If print is True, the selected file starts to print immediately - """ - location = self._prepend_local(location) - payload = {'command': 'select', 'print': prnt} - url = 'http://' + self.ip + '/api/files/{}'.format(location) - headers = {'content-type': 'application/json', 'X-Api-Key': self.apiKey} - requests.post(url, data=json.dumps(payload), headers=headers) - - def getImage(self, name): - url = 'http://' + self.ip + '/downloads/files/local/' + name - headers = {'X-Api-Key': self.apiKey} - response = requests.get(url, headers=headers, stream=True) - if response.status_code == 200: - content = response.content - start = content.find(b'; thumbnail begin') - end = content.find(b'; thumbnail end') - if start != -1 and end != -1: - thumbnail = content[start:end] - thumbnail = base64.b64decode(thumbnail[thumbnail.find(b'\n')+1:].replace(b'; ', b'').replace(b'\r\n', b'')) - return thumbnail - else: - return False - else: - return False - - # Upload directly to directory - # Download Timelapse - # Print from USB - # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - # +++++++++++++++++++++++++ Job Handeling+++++++++++++++++++++++++++++++++++++++ - # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - - - ''' - Job Handeling functions, print pause, get cureent file info etc - ''' - - def getJobInformation(self): - """ - Retrieve information about the current job (if there is one) - """ - url = 'http://' + self.ip + '/api/job' - headers = {'X-Api-Key': self.apiKey} - response = requests.get(url, headers=headers) - temp = response.json() - return temp - - def startPrint(self): - """ - Starts the print of the currently selected file - - Use select() to select a file - """ - url = 'http://' + self.ip + '/api/job' - payload = {'command': 'start'} - headers = {'content-type': 'application/json', 'X-Api-Key': self.apiKey} - requests.post(url, data=json.dumps(payload), headers=headers) - - def pausePrint(self): - """ - Pauses/unpauses the current print job - - There must be an active print job for this to work - """ - url = 'http://' + self.ip + '/api/job' - payload = {'command': 'pause'} - headers = {'content-type': 'application/json', 'X-Api-Key': self.apiKey} - requests.post(url, data=json.dumps(payload), headers=headers) - - def restartPrint(self): - """ - Starts the print of the currently selected file - - Use select() to select a file - """ - url = 'http://' + self.ip + '/api/job' - payload = {'command': 'restart'} - headers = {'content-type': 'application/json', 'X-Api-Key': self.apiKey} - requests.post(url, data=json.dumps(payload), headers=headers) - - def cancelPrint(self): - """ - Starts the print of the currently selected file - - Use select() to select a file - """ - url = 'http://' + self.ip + '/api/job' - payload = {'command': 'cancel'} - headers = {'content-type': 'application/json', 'X-Api-Key': self.apiKey} - requests.post(url, data=json.dumps(payload), headers=headers) - - # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - # ++++++++++++++++++++++++ Connection Handling +++++++++++++++++++++++++++++++++ - # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - def version(self): - """ - Retrieve information regarding server and API version - """ - url = 'http://' + self.ip + '/api/version' - headers = {'X-Api-Key': self.apiKey} - response = requests.get(url, headers=headers) - temp = response.json() - return temp - - def getPrinterConnectionSettings(self): - """ - Retrieve the current connection settings, including information - regarding the available baudrates and serial ports and the - current connection state. - """ - - url = 'http://' + self.ip + '/api/connection' - headers = {'X-Api-Key': self.apiKey} - response = requests.get(url, headers=headers) - temp = response.json() - return temp - - def connectPrinter(self, port=None, baudrate=None, printer_profile=None, save=None, autoconnect=None): - """ - Instructs OctoPrint to connect to the printer - - port: Optional, specific port to connect to. If not set the current - portPreference will be used, or if no preference is available auto - detection will be attempted. - - baudrate: Optional, specific baudrate to connect with. If not set - the current baudratePreference will be used, or if no preference - is available auto detection will be attempted. - - printer_profile: Optional, specific printer profile to use for - connection. If not set the current default printer profile - will be used. - - save: Optional, whether to save the request's port and baudrate - settings as new preferences. Defaults to false if not set. - - autoconnect: Optional, whether to automatically connect to the printer - on OctoPrint's startup in the future. If not set no changes will be - made to the current configuration. - """ - payload = {'command': 'connect'} - if port is not None: - payload['port'] = port - if baudrate is not None: - payload['baudrate'] = baudrate - if printer_profile is not None: - payload['printerProfile'] = printer_profile - if save is not None: - payload['save'] = save - if autoconnect is not None: - payload['autoconnect'] = autoconnect - url = 'http://' + self.ip + '/api/connection' - headers = {'content-type': 'application/json', 'X-Api-Key': self.apiKey} - requests.post(url, data=json.dumps(payload), headers=headers) - - def disconnect(self): - """ - Instructs OctoPrint to disconnect from the printer - """ - - url = 'http://' + self.ip + '/api/connection' - payload = {'command': 'disconnect'} - headers = {'content-type': 'application/json', 'X-Api-Key': self.apiKey} - requests.post(url, data=json.dumps(payload), headers=headers) - - # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - # ++++++++++++++++++ Printer Operations ++++++++++++++++++++++++++++++++++++++++ - # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - - def getPrinterState(self, exclude=None, history=False, limit=None): - """ - Retrieves the current state of the printer - Returned information includes: - * temperature information - * SD state (if available) - * general printer state - Temperature information can also be made to include the printer's - temperature history by setting the history argument. - The amount of data points to return here can be limited using the limit - argument. - Clients can specify a list of attributes to not return in the response - (e.g. if they don't need it) via the exclude argument. - """ - url = 'http://' + self.ip + '/api/printer' - headers = {'X-Api-Key': self.apiKey} - payload = {"exclude": exclude, "history": history, "limit": limit} - response = requests.get(url, params=payload, headers=headers) - # Handle error exception - if response.status_code == 409: - return response.text, response.status_code - else: - return response.json(), response.status_code - - def getToolState(self, history=False, limit=None): - """ - Retrieves the current temperature data (actual, target and offset) plus - optionally a (limited) history (actual, target, timestamp) for all of - the printer's available tools. - - It's also possible to retrieve the temperature history by setting the - history argument. The amount of returned history data points can be - limited using the limit argument. - """ - url = 'http://' + self.ip + '/api/tool' - headers = {'X-Api-Key': self.apiKey} - payload = {"history": history, "limit": limit} - response = requests.get(url, params=payload, headers=headers) - temp = response.json() - return temp - - def getBedState(self, history=False, limit=None): - """ - Retrieves the current temperature data (actual, target and offset) plus - optionally a (limited) history (actual, target, timestamp) for all of - the printer's available tools. - - It's also possible to retrieve the temperature history by setting the - history argument. The amount of returned history data points can be - limited using the limit argument. - """ - url = 'http://' + self.ip + '/api/bed' - headers = {'X-Api-Key': self.apiKey} - payload = {"history": history, "limit": limit} - response = requests.get(url, params=payload, headers=headers) - temp = response.json() - return temp - - def jog(self, x=None, y=None, z=None, absolute=False, speed=None): - - """ - Jogs the print head (relatively or absolutly) by a defined amount in one or more - axes. Additional parameters are: - x: Optional. Amount to jog print head on x axis, must be a valid - number corresponding to the distance to travel in mm. - y: Optional. Amount to jog print head on y axis, must be a valid - number corresponding to the distance to travel in mm. - z: Optional. Amount to jog print head on z axis, must be a valid - number corresponding to the distance to travel in mm. - """ - url = 'http://' + self.ip + '/api/printer/printhead' - payload = {'command': 'jog', 'absolute': absolute} - if x is not None: - payload['x'] = x - if y is not None: - payload['y'] = y - if z is not None: - payload['z'] = z - if speed is not None: - payload['speed'] = speed - print ("jog called" + str(payload)) - headers = {'content-type': 'application/json', 'X-Api-Key': self.apiKey} - requests.post(url, data=json.dumps(payload), headers=headers) - - def home(self, axes=None): - """ - Homes the print head in all of the given axes. - Additional parameters are: - - axes: A list of axes which to home, valid values are one or more of - 'x', 'y', 'z'. Defaults to all. - """ - url = 'http://' + self.ip + '/api/printer/printhead' - axes = [a.lower()[:1] for a in axes] if axes else ['x', 'y', 'z'] - payload = {"command": "home", "axes": axes} - headers = {'content-type': 'application/json', 'X-Api-Key': self.apiKey} - requests.post(url, data=json.dumps(payload), headers=headers) - - def feedrate(self, factor): - """ - Changes the feedrate factor to apply to the movement's of the axes. - - factor: The new factor, percentage as integer or float (percentage - divided by 100) between 50 and 200%. - """ - url = 'http://' + self.ip + '/api/printer/printhead' - payload = {'command': 'feedrate', 'factor': factor} - headers = {'content-type': 'application/json', 'X-Api-Key': self.apiKey} - requests.post(url, data=json.dumps(payload), headers=headers) - - @classmethod - def _tool_dict(cls, data): - if isinstance(data, (int, float)): - data = (data,) - if isinstance(data, dict): - ret = data - else: - ret = {} - for n, thing in enumerate(data): - ret['tool{}'.format(n)] = thing - return ret - - def setToolTemperature(self, targets): - """ - Sets the given target temperature on the printer's tools. - Additional parameters: - targets: Target temperature(s) to set. - Can be one number (for tool0), list of numbers or dict, where keys - must match the format tool{n} with n being the tool's index starting - with 0. - """ - url = 'http://' + self.ip + '/api/printer/tool' - targets = self._tool_dict(targets) - payload = {"command": "target", 'targets': targets} - headers = {'content-type': 'application/json', 'X-Api-Key': self.apiKey} - requests.post(url, data=json.dumps(payload), headers=headers) - - def setToolOffsets(self, offsets): - """ - Sets the given target temperature on the printer's tools. - Additional parameters: - targets: Target temperature(s) to set. - Can be one number (for tool0), list of numbers or dict, where keys - must match the format tool{n} with n being the tool's index starting - with 0. - """ - url = 'http://' + self.ip + '/api/printer/tool' - offsets = self._tool_dict(offsets) - payload = {"command": "target", 'offsets': offsets} - headers = {'content-type': 'application/json', 'X-Api-Key': self.apiKey} - requests.post(url, data=json.dumps(payload), headers=headers) - - def selectTool(self, tool): - """ - Selects the printer's current tool. - Additional parameters: - - tool: Tool to select, format tool{n} with n being the tool's index - starting with 0. Or integer. - """ - url = 'http://' + self.ip + '/api/printer/tool' - if isinstance(tool, int): - tool = 'tool{}'.format(tool) - payload = {'command': 'select', 'tool': tool} - headers = {'content-type': 'application/json', 'X-Api-Key': self.apiKey} - requests.post(url, data=json.dumps(payload), headers=headers) - - def extrude(self, amount): - """ - Extrudes the given amount of filament from the currently selected tool - - Additional parameters: - - amount: The amount of filament to extrude in mm. - May be negative to retract. - """ - url = 'http://' + self.ip + '/api/printer/tool' - payload = {'command': 'extrude', 'amount': amount} - headers = {'content-type': 'application/json', 'X-Api-Key': self.apiKey} - requests.post(url, data=json.dumps(payload), headers=headers) - - def retract(self, amount): - """ - Retracts the given amount of filament from the currently selected tool - - Additional parameters: - - amount: The amount of filament to retract in mm. - May be negative to extrude. - """ - self.extrude(-amount) - - def flowrate(self, factor): - """ - Changes the flow rate factor to apply to extrusion of the tool. - - factor: The new factor, percentage as integer or float - (percentage divided by 100) between 75 and 125%. - """ - url = 'http://' + self.ip + '/api/printer/tool' - payload = {'command': 'flowrate', 'factor': factor} - headers = {'content-type': 'application/json', 'X-Api-Key': self.apiKey} - requests.post(url, data=json.dumps(payload), headers=headers) - - def setBedTemperature(self, target): - """ - Sets the given target temperature on the printer's bed. - - target: Target temperature to set. - """ - url = 'http://' + self.ip + '/api/printer/bed' - payload = {'command': 'target', 'target': target} - headers = {'content-type': 'application/json', 'X-Api-Key': self.apiKey} - requests.post(url, data=json.dumps(payload), headers=headers) - - def setbedOffset(self, offset): - """ - Sets the given temperature offset on the printer's bed. - - offset: Temperature offset to set. - """ - url = 'http://' + self.ip + '/api/printer/bed' - payload = {'command': 'offset', 'offset': offset} - headers = {'content-type': 'application/json', 'X-Api-Key': self.apiKey} - requests.post(url, data=json.dumps(payload), headers=headers) - - def initialiseSd(self): - """ - Initializes the printer's SD card, making it available for use. - This also includes an initial retrieval of the list of files currently - stored on the SD card, so after issuing files(location=sd) a retrieval - of the files on SD card will return a successful result. - - If OctoPrint detects the availability of a SD card on the printer - during connection, it will automatically attempt to initialize it. - """ - url = 'http://' + self.ip + '/api/printer/sd' - payload = {'command': 'init'} - headers = {'content-type': 'application/json', 'X-Api-Key': self.apiKey} - requests.post(url, data=json.dumps(payload), headers=headers) - - def sdRefresh(self): - """ - Refreshes the list of files stored on the printer''s SD card. - Will raise a 409 Conflict if the card has not been initialized yet - with sd_init(). - """ - url = 'http://' + self.ip + '/api/printer/sd' - payload = {'command': 'refresh'} - headers = {'content-type': 'application/json', 'X-Api-Key': self.apiKey} - requests.post(url, data=json.dumps(payload), headers=headers) - - def sdRelease(self): - """ - Releases the SD card from the printer. The reverse operation to init. - After issuing this command, the SD card won't be available anymore, - hence and operations targeting files stored on it will fail. - Will raise a 409 Conflict if the card has not been initialized yet - with sd_init(). - """ - url = 'http://' + self.ip + '/api/printer/sd' - payload = {'command': 'release'} - headers = {'content-type': 'application/json', 'X-Api-Key': self.apiKey} - requests.post(url, data=json.dumps(payload), headers=headers) - - def getSdState(self): - """ - Retrieves the current state of the printer's SD card. - - If SD support has been disabled in OctoPrint's settings, - a 404 Not Found is risen. - """ - url = 'http://' + self.ip + '/api/printer/sd' - headers = {'X-Api-Key': self.apiKey} - response = requests.get(url, headers=headers) - temp = response.json() - return temp - - def gcode(self, command): - """ - Sends any command to the printer via the serial interface. - Should be used with some care as some commands can interfere with or - even stop a running print job. - - command: A single string command or command separated by newlines - or a list of commands - """ - try: - commands = command.split('\n') - except AttributeError: - # already an iterable - commands = list(command) - url = 'http://' + self.ip + '/api/printer/command' - payload = {'commands': commands} - headers = {'content-type': 'application/json', 'X-Api-Key': self.apiKey} - requests.post(url, data=json.dumps(payload), headers=headers) - - def getSoftwareUpdateInfo(self): - """ - get information from the software update API about software module versions, ad if updates are available - :return: - """ - url = 'http://' + self.ip + '/plugin/softwareupdate/check' - headers = {'X-Api-Key': self.apiKey} - response = requests.get(url, headers=headers) - temp = response.json() - return temp - - def performSoftwareUpdate(self,force = False): - url = 'http://' + self.ip + '/plugin/softwareupdate/update' - payload = {'force': str(force).lower()} - headers = {'content-type': 'application/json', 'X-Api-Key': self.apiKey} - requests.post(url, data=json.dumps(payload), headers=headers) - - - def isFailureDetected(self): - url = 'http://' + self.ip + '/plugin/TwinDragonPrintRestore/isFailureDetected' - headers = {'X-Api-Key': self.apiKey} - response = requests.get(url, headers=headers) - temp = response.json() - return temp - - def restore(self, restore = False): - url = 'http://' + self.ip + '/plugin/TwinDragonPrintRestore/restore' - headers = {'content-type': 'application/json', 'X-Api-Key': self.apiKey} - payload = {'restore': restore} - response = requests.post(url, data=json.dumps(payload), headers=headers) - temp = response.json() - return temp - - def getPrintRestoreSettings(self): - url = 'http://' + self.ip + '/plugin/TwinDragonPrintRestore/getSettings' - headers = {'X-Api-Key': self.apiKey} - response = requests.get(url, headers=headers) - temp = response.json() - return temp - - def savePrintRestoreSettigns(self, restore = False, enabled = True, interval = 1): - url = 'http://' + self.ip + '/plugin/TwinDragonPrintRestore/saveSettings' - headers = {'content-type': 'application/json', 'X-Api-Key': self.apiKey} - payload = {'restore': restore, "interval" : interval, "enabled": enabled} - requests.post(url, data=json.dumps(payload), headers=headers) - - def overrideDoorLock(self): - """ - locks and unlocks the front door, needs Volterra plugin installed - """ - url = 'http://' + self.ip + '/plugin/VolterraServices/lock_override' - headers = {'content-type': 'application/json', 'X-Api-Key': self.apiKey} - temp = requests.get(url, headers=headers) - return temp diff --git a/src/processAutomationController/processAutomationController.py b/src/processAutomationController/processAutomationController.py new file mode 100644 index 00000000..3e406d71 --- /dev/null +++ b/src/processAutomationController/processAutomationController.py @@ -0,0 +1,231 @@ +from PyQt5.QtCore import QObject, pyqtSignal +from utils.helpers import run_async +import time + +# TBD clean play pause process. use printer printing status to diferentiate between control and main printing sequence + +class ProcessAutomationController(QObject): + progress_update_signal = pyqtSignal(int) + + def __init__(self, main_window): + super(ProcessAutomationController, self).__init__() + self.main_window = main_window + self.process_running = False + + # Connect the progress update signal to the slot + self.progress_update_signal.connect(self.update_progress_bar) + + def update_progress_bar(self, value): + """Slot to update the progress bar value.""" + self.main_window.home_screen.printProgressBar.setValue(value) + + def initialLevellingRecoat(self): + """Perform the initial levelling recoat.""" + self.set_motion_control_buttons_enabled(False) + + layerHeight = self.main_window.printer_status.layerHeight + initialLevellingHeight = self.main_window.printer_status.initialLevellingHeight + recoatCount = int(initialLevellingHeight / layerHeight) + sequence = self.main_window.printer_status.initialLevellingRecoatingSequence + + for i in range(recoatCount): + if not self.process_running: + break + + # Pause handling + while not self.main_window.home_screen.playPauseButton.isChecked(): + if not self.process_running: + break + time.sleep(1) # Sleep for a short duration to avoid busy waiting + + if not self.process_running: + break + + # Perform recoat operation + sequence_replaced = replace_placeholders(sequence, self.main_window.printer_status) + for line in sequence_replaced.split('\n'): + self.main_window.moonraker_api.send_gcode(line) + + self.set_motion_control_buttons_enabled(True) + + def heatedBufferRecoat(self): + """Perform the heated buffer recoat.""" + self.set_motion_control_buttons_enabled(False) + + layerHeight = self.main_window.printer_status.layerHeight + heatedBufferHeight = self.main_window.printer_status.heatedBufferHeight + recoatCount = int(heatedBufferHeight / layerHeight) + sequence = self.main_window.printer_status.heatedBufferRecoatingSequence + + for i in range(recoatCount): + if not self.process_running: + break + + while True: + setpoint = self.main_window.printer_status.chamberTemperatureSetpoint + temps = self.main_window.printer_status.chamberTemperatures + if all(temps.get(pos, 0) >= setpoint for pos in ['middle-center']): + time.sleep(2) #wait 20 secs atleast for layer to heat + break + if not self.process_running: + self.progress_update_signal.emit(0) + break + time.sleep(1) # Sleep for a short duration to avoid busy waiting + + # Pause handling + while not self.main_window.home_screen.playPauseButton.isChecked(): + if not self.process_running: + break + time.sleep(1) # Sleep for a short duration to avoid busy waiting + + if not self.process_running: + break + + # Perform recoat operation + sequence_replaced = replace_placeholders(sequence, self.main_window.printer_status) + for line in sequence_replaced.split('\n'): + self.main_window.moonraker_api.send_gcode(line) + + self.set_motion_control_buttons_enabled(True) + + def dose_recoat_layer(self): + """Perform a single recoat using the layer height from the parameters screen.""" + self.set_motion_control_buttons_enabled(False) # Disable motion control buttons + sequence = self.main_window.printer_status.printingRecoatingSequence + sequence_replaced = replace_placeholders(sequence, self.main_window.printer_status) + for line in sequence_replaced.split('\n'): + self.main_window.moonraker_api.send_gcode(line) + self.progress_update_signal.emit(100) + self.set_motion_control_buttons_enabled(True) # Re-enable motion control buttons + + def prepare_powder_loading(self): + """Prepare for powder loading.""" + self.set_motion_control_buttons_enabled(False) + sequence = self.main_window.printer_status.powderLoadingSequence + sequence_replaced = replace_placeholders(sequence, self.main_window.printer_status) + for line in sequence_replaced.split('\n'): + self.main_window.moonraker_api.send_gcode(line) + self.set_motion_control_buttons_enabled(True) + + def move_to_starting_sequence(self): + """Execute the move to starting sequence.""" + self.set_motion_control_buttons_enabled(False) + sequence = self.main_window.printer_status.moveToStartingSequence + sequence_replaced = replace_placeholders(sequence, self.main_window.printer_status) + for line in sequence_replaced.split('\n'): + self.main_window.moonraker_api.send_gcode(line) + self.set_motion_control_buttons_enabled(True) + + def prepare_for_part_removal_sequence(self): + """Execute the prepare for part removal sequence.""" + self.set_motion_control_buttons_enabled(False) + sequence = self.main_window.printer_status.prepareForPartRemovalSequence + sequence_replaced = replace_placeholders(sequence, self.main_window.printer_status) + for line in sequence_replaced.split('\n'): + self.main_window.moonraker_api.send_gcode(line) + self.set_motion_control_buttons_enabled(True) + + @run_async + def start_printing_sequence(self): + """Start the main printing sequence.""" + self.set_motion_control_buttons_enabled(False) + self.progress_update_signal.emit(0) + + # Step 1: Initial Levelling Recoat + self.initialLevellingRecoat() + self.progress_update_signal.emit(10) + print("Initial Levelling Recoat done") + + # Step 2: Heated Buffer Recoat + self.heatedBufferRecoat() + self.progress_update_signal.emit(20) + print("Heated Buffer Recoat done") + + # Step 3 and 4: Mark laser and dose recoat layer until partHeight is achieved + layerHeight = self.main_window.printer_status.layerHeight + partHeight = self.main_window.printer_status.partHeight + recoatCount = int(partHeight / layerHeight) + + for i in range(recoatCount): + if not self.process_running: + self.progress_update_signal.emit(0) + break + + # Pause handling + while not self.main_window.home_screen.playPauseButton.isChecked(): + if not self.process_running: + self.progress_update_signal.emit(0) + break + time.sleep(1) # Sleep for a short duration to avoid busy waiting + + while True: + setpoint = self.main_window.printer_status.chamberTemperatureSetpoint + temps = self.main_window.printer_status.chamberTemperatures + if all(temps.get(pos, 0) >= setpoint for pos in ['middle-center']): + time.sleep(2) #wait 20 secs atleast for layer to heat + break + if not self.process_running: + self.progress_update_signal.emit(0) + break + time.sleep(1) # Sleep for a short duration to avoid busy waiting + + if not self.process_running: + self.progress_update_signal.emit(0) + break + + print("Marking layer number: ", i) + # Mark laser until the command is successfully sent + future = self.main_window.scancard.start_mark() + response = future.result() + time.sleep(5) # Sleep for a short duration to avoid busy waiting \\ to ensure we get latest status + while self.main_window.printer_status.scancard_status == "Marking": + time.sleep(1) + if not self.process_running: + self.progress_update_signal.emit(0) + break + + if not self.process_running: + self.progress_update_signal.emit(0) + break + + # Dose recoat layer + self.dose_recoat_layer() + progress = int((i + 1) / recoatCount * 60) + 20 + self.progress_update_signal.emit(progress) + + # Step 5: Final Heated Buffer Recoat + self.heatedBufferRecoat() + self.progress_update_signal.emit(100) + + self.set_motion_control_buttons_enabled(True) + + def stop_process(self): + """Stop the recoat process.""" + self.process_running = False + self.main_window.home_screen.playPauseButton.setChecked(False) + self.progress_update_signal.emit(0) + + def set_motion_control_buttons_enabled(self, enabled): + """Enable or disable motion control buttons.""" + for button in self.main_window.control_screen.motion_control_buttons: + button.setEnabled(enabled) + +def replace_placeholders(sequence: str, printer_status) -> str: + """Replace placeholders in the sequence with actual values from the printer_status model.""" + placeholders = { + "{layerHeight}": printer_status.layerHeight, + "{initialLevellingHeight}": printer_status.initialLevellingHeight, + "{heatedBufferHeight}": printer_status.heatedBufferHeight, + "{powderLoadingExtraHeightGap}": printer_status.powderLoadingExtraHeightGap, + "{bedTemperature}": printer_status.bedTemperature, + "{volumeTemperature}": printer_status.volumeTemperature, + "{chamberTemperature}": printer_status.chamberTemperature, + "{p}": printer_status.p, + "{i}": printer_status.i, + "{d}": printer_status.d, + "{powderLoadingHeight}": printer_status.initialLevellingHeight + 2 * printer_status.heatedBufferHeight + printer_status.partHeight, + "{dosingHeight}": printer_status.dosingHeight # Add dosingHeight + } + for placeholder, value in placeholders.items(): + sequence = sequence.replace(placeholder, str(value)) + return sequence \ No newline at end of file diff --git a/src/rgbCamera/rgbCamera.py b/src/rgbCamera/rgbCamera.py new file mode 100644 index 00000000..3f5c275b --- /dev/null +++ b/src/rgbCamera/rgbCamera.py @@ -0,0 +1,50 @@ +import sys +import cv2 +import time +from PyQt5.QtWidgets import QApplication, QLabel, QVBoxLayout, QWidget +from PyQt5.QtGui import QImage, QPixmap +from PyQt5.QtCore import QTimer +from PyQt5.QtCore import QThread, pyqtSignal +import numpy as np + +class RGBCamera(QThread): + rgb_camera_frame_ready = pyqtSignal(np.ndarray) + + def __init__(self): + super().__init__() + self.cap = cv2.VideoCapture(0) # Ensure the correct camera index + if not self.cap.isOpened(): + print("Error: Could not open the IR camera.") + exit() + def run(self): + """Runs the camera processing loop asynchronously.""" + self.running = True + while self.running: + start_time = time.time() + self.update_frame() + elapsed_time = time.time() - start_time + sleep_time = max(0, (1/30) - elapsed_time) + time.sleep(sleep_time) + + def update_frame(self): + ret, frame = self.cap.read() + if not ret: + print("Error: Failed to capture an image.") + time.sleep(0.5) # Add a small delay before retrying + return + zoom_factor = 0.5 + # Define the region of interest (ROI) for cropping + height, width, _ = frame.shape + roi_size = int(min(height, width) * zoom_factor) # Define the size of the inner square (half of the smaller dimension) + x_center, y_center = width // 2, height // 2 # Center of the frame + x1, y1 = x_center - roi_size // 2, y_center - roi_size // 2 + x2, y2 = x_center + roi_size // 2, y_center + roi_size // 2 + + # Crop the frame to the ROI + cropped_frame = frame[y1:y2, x1:x2] + + # Resize the cropped frame to the original frame size to achieve zoom effect + zoomed_frame = cv2.resize(cropped_frame, (width, height)) + self.rgb_camera_frame_ready.emit(zoomed_frame) # Emit the frame for display + + diff --git a/src/temperatureController/chamberTemperatureController.py b/src/temperatureController/chamberTemperatureController.py new file mode 100644 index 00000000..2d502003 --- /dev/null +++ b/src/temperatureController/chamberTemperatureController.py @@ -0,0 +1,85 @@ +from PyQt5.QtCore import QThread, pyqtSlot +import numpy as np +from simple_pid import PID +from .heaterBoard import HeaterBoard + +class ChamberTemperatureController(QThread): + def __init__(self, printer_status): + super().__init__() + self.heater_board = HeaterBoard() + self.printer_status = printer_status + + # Connect the temperatures_updated signal to the control_heater slot + self.printer_status.temperatures_updated.connect(self.control_heater) + + # Initialize PID controllers for each side + self.pid_bottom = PID(15, 0.000001, 0.001, setpoint=0) + self.pid_right = PID(15, 0.000001, 0.001, setpoint=0) + self.pid_top = PID(15, 0.000001, 0.001, setpoint=0) + self.pid_left = PID(15, 0.000001, 0.001, setpoint=0) + + # Set output limits for the PID controllers to clamp the integral factor + self.pid_bottom.output_limits = (1, 99) + self.pid_right.output_limits = (1, 99) + self.pid_top.output_limits = (1, 99) + self.pid_left.output_limits = (1, 99) + + # Store the previous setpoint to detect changes + self.previous_setpoint = self.printer_status.chamberTemperatureSetpoint + + def reset_pids(self): + """Reset the PID controllers.""" + self.pid_bottom.reset() + self.pid_right.reset() + self.pid_top.reset() + self.pid_left.reset() + + @pyqtSlot(np.ndarray, dict) + def control_heater(self, frame, chamberTemperatures): + """Control the heater power based on the setpoint and actual temperatures.""" + setpoint = self.printer_status.chamberTemperatureSetpoint + + # Check if the setpoint has changed + if setpoint != self.previous_setpoint: + print(f"Setpoint changed from {self.previous_setpoint} to {setpoint}. Resetting PIDs.") + self.reset_pids() + self.previous_setpoint = setpoint + + temps = chamberTemperatures + bottom_temp = temps.get('bottom-center', 0) + right_temp = temps.get('middle-right', 0) + top_temp = temps.get('top-center', 0) + left_temp = temps.get('middle-left', 0) + middle_center_temp = temps.get('middle-center', 0) + + # Update setpoints for each PID controller + self.pid_bottom.setpoint = setpoint + self.pid_right.setpoint = setpoint + self.pid_top.setpoint = setpoint + self.pid_left.setpoint = setpoint + + # Compute the control values + control_bottom = int(self.pid_bottom(bottom_temp)) + control_right = int(self.pid_right(right_temp)) + control_top = int(self.pid_top(top_temp)) + control_left = int(self.pid_left(left_temp)) + + # If middle-center temperature goes beyond the setpoint, reduce the output of other PIDs + if middle_center_temp > setpoint: + reduction_factor = 0.75 + control_bottom = int(control_bottom * reduction_factor) + control_right = int(control_right * reduction_factor) + control_top = int(control_top * reduction_factor) + control_left = int(control_left * reduction_factor) + + # Clamp the control values between 1 and 99 + control_bottom = max(1, min(99, control_bottom)) + control_right = max(1, min(99, control_right)) + control_top = max(1, min(99, control_top)) + control_left = max(1, min(99, control_left)) + + # Apply the control values to the heater board + self.heater_board.setHeaterPowers(control_bottom, control_bottom, control_right, control_right // 2, control_top, control_top, control_left, control_left // 2) + + # Log the control values for debugging + # print(f"Control values - Bottom: {control_bottom}, Right: {control_right}, Top: {control_top}, Left: {control_left}") \ No newline at end of file diff --git a/src/temperatureController/heaterBoard.py b/src/temperatureController/heaterBoard.py new file mode 100644 index 00000000..bef13450 --- /dev/null +++ b/src/temperatureController/heaterBoard.py @@ -0,0 +1,34 @@ +from .serialProtocol import SerialProtocol + +class HeaterBoard: + def __init__(self, port="COM19"): #TBD take port as input from frontend + self.serial_model = SerialProtocol(port="COM19", baudrate=115200, timeout=1) + + def setHeaterPowers(self, ch1, ch2, ch4, ch3, ch5, ch6, ch7, ch8): + command = f"8,{ch1},{ch2},{ch3},{ch6},{ch5},{ch4},{ch7},{ch8}" + future = self.serial_model.send_command_async(command) # Use the asynchronous method + future.add_done_callback(self.handle_response) # Handle the response when done + + def handle_response(self, future): + try: + response = future.result() + # print(f"Async Response: {response}") + except Exception as e: + print(f"Error in async response: {e}") + + def stopHeaters(self): + command = "8,1,1,1,1,1,1,1,1" + future = self.serial_model.send_command_async(command) # Use the asynchronous method + future.add_done_callback(self.handle_response) # Handle the response when done + print("Heater stopped") + + def enableWatchdog(self, event): + command = f"E" + future = self.serial_model.send_command_async(command) # Use the asynchronous method + future.add_done_callback(self.handle_response) # Handle the response when done + + def disableWatchdog(self, event): + command = f"D" + future = self.serial_model.send_command_async(command) # Use the asynchronous method + future.add_done_callback(self.handle_response) # Handle the response when done + diff --git a/src/temperatureController/serialProtocol.py b/src/temperatureController/serialProtocol.py new file mode 100644 index 00000000..49e9cbce --- /dev/null +++ b/src/temperatureController/serialProtocol.py @@ -0,0 +1,82 @@ +import serial +import time +from concurrent.futures import ThreadPoolExecutor, Future +from PyQt5.QtCore import QTimer, QMutex + +class SerialProtocol: + def __init__(self, port="COM19", baudrate=115200, timeout=1): + self.port = port + self.baudrate = baudrate + self.timeout = timeout + self.ser = None + self.executor = ThreadPoolExecutor(max_workers=1) # Initialize a thread pool executor + self.connect() + + # Set up a timer to periodically check the connection + self.timer = QTimer() + self.timer.timeout.connect(self.check_connection) + self.timer.start(5000) # Check every 5 seconds + + # Mutex to ensure only one command is sent at a time + self.mutex = QMutex() + + def connect(self): + try: + self.ser = serial.Serial(self.port, self.baudrate, timeout=self.timeout) + time.sleep(2) # Wait for the board to initialize + print(f"Connected to {self.port}") + except serial.SerialException as e: + print(f"Serial error: {e}") + self.ser = None + + def check_connection(self): + if self.ser is None or not self.ser.is_open: + print("Serial connection lost. Attempting to reconnect...") + self.connect() + + def flush_buffers(self): + """Flush the input and output buffers of the serial connection.""" + if self.ser: + self.ser.reset_input_buffer() + self.ser.reset_output_buffer() + print("Serial buffers flushed.") + + def send_command(self, command): + if not self.ser: + print("Serial port is not initialized.") + return + + self.mutex.lock() # Lock the mutex to ensure only one command is sent at a time + try: + # Ensure command starts with '$' + if not command.startswith('$'): + command = '$' + command + + # Append '\r\n' automatically + command += '\r\n' + + # Send command + self.ser.write(command.encode()) + self.ser.flush() # Ensure data is sent + time.sleep(0.1) # Wait for the response + + # Read response + response = self.ser.readline().decode().strip() + # print(f"Response: {response}") + return response + + except Exception as e: + print(f"Error: {e}") + self.flush_buffers() # Flush the buffers if an error occurs + finally: + self.mutex.unlock() # Unlock the mutex + + def send_command_async(self, command) -> Future: + """Send command asynchronously and return a Future object.""" + return self.executor.submit(self.send_command, command) + + def close(self): + if self.ser: + self.ser.close() + self.executor.shutdown(wait=False) # Shutdown the executor + self.timer.stop() # Stop the timer \ No newline at end of file diff --git a/src/thermalCamera/__init__.py b/src/thermalCamera/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/thermalCamera/distance_correction.dat b/src/thermalCamera/distance_correction.dat new file mode 100644 index 00000000..6247e241 --- /dev/null +++ b/src/thermalCamera/distance_correction.dat @@ -0,0 +1,45 @@ +# Distance correction factor +# distance in cm, correction (raw), correction (clean) +# ---------------------------------------------------------------------- +5 0.86 0.86 +10 0.94 0.94 +15 1.0 1.00 +20 1.03 1.03 +25 1.04 1.04 +30 1.09 1.07 +35 1.09 1.08 +40 1.09 1.09 +50 1.09 1.09 +60 1.1 1.10 +70 1.11 1.11 +80 1.11 1.11 +90 1.12 1.12 +100 1.12 1.13 +125 1.14 1.15 +150 1.16 1.17 +175 1.17 1.20 +200 1.2 1.22 +225 1.2 1.24 +250 1.26 1.26 +275 1.26 1.28 +300 1.26 1.31 +325 1.27 1.33 +350 1.27 1.35 +375 1.38 1.37 +400 1.39 1.39 +425 1.42 1.42 +450 1.47 1.44 +475 1.5 1.46 +500 1.5 1.48 +525 1.50 1.50 +550 1.57 1.53 +575 1.55 1.55 +600 1.63 1.57 +625 1.59 1.59 +650 1.61 1.61 +675 1.64 1.64 +700 1.66 1.66 +725 1.68 1.68 +750 1.70 1.70 +775 1.72 1.72 +800 1.75 1.75 diff --git a/src/thermalCamera/example.py b/src/thermalCamera/example.py new file mode 100644 index 00000000..5b7b8fd7 --- /dev/null +++ b/src/thermalCamera/example.py @@ -0,0 +1,103 @@ +import sys +import os +import signal +import logging +import threading +import numpy as np +import cv2 as cv +from PyQt5.QtCore import QThread, pyqtSignal +from .mi48 import MI48, format_header, format_framestats # Connects and communicates with the MI48 thermal camera +from .utils import data_to_frame, remap, cv_filter, RollingAverageFilter, connect_senxor + +# This will enable mi48 logging debug messages +logger = logging.getLogger(__name__) +logging.basicConfig(level=os.environ.get("LOGLEVEL", "DEBUG")) + +# Make the a global variable and use it as an instance of the mi48. +# This allows it to be used directly in a signal_handler. +global mi48 + +# define a signal handler to ensure clean closure upon CTRL+C +# or kill from terminal +def signal_handler(sig, frame): + """Ensure clean exit in case of SIGINT or SIGTERM""" + logger.info("Exiting due to SIGINT or SIGTERM") + mi48.stop() + cv.destroyAllWindows() + logger.info("Done.") + sys.exit(0) + +# Define the signals that should be handled to ensure clean exit +signal.signal(signal.SIGINT, signal_handler) +signal.signal(signal.SIGTERM, signal_handler) + +# Make an instance of the MI48, attaching USB for +# both control and data interface. +# can try connect_senxor(src='/dev/ttyS3') or similar if default cannot be found +mi48, connected_port, port_names = connect_senxor() + +# print out camera info +logger.info('Camera info:') +logger.info(mi48.camera_info) + +# set desired FPS +if len(sys.argv) == 2: + STREAM_FPS = int(sys.argv[1]) +else: + STREAM_FPS = 15 +mi48.set_fps(STREAM_FPS) + +# see if filtering is available in MI48 and set it up +mi48.disable_filter(f1=True, f2=True, f3=True) +mi48.set_filter_1(85) +mi48.enable_filter(f1=True, f2=False, f3=False, f3_ks_5=False) +mi48.set_offset_corr(0.0) + +mi48.set_sens_factor(100) +mi48.get_sens_factor() + +# initiate continuous frame acquisition +with_header = True +mi48.start(stream=True, with_header=with_header) + +# change this to false if not interested in the image +GUI = True + +# set cv_filter parameters +par = {'blur_ks':3, 'd':5, 'sigmaColor': 27, 'sigmaSpace': 27} + +dminav = RollingAverageFilter(N=10) +dmaxav = RollingAverageFilter(N=10) + +while True: + data, header = mi48.read() + if data is None: + logger.critical('NONE data received instead of GFRA') + mi48.stop() + sys.exit(1) + + min_temp = dminav(data.min()) # + 1.5 + max_temp = dmaxav(data.max()) # - 1.5 + frame = data_to_frame(data, (80,62), hflip=False); + frame = np.clip(frame, min_temp, max_temp) + filt_uint8 = cv_filter(remap(frame), par, use_median=True, + use_bilat=True, use_nlm=False) + # + if header is not None: + logger.debug(' '.join([format_header(header), + format_framestats(data)])) + else: + logger.debug(format_framestats(data)) + + if GUI: +# cv_render(filt_uint8, resize=(400,310), colormap='ironbow') + cv_render(filt_uint8, resize=(400,310), colormap='rainbow2') + # cv_render(remap(frame), resize=(400,310), colormap='rainbow2') + key = cv.waitKey(1) # & 0xFF + if key == ord("q"): + break +# time.sleep(1) + +# stop capture and quit +mi48.stop() +cv.destroyAllWindows() diff --git a/src/thermalCamera/interfaces.py b/src/thermalCamera/interfaces.py new file mode 100644 index 00000000..8872c9c3 --- /dev/null +++ b/src/thermalCamera/interfaces.py @@ -0,0 +1,452 @@ +# Copyright (C) Meridian Innovation Ltd. Hong Kong, 2020. All rights reserved. +import numpy as np +import logging +import time +from pprint import pformat +from .mi48 import get_reg_name + +# the following dependency is only for get_serial +import serial +import serial.tools.list_ports + + +logger = logging.getLogger(__name__) + +def cksum(data, sum=0): + """Calculate simple sum over data, allowing for non-zero init""" + try: + sum = sum + for byte in data: + sum += byte + return sum + except Exception as e: + logger.error(f"Error in cksum: {e}") + return sum + + +class I2C_Interface: + """I2C interface object to access a connected device""" + def __init__(self, i2c_bus, chip_addr): + self.device = i2c_bus + self.chip_addr = chip_addr + + def open(self): + try: + self.device.open() + except Exception as e: + logger.error(f"Error opening I2C device: {e}") + + def regread(self, register_addr, regname=""): + try: + byte = self.device.read_byte_data(self.chip_addr, register_addr) + return byte + except Exception as e: + logger.error(f"Error reading I2C register {regname}: {e}") + return None + + def regwrite(self, register_addr, register_value, regname=""): + try: + byte = register_value # no need to .encode() + self.device.write_byte_data(self.chip_addr, register_addr, byte) + except Exception as e: + logger.error(f"Error writing I2C register {regname}: {e}") + + def reset_input_buffer(self): + try: + self.device.reset_input_buffer() + except AttributeError: + # device may not support that + pass + except Exception as e: + logger.error(f"Error resetting I2C input buffer: {e}") + + def reset_output_buffer(self): + try: + self.device.reset_output_buffer() + except AttributeError: + # device may not support that + pass + except Exception as e: + logger.error(f"Error resetting I2C output buffer: {e}") + + def close(self): + try: + self.device.close() + except Exception as e: + logger.error(f"Error closing I2C device: {e}") + + +class SPI_Interface: + """SPI interface object to access a connected device""" + def __init__(self, spi_device, xfer_size): + self.device = spi_device + # host system would typically have a buffer that is + # smaller than the entire frame + self.xfer_size = xfer_size + + def open(self): + try: + self.device.open() + except Exception as e: + logger.error(f"Error opening SPI device: {e}") + + def read(self, length_in_words): + try: + # MI48 operates as a full duplex device and requires + # a dummy write byte for every byte read back + length_in_bytes = 2 * length_in_words + dummy_bytes = [0,] * self.xfer_size + xfer_size_words = int(self.xfer_size / 2) + length_in_words = int(length_in_bytes / 2) + # create a couple of buffers for storage of intermediate + # transfer and for the return buffer + data = np.zeros(length_in_words, dtype=np.uint16) + # make up a counter of how many words we have received + n_words = 0 + # loop until we receive the required number of words + while n_words < length_in_words: + i0 = n_words + i1 = n_words + xfer_size_words + # Keep the CS asserted throughout the transfer + # This should be a property of device.xfer. + # If device is an instance of spidev on rpi, this seems to be + # true for both xfer and xfer2 routines. + # For the sake of generality, keep this as xfer + response = self.device.xfer(dummy_bytes) + # interpret the response as array of uint8 + buffer = np.array(response).astype('u1') + n_words += int(len(buffer) / 2.) + # The MI48 assumes 16 bit word transfer with MSbit first. + # But we are reading with 8-bit word transfers on the RPI, + # and storing the MSB to lower location than the LSB. + # Hence we end up with big-endian data of unsigned 2-byte ints. + _data = np.ndarray(shape=(int(len(buffer) / 2),), + buffer=buffer, dtype='>u2') + try: + data[i0: i1] = _data + except IndexError: + # depending on xfer_size, the last transfer may be shorter + # print(i0, 2*i0, length_in_words, len(_data), len(buffer)) + data[i0:] = _data[:length_in_words - i0] + return data + except Exception as e: + logger.error(f"Error reading SPI data: {e}") + return None + + def reset_input_buffer(self): + try: + self.device.reset_input_buffer() + except AttributeError: + # device may not support that + pass + except Exception as e: + logger.error(f"Error resetting SPI input buffer: {e}") + + def reset_output_buffer(self): + try: + self.device.reset_output_buffer() + except AttributeError: + # device may not support that + pass + except Exception as e: + logger.error(f"Error resetting SPI output buffer: {e}") + + def close(self): + try: + self.device.close() + except Exception as e: + logger.error(f"Error closing SPI device: {e}") + + +# -------------------------- +# USB Vendor ID +# -------------------------- +MI_VID = 1046 # 0x0416 + +# -------------------------- +# USB Product ID +# -------------------------- +MI_PID_EVK = 45058 # 0xB002 EVK +MI_PID_XPRO = 45088 # 0xB002 XPro +MI_PIDs = [MI_PID_EVK, MI_PID_XPRO] + +# ------------------------------------------ +# USB header specific field lengths in bytes +# ------------------------------------------ +USB_CMD_LEN = 4 +USB_ACK_LEN = 4 +USB_CKS_LEN = 4 # check sum +USB_HDR_LEN = 320 + +class USB_Interface: + """USB interface object to access a connected device""" + + def __init__(self, port): + self.port = port + self.log = logger + + def open(self): + try: + self.port.open() + self.topen = time.time() + except Exception as e: + logger.error(f"Error opening USB port: {e}") + + def close(self): + try: + self.port.close() + except Exception as e: + logger.error(f"Error closing USB port: {e}") + + def reset_input_buffer(self): + try: + self.port.reset_input_buffer() + except Exception as e: + logger.error(f"Error resetting USB input buffer: {e}") + + def reset_output_buffer(self): + try: + self.port.reset_output_buffer() + except Exception as e: + logger.error(f"Error resetting USB output buffer: {e}") + + def regread(self, reg, regname=""): + """Read a control/status register via USB protocol""" + try: + result = None + while result is None: + cmd = 'RREG{:02X}XXXXXX'.format(reg) + cmd = ' #{:04X}{}'.format(len(cmd), cmd) + cmd_name = 'GET_{}'.format(regname) + result = usb_command(self.port, cmd, cmd_name) + if result is None: return + if not isinstance(result, int): + result = None + return result + except Exception as e: + logger.error(f"Error reading USB register {regname}: {e}") + return None + + def regwrite(self, reg, value, regname=""): + """Write to a control register via USB protocol""" + try: + cmd = 'WREG{:02X}{:02X}XXXX'.format(reg, value) + cmd = ' #{:04X}{}'.format(len(cmd), cmd) + cmd_name = 'SET_{}'.format(regname) + usb_command(self.port, cmd, cmd_name) + except Exception as e: + logger.error(f"Error writing USB register {regname}: {e}") + + def read(self, size_in_words): + """Read a GFRA acknowledge, remove USB header, and return data frame. + + The returned data frame is a 1-D numpy array of unsigned int16. + """ + try: + cmd, data = usb_acknowledge(self.port) + if cmd == 'GFRA': + # data is a sequence (1-d array) of 16-bit unsigned ints + # here we drop the USB header + return data[-size_in_words:] + else: + self.log.warning('read returned {} acknowledge.'.format(cmd)) + return None + except Exception as e: + logger.error(f"Error reading USB data: {e}") + return None + + +def usb_command(port, cmd: str, cmd_name='', verbose=True): + """send command to MI48 via USB and return its acknowledge""" + try: + _cmd = '' + retries = 3 # Number of retries for command + while _cmd != cmd[8:12] and retries > 0: + try: + port.write(cmd.encode()) + except serial.SerialTimeoutException: + logger.error("Write timeout occurred") + return None + _cmd, data = usb_acknowledge(port) + if _cmd != cmd[8:12]: + if verbose: + logger.debug('Expected ACK: {}, rcvd: {}'. + format(cmd[8:12], _cmd)) + logger.debug('Resetting input buffer') + port.reset_input_buffer() + retries -= 1 + if _cmd == 'RREG': + assert isinstance(data, int) + if verbose: logger.debug('{}'.format(fmt_usb_cmd(cmd, data))) + return data + except Exception as e: + logger.error(f"Error in usb_command: {e}") + return None + +def usb_acknowledge(port): + """Receive the EVK acknowledge and parse it""" + try: + ack = None + retries = 3 # Number of retries for acknowledge + while ack is None and retries > 0: + ack = usb_get_ack(port) + if ack is None: + port.reset_input_buffer() + retries -= 1 + if ack is None: + logger.error("Failed to receive valid acknowledge after retries") + return None + parsed = usb_parse_ack(*ack) + return parsed + except Exception as e: + logger.error(f"Error in usb_acknowledge: {e}") + return None + +def usb_parse_ack(cmd:str, data:bytes): + """ + Parse command and return the command string and a data item. + + The data item depends on the type of acknowledge: + + * 'GFRA' -- a 1-D array of 16-bit unsigned integers. + * 'RREG' -- an integer + * 'WREG' -- a None value + * 'SERR' -- decoded data field + """ + try: + cmd = cmd.decode() + if cmd == 'WREG': + # An acknowledge to a register-write contains no data + return cmd, None + if cmd == 'RREG': + # read command returns only a register value + return cmd, int(data.decode(), base=16) + if cmd == 'SERR': + # I have no info on what SERR contains... undocumented + return cmd, data.decode() + if cmd == 'GFRA': + # Frame acknowledge contains unencoded unsigned 16-bit ints + data = np.frombuffer(data, dtype='u2') + return cmd, data + except Exception as e: + logger.error(f"Error in usb_parse_ack: {e}") + return None + +def usb_get_ack(port): + """ + Obtain an acknowledge to a command sent to a virtual serial port + + Ack has the following format: + | ' #' | 4B length(LenCmdDat) | 4B command | data (lenth - 8B) | 4B CKS | + + Return bytes. + """ + try: + res = '' + while res != ' #': + res = port.read(4) + if res is None: + # likely the result of interface.read timeout (e.g. for USB) + return None + try: + res = res.decode() + except UnicodeDecodeError: + # This will happen if we're draining USB buffer from GFRA ack. + # so we ignore till we reach the beginning of the next frame. + res = '' + + # Read the length field and start check sum calculation + _len = port.read(USB_ACK_LEN) + cs = cksum(_len) + try: + ack_len = int(_len.decode(), base=16) + except ValueError: + return None + data_len = ack_len - USB_ACK_LEN - USB_CMD_LEN + # Read the data part of the payload and update the checksum + cmd = port.read(USB_CMD_LEN) + cs = cksum(cmd, cs) + data = port.read(data_len) + if data_len > 0: + cs = cksum(data, cs) + cs = cs & 0xFFFF + # Read the check sum field + cks = port.read(USB_CKS_LEN) + try: + cks.decode() + except UnicodeDecodeError: + logger.error(f'USB check sum decode error: {cks}') + return None + try: + cks = int(cks, base=16) + except ValueError: + # if host too slow, we get invalid literals here + logger.error(f'Bad USB check sum literals for {cmd}: {cks}') + return None + if cs != cks: + logger.error(f'Check sum mismatch: calculated {hex(cs)}, received {hex(cks)}') + return None + return cmd, data + except Exception as e: + logger.error(f"Error in usb_get_ack: {e}") + return None + +def fmt_usb_cmd(cmd, data): + """Command is a string already; here we return a more informative one""" + try: + s = [] + # s.append(cmd[:8]) # len + s.append(cmd[8:12]) # type + s.append(cmd[12:14]) # addr + s.append('{:16s}'.format(get_reg_name(int(cmd[12:14], 16)))) + + if cmd[8:12] == 'WREG': + val = int(cmd[14:16], 16) + s.append('0x{:02X}'.format(val)) + + if cmd[8:12] == 'RREG': + assert isinstance(data, int) + s.append('0x{:02X}'.format(data)) # value + # s.append('(0x{:02X})'.format(data)) + + return ' '+' '.join(s) + except Exception as e: + logger.error(f"Error in fmt_usb_cmd: {e}") + return '' + +def get_serial(open_ports=None, comport=None, verbose=True): + """Open a serial port to which the MI48 is attached. + + Raise UnboundLocalError if no serial port is successfully open + """ + try: + for p in list(serial.tools.list_ports.comports()): + if p.vid == MI_VID and p.pid in MI_PIDs: + # check it is the comport we want and skip if not + # if we did not specify description/name then get the + # first that we find to match Meridian's devices + logger.info(f'Senxor detected: {p.description}') + if comport is not None and comport not in p.description: + continue + try: + ser = serial.Serial(p.device) + except serial.SerialException: + # assume it is open + logger.info('Failed opening port:\n{}'.format(p)) + # do not raise, but check the next port in the list + continue + # here we already have gotten a serial device; set it up + ser.baudrate = 115200 + ser.rtscts = True + ser.dsrdtr = True + ser.timeout = 0.5 + ser.write_timeout = 0.5 + logger.info('Opened USB port:\n{}\n'.format(pformat(ser))) + break + # the following return statement will generate UnboundLocalError + # if no serial was successfully opened + return ser + except Exception as e: + logger.error(f"Error in get_serial: {e}") + return None + diff --git a/src/thermalCamera/mi48.py b/src/thermalCamera/mi48.py new file mode 100644 index 00000000..70973f6c --- /dev/null +++ b/src/thermalCamera/mi48.py @@ -0,0 +1,930 @@ +# Copyright (C) Meridian Innovation Ltd. Hong Kong, 2020. All rights reserved. +# +import sys +sys.path.append("/home/test/myenv/lib/python3.11/site-packages") +import logging +import functools +import time +import struct +import array +import numpy as np + +# For CRC reference start with http://crcmod/sourceforge.net/crcmod.predefined.html +# The MI48 implements the CRC-16/CCITT-FALSE +# polynomial = 0x11021, init=0xFFFF, reversed=False, xor-out=0x0000, +# check=0x29B1 (for input of b'123456789) +import crcmod.predefined + +def logger_wrapper(name, level, msg, exc_info=None, logger=None): + _msg = '{:12s} {}'.format(name, msg) + if logger == None: + logging.log(level, _msg, exc_info=exc_info) + else: + logger.log(level, _msg, exc_info=exc_info) + +# ======================= +# MI48xx specific objects +# ======================= +# Reference for temperature conversion K to C +KELVIN_0 = -273.15 # in Celsius +T_OFFSET_UNIT = 0.05 # increment unit for OFFSET_CORR register in K + +# Word index in SPI header field indexing referenced to SPI header base index +SPIHDR_FRCNT = 0 +SPIHDR_SXVDD = 1 +SPIHDR_SXTA = 2 +SPIHDR_TIME = 3 # two words +SPIHDR_MAXV = 5 +SPIHDR_MINV = 6 +SPIHDR_CRC = 7 + +DEFAULT_CTRL_STAT = { + 'FRAME_MODE': 0x20, + 'STATUS': 0x00, + 'FRAME_RATE': 0x04, + 'POWER_DOWN_1': 0x00, + 'POWER_DOWN_2': 0x02, + "SENS_FACTOR" : 0x64, # read from camera module; ideally 0x64 = 1.00 + 'EMISSIVITY': 0x5F, + 'OFFSET_CORR': 0x00, + 'FILTER_CTRL': 0x00, + 'FILTER_1_LSB': 0x32, + 'FILTER_1_MSB': 0x00, + 'FILTER_2': 0x04, +} + + +# MI48Ax register map +regmap = { + "EVK_TEST" : 0x00, # Check if we have a bridge-board-evk + mi48 coreboard or only EVK board + "EVK_ID" : 0xA5, # Check EVK ID, persistent ID regardless of the shifting virtual USB comport + # + "SENXOR_POWERUP": 0xB0, # Power up for senxor + "FRAME_MODE" : 0xB1, # RW Control of capture and readout + "FW_VERSION_1" : 0xB2, # R Firmware Version (Major, Minor) + "FW_VERSION_2" : 0xB3, # R Firmware Version (Build) + "FRAME_RATE" : 0xB4, # RW Frame rate delivery through the SPI interface + "POWER_DOWN_1" : 0xB5, # RW Control of power down parameters + "STATUS" : 0xB6, # R Status of the MI48 + "POWER_DOWN_2" : 0xB7, # RW Control of power down parameters + "SENXOR_TYPE" : 0xBA, # R Type of the attached camera module + "MODULE_TYPE" : 0xBB, # R Type of the attached camera module + "SENS_FACTOR" : 0xC2, # RW Sensitivity factor (float, unitless) + "EMISSIVITY" : 0xCA, # RW Emissivity value for conversion to [K] + "OFFSET_CORR" : 0xCB, # RW Temperature offset correction per frame + 'FILTER_CTRL' : 0xD0, # RW Control of filters + 'FILTER_1_LSB' : 0xD1, # RW Settings of filter 1 (temporal LSB) + 'FILTER_1_MSB' : 0xD2, # RW Settings of filter 1 (temporal MSB) + 'FILTER_2' : 0xD3, # RW Settings of filter 2 (rolling average) + "FLASH_CTRL" : 0xD8, # RW Control read/write to MI48 flash-memory + "SENXOR_ID" : 0xE0, # R Serial number of the attached camera module + "SENXOR_ID_0" : 0xE0, # R Serial number of the attached camera module + "SENXOR_ID_1" : 0xE1, # R Serial number of the attached camera module + "SENXOR_ID_2" : 0xE2, # R Serial number of the attached camera module + "SENXOR_ID_3" : 0xE3, # R Serial number of the attached camera module + "SENXOR_ID_4" : 0xE4, # R Serial number of the attached camera module + "SENXOR_ID_5" : 0xE5, # R Serial number of the attached camera module +} + +MI48_FRAME_MODE = 0xB1 # RW Control the capture and readout of thermal data +MI48_FW_VERSION_1 = 0xB2 # R Firmware Version (Major, Minor) +MI48_FW_VERSION_2 = 0xB3 # R Firmware Version (Build) +MI48_FRAME_RATE = 0xB4 # RW Frame rate delivery through the SPI interface +MI48_POWER_DOWN_1 = 0xB5 # RW Control of power down parameters +MI48_STATUS = 0xB6 # R Status of the attached camera module and MI48A1 interface operations +MI48_POWER_DOWN_2 = 0xB7 # RW Control of power down parameters +MI48_SENXOR_TYPE = 0xBA # R Type of the attached camera module +MI48_SENS_FACTOR = 0xC2 # RW Sensitivity factor, unitless, float +MI48_EMISSIVITY = 0xCA # RW Emissivity value for conversion of SenXorTM data to temperature +MI48_OFFSET_CORR = 0xCB # RW Temperature offset correction for the entire data frame +MI48_FILTER_CTRL = 0xD0 # RW Control of filters +MI48_FILTER_1_LSB = 0xD1 # RW Settings of filter 1 (temporal LSB) +MI48_FILTER_1_MSB = 0xD2 # RW Settings of filter 1 (temporal MSB) +MI48_FILTER_2 = 0xD3 # RW Settings of filter 2 (rolling average) +MI48_FLASH_CTRL = 0xD8 # RW Control of RW to MI48 flash-memory +MI48_SENXOR_ID = 0xE0 # R Serial number of the attached camera module +MI48_SENXOR_ID_0 = 0xE0 # R Serial number of the attached camera module +MI48_SENXOR_ID_1 = 0xE1 # R Serial number of the attached camera module +MI48_SENXOR_ID_2 = 0xE2 # R Serial number of the attached camera module +MI48_SENXOR_ID_3 = 0xE3 # R Serial number of the attached camera module +MI48_SENXOR_ID_4 = 0xE4 # R Serial number of the attached camera module +MI48_SENXOR_ID_5 = 0xE5 # R Serial number of the attached camera module +MI48_SENXOR_ID_LEN = 6 # number of bytes of the SENXOR_ID + +# STATUS Register Flags Masks +READOUT_TOO_SLOW = 0x02 +SENXOR_IF_ERROR = 0x04 +SXIF_ERROR = 0x04 # alias +CAPTURE_ERROR = 0x08 +DATA_READY = 0x10 +BOOTING_UP = 0x20 + +# FRAME_MODE Register Flags Masks +GET_SINGLE_FRAME = 0x01 +CONTINUOUS_STREAM = 0x02 +READOUT_MODE = 0x1C # bits 4-2 +NO_HEADER = 0x20 # skip header HEADER data + +# camera module type and FPA +SENXOR_NAME = { + 0: 'MI0801 non-MP', # non-MP modules + 1: 'MI0801', # MP modules + 2: 'MI0301', + 3: 'MI0802', + 4: 'MI0802', +} + +FPA_SHAPE = { + 'MI0801 non-MP': (80, 62), + 'MI0801': (80, 62), + 'MI0802': (80, 62), + 'MI0301': (32, 32), + 'bobcat': (80, 62), + 'bobcat-2': (80, 62), + 'lynx': (32, 32), + 'cougar': (80, 62), + 'panther': (160, 120), + 0: (80, 62), + 1: (80, 62), + 2: (32, 32), + 3: (80, 62), + 4: (80, 62), + 8: (160, 120), +} + + +crc16 = crcmod.predefined.mkCrcFun('crc-ccitt-false') + + +class MI48: + """ + MI48xx abstraction + """ + def __init__(self, interfaces:list, fps=None, name="MI48", + reset_handler=None, data_ready=None, read_raw=False): + """Initialise with a serial port""" + # logging stuff + self.name = name + self.log = functools.partial(logger_wrapper, self.name, logger=None) + # interface handles + self.interfaces = interfaces + # note that this will potentially clear only the host + # interface buffers; meanwhile, the MI48 buffers would + # require different handling, if the MI48 was left in + # an unknown state + self.clear_interface_buffers() + self.reset = reset_handler + if self.reset is not None: self.reset() + if data_ready is not None: + self.data_ready = data_ready + # this should be read from the camera module + self.fpa_shape = None + # check if EVK without bridge or if Jig board + self.parse_header = self.has_evk_bridge() + if not self.parse_header: + self.powerup() + # At this stage check that MI48 is not streaming already, + # which may happen if termination of last stream was not handled + # cleanly. If we do not stop the MI48 here, the status handling + # during boot-up and error-handling will be messed up. + mode = self.get_mode() + if mode & (GET_SINGLE_FRAME | CONTINUOUS_STREAM): +# # if the MI48 is streaming, it's obviously booted up before + self.stop_capture() + # + # check what camera we have + self.camera_info = self.get_camera_info() + # do not parse frame header if MI48 is not on the core dev board + self.parse_header = self.has_evk_bridge() + # check status register and raise relevant flags + status, mode = self.bootup(verbose=True) + # may need to handle ValueError from above call: + # happens if USB is still streaming when we restart + # and instead of RREG (int) we get GFRA acknowledge (array) + self.capture_no_header = mode & NO_HEADER + bootup_error =\ + (mode & GET_SINGLE_FRAME) or (mode & CONTINUOUS_STREAM)\ + or (status & READOUT_TOO_SLOW) or (status & DATA_READY)\ + or (status & CAPTURE_ERROR) or (status & SXIF_ERROR) + # see if we need to react to any errors + if bootup_error: + status, mode = self.error_handler(status, mode, verbose=True) + self.log(logging.DEBUG, 'Status: {}'.format(hex(status))) + self.log(logging.DEBUG, 'Mode : {}'.format(hex(mode))) + self.capture_no_header = mode & NO_HEADER + # reset crc.error + self.crc_error = False + # set FPS + if fps is not None: + self.set_fps(fps) + # set the format of the returned data + self.read_raw = read_raw + + def bootup(self, verbose=False, powerup=False): + """Ensure bootup of the mi48 is complete, returning MODE and STATUS. + + Return all flags raised at any one point while looping and waiting for + boot-up to complete, so that any error handling can be done after + boot-up. Exception is boot_in_progress flag, as we're handling it here. + This is necessary because error handling will likely require register + write, which is allowed only once that bootup is comlete. + """ + # NOTE on the timeout (in seconds) below: + # On WINDOWS the clock has poor resolution, which probably + # depends on CPU frequency too. + timeout = max(0.025, time.get_clock_info('monotonic').resolution) + if powerup: self.powerup() + self.check_ctrl_stat_regs() + t0 = time.monotonic() + status = self.get_status(verbose=verbose) + mode = self.get_mode(verbose=verbose) + boot_in_progress = status & BOOTING_UP +# no_header = mode & NO_HEADER + while boot_in_progress: + status = self.get_status(verbose=True) + mode = self.get_mode(verbose=True) + boot_in_progress = status & BOOTING_UP + time.sleep(timeout) + t1 = time.monotonic() + self.log(logging.DEBUG, 'Bootup complete in {:.0f} ms'. + format(1.e3 * (t1-t0))) + # clear boot in progress flag as we're done with it + status = status & (~BOOTING_UP & 0xFF) + self.log(logging.DEBUG, 'Status: {}'.format(hex(status))) + self.log(logging.DEBUG, 'Mode : {}'.format(hex(mode))) +# mode = mode & no_header + return status, mode + + def error_handler(self, status, mode, verbose=False): + """Attempt to bring the MI48 to a clean state. + + Specifically, attempt to stop capture and clear + the STATUS (upon read) and FRAME_MODE registers, also + clean the MI48 output frame buffer (read and dump the frame). + If a reset handler is provided to self, then use it if an + error between camera module and MI48 is detected + """ + if mode & (GET_SINGLE_FRAME | CONTINUOUS_STREAM): + # Stop MI48 capture + self.log(logging.WARNING, 'Attempting MI48.stop_capture()') + mode = self.stop_capture(verbose) + if (status & READOUT_TOO_SLOW): + self.log(logging.WARNING, 'Ignoring Readout Too Slow flag') + # ignore readout_too_slow for the moment. + # if we try to dump a frame based on Readout_too_slow or + # Capture_error without seeing data_ready, then in the + # case of a USB interface we will hang forever, as the + # Ack will never come. For the SPI it doesn't matter, since + # it is a full duplex, and we get data as long as we, as a + # master, push out zeros on the bus. + if (status & DATA_READY): + # Attempt to clear output buffer of the SPI slave interface + # If USB interface, that would have been tackled by MI48 FW. + # This should not be seen at boot up for USB interface. + self.log(logging.DEBUG, 'Trying to dump a frame') + data, header = self.read() + self.log(logging.WARNING, 'Dumping residual frame:') + self.log(logging.WARNING, header) + self.log(logging.WARNING, data[:20]) + # status and mode will be clean by here from the above + if (status & CAPTURE_ERROR): + self.log(logging.ERROR, + 'Capture ERROR: typically due to bad FPC connection.\n' + ' Check FPC and connectors.\n' + ' If problem percist, try to prepare reproducible\n', + ' example and call/email Meridian.') + + if (status & SXIF_ERROR): + try: + self.log(logging.ERROR, + 'SenXor Interface ERROR: Attempting SW reset of MI48') + self.reset() + except TypeError: + # no reset handle provided + self.log(logging.ERROR, + ' SW Reset handle not available.\n' + ' Please press HW reset of MI48\n') + raise RuntimeError + self.log(logging.DEBUG, 'Trying to get status again') + status = self.get_status(verbose=True) + return status, mode + + def regread(self, reg): + """Read a control/status register; Allow hex or str for reg""" + if isinstance(reg, str): + regname = reg + # Try to get the address value from the register map, but + # if the address refers to the internal flash, there will be + # no entry in the regmap dictionary; use value directly + try: + reg = regmap[reg] + except KeyError: + # assume hex string + reg = int(reg) + else: + # assume integer; make up the hex representation for logging + regname = f'0x{reg:02X}' + return self.interfaces[0].regread(reg, regname) + + def regwrite(self, reg, value): + """Write to a control register""" + if isinstance(reg, str): + regname = reg + reg = regmap[regname] + else: + regname = "" + return self.interfaces[0].regwrite(reg, value, regname) + + + def read(self): + """Read a data frame + + Return the temperature data or (data, header), where the + header is a dictionary. + The returned data is a 2D array of np.float16 representing the + temperature in Celsius. + Header values if requested are also decoded from bytes. + """ + # figure out how many words to get; recall 2 bytes per pixel + data_size = np.prod(self.fpa_shape) + size_in_words = data_size + if not self.capture_no_header: + size_in_words += self.cols + # print('Reading {} words'.format(size_in_words)) + + # The spi device must provide read(number-of-bytes) function + response = self.interfaces[1].read(size_in_words) + + # Obtain the data but do NOT convert to degrees C yet, + # because we have to calculate CRC on it first. + # Assume the interfaces[1].read() returns 16-bit integers + # Recall that the temperature data frame is after the + # optional header + try: + data = response[-data_size:] + except TypeError: + # if interface.read() yields None we've got an error + return None, None + + # Parse the optional header; else return the data + # If the MI48 is not on the core-development board, do not parse + if self.capture_no_header or not self.parse_header: + header = None + else: + _header = response[:-data_size] + header = self.parse_frame_header(_header) + self.crc_error = False + # check crc + # note that MI48 implements CRC-16/CCITT-FALSE which + # must be initialised with 0xFFFF + _crc = crc16(data) + if not header['crc'] == hex(_crc): + self.crc_error = True + self.log(logging.ERROR, 'Frame CRC error. '+ + 'Header CRC: {}, Data CRC: {}'.\ + format(header['crc'], hex(_crc))) + + # Once we have done the CRC check, convert to degrees C + # unless raw numbers are requested + if self.read_raw: + return data, header + else: + data = data / 10. + KELVIN_0 + return data.astype(np.float16), header + + def has_evk_bridge(self): + """ + Check if MI48 has a bridge-board + mi48 core dev board or + only the bare EVK board + """ + res = self.regread('EVK_TEST') + has_bridge = res == 0xFF + return has_bridge + + def get_evk_socket_id(self): + """Return the position (1 to 25; top left to bottom right; per row) in on the jig""" + res = self.regread('EVK_ID') + return res + + def powerup(self): + """Read calibration data from flash, and initialise SenXor""" + self.regwrite('SENXOR_POWERUP', 0x13) + time.sleep(0.1) + + def get_status(self, verbose=False): + """Read status register; log if non-zero status in verbose mode""" + status = self.regread('STATUS') + if verbose and status != 0: + self.log(logging.WARNING,'Non-zero STATUS: 0x{:02X}'. + format(status)) + self.log(logging.WARNING, ', '.join(self.parse_status(status))) + return status + + def parse_status(self, regvalue): + """Return a list of strings corresponding to set flags""" + s = [] + if regvalue & 0x02: s.append('Readout too slow') + if regvalue & 0x04: s.append('SenXor interface ERROR') + if regvalue & 0x08: s.append('Capture ERROR') + if regvalue & 0x10: s.append('Data ready') + if regvalue & 0x20: s.append('Boot up in progress') + return s + + def get_mode(self, verbose=False): + mode = self.regread('FRAME_MODE') + # it seems that if there is a SIGINT, serial interface may + # close immediately, and a timeout will return None here. + if mode is None: return None + if verbose and (mode & 0x03)!= 0x00: + self.log(logging.WARNING,'Capture in progress: 0x{:02X}'. + format(mode)) + self.log(logging.WARNING, ', '.join(self.parse_mode(mode))) + return mode + + def parse_mode(self, regvalue): + """Return a list of strings corresponding to set flags""" + s = [] + if regvalue & 0x01: s.append('Single capture in progress') + if regvalue & 0x02: s.append('Continuous streaming') + if regvalue & 0x10: s.append('No frame header') + return s + + def get_pm1(self): + return self.regread('POWER_DOWN_1') + + def get_pm2(self): + return self.regread('POWER_DOWN_2') + + def get_frame_rate(self): + return self.regread('FRAME_RATE') + + def get_emissivity(self): + return self.regread('EMISSIVITY') + + def get_sens_factor(self): + return self.regread('SENS_FACTOR') + + def get_offset_corr_regvalue(self): + """Read the value of OFFSET_CORR register""" + return self.regread('OFFSET_CORR') + + def get_offset_corr_K(self): + """Get the temperature offset corresponding to OFFSET_CORR register""" + unit = T_OFFSET_UNIT + n = self.regread('OFFSET_CORR') + if n < 128: + return n * unit + else: + return (n - 256) * unit + + def get_filter_ctrl(self): + """Return the value of Filter Control Register""" + return self.regread('FILTER_CTRL') + + def get_filter_1(self): + """Return the strength of the temporal filter""" + lsb = self.regread('FILTER_1_LSB') + msb = self.regread('FILTER_2_LSB') + res = (msb << 8) + lsb + return res + + def get_filter_2(self): + """Return the depth of the Rolling Average filter""" + return self.regread('FILTER_2') + + def get_camera_info(self): + """Get camera info: senxor type/ID, maxFPS, FW version""" + try: + return self.camera_info + except AttributeError: + # if we haven't yet read the info from camera module + pass + # read camera module info + res = {} + self.camera_info = res + res['NAME'] = self.name + res['CAMERA_TYPE'] = self.get_camera_type() + res['MODULE_TYPE'] = self.get_module_type() + res['EVK_ID'] = self.get_evk_socket_id() + uid, uid_hex, uid_hexsn = self.get_camera_id() + res['CAMERA_ID'] = uid_hex + res['CAMERA_MFG'] = uid_hexsn + res['SN'] = 'SN'+uid_hex + res['FW_VERSION'] = self.get_fw_version() + self.camera_type = res['CAMERA_TYPE'] + self.module_type = res['MODULE_TYPE'] + self.camera_name = SENXOR_NAME[self.camera_type] + self.fpa_shape = FPA_SHAPE[self.camera_type] + self.cols = self.fpa_shape[0] + self.rows = self.fpa_shape[1] + self.camera_id = res['CAMERA_ID'] + self.camera_id_hexsn = res['CAMERA_MFG'] + self.sn = res['SN'].upper() + self.fw_version = res['FW_VERSION'] + res['MAX_FPS'] = self.get_max_fps() + self.maxfps = res['MAX_FPS'] + # note that current FPS requires self.maxfps, + # becuase we can only read the divisor + res['Current FPS'] = self.get_fps() + return res + + def get_ctrl_stat_regs(self): + """Read all registers, return a dictionary {'RegName': 0xValue}""" + res = {} + self.log(logging.DEBUG, 'Reading Control and Status Regs:') + for reg in list(DEFAULT_CTRL_STAT.keys()): + res[reg] = self.regread(reg) + return res + + def check_ctrl_stat_regs(self, expect=None): + """Check control and statuts registers as expected""" + self.log(logging.DEBUG, 'Checking Control and Status Regs:') + if expect is None: + expect = DEFAULT_CTRL_STAT + res = self.get_ctrl_stat_regs() + for reg, val in res.items(): + log_level = logging.DEBUG + if reg in expect.keys(): + _exp = expect.get(reg) + if val != _exp: + log_level = logging.WARNING + self.log(log_level, '{}: {} (expected {})'. + format(reg, hex(val), hex(_exp))) + continue + self.log(log_level, '{}: {}'.format(reg, val)) + + def get_max_fps(self): + """Get some frames in continuous mode and establish max FPS""" + # TODO: real implementation of burst capture 250 frames, and + # determine average FPS. + # Or at least map maxfps to corresponding FW of the MI48 + # and the camera type. + if self.camera_type in [0,1]: + maxfps = 25.5 # this is true for Bobcat with latest MI48Ax + return maxfps + if self.camera_type in [2]: + maxfps = 28.57 # lynx + return maxfps + maxfps = 30.0 # aspirational + return maxfps + + def get_fps(self): + """Get current FPS [1/s]""" + divisor = self.get_frame_rate() + try: + return float(self.maxfps) / divisor + except ZeroDivisionError: + return self.maxfps + + def set_frame_rate(self, fps_divisor:int): + """Set the frame rate divisor register (integer)""" + self.regwrite('FRAME_RATE', fps_divisor) + + def set_fps(self, fps): + """Set the desired FPS [1/s] or the closest possible""" + try: + fps_divisor = int(round(float(self.maxfps) / fps)) + except ZeroDivisionError: + fps_divisor = 32 + self.log(logging.DEBUG, 'FPS target {}, divisor {}, actual {}'. + format(fps, fps_divisor, self.maxfps/fps_divisor)) + self.regwrite('FRAME_RATE', fps_divisor) + return None + + def set_emissivity(self, emissivity): + """Set emissivity, given in integer % (1-100) or float (0-1)""" + if emissivity > 100 or emissivity <= 0: + raise ValueError('Emissivity must be 0 to 1 (float) or 1 to 100 (int, %)') + # + if emissivity <= 1: + # assume a fraction; but MI48 accepts only integer % + emissivity *= 100 + # ensure we have an int for regwrite, even if we get a float > 1, e.g. 93.0 + emissivity = int(emissivity) + # + self.log(logging.DEBUG, 'Setting emissivity to {} %'. + format(emissivity)) + self.regwrite('EMISSIVITY', emissivity) + return None + + def enable_filter(self, f1=False, f2=False, f3=False, f3_ks_5=False): + """ + Enable filters: f1-temporal, f3-median, f2-rolling average. + + Implement a read-modify-write operation, so that filters may + be toggled independently. + """ + while True: + try: + fctrl = self.regread('FILTER_CTRL') + break + except: + pass + #fctrl = 0x00 + if f1: + # enable and initialise filter 1 + fctrl |= 0x03 # bit 0 and 1 + if f2: + fctrl |= 0x04 # bit 3 + if f3: + fctrl |= 0x40 # bit 6 + if f3_ks_5: + fctrl |= 0x20 # bit 5 + msg = "Enabling" + if fctrl & 0x01: + fset1 = self.get_filter_1() + msg += ' Filter 1 ({})'.format(hex(fset1)) + if fctrl & 0x04: + fset2 = self.get_filter_2() + msg += ' Filter 2 ({})'.format(hex(fset2)) + if fctrl & 0x40: + msg += ' Filter 3 ({})'.format(hex(fctrl & 0x20)) + self.log(logging.DEBUG, msg) + self.regwrite('FILTER_CTRL', fctrl) + time.sleep(40.e-3) + self.log(logging.DEBUG, 'FILTER_CONTROL {}'.format( + hex(self.get_filter_ctrl()))) + #return self.regread('FILTER_CTRL') + return None + + def disable_filter(self, f1=True, f2=True, f3=True): + fctrl = self.regread('FILTER_CTRL') + if f1: + # disable filter 1 + fctrl &= 0xFC # clear bit 0 and 1 (1111_1100) + if f2: + # disable filter 2 + fctrl &= 0xFB # clear bit 2 (1111_1011) + if f3: + # disable filter 3 + fctrl &= 0xBF # clear bit 6 (1011_1111) + msg = "Disabling" + if f1: + msg += ' Filter 1' + if f2: + msg += ' Filter 2' + if f3: + msg += ' Filter 3' + self.log(logging.DEBUG, msg) + self.regwrite('FILTER_CTRL', fctrl) + self.log(logging.DEBUG, 'FILTER_CONTROL {}'.format( + hex(self.get_filter_ctrl()))) + return None + + def get_filter_1(self): + lsb = self.regread('FILTER_1_LSB') + msb = self.regread('FILTER_1_MSB') + res = (msb << 8) + lsb + return res + + def set_filter_1(self, setting=None): + if None: + lsb = DEFAULT_CTRL_STAT['FILTER_1_LSB'] + msb = DEFAULT_CTRL_STAT['FILTER_1_MSB'] + lsb = setting & 0xFF + msb = (setting & 0xFF00) >> 8 + self.regwrite('FILTER_1_LSB', lsb) + self.regwrite('FILTER_1_MSB', msb) + return None + + def set_filter_2(self, setting=DEFAULT_CTRL_STAT['FILTER_2']): + self.regwrite('FILTER_2', setting) + return None + + def set_sens_factor(self, sens_factor): + """ + Set sensitivity enhancement factor to `sens_factor`. + `sens_factor` can be: + + * a positive float < 3, + * an int, in %, e.g 100 % => 1.0 130 % => 1.3 + * a hex int, in %, e.g. 0x64 == 100 => 1.0 + """ + if sens_factor > 3: + # assume we're giving it as hex register value or int or anyway x100 + sens_factor *= 0.01 + self.log(logging.DEBUG, f'Setting sensitivity factor to {sens_factor}') + regval = int(sens_factor * 100) + self.regwrite('SENS_FACTOR', regval) + return None + + def set_offset_corr(self, offset_in_Kelvin): + """Set an offset across entire frame in Kelvin; in increment of 0.05 K""" + assert offset_in_Kelvin <= 6.35 and offset_in_Kelvin >= -6.4 + n = int(round(offset_in_Kelvin / T_OFFSET_UNIT)) + if n < 0: + regval = 256 - abs(n) + else: + regval = n + self.log(logging.DEBUG, 'Setting temperature offset, [K]: {}, regvalue: {}'. + format(offset_in_Kelvin, regval)) + self.regwrite('OFFSET_CORR', regval) + return None + + def get_camera_type(self): + """Read SenXor_Type register""" + return self.regread('SENXOR_TYPE') + + def get_module_type(self): + """Read Module_Type register""" + return self.regread('MODULE_TYPE') + + def get_camera_id(self): + """Read SenXor_ID register; Return string Year.Week.Fab.SerNum + """ + uid = [] + for i in range(0, MI48_SENXOR_ID_LEN): + uid.append(self.regread('SENXOR_ID_{}'.format(i))) + uid_hex = bytearray(uid).hex() + year = 2000 + uid[0] + week = uid[1] + fab = uid[2] + sernum_hex = bytearray(uid[3:]).hex() + sernum = (uid[3] << 16) + (uid[4] << 8) + uid[5] + uid = '{}.{}.{}.{}'.format(year, week, fab, sernum) + uid_hexsn = '{}.{}.{}.{}'.format(year, week, fab, sernum_hex) + return uid, uid_hex, uid_hexsn + + def get_fw_version(self): + """Get maj.min.build of EVK FW; return as a string""" + fwv = self.regread('FW_VERSION_1') + fwb = self.regread('FW_VERSION_2') + fwv_major = (fwv >> 4) & 0xF + fwv_minor = fwv & 0xF + fwv_build = fwb + return '{}.{}.{}'.format(fwv_major, fwv_minor, fwv_build) + + def enable_user_flash(self): + self.regwrite('FLASH_CTRL', 0x01) + + def disable_user_flash(self): + self.regwrite('FLASH_CTRL', 0x00) + + def get_compensation_params(self, npar=4, base_addr=0): + """ + Read the compensation parameters stored in the MI48 flash. + + Return a list of `npar` floats, where `npar` is the number of + parameters. + The parameters are stored at `base_addr` in the user + flash space, using little-endian order, i.e. LSB to 0x00 etc., + in the form of 4--byte IEEE-754 numbers. + """ + params = [] + for i in range(npar): + # Parameters are stored as IEEE-754 floats, i.e. 4-bytes, + # little endian. + # When we read the MI48 we get back unsigned int for each + # byte. + int_list = [] + for j in range(4): + flash_addr = base_addr + 4 * i + j + int_list.append(self.regread(flash_addr)) + time.sleep(1) + byte_array = array.array('B', int_list) + params.append(struct.unpack(' stop_timeout: # in ms + self.log(logging.DEBUG, + 'Camera module failed to stop in {:.0f} ms'.\ + format(1.e3 * stop_timeout)) + return mode + self.log(logging.DEBUG, 'Camera module stopped in {:.0f} ms.'. + format(1.e3 * delay)) + return mode + + def clear_interface_buffers(self): + """may need to overload this""" + for intface in self.interfaces: + intface.reset_input_buffer() + intface.reset_output_buffer() + + def close_interfaces(self): + """may need to overaload this""" + for intface in self.interfaces: + intface.close() + + def stop(self, poll_timeout=0.1, stop_timeout=0.5): + """Stop capture and close ports to device""" + # stop external device first + self.log(logging.DEBUG, 'Stopping camera module') + self.stop_capture(poll_timeout=poll_timeout, + stop_timeout=stop_timeout) + # close the interfaces + # purge interface buffers to make sure we don't + # accidentally read/write something old next time we start + self.log(logging.DEBUG, 'Closing host interfaces') + self.clear_interface_buffers() + self.close_interfaces() + return None + + def __repr__(self): + _s = [] + _s.append('Camera Type {} (type {}), resolution {}, max FPS {}'. + format(self.camera_name, self.camera_type, + self.fpa_shape, self.maxfps)) + _s.append('FW version {}'.format(self.fw_version)) + _s.append('SenXor ID {}'.format(self.camera_id)) + return '\n'.join(_s) + +def get_reg_name(addr): + """Given a register address, return its name""" + for key, val in regmap.items(): + if val == addr: return key + return 'Unknown reg: 0x{:02X}'.format(addr) + +def format_header(hdr): + """Format frame header to represent in log messages""" + s = "FID{:6d} time{:8d} V_dd {:5.3f} T_SX {:5.2f}".\ + format(hdr['frame_counter'], hdr['timestamp'],\ + hdr['senxor_vdd'], hdr['senxor_temperature']) + s += '\n' + return s + +def format_framestats(data): + """Format data frame stats to represent in log messages""" + s = "Min {:6.1f} Max {:6.1f} Avg {:5.1f} Std {:3.1f}".\ + format(data.min(), data.max(), data.mean(), + data.astype(np.float64).std()) + return s + + diff --git a/src/thermalCamera/plots.py b/src/thermalCamera/plots.py new file mode 100644 index 00000000..2b14c088 --- /dev/null +++ b/src/thermalCamera/plots.py @@ -0,0 +1,310 @@ +# Copyright (C) Meridian Innovation Ltd. Hong Kong, 2019 - 2022. All rights reserved. +# +import logging +import numpy as np +import matplotlib +import matplotlib.pyplot as plt +import matplotlib.patches as patches +import matplotlib.path as path +import cv2 as cv +matplotlib.use('TkAgg') +logging.getLogger('matplotlib.font_manager').disabled = True +logging.getLogger('matplotlib').setLevel(logging.WARNING) + + +def get_hist_patch(data, *args, **kwargs): + """Calculate counts and bins and return a patch for drawing""" + + # extract some args for the rendering of histogram + # the rest -- pass to np.hystogram + hist_edge_color = kwargs.pop('hist_edge_color', 'yellow') + hist_face_color = kwargs.pop('hist_face_color', 'green') + hist_face_alpha = kwargs.pop('hist_face_alpha', 0.5) + + # get histogram data: counts and bin values + counts, bins = np.histogram(data, *args, **kwargs) + + # get the corners of the rectangles for the histogram + left = np.array(bins[:-1]) + right = np.array(bins[1:]) + bottom = np.zeros(len(left)) + top = bottom + counts + nrects = len(left) + + # Generate paths that can be visualised by an artist + # The path must be defined in terms of vertexes and codes (c.f. matplotlib.path) + # The relevant codes here are: LINETO, MOVETO, CLOSEPOLY, because + # we're dealing with rectangular patches, one per bin + # For each rectangle, we need to: + # * move to left,bottom + # * draw three lines in desired colour, and + # * close the polygon. + # This is equivalent to having 5 vertexes that define the path + nverts = nrects * 5 + # Each virtex has 2 coordinates: (x,y), hence a 2D array is needed + # and each virtex requires an integer code: + # Use stride of 5 to address corresponding vertex and code of each rectangle + verts = np.zeros((nverts, 2)) + verts[0::5, 0] = left + verts[0::5, 1] = bottom + verts[1::5, 0] = left + verts[1::5, 1] = top + verts[2::5, 0] = right + verts[2::5, 1] = top + verts[3::5, 0] = right + verts[3::5, 1] = bottom +# verts[4::5] = verts[0::5] + codes = np.ones(nverts, dtype=np.uint8) * path.Path.LINETO + codes[0::5] = path.Path.MOVETO + codes[4::5] = path.Path.CLOSEPOLY + + # compose that line paths and enclosed patches + _path = path.Path(verts, codes) + patch = patches.PathPatch(_path, facecolor=hist_face_color, + edgecolor=hist_edge_color, alpha=hist_face_alpha) + return patch + +def get_image(figure): + """Transform a matplotlib `figure` to an image to be displayed by `opencv.imshow`""" + # redraw the canvas + canvas = figure.canvas + canvas.draw() + # convert canvas to image + img = np.frombuffer(canvas.tostring_rgb(), dtype=np.uint8) + img = img.reshape(canvas.get_width_height()[::-1] + (3,)) + # matplotlib img is rgb, convert to opencv's default bgr + img = cv.cvtColor(img, cv.COLOR_RGB2BGR) + return img + +class Histogram: + """Continuously updatable histogram plot""" + + def __init__(self, data, figsize=(6,5), param=None): + # create a figure to be updated + if figsize[1] > 20: + # assume figsize is given in pixels if heigth > 20 + # to translate to inches, we need the screen DPI + try: + self.dpi = param.get('dpi', 100) + except TypeError: + # param is None + self.dpi = 100 + figsize = [x / self.dpi for x in figsize] + self.fig, self.ax = plt.subplots(nrows=1, ncols=1, figsize=figsize) + self.fig.tight_layout() + if param.get('xlabel', None) is not None: + self.ax.set_xlabel(param.pop('xlabel')) + if param.get('ylabel', None) is not None: + self.ax.set_ylabel(param.pop('ylabel')) + self.ax.autoscale(enable=False) + if param.get('xlim', None) is not None: + self.ax.set_xlim(param.pop('xlim')) + if param.get('ylim', None) is not None: + self.ax.set_ylim(param.pop('ylim')) + if param.get('xticks', None) is not None: + self.ax.xaxis.set_ticks(param.pop('xticks')) + if param.get('yticks', None) is not None: + self.ax.yaxis.set_ticks(param.pop('yticks')) + self.labels = param.pop('labels', None) + self.data = data + self.patch = get_hist_patch(data, **param) + self.nbins = param.get('bins', 50) + self.ax.add_patch(self.patch) + + def update(self, data=None): + if data is not None: + self.data = data + patch = get_hist_patch(self.data, bins=self.nbins) + self.patch.set_path(patch.get_path()) + # return list is required by FuncAnimation + return self.patch, + + def get_image(self): + """Return an image to be displayed by `OpenCV.imshow`""" + # redraw the canvas + return get_image(self.ax.figure) + + +class LinePlot(): + """Lie plot of one or more variables on same Y axis""" + def __init__(self, data, figsize=(6,5), param=None): + # create a figure to be updated + if figsize[1] > 20: + # assume figsize is given in pixels if heigth > 20 + # to translate to inches, we need the screen DPI + try: + self.dpi = param.get('dpi', 100) + except TypeError: + # param is None + self.dpi = 100 + figsize = [x / self.dpi for x in figsize] + self.fig, self.ax = plt.subplots(nrows=1, ncols=1, figsize=figsize) + self.ax.autoscale(enable=True) + if param.get('xlabel', None) is not None: + self.ax.set_xlabel(param['xlabel']) + if param.get('ylabel', None) is not None: + self.ax.set_ylabel(param['ylabel']) + if param.get('xlim', None) is not None: + self.ax.set_xlim(param['xlim']) + if param.get('ylim', None) is not None: + self.ax.set_ylim(param['ylim']) + if param.get('xticks', None) is not None: + self.ax.xaxis.set_ticks(param['xticks']) + if param.get('yticks', None) is not None: + self.ax.yaxis.set_ticks(param['yticks']) + # for convenience put grid and labels on each side of y-axis + # self.ax.tick_params(labelright=True, right=True) + self.ax.grid(True) + self.ax.figure.tight_layout() + self.labels = param.get('labels', None) + # establish a reference to a data object that + # is updated outside, but is accessible to self.update + self.data = data + # define the plot object + self.markers = ['x', '+', 'x', '+', 's', 'd'] + self.markers = ['+']*6 + self.setup(nvars=self.data.shape[1]-1) + + def setup(self, nvars=1): + """Setup an empty scatter plot which will be dynamically updated""" + self.lines = [] + for i in range(nvars): + lines = self.ax.plot(self.data[:, 0], self.data[:, i+1], + marker=self.markers[i], ms=3, linestyle='None') + self.lines.append(lines[0]) + # print('Initilizing plot') + # print(self.lines, len(self.lines)) + if self.labels is not None: + self.ax.legend(self.lines, self.labels, loc=3, ncol=2) + self.fig.tight_layout() + return self.lines, + + def update(self, data=None): + """Update the positions, colors and size of the scatter points""" + #t0 = time.time() + if data is None: + data = self.data + for i, line in enumerate(self.lines): + # update only ydata, xdata is set in self.setup() + line.set_ydata(data[:, i+1]) + #cost = time.time() - t0 + #print('Plot update: {} ms'.format(cost*1.e3)) + return self.lines, + + def get_image(self): + """Return an image to be displayed by `OpenCV.imshow`""" + # redraw the canvas + return get_image(self.ax.figure) + + +class LivePlot2Y(): + """Live plot of variables on 2 Y-axis""" + def __init__(self, data, data2, figsize=(6,5), param=None): + # create a figure to be updated + if figsize[1] > 20: + # assume figsize is given in pixels if heigth > 20 + # to translate to inches, we need the screen DPI + try: + self.dpi = param.get('dpi', 100) + except TypeError: + # param is None + self.dpi = 100 + figsize = [x / self.dpi for x in figsize] + self.fig, self.ax = plt.subplots(nrows=1, ncols=1, + figsize=figsize) + self.ax.autoscale(enable=True) + self.ax2 = self.ax.twinx() + # X axis + if param.get('xlabel', None) is not None: + self.ax.set_xlabel(param['xlabel']) + if param.get('xlim', None) is not None: + self.ax.set_xlim(param['xlim']) + if param.get('xticks', None) is not None: + self.ax.xaxis.set_ticks(param['xticks']) + # Y axis (left) + if param.get('ylabel', None) is not None: + self.ax.set_ylabel(param['ylabel']) + if param.get('ylim', None) is not None: + self.ax.set_ylim(param['ylim']) + if param.get('yticks', None) is not None: + self.ax.yaxis.set_ticks(param['yticks']) + # Y2 axis (right) + if param.get('y2label', None) is not None: + self.ax2.set_ylabel(param['y2label']) + if param.get('y2lim', None) is not None: + self.ax2.set_ylim(param['y2lim']) + if param.get('y2ticks', None) is not None: + self.ax2.yaxis.set_ticks(param['y2ticks']) + # Line labels and colors + self.labels = param.get('labels', None) + self.colors = param.get('colors', None) + # Establish a reference to a data object that + # is updated outside, but is accessible to self.update + # Note: data has X and left-Y items, data2 has only right-Y items + self.data = data + self.data2 = data2 + # define the plot object + self.setup(nvars=self.data.shape[1]-1, + nvars2=self.data2.shape[1]) + self.fig.tight_layout() + + def setup(self, nvars=1, nvars2=1): + """Setup an empty scatter plot which will be dynamically updated""" + self.lines = [] + self.lines2 = [] + # ax: note that data contains X values as first column + for i in range(nvars): + lines = self.ax.plot(self.data[:, 0], self.data[:, i+1], + marker='o', ms=3, linestyle='None') + self.lines.append(lines[0]) + # ax2: data2 contains only Y values; reuse X from data + for i in range(nvars2): + lines = self.ax2.plot(self.data[:, 0], self.data2[:, i], + marker='+', ms=5, linestyle='None') + self.lines2.append(lines[0]) + if self.labels is not None: + self.ax.legend(self.lines, self.labels[:len(self.lines)], loc=2) + self.ax2.legend(self.lines2, self.labels[len(self.lines):], loc=1) + if self.colors is not None: + assert len(self.colors) == len(self.lines) + len(self.lines2) + for i, line in enumerate(self.lines): + line.set_color(self.colors[i]) + for i, line in enumerate(self.lines2): + line.set_color(self.colors[i+len(self.lines)]) + self.ax.legend(self.lines, self.labels[:len(self.lines)], loc=2) + self.ax2.legend(self.lines2, self.labels[len(self.lines):], loc=1) + return self.lines, self.lines2 + + def update(self, *args, **kwargs): + """Update the positions, colors and size of the scatter points""" + #t0 = time.time() + try: + data = kwargs['data'] + data2 = kwargs['data2'] + except KeyError: + data = self.data + data2 = self.data2 + for i, line in enumerate(self.lines): + # update only ydata, xdata is set in self.setup() + line.set_ydata(data[:, i+1]) + for i, line in enumerate(self.lines2): + # update only ydata, xdata is set in self.setup() + line.set_ydata(data2[:, i]) + # must return a list of artists to use 'blit=True' + #cost = time.time() - t0 + #print('Plot update: {} ms'.format(cost*1.e3)) + return self.lines, self.lines2 + + def get_image(self): + """Return an image ready to be displayed by OpenCV""" + # redraw the canvas + canvas = self.ax.figure.canvas + canvas.draw() + # convert canvas to image + img = np.frombuffer(canvas.tostring_rgb(), dtype=np.uint8) + img = img.reshape(canvas.get_width_height()[::-1] + (3,)) + # matplotlib img is rgb, convert to opencv's default bgr + img = cv.cvtColor(img, cv.COLOR_RGB2BGR) + return img + + diff --git a/src/thermalCamera/thermal_camera.py b/src/thermalCamera/thermal_camera.py new file mode 100644 index 00000000..695ce5e5 --- /dev/null +++ b/src/thermalCamera/thermal_camera.py @@ -0,0 +1,219 @@ +import sys +import os +import signal +import logging +import threading +import numpy as np +import cv2 as cv +from PyQt5.QtCore import QThread, pyqtSignal +from .mi48 import MI48, format_header, format_framestats # Connects and communicates with the MI48 thermal camera +from .utils import data_to_frame, remap, cv_filter, RollingAverageFilter, connect_senxor + + +def replace_dead_pixels(frame, min_val=0, max_val=220): + """Replace dead pixels with the average of surrounding 48 pixels.""" + for i in range(3, frame.shape[0] - 3): + for j in range(3, frame.shape[1] - 3): + if frame[i, j] < min_val or frame[i, j] > max_val: + surrounding_pixels = [ + frame[i-3, j-3], frame[i-3, j-2], frame[i-3, j-1], frame[i-3, j], frame[i-3, j+1], frame[i-3, j+2], frame[i-3, j+3], # top row + frame[i-2, j-3], frame[i-2, j-2], frame[i-2, j-1], frame[i-2, j], frame[i-2, j+1], frame[i-2, j+2], frame[i-2, j+3], # second row + frame[i-1, j-3], frame[i-1, j-2], frame[i-1, j-1], frame[i-1, j], frame[i-1, j+1], frame[i-1, j+2], frame[i-1, j+3], # third row + frame[i, j-3], frame[i, j-2], frame[i, j-1], frame[i, j+1], frame[i, j+2], frame[i, j+3], # middle row (excluding center) + frame[i+1, j-3], frame[i+1, j-2], frame[i+1, j-1], frame[i+1, j], frame[i+1, j+1], frame[i+1, j+2], frame[i+1, j+3], # fifth row + frame[i+2, j-3], frame[i+2, j-2], frame[i+2, j-1], frame[i+2, j], frame[i+2, j+1], frame[i+2, j+2], frame[i+2, j+3], # sixth row + frame[i+3, j-3], frame[i+3, j-2], frame[i+3, j-1], frame[i+3, j], frame[i+3, j+1], frame[i+3, j+2], frame[i+3, j+3] # bottom row + ] + frame[i, j] = np.mean(surrounding_pixels) + return frame + +class ThermalCamera(QThread): + thermal_camera_frame_ready = pyqtSignal(np.ndarray, dict) + max_temp_signal = pyqtSignal(float) # Add a new signal for the maximum temperature + + def __init__(self, roi=(0, 0, 80, 80), com_port=None): + """ + Initializes the thermal camera with a given ROI and optional COM port. + Runs in a separate thread. + """ + super().__init__() + self.roi = roi # (x1, y1, x2, y2) cam FOV crop + self.com_port = com_port #cam com + self.running = True + self.latest_frame = None + self.lock = threading.Lock() + + self.temps = {f"Section {i}": 0 for i in range(1, 10)} + + # Connect to the MI48 camera. detects automatically + self.mi48, self.connected_port, _ = connect_senxor(src=self.com_port) if self.com_port else connect_senxor() + + # Set camera parameters + self.mi48.set_fps(10) # Set Frames Per Second (FPS) 15-->25 + self.mi48.disable_filter(f1=True, f2=True, f3=True) # Disable all filters + self.mi48.set_filter_1(85) # Set internal filter sett 1 to 85 + self.mi48.enable_filter(f1=True, f2=False, f3=False, f3_ks_5=False) + self.mi48.set_offset_corr(0.0) # Set offset correction to 0.0 + self.mi48.set_sens_factor(100) # Set sensitivity factor to 100 + + # Start streaming + self.mi48.start(stream=True, with_header=True) + + self.dminav = RollingAverageFilter(N=10) + self.dmaxav = RollingAverageFilter(N=10) + + def run(self): + """Runs the camera processing loop asynchronously.""" + while self.running: + self.process_frame() + + def process_frame(self): + """Processes a frame: crops ROI, calculates temperatures, overlays grid and text.""" + try: + data, header = self.mi48.read() + if data is None: + return + + # Calculate min/max temperatures + min_temp = self.dminav(data.min()) + max_temp = self.dmaxav(data.max()) + + # Convert raw data to an image frame + frame = data_to_frame(data, (80, 62), hflip=True) + + # Replace dead pixels + frame = replace_dead_pixels(frame) + + # Clip the frame to the min/max temperatures + frame = np.clip(frame, min_temp, max_temp) + + # Vertical flip and rotate + #frame = cv.flip(frame, 1) + frame = cv.rotate(frame, cv.ROTATE_90_CLOCKWISE) + + # Apply filters + filt_frame = cv_filter(remap(frame), {'blur_ks': 3, 'd': 5, 'sigmaColor': 27, 'sigmaSpace': 27}, #Remaps temperature values for visualization + use_median=True, use_bilat=True, use_nlm=False) #Applies smoothing filters to reduce noise. + + # Crop to ROI + x1, y1, x2, y2 = self.roi + roi_frame = filt_frame[y1:y2, x1:x2] + + # Apply thermal color mapping + roi_frame = cv.applyColorMap(roi_frame, cv.COLORMAP_INFERNO) + + # Resize the frame to make it larger + roi_frame = cv.resize(roi_frame, (600, 600), interpolation=cv.INTER_LINEAR) + + # Draw the 3×3 grid + self.draw_grid(roi_frame) + + # Calculate section temperatures + temps = self.calculate_temperatures(frame, x1, y1, x2, y2) + + # Overlay text on the image + self.overlay_text(roi_frame, temps) + + # Draw a white rectangle around the point of maximum temperature + max_temp_loc = np.unravel_index(np.argmax(frame, axis=None), frame.shape) + max_temp_loc = (max_temp_loc[1] - x1, max_temp_loc[0] - y1) # Adjust for ROI + max_temp_loc = (max_temp_loc[0] * 600 // (x2 - x1), max_temp_loc[1] * 600 // (y2 - y1)) # Scale to resized frame + cv.rectangle(roi_frame, (max_temp_loc[0] - 5, max_temp_loc[1] - 5), (max_temp_loc[0] + 5, max_temp_loc[1] + 5), (128, 128, 128), 1) + + # Emit the maximum temperature after dead pixel correction + self.max_temp_signal.emit(frame.max()) + + # Store the latest frame for streaming + with self.lock: + self.latest_frame = roi_frame + + self.thermal_camera_frame_ready.emit(roi_frame, temps) # Emit the frame for display + except Exception as e: + logging.error(f"Error processing frame: {e}") + + def draw_grid(self, frame): + """Draws a 3×3 grid overlay on the thermal feed.""" + try: + h, w = frame.shape[:2] # Get frame dimensions + step_w, step_h = w // 3, h // 3 # Divide width and height into 3 sections to get 3x3 grid + + # Draw vertical lines + for i in range(1, 3): + x = i * step_w + cv.line(frame, (x, 0), (x, h), (128, 128, 128), 1) + + # Draw horizontal lines + for i in range(1, 3): + y = i * step_h + cv.line(frame, (0, y), (w, y), (128, 128, 128), 1) + except Exception as e: + logging.error(f"Error drawing grid: {e}") + + def calculate_temperatures(self, frame, x1, y1, x2, y2): + """Calculates the average temperatures for 9 sections in a 3x3 grid.""" + try: + w, h = x2 - x1, y2 - y1 + section_w, section_h = w // 3, h // 3 # Divide into 3x3 grid + + # Define sections + sections = { + "top-left": frame[y1:y1+section_h, x1:x1+section_w], + "top-center": frame[y1:y1+section_h, x1+section_w:x1+2*section_w], + "top-right": frame[y1:y1+section_h, x1+2*section_w:x2], + "middle-left": frame[y1+section_h:y1+2*section_h, x1:x1+section_w], + "middle-center": frame[y1+section_h:y1+2*section_h, x1+section_w:x1+2*section_w], + "middle-right": frame[y1+section_h:y1+2*section_h, x1+2*section_w:x2], + "bottom-left": frame[y1+2*section_h:y2, x1:x1+section_w], + "bottom-center": frame[y1+2*section_h:y2, x1+section_w:x1+2*section_w], + "bottom-right": frame[y1+2*section_h:y2, x1+2*section_w:x2] + } + + # Calculate average temperature for each section + self.temps = {name: np.mean(region) for name, region in sections.items()} + return self.temps + except Exception as e: + logging.error(f"Error calculating temperatures: {e}") + return self.temps + + def get_avg_temperatures(self): + """Returns the latest average temperatures for the 9 sections.""" + return self.temps + + def overlay_text(self, frame, temps): + """Overlays temperature values on the image.""" + try: + h, w = frame.shape[:2] + section_w, section_h = w // 3, h // 3 # Grid size + + # Set positions to display average temperature + positions = { + "top-left": (section_w // 4, section_h // 2), + "top-center": (w // 2 - 50 , section_h // 2), + "top-right": (w - section_w // 2 - section_w // 4, section_h // 2), + "middle-left": (section_w // 4, h // 2 ), + "middle-center": (w // 2 - 50 , h // 2 ), + "middle-right": (w - section_w // 2 - 50 , h // 2 ), + "bottom-left": (section_w // 4, h - section_h // 2), + "bottom-center": (w // 2 - 50, h - section_h // 2), + "bottom-right": (w - section_w // 2 - 50 , h - section_h // 2) + } + + # Overlay text for each section + for section, temp in temps.items(): + x, y = positions[section] + cv.putText(frame, f"{temp:.2f}C", (x, y), cv.FONT_HERSHEY_SIMPLEX, 1, (128, 128, 128), 1) + + # # Draw section labels + # for i, (section, (x, y)) in enumerate(positions.items(), 1): + # label_x = (i - 1) % 3 * section_w + section_w // 2 + # label_y = (i - 1) // 3 * section_h + section_h // 2 + # cv.putText(frame, f"{section}", (label_x, label_y), cv.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1) + except Exception as e: + logging.error(f"Error overlaying text: {e}") + + def stop(self): + """Stops the camera.""" + self.running = False + self.mi48.stop() + cv.destroyAllWindows() + diff --git a/src/thermalCamera/utils.py b/src/thermalCamera/utils.py new file mode 100644 index 00000000..9503b39c --- /dev/null +++ b/src/thermalCamera/utils.py @@ -0,0 +1,1073 @@ +# Copyright (C) Meridian Innovation Ltd. Hong Kong, 2020. All rights reserved. + +import time +import os +import logging +import math +import itertools +from functools import partial +from pathlib import Path +import operator +import numpy as np +import cv2 as cv +import cmapy +from serial.tools import list_ports +from serial import Serial, SerialException +from .mi48 import MI48 +from .interfaces import MI_VID, MI_PIDs, USB_Interface + +list_ironbow_b = [0,6,12,18,27,38,49,59,64,68,73,78,82,86,90,94,98,102,105,109,112,115,119,122,124,127,129,132,134,136,138,140,142,145,147,148,150,151,152,153,154,155,157,158,159,160,161,163,163,164,165,166,166,167,167,167,167,167,166,166,166,165,165,165,165,164,164,164,163,162,161,160,160,160,158,157,156,155,153,152,151,150,148,147,146,145,143,142,141,140,138,136,134,132,130,127,125,123,121,119,118,116,114,112,110,108,106,104,102,100,98,96,94,92,90,88,86,84,82,80,78,75,73,71,69,67,65,63,61,59,57,55,53,51,49,48,46,44,42,40,38,36,34,32,31,29,27,25,24,22,21,20,18,17,16,15,13,12,11,9,8,7,6,4,3,2,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,3,5,6,7,9,10,12,13,14,16,17,20,23,26,28,31,34,37,39,42,45,48,50,53,56,59,62,66,70,74,78,82,86,91,96,101,106,111,115,120,125,130,135,140,146,152,158,164,171,178,185,192,201,210,219,229,237,243,248,251,254] +list_ironbow_g = [0,0,0,0,0,0,0,0,0,1,2,3,4,3,3,2,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,2,2,2,2,3,3,3,4,5,6,7,8,9,10,11,12,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,30,31,32,33,34,35,36,37,39,40,42,43,45,47,48,50,51,53,54,56,58,59,61,62,64,65,67,69,70,72,73,75,76,78,80,81,83,84,86,88,89,91,93,95,96,98,100,102,103,105,107,109,110,112,114,116,117,119,121,122,124,126,128,129,131,133,134,136,138,139,141,143,145,146,148,150,151,153,155,156,158,160,161,163,165,167,168,170,172,173,175,177,178,180,182,184,185,187,188,190,191,193,194,196,197,199,200,202,203,205,206,208,209,211,212,214,215,216,217,219,220,221,223,224,225,227,228,229,231,232,233,235,235,236,236,237,238,239,240,241,242,243,244,245,246,247,248,249,249,250,251,252,253,254,255,255,255,255,255,254,254,254,254,254] +list_ironbow_r = [0,0,0,0,0,0,0,0,0,0,0,0,0,2,5,9,12,16,19,23,26,29,33,36,39,43,46,49,52,54,57,60,63,66,69,71,74,77,80,83,85,88,91,94,96,99,102,105,107,110,112,115,117,120,122,124,127,129,131,133,136,138,140,142,145,147,149,151,154,156,158,160,161,163,165,167,169,170,172,174,176,178,179,181,183,185,187,189,190,192,194,195,196,198,199,201,202,204,205,206,208,209,211,212,213,214,215,216,217,218,219,221,222,223,224,225,226,227,228,229,230,231,232,234,235,236,237,238,239,240,241,242,243,243,244,245,245,246,247,248,248,249,250,250,251,251,252,253,253,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,254,254,254,253,253,252,252,252,251,251,250,250,250,250,250,249,248,247,246,246,245,245,245,246,247,249,251,254] +lut_ironbow = np.zeros((256, 1, 3), dtype=np.uint8) +lut_ironbow[:,:,0] = np.array(list_ironbow_b).reshape(256,1) +lut_ironbow[:,:,1] = np.array(list_ironbow_g).reshape(256,1) +lut_ironbow[:,:,2] = np.array(list_ironbow_r).reshape(256,1) + +list_rainbow2 = [ 1, 3, 74, 0, 3, 74, 0, 3, 75, 0, 3, 75, 0, 3, 76, 0, 3, 76, 0, 3, 77, 0, 3, 79, 0, 3, 82, 0, 5, 85, 0, 7, 88, 0, 10, 91, 0, 14, 94, 0, 19, 98, 0, 22, 100, 0, 25, 103, 0, 28, 106, 0, 32, 109, 0, 35, 112, 0, 38, 116, 0, 40, 119, 0, 42, 123, 0, 45, 128, 0, 49, 133, 0, 50, 134, 0, 51, 136, 0, 52, 137, 0, 53, 139, 0, 54, 142, 0, 55, 144, 0, 56, 145, 0, 58, 149, 0, 61, 154, 0, 63, 156, 0, 65, 159, 0, 66, 161, 0, 68, 164, 0, 69, 167, 0, 71, 170, 0, 73, 174, 0, 75, 179, 0, 76, 181, 0, 78, 184, 0, 79, 187, 0, 80, 188, 0, 81, 190, 0, 84, 194, 0, 87, 198, 0, 88, 200, 0, 90, 203, 0, 92, 205, 0, 94, 207, 0, 94, 208, 0, 95, 209, 0, 96, 210, 0, 97, 211, 0, 99, 214, 0, 102, 217, 0, 103, 218, 0, 104, 219, 0, 105, 220, 0, 107, 221, 0, 109, 223, 0, 111, 223, 0, 113, 223, 0, 115, 222, 0, 117, 221, 0, 118, 220, 1, 120, 219, 1, 122, 217, 2, 124, 216, 2, 126, 214, 3, 129, 212, 3, 131, 207, 4, 132, 205, 4, 133, 202, 4, 134, 197, 5, 136, 192, 6, 138, 185, 7, 141, 178, 8, 142, 172, 10, 144, 166, 10, 144, 162, 11, 145, 158, 12, 146, 153, 13, 147, 149, 15, 149, 140, 17, 151, 132, 22, 153, 120, 25, 154, 115, 28, 156, 109, 34, 158, 101, 40, 160, 94, 45, 162, 86, 51, 164, 79, 59, 167, 69, 67, 171, 60, 72, 173, 54, 78, 175, 48, 83, 177, 43, 89, 179, 39, 93, 181, 35, 98, 183, 31, 105, 185, 26, 109, 187, 23, 113, 188, 21, 118, 189, 19, 123, 191, 17, 128, 193, 14, 134, 195, 12, 138, 196, 10, 142, 197, 8, 146, 198, 6, 151, 200, 5, 155, 201, 4, 160, 203, 3, 164, 204, 2, 169, 205, 2, 173, 206, 1, 175, 207, 1, 178, 207, 1, 184, 208, 0, 190, 210, 0, 193, 211, 0, 196, 212, 0, 199, 212, 0, 202, 213, 1, 207, 214, 2, 212, 215, 3, 215, 214, 3, 218, 214, 3, 220, 213, 3, 222, 213, 4, 224, 212, 4, 225, 212, 5, 226, 212, 5, 229, 211, 5, 232, 211, 6, 232, 211, 6, 233, 211, 6, 234, 210, 6, 235, 210, 7, 236, 209, 7, 237, 208, 8, 239, 206, 8, 241, 204, 9, 242, 203, 9, 244, 202, 10, 244, 201, 10, 245, 200, 10, 245, 199, 11, 246, 198, 11, 247, 197, 12, 248, 194, 13, 249, 191, 14, 250, 189, 14, 251, 187, 15, 251, 185, 16, 252, 183, 17, 252, 178, 18, 253, 174, 19, 253, 171, 19, 254, 168, 20, 254, 165, 21, 254, 164, 21, 255, 163, 22, 255, 161, 22, 255, 159, 23, 255, 157, 23, 255, 155, 24, 255, 149, 25, 255, 143, 27, 255, 139, 28, 255, 135, 30, 255, 131, 31, 255, 127, 32, 255, 118, 34, 255, 110, 36, 255, 104, 37, 255, 101, 38, 255, 99, 39, 255, 93, 40, 255, 88, 42, 254, 82, 43, 254, 77, 45, 254, 69, 47, 254, 62, 49, 253, 57, 50, 253, 53, 52, 252, 49, 53, 252, 45, 55, 251, 39, 57, 251, 33, 59, 251, 32, 60, 251, 31, 60, 251, 30, 61, 251, 29, 61, 251, 28, 62, 250, 27, 63, 250, 27, 65, 249, 26, 66, 249, 26, 68, 248, 25, 70, 248, 24, 73, 247, 24, 75, 247, 25, 77, 247, 25, 79, 247, 26, 81, 247, 32, 83, 247, 35, 85, 247, 38, 86, 247, 42, 88, 247, 46, 90, 247, 50, 92, 248, 55, 94, 248, 59, 96, 248, 64, 98, 248, 72, 101, 249, 81, 104, 249, 87, 106, 250, 93, 108, 250, 95, 109, 250, 98, 110, 250, 100, 111, 251, 101, 112, 251, 102, 113, 251, 109, 117, 252, 116, 121, 252, 121, 123, 253, 126, 126, 253, 130, 128, 254, 135, 131, 254, 139, 133, 254, 144, 136, 254, 151, 140, 255, 158, 144, 255, 163, 146, 255, 168, 149, 255, 173, 152, 255, 176, 153, 255, 178, 155, 255, 184, 160, 255, 191, 165, 255, 195, 168, 255, 199, 172, 255, 203, 175, 255, 207, 179, 255, 211, 182, 255, 216, 185, 255, 218, 190, 255, 220, 196, 255, 222, 200, 255, 225, 202, 255, 227, 204, 255, 230, 206, 255, 233, 208 ] +lut_rainbow2 = np.zeros((256, 1, 3), dtype=np.uint8) +lut_rainbow2[:,:,0] = np.array(list_rainbow2[2::3]).reshape(256,1) +lut_rainbow2[:,:,1] = np.array(list_rainbow2[1::3]).reshape(256,1) +lut_rainbow2[:,:,2] = np.array(list_rainbow2[0::3]).reshape(256,1) + + +colormaps = { + 'autumn': cv.COLORMAP_AUTUMN, + 'bone': cv.COLORMAP_BONE, + 'jet': cv.COLORMAP_JET, + 'winter': cv.COLORMAP_WINTER, + 'rainbow': cv.COLORMAP_RAINBOW, + 'ocean': cv.COLORMAP_OCEAN, + 'summer': cv.COLORMAP_SUMMER, + 'spring': cv.COLORMAP_SPRING, + 'cool': cv.COLORMAP_COOL, + 'hsv': cv.COLORMAP_HSV, + 'pink': cv.COLORMAP_PINK, + 'hot': cv.COLORMAP_HOT, + 'parula': cv.COLORMAP_PARULA, + 'magma': cv.COLORMAP_MAGMA, + 'inferno': cv.COLORMAP_INFERNO, + 'plasma': cv.COLORMAP_PLASMA, + 'viridis': cv.COLORMAP_VIRIDIS, + 'cividis': cv.COLORMAP_CIVIDIS, + 'twilight': cv.COLORMAP_TWILIGHT, + 'twilight_shifted': cv.COLORMAP_TWILIGHT_SHIFTED, + 'turbo': cv.COLORMAP_TURBO, + 'rainbow2': lut_rainbow2, + 'ironbow': lut_ironbow[-256:], +} + +def connect_senxor(src=None, name=None): + """ + Return an MI48 instance corresponding to the SenXor module connected to `src` + + `src` can be either the name of a virtual comport, e.g. COM6, or a sequential + number, e.g. 0, 1, etc. + if `name` (stirng) is not None, it will be assigned to mi48.name instance, else + the name of the virtual comport will be assigned to the mi48.name. + + Return None, if no connection to SenXor can be established. + """ + cam_index, port_name = None, None + try: + src = int(src) + cam_index = src + except ValueError: + port_name = src.upper() + except TypeError: + pass + mi48 = None + connected_port = None + port_names = [] + for p in list_ports.comports(): + if p.vid == MI_VID and p.pid in MI_PIDs: + port = p.description.split()[-1][1:-1] + port_names.append(port) + if port_name is not None and port_name != port: continue + if cam_index is not None and cam_index != len(port_names)-1: continue + try: + ser = Serial(p.device) + except SerialException: + # port already open + if port_name is not None: + logging.warning(f'{port_name} seems already open') + if cam_index is not None: + logging.warning(f'Thermal image source {cam_index}' + ' seems already open') + continue + usb = USB_Interface(ser) + connected_port = port + if name is None: name = connected_port + mi48 = MI48([usb,usb], name=name, read_raw=False) + return mi48, connected_port, port_names + +def data_to_frame(data, array_shape, hflip=False): + """ + Convert 1D array into nH x nV 2D array corresponding to the FPA. + + Use this func to change orientation to forward looking camera with `hflip`. + """ + # Note that the data coming for the EVK is stored as a 1D array. + # the data.reshape() reconstructs the 2D FPA array shape; + # Note the data ordering is 'F' (fortran-like). + nc, nr = array_shape + if hflip: + # The flipping below realises horisontal flip, assuming that + # the USB port faces the ceiling or the sky, to correct for + # left/right flip in the camera, if necessary. + frame = np.flip(data.reshape(array_shape, order='F').T, 1) + else: + frame = data.reshape(array_shape, order='F').T + return frame.copy() + +def remap(data, new_range=(0, 255), curr_range=None, to_uint8=True): + """ + Remap data from one range to another; return float16. + + This function is critical for working with temperature data and + maintaining accuracy. + + The mapping is a linear transformation: + + l1, h1 = curr_range + + l2, h2 = new_range + + x = (data - l1) / (h1 - l1) + + out = l2 + x * (h2 - l1) + + If `curr_range` is not specified, assume it is defined by the data limits. + If `to_uint8` is true, return an uint8, instead of float16. This is + useful in conjuction with `new_range` being (0, 255), to prepare for + many OpneCV routines which accept only uint8. + """ + lo2, hi2 = new_range + # + if curr_range is None: + lo1 = np.min(data) + hi1 = np.max(data) + else: + lo1, hi1 = curr_range + # + # The relpos below represents the relative position of _data in the + # current range. + # We could potentially manipulate relpos by some function to + # realise non-linear remapping + relpos = (data - lo1) / float(hi1 - lo1) + out = lo2 + relpos * (hi2 - lo2) + # + if to_uint8: + return out.astype('uint8') + else: + return out.astype('float16') + +def get_default_outfile(src_id=None, ext='csv'): + """Yield a timestamped filename with specified extension.""" + ts = time.strftime('%Y%m%d-%H%M%S', time.localtime()) + if src_id is not None: + filename = "{}-{}.{}".format(src_id, ts, ext) + else: + filename = "{}.{}".format(ts, ext) + return filename + + +#fib = [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 600] +def get_colormap(colormap='rainbow2', nc=None): + """ + Return a 256-color LUT corresponding to `colormap`. + + `colormap` is either from open cv, matplotlib or explicitly defined above. + If `nc` is not None, return a quantized colormap with `nc` different colors. + """ + try: + # use defualt opencv maps or explicitly defined above + cmap = colormaps[colormap] + except KeyError: + cmap = cmapy.cmap(colormap) + if nc is not None: + # some names appear in both OpenCV (int), and Matplotlib (LUT) + # attempt to pick up the one from Matplotlib + if isinstance(cmap, int): + try: + cmap = cmapy.cmap(colormap) + except KeyError: + # return non-quantized CV cmap + return cmap + # we need to create a LUT with 256 entries, and these entries + # are indexes in the actual color map; there are `nc` such indexes + nmax = 256 + # number of indexes per color + ipc = nmax // nc + # If nmax is not multiple of nc, then we have to patch up the LUT. + # Below, we choose to patch up with the highest index + delta = nmax % nc + lut = [int((j // ipc) / (nc-1) * (nmax-1)) for j in range(nmax-delta)] + lut += [nmax-1,] * delta + cmap = np.array([cmap[i] for i in lut], dtype='uint8') + return cmap + + +def cv_render(data, title='', resize=(800, 620), colormap='jet', + interpolation=cv.INTER_CUBIC, display=True, n_colors=None): + """ + Render and display a 2D numpy array data of type uint8, using OpenCV. + + Color the image using any of the supported OpenCV colormaps. + Resize the image, ensuring the aspect ratio is maintained. + Use cubic interpolation when upsizing. + + If `display` is true, render the image in an OpenCV-controled window. + Else, return the OpenCV image object. + """ + # colormap may be either a colormap list or a string + cmap = get_colormap(colormap, n_colors) + cvcol = cv.applyColorMap(data, cmap) + if isinstance(resize, tuple) or isinstance(resize, list): + cvresize = cv.resize(cvcol, dsize=resize, + interpolation=interpolation) + else: + cvresize = cv.resize(cvcol, dsize=None, fx=resize, fy=resize, + interpolation=interpolation) + if display: + cv.imshow(title, cvresize) + return cvresize + +def cv_filter(data, parameters=None, use_median=True, use_bilat=True, + use_nlm=False): + """ + Spatial filtering based on a sequence of Meidan, Bilateral and NLM. + + Requires uint8 and returns uint8 data. + + For best results, set both Bilateral and Non-Local Means + flags to true. + + See OpenCV for significance and values of the optional parameters + """ + # default median filter parameters + par = {'blur_ks': 5} + # default bilateral filter parameters + par.update({'d': 7, 'sigmaColor': 23, 'sigmaSpace': 23}) + # default nlmeans filter parameters + par.update({'h': 5, 'templateWindowSize': 5, 'searchWindowSize': 11}) + # update parameters from caller + if parameters is not None: + par.update(parameters) + # do the filtering + filtered = data + if use_median: + #t0 = time.time() + filtered = cv.medianBlur(filtered, par['blur_ks']) + #print('Median cost [ms]: {:8.4f}'.format(time.time() - t0)) + if use_bilat: + #t0 = time.time() + filtered = cv.bilateralFilter(filtered, d=par.get('d'), + sigmaColor=par.get('sigmaColor'), + sigmaSpace=par.get('sigmaSpace')) + #print('Bilateral cost [ms]: {:8.4f}'.format(time.time() - t0)) + + # nlmeans works only on uint8; the result is also uint8 + # so we must normalise and then renormalise + if use_nlm: + t0 = time.time() + filtered = cv.fastNlMeansDenoising(filtered, None, h=par.get('h'), + templateWindowSize=par.get('templateWindowSize'), + searchWindowSize=par.get('searchWindowSize')) + print('NLMeans cost [ms]: {:8.4f}'.format(time.time() - t0)) + return filtered + +def clip_frame(frame, minval=None, maxval=None, c0=0.0, c1=0.0): + """ + Clip the lowest and highest of the `frame`. + + The output range is shrunk from top and bottom by a fraction -- + `c0` and `c1` respectively, of the temperature range, where the temperature + range is defined ither from the intrinsic frame range, or, by the `minval` + and `maxval` if provided. + """ + try: + _range = maxval - minval + except TypeError: + minval, maxval = frame.min(), frame.max() + _range = maxval - minval + m0 = minval + c0 * _range + m1 = maxval - c1 * _range + return np.clip(frame, m0, m1) + + +class TrueAverageFilter: + + def __init__(self, depth): + self.depth = depth + self.buf = np.zeros(shape=(depth,62,80)) + self.counter = 0 + self.ix = 0 + self.av = 0 + + def update(self, new): + if self.counter < self.depth: self.counter += 1 + self.buf[self.ix] = new + self.av = np.sum(self.buf, axis=0) + self.av = self.av / self.counter + self.ix += 1 + if self.ix > self.depth - 1: self.ix = 0 + return self.av + + def __call__(self, new): + return self.update(new) + + +class RollingAverageFilter: + + def __init__(self, N=4): + """ + Rolling average filter over ``N`` frames. + + Usage: + + # establish rolling average over 20 frames + min_filter = RollingAverageFilter(N=20) + ... + + min_temp = min_filter(measured_min) + """ + self.N = N + self.count = 0 + self.av = 0 + self.update = self._update_0 + + def _update_0(self, new): + self.count += 1 + self.av += 1. / self.count * (new - self.av) + if not self.count < self.N: + self.update = self._update_1 + + def _update_1(self, new): + self.av += 1. / self.N * (new - self.av) + + def clear(self): + self.__init__(self.N) + + def __call__(self, new): + self.update(new) + return self.av + + +class FibonacciAverageFilter: + + fib = [0, 1, 2, 3, 5, 8, 13, 21, 34, 55] + + def __init__(self, initial, N=6, i_start=1): + """ + Fibonacci-weighted average filter over ``N`` frames. + + Usage: + + # establish rolling average over 20 frames + min_filter = RollingAverageFilter(initial_min, N=20) + ... + + min_temp = min_filter(measured_min) + """ + self.N = N + self.s = i_start + self.p = i_start + N + self.frames = [initial] * self.N + w = np.array(self.fib[self.s: self.p]) + self.weights = np.asarray(w)/np.sum(w) + + def __call__(self, new): + """Update the rolling average estimate""" + self.frames = self.frames[1:] + [new] + #print(len(self.frames)) + self.frames[-1] = np.sum([w*f for w,f in + zip(self.weights, self.frames)], axis=0) + + #print(self.frames[-1].shape) + return self.frames[-1] + + +class KeyboardHandler: + """ + Add a handler for a specific key to allow interactive change of + parameters which are stored in a dictionary. + """ + def __init__(self, pardict): + """ + Keyboard handler that allows for interactive change of parameters. + + pardict (dict): a dictionary with parameter key-val pairs to be + affected + """ + self.parameters = pardict + self.actions = {} + self.triggers = {} + + def register(self, key, parname, action='toggle', bounds=None, trigger=None): + """ + Associate a key-press with certain modification of a parameter. + + Parameters: + + key: key (toggle) or a pair of keys (increment/decrement) + parname: name of parameter from ``pardict`` to be affected + action: 'toggle' by default; if integer, then treat as delta + by which to increment/decrement by pair of keys + bounds: limits for increment/decrement action + trigger: a function that is passed the updated parameter value + """ + if action=='toggle': + self.actions[key] = (parname, partial(operator.not_), bounds) + else: + delta = -action + self.actions[key[0]] = (parname, partial(operator.add, delta), bounds) + delta = action + self.actions[key[1]] = (parname, partial(operator.add, delta), bounds) + if trigger is not None: + try: + for k in key: + self.triggers[k] = partial(trigger[0], *trigger[1:]) + except AttributeError: + self.triggers[key] = partial(trigger[0], *trigger[1:]) + + def __call__(self, key): + """Execute the action associated with pressing of the given key""" + try: + pname, func, bounds = self.actions[key] + pval = self.parameters[pname] + newval = func(pval) + if bounds is None: + self.parameters[pname] = newval + else: + self.parameters[pname] = newval + if newval < bounds[0]: + self.parameters[pname] = bounds[0] + if newval > bounds[1]: + self.parameters[pname] = bounds[1] + # do whatever requested after the parameter update + try: + self.triggers[key](self.parameters[pname]) + except KeyError: + # no further action triggered by the pressed key + pass + except KeyError: + # wrong key pressed + pass + + +class TestData: + nc, nr = 80, 62 + nh = 80 + def __init__(self): + """Create a dictionary to store all data. + + The dictionary key is decided upon adding items. + When adding data, we can pass either a tupple (Vdd, Tsx, Frames), or + a 2D array of shape N_frames, n_header+n_col*n_rows. + In the latter case, the assumption is that header[:, 1] and header[:, 2] + are Vdd and Tsx. These are parsed to produce the correct units (V, and degC) + (Vdd, Tsx, frame) is the stored dictionary value. + """ + self.data = {} + + def update(self, key, data): + """Add data as a tupple (Vdd, Tsx, Frames) or a 2D array from np.loadtxt""" + try: + Vdd, Tsx, frames = data + except ValueError: + frames = data[:, -self.nc * self.nr:] + Vdd = data[:, 2] # * 1.e-4 + Tsx = data[:, 3] # 100 + KELVIN0 + self.data[key] = Vdd, Tsx, frames + + def get(self, key): + """Retrieve data for a given key""" + return self.data[key] + + +def quick_segment(data, param=None): + """ + Perform a quick hot-on-cold segmentation and return the contour lines, + the mask of the hot contours, and their statistics, as a 3-tuple. + + Input `data` must be a 2D frame. + Parameters control the bilateral filtering, median bluring for + smoothing the contours, and how to do the hot-on-cold thresholding. + """ + p = { + 'use_bilat': True, + 'bilat_d': 7, + 'bilat_sigmaColor': 23, + 'bilat_sigmaSpace': 23, + # + 'use_median': True, + 'median_ksize': 3, + # + 'thresholding': 'adaptive', # or Otsu? + 'adaptth_blockSize': 97, + 'adaptth_C': -29, + # + 'contour_minArea': -9, + } + if param is not None: p.update(param) + img = data.copy() + if p['use_bilat']: + # reduce noise + img = cv.bilateralFilter(img.astype(np.float32), d=p['bilat_d'], + sigmaColor=p['bilat_sigmaColor'], + sigmaSpace=p['bilat_sigmaSpace']) + # abserr = np.abs(bilat - bilat.mean()) + # segment the image + img = cv.adaptiveThreshold(remap(img), 1, cv.ADAPTIVE_THRESH_GAUSSIAN_C, + cv.THRESH_BINARY, + blockSize=p['adaptth_blockSize'], + C=p['adaptth_C']) + if p['use_median']: + # smooth the contours of the segments + img = cv.medianBlur(img, ksize=p['median_ksize']) + + # extract the contours and return their line, mask and statistics + contours, hierarchy = cv.findContours(img, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE) + cntr_stats = get_contour_stats(data, contours, minArea=p['contour_minArea']) + contours, masks, cntrstats = zip(*cntr_stats) + return contours, masks, cntrstats + + +def get_contour_stats(data, contours, minArea=None, min_sdev=None, + mean_range=None, sortby='mean'): + """ + Return a list of tupples: [(contour, mask, metrics)]. + + The 'metrics' is a dictionary with the following keys: + centroid, area, mean, median, sdev, min, max, spread, + center, center_9, center_5. + + The 'area' is with a sign: negative = hot on cold; positive = cold on hot. + The center, center_9 and center_5 are the values of the pixel at + the 'centroid' coordinates, or the mean of the 5 or 9 pixels centered + at the 'centroid' coordinates of the frame. + """ + output = [] + # first identify warm-on-cold contours that have at least + # minArea number of pixels + for i, c in enumerate(contours): + # rough estimate of area with sign, to distinguish b/w + # hot on cold (-ve) and cold on hot (+ve) + area = cv.contourArea(c, oriented=True) + # work only with hot on cold contours, assuming hot is foreground + if minArea is None or area < minArea: + # create a filled mask for the current contour + mask = np.zeros(data.shape, dtype='uint8') + cv.drawContours(mask, contours, i, 1, cv.FILLED) + # get some metrics of the data within the current contour + metrics = {} + M = cv.moments(c) + cx = int(M['m10']/M['m00']) + cy = int(M['m01']/M['m00']) + centroid = (cx, cy) + centre_9_ix_iy = np.array([[cx+i, cy+j] for i in [-1, 0, +1] for j in [-1, 0, +1]], ndmin=2, dtype=int) + centre_5_ix_iy = np.array([[cx-1, cy], [cx, cy], [cx+1, cy], [cx, cy-1], [cx, cy+1], ], ndmin=2, dtype=int) + metrics['centroid'] = centroid + metrics['area'] = int(math.copysign(len( mask[mask != 0]), area)) + metrics['mean'] = data[mask != 0].mean() + metrics['median'] = np.median(data[mask != 0]) + metrics['sdev'] = data[mask != 0].astype(np.float32).std() + metrics['min'] = data[mask != 0].min() + metrics['max'] = data[mask != 0].max() + metrics['spread'] = np.ptp(data[mask != 0]) + # note that when we work with indexes, y-index comes first (row), + # then x-index (column) + metrics['center_9'] = data[centre_9_ix_iy[:,1], centre_9_ix_iy[:,0]].mean() + metrics['center_5'] = data[centre_5_ix_iy[:,1], centre_5_ix_iy[:,0]].mean() + metrics['center'] = data[cy, cx] + output.append((c, mask, metrics)) + # now check other features of the contours and exclude them + # if not matching + exclude = [] + if output: + for i, (c, mask, metrics) in enumerate(output): + if min_sdev is not None and metrics['sdev'] < min_sdev: + exclude.append(i) + if mean_range is not None and\ + (mean_range[0] > metrics['mean'] or\ + mean_range[1] < metrics['mean']): + exclude.append(i) + if exclude: + output = [output[i] for i in range(len(output)) if i not in exclude] + # return the contours sorted by the desired metric + output = sorted(output, key=lambda L: L[2][sortby], reverse=True) + return output + +def get_ipx_1D(icol_irow, n=9, ncols=80): + """ + Return the 1-D vector indexes of the `n` pixels centered on `icol_irow` + pixel of the 2-D frame. + """ + ipc, ipr = icol_irow + ipx = ncols * ipr + ipc-1 + + # special cases first + if n == 1: + ipx = [ipx] + return ipx + if n == 3: + ipx = [ipx, ipx-1, ipx+1] + return ipx + if n == 5: + ipx = [ipx, ipx-1, ipx+1, ipx-ncols, ipx+ncols] + return ipx + if n == 6: + ipx = [ipx, ipx-ncols-1, ipx-ncols+1, ipx+ncols-1, ipx+ncols+1] + return ipx + + # if n == (2q+1)^2 + q, r = int(np.sqrt(n) // 2), int(np.sqrt(n) % 2) + assert r == 1 + offs = range(-q, q+1) + ix_offs = [coloffs + ncols * rowoffs for (rowoffs, coloffs) + in itertools.product(offs, offs)] + ipx = [ipx + offs for offs in ix_offs] + return ipx + +def get_spot_offsets(n=9): + q, r = int(np.sqrt(n) // 2), int(np.sqrt(n) % 2) + assert r == 1 + offs = range(-q, q+1) + return np.array(list(itertools.product(offs,offs))) + +def get_spot_in_frame(centre=(40,31), n=9): + offs = get_spot_offsets() + return np.array(centre) + offs + +def stptime2float(x, fmt="%Y-%m-%dT%H:%M:%S.%f%z"): + """ + Convert the time string into a numpy float. + + This function may be used as a converter, when reading e.g. output + from the SenXorViewer file to a numpy array, via np.loadtxt. + However, this is not recommended. + Instead, read the frame data separately: + data = np.loadtext(filename, usecols=range(n, n+4960), delimiter=',') + and construct a pandas dataframe for the n columns of header related stuff + plus select pixels as necessary. + """ + dt = datetime.datetime.strptime(x, fmt) + return np.datetime64(dt).astype(float) + + +def compose_display(img_list): + """ + Compose a single image out of a list of opencv-rendered images of the same size + """ + if len(img_list) == 4: + top_img = np.hstack(img_list[:2]) + bot_img = np.hstack(img_list[2:]) + img = np.vstack((top_img, bot_img)) + return img + if len(img_list) == 6: + top_img = np.hstack(img_list[:3]) + bot_img = np.hstack(img_list[3:]) + img = np.vstack((top_img, bot_img)) + return img + if len(img_list) == 8: + top_img = np.hstack(img_list[:4]) + bot_img = np.hstack(img_list[4:]) + img = np.vstack((top_img, bot_img)) + return img + img = np.hstack(img_list) + return img + + +def annotate(image, isd, scale=1): + """ + Annotate the `image` with elements of input data structure `isd`. + + Currently supported annotation components are: contours, texts, rectangles. + On the assumption that `isd` elements are computed within the original + thermal frame, we must rescale them to the resolution of `image`. + This can easily be accomplished by the `scale` parameter. However, + if the isd components are obtained at different resolutions, e.g. from + visual and thermal stream, then scale must remain 1, and the actual + scaling must be done outside of this routine, at the time of composing the `isd`. + """ + # contours are arrays of points; must be scaled to the resolution of frame. + for contour in isd['contours']: + cv.drawContours(image, [contour * scale], contourIdx=0, color=GREEN, thickness=2) + + # texts require coordinates; adjusting the fontsize may be necessary + for text, coord in zip(isd['texts'], isd['text_coords']): + coord = (coord[0] * scale, coord[1] * scale) + cv.putText(image, text, coord, CVFONT, CVFONT_SIZE * 2./scale, GREEN, 2) + + # boxes are rectangles; must be scaled to the frame size + for pts in isd['rectangles']: + pts = [p * scale for p in pts] + cv.rectangle(image, (pts[0], pts[1]), (pts[2], pts[3]), GREEN, 2) + + return image + + +class Display: + """ + This class helps to compose a display of a list of rendered images, + optionally locating the window at a specified location on the screen. + """ + + def __init__(self, options, composer=compose_display): + """ + Decide how to organize rendered images on the display. + `options` is a dictionary: + + * `window_coord` -- in x,y pixels, + * `window_title` -- as a string. + """ + self.coord = options['window_coord'] + self.title = options['window_title'].upper() + self.composer = composer + self.dir = Path(options.get('directory', 'images')) + try: + os.mkdir(self.dir) + except FileExistsError: + pass + + def __call__(self, img_list): + self.img = self.composer(img_list) + cv.imshow(self.title, self.img) + if self.coord is not None: + cv.moveWindow(self.title, *self.coord) + + def save(self, filename): + """ + Save the image to a file. + + Filename includes extension, which determines the output format, + but excludes the directory (it is set during class initialization). + """ + filepath = str(self.dir / filename) + cv.imwrite(filepath, self.img) + + +class HotSpot: + """ + Object to calculate and store the thermal and geometrical metrics/statistics + of a hot spot. + The object has an `osd_attributes` member, which lists the accessible attributes + for composing the output structured data dictionary. + """ + def __init__(self, ix, frame, contour, mask, stats, p): + # parameters + self.p = p + # index + self.ix = ix + self.ny, self.nx = frame.shape + # contour + self.contour = contour + # mask corresponding to contour + self.mask = mask + # start composing output structure data based on the + # contour statistics + self.osd = stats.copy() # copy, because we'll change it here + # box returned below is (center(x,y), (width, height), angle of rotation) + min_area_box = cv.minAreaRect(contour) + # calculate area as width * height in pixels (int) + self.bbox_area = min_area_box[1][0] * min_area_box[1][1] + # get a list of points from the given (possibly rotated) box + # min_area_box is [list of virtexes] + self.min_area_bbox = np.asarray(cv.boxPoints(min_area_box), dtype=int) + # establish the background temperature relevant to the hotspot + # by defining an extended box that captures the relevant background emission + # note we pass the rotated box not the list of virtexes + self._extend_bbox(min_area_box) + # background mask + self._bg_mask() + # Background temperature; it may be better to do some averaging + self.bg = np.mean(sorted(frame[~self.bg_mask])[:12]) + self.bg_min = frame[~self.bg_mask].min() + # update the osd dictionary with local calculations in addition to contour stats + self.osd.update({ + 'contour': self.contour, # list of points 2D + 'min_area_bbox': self.min_area_bbox, # list of virtexes + 'extended_bbox': self.extended_bbox, # list of virtexes + 'extended_bbox_rotated': self.extended_bbox_rotated, # list of virtexes + 'bbox_area': self.bbox_area, # area in pixels + 'bg_min': self.bg_min, # background temperature from min of + # extended box around hot spot + 'bg': self.bg # background temperature from the + # mean of the 12 lowest values in + # the extended box around hot spot + }) + self.out_frames = { + 'hs_mask': self.mask * 255, # hot spot mask + 'bg_mask': self.bg_mask * 255, # background area overlapping hot spot + } + + def _extend_bbox(self, min_area_box, e=None): + """ + Extend the bounding box of the hot spot by `e` number of pixels on each side + `min_area_box` is of the form: [(x_center, y_center), width, heights, angle] + and is always a rectangle. + `e` is the extension by which to enlarge on each side of the box. + """ + if e is None: e = self.p['bbox_extension'] + w1 = min_area_box[1][0] + 2 * e + h1 = min_area_box[1][1] + 2 * e + # clear angle of rotation, because we can't easily slice for background + # temperature estimation, but use the same center and the new width + # box = (min_area_box[0], (w1, h1), min_area_box[2]) + box_rotated = (min_area_box[0], (w1, h1), min_area_box[2]) + box = (min_area_box[0], (w1, h1), 0) + # transform the box definition to a [list of virtexes] + # below we get an array of points which can be visualized as 'contours' + self.extended_bbox_rotated = np.asarray(cv.boxPoints(box_rotated), dtype=int) + self.extended_bbox = np.asarray(cv.boxPoints(box), dtype=int) + + def _bg_mask(self): + """ + Return an array with the frame size: + True outside the extended box of the hot spot + False within the extended box + """ + # Get the top-left and bottom right corner of the box + # Note that because it comes from a rotated box, the order of + # the points in the array does not correspond to any specific enumeration + # Therefore, we cannot rely on indexing to extract top-left and bottom-right + x0 = np.asarray(self.extended_bbox)[:,0].min() + x1 = np.asarray(self.extended_bbox)[:,0].max() + y0 = np.asarray(self.extended_bbox)[:,1].min() + y1 = np.asarray(self.extended_bbox)[:,1].max() + # The extended indexes may go below 0 and beyond frame size. + # Here we constrain them to 0 and nx or ny + (x0, y0) = max(x0, 0), max(y0, 0) + (x1, y1) = min(x1, self.nx-1), min(y1, self.ny-1) + self.bg_mask = np.ones(self.mask.shape, dtype=bool) + self.bg_mask[y0:y1, x0:x1] = False + + +class ColdSpot: + """ + Object to calculate and store the thermal and geometrical metrics/statistics + of a cold spot. + The object has an `osd_attributes` member, which lists the accessible attributes + for composing the output structured data dictionary. + """ + def __init__(self, ix, frame, contour, mask, stats, p): + # parameters + self.p = p + # index + self.ix = ix + self.ny, self.nx = frame.shape + # contour + self.contour = contour + # mask corresponding to contour + self.mask = mask + # start composing output structure data based on the + # contour statistics + self.osd = stats.copy() # copy, because we'll change it here + # box returned below is (center(x,y), (width, height), angle of rotation) + min_area_box = cv.minAreaRect(contour) + # calculate area as width * height in pixels (int) + self.bbox_area = min_area_box[1][0] * min_area_box[1][1] + # get a list of points from the given (possibly rotated) box + # min_area_box is [list of virtexes] + self.min_area_bbox = np.asarray(cv.boxPoints(min_area_box), dtype=int) + # establish the background temperature relevant to the hotspot + # by defining an extended box that captures the relevant background emission + # note we pass the rotated box not the list of virtexes + self._extend_bbox(min_area_box) + # background mask + self._bg_mask() + # Background temperature; it may be better to do some averaging + # + self.bg = np.mean(sorted(frame[~self.bg_mask])[12:]) + self.bg_max = frame[~self.bg_mask].max() + # update the osd dictionary with local calculations in addition to contour stats + self.osd.update({ + 'contour': self.contour, # list of points 2D + 'min_area_bbox': self.min_area_bbox, # list of virtexes + 'extended_bbox': self.extended_bbox, # list of virtexes + 'extended_bbox_rotated': self.extended_bbox_rotated, # list of virtexes + 'bbox_area': self.bbox_area, # area in pixels + 'bg_max': self.bg_max, # background temperature from max of + # extended box around cold spot + 'bg': self.bg # background temperature from the + # mean of the 12 highest values in + # the extended box around cold spot + }) + self.out_frames = { + 'hs_mask': self.mask * 255, # hot spot mask + 'bg_mask': self.bg_mask * 255, # background area overlapping hot spot + } + + def _extend_bbox(self, min_area_box, e=None): + """ + Extend the bounding box of the hot spot by `e` number of pixels on each side + `min_area_box` is of the form: [(x_center, y_center), width, heights, angle] + and is always a rectangle. + `e` is the extension by which to enlarge on each side of the box. + """ + if e is None: e = self.p['bbox_extension'] + w1 = min_area_box[1][0] + 2 * e + h1 = min_area_box[1][1] + 2 * e + # clear angle of rotation, because we can't easily slice for background + # temperature estimation, but use the same center and the new width + # box = (min_area_box[0], (w1, h1), min_area_box[2]) + box_rotated = (min_area_box[0], (w1, h1), min_area_box[2]) + box = (min_area_box[0], (w1, h1), 0) + # transform the box definition to a [list of virtexes] + # below we get an array of points which can be visualized as 'contours' + self.extended_bbox_rotated = np.asarray(cv.boxPoints(box_rotated), dtype=int) + self.extended_bbox = np.asarray(cv.boxPoints(box), dtype=int) + + def _bg_mask(self): + """ + Return an array with the frame size: + True outside the extended box of the hot spot + False within the extended box + """ + # Get the top-left and bottom right corner of the box + # Note that because it comes from a rotated box, the order of + # the points in the array does not correspond to any specific enumeration + # Therefore, we cannot rely on indexing to extract top-left and bottom-right + x0 = np.asarray(self.extended_bbox)[:,0].min() + x1 = np.asarray(self.extended_bbox)[:,0].max() + y0 = np.asarray(self.extended_bbox)[:,1].min() + y1 = np.asarray(self.extended_bbox)[:,1].max() + # The extended indexes may go below 0 and beyond frame size. + # Here we constrain them to 0 and nx or ny + (x0, y0) = max(x0, 0), max(y0, 0) + (x1, y1) = min(x1, self.nx-1), min(y1, self.ny-1) + self.bg_mask = np.ones(self.mask.shape, dtype=bool) + self.bg_mask[y0:y1, x0:x1] = False + + +class CVSegment: + """ + A class for quick segmentation based on simple, Otsu, or Adaptive threshold. + """ + def _adaptive_threshold(self, frame, *args, **kwargs): + threshold = None + binary = cv.adaptiveThreshold(frame, *args, **kwargs) + return threshold, binary + + def _otsu_threshold(self, frame, otsu_threshold_delta, *args, **kwargs): + """ + Otsu's algorithm of threshold selection is based on a histogram analysis. + In thermal imaging, it seems that more stable result may be obtained by + shifting the threshold a bit. This is achieved by the `otsu_threshold_delta`. + """ + threshold, binary = cv.threshold(frame, *args, **kwargs) + threshold += otsu_threshold_delta + # print(threshold - otsu_threshold_delta, threshold) + threshold, binary = cv.threshold(frame, thresh=threshold, + maxval=1, type=cv.THRESH_BINARY) + return threshold, binary + + def _contour(self, data, binary): + contours, hierarchy = cv.findContours(binary, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE) + contours = get_contour_stats(data, contours, minArea=self.p['contour_minArea']) + return contours + + def __init__(self, p): + self.p = p + if p['threshold_type'] == 'simple': + self.threshold = partial(cv.threshold, thresh=p['threshold'], + maxval=1, type=cv.THRESH_BINARY) + if p['threshold_type'] == 'otsu': + self.threshold = partial(self._otsu_threshold, + otsu_threshold_delta=p['otsu_threshold_delta'], + thresh=p['threshold'], maxval=1, + type=cv.THRESH_BINARY+cv.THRESH_OTSU) + if p['threshold_type'] == 'adaptive': + self.threshold = partial(self._adaptive_threshold, + maxValue=1, adaptiveMethod=cv.ADAPTIVE_THRESH_MEAN_C, + thresholdType=cv.THRESH_BINARY, + blockSize=p['threshold_blocksize'], C=p['threshold_C']) + + def __call__(self, frame, frui8=None): + # binarise + if frui8 is None: + frui8 = remap(frame) + self.frui8 = frui8 + threshold, binary = self.threshold(frui8) + self.binary = binary + # contours of the hot spots and get the corresonding masks and stats + contours = (self._contour(frame, binary)) + self.hotspots = [HotSpot(i, frame, c[0], c[1], c[2], self.p)\ + for i, c in enumerate(contours)] + # formulate the intermediate processing frames and output structured data + self.out_frames = {'normed': self.frui8, 'binary': self.binary} + self.osd = {'n_hotspots': len(self.hotspots)} + + +class CVSegmentCH: + """ + A class for quick segmentation on both hot-on-cold and cold-on-hot split. + """ + def _adaptive_threshold(self, frame, *args, **kwargs): + threshold = None + binary = cv.adaptiveThreshold(frame, *args, **kwargs) + return threshold, binary + + def _otsu_threshold(self, frame, otsu_threshold_delta, *args, **kwargs): + """ + Otsu's algorithm of threshold selection is based on a histogram analysis. + In thermal imaging, it seems that more stable result may be obtained by + shifting the threshold a bit. This is achieved by the `otsu_threshold_delta`. + """ + threshold, binary = cv.threshold(frame, *args, **kwargs) + threshold += otsu_threshold_delta + # print(threshold - otsu_threshold_delta, threshold) + threshold, binary = cv.threshold(frame, thresh=threshold, + maxval=1, type=cv.THRESH_BINARY) + return threshold, binary + + def _contour(self, data, binary): + contours, hierarchy = cv.findContours(binary, cv.RETR_TREE, + cv.CHAIN_APPROX_SIMPLE) + contours = get_contour_stats(data, contours, minArea=self.p['contour_minArea']) + return contours + + def __init__(self, p): + self.p = p + if p['threshold_type'] == 'simple': + self.threshold = partial(cv.threshold, thresh=p['threshold'], + maxval=1, type=cv.THRESH_BINARY) + if p['threshold_type'] == 'otsu': + self.threshold = partial(self._otsu_threshold, + otsu_threshold_delta=p['otsu_threshold_delta'], + thresh=p['threshold'], maxval=1, + type=cv.THRESH_BINARY+cv.THRESH_OTSU) + if p['threshold_type'] == 'adaptive': + self.threshold = partial(self._adaptive_threshold, + maxValue=1, adaptiveMethod=cv.ADAPTIVE_THRESH_MEAN_C, + thresholdType=cv.THRESH_BINARY, + blockSize=p['threshold_blocksize'], C=p['threshold_C']) + + def __call__(self, frame, frui8=None): + # binarise + if frui8 is None: + frui8 = remap(frame) + self.frui8 = frui8 + hs_threshold, hs_binary = self.threshold(frui8) + cs_threshold, cs_binary = self.threshold(255-frui8) + self.hs_binary = hs_binary + self.cs_binary = cs_binary + # contours of the hot spots and get the corresonding masks and stats + contours = (self._contour(frame, hs_binary)) + self.hotspots = [HotSpot(i, frame, c[0], c[1], c[2], self.p)\ + for i, c in enumerate(contours)] + # contours of the cold spots and get the corresonding masks and stats + contours = (self._contour(frame, cs_binary)) + self.coldspots = [ColdSpot(i, frame, c[0], c[1], c[2], self.p)\ + for i, c in enumerate(contours)] + # formulate the intermediate processing frames and output structured data + self.out_frames = {'normed': self.frui8, + 'hs_binary': self.hs_binary, + 'cs_binary': self.cs_binary} + self.osd = { + 'n_hotspots': len(self.hotspots), + 'n_coldspots': len(self.coldspots) + } + + diff --git a/src/ui/__pycache__/home_screen.cpython-313.pyc b/src/ui/__pycache__/home_screen.cpython-313.pyc deleted file mode 100644 index 4dc1cb84..00000000 Binary files a/src/ui/__pycache__/home_screen.cpython-313.pyc and /dev/null differ diff --git a/src/ui/__pycache__/loading_screen.cpython-313.pyc b/src/ui/__pycache__/loading_screen.cpython-313.pyc deleted file mode 100644 index 1f5cdabd..00000000 Binary files a/src/ui/__pycache__/loading_screen.cpython-313.pyc and /dev/null differ diff --git a/src/ui/__pycache__/main_window.cpython-313.pyc b/src/ui/__pycache__/main_window.cpython-313.pyc deleted file mode 100644 index 37375544..00000000 Binary files a/src/ui/__pycache__/main_window.cpython-313.pyc and /dev/null differ diff --git a/src/ui/__pycache__/menu_screen.cpython-313.pyc b/src/ui/__pycache__/menu_screen.cpython-313.pyc deleted file mode 100644 index 786583eb..00000000 Binary files a/src/ui/__pycache__/menu_screen.cpython-313.pyc and /dev/null differ diff --git a/src/ui/__pycache__/network_settings.cpython-313.pyc b/src/ui/__pycache__/network_settings.cpython-313.pyc deleted file mode 100644 index 6441fe53..00000000 Binary files a/src/ui/__pycache__/network_settings.cpython-313.pyc and /dev/null differ diff --git a/src/ui/__pycache__/settings_screen.cpython-313.pyc b/src/ui/__pycache__/settings_screen.cpython-313.pyc deleted file mode 100644 index 92ab4d10..00000000 Binary files a/src/ui/__pycache__/settings_screen.cpython-313.pyc and /dev/null differ diff --git a/src/ui/control_screen/control_screen.py b/src/ui/control_screen/control_screen.py new file mode 100644 index 00000000..70865000 --- /dev/null +++ b/src/ui/control_screen/control_screen.py @@ -0,0 +1,269 @@ +#TBD: incase a gcode is yet to be executed, block the thread from executing another gcode in moonraker api + +from PyQt5 import uic +from PyQt5.QtWidgets import (QWidget, QPushButton, QSpinBox, QProgressBar, QSizePolicy, QVBoxLayout, QMessageBox, QLabel) +from PyQt5.QtCore import pyqtSlot, pyqtSignal, QTimer +from PyQt5.QtGui import QImage +import numpy as np +from ui.custom_widgets import ImageWidget +from utils.helpers import run_async +import time +from processAutomationController.processAutomationController import ProcessAutomationController + +class ControlScreen(QWidget): + progress_update_signal = pyqtSignal(int) + + def __init__(self, main_window): + super(ControlScreen, self).__init__(main_window) + self.main_window = main_window + + # Load the control screen UI + self.load_ui() + + # Initialize UI elements + self.initialize_ui_elements() + + # Initialize ProcessAutomationController + self.process_automation_controller = ProcessAutomationController(main_window) + + # Setup signal-slot connections + self.setup_connections() + + # Replace QWidget with custom ImageWidget + self.setup_custom_widgets() + + # Connect signals to slots + self.connect_signals() + + self.motion_control_buttons = [ + self.homeBuildModuleButton, self.undockButton, self.dockButton, + self.homeFeedButton, self.homeZButton, self.step01Button, + self.step1Button, self.step10Button, self.step100Button, + self.moveZMButton, self.moveZPButton, self.moveFeedMButton, + self.moveFeedPButton, self.setBedTempButton, self.setVolumeTempButton, + self.homeRecoaterButton, self.recoatButton, self.moveToStartingPositionButton, + self.prepareForPartRemovalButton, self.initialLevellingRecoatButton, + self.heatedBufferRecoatButton, self.doseRecoatLayerButton, self.preparePowderLoadingButton + ] + + # Connect the scancard status update signal to the label update slot + self.main_window.printer_status.scancard_status_updated.connect(self.update_laser_status) + + def load_ui(self): + try: + uic.loadUi('src/ui/control_screen/control_screen.ui', self) + print("ControlScreen UI loaded successfully") + except Exception as e: + print(f"Failed to load ControlScreen UI: {e}") + + def initialize_ui_elements(self): + self.chamberTempSpinBox = self.findChild(QSpinBox, "chamberTempSpinBox") + self.setChamberTempButton = self.findChild(QPushButton, "setChamberTempButton") + self.cooldownButton = self.findChild(QPushButton, "cooldownButton") + + self.homeBuildModuleButton = self.findChild(QPushButton, "homeBuildModuleButton") + self.undockButton = self.findChild(QPushButton, "undockButton") + self.dockButton = self.findChild(QPushButton, "dockButton") + self.homeFeedButton = self.findChild(QPushButton, "homeFeedButton") + self.homeZButton = self.findChild(QPushButton, "homeZButton") + self.step01Button = self.findChild(QPushButton, "step01Button") + self.step1Button = self.findChild(QPushButton, "step1Button") + self.step10Button = self.findChild(QPushButton, "step10Button") + self.step100Button = self.findChild(QPushButton, "step100Button") + self.moveZMButton = self.findChild(QPushButton, "moveZMButton") + self.moveZPButton = self.findChild(QPushButton, "moveZPButton") + self.moveFeedMButton = self.findChild(QPushButton, "moveFeedMButton") + self.moveFeedPButton = self.findChild(QPushButton, "moveFeedPButton") + self.setBedTempButton = self.findChild(QPushButton, "setBedTempButton") + self.bedTempSpinBox = self.findChild(QSpinBox, "bedTempSpinBox") + self.setVolumeTempButton = self.findChild(QPushButton, "setVolumeTempButton") + self.volumeTempSpinBox = self.findChild(QSpinBox, "volumeTempSpinBox") + + self.homeRecoaterButton = self.findChild(QPushButton, "homeRecoaterButton") + self.recoatButton = self.findChild(QPushButton, "recoatButton") + self.initialLevellingRecoatButton = self.findChild(QPushButton, "initialLevellingRecoatButton") + self.heatedBufferRecoatButton = self.findChild(QPushButton, "heatedBufferRecoatButton") + self.doseRecoatLayerButton = self.findChild(QPushButton, "doseRecoatLayerButton") + self.preparePowderLoadingButton = self.findChild(QPushButton, "preparePowderLoadingButton") + + self.stopProcessButton = self.findChild(QPushButton, "stopProcessButton") + self.recoaterProgressBar = self.findChild(QProgressBar, "recoaterProgressBar") + + self.moveToStartingPositionButton = self.findChild(QPushButton, "moveToStartingPositionButton") + self.prepareForPartRemovalButton = self.findChild(QPushButton, "prepareForPartRemovalButton") + + self.maxTempLabel = self.findChild(QLabel, "maxTempLabel") # Find the maxTempLabel + + # Initialize scanCardStatusLabel + self.scanCardStatusLabel = self.findChild(QLabel, "scanCardStatusLabel") + + # Initialize start and stop marking buttons + self.startMarkingButton = self.findChild(QPushButton, "startMarkingButton") + self.stopMarkingButton = self.findChild(QPushButton, "stopMarkingButton") + + def setup_connections(self): + self.step = 10 + self.setStep(10) + self.homeBuildModuleButton.clicked.connect(lambda: self.run_async_send_gcode("G28 Z Y\nM400")) + self.undockButton.clicked.connect(lambda: self.run_async_send_gcode("goDown\nM400")) + self.dockButton.clicked.connect(lambda: self.run_async_send_gcode("liftUp\nM400")) + self.setChamberTempButton.clicked.connect(lambda: self.update_setpoint(self.chamberTempSpinBox.value())) + self.cooldownButton.clicked.connect(self.cooldown) + self.homeFeedButton.clicked.connect(lambda: self.run_async_send_gcode("G28 Y\nM400")) + self.homeZButton.clicked.connect(lambda: self.run_async_send_gcode("G28 Z\nM400")) + self.step01Button.clicked.connect(lambda: self.setStep(0.1)) + self.step1Button.clicked.connect(lambda: self.setStep(1)) + self.step10Button.clicked.connect(lambda: self.setStep(10)) + self.step100Button.clicked.connect(lambda: self.setStep(100)) + self.moveZMButton.clicked.connect(lambda: self.run_async_send_gcode(f"G91\nG0 Z-{self.step}\nG90\nM400")) + self.moveZPButton.clicked.connect(lambda: self.run_async_send_gcode(f"G91\nG0 Z{self.step}\nG90\nM400")) + self.moveFeedMButton.clicked.connect(lambda: self.run_async_send_gcode(f"G91\nG0 Y-{self.step}\nG90\nM400")) + self.moveFeedPButton.clicked.connect(lambda: self.run_async_send_gcode(f"G91\nG0 Y{self.step}\nG90\nM400")) + self.setBedTempButton.clicked.connect(lambda: self.run_async_send_gcode(f"SET_HEATER_TEMPERATURE HEATER=heater_bed TARGET={self.bedTempSpinBox.value()}")) + self.setVolumeTempButton.clicked.connect(self.setVolumeHeaterTemp) + self.initialLevellingRecoatButton.clicked.connect(self.confirm_initial_levelling_recoat) + self.heatedBufferRecoatButton.clicked.connect(self.confirm_heated_buffer_recoat) + self.doseRecoatLayerButton.clicked.connect(lambda: self.run_async_process(self.process_automation_controller.dose_recoat_layer)) + self.preparePowderLoadingButton.clicked.connect(lambda: self.run_async_process(self.process_automation_controller.prepare_powder_loading)) + self.stopProcessButton.clicked.connect(self.process_automation_controller.stop_process) + self.homeRecoaterButton.clicked.connect(lambda: self.run_async_send_gcode("homeRecoater")) + self.recoatButton.clicked.connect(lambda: self.run_async_send_gcode("recoat")) + self.moveToStartingPositionButton.clicked.connect(lambda: self.run_async_process(self.process_automation_controller.move_to_starting_sequence)) + self.prepareForPartRemovalButton.clicked.connect(lambda: self.run_async_process(self.process_automation_controller.prepare_for_part_removal_sequence)) + + # Connect start and stop marking buttons to Scancard functions + self.startMarkingButton.clicked.connect(self.main_window.scancard.start_mark) + self.stopMarkingButton.clicked.connect(self.main_window.scancard.stop_mark) + + @run_async + def run_async_send_gcode(self, gcode): + self.main_window.moonraker_api.send_gcode(gcode) + + @run_async + def run_async_process(self, process_method): + process_method() + + def setup_custom_widgets(self): + thermal_camera_container = self.findChild(QWidget, "thermalCameraWidget") + self.thermalCameraWidget = ImageWidget(thermal_camera_container) + layout = QVBoxLayout(thermal_camera_container) + layout.addWidget(self.thermalCameraWidget) + self.thermalCameraWidget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + + rgb_camera_container = self.findChild(QWidget, "rgbCameraWidget") + self.rgbCameraWidget = ImageWidget(rgb_camera_container) + layout = QVBoxLayout(rgb_camera_container) + layout.addWidget(self.rgbCameraWidget) + self.rgbCameraWidget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + + def connect_signals(self): + self.main_window.printer_status.temperatures_updated.connect(self.update_thermal_camera_widget) + self.main_window.printer_status.rgb_frame_updated.connect(self.update_rgb_camera_widget) + self.main_window.printer_status.maxtemp_updated.connect(self.update_max_temp_label) # Connect the maxtemp_updated signal + + @pyqtSlot(float) + def update_max_temp_label(self, max_temp): + """Slot to update the text of maxTempLabel with the maximum temperature.""" + self.maxTempLabel.setText(f"Max Temp: {max_temp:.2f}°C") + + def set_motion_control_buttons_enabled(self, enabled): + """Enable or disable motion control buttons.""" + for button in self.motion_control_buttons: + button.setEnabled(enabled) + + def confirm_initial_levelling_recoat(self): + """Show a confirmation dialog before starting the initial levelling recoat.""" + reply = QMessageBox.question(self, 'Confirmation', + 'Ensure that the build module is moved to the starting position and recoater is homed before starting the initial levelling recoat. Do you want to proceed?', + QMessageBox.Yes | QMessageBox.No, QMessageBox.No) + if reply == QMessageBox.Yes: + self.process_automation_controller.process_running = True + self.set_motion_control_buttons_enabled(False) + self.run_async_process(self.process_automation_controller.initialLevellingRecoat) + + def confirm_heated_buffer_recoat(self): + """Show a confirmation dialog before starting the heated buffer recoat.""" + reply = QMessageBox.question(self, 'Confirmation', + 'Ensure that the build module is moved to the starting position and recoater is homed before starting the heated buffer recoat. Do you want to proceed?', + QMessageBox.Yes | QMessageBox.No, QMessageBox.No) + if reply == QMessageBox.Yes: + self.process_automation_controller.process_running = True + self.set_motion_control_buttons_enabled(False) + self.run_async_process(self.process_automation_controller.heatedBufferRecoat) + + def setVolumeHeaterTemp(self): + """Set the volume heater temperature.""" + target_temp = self.volumeTempSpinBox.value() + self.main_window.moonraker_api.send_gcode(f"SET_HEATER_TEMPERATURE HEATER=bed_heater_front TARGET={target_temp}") + self.main_window.moonraker_api.send_gcode(f"SET_HEATER_TEMPERATURE HEATER=bed_heater_left TARGET={target_temp}") + self.main_window.moonraker_api.send_gcode(f"SET_HEATER_TEMPERATURE HEATER=bed_heater_right TARGET={target_temp}") + + def update_setpoint(self, value): + """Update the chamber temperature setpoint in the PrinterStatus model.""" + self.main_window.printer_status.chamberTemperatureSetpoint = value + print(f"Chamber temperature setpoint updated to {value}") + + @pyqtSlot(np.ndarray, dict) + def update_thermal_camera_widget(self, frame, temps): + """Update the thermal camera widget.""" + if frame is not None: + image = QImage(frame.data, frame.shape[1], frame.shape[0], frame.strides[0], QImage.Format_BGR888) + self.thermalCameraWidget.setImage(image) + + @pyqtSlot(np.ndarray) + def update_rgb_camera_widget(self, frame): + """Update the RGB camera widget.""" + if frame is not None: + height, width, channel = frame.shape + bytes_per_line = 3 * width + image = QImage(frame.data, width, height, bytes_per_line, QImage.Format_RGB888).rgbSwapped() + self.rgbCameraWidget.setImage(image) + + def cooldown(self): + """Cooldown the chamber.""" + self.main_window.printer_status.chamberTemperatureSetpoint = 0 + self.chamberTempSpinBox.setValue(0) + + def setStep(self, stepRate): + """Set the step rate for movement.""" + try: + self.step100Button.setFlat(stepRate == 100) + self.step1Button.setFlat(stepRate == 1) + self.step10Button.setFlat(stepRate == 10) + self.step01Button.setFlat(stepRate == 0.1) + self.step = stepRate + except Exception as e: + print(f"Error in setting step: {e}") + + def start_marking(self): + """Start the marking process.""" + self.main_window.start_scancard_mark() + + def stop_marking(self): + """Stop the marking process.""" + self.main_window.stop_scancard_mark() + + def update_laser_status(self, status): + """Update the laser status label.""" + self.scanCardStatusLabel.setText(f"Laser Status: {status}") + + +def replace_placeholders(sequence: str, printer_status) -> str: + """Replace placeholders in the sequence with actual values from the printer_status model.""" + placeholders = { + "{layerHeight}": printer_status.layerHeight, + "{initialLevellingHeight}": printer_status.initialLevellingHeight, + "{heatedBufferHeight}": printer_status.heatedBufferHeight, + "{powderLoadingExtraHeightGap}": printer_status.powderLoadingExtraHeightGap, + "{bedTemperature}": printer_status.bedTemperature, + "{volumeTemperature}": printer_status.volumeTemperature, + "{chamberTemperature}": printer_status.chamberTemperature, + "{p}": printer_status.p, + "{i}": printer_status.i, + "{d}": printer_status.d, + "{powderLoadingHeight}": printer_status.initialLevellingHeight + 2 * printer_status.heatedBufferHeight + printer_status.partHeight, + "{dosingHeight}": printer_status.dosingHeight # Add dosingHeight + } + for placeholder, value in placeholders.items(): + sequence = sequence.replace(placeholder, str(value)) + return sequence \ No newline at end of file diff --git a/src/ui/control_screen/control_screen.ui b/src/ui/control_screen/control_screen.ui new file mode 100644 index 00000000..3e24456f --- /dev/null +++ b/src/ui/control_screen/control_screen.ui @@ -0,0 +1,3343 @@ + + + Form + + + + 0 + 0 + 1554 + 834 + + + + Form + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + QFrame#buildModuleFrame { + border: 2px solid gray; + border-radius: 20px; +} + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 9 + + + 9 + + + 9 + + + 9 + + + + + + 16777215 + 200 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 5 + + + 5 + + + 5 + + + 5 + + + 6 + + + + + + 200 + 50 + + + + + 350 + 50 + + + + + Gotham + 12 + + + + QPushButton { + border: 1px solid rgb(87, 87, 87); + + background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); +border-radius:20px; +} + +QPushButton:pressed { + background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #dadbde, stop: 1 #f6f7fa); +} + +QPushButton:flat { + border: none; /* no border for a flat push button */ +} + +QPushButton:default { + border-color: navy; /* make the default button prominent */ +} + +QPushButton:focus { + outline: none; +} + + + Prepare for Part Removal + + + + 20 + 20 + + + + + + + + + 200 + 50 + + + + + 500 + 50 + + + + + Gotham + 12 + + + + QPushButton { + border: 1px solid rgb(87, 87, 87); + + background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); +border-radius:20px; +} + +QPushButton:pressed { + background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #dadbde, stop: 1 #f6f7fa); +} + +QPushButton:flat { + border: none; /* no border for a flat push button */ +} + +QPushButton:default { + border-color: navy; /* make the default button prominent */ +} + +QPushButton:focus { + outline: none; +} + + + Undock + + + + 20 + 20 + + + + + + + + + 200 + 50 + + + + + 350 + 50 + + + + + Gotham + 12 + + + + QPushButton { + border: 1px solid rgb(87, 87, 87); + + background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); +border-radius:20px; +} + +QPushButton:pressed { + background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #dadbde, stop: 1 #f6f7fa); +} + +QPushButton:flat { + border: none; /* no border for a flat push button */ +} + +QPushButton:default { + border-color: navy; /* make the default button prominent */ +} + +QPushButton:focus { + outline: none; +} + + + Home Build Module + + + + 20 + 20 + + + + + + + + + 200 + 50 + + + + + 500 + 50 + + + + + Gotham + 12 + + + + QPushButton { + border: 1px solid rgb(87, 87, 87); + + background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); +border-radius:20px; +} + +QPushButton:pressed { + background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #dadbde, stop: 1 #f6f7fa); +} + +QPushButton:flat { + border: none; /* no border for a flat push button */ +} + +QPushButton:default { + border-color: navy; /* make the default button prominent */ +} + +QPushButton:focus { + outline: none; +} + + + Prepare Powder Loading + + + + 20 + 20 + + + + + + + + + 200 + 50 + + + + + 500 + 50 + + + + + Gotham + 12 + + + + QPushButton { + border: 1px solid rgb(87, 87, 87); + + background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); +border-radius:20px; +} + +QPushButton:pressed { + background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #dadbde, stop: 1 #f6f7fa); +} + +QPushButton:flat { + border: none; /* no border for a flat push button */ +} + +QPushButton:default { + border-color: navy; /* make the default button prominent */ +} + +QPushButton:focus { + outline: none; +} + + + Move to Starting Position + + + + 20 + 20 + + + + + + + + + 200 + 50 + + + + + 500 + 50 + + + + + Gotham + 12 + + + + QPushButton { + border: 1px solid rgb(87, 87, 87); + + background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); +border-radius:20px; +} + +QPushButton:pressed { + background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #dadbde, stop: 1 #f6f7fa); +} + +QPushButton:flat { + border: none; /* no border for a flat push button */ +} + +QPushButton:default { + border-color: navy; /* make the default button prominent */ +} + +QPushButton:focus { + outline: none; +} + + + Dock + + + + 20 + 20 + + + + + + + + + 16777215 + 20 + + + + + Gotham + 16 + 50 + false + + + + color: grey; + + + BUILD MODULE + + + + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 300 + + + + + 16777215 + 16777215 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 16777215 + 20 + + + + + Gotham + 16 + 50 + false + + + + color: grey; + + + Z Axis + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 10 + + + + + + + + + 0 + 0 + + + + + 101 + 91 + + + + + MS Shell Dlg 2 + 20 + + + + QPushButton { + border: 1px solid rgb(87, 87, 87); + + background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); + border-top-left-radius: 15px; + border-top-right-radius: 15px; +} + +QPushButton:pressed { + background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #dadbde, stop: 1 #f6f7fa); +} + +QPushButton:flat { + border: none; /* no border for a flat push button */ +} + +QPushButton:default { + border-color: navy; /* make the default button prominent */ +} + +QPushButton:focus { + outline: none; +} + + + + + + + :/Navigation/img/Navigation/arrows.png:/Navigation/img/Navigation/arrows.png + + + + 40 + 40 + + + + false + + + false + + + false + + + false + + + + + + + + 0 + 0 + + + + + 101 + 91 + + + + + MS Shell Dlg 2 + 20 + + + + QPushButton { + border: 1px solid rgb(87, 87, 87); + + background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); +} + +QPushButton:pressed { + background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #dadbde, stop: 1 #f6f7fa); +} + +QPushButton:flat { + border: none; /* no border for a flat push button */ +} + +QPushButton:default { + border-color: navy; /* make the default button prominent */ +} + +QPushButton:focus { + outline: none; +} + + + + + + + :/Icons/img/icons/home-icon-silhouette.png:/Icons/img/icons/home-icon-silhouette.png + + + + 40 + 40 + + + + false + + + false + + + false + + + false + + + + + + + + 0 + 0 + + + + + 101 + 91 + + + + + MS Shell Dlg 2 + 20 + + + + QPushButton { + border: 1px solid rgb(87, 87, 87); + + background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); + border-bottom-left-radius: 15px; + border-bottom-right-radius: 15px; +} + +QPushButton:pressed { + background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #dadbde, stop: 1 #f6f7fa); +} + +QPushButton:flat { + border: none; /* no border for a flat push button */ +} + +QPushButton:default { + border-color: navy; /* make the default button prominent */ +} + +QPushButton:focus { + outline: none; +} + + + + + + + :/Navigation/img/Navigation/arrows-5.png:/Navigation/img/Navigation/arrows-5.png + + + + 40 + 40 + + + + false + + + false + + + false + + + false + + + + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 16777215 + 20 + + + + + Gotham + 16 + 50 + false + + + + color: grey; + + + Feed Axis + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 10 + + + + + + + + + 0 + 0 + + + + + 101 + 91 + + + + + MS Shell Dlg 2 + 20 + + + + QPushButton { + border: 1px solid rgb(87, 87, 87); + + background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); + border-top-left-radius: 15px; + border-top-right-radius: 15px; +} + +QPushButton:pressed { + background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #dadbde, stop: 1 #f6f7fa); +} + +QPushButton:flat { + border: none; /* no border for a flat push button */ +} + +QPushButton:default { + border-color: navy; /* make the default button prominent */ +} + +QPushButton:focus { + outline: none; +} + + + + + + + :/Navigation/img/Navigation/arrows.png:/Navigation/img/Navigation/arrows.png + + + + 40 + 40 + + + + false + + + false + + + false + + + false + + + + + + + + 0 + 0 + + + + + 101 + 91 + + + + + MS Shell Dlg 2 + 20 + + + + QPushButton { + border: 1px solid rgb(87, 87, 87); + + background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); +} + +QPushButton:pressed { + background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #dadbde, stop: 1 #f6f7fa); +} + +QPushButton:flat { + border: none; /* no border for a flat push button */ +} + +QPushButton:default { + border-color: navy; /* make the default button prominent */ +} + +QPushButton:focus { + outline: none; +} + + + + + + + :/Icons/img/icons/home-icon-silhouette.png:/Icons/img/icons/home-icon-silhouette.png + + + + 40 + 40 + + + + false + + + false + + + false + + + false + + + + + + + + 0 + 0 + + + + + 101 + 91 + + + + + MS Shell Dlg 2 + 20 + + + + QPushButton { + border: 1px solid rgb(87, 87, 87); + + background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); + border-bottom-left-radius: 15px; + border-bottom-right-radius: 15px; +} + +QPushButton:pressed { + background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #dadbde, stop: 1 #f6f7fa); +} + +QPushButton:flat { + border: none; /* no border for a flat push button */ +} + +QPushButton:default { + border-color: navy; /* make the default button prominent */ +} + +QPushButton:focus { + outline: none; +} + + + + + + + :/Navigation/img/Navigation/arrows-5.png:/Navigation/img/Navigation/arrows-5.png + + + + 40 + 40 + + + + false + + + false + + + false + + + false + + + + + + + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 60 + + + + + MS Shell Dlg 2 + 15 + + + + QPushButton { + border: 1px solid rgb(87, 87, 87); +border-bottom-left-radius: 15px; +border-top-left-radius: 15px; + + background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); +} + +QPushButton:pressed { + background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #dadbde, stop: 1 #f6f7fa); +} + +QPushButton:flat { + border-bottom: none; /* no border for a flat push button */ + background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #dadbde, stop: 1 #f6f7fa); +} + +QPushButton:default { + border-color: navy; /* make the default button prominent */ +} + +QPushButton:focus { + outline: none; +} + + + 0.1 mm + + + + 40 + 40 + + + + false + + + false + + + false + + + false + + + + + + + + 0 + 60 + + + + + MS Shell Dlg 2 + 15 + + + + QPushButton { + border: 1px solid rgb(87, 87, 87); + background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); +} + +QPushButton:pressed { + background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #dadbde, stop: 1 #f6f7fa); +} + +QPushButton:flat { + border-bottom: none; /* no border for a flat push button */ + background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #dadbde, stop: 1 #f6f7fa); +} + +QPushButton:default { + border-color: navy; /* make the default button prominent */ +} + +QPushButton:focus { + outline: none; +} + + + 1 mm + + + + 40 + 40 + + + + false + + + false + + + false + + + false + + + + + + + + 0 + 60 + + + + + MS Shell Dlg 2 + 15 + + + + QPushButton { + border: 1px solid rgb(87, 87, 87); + background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); +} + +QPushButton:pressed { + background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #dadbde, stop: 1 #f6f7fa); +} + +QPushButton:flat { + border-bottom: none; /* no border for a flat push button */ + background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #dadbde, stop: 1 #f6f7fa); +} + +QPushButton:default { + border-color: navy; /* make the default button prominent */ +} + +QPushButton:focus { + outline: none; +} + + + 10 mm + + + + 40 + 40 + + + + false + + + false + + + false + + + false + + + + + + + + 0 + 60 + + + + + MS Shell Dlg 2 + 15 + + + + QPushButton { + border: 1px solid rgb(87, 87, 87); + background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); +} + +QPushButton:pressed { + background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #dadbde, stop: 1 #f6f7fa); +} + +QPushButton:flat { + border-bottom: none; /* no border for a flat push button */ + background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #dadbde, stop: 1 #f6f7fa); +} + +QPushButton:default { + border-color: navy; /* make the default button prominent */ +} + +QPushButton:focus { + outline: none; +} + + + 100 mm + + + + 40 + 40 + + + + true + + + false + + + false + + + false + + + false + + + + + + + + 0 + 60 + + + + + MS Shell Dlg 2 + 20 + + + + QPushButton { + border: 1px solid rgb(87, 87, 87); + border-bottom-right-radius: 15px; + border-top-right-radius: 15px; + background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); +} + +QPushButton:pressed { + background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #dadbde, stop: 1 #f6f7fa); +} + +QPushButton:flat { +} + +QPushButton:default { + border-color: navy; /* make the default button prominent */ +} + +QPushButton:focus { + outline: none; +} + + + + + + + :/Icons/img/icons/motor.png:/Icons/img/icons/motor.png + + + + 40 + 40 + + + + false + + + false + + + false + + + false + + + + + + + + + + + + + + + + + 0 + 221 + + + + QFrame #temperatureFrame { + border: 2px solid gray; + border-radius: 20px; +} + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 16777215 + 20 + + + + + Gotham + 16 + 50 + false + + + + color: grey; + + + TEMPERATURES + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 10 + + + + + + + + + 16777215 + 20 + + + + + Gotham + 16 + 50 + false + + + + color: grey; + + + Bed + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 10 + + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 131 + + + + + Gotham + 20 + + + + QSpinBox { + padding-right: 5px; /* make room for the arrows */ + color: rgb(0, 0, 0); + background-color: rgba(255, 255, 255, 0); + +} +QSpinBox ::text:selected { + background-color: rgb(0, 0, 0); + +} +QSpinBox::up-button { + border: 1px solid rgb(87, 87, 87); + +border-top-left-radius: 15px; + background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); + width: 60px; + height: 61px; + padding: 2px; +} + +QSpinBox::up-arrow { +image: url(:/Navigation/img/Navigation/arrows.png); + width: 40px; + height: 40px; +padding: 5px; } + + + +QSpinBox::up-button:pressed { + background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #dadbde, stop: 1 #f6f7fa); +} + + +QSpinBox::down-button { + border: 1px solid rgb(87, 87, 87); +border-bottom-left-radius: 15px; + background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); + width: 60px; + height: 61px; + padding: 2px; +} + +QSpinBox::down-arrow { +image: url(:/Navigation/img/Navigation/arrows-5.png); + width: 40px; + height: 40px; +padding: 5px; +} + +QSpinBox::down-button:pressed { + background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #dadbde, stop: 1 #f6f7fa); + +} + + + + + false + + + false + + + QAbstractSpinBox::UpDownArrows + + + true + + + °C + + + 300 + + + 1 + + + 0 + + + + + + + + 71 + 131 + + + + + 100 + 16777215 + + + + + Gotham + 13 + + + + QPushButton { + border: 1px solid rgb(87, 87, 87); + background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); +border-bottom-right-radius: 15px; +border-top-right-radius: 15px; + +} + +QPushButton:pressed { + background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #dadbde, stop: 1 #f6f7fa); +} + +QPushButton:flat { + border: none; /* no border for a flat push button */ +} + +QPushButton:default { + border-color: navy; /* make the default button prominent */ +} + +QPushButton:focus { + outline: none; +} + + + + + + + :/Icons/img/icons/verification-mark.png:/Icons/img/icons/verification-mark.png + + + + 50 + 50 + + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Expanding + + + + 20 + 20 + + + + + + + + + 16777215 + 20 + + + + + Gotham + 16 + 50 + false + + + + color: grey; + + + Volume + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 10 + + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 131 + + + + + Gotham + 20 + + + + QSpinBox { + padding-right: 5px; /* make room for the arrows */ + color: rgb(0, 0, 0); + background-color: rgba(255, 255, 255, 0); + +} +QSpinBox ::text:selected { + background-color: rgb(0, 0, 0); + +} +QSpinBox::up-button { + border: 1px solid rgb(87, 87, 87); + +border-top-left-radius: 15px; + background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); + width: 60px; + height: 61px; + padding: 2px; +} + +QSpinBox::up-arrow { +image: url(:/Navigation/img/Navigation/arrows.png); + width: 40px; + height: 40px; +padding: 5px; } + + + +QSpinBox::up-button:pressed { + background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #dadbde, stop: 1 #f6f7fa); +} + + +QSpinBox::down-button { + border: 1px solid rgb(87, 87, 87); +border-bottom-left-radius: 15px; + background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); + width: 60px; + height: 61px; + padding: 2px; +} + +QSpinBox::down-arrow { +image: url(:/Navigation/img/Navigation/arrows-5.png); + width: 40px; + height: 40px; +padding: 5px; +} + +QSpinBox::down-button:pressed { + background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #dadbde, stop: 1 #f6f7fa); + +} + + + + + false + + + false + + + QAbstractSpinBox::UpDownArrows + + + true + + + °C + + + 300 + + + 1 + + + 0 + + + + + + + + 71 + 131 + + + + + 100 + 16777215 + + + + + Gotham + 13 + + + + QPushButton { + border: 1px solid rgb(87, 87, 87); + background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); +border-bottom-right-radius: 15px; +border-top-right-radius: 15px; + +} + +QPushButton:pressed { + background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #dadbde, stop: 1 #f6f7fa); +} + +QPushButton:flat { + border: none; /* no border for a flat push button */ +} + +QPushButton:default { + border-color: navy; /* make the default button prominent */ +} + +QPushButton:focus { + outline: none; +} + + + + + + + :/Icons/img/icons/verification-mark.png:/Icons/img/icons/verification-mark.png + + + + 50 + 50 + + + + + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Expanding + + + + 20 + 20 + + + + + + + + + 16777215 + 20 + + + + + Gotham + 16 + 50 + false + + + + color: grey; + + + Chamber + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 10 + + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 131 + + + + + Gotham + 20 + + + + QSpinBox { + padding-right: 5px; /* make room for the arrows */ + color: rgb(0, 0, 0); + background-color: rgba(255, 255, 255, 0); + +} +QSpinBox ::text:selected { + background-color: rgb(0, 0, 0); + +} +QSpinBox::up-button { + border: 1px solid rgb(87, 87, 87); + +border-top-left-radius: 15px; + background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); + width: 60px; + height: 61px; + padding: 2px; +} + +QSpinBox::up-arrow { +image: url(:/Navigation/img/Navigation/arrows.png); + width: 40px; + height: 40px; +padding: 5px; } + + + +QSpinBox::up-button:pressed { + background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #dadbde, stop: 1 #f6f7fa); +} + + +QSpinBox::down-button { + border: 1px solid rgb(87, 87, 87); +border-bottom-left-radius: 15px; + background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); + width: 60px; + height: 61px; + padding: 2px; +} + +QSpinBox::down-arrow { +image: url(:/Navigation/img/Navigation/arrows-5.png); + width: 40px; + height: 40px; +padding: 5px; +} + +QSpinBox::down-button:pressed { + background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #dadbde, stop: 1 #f6f7fa); + +} + + + + + false + + + false + + + QAbstractSpinBox::UpDownArrows + + + true + + + °C + + + 300 + + + 1 + + + 0 + + + + + + + + 71 + 131 + + + + + 100 + 16777215 + + + + + Gotham + 13 + + + + QPushButton { + border: 1px solid rgb(87, 87, 87); + background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); +border-bottom-right-radius: 15px; +border-top-right-radius: 15px; + +} + +QPushButton:pressed { + background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #dadbde, stop: 1 #f6f7fa); +} + +QPushButton:flat { + border: none; /* no border for a flat push button */ +} + +QPushButton:default { + border-color: navy; /* make the default button prominent */ +} + +QPushButton:focus { + outline: none; +} + + + + + + + :/Icons/img/icons/verification-mark.png:/Icons/img/icons/verification-mark.png + + + + 50 + 50 + + + + + + + + + + + + 0 + 80 + + + + + Gotham + 15 + + + + QPushButton { + border: 1px solid rgb(87, 87, 87); + background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); +border-radius: 15px; +} + +QPushButton:pressed { + background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #dadbde, stop: 1 #f6f7fa); +} + +QPushButton:flat { + border: none; /* no border for a flat push button */ +} + +QPushButton:default { + border-color: navy; /* make the default button prominent */ +} + +QPushButton:focus { + outline: none; +} + + + + + + + :/Icons/img/icons/snowflake.png:/Icons/img/icons/snowflake.png + + + + 50 + 50 + + + + + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QFrame #recoaterFrame{ + border: 2px solid gray; + border-radius: 20px; +} + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + 16777215 + 20 + + + + + Gotham + 16 + 50 + false + + + + color: grey; + + + RECOATER + + + + + + + + 50 + 0 + + + + + Gotham + 16 + 50 + false + false + PreferAntialias + + + + QProgressBar::chunk { + + background-color: qlineargradient(spread:pad, x1:0, y1:0.523, x2:0, y2:0.534, stop:0 rgba(130, 203, 117, 255), stop:1 rgba(66, 191, 85, 255)); +border: 1px solid green; + border-radius: 10px; + +} + +QProgressBar { + border: 1px solid rgb(87, 87, 87); + border-radius: 10px; + background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0, stop:0 rgba(150, 150, 150, 255), stop:1 rgba(180, 180, 180, 255)); +} + + + + 100 + + + 0 + + + Qt::AlignCenter + + + true + + + Qt::Horizontal + + + %p% + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 200 + 50 + + + + + 350 + 50 + + + + + Gotham + 12 + + + + QPushButton { + border: 1px solid rgb(87, 87, 87); + + background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); +border-radius:20px; +} + +QPushButton:pressed { + background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #dadbde, stop: 1 #f6f7fa); +} + +QPushButton:flat { + border: none; /* no border for a flat push button */ +} + +QPushButton:default { + border-color: navy; /* make the default button prominent */ +} + +QPushButton:focus { + outline: none; +} + + + Initial Levelling Recoat + + + + 20 + 20 + + + + + + + + + 200 + 50 + + + + + 350 + 50 + + + + + Gotham + 12 + + + + QPushButton { + border: 1px solid rgb(87, 87, 87); + + background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); +border-radius:20px; +} + +QPushButton:pressed { + background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #dadbde, stop: 1 #f6f7fa); +} + +QPushButton:flat { + border: none; /* no border for a flat push button */ +} + +QPushButton:default { + border-color: navy; /* make the default button prominent */ +} + +QPushButton:focus { + outline: none; +} + + + Home Recoater + + + + 20 + 20 + + + + + + + + + 200 + 50 + + + + + 350 + 50 + + + + + Gotham + 12 + + + + QPushButton { + border: 1px solid rgb(87, 87, 87); + + background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); +border-radius:20px; +} + +QPushButton:pressed { + background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #dadbde, stop: 1 #f6f7fa); +} + +QPushButton:flat { + border: none; /* no border for a flat push button */ +} + +QPushButton:default { + border-color: navy; /* make the default button prominent */ +} + +QPushButton:focus { + outline: none; +} + + + Heated Buffer Recoat + + + + 20 + 20 + + + + + + + + + 200 + 50 + + + + + 350 + 50 + + + + + Gotham + 12 + + + + QPushButton { + border: 1px solid rgb(87, 87, 87); + + background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); +border-radius:20px; +} + +QPushButton:pressed { + background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #dadbde, stop: 1 #f6f7fa); +} + +QPushButton:flat { + border: none; /* no border for a flat push button */ +} + +QPushButton:default { + border-color: navy; /* make the default button prominent */ +} + +QPushButton:focus { + outline: none; +} + + + Recoat + + + + 20 + 20 + + + + + + + + + + + + 200 + 50 + + + + + 500 + 50 + + + + + Gotham + 12 + + + + QPushButton { + border: 1px solid rgb(87, 87, 87); + + background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); +border-radius:20px; +} + +QPushButton:pressed { + background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #dadbde, stop: 1 #f6f7fa); +} + +QPushButton:flat { + border: none; /* no border for a flat push button */ +} + +QPushButton:default { + border-color: navy; /* make the default button prominent */ +} + +QPushButton:focus { + outline: none; +} + + + Dose-Recoat Layer + + + + 20 + 20 + + + + + + + + + 150 + 100 + + + + + Gotham + 16 + + + + QPushButton { + border: 1px solid rgb(87, 87, 87); + + background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); +border-radius:50px; +} + +QPushButton:pressed { + background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #dadbde, stop: 1 #f6f7fa); +} + +QPushButton:flat { + border: none; /* no border for a flat push button */ +} + +QPushButton:default { + border-color: navy; /* make the default button prominent */ +} + +QPushButton:focus { + outline: none; +} + + + + + + + :/Icons/img/icons/video-player-stop-button.png:/Icons/img/icons/video-player-stop-button.png + + + + 50 + 50 + + + + + + + + + + + QFrame #laserscannerFrame{ + border: 2px solid gray; + border-radius: 20px; +} + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + 16777215 + 20 + + + + + Gotham + 16 + 50 + false + + + + color: grey; + + + LASER/SCANNER + + + + + + + + 16777215 + 20 + + + + + Gotham + 12 + 50 + false + + + + color: grey; + + + LaserStatusText + + + + + + + + 200 + 50 + + + + + 350 + 50 + + + + + Gotham + 12 + + + + QPushButton { + border: 1px solid rgb(87, 87, 87); + + background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); +border-radius:20px; +} + +QPushButton:pressed { + background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #dadbde, stop: 1 #f6f7fa); +} + +QPushButton:flat { + border: none; /* no border for a flat push button */ +} + +QPushButton:default { + border-color: navy; /* make the default button prominent */ +} + +QPushButton:focus { + outline: none; +} + + + Optics ON + + + + 20 + 20 + + + + + + + + + 200 + 50 + + + + + 350 + 50 + + + + + Gotham + 12 + + + + QPushButton { + border: 1px solid rgb(87, 87, 87); + + background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); +border-radius:20px; +} + +QPushButton:pressed { + background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #dadbde, stop: 1 #f6f7fa); +} + +QPushButton:flat { + border: none; /* no border for a flat push button */ +} + +QPushButton:default { + border-color: navy; /* make the default button prominent */ +} + +QPushButton:focus { + outline: none; +} + + + Optics Off + + + + 20 + 20 + + + + + + + + + 200 + 50 + + + + + 350 + 50 + + + + + Gotham + 12 + + + + QPushButton { + border: 1px solid rgb(87, 87, 87); + + background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); +border-radius:20px; +} + +QPushButton:pressed { + background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #dadbde, stop: 1 #f6f7fa); +} + +QPushButton:flat { + border: none; /* no border for a flat push button */ +} + +QPushButton:default { + border-color: navy; /* make the default button prominent */ +} + +QPushButton:focus { + outline: none; +} + + + Scanner ON + + + + 20 + 20 + + + + + + + + + 200 + 50 + + + + + 350 + 50 + + + + + Gotham + 12 + + + + QPushButton { + border: 1px solid rgb(87, 87, 87); + + background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); +border-radius:20px; +} + +QPushButton:pressed { + background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #dadbde, stop: 1 #f6f7fa); +} + +QPushButton:flat { + border: none; /* no border for a flat push button */ +} + +QPushButton:default { + border-color: navy; /* make the default button prominent */ +} + +QPushButton:focus { + outline: none; +} + + + Scanner Off + + + + 20 + 20 + + + + + + + + + 200 + 50 + + + + + 350 + 50 + + + + + Gotham + 12 + + + + QPushButton { + border: 1px solid rgb(87, 87, 87); + + background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); +border-radius:20px; +} + +QPushButton:pressed { + background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #dadbde, stop: 1 #f6f7fa); +} + +QPushButton:flat { + border: none; /* no border for a flat push button */ +} + +QPushButton:default { + border-color: navy; /* make the default button prominent */ +} + +QPushButton:focus { + outline: none; +} + + + Start Marking + + + + 20 + 20 + + + + + + + + + 200 + 50 + + + + + 350 + 50 + + + + + Gotham + 12 + + + + QPushButton { + border: 1px solid rgb(87, 87, 87); + + background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); +border-radius:20px; +} + +QPushButton:pressed { + background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #dadbde, stop: 1 #f6f7fa); +} + +QPushButton:flat { + border: none; /* no border for a flat push button */ +} + +QPushButton:default { + border-color: navy; /* make the default button prominent */ +} + +QPushButton:focus { + outline: none; +} + + + Stop Marking + + + + 20 + 20 + + + + + + + + + + + + + + QFrame #cameraFrames{ + border: 2px solid gray; + border-radius: 20px; +} + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + 350 + 350 + + + + + 350 + 350 + + + + + + + + + + + + 16777215 + 40 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 16777215 + 20 + + + + + Gotham + 12 + 50 + false + + + + color: grey; + + + Thermal Camera + + + + + + + + 16777215 + 20 + + + + + Gotham + 12 + 50 + false + + + + color: grey; + + + maxTemp + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 10 + + + + + + + + + 350 + 350 + + + + + 350 + 350 + + + + + + + + + + + + 0 + 0 + + + + + 16777215 + 20 + + + + + Gotham + 12 + 50 + false + + + + color: grey; + + + RGB Camera Feed + + + + + + + + + + + + + + + + diff --git a/src/ui/custom_widgets.py b/src/ui/custom_widgets.py new file mode 100644 index 00000000..e5c34c86 --- /dev/null +++ b/src/ui/custom_widgets.py @@ -0,0 +1,19 @@ +from PyQt5.QtWidgets import QWidget +from PyQt5.QtGui import QImage, QPainter, QPixmap +from PyQt5.QtCore import Qt + +class ImageWidget(QWidget): + def __init__(self, parent=None): + super(ImageWidget, self).__init__(parent) + self.image = None + + def setImage(self, image): + self.image = image + self.update() + + def paintEvent(self, event): + painter = QPainter(self) + if self.image: + rect = self.rect() + scaled_image = self.image.scaled(rect.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation) + painter.drawImage(rect, scaled_image) \ No newline at end of file diff --git a/src/ui/home_screen/__pycache__/home_screen.cpython-313.pyc b/src/ui/home_screen/__pycache__/home_screen.cpython-313.pyc deleted file mode 100644 index 72f15244..00000000 Binary files a/src/ui/home_screen/__pycache__/home_screen.cpython-313.pyc and /dev/null differ diff --git a/src/ui/home_screen/home_screen.py b/src/ui/home_screen/home_screen.py index 85b8d18f..da967109 100644 --- a/src/ui/home_screen/home_screen.py +++ b/src/ui/home_screen/home_screen.py @@ -1,60 +1,136 @@ from PyQt5 import uic -from PyQt5.QtWidgets import QWidget, QToolButton, QPushButton +from PyQt5.QtWidgets import (QWidget, QToolButton, QPushButton, QLineEdit, QLabel, + QComboBox, QFrame, QProgressBar, QSizePolicy, QVBoxLayout, QFileDialog) +from PyQt5.QtGui import QImage, QPixmap +from PyQt5.QtCore import pyqtSlot +import numpy as np +import pyqtgraph as pg +from ui.custom_widgets import ImageWidget +from utils.helpers import run_async # Import the run_async decorator class HomeScreen(QWidget): def __init__(self, main_window): super(HomeScreen, self).__init__() self.main_window = main_window + self.is_paused = False # Add this line to initialize the pause flag - # Load the .ui file + # Load the UI file try: uic.loadUi('src/ui/home_screen/home_screen.ui', self) - print("UI file loaded successfully") + print("HomeScreen UI loaded successfully") except Exception as e: print(f"Failed to load UI file: {e}") - # Find buttons by their object names - self.doorLockButton = self.findChild(QToolButton, 'doorLockButton') - self.menuButton = self.findChild(QPushButton, 'menuButton') - self.stopButton = self.findChild(QPushButton, 'stopButton') - self.playPauseButton = self.findChild(QPushButton, 'playPauseButton') - self.controlButton = self.findChild(QPushButton, 'controlButton') - - # Debug prints to check if buttons are found - # print(f"doorLockButton: {self.doorLockButton}") - # print(f"menuButton: {self.menuButton}") - # print(f"stopButton: {self.stopButton}") - # print(f"playPauseButton: {self.playPauseButton}") - # print(f"controlButton: {self.controlButton}") - - # Check if buttons are found - if not all([self.doorLockButton, self.menuButton, self.stopButton, self.playPauseButton, self.controlButton]): - raise ValueError("One or more buttons not found in the UI file") - - # Connect buttons to their respective functions - self.doorLockButton.clicked.connect(self.toggle_door_lock) - self.menuButton.clicked.connect(self.open_menu) - self.stopButton.clicked.connect(self.stop_print) - self.playPauseButton.clicked.connect(self.play_pause_print) - self.controlButton.clicked.connect(self.open_control_panel) - - def toggle_door_lock(self): - # Placeholder for toggle door lock logic - print("Toggle Door Lock button clicked") - - def open_menu(self): - # Logic to open the menu screen - self.main_window.switch_screen(self.main_window.menu_screen) - print("Menu button clicked") - - def stop_print(self): - # Placeholder for stop print logic - print("Stop Print button clicked") - - def play_pause_print(self): - # Placeholder for play/pause print logic - print("Play/Pause button clicked") - - def open_control_panel(self): - # Placeholder for open control panel logic - print("Control Panel button clicked") \ No newline at end of file + # Initialize labels + self.bedTargetTemperature = self.findChild(QLabel, "bedTargetTemperature") + self.bedActualTemperature = self.findChild(QLabel, "bedActualTemperature") + self.chamberTargetTemperature = self.findChild(QLabel, "chamberTargetTemperature") + self.chamberActualTemperature = self.findChild(QLabel, "chamberActualTemperature") + self.volumeTargetTemperature = self.findChild(QLabel, "volumeTargetTemperature") + self.volumeActualTemperature = self.findChild(QLabel, "volumeActualTemperature") + self.fileInfoLabel = self.findChild(QLabel, "fileInfoLabel") + self.maxTempLabel = self.findChild(QLabel, "maxTempLabel") # Find the maxTempLabel + + # Initialize QPushButtons (if any) + self.stopButton = self.findChild(QPushButton, "stopButton") + self.playPauseButton = self.findChild(QPushButton, "playPauseButton") + self.loadFileButton = self.findChild(QPushButton, "loadFileButton") + self.stopHeatingButton = self.findChild(QPushButton, "stopHeatingButton") + self.setPIDButton = self.findChild(QPushButton, "setPIDButton") + + # Initialize QLineEdits for PID parameters + self.p_LineEdit = self.findChild(QLineEdit, "p_LineEdit") + self.i_LineEdit = self.findChild(QLineEdit, "i_LineEdit") + self.d_lineEdit = self.findChild(QLineEdit, "d_lineEdit") + + # Initialize QProgressBars + self.bedTempBar = self.findChild(QProgressBar, "bedTempBar") + self.printProgressBar = self.findChild(QProgressBar, "printProgressBar") + self.volumeTempBar = self.findChild(QProgressBar, "volumeTempBar") + self.chamberTempBar = self.findChild(QProgressBar, "chamberTempBar") + + # Initialize additional widget elements (graph and camera feed areas) + self.chamberTempGraphWidget = self.findChild(QWidget, "chamberTempGraphWidget") + self.layerPreviewWidget = self.findChild(QWidget, "layerPreviewWidget") + + # Replace the QWidget with the custom ImageWidget + thermal_camera_container = self.findChild(QWidget, "thermalCameraWidget") + self.thermalCameraWidget = ImageWidget(thermal_camera_container) + layout = QVBoxLayout(thermal_camera_container) + layout.addWidget(self.thermalCameraWidget) + self.thermalCameraWidget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + + # Replace the QWidget with the custom ImageWidget + rgb_camera_container = self.findChild(QWidget, "rgbCameraWidget") + self.rgbCameraWidget = ImageWidget(rgb_camera_container) + layout = QVBoxLayout(rgb_camera_container) + layout.addWidget(self.rgbCameraWidget) + self.rgbCameraWidget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + + # Connect the temperatures_updated signal to the update_thermal_camera_widget slot + self.main_window.printer_status.temperatures_updated.connect(self.update_thermal_camera_widget) + self.main_window.printer_status.rgb_frame_updated.connect(self.update_rgb_camera_widget) + self.main_window.printer_status.maxtemp_updated.connect(self.update_max_temp_label) # Connect the maxtemp_updated signal + + # Initialize the plot for max temperature + self.max_temp_plot = pg.PlotWidget() + self.chamberTempGraphWidget.setLayout(QVBoxLayout()) # Set a layout for chamberTempGraphWidget + self.chamberTempGraphWidget.layout().addWidget(self.max_temp_plot) + self.max_temp_curve = self.max_temp_plot.plot(pen='r') + self.max_temp_data = [] + + # Connect buttons to their respective slots + self.playPauseButton.clicked.connect(self.toggle_printing) + self.stopButton.clicked.connect(self.stop_printing) + self.loadFileButton.clicked.connect(self.load_file) # Connect the loadFileButton to the load_file method + + @run_async # Apply the run_async decorator + def start_printing_sequence(self): + self.main_window.process_automation_controller.start_printing_sequence() + + def toggle_printing(self): + if self.playPauseButton.isChecked(): + if self.is_paused: + self.is_paused = False + else: + self.main_window.process_automation_controller.process_running = True + self.start_printing_sequence() # Call the decorated method + else: + self.is_paused = True # Set the pause flag + + def stop_printing(self): + self.main_window.process_automation_controller.stop_process() + + @pyqtSlot(np.ndarray, dict) + def update_thermal_camera_widget(self, frame, temps): + if frame is not None: + image = QImage(frame.data, frame.shape[1], frame.shape[0], frame.strides[0], QImage.Format_BGR888) + self.thermalCameraWidget.setImage(image) + + @pyqtSlot(np.ndarray) + def update_rgb_camera_widget(self, frame): + if frame is not None: + height, width, channel = frame.shape + bytes_per_line = 3 * width + image = QImage(frame.data, width, height, bytes_per_line, QImage.Format_RGB888).rgbSwapped() + self.rgbCameraWidget.setImage(image) + + @pyqtSlot(float) + def update_max_temp_label(self, max_temp): + """Slot to update the text of maxTempLabel with the maximum temperature.""" + self.maxTempLabel.setText(f"Max Temp: {max_temp:.2f}°C") + self.update_max_temp_plot(max_temp) + + def update_max_temp_plot(self, max_temp): + """Update the max temperature plot with the new value.""" + self.max_temp_data.append(max_temp) + # Keep only the last 60 entries (assuming 1 entry per second for the last minute) + if len(self.max_temp_data) > 60: + self.max_temp_data.pop(0) + self.max_temp_curve.setData(self.max_temp_data) + + def load_file(self): + options = QFileDialog.Options() + file_path, _ = QFileDialog.getOpenFileName(self, "Open File", "", "All Files (*);;EMD Files (*.emd)", options=options) + if file_path: + self.main_window.open_scancard_file(file_path) \ No newline at end of file diff --git a/src/ui/home_screen/home_screen.ui b/src/ui/home_screen/home_screen.ui index bdf000ea..cfa45d36 100644 --- a/src/ui/home_screen/home_screen.ui +++ b/src/ui/home_screen/home_screen.ui @@ -1,938 +1,935 @@ - homeScreen - + Form + 0 0 - 800 - 480 + 1594 + 953 Form - - - - - - - 0 - 0 - 800 - 480 - - - - background-color: rgb(40, 40, 40); - - - QFrame::StyledPanel - - - QFrame::Raised - - - - - 40 - 90 - 61 - 31 - - - - - Gotham - 16 - 50 - false - false - false - - - - - color: white; -background-color: rgba(255, 255, 255, 0); - - - 0 - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - 20 - 20 - 31 - 31 - - - - - Gotham - 16 - 50 - false - false - false - - - - border: 1px solid rgb(87, 87, 87); - border-radius: 10px; - background-color: qlineargradient(spread:pad, x1:0, y1:0.523, x2:0, y2:0.534, stop:0 rgba(130, 203, 117, 255), stop:1 rgba(66, 191, 85, 255)); - - - - - - Qt::AlignCenter - - - - - - 200 - 150 - 20 - 41 - - - - - Gotham - 16 - - - - -color: black; -background-color: rgb(255, 255, 255,0); - - - 1 - - - - - - 0 - 0 - 801 - 81 - - - - - Gotham - 16 - - - - border-bottom: 1px solid rgb(87, 87, 87); - background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); - - - QFrame::NoFrame - - - QFrame::Raised - - - - - - - - - 30 - 210 - 61 - 31 - - - - - Gotham - 16 - 50 - false - false - false - - - - - color: white; -background-color: rgba(0, 0, 0, 0); - - - 0 - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - 560 - 10 - 231 - 31 - - - - - Gotham - 16 - 50 - false - false - false - - - - + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + 0 + 300 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 200 + 200 + + + + + 16777215 + 350 + + + + background-color: rgb(232, 232, 232); + + + + + + + + + + + + 16777215 + 30 + + + + + Gotham + 16 + 50 + false + + + + color: grey; + + + Chamber Temperature Graph + + + + + + + + 16777215 + 16777215 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 350 + 0 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 150 + + + + + 16777215 + 200 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 69 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + Gotham + 16 + 50 + false + false + false + + + + + color: grey; background-color: rgba(255, 255, 255, 0); - - - Not Connected - - - true - - - Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing - - - true - - - - - - 170 - 200 - 71 - 51 - - - - - Gotham - 16 - 50 - false - false - false - - - - - color: white; + + + 0 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + Gotham + 16 + 50 + false + false + false + + + + + color: grey; background-color: rgba(0, 0, 0, 0); - - - 0 - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - 250 - 110 - 21 - 121 - - - - - Gotham - 16 - - - - QProgressBar::chunk { + + + 0 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + Gotham + 16 + + + + QProgressBar::chunk { border-radius: 5px; background-color: qlineargradient(spread:pad, x1:0.517, y1:0, x2:0.522, y2:0, stop:0.0336134 rgba(74, 183, 255, 255), stop:1 rgba(53, 173, 242, 255)); } QProgressBar { - border: 1px solid white; + border: 1px solid grey; border-radius: 5px; } - - - 300 - - - 200 - - - Qt::AlignCenter - - - false - - - Qt::Vertical - - - %v - - - - - - 180 - 90 - 61 - 31 - - - - - Gotham - 16 - 50 - false - false - false - - - - - color: white; + + + 300 + + + 200 + + + Qt::AlignCenter + + + false + + + Qt::Vertical + + + %v + + + + + + + + Gotham + 16 + 50 + false + false + false + + + + + color: grey; +background-color: rgba(255, 255, 255, 0); + + + °C + + + + + + + + + + + 100 + 0 + + + + + Gotham + 12 + 50 + false + false + false + + + + + color: grey; +background-color: rgba(0, 0, 0, 0); + + + Bed + + + true + + + Qt::AlignCenter + + + true + + + + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 69 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + Gotham + 16 + 50 + false + false + false + + + + + color: grey; background-color: rgba(255, 255, 255, 0); - - - 0 - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - true - - - - 489 - 80 - 311 - 291 - - - - - Gotham - 16 - - - - background-color: rgba(255, 255, 255, 0); - - - - - - :/Logos & Branding/img/Logos/thumbnail.png - - - true - - - - - - 180 - 148 - 51 - 51 - - - - - Gotham - 16 - 50 - false - false - false - - - - - color: white; - - - - - - :/Icons/img/icons/Nozzle.png - - - true - - - - - - 105 - 110 - 21 - 121 - - - - - Gotham - 16 - - - - QProgressBar::chunk { + + + 0 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + Gotham + 16 + 50 + false + false + false + + + + + color: grey; +background-color: rgba(0, 0, 0, 0); + + + 0 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + Gotham + 16 + + + + QProgressBar::chunk { border-radius: 5px; background-color: qlineargradient(spread:pad, x1:0.517, y1:0, x2:0.522, y2:0, stop:0.0336134 rgba(74, 183, 255, 255), stop:1 rgba(53, 173, 242, 255)); } QProgressBar { - border: 1px solid white; + border: 1px solid grey; border-radius: 5px; } - - - 300 - - - 200 - - - Qt::AlignCenter - - - false - - - Qt::Vertical - - - %v - - - - - - 50 - 150 - 20 - 41 - - - - - Gotham - 16 - - - - -color: black; -background-color: rgb(255, 255, 255,0); - - - 0 - - - - - - 55 - 10 - 551 - 61 - - - - - Gotham - 16 - 50 - false - false - false - - - - + + + 300 + + + 200 + + + Qt::AlignCenter + + + false + + + Qt::Vertical + + + %v + + + + + + + + Gotham + 16 + 50 + false + false + false + + + + + color: grey; background-color: rgba(255, 255, 255, 0); - - - STATUS - - - true - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - 32 - 148 - 51 - 51 - - - - - Gotham - 16 - 50 - false - false - false - - - - - color: white; - - - - - - :/Icons/img/icons/Nozzle.png - - - true - - - - - - 460 - 80 - 20 - 281 - - - - - Gotham - 16 - - - - Qt::Vertical - - - - - - 2 - 240 - 471 - 121 - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - 6 - - - 9 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - Gotham - 16 - 50 - false - - - - color: rgb(255, 255, 255); - - - File Name: - - - - - - - - Gotham - 16 - 50 - false - - - - color: rgb(255, 255, 255); - - - Print Time: - - - - - - - - Gotham - 16 - - - - color: rgb(255, 255, 255); -background-color: rgba(255, 255, 255, 0); - - - printTime - - - - - - - - Gotham - 16 - 50 - false - - - - color: rgb(255, 255, 255); - - - Time Left: - - - - - - - - Gotham - 16 - - - - color: rgb(255, 255, 255); -background-color: rgba(255, 255, 255, 0); - - - timeLeft - - - false - - - - - - - - Gotham - 16 - - - - color: rgb(255, 255, 255); -background-color: rgba(255, 255, 255, 0); - - - fileName - - - true - - - false - - - - - - - - - 340 - 220 - 41 - 21 - - - - - Gotham - 16 - 50 - false - false - false - - - - - color: white; + + + °C + + + + + + + + + + + 100 + 0 + + + + + Gotham + 12 + 50 + false + false + false + + + + + color: grey; background-color: rgba(0, 0, 0, 0); - - - 0 - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - 340 - 100 - 41 - 21 - - - - - Gotham - 16 - 50 - false - false - false - - - - - color: white; -background-color: rgba(255, 255, 255, 0); - - - 0 - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - 320 - 140 - 61 - 61 - - - - - Gotham - 16 - 50 - false - false - false - - - - - color: white; + + + Volume + + + true + + + Qt::AlignCenter + + + true + + + + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 69 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + Gotham + 16 + 50 + false + false + false + + + + + color: grey; background-color: rgba(255, 255, 255, 0); - - - - - - :/Icons/img/icons/bed.png - - - true - - - - - - -2 - 360 - 804 - 121 - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - 85 - - - - - Gotham - 16 - - - - QPushButton { - border: 1px solid rgb(87, 87, 87); - background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); -border-top-left-radius: 10px; - -} - -QPushButton:pressed { - - background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, - stop: 0 #dadbde, stop: 1 #f6f7fa); -} - -QPushButton:flat { - border: none; /* no border for a flat push button */ + + + 0 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + Gotham + 16 + 50 + false + false + false + + + + + color: grey; +background-color: rgba(0, 0, 0, 0); + + + 0 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + Gotham + 16 + + + + QProgressBar::chunk { + border-radius: 5px; + background-color: qlineargradient(spread:pad, x1:0.517, y1:0, x2:0.522, y2:0, stop:0.0336134 rgba(74, 183, 255, 255), stop:1 rgba(53, 173, 242, 255)); } -QPushButton:default { - border-color: navy; /* make the default button prominent */ +QProgressBar { + border: 1px solid grey; + border-radius: 5px; } - -QPushButton:focus { - outline: none; -} - - - - - - - :/Icons/img/icons/menu.png:/Icons/img/icons/menu.png - - - - 50 - 50 - - - - false - - - false - - - false - - - false - - - - - - - - 0 - 85 - - - - - Gotham - 16 - - - - QPushButton { - border: 1px solid rgb(87, 87, 87); + + + + 300 + + + 200 + + + Qt::AlignCenter + + + false + + + Qt::Vertical + + + %v + + + + + + + + Gotham + 16 + 50 + false + false + false + + + + + color: grey; +background-color: rgba(255, 255, 255, 0); + + + °C + + + + + + + + + + + 100 + 0 + + + + + Gotham + 12 + 50 + false + false + false + + + + + color: grey; +background-color: rgba(0, 0, 0, 0); + + + Chamber + + + true + + + Qt::AlignCenter + + + true + + + + + + + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 150 + 150 + + + + + Gotham + 16 + + + + QPushButton { + border: 1px solid rgb(87, 87, 87); background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); +border-radius:50px; } QPushButton:pressed { @@ -951,52 +948,41 @@ QPushButton:default { QPushButton:focus { outline: none; } - - - - - - - :/Icons/img/icons/settings-1.png:/Icons/img/icons/settings-1.png - - - - 50 - 50 - - - - false - - - false - - - false - - - false - - - - - - - - 0 - 85 - - - - - Gotham - 16 - - - - QPushButton { + + + + + + + :/Icons/img/icons/video-player-stop-button.png:/Icons/img/icons/video-player-stop-button.png + + + + 50 + 50 + + + + + + + + + 150 + 150 + + + + + Gotham + 16 + + + + QPushButton { border: 1px solid rgb(87, 87, 87); background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); +border-radius:50px; } @@ -1017,58 +1003,129 @@ QPushButton:default { QPushButton:focus { outline: none; } - - - - - - - :/Icons/img/icons/play-button.png - :/Icons/img/icons/pause-button.png:/Icons/img/icons/play-button.png - - - - 50 - 50 - - - - true - - - false - - - false - - - false - - - false - - - - - - - - 0 - 85 - - - - - Gotham - 16 - - - - QPushButton { + + + + + + + :/Icons/img/icons/play-button.png + :/Icons/img/icons/pause-button.png:/Icons/img/icons/play-button.png + + + + 50 + 50 + + + + true + + + false + + + false + + + false + + + false + + + + + + + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + + + Gotham + 16 + 50 + false + + + + color: grey; + + + File Name: + + + + + + + + 150 + 50 + + + + + 200 + 50 + + + + + Gotham + 16 + + + + QPushButton { border: 1px solid rgb(87, 87, 87); background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); -border-top-right-radius: 10px; +border-radius:20px; } QPushButton:pressed { @@ -1087,45 +1144,131 @@ QPushButton:default { QPushButton:focus { outline: none; } - - - - - - - :/Icons/img/icons/video-player-stop-button.png:/Icons/img/icons/video-player-stop-button.png - - - - 50 - 50 - - - - - - - - - - - - 50 - 0 - - - - - Gotham - 16 - 50 - false - false - PreferAntialias - - - - QProgressBar::chunk { + + + Load File + + + + :/Icons/img/icons/folder.png:/Icons/img/icons/folder.png + + + + 20 + 20 + + + + + + + + + + + + 400 + 16777215 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 300 + 300 + + + + + 300 + 300 + + + + background-color: rgb(232, 232, 232); + + + + + + + + + 16777215 + 20 + + + + + Gotham + 16 + 50 + false + + + + color: grey; + + + Layer Preview + + + + + + + + + + + + + + + + + 50 + 0 + + + + + Gotham + 16 + 50 + false + false + PreferAntialias + + + + QProgressBar::chunk { background-color: qlineargradient(spread:pad, x1:0, y1:0.523, x2:0, y2:0.534, stop:0 rgba(130, 203, 117, 255), stop:1 rgba(66, 191, 85, 255)); border: 1px solid green; @@ -1138,267 +1281,226 @@ QProgressBar { background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0, stop:0 rgba(150, 150, 150, 255), stop:1 rgba(180, 180, 180, 255)); } - - - 100 - - - 0 - - - Qt::AlignCenter - - - true - - - Qt::Horizontal - - - %p% - - - - - - - - - 420 - 100 - 70 - 21 - - - - - Gotham - 16 - 50 - false - false - false - - - - - color: white; -background-color: rgba(255, 255, 255, 0); - - - °C - - - - - true - - - - 709 - 80 - 91 - 101 - - - - - 0 - 0 - - - - - Gotham - 7 - - - - QToolButton { - padding-top: 20px; - border: 1px solid rgb(87, 87, 87); - background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); - -border-top-left-radius: 10px; - -border-bottom-left-radius: 10px; - - - -} - -QToolButton:pressed { - background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, - stop: 0 #dadbde, stop: 1 #f6f7fa); -} - -QToolButton:flat { - border: none; /* no border for a flat push button */ -} - -QToolButton:default { - border-color: navy; /* make the default button prominent */ -} - - - Toggle Door - - - - :/Icons/img/icons/doorUnlock.png - :/Icons/img/icons/doorLock.png:/Icons/img/icons/doorUnlock.png - - - - 50 - 50 - - - - true - - - false - - - Qt::ToolButtonTextUnderIcon - - - false - - - - - - 395 - 110 - 21 - 121 - - - - - Gotham - 16 - - - - QProgressBar::chunk { - border-radius: 5px; - background-color: qlineargradient(spread:pad, x1:0.517, y1:0, x2:0.522, y2:0, stop:0.0336134 rgba(74, 183, 255, 255), stop:1 rgba(53, 173, 242, 255)); -} - -QProgressBar { - border: 1px solid white; - border-radius: 5px; -} - - - - 300 - - - 10 - - - Qt::AlignCenter - - - false - - - Qt::Vertical - - - %v - - - - - - 130 - 100 - 70 - 21 - - - - - Gotham - 16 - 50 - false - false - false - - - - - color: white; -background-color: rgba(255, 255, 255, 0); - - - °C - - - - - - 280 - 100 - 70 - 21 - - - - - Gotham - 16 - 50 - false - false - false - - - - - color: white; -background-color: rgba(255, 255, 255, 0); - - - °C - - - tool0TargetTemperature - label - statusBar - tool0ActualTemperature - ipStatus - tool1ActualTemperature - tool1TempBar - tool1TargetTemperature - printPreviewMain - tool1Label - tool0TempBar - label_3 - printerStatus - tool0Label - fileInfoFrame - bedActualTemperatute - bedTargetTemperature - bedLabel - celciusLabel - doorLockButton - bedTempBar - celciusLabel_3 - celciusLabel_2 - line - buttonProgrssFrame - printerStatusColour - + + + 100 + + + 0 + + + Qt::AlignCenter + + + true + + + Qt::Horizontal + + + %p% + + + + + + + + + + + + + + 400 + 0 + + + + + 400 + 20000 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + 350 + 350 + + + + + 400 + 400 + + + + + + + + + + + + 16777215 + 40 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 16777215 + 20 + + + + + Gotham + 12 + 50 + false + + + + color: grey; + + + Thermal Camera Max Temp: + + + + + + + + 16777215 + 20 + + + + + Gotham + 12 + 50 + false + + + + color: grey; + + + maxTemp + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 20 + + + + + + + + + 350 + 350 + + + + + 400 + 400 + + + + + + + + + + + + 0 + 0 + + + + + 16777215 + 20 + + + + + Gotham + 12 + 50 + false + + + + color: grey; + + + RGB Camera Feed + + + + + + + + + + - + diff --git a/src/ui/loading_screen/__pycache__/loading_screen.cpython-313.pyc b/src/ui/loading_screen/__pycache__/loading_screen.cpython-313.pyc deleted file mode 100644 index 40dadece..00000000 Binary files a/src/ui/loading_screen/__pycache__/loading_screen.cpython-313.pyc and /dev/null differ diff --git a/src/ui/loading_screen/loading_screen.py b/src/ui/loading_screen/loading_screen.py index 192c46e0..0b5193e4 100644 --- a/src/ui/loading_screen/loading_screen.py +++ b/src/ui/loading_screen/loading_screen.py @@ -7,13 +7,20 @@ class LoadingScreen(QWidget): def __init__(self, main_window): super(LoadingScreen, self).__init__() self.main_window = main_window - uic.loadUi('src/ui/loading_screen/loading_screen.ui', self) + + # Load the .ui file + try: + uic.loadUi('src/ui/loading_screen/loading_screen.ui', self) + print("UI file loaded successfully") + except Exception as e: + print(f"Failed to load UI file: {e}") + # Set up the loading GIF - self.loadingGif = self.findChild(QLabel, 'loadingGif') - self.movie = QMovie(":/Misc/img/loading_animation.gif") - self.loadingGif.setMovie(self.movie) - self.movie.start() + # self.loadingGif = self.findChild(QLabel, 'loadingGif') + # self.movie = QMovie(":/Misc/img/loading_animation.gif") + # self.loadingGif.setMovie(self.movie) + # self.movie.start() # Set up the timer to switch to the home screen after 20 seconds self.timer = QTimer(self) @@ -21,6 +28,6 @@ def __init__(self, main_window): self.timer.start(1000) # 20000 milliseconds = 20 seconds def stop_movie_and_switch(self): - self.movie.stop() + # self.movie.stop() self.timer.stop() - self.main_window.switch_screen(self.main_window.home_screen) \ No newline at end of file + self.main_window.switch_screen(self.main_window.tab_screen) \ No newline at end of file diff --git a/src/ui/loading_screen/loading_screen.ui b/src/ui/loading_screen/loading_screen.ui index 7dd14acd..ac6be39b 100644 --- a/src/ui/loading_screen/loading_screen.ui +++ b/src/ui/loading_screen/loading_screen.ui @@ -1,277 +1,288 @@ - loadingScreenWidget - + Form + 0 0 - 800 - 480 + 1234 + 914 Form - + background-color: rgb(21, 25, 33); - - - - 0 - 0 - 800 - 480 - + + + 0 - - background-color: rgb(21, 25, 33); + + 0 - - QFrame::StyledPanel + + 0 - - QFrame::Raised + + 0 - - - - 50 - 0 - 711 - 161 - - - - - - - :/Logos & Branding/img/Logos/Fracktal Logo.png - - - true - - - - - - 0 - -10 - 800 - 600 - - - - Qt::NoContextMenu - - - background-color: rgb(255, 255, 255,0); - - - - - - :/Misc/img/loading_animation.gif - - - true - - - - - - 360 - 270 - 101 - 31 - - - - - Gotham Medium - 12 - - - - Qt::NoContextMenu - - - color: rgb(255, 255, 255); -background-color: rgba(255, 255, 255, 0); - - - Loading ... - - - true - - - false - - - - - - 0 - 430 - 800 - 40 - - - - - 800 - 40 - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - 15 - 20 - - - - - 20 - 20 - - - - - - - :/Logos & Branding/img/Logos/control_center_logo.png - - - true - - - - - - - - 0 - 20 - - - - - 400 - 20 - - - - - Gotham Medium - 12 - - - - Qt::NoContextMenu - - - color: rgb(255, 255, 255); + + 0 + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 800 + 40 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 15 + 20 + + + + + 20 + 20 + + + + + + + :/Logos & Branding/img/Logos/control_center_logo.png + + + true + + + + + + + + 0 + 20 + + + + + 400 + 20 + + + + + Gotham Medium + 12 + + + + Qt::NoContextMenu + + + color: rgb(255, 255, 255); background-color: rgba(255, 255, 255, 0); - - - - - - :/Logos & Branding/img/Logos/control_center_logo_text.png - - - true - - - false - - - - - - - Qt::Horizontal - - - - 60 - 20 - - - - - - - - - 25 - 25 - - - - - 50 - 50 - - - - - MS Shell Dlg 2 - 9 - - - - Qt::NoContextMenu - - - color: rgb(255, 255, 255); + + + + + + :/Logos & Branding/img/Logos/control_center_logo_text.png + + + true + + + false + + + + + + + Qt::Horizontal + + + + 60 + 20 + + + + + + + + + 25 + 25 + + + + + 50 + 50 + + + + + MS Shell Dlg 2 + 9 + + + + Qt::NoContextMenu + + + color: rgb(255, 255, 255); background-color: rgba(255, 255, 255, 0); - - - V 1.0 - - - true - - - false - - - - - - loadingGif - loading - Logo - frame_2 - + + + V 1.0 + + + true + + + false + + + + + + + + + + + 711 + 161 + + + + + 711 + 161 + + + + + + + :/Logos & Branding/img/Logos/Fracktal Logo.png + + + true + + + + + + + + 800 + 600 + + + + + 800 + 600 + + + + Qt::NoContextMenu + + + background-color: rgb(255, 255, 255,0); + + + + + + :/Misc/img/loading_animation.gif + + + true + + + + + + + - + + diff --git a/src/ui/main_window.py b/src/ui/main_window.py index e0529f0e..89738292 100644 --- a/src/ui/main_window.py +++ b/src/ui/main_window.py @@ -1,14 +1,30 @@ from PyQt5.QtWidgets import QMainWindow, QWidget, QVBoxLayout, QStackedWidget -from ui.home_screen.home_screen import HomeScreen from ui.loading_screen.loading_screen import LoadingScreen -from ui.menu_screen.menu_screen import MenuScreen -from ui.settings_screen.settings_screen import SettingsScreen +from ui.tab_screen.tab_screen import TabScreen +from config import Config +from models.printer_status import PrinterStatus +from PyQt5.QtCore import QTimer +from temperatureController.chamberTemperatureController import ChamberTemperatureController # Ensure this import is present +from Feeltek.scanCard import Scancard # Import Scancard +from processAutomationController.processAutomationController import ProcessAutomationController +from utils.helpers import run_async + +if not Config.DEVELOPMENT_MODE: + from temperatureController.heaterBoard import HeaterBoard + from thermalCamera.thermal_camera import ThermalCamera + from rgbCamera.rgbCamera import RGBCamera + from moonrakerClient.moonrakerClient import MoonrakerAPI + import ui.resources.resource_rc # Ensure resources are loaded import traceback class MainWindow(QMainWindow): def __init__(self): super(MainWindow, self).__init__() + + self.printer_status = PrinterStatus() # Create an instance of the PrinterStatus model + self.process_automation_controller = ProcessAutomationController(self) # Initialize ProcessAutomationController + self.central_widget = QWidget() self.setCentralWidget(self.central_widget) @@ -17,42 +33,193 @@ def __init__(self): self.stacked_widget = QStackedWidget() self.layout.addWidget(self.stacked_widget) + + if not Config.DEVELOPMENT_MODE: + self.thermal_camera = ThermalCamera(roi=(2, 13, 59, 64)) + self.thermal_camera.thermal_camera_frame_ready.connect(self.update_frame) + self.thermal_camera.max_temp_signal.connect(self.update_max_temp) # Connect max_temp_signal to update_max_temp + self.thermal_camera.start() + + self.rgb_camera = RGBCamera() + self.rgb_camera.rgb_camera_frame_ready.connect(self.update_rgb_frame) + self.rgb_camera.start() + else: + self.thermal_camera = None + self.rgb_camera = None + + # Initialize HeaterBoard and ChamberTemperatureController if not in development mode + if not Config.DEVELOPMENT_MODE: + self.chamber_temp_controller = ChamberTemperatureController(self.printer_status) + else: + self.chamber_temp_controller = None + + # Initialize MoonrakerAPI if not in development mode + if not Config.DEVELOPMENT_MODE: + self.moonraker_api = MoonrakerAPI('http://10.20.1.135') + else: + self.moonraker_api = MockMoonrakerAPI() + + # Initialize Scancard + self.scancard = Scancard(self) if not Config.DEVELOPMENT_MODE else MockScancard(self) + + # Set up a QTimer to periodically check the Scancard status + self.scancard_timer = QTimer(self) + self.scancard_timer.timeout.connect(self.handle_scancard_status_change) + self.scancard_timer.start(5000) # Check status every 5000 ms (5 seconds) # Load sub UIs based on configuration - self.load_home_screen() self.load_loading_screen() - self.load_menu_screen() - self.load_settings_screen() + self.load_tab_screen() self.switch_screen(self.loading_screen) # Adjust the size of the main window to fit its contents self.adjustSize() - def load_home_screen(self): - self.home_screen = HomeScreen(self) - self.stacked_widget.addWidget(self.home_screen) + self.process_automation_controller.progress_update_signal.connect(self.update_progress_bar) + + def update_progress_bar(self, value): + self.home_screen.printProgressBar.setValue(value) + self.control_screen.recoaterProgressBar.setValue(value) def load_loading_screen(self): self.loading_screen = LoadingScreen(self) self.stacked_widget.addWidget(self.loading_screen) - - def load_menu_screen(self): - self.menu_screen = MenuScreen(self) - self.stacked_widget.addWidget(self.menu_screen) - - def load_settings_screen(self): - self.settings_screen = SettingsScreen(self) - self.stacked_widget.addWidget(self.settings_screen) + + def load_tab_screen(self): + self.tab_screen = TabScreen(self) + self.stacked_widget.addWidget(self.tab_screen) def switch_screen(self, widget): print(f"Switching to screen: {widget}") - traceback.print_stack() # Print the call stack self.stacked_widget.setCurrentWidget(widget) -# self.adjustSize() # Adjust size after switching screens + self.adjustSize() # Adjust size after switching screens + + def switch_to_tab_screen(self): + self.switch_screen(self.tab_screen) + + def update_frame(self, frame, chamberTemperatures): + if frame is not None and chamberTemperatures is not None: + # Convert temps values to regular float + converted_temps = {key: float(value) for key, value in chamberTemperatures.items()} + self.printer_status.updateTemperatures(frame, converted_temps) + + def update_max_temp(self, max_temp): + self.printer_status.updateMaxTemp(max_temp) + + def update_rgb_frame(self, frame): + if frame is not None: + self.printer_status.updateRGBFrame(frame) + + # Add methods to interact with Scancard + def start_scancard_mark(self): + self.scancard.start_mark() + + def stop_scancard_mark(self): + self.scancard.stop_mark() + + @run_async + def handle_scancard_status_change(self): + future = self.scancard.get_working_status() + future.add_done_callback(self.update_scancard_status) + + def update_scancard_status(self, future): + try: + status = future.result() + self.printer_status.updateScancardStatus(status) + self.control_screen.scanCardStatusLabel.setText("Status: " + self.printer_status.scancard_status) + # print(f"Scancard status: {self.printer_status.scancard_status}") + except Exception as e: + print(f"Failed to update Scancard status: {e}") + + def open_scancard_file(self, file_path: str): + close_future = self.scancard.close_file() + close_future.add_done_callback(lambda f: self._handle_close_file_result_and_open(f, file_path)) + + def _handle_close_file_result_and_open(self, future, file_path: str): + try: + result = future.result() + if result is None: + raise ValueError("No result returned from close_file command") + meaning = self._get_scancard_return_meaning(result.get("ret_value")) + print(f"Close file result: {result} - {meaning}") + except Exception as e: + print(f"Failed to close Scancard file: {e}") + finally: + print("Executing finally block") + self._open_scancard_file(file_path) + + def _open_scancard_file(self, file_path: str): + print(f"Opening Scancard file: {file_path}") + future = self.scancard.open_file(file_path) + future.add_done_callback(lambda f: self._handle_open_file_result(f, file_path)) + + def _handle_open_file_result(self, future, file_path: str): + try: + result = future.result() + if result is None: + raise ValueError("No result returned from open_file command") + meaning = self._get_scancard_return_meaning(result.get("ret_value")) + print(f"Open file result: {result} - {meaning}") + self.update_file_info_label(file_path) + except Exception as e: + print(f"Failed to open Scancard file: {e}") + + def _get_scancard_return_meaning(self, ret_value): + meanings = { + 1: "Execution successful", + 0: "Not executed", + -1: "Failed to open", + -2: "File does not exist" + } + return meanings.get(ret_value, "Unknown return value") + + def update_file_info_label(self, file_path: str): + self.home_screen.fileInfoLabel.setText(file_path) + +class MockMoonrakerAPI: + def __init__(self): + print("MockMoonrakerAPI initialized") + + def send_gcode(self, cmd): + print(f"MockMoonrakerAPI.send_gcode called with cmd: {cmd}") + + def query_status(self): + print("MockMoonrakerAPI.query_status called") + return {"status": "mock_status"} + + def query_temperatures(self): + print("MockMoonrakerAPI.query_temperatures called") + return {"temperatures": "mock_temperatures"} + +class MockScancard: + def __init__(self, main_window): + print("MockScancard initialized") + + def start_mark(self): + print("MockScancard.start_mark called") + + def stop_mark(self): + print("MockScancard.stop_mark called") + + def get_working_status(self): + print("MockScancard.get_working_status called") + return MockFuture() + + def open_file(self, file_path): + print(f"MockScancard.open_file called with file_path: {file_path}") + return MockFuture() + + def close_file(self): + print("MockScancard.close_file called") + return MockFuture() + +class MockFuture: + def add_done_callback(self, callback): + print("MockFuture.add_done_callback called") + callback(self) - def switch_to_home_screen(self): - self.switch_screen(self.home_screen) + def result(self): + print("MockFuture.result called") + return {"ret_value": 1} # Simulated response - def switch_to_network_settings(self): - self.switch_screen(self.network_settings) diff --git a/src/ui/menu_screen/__pycache__/menu_screen.cpython-313.pyc b/src/ui/menu_screen/__pycache__/menu_screen.cpython-313.pyc deleted file mode 100644 index 6c1e7dff..00000000 Binary files a/src/ui/menu_screen/__pycache__/menu_screen.cpython-313.pyc and /dev/null differ diff --git a/src/ui/menu_screen/menu_screen.py b/src/ui/menu_screen/menu_screen.py deleted file mode 100644 index 4a36cf19..00000000 --- a/src/ui/menu_screen/menu_screen.py +++ /dev/null @@ -1,61 +0,0 @@ -from PyQt5 import uic -from PyQt5.QtWidgets import QWidget, QToolButton, QPushButton - -class MenuScreen(QWidget): - def __init__(self, main_window): - super(MenuScreen, self).__init__() - self.main_window = main_window - uic.loadUi('src/ui/menu_screen/menu_screen.ui', self) - - # Find buttons by their object names - self.menuPrintButton = self.findChild(QToolButton, 'menuPrintButton') - self.menuControlButton = self.findChild(QToolButton, 'menuControlButton') - self.menuCalibrateButton = self.findChild(QToolButton, 'menuCalibrateButton') - self.menuCartButton = self.findChild(QToolButton, 'menuCartButton') - self.menuSettingsButton = self.findChild(QToolButton, 'menuSettingsButton') - self.menuBackButton = self.findChild(QPushButton, 'menuBackButton') - - # Debug prints to check if buttons are found - # print(f"menuPrintButton: {self.menuPrintButton}") - # print(f"menuControlButton: {self.menuControlButton}") - # print(f"menuCalibrateButton: {self.menuCalibrateButton}") - # print(f"menuCartButton: {self.menuCartButton}") - # print(f"menuSettingsButton: {self.menuSettingsButton}") - # print(f"menuBackButton: {self.menuBackButton}") - - # Check if buttons are found - if not all([self.menuPrintButton, self.menuControlButton, self.menuCalibrateButton, self.menuCartButton, self.menuSettingsButton, self.menuBackButton]): - raise ValueError("One or more buttons not found in the UI file") - - # Connect buttons to their respective functions - self.menuPrintButton.clicked.connect(self.open_print) - self.menuControlButton.clicked.connect(self.open_control) - self.menuCalibrateButton.clicked.connect(self.open_calibrate) - self.menuCartButton.clicked.connect(self.open_cart) - self.menuSettingsButton.clicked.connect(self.open_settings) - self.menuBackButton.clicked.connect(self.go_back) - - def open_print(self): - # Placeholder for open print logic - print("Print button clicked") - - def open_control(self): - # Placeholder for open control logic - print("Control button clicked") - - def open_calibrate(self): - # Placeholder for open calibrate logic - print("Calibrate button clicked") - - def open_cart(self): - # Placeholder for open cart logic - print("Cart button clicked") - - def open_settings(self): - # Logic to open the settings screen - self.main_window.switch_screen(self.main_window.settings_screen) - print("Settings button clicked") - - def go_back(self): - # Placeholder for go back logic - print("Back button clicked") \ No newline at end of file diff --git a/src/ui/menu_screen/menu_screen.ui b/src/ui/menu_screen/menu_screen.ui deleted file mode 100644 index 5e08de3b..00000000 --- a/src/ui/menu_screen/menu_screen.ui +++ /dev/null @@ -1,395 +0,0 @@ - - - menu_screen - - - - 0 - 0 - 800 - 480 - - - - Form - - - - - -1 - 0 - 801 - 480 - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 266 - 240 - - - - - Gotham - 16 - - - - QToolButton { - padding-top: 20px; - border: 1px solid rgb(87, 87, 87); - background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); - - - -} - -QToolButton:pressed { - background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, - stop: 0 #dadbde, stop: 1 #f6f7fa); -} - -QToolButton:flat { - border: none; /* no border for a flat push button */ -} - -QToolButton:default { - border-color: navy; /* make the default button prominent */ -} - - - Print - - - - ../../../../TwinDragon600x600CM4TouchUI/octoprint_TwinDragon600x600CM4TouchUI/templates/img/printer.png../../../../TwinDragon600x600CM4TouchUI/octoprint_TwinDragon600x600CM4TouchUI/templates/img/printer.png - - - - 150 - 150 - - - - false - - - Qt::ToolButtonTextUnderIcon - - - - - - - - 266 - 240 - - - - - Gotham - 16 - - - - QToolButton { -padding-top: 20px; - border: 1px solid rgb(87, 87, 87); - background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); - -} - -QToolButton:pressed { - background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, - stop: 0 #dadbde, stop: 1 #f6f7fa); -} - -QToolButton:flat { - border: none; /* no border for a flat push button */ -} - -QToolButton:default { - border-color: navy; /* make the default button prominent */ -} - - - Control - - - - ../../../../TwinDragon600x600CM4TouchUI/octoprint_TwinDragon600x600CM4TouchUI/templates/img/settings-1.png../../../../TwinDragon600x600CM4TouchUI/octoprint_TwinDragon600x600CM4TouchUI/templates/img/settings-1.png - - - - 150 - 150 - - - - false - - - Qt::ToolButtonTextUnderIcon - - - - - - - - 266 - 240 - - - - - Gotham - 16 - - - - QToolButton { -padding-top: 20px; - border: 1px solid rgb(87, 87, 87); - background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); - -} - -QToolButton:pressed { - background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, - stop: 0 #dadbde, stop: 1 #f6f7fa); -} - -QToolButton:flat { - border: none; /* no border for a flat push button */ -} - -QToolButton:default { - border-color: navy; /* make the default button prominent */ -} - - - Calibrate - - - - ../../../../TwinDragon600x600CM4TouchUI/octoprint_TwinDragon600x600CM4TouchUI/templates/img/reload.png../../../../TwinDragon600x600CM4TouchUI/octoprint_TwinDragon600x600CM4TouchUI/templates/img/reload.png - - - - 150 - 150 - - - - false - - - Qt::ToolButtonTextUnderIcon - - - - - - - - 266 - 240 - - - - - Gotham - 16 - - - - QToolButton { -padding-top: 20px; - border: 1px solid rgb(87, 87, 87); - background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); - -} - -QToolButton:pressed { - background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, - stop: 0 #dadbde, stop: 1 #f6f7fa); -} - -QToolButton:flat { - border: none; /* no border for a flat push button */ -} - -QToolButton:default { - border-color: navy; /* make the default button prominent */ -} - - - Cart - - - - ../../../../TwinDragon600x600CM4TouchUI/octoprint_TwinDragon600x600CM4TouchUI/templates/img/cart.png../../../../TwinDragon600x600CM4TouchUI/octoprint_TwinDragon600x600CM4TouchUI/templates/img/cart.png - - - - 150 - 150 - - - - false - - - Qt::ToolButtonTextUnderIcon - - - false - - - - - - - - 266 - 240 - - - - - Gotham - 16 - - - - QToolButton { -padding-top: 20px; - border: 1px solid rgb(87, 87, 87); - background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); - -} - -QToolButton:pressed { - background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, - stop: 0 #dadbde, stop: 1 #f6f7fa); -} - -QToolButton:flat { - border: none; /* no border for a flat push button */ -} - -QToolButton:default { - border-color: navy; /* make the default button prominent */ -} - - - Settings - - - - ../../../../TwinDragon600x600CM4TouchUI/octoprint_TwinDragon600x600CM4TouchUI/templates/img/settings.png../../../../TwinDragon600x600CM4TouchUI/octoprint_TwinDragon600x600CM4TouchUI/templates/img/settings.png - - - - 150 - 150 - - - - false - - - Qt::ToolButtonTextUnderIcon - - - - - - - - 266 - 240 - - - - - Gotham - 16 - - - - QPushButton { - border: 1px solid rgb(87, 87, 87); - background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); - -} - -QPushButton:pressed { - background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, - stop: 0 #dadbde, stop: 1 #f6f7fa); -} - -QPushButton:flat { - border: none; /* no border for a flat push button */ -} - -QPushButton:default { - border-color: navy; /* make the default button prominent */ -} - - - Back - - - - ../../../../TwinDragon600x600CM4TouchUI/octoprint_TwinDragon600x600CM4TouchUI/templates/img/arrows-4.png../../../../TwinDragon600x600CM4TouchUI/octoprint_TwinDragon600x600CM4TouchUI/templates/img/arrows-4.png - - - - 150 - 150 - - - - false - - - - - menuCartButton - menuPrintButton - menuCalibrateButton - menuControlButton - menuSettingsButton - menuBackButton - - - - - diff --git a/src/ui/parameters_screen/parameters_screen.py b/src/ui/parameters_screen/parameters_screen.py new file mode 100644 index 00000000..129085e7 --- /dev/null +++ b/src/ui/parameters_screen/parameters_screen.py @@ -0,0 +1,204 @@ +from PyQt5 import uic +from PyQt5.QtWidgets import QWidget, QLineEdit, QPushButton, QPlainTextEdit +import yaml + +class ParametersScreen(QWidget): + def __init__(self, main_window): + super(ParametersScreen, self).__init__() + self.main_window = main_window + self.printer_status = main_window.printer_status # Assuming main_window has a printer_status attribute + + try: + uic.loadUi('src/ui/parameters_screen/parameters_screen.ui', self) + print("ParametersScreen UI loaded successfully") + except Exception as e: + print(f"Failed to load ParametersScreen UI: {e}") + + # Initialize all QLineEdit widgets + self.layerHeightLineEdit = self.findChild(QLineEdit, "layerHeightLineEdit") + self.initialLevellingHeightLineEdit = self.findChild(QLineEdit, "initialLevellingHeightLineEdit") + self.heatedBufferHeightLineEdit = self.findChild(QLineEdit, "heatedBufferHeightLineEdit") + self.powderLoadingExtraHeightGapLineEdit = self.findChild(QLineEdit, "powderLoadingExtraHeightGapLineEdit") + self.bedTemperatureLineEdit = self.findChild(QLineEdit, "bedTemperatureLineEdit") + self.volumeTemberatureLineEdit = self.findChild(QLineEdit, "volumeTemberatureLineEdit") + self.chamberTemperatureLineEdit = self.findChild(QLineEdit, "chamberTemperatureLineEdit") + self.pLineEdit = self.findChild(QLineEdit, "pLineEdit") + self.iLineEdit = self.findChild(QLineEdit, "iLineEdit") + self.dLineEdit = self.findChild(QLineEdit, "dLineEdit") + self.partHeightLineEdit = self.findChild(QLineEdit, "partHeightLineEdit") + self.dosingHeightLineEdit = self.findChild(QLineEdit, "dosingHeightLineEdit") # Add dosingHeightLineEdit + + # Initialize all QPlainTextEdit widgets + self.powderLoadingSequenceText = self.findChild(QPlainTextEdit, "powderLoadingSequenceText") + self.moveToStartingSequenceText = self.findChild(QPlainTextEdit, "moveToStartingSequenceText") + self.prepareForPartRemovalSequenceText = self.findChild(QPlainTextEdit, "prepareForPartRemovalSequenceText") + self.initialLevellingRecoatingSequenceText = self.findChild(QPlainTextEdit, "initialLevellingRecoatingSequenceText") + self.heatedBufferRecoatingSequenceText = self.findChild(QPlainTextEdit, "heatedBufferRecoatingSequenceText") + self.printingRecoatingSequenceText = self.findChild(QPlainTextEdit, "printingRecoatingSequenceText") + + # Initialize buttons + self.saveChangesButton = self.findChild(QPushButton, "saveChangesButton") + self.revertChangesButton = self.findChild(QPushButton, "revertChangesButton") + + # Store initial values + self.initial_values = { + "layerHeight": self.printer_status.layerHeight, + "initialLevellingHeight": self.printer_status.initialLevellingHeight, + "heatedBufferHeight": self.printer_status.heatedBufferHeight, + "powderLoadingExtraHeightGap": self.printer_status.powderLoadingExtraHeightGap, + "bedTemperature": self.printer_status.bedTemperature, + "volumeTemperature": self.printer_status.volumeTemperature, + "chamberTemperature": self.printer_status.chamberTemperature, + "p": self.printer_status.p, + "i": self.printer_status.i, + "d": self.printer_status.d, + "powderLoadingSequence": self.printer_status.powderLoadingSequence, + "moveToStartingSequence": self.printer_status.moveToStartingSequence, + "prepareForPartRemovalSequence": self.printer_status.prepareForPartRemovalSequence, + "initialLevellingRecoatingSequence": self.printer_status.initialLevellingRecoatingSequence, + "heatedBufferRecoatingSequence": self.printer_status.heatedBufferRecoatingSequence, + "printingRecoatingSequence": self.printer_status.printingRecoatingSequence, + "partHeight": self.printer_status.partHeight, + "dosingHeight": self.printer_status.dosingHeight # Add dosingHeight + } + + # Connect buttons to methods + self.saveChangesButton.clicked.connect(self.save_changes) + self.revertChangesButton.clicked.connect(self.revert_changes) + + # Load parameters from YAML file + self.load_parameters() + + def load_parameters(self): + try: + with open('parameters.yaml', 'r') as file: + parameters = yaml.safe_load(file) + self.printer_status.setLayerHeight(float(parameters["layerHeight"])) + self.printer_status.setInitialLevellingHeight(float(parameters["initialLevellingHeight"])) + self.printer_status.setHeatedBufferHeight(float(parameters["heatedBufferHeight"])) + self.printer_status.setPowderLoadingExtraHeightGap(float(parameters["powderLoadingExtraHeightGap"])) + self.printer_status.setBedTemperature(float(parameters["bedTemperature"])) + self.printer_status.setVolumeTemperature(float(parameters["volumeTemperature"])) + self.printer_status.setChamberTemperature(float(parameters["chamberTemperature"])) + self.printer_status.setP(float(parameters["p"])) + self.printer_status.setI(float(parameters["i"])) + self.printer_status.setD(float(parameters["d"])) + self.printer_status.setPartHeight(float(parameters["partHeight"])) + self.printer_status.setDosingHeight(float(parameters["dosingHeight"])) # Add dosingHeight + self.printer_status.setPowderLoadingSequence(parameters["powderLoadingSequence"]) + self.printer_status.setMoveToStartingSequence(parameters["moveToStartingSequence"]) + self.printer_status.setPrepareForPartRemovalSequence(parameters["prepareForPartRemovalSequence"]) + self.printer_status.setInitialLevellingRecoatingSequence(parameters["initialLevellingRecoatingSequence"]) + self.printer_status.setHeatedBufferRecoatingSequence(parameters["heatedBufferRecoatingSequence"]) + self.printer_status.setPrintingRecoatingSequence(parameters["printingRecoatingSequence"]) + + # Set values to QLineEdit and QPlainTextEdit widgets + self.layerHeightLineEdit.setText(parameters["layerHeight"]) + self.initialLevellingHeightLineEdit.setText(parameters["initialLevellingHeight"]) + self.heatedBufferHeightLineEdit.setText(parameters["heatedBufferHeight"]) + self.powderLoadingExtraHeightGapLineEdit.setText(parameters["powderLoadingExtraHeightGap"]) + self.bedTemperatureLineEdit.setText(parameters["bedTemperature"]) + self.volumeTemberatureLineEdit.setText(parameters["volumeTemperature"]) + self.chamberTemperatureLineEdit.setText(parameters["chamberTemperature"]) + self.pLineEdit.setText(parameters["p"]) + self.iLineEdit.setText(parameters["i"]) + self.dLineEdit.setText(parameters["d"]) + self.partHeightLineEdit.setText(parameters["partHeight"]) + self.dosingHeightLineEdit.setText(parameters["dosingHeight"]) # Add dosingHeight + self.powderLoadingSequenceText.setPlainText(parameters["powderLoadingSequence"]) + self.moveToStartingSequenceText.setPlainText(parameters["moveToStartingSequence"]) + self.prepareForPartRemovalSequenceText.setPlainText(parameters["prepareForPartRemovalSequence"]) + self.initialLevellingRecoatingSequenceText.setPlainText(parameters["initialLevellingRecoatingSequence"]) + self.heatedBufferRecoatingSequenceText.setPlainText(parameters["heatedBufferRecoatingSequence"]) + self.printingRecoatingSequenceText.setPlainText(parameters["printingRecoatingSequence"]) + except FileNotFoundError: + print("parameters.yaml file not found. Using initial values.") + + def save_changes(self): + self.printer_status.setLayerHeight(float(self.layerHeightLineEdit.text())) + self.printer_status.setInitialLevellingHeight(float(self.initialLevellingHeightLineEdit.text())) + self.printer_status.setHeatedBufferHeight(float(self.heatedBufferHeightLineEdit.text())) + self.printer_status.setPowderLoadingExtraHeightGap(float(self.powderLoadingExtraHeightGapLineEdit.text())) + self.printer_status.setBedTemperature(float(self.bedTemperatureLineEdit.text())) + self.printer_status.setVolumeTemperature(float(self.volumeTemberatureLineEdit.text())) + self.printer_status.setChamberTemperature(float(self.chamberTemperatureLineEdit.text())) + self.printer_status.setP(float(self.pLineEdit.text())) + self.printer_status.setI(float(self.iLineEdit.text())) + self.printer_status.setD(float(self.dLineEdit.text())) + self.printer_status.setPartHeight(float(self.partHeightLineEdit.text())) + self.printer_status.setDosingHeight(float(self.dosingHeightLineEdit.text())) # Add dosingHeight + + self.printer_status.setPowderLoadingSequence(self.powderLoadingSequenceText.toPlainText()) + self.printer_status.setMoveToStartingSequence(self.moveToStartingSequenceText.toPlainText()) + self.printer_status.setPrepareForPartRemovalSequence(self.prepareForPartRemovalSequenceText.toPlainText()) + self.printer_status.setInitialLevellingRecoatingSequence(self.initialLevellingRecoatingSequenceText.toPlainText()) + self.printer_status.setHeatedBufferRecoatingSequence(self.heatedBufferRecoatingSequenceText.toPlainText()) + self.printer_status.setPrintingRecoatingSequence(self.printingRecoatingSequenceText.toPlainText()) + + # Save to YAML file + parameters = { + "layerHeight": self.layerHeightLineEdit.text(), + "initialLevellingHeight": self.initialLevellingHeightLineEdit.text(), + "heatedBufferHeight": self.heatedBufferHeightLineEdit.text(), + "powderLoadingExtraHeightGap": self.powderLoadingExtraHeightGapLineEdit.text(), + "bedTemperature": self.bedTemperatureLineEdit.text(), + "volumeTemperature": self.volumeTemberatureLineEdit.text(), + "chamberTemperature": self.chamberTemperatureLineEdit.text(), + "p": self.pLineEdit.text(), + "i": self.iLineEdit.text(), + "d": self.dLineEdit.text(), + "powderLoadingSequence": self.powderLoadingSequenceText.toPlainText(), + "moveToStartingSequence": self.moveToStartingSequenceText.toPlainText(), + "prepareForPartRemovalSequence": self.prepareForPartRemovalSequenceText.toPlainText(), + "initialLevellingRecoatingSequence": self.initialLevellingRecoatingSequenceText.toPlainText(), + "heatedBufferRecoatingSequence": self.heatedBufferRecoatingSequenceText.toPlainText(), + "printingRecoatingSequence": self.printingRecoatingSequenceText.toPlainText(), + "partHeight": self.partHeightLineEdit.text(), + "dosingHeight": self.dosingHeightLineEdit.text() # Add dosingHeight + } + with open('parameters.yaml', 'w') as file: + yaml.dump(parameters, file) + + def revert_changes(self): + # Load from YAML file + try: + with open('parameters.yaml', 'r') as file: + parameters = yaml.safe_load(file) + self.layerHeightLineEdit.setText(parameters["layerHeight"]) + self.initialLevellingHeightLineEdit.setText(parameters["initialLevellingHeight"]) + self.heatedBufferHeightLineEdit.setText(parameters["heatedBufferHeight"]) + self.powderLoadingExtraHeightGapLineEdit.setText(parameters["powderLoadingExtraHeightGap"]) + self.bedTemperatureLineEdit.setText(parameters["bedTemperature"]) + self.volumeTemberatureLineEdit.setText(parameters["volumeTemperature"]) + self.chamberTemperatureLineEdit.setText(parameters["chamberTemperature"]) + self.pLineEdit.setText(parameters["p"]) + self.iLineEdit.setText(parameters["i"]) + self.dLineEdit.setText(parameters["d"]) + self.partHeightLineEdit.setText(parameters["partHeight"]) + self.dosingHeightLineEdit.setText(parameters["dosingHeight"]) # Add dosingHeight + self.powderLoadingSequenceText.setPlainText(parameters["powderLoadingSequence"]) + self.moveToStartingSequenceText.setPlainText(parameters["moveToStartingSequence"]) + self.prepareForPartRemovalSequenceText.setPlainText(parameters["prepareForPartRemovalSequence"]) + self.initialLevellingRecoatingSequenceText.setPlainText(parameters["initialLevellingRecoatingSequence"]) + self.heatedBufferRecoatingSequenceText.setPlainText(parameters["heatedBufferRecoatingSequence"]) + self.printingRecoatingSequenceText.setPlainText(parameters["printingRecoatingSequence"]) + except FileNotFoundError: + print("parameters.yaml file not found. Reverting to initial values.") + self.layerHeightLineEdit.setText(str(self.initial_values["layerHeight"])) + self.initialLevellingHeightLineEdit.setText(str(self.initial_values["initialLevellingHeight"])) + self.heatedBufferHeightLineEdit.setText(str(self.initial_values["heatedBufferHeight"])) + self.powderLoadingExtraHeightGapLineEdit.setText(str(self.initial_values["powderLoadingExtraHeightGap"])) + self.bedTemperatureLineEdit.setText(str(self.initial_values["bedTemperature"])) + self.volumeTemberatureLineEdit.setText(str(self.initial_values["volumeTemperature"])) + self.chamberTemperatureLineEdit.setText(str(self.initial_values["chamberTemperature"])) + self.pLineEdit.setText(str(self.initial_values["p"])) + self.iLineEdit.setText(str(self.initial_values["i"])) + self.dLineEdit.setText(str(self.initial_values["d"])) + self.partHeightLineEdit.setText(str(self.initial_values["partHeight"])) + self.dosingHeightLineEdit.setText(str(self.initial_values["dosingHeight"])) # Add dosingHeight + self.powderLoadingSequenceText.setPlainText(self.initial_values["powderLoadingSequence"]) + self.moveToStartingSequenceText.setPlainText(self.initial_values["moveToStartingSequence"]) + self.prepareForPartRemovalSequenceText.setPlainText(self.initial_values["prepareForPartRemovalSequence"]) + self.initialLevellingRecoatingSequenceText.setPlainText(self.initial_values["initialLevellingRecoatingSequence"]) + self.heatedBufferRecoatingSequenceText.setPlainText(self.initial_values["heatedBufferRecoatingSequence"]) + self.printingRecoatingSequenceText.setPlainText(self.initial_values["printingRecoatingSequence"]) \ No newline at end of file diff --git a/src/ui/parameters_screen/parameters_screen.ui b/src/ui/parameters_screen/parameters_screen.ui new file mode 100644 index 00000000..1d8d4d72 --- /dev/null +++ b/src/ui/parameters_screen/parameters_screen.ui @@ -0,0 +1,1219 @@ + + + Form + + + + 0 + 0 + 1542 + 1019 + + + + Form + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + 0 + 100 + + + + + 16777215 + 20 + + + + + Gotham + 16 + 50 + false + + + + color: grey; + + + Build Module Process Sequences + + + true + + + + + + + Qt::Horizontal + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + 0 + 100 + + + + + 16777215 + 20 + + + + + Gotham + 16 + 50 + false + + + + color: grey; + + + Prepare Powder Loading Sequence {powderLoadingHeight} = initialLevellingHeight + partHeight+ 2 * heatedBufferHeight + + + true + + + + + + + G28 Z Y +G0 Y{powderLoadingHeight} F600 +M400 +G91 +G0 Y{powderLoadingExtraHeightGap} F600 +G90 +M400 +goDown +goDown +goDown +goDown + + + + + + + + + 0 + 100 + + + + + 16777215 + 20 + + + + + Gotham + 16 + 50 + false + + + + color: grey; + + + Move to Starting Position Sequence {powderLoadingHeight} = initialLevellingHeight + partHeight+ 2 * heatedBufferHeigh + + + true + + + + + + + G28 Z Y +M400 +G0 Z0 Y{powderLoadingHeight} +M400 + + + + + + + + 0 + 100 + + + + + 16777215 + 20 + + + + + Gotham + 16 + 50 + false + + + + color: grey; + + + Prepare for Part Removal Sequence + + + true + + + + + + + G91 +G0 Z{powderLoadingExtraHeightGap} Y{powderLoadingExtraHeightGap} +G90 +M400 +goDown +goDown +goDown +goDown + + + + + + + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + 0 + 100 + + + + + 16777215 + 20 + + + + + Gotham + 16 + 50 + false + + + + color: grey; + + + Recoater Process Sequences + + + true + + + + + + + Qt::Horizontal + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + 0 + 100 + + + + + 16777215 + 20 + + + + + Gotham + 16 + 50 + false + + + + color: grey; + + + Initial Levelling Recoating Sequence + + + true + + + + + + + homeRecoater +G91 +G0 Z{layerHeight} Y-{dosingHeight} +G90 +M400 +recoat +M400 +homeRecoater + + + + + + + + 0 + 100 + + + + + 16777215 + 20 + + + + + Gotham + 16 + 50 + false + + + + color: grey; + + + Heated Buffer Recoating Sequence + + + true + + + + + + + homeRecoater +G91 +G0 Z{layerHeight} Y-{layerHeight} +G90 +M400 +recoat +M400 +homeRecoater + + + + + + + + 0 + 100 + + + + + 16777215 + 20 + + + + + Gotham + 16 + 50 + false + + + + color: grey; + + + Dosing & Recoating Sequence + + + true + + + + + + + homeRecoater +G91 +G0 Z{layerHeight} Y-{layerHeight} +G90 +M400 +recoat +M400 +homeRecoater + + + + + + + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + 0 + 100 + + + + + 16777215 + 20 + + + + + Gotham + 16 + 50 + false + + + + color: grey; + + + Process Parameters + + + true + + + + + + + Qt::Horizontal + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + 0 + 60 + + + + + 16777215 + 100 + + + + + Gotham + 12 + 50 + false + + + + color: grey; + + + Initial Levelling Height {initialLevellingHeight} + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + + + + + + 0 + 60 + + + + + 16777215 + 100 + + + + + Gotham + 12 + 50 + false + + + + color: grey; + + + Heated Buffer Height {heatedBufferHeight} + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + + + + + + 0 + 30 + + + + 5 + + + + + + + + 0 + 30 + + + + 10 + + + + + + + + 0 + 60 + + + + + 16777215 + 100 + + + + + Gotham + 12 + 50 + false + + + + color: grey; + + + Powder Loading Extra Height Gap {powderLoadingExtraHeightGap} + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + + + + + + 0 + 30 + + + + 20 + + + + + + + + 0 + 60 + + + + + 16777215 + 100 + + + + + Gotham + 12 + 50 + false + + + + color: grey; + + + Bed Temperature {bedTemperature} + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + + + + + + 0 + 30 + + + + 120 + + + + + + + + 0 + 60 + + + + + 16777215 + 100 + + + + + Gotham + 12 + 50 + false + + + + color: grey; + + + Volume Temperature {volumeTemperatures} + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + + + + + + 0 + 30 + + + + 120 + + + + + + + + 0 + 30 + + + + 160 + + + + + + + + 0 + 60 + + + + + 16777215 + 100 + + + + + Gotham + 12 + 50 + false + + + + color: grey; + + + Chamber Temperature {chamberTemperature} + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + + + + + + 0 + 30 + + + + 10 + + + + + + + + 0 + 60 + + + + + 16777215 + 100 + + + + + Gotham + 12 + 50 + false + + + + color: grey; + + + P {p} + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + + + + + + 0 + 30 + + + + 0.01 + + + + + + + + 0 + 60 + + + + + 16777215 + 100 + + + + + Gotham + 12 + 50 + false + + + + color: grey; + + + I {i} + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + + + + + + 0 + 30 + + + + 0.5 + + + + + + + + 0 + 60 + + + + + 16777215 + 100 + + + + + Gotham + 12 + 50 + false + + + + color: grey; + + + D {d} + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + + + + + + 0 + 60 + + + + + 16777215 + 100 + + + + + Gotham + 12 + 50 + false + + + + color: grey; + + + Part Height {partHeight} + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + + + + + + 0 + 30 + + + + 10 + + + + + + + + 0 + 60 + + + + + 16777215 + 100 + + + + + Gotham + 12 + 50 + false + + + + color: grey; + + + Layer Height {layerHeight} + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + + + + + + 0 + 30 + + + + 0.1 + + + + + + + + 0 + 60 + + + + + 16777215 + 100 + + + + + Gotham + 12 + 50 + false + + + + color: grey; + + + Dosing Height {dosingHeight} + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + + + + + + 0 + 30 + + + + 0.15 + + + + + + + + + + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + 200 + 50 + + + + + 350 + 50 + + + + + Gotham + 12 + + + + QPushButton { + border: 1px solid rgb(87, 87, 87); + + background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); +border-radius:20px; +} + +QPushButton:pressed { + background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #dadbde, stop: 1 #f6f7fa); +} + +QPushButton:flat { + border: none; /* no border for a flat push button */ +} + +QPushButton:default { + border-color: navy; /* make the default button prominent */ +} + +QPushButton:focus { + outline: none; +} + + + Revert Changes + + + + 20 + 20 + + + + + + + + + 200 + 50 + + + + + 350 + 50 + + + + + Gotham + 12 + + + + QPushButton { + border: 1px solid rgb(87, 87, 87); + + background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); +border-radius:20px; +} + +QPushButton:pressed { + background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #dadbde, stop: 1 #f6f7fa); +} + +QPushButton:flat { + border: none; /* no border for a flat push button */ +} + +QPushButton:default { + border-color: navy; /* make the default button prominent */ +} + +QPushButton:focus { + outline: none; +} + + + Save Changes + + + + 20 + 20 + + + + + + + + + + + + diff --git a/src/ui/resources/__pycache__/resource_rc.cpython-313.pyc b/src/ui/resources/__pycache__/resource_rc.cpython-313.pyc deleted file mode 100644 index 70ea6d5a..00000000 Binary files a/src/ui/resources/__pycache__/resource_rc.cpython-313.pyc and /dev/null differ diff --git a/src/ui/resources/img/Logos/control_center_logo_black.png b/src/ui/resources/img/Logos/control_center_logo_black.png new file mode 100644 index 00000000..77283fa0 Binary files /dev/null and b/src/ui/resources/img/Logos/control_center_logo_black.png differ diff --git a/src/ui/resources/resource.qrc b/src/ui/resources/resource.qrc index 75539b98..8f6fd63a 100644 --- a/src/ui/resources/resource.qrc +++ b/src/ui/resources/resource.qrc @@ -1,5 +1,6 @@ + img/Logos/control_center_logo_black.png img/Logos/control_center_logo_text.png img/Logos/control_center_logo.png img/Logos/Fracktal Logo.png diff --git a/src/ui/resources/resource_rc.py b/src/ui/resources/resource_rc.py index cd5703b5..b58a5755 100644 --- a/src/ui/resources/resource_rc.py +++ b/src/ui/resources/resource_rc.py @@ -2774,6 +2774,828 @@ \x1a\x9a\x48\x1a\x1a\x9a\x48\x1a\x1a\x1a\x3e\xfe\xdf\x00\x42\x55\ \x27\x7d\xc7\xe7\x77\x42\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\ \x60\x82\ +\x00\x00\x33\x3c\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x0c\xba\x00\x00\x00\xd4\x08\x06\x00\x00\x00\x21\xea\xab\x1c\ +\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x2e\x23\x00\x00\x2e\x23\ +\x01\x78\xa5\x3f\x76\x00\x00\x32\xee\x49\x44\x41\x54\x78\x9c\xed\ +\xdd\x77\xb8\x6e\x77\x55\x27\xf0\x6f\x0a\x91\xde\x06\xa4\x43\x58\ +\xf4\xd0\x86\x2a\xbd\x0a\x82\x0e\x92\x41\xba\x82\x0a\xa8\x84\x01\ +\x41\xa5\x2b\xc2\x58\x50\x11\x86\x22\xd2\x04\x91\xa8\x54\x19\x3a\ +\x08\x0e\xc5\x80\x34\x91\x40\x10\x02\x11\x96\x14\xa5\x23\x24\x24\ +\x24\x84\x24\x77\xfe\x38\x07\x38\x21\xb9\x37\xf7\x9c\xb3\xf7\xbb\ +\xf7\xfb\xbe\x9f\xcf\xf3\x9c\xe7\x11\xcc\xbb\xf6\xd2\xdc\xdf\x3d\ +\x6b\xed\xf5\xae\xbd\x0f\xd8\xb3\x67\x4f\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x60\x6a\x07\x4e\x9d\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x24\x16\x5d\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x98\x09\x8b\x2e\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\xcc\x82\x45\x17\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x66\xc1\xa2\x0b\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\xb3\x60\xd1\x05\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x80\x59\xb0\xe8\x02\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\xc0\x2c\x58\x74\x01\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x60\x16\x2c\xba\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x30\x0b\x16\x5d\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x98\x05\x8b\x2e\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\xcc\x82\x45\x17\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x66\xc1\xa2\x0b\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\xb3\x60\xd1\x05\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x80\x59\xb0\xe8\x02\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\xc0\x2c\x58\x74\x01\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x60\x16\x2c\xba\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x30\x0b\x16\x5d\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x98\x05\x8b\x2e\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\xcc\x82\x45\x17\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x66\xc1\xa2\x0b\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\xb3\x60\xd1\x05\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x80\x59\xb0\xe8\x02\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\xc0\x2c\x58\x74\x01\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x60\x16\x2c\xba\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x30\x0b\x16\x5d\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x98\x05\x8b\x2e\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\xcc\x82\x45\x17\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x66\xc1\xa2\x0b\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\xb3\x60\xd1\x05\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x80\x59\xb0\xe8\x02\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\xc0\x2c\x58\x74\x01\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x60\x16\x2c\xba\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x30\x0b\x16\x5d\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x98\x05\x8b\x2e\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\xcc\x82\x45\x17\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x66\xc1\xa2\x0b\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\xb3\x60\xd1\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x80\x59\xb0\xe8\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\xc0\x2c\x58\x74\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x60\x16\x2c\xba\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x30\x0b\x16\x5d\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x98\x05\x8b\x2e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\xcc\x82\x45\x17\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x66\xc1\xa2\x0b\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\xb3\x60\xd1\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x80\x59\xb0\xe8\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\xc0\x2c\x58\x74\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x60\x16\x0e\x9e\x3a\x81\x75\x54\x55\x17\x48\x72\x50\x92\x0b\ +\x4f\x9c\x0a\xec\xc6\x19\xdd\xfd\xf9\xa9\x93\x18\x9b\xf3\xba\xb6\ +\x4e\x4f\xf2\xed\x24\xa7\x76\xf7\x77\xa6\x4e\x66\x6e\xaa\xea\xd2\ +\x49\x0e\x19\x28\xdc\xd7\xbb\xfb\xc4\x81\x62\xb1\xc6\xaa\xea\x90\ +\x24\x97\x1e\x28\xdc\x69\xdd\xfd\x1f\xbb\xc8\xe5\xf2\xb1\x50\x0e\ +\x73\xf2\x1f\xdd\x7d\xda\xfe\xfe\xc3\x03\xff\x9e\x83\x65\x70\x7c\ +\x92\xd3\xbb\xfb\x84\xa9\x13\xe1\xcc\xf4\xa3\x6b\x4b\x3f\xba\x0f\ +\x03\xff\x9e\xfe\x72\x77\x9f\x32\x50\x2c\x00\x56\x80\xfa\x8b\x15\ +\xb1\xa3\xf9\x5d\x55\x5d\x30\xc9\x45\x47\xc8\x07\xd8\x99\xef\x74\ +\xf7\x57\x87\x08\x64\xae\xc7\x1c\x99\xeb\x01\xfb\x70\x8e\x73\x3d\ +\xb5\x2b\xcc\xce\xae\x6a\x57\xf3\x79\xd6\x90\xf9\xfc\x36\x58\x74\ +\x19\x41\x55\x5d\x2c\xc9\x75\x92\x5c\x2b\xc9\xd5\x92\x5c\x21\xc9\ +\xe5\x93\x5c\x32\xc9\x45\xe2\xff\xef\xac\x86\xe3\xb3\x02\xc3\x1e\ +\xe7\x95\x73\x52\x55\xa7\x24\xf9\x46\x92\x2f\x26\xe9\xcd\x9f\x4f\ +\x24\x39\x26\xc9\xb1\xdd\xfd\xbd\x09\xd3\x9b\xca\x9b\x93\x5c\x77\ +\xa0\x58\x5f\xaf\xaa\xdf\x49\xf2\xc2\xee\x3e\x7d\xa0\x98\xac\xa7\ +\xc3\x92\x1c\x3d\x50\xac\xcf\x25\x39\x74\x17\x9f\x3f\x26\xc9\x85\ +\x86\x49\x05\x18\xc0\x15\x93\x7c\x76\x1b\xff\xfc\x90\xbf\xe7\x60\ +\x69\x54\xd5\xa9\x49\xbe\x9c\xe4\x3f\x93\x7c\x32\xc9\xb1\x49\x3e\ +\x92\xe4\x7d\xbe\xc0\x30\x0e\xfd\x28\xe7\x44\x3f\x7a\xb6\x86\xfc\ +\x3d\x7d\xdb\x24\xef\x1a\x28\x16\x00\x4b\x40\xfd\xc5\x9a\xd8\xe9\ +\xfc\xee\x01\x49\x9e\x3e\x6c\x2a\xc0\x2e\xbc\x2e\xc9\xe1\x03\xc5\ +\x32\xd7\x63\x8e\xcc\xf5\x80\xbd\xd9\x9f\xb9\x9e\xda\x15\xe6\x65\ +\xb7\xb5\xab\xf9\x3c\x6b\xc9\x7c\x7e\xff\xb8\x61\x3b\x80\xaa\xba\ +\x5c\x92\x3b\x64\x63\x38\x7a\xd3\x24\x57\x9a\x36\x23\x60\x6f\x9c\ +\x57\x76\xe0\xdc\x49\x2e\xb3\xf9\x73\xa3\x1f\xf9\xdf\x9d\x5c\x55\ +\x1f\x4c\xf2\x9e\x24\x6f\x4b\xf2\xde\xed\x3c\x31\x9e\x24\xc9\xc5\ +\x92\x3c\x2f\xc9\x43\xaa\xea\xe1\xdd\xfd\xae\x89\xf3\x01\x00\x58\ +\x57\x87\x64\xe3\x4b\x7e\x97\xcf\x46\xaf\xf4\x7d\xa7\x57\xd5\x47\ +\x92\xbc\x25\xc9\xeb\x93\x7c\xa8\xbb\xf7\x2c\x3e\xbd\xe5\xa7\x1f\ +\x65\x07\xf4\xa3\x00\xb0\x0b\xea\x2f\x00\x18\x9c\xb9\x1e\x00\x00\ +\xc0\x30\xcc\xe7\xf7\x83\x45\x97\x1d\xaa\xaa\xab\x27\xb9\xc7\xe6\ +\xcf\xb5\x27\x4e\x07\xd8\x07\xe7\x95\x11\x9d\x27\xc9\xad\x37\x7f\ +\x7e\x3b\xc9\xf1\x55\xf5\xf7\x49\x5e\x99\xe4\x4d\xdd\xfd\xdd\x29\ +\x93\x5b\x32\xd7\x49\xf2\xce\xaa\x7a\x75\x92\x47\x75\xf7\xbf\x4f\ +\x9d\x10\x00\x00\x49\x92\x83\x92\xdc\x60\xf3\xe7\x77\x92\x7c\xae\ +\xaa\x5e\x92\xe4\xaf\xd4\x6c\xe7\x4c\x3f\xca\x88\xf4\xa3\x00\x70\ +\x36\xd4\x5f\x00\xb0\x10\xe6\x7a\x00\x00\x00\xe3\x30\x9f\xdf\xc2\ +\xa2\xcb\x36\x54\xd5\xf9\x93\xdc\x37\xc9\x03\x93\xdc\x78\xe2\x74\ +\x80\x7d\x70\x5e\x99\xc8\x85\x92\xdc\x6b\xf3\xe7\x84\xaa\xfa\xdb\ +\x24\x2f\xe8\xee\x8f\x4c\x9a\xd5\x72\xf9\xb9\x24\x3f\x53\x55\xff\ +\x27\xc9\x93\xbb\xfb\xa4\xa9\x13\x02\x00\xe0\x4c\xae\x90\xe4\x77\ +\x93\x3c\xa1\xaa\x5e\x97\xe4\xa9\xdd\xfd\x4f\x13\xe7\x34\x2b\xfa\ +\x51\x26\xa2\x1f\x05\x60\x6d\xa9\xbf\x00\x60\x32\xe6\x7a\x00\x00\ +\x00\xe3\x5a\xeb\xf9\xfc\x81\x53\x27\xb0\x0c\xaa\xea\xb2\x55\xf5\ +\x94\x24\xff\x91\xe4\xf9\x71\x93\x1c\x66\xcb\x79\x65\x46\x2e\x98\ +\xe4\x88\x24\x47\x57\xd5\xfb\xaa\xea\xee\x55\xe5\xf7\xee\xfe\x39\ +\x77\x92\xc7\x27\x39\xae\xaa\xee\x57\x55\x07\x4c\x9d\x10\x00\x00\ +\x67\x71\x40\x92\xc3\x93\xbc\xa7\xaa\xde\x59\x55\x37\x9a\x38\x9f\ +\xc9\xe9\x47\x99\x11\xfd\x28\x00\x6b\x41\xfd\x05\x00\xb3\x60\xae\ +\x07\x00\x00\x30\xbe\xb5\x9c\xcf\x1b\x70\xee\x43\x55\x5d\xaa\xaa\ +\x9e\x91\xe4\x33\x49\x1e\x95\x8d\x27\x23\x02\x33\xe4\xbc\x32\x73\ +\x37\x49\xf2\xaa\x24\x9f\xaa\xaa\x5f\xaa\x2a\x6f\x54\xdb\x3f\x97\ +\x4e\x72\x64\x92\xf7\x55\xd5\x4f\x4c\x9d\x0c\x00\x00\x7b\x75\x9b\ +\x24\x1f\xac\xaa\x57\x54\xd5\x65\xa7\x4e\x66\xd1\xf4\xa3\xcc\x9c\ +\x7e\x14\x80\x95\xa3\xfe\x02\x80\x59\x32\xd7\x03\x00\x00\x58\x8c\ +\xdb\x64\x4d\xe6\xf3\x16\x5d\xce\x46\x55\x9d\xa7\xaa\x9e\x98\x8d\ +\x1b\xe4\x0f\x4f\x72\xc8\xc4\x29\x01\x7b\xe1\xbc\xb2\x64\xae\x9c\ +\xe4\xc5\x49\x3e\x5e\x55\x77\x9f\x3a\x99\x25\xf2\x13\x49\xde\x5f\ +\x55\x47\x56\xd5\xa5\xa7\x4e\x06\x00\x80\xbd\xba\x67\x92\x4f\x54\ +\xd5\xaf\xaf\xc3\xdb\x23\xf4\xa3\x2c\x19\xfd\x28\x00\x4b\x4f\xfd\ +\x05\x00\x4b\xc1\x5c\x0f\x00\x00\x60\x31\x56\x7e\x3e\xbf\x92\xff\ +\x47\xed\x46\x55\xdd\x25\xc9\xa7\x92\x3c\x29\xc9\x79\xa6\xcd\x06\ +\xd8\x17\xe7\x95\x25\x76\xd5\x24\xaf\xaa\xaa\xa3\xaa\xea\x7a\x53\ +\x27\xb3\x44\xee\x97\x8d\xd7\x9e\x3f\xae\xaa\xce\x3d\x75\x32\x00\ +\x00\x9c\xad\x0b\x24\x79\x66\x92\xb7\x57\xd5\x65\xa6\x4e\x66\x2c\ +\xfa\x51\x96\x98\x7e\x14\x80\xa5\xa4\xfe\x02\x80\xa5\x63\xae\x07\ +\x00\x00\x30\xbe\x95\x9e\xcf\x5b\x74\xd9\x54\x55\x17\xab\xaa\x97\ +\x27\x79\x7d\x92\xcb\x4d\x9d\x0f\xb0\x77\xce\x2b\x2b\xe4\x96\x49\ +\xfe\xb9\xaa\x9e\x56\x55\xe7\x9d\x3a\x99\x25\x71\xbe\x24\x4f\x4e\ +\x72\x6c\x55\xdd\x6d\xea\x64\x00\x00\xd8\xab\xdb\x24\x39\xa6\xaa\ +\xee\x3c\x75\x22\x43\xd2\x8f\xb2\x42\xf4\xa3\x00\x2c\x05\xf5\x17\ +\x00\x2c\x35\x73\x3d\x00\x00\x80\xc5\xb8\x4d\x56\x70\x3e\x6f\xd1\ +\x25\x49\x55\xdd\x21\xc9\x31\x49\xee\x35\x75\x2e\xc0\xbe\x39\xaf\ +\xac\xa0\x83\x92\xfc\x66\x92\x7f\xad\xaa\x9b\x4d\x9d\xcc\x12\x39\ +\x34\xc9\xab\xab\xea\x9d\x55\x75\x9d\xa9\x93\x01\x00\xe0\x6c\x5d\ +\x34\xc9\x1b\xab\xea\x51\x53\x27\x32\x04\xfd\x28\x2b\x48\x3f\x0a\ +\xc0\xac\xa9\xbf\x00\x60\x65\x1c\x1a\x73\x3d\x00\x00\x80\xb1\xad\ +\xd4\x7c\x3e\x59\xf3\x45\x97\xaa\x3a\xa8\xaa\xfe\x30\xc9\xdb\x92\ +\x5c\x6a\xea\x7c\x80\xbd\x73\x5e\x59\x03\x57\x4c\xf2\xee\xaa\xfa\ +\xbd\xaa\x3a\x68\xea\x64\x96\xc8\x6d\x92\x1c\x5d\x55\xcf\xad\xaa\ +\x8b\x4f\x9d\x0c\x00\x00\x67\x71\x60\x92\xa7\x54\xd5\xf3\x96\xb5\ +\xce\xd5\x8f\xb2\x06\xf4\xa3\x00\xcc\x8a\xfa\x0b\x00\x56\xd6\x6d\ +\x62\xae\x07\x00\x00\x30\xa6\xa5\x9f\xcf\x6f\xb5\xb6\x8b\x2e\x55\ +\x75\xd1\x24\x6f\x4a\xf2\xf8\xa9\x73\x01\xf6\xcd\x79\x65\x8d\x1c\ +\x98\xe4\x09\x49\xde\xea\xe6\xee\xb6\x1c\x98\xe4\xc1\x49\x8e\xab\ +\xaa\x47\x54\xd5\xb9\xa6\x4e\x08\x00\x80\xb3\xf8\xb5\x24\x7f\x5d\ +\x55\x87\x4c\x9d\xc8\x76\xe8\x47\x59\x23\xfa\x51\x00\x66\x41\xfd\ +\x05\x00\x2b\xcf\x5c\x0f\x00\x00\x60\x7c\x4b\x39\x9f\xff\x51\x6b\ +\xb9\xe8\x52\x55\x95\xe4\x7d\x49\x7e\x6a\xea\x5c\x80\x7d\x73\x5e\ +\x59\x53\xb7\x4f\xf2\x2f\x5e\xdd\xbd\x6d\x17\x4e\xf2\xf4\x24\xc7\ +\x54\xd5\x9d\x26\xce\x05\x00\x80\xb3\xba\x4f\x92\xbf\x5a\x96\x27\ +\xc7\xe8\x47\x59\x53\xfa\x51\x00\x26\xa3\xfe\x02\x80\xb5\x72\xe1\ +\x98\xeb\x01\x00\x00\x8c\x69\xa9\xe6\xf3\x67\x67\xed\x16\x5d\xaa\ +\xea\xc6\x49\x3e\x90\xe4\xaa\x53\xe7\x02\xec\x9b\xf3\xca\x9a\xbb\ +\x5c\x92\xf7\xb8\xb1\xbb\x23\x57\x4f\xf2\x96\xaa\x7a\x53\x55\xf9\ +\xfb\x03\x00\x60\x5e\xee\x93\xe4\xcf\xa6\x4e\xe2\x9c\xe8\x47\x59\ +\x73\xfa\x51\x00\x16\x4e\xfd\x05\x00\x6b\xcb\x5c\x0f\x00\x00\x60\ +\x3c\x4b\x31\x9f\xdf\x9b\xb5\x5a\x74\xa9\xaa\xdb\x25\xf9\x7f\x49\ +\x2e\x36\x75\x2e\xc0\xbe\x39\xaf\x90\x24\xb9\x40\x92\xd7\x57\xd5\ +\xbd\xa6\x4e\x64\x49\xfd\x74\x92\x7f\xad\xaa\xa7\x55\xd5\x85\xa6\ +\x4e\x06\x00\x80\x1f\x38\xa2\xaa\x1e\x39\x75\x12\x7b\xa3\x1f\x85\ +\x24\xfa\x51\x00\x16\x48\xfd\x05\x00\xc4\x5c\x0f\x00\x00\x60\x2c\ +\xb3\x9e\xcf\xef\xcb\xda\x2c\xba\x6c\xde\x24\x7f\x63\x36\x86\xb4\ +\xc0\x8c\x39\xaf\x70\x26\xe7\x4a\xf2\xd2\xaa\xfa\xf9\xa9\x13\x59\ +\x52\xe7\x4a\xf2\x9b\x49\xfe\xad\xaa\x7e\xb5\xaa\xd6\xa6\xf6\x01\ +\x00\x98\xb9\x3f\x99\xe3\xdb\x22\xf4\xa3\x70\x26\xfa\x51\x00\x46\ +\xa7\xfe\x02\x00\xb6\x30\xd7\x03\x00\x00\x18\xc7\x2c\xe7\xf3\xe7\ +\x64\x2d\x9a\xc2\x2d\x37\xc9\xcf\x33\x75\x2e\xc0\xbe\x39\xaf\x70\ +\xb6\x0e\x4c\x72\x64\x55\xdd\x67\xea\x44\x96\xd8\xc5\x93\x3c\x3f\ +\xc9\x87\xab\xea\x56\x53\x27\x03\x00\x40\x0e\x4c\xf2\x37\x55\x75\ +\xe9\xa9\x13\xf9\x3e\xfd\x28\x9c\x2d\xfd\x28\x00\xa3\x51\x7f\x01\ +\x00\x7b\x61\xae\x07\x00\x00\x30\xac\xd9\xcd\xe7\xf7\xc7\xca\x2f\ +\xba\x54\xd5\x8d\x93\xbc\x36\x6e\x92\xc3\xec\x39\xaf\xb0\x4f\x07\ +\x26\x79\x49\x55\xdd\x79\xea\x44\x96\xdc\x75\x93\xfc\x63\x55\xbd\ +\xa2\xaa\x0e\x9d\x3a\x19\x00\x80\x35\xf7\xdf\xb2\xf1\x05\xfa\xc9\ +\xef\x4f\xe9\x47\x61\x9f\xf4\xa3\x00\x0c\x4e\xfd\x05\x00\xec\x07\ +\x73\x3d\x00\x00\x80\xe1\xcc\x66\x3e\xbf\xbf\x0e\x9e\x3a\x81\x31\ +\x55\xd5\x95\x92\xbc\x29\xf3\x7d\xdd\xf9\xb7\x93\x9c\x31\x75\x12\ +\xb0\x43\xc7\x0f\x19\xcc\x79\x65\x66\x0e\xcc\x3c\xff\x2c\x9e\x2b\ +\xc9\xab\xaa\xea\x96\xdd\x7d\xf4\xd4\xc9\x2c\xb9\x7b\x26\xf9\xd9\ +\xaa\xfa\xd3\x24\x7f\xd2\xdd\x27\x4d\x9d\x10\x2b\x65\xd0\xdf\x91\ +\x8c\xe2\x42\x03\xc6\xf2\xef\x7b\xfe\xb6\x5b\xc3\x7d\x3b\xfe\xbd\ +\xb2\x5e\xce\x9b\x8d\x3a\x73\x2a\xb7\x4f\xf2\xe0\x24\xcf\x99\x2a\ +\x01\xfd\x28\x33\xa3\x1f\x05\x60\xe5\xa9\xbf\x60\x54\x3b\xbd\xa7\ +\xf1\xdd\x5d\x7c\x16\x18\x9e\xb9\xd5\x99\x99\xeb\x31\x26\xbf\xff\ +\xe6\xcf\x5c\x6f\xbd\xec\x4f\x2f\xa6\x76\x85\x79\xd9\x6d\x6d\x66\ +\x3e\xcf\xba\x59\xfb\xf9\xfc\x76\x1c\xb0\x67\xcf\x9e\xa9\x73\x18\ +\x45\x55\x5d\x34\xc9\xfb\x93\x5c\x65\xc2\x34\xf6\x24\x39\x26\xc9\ +\xbb\x93\x7c\x32\xc9\x71\x49\x3e\x9d\xe4\x6b\xdd\x7d\xe2\x84\x79\ +\xc1\xac\x38\xaf\xcc\x55\x55\x9d\x3f\xc9\x25\xb2\xf1\x67\xf3\xca\ +\x49\xae\x91\xe4\x56\x49\xae\x99\xe4\x80\x09\x53\xfb\x62\x92\x1b\ +\x74\xf7\x97\xa7\xb8\x78\x55\x7d\x24\x1b\x4f\x50\x5a\x15\xff\x99\ +\xe4\x31\x49\x5e\xda\xdd\xab\x59\x18\xad\x81\xaa\xfa\xef\x49\x86\ +\xfa\xc2\xdd\xe7\xba\xfb\xd0\x81\x62\x31\x43\x55\x35\xd8\x59\xef\ +\xee\x29\x7f\x1f\x00\x0c\xa2\xaa\xce\x97\x8d\x7a\xf7\xb0\x24\xb7\ +\x4e\x72\xbb\x2c\xb6\x3f\x3b\x21\xc9\xd5\xbb\xfb\x4b\x0b\xbc\x66\ +\x12\xfd\x28\xf3\xa5\x1f\x3d\x7b\x03\xf7\xa3\xb7\xed\xee\x77\x0d\ +\x14\x0b\x80\xfd\xa4\xfe\x02\x80\xc5\x32\xd7\x63\x8e\xcc\xf5\xd8\ +\x0e\x73\x3d\x00\x60\xd5\xac\xf3\x7c\x7e\xbb\x56\xf2\x8d\x2e\x55\ +\x75\x50\x92\x97\x67\x9a\x9b\xe4\xdf\x49\xf2\xba\x24\xaf\x4c\x72\ +\x54\x77\xff\xd7\x04\x39\xc0\xd2\x70\x5e\x99\xb3\xcd\xa1\xe6\x89\ +\x49\x3e\xb3\xf5\xbf\xdf\x1c\xc6\xde\x3a\xc9\xbd\x92\xdc\x35\xc9\ +\xb9\x17\x9c\xda\xa5\x93\xbc\xb2\xaa\x6e\xd7\xdd\xa7\x2d\xf8\xda\ +\xab\xe8\x32\x49\xfe\x26\xc9\xff\xaa\xaa\x47\x74\xf7\x07\xa7\x4e\ +\x08\x00\x60\x91\x36\x9f\x82\xf9\xd1\xcd\x9f\x97\x25\x49\x55\xdd\ +\x20\xc9\x2f\x27\x79\x60\xc6\xaf\x77\x2f\x98\xe4\xa9\x49\x7e\x7e\ +\xe4\xeb\x9c\x89\x7e\x94\x39\xd3\x8f\x02\xb0\x8a\xd4\x5f\x00\xc0\ +\x00\xcc\xf5\x00\x00\x80\xa5\xb6\xae\xf3\xf9\x9d\x58\xc9\x45\x97\ +\x24\xbf\x9f\xe4\x0e\x0b\xbe\xe6\x07\x93\x3c\x3b\xc9\x6b\x3c\xed\ +\x09\xb6\xc5\x79\x65\xe9\x6c\x0e\x41\x5f\x93\xe4\x35\x55\x75\x81\ +\x24\x3f\x97\xe4\xa1\x49\x6e\xb0\xc0\x34\x6e\x99\xe4\xc9\x49\x1e\ +\xbd\xc0\x6b\x8e\xe1\xb4\xcc\xa7\x1e\xb9\x69\x92\xf7\x57\xd5\x91\ +\x49\x1e\xb7\x0c\x1b\xcb\x00\x00\x63\xe9\xee\x7f\x49\xf2\x2f\x55\ +\xf5\xbf\x93\x3c\x3e\xc9\xc3\x92\x1c\x34\xe2\x25\xef\x5b\x55\x4f\ +\xeb\xee\x0f\x8f\x78\x8d\x1f\xa5\x1f\x65\xe9\xe8\x47\x01\x58\x72\ +\xea\x2f\x00\x58\x6e\xe6\x7a\x00\x00\x00\x23\x58\x93\xf9\xfc\xb6\ +\x1d\x38\x75\x02\x43\xab\xaa\x3b\x26\x79\xdc\x02\x2f\x79\x54\x92\ +\xdb\x76\xf7\x4f\x74\xf7\x5f\xbb\x49\x0e\xfb\xcf\x79\x65\x15\x74\ +\xf7\xb7\xbb\xfb\xaf\xba\xfb\x86\x49\x7e\x32\xc9\x07\x16\x78\xf9\ +\x47\x56\xd5\x6d\x17\x78\xbd\x31\xdc\x33\xc9\x9f\x25\x39\x7d\xea\ +\x44\x36\x1d\x90\xe4\x17\x93\x1c\x57\x55\x8f\xad\xaa\x1f\x9b\x3a\ +\x21\x00\x80\x29\x75\xf7\xd7\xba\xfb\x37\x92\x5c\x3f\xc9\xd8\x37\ +\xb9\xfe\x78\xe4\xf8\x3f\xa0\x1f\x65\x15\xe8\x47\x01\x58\x26\xea\ +\x2f\x00\x58\x09\xe6\x7a\x00\x00\x00\x23\x5a\xd5\xf9\xfc\x4e\xad\ +\xd4\xa2\x4b\x55\x5d\x3c\xc9\x4b\x16\x74\xb9\xcf\x25\xf9\xb9\xee\ +\xbe\x75\x77\xbf\x6b\x41\xd7\x84\x95\xe1\xbc\xb2\x8a\xba\xfb\xed\ +\xd9\x78\x7a\xd0\xbd\x93\x7c\x61\x01\x97\x3c\x20\xc9\x91\x55\x75\ +\xc1\x05\x5c\x6b\x2c\xdf\xec\xee\x5f\x4f\x72\x9d\x24\x6f\x9b\x3a\ +\x99\x2d\xce\x9f\xe4\x8f\x92\x1c\x5b\x55\x87\x4f\x9c\x0b\x00\xc0\ +\xe4\xba\xfb\x98\x24\x37\x4b\xf2\xe7\x23\x5e\xe6\x0e\x55\x75\xe3\ +\x11\xe3\x27\xd1\x8f\xb2\x9a\xf4\xa3\x00\xcc\x99\xfa\x0b\x00\x56\ +\x86\xb9\x1e\x00\x00\xc0\x02\xac\xd2\x7c\x7e\x37\x56\x6a\xd1\x25\ +\x1b\xaf\x1e\xbf\xe4\x02\xae\xf3\xac\x24\xd7\xea\xee\xff\xbb\x80\ +\x6b\xc1\xaa\x72\x5e\x59\x49\xdd\xbd\xa7\xbb\x5f\x91\xe4\x5a\x49\ +\x9e\xbf\x80\x4b\x5e\x36\x1b\x37\x6e\x97\x5a\x77\x7f\xa2\xbb\x7f\ +\x2a\xc9\xcf\x26\xf9\xf4\xd4\xf9\x6c\x71\xc5\x24\xaf\xa9\xaa\xb7\ +\x57\xd5\xb5\xa7\x4e\x06\x00\x60\x4a\xdd\xfd\xdd\xee\x7e\x68\x92\ +\xdf\x18\xf1\x32\x8f\x1a\x31\xf6\xf7\xe9\x47\x59\x49\xfa\x51\x00\ +\x66\x4c\xfd\x05\x00\x2b\xc4\x5c\x0f\x00\x00\x60\x7c\x2b\x34\x9f\ +\xdf\xb1\x95\x59\x74\xa9\xaa\xbb\x66\xe3\x35\xa9\x63\xfa\x7a\x92\ +\x3b\x75\xf7\xc3\xbd\xe2\x1c\x76\xce\x79\x65\x1d\x74\xf7\x09\xdd\ +\xfd\xe0\x6c\xdc\xe0\xfd\xe6\xc8\x97\x3b\xa2\xaa\x6e\x3a\xf2\x35\ +\x16\xa2\xbb\xdf\x90\xe4\x9a\x49\x1e\x9d\xe4\xdb\x13\xa7\xb3\xd5\ +\xed\x92\x1c\x5d\x55\xcf\xa9\xaa\xff\x36\x75\x32\x00\x00\x53\xea\ +\xee\x67\x24\xf9\xc5\x91\xc2\xdf\xad\xaa\x2e\x37\x52\x6c\xfd\x28\ +\x6b\x41\x3f\x0a\xc0\x9c\xa8\xbf\x00\x60\x75\x99\xeb\x01\x00\x00\ +\x8c\x6f\x99\xe7\xf3\xbb\xb5\x12\x8b\x2e\x55\x75\xde\x6c\x3c\x0d\ +\x6a\x4c\x1f\x4e\x72\xc3\xee\x7e\xeb\xc8\xd7\x81\x95\xe6\xbc\xb2\ +\x6e\x36\x6f\xf0\xde\x30\xc9\x31\x23\x5e\xe6\x80\x24\xcf\xac\xaa\ +\x03\x46\xbc\xc6\xc2\x74\xf7\xa9\xdd\xfd\xa7\x49\xae\x92\xe4\x45\ +\x49\xf6\x4c\x9c\xd2\xf7\x1d\x94\xe4\x88\x24\x9f\xae\xaa\x5f\xaf\ +\xaa\x83\xa7\x4e\x08\x00\x60\x2a\xdd\x7d\x64\xc6\x79\x72\xcc\x81\ +\x19\xe9\x26\x9d\x7e\x94\x75\xa3\x1f\x05\x60\x6a\xea\x2f\x00\x58\ +\x7d\xe6\x7a\x00\x00\x00\xe3\x5b\xc6\xf9\xfc\x10\x56\x62\xd1\x25\ +\xc9\x63\x92\x5c\x76\xc4\xf8\x6f\x49\x72\xab\xee\xfe\xdc\x88\xd7\ +\x80\x75\xe1\xbc\xb2\x76\xba\xbb\x93\xdc\x32\xc9\xdb\x46\xbc\xcc\ +\x8d\x92\xdc\x6f\xc4\xf8\x0b\xd7\xdd\x5f\xe9\xee\x07\x65\xe3\x8b\ +\x59\xef\x99\x3a\x9f\x2d\x2e\x9c\xe4\x99\x49\x3e\x5a\x55\x3f\x35\ +\x71\x2e\x00\x00\x93\xd9\x7c\x72\xcc\xf3\x47\x08\x3d\xd6\x8d\x34\ +\xfd\x28\x6b\x47\x3f\x0a\xc0\xc4\xd4\x5f\x00\xb0\x26\xcc\xf5\x00\ +\x00\x00\xc6\xb5\x84\xf3\xf9\x5d\x5b\xfa\x45\x97\xaa\xba\x54\x36\ +\x5e\x83\x3a\x96\x97\x26\xf9\xd9\xee\x3e\x69\xc4\x6b\xc0\x5a\x70\ +\x5e\x59\x67\xdd\x7d\x42\x92\xbb\x24\x79\xcd\x88\x97\xf9\x83\xaa\ +\x3a\x64\xc4\xf8\x93\xe8\xee\x0f\x27\xb9\x55\x92\x7b\x27\xf9\xc2\ +\xc4\xe9\x6c\x75\x58\x92\xbf\xaf\xaa\xd7\x57\xd5\x55\xa6\x4e\x06\ +\x00\x60\x22\x8f\x48\x72\xf4\xc0\x31\xaf\x5c\x55\xd7\x1d\x32\xa0\ +\x7e\x94\x75\xa6\x1f\x05\x60\x0a\xea\x2f\x00\x58\x4f\xe6\x7a\x00\ +\x00\x00\xa3\x7a\x44\x96\x60\x3e\x3f\x94\xa5\x5f\x74\x49\xf2\x84\ +\x24\xe7\x1e\x29\xf6\x4b\x93\xdc\xbf\xbb\x4f\x1b\x29\x3e\xac\x1b\ +\xe7\x95\xb5\xd6\xdd\xa7\x26\xb9\x47\xc6\xfb\x72\xd1\xe5\x92\x3c\ +\x60\xa4\xd8\x93\xea\xee\x3d\xdd\xfd\x8a\x24\x57\x4b\xf2\xbf\x93\ +\x9c\x3c\x71\x4a\x5b\xdd\x25\xc9\xc7\xab\xea\x4f\xab\xea\x82\x53\ +\x27\x03\x00\xb0\x48\xdd\x7d\x4a\x92\x07\x26\x39\x7d\xe0\xd0\x77\ +\x1d\x38\x9e\x7e\x94\xb5\xa6\x1f\x05\x60\x02\xea\x2f\x00\x58\x53\ +\xe6\x7a\x00\x00\x00\xe3\x58\xa2\xf9\xfc\x20\x96\x7a\xd1\xa5\xaa\ +\x2e\x9f\xe4\x41\x23\x85\x7f\x73\x36\x6e\x92\x0f\xfd\x07\x01\xd6\ +\x92\xf3\x0a\x1b\x36\xff\x9c\xde\x3b\xc9\xdb\x46\xba\xc4\xe3\xab\ +\xea\x5c\x23\xc5\x9e\x5c\x77\x9f\xdc\xdd\x4f\xca\xc6\x8d\xf1\x97\ +\x4f\x9c\xce\x56\xe7\x4a\xf2\xc8\x24\xff\x56\x55\x0f\xaa\xaa\xa5\ +\xae\xb1\x00\x00\xb6\xa3\xbb\x8f\x4e\xf2\xbc\x81\xc3\xde\x69\xa8\ +\x40\xfa\x51\xd8\xa0\x1f\x05\x60\x51\xd4\x5f\x00\x40\x62\xae\x07\ +\x00\x00\x30\x86\xb9\xcf\xe7\x87\xb4\xec\xcd\xda\xaf\x67\xa3\x01\ +\x1d\xda\x87\x93\xdc\xc3\x4d\x72\x18\x94\xf3\x0a\x9b\xb6\x3c\x49\ +\xf7\x63\x23\x84\xbf\x5c\x92\x7b\x8d\x10\x77\x56\xba\xfb\x0b\xdd\ +\x7d\x9f\x24\xb7\x48\xf2\x2f\x53\xe7\xb3\xc5\x8f\x27\xf9\x8b\x24\ +\x1f\xaa\xaa\x5b\x4e\x9d\x0c\x00\xc0\x02\xfd\x61\x92\x53\x06\x8c\ +\x77\xc3\xaa\x1a\xea\x09\xe0\xfa\x51\xd8\xa4\x1f\x05\x60\x41\xd4\ +\x5f\x00\xc0\x0f\x98\xeb\x01\x00\x00\x0c\x6e\xce\xf3\xf9\xc1\x2c\ +\xed\xa2\x4b\x55\x5d\x20\xc9\xaf\x8c\x10\xfa\xeb\x49\x0e\xef\xee\ +\xef\x8c\x10\x1b\xd6\x92\xf3\x0a\x67\xd5\xdd\x27\x24\x39\x3c\xc9\ +\x37\x47\x08\xff\xc8\x11\x62\xce\x52\x77\xff\x53\x92\x1b\x67\xe3\ +\x75\x7c\x5f\x99\x38\x9d\xad\xae\x97\xe4\xa8\xaa\x7a\xf9\xe6\x13\ +\x2c\x01\x00\x56\x5a\x77\x7f\x29\xc9\x91\x03\x86\x3c\x57\x92\x1b\ +\xed\x36\x88\x7e\x14\xce\x4a\x3f\x0a\xc0\x98\xd4\x5f\x00\xc0\xde\ +\x98\xeb\x01\x00\x00\x0c\x63\xae\xf3\xf9\xa1\x2d\xed\xa2\x4b\x92\ +\x5f\x48\x72\xc1\x31\xe2\x76\xf7\x17\x46\x88\x0b\xeb\xcc\x79\x85\ +\xb3\xd1\xdd\x9d\xe4\x97\x46\x08\x7d\xdd\xaa\xba\xd9\x08\x71\x67\ +\xa9\xbb\xcf\xe8\xee\xbf\x4c\x72\xd5\x24\x4f\x49\x72\xea\xc4\x29\ +\x6d\x75\xaf\x24\x9f\xaa\xaa\x27\x55\xd5\x79\xa7\x4e\x06\x00\x60\ +\x64\x2f\x1e\x38\xde\xf5\x06\x88\xa1\x1f\x85\xb3\xa1\x1f\x05\x60\ +\x44\xea\x2f\x00\x60\xaf\xcc\xf5\x00\x00\x00\x06\x33\xc7\xf9\xfc\ +\xa0\x96\x79\xd1\xe5\x81\x23\xc4\x7c\x56\x77\xbf\x75\x84\xb8\xb0\ +\xee\x9c\x57\xd8\x8b\xee\x7e\x7d\x92\x17\x8c\x10\x7a\x8c\x73\x37\ +\x6b\xdd\x7d\x42\x77\x3f\x26\xc9\x35\x93\xbc\x7e\xea\x7c\xb6\x38\ +\x77\x92\x27\x66\xe3\xc6\xf8\x7d\xaa\xea\x80\xa9\x13\x02\x00\x18\ +\x43\x77\xbf\x3f\xc9\x67\x07\x0c\x79\xd8\x00\x31\xf4\xa3\xb0\x17\ +\xfa\x51\x00\x46\xa2\xfe\x02\x00\xce\x91\xb9\x1e\x00\x00\xc0\xee\ +\xcc\x74\x3e\x3f\xa8\xa5\x5c\x74\xa9\xaa\x6b\x25\xb9\xc1\xc0\x61\ +\x3f\x97\xe4\xf1\x03\xc7\x84\xb5\xe7\xbc\xc2\x7e\x79\x54\x92\xff\ +\x18\x38\xe6\xbd\xaa\xea\x3c\x03\xc7\x5c\x0a\xdd\xfd\xe9\xee\xbe\ +\x6b\x92\x3b\x26\xf9\xc4\xd4\xf9\x6c\x71\xd9\x24\x2f\x4d\xf2\x9e\ +\xaa\xba\xe1\xd4\xc9\x00\x00\x8c\xe4\xed\x03\xc6\xba\xda\x6e\x3e\ +\xac\x1f\x85\xfd\xa2\x1f\x05\x60\x30\xea\x2f\x00\x60\xbb\xcc\xf5\ +\x00\x00\x00\x76\x65\x36\xf3\xf9\x31\x2c\xe5\xa2\x4b\x36\x5e\x17\ +\x3a\xb4\x47\x74\xf7\x49\x23\xc4\x85\x75\xe7\xbc\xc2\x39\xe8\xee\ +\x13\xb2\xf1\xe5\xa2\x21\x9d\x2f\xc9\x5d\x07\x8e\xb9\x54\xba\xfb\ +\x1f\x92\x5c\x37\xc9\xc3\x92\x7c\x73\xe2\x74\xb6\xba\x59\x92\x0f\ +\x56\xd5\x5f\x56\xd5\x25\xa7\x4e\x06\x00\x60\x60\xef\x1e\x30\xd6\ +\xa5\x77\xf9\x79\xfd\x28\x9c\x03\xfd\x28\x00\x03\x53\x7f\x01\x00\ +\x3b\x62\xae\x07\x00\x00\xb0\x23\x73\x9a\xcf\x0f\x6e\x59\x17\x5d\ +\xfe\xe7\xc0\xf1\x8e\xea\xee\xd7\x0e\x1c\x13\xd8\xe0\xbc\xc2\xfe\ +\x79\x45\x92\x0f\x0c\x1c\xf3\x1e\x03\xc7\x5b\x3a\xdd\x7d\x5a\x77\ +\x3f\x3b\xc9\x95\x93\xfc\x79\x92\xd3\x27\x4e\xe9\xfb\x0e\x48\xf2\ +\xcb\x49\x8e\xab\xaa\xc7\x54\xd5\x21\x53\x27\x04\x00\x30\x90\x21\ +\x9f\xbc\xb9\xdb\x1b\x69\xfa\x51\xd8\x3f\xfa\x51\x00\x86\xa2\xfe\ +\x02\x00\x76\xcc\x5c\x0f\x00\x00\x60\xdb\xe6\x34\x9f\x1f\xdc\xd2\ +\x2d\xba\x54\xd5\x15\x92\x5c\x73\xe0\xb0\x4f\x1c\x38\x1e\x10\xe7\ +\x15\xb6\xa3\xbb\xf7\x24\x79\xd2\xc0\x61\xef\xe8\x46\xeb\x86\xee\ +\xfe\xaf\xee\x7e\x68\x36\x9e\x04\xf5\xff\xa6\xce\x67\x8b\x0b\x24\ +\xf9\xe3\x24\x9f\xa8\x2a\x4f\x3c\x06\x00\x56\x41\x0f\x18\xeb\xfc\ +\x55\xb5\xa3\x7b\x57\xfa\x51\xd8\x7f\xfa\x51\x00\x86\xa0\xfe\x02\ +\x00\x86\x62\xae\x07\x00\x00\xb0\xdf\x66\x31\x9f\x1f\xcb\xac\x92\ +\xd9\x4f\x77\x1c\x38\xde\xfb\xbb\xfb\x5d\x03\xc7\x04\x36\x38\xaf\ +\xb0\x0d\xdd\xfd\xf7\x49\x8e\x1e\x30\xe4\xf9\x93\xdc\x62\xc0\x78\ +\x4b\xaf\xbb\x3f\xde\xdd\x77\xc8\xc6\xd3\x25\x3f\x33\x75\x3e\x5b\ +\x5c\x29\xc9\x6b\xab\xea\x1f\xaa\x6a\xe8\x2f\x04\x00\x00\x2c\x4c\ +\x77\x7f\x63\xe0\x90\x17\xdc\xe1\xe7\xf4\xa3\xb0\x0d\xfa\x51\x00\ +\x06\xa0\xfe\x02\x00\x06\x65\xae\x07\x00\x00\xb0\x6f\x33\x9a\xcf\ +\x8f\x62\x19\x17\x5d\x6e\x37\x70\xbc\x67\x0f\x1c\x0f\xf8\x21\xe7\ +\x15\xb6\x6f\xe8\x3f\xe7\x77\x18\x38\xde\x4a\xe8\xee\xd7\x66\xe3\ +\x09\x93\x8f\x4d\xf2\xed\x69\xb3\x39\x93\x9f\x4c\xf2\xd1\xaa\x7a\ +\x76\x55\x5d\x74\xea\x64\x00\x00\x76\xe8\xa4\x01\x63\x1d\xb0\xc3\ +\xcf\xe9\x47\x61\xfb\xf4\xa3\x00\xec\x86\xfa\x0b\x00\x18\x85\xb9\ +\x1e\x00\x00\xc0\x3e\xcd\x61\x3e\x3f\x8a\x65\x5c\x74\xb9\xc9\x80\ +\xb1\x4e\x4a\xf2\xda\x01\xe3\x01\x67\xe6\xbc\xc2\xf6\xbd\x3a\xc9\ +\x29\x03\xc6\xbb\xf9\x80\xb1\x56\x4a\x77\x7f\xb7\xbb\xff\x24\xc9\ +\x55\x93\xbc\x38\xc9\x9e\x89\x53\xfa\xbe\x83\x92\xfc\xaf\x24\x9f\ +\xae\xaa\x87\x56\xd5\xc1\x53\x27\x04\x00\xb0\x4d\x07\x0d\x18\x6b\ +\xa7\x35\x9a\x7e\x14\xb6\x4f\x3f\x0a\xc0\x6e\xa8\xbf\x00\x80\xd1\ +\x98\xeb\x01\x00\x00\xec\xd5\x1c\xe6\xf3\xa3\x58\xaa\x45\x97\xaa\ +\xba\x44\x92\x43\x07\x0c\xf9\x9a\xee\x1e\x72\x8b\x09\xd8\xe4\xbc\ +\xc2\xce\x74\xf7\xf1\x49\xde\x30\x60\xc8\x1b\x56\xd5\xb9\x06\x8c\ +\xb7\x72\xba\xfb\xcb\xdd\xfd\x80\x24\x37\x4e\xf2\xde\xa9\xf3\xd9\ +\xe2\x22\x49\xfe\x2c\xc9\x47\xaa\xca\x93\x90\x01\x80\xa5\x50\x55\ +\x07\x24\x39\xf7\xc4\x39\xe8\x47\x61\x07\xf4\xa3\x00\xec\x94\xfa\ +\x0b\x00\x58\x14\x73\x3d\x00\x00\x80\x1f\x9a\xc3\x7c\x7e\x4c\xcb\ +\xf6\x24\x81\xeb\x0c\x1c\xef\x95\x03\xc7\x03\x7e\xc8\x79\x85\x9d\ +\xfb\xbb\x24\xf7\x18\x28\xd6\x79\x92\x5c\x23\xc9\x31\x03\xc5\x5b\ +\x59\xdd\xfd\xa1\xaa\xba\x45\x92\x7b\x27\x79\x4a\x92\xcb\x4e\x9c\ +\xd2\xf7\x5d\x33\xc9\xdb\xaa\xea\x75\x49\x1e\xd9\xdd\x9f\x9e\x3a\ +\x21\x00\x80\x7d\xb8\xe4\xc0\xf1\x4e\xdc\xc1\x67\xf4\xa3\xb0\x73\ +\xfa\x51\x00\x76\x42\xfd\x05\x4b\xae\xaa\x7e\x2c\x1b\xf5\x1b\x30\ +\x0f\xdf\xb3\xf4\xb9\x6f\xe6\x7a\x00\xeb\x4b\xed\x0a\xb3\xa3\x76\ +\x85\x69\xcd\x61\x3e\x3f\x9a\x65\x5b\x74\xb9\xe6\x80\xb1\xce\x48\ +\x72\xd4\x80\xf1\x80\x33\x73\x5e\x61\xe7\xde\x39\x70\xbc\x6b\xc7\ +\x17\x8b\xf6\x4b\x77\xef\x49\xf2\xb2\xcd\x9b\xcf\x8f\x49\xf2\xe8\ +\xcc\x67\xe3\xf9\xae\x49\xee\x5c\x55\xcf\x48\xf2\x87\xdd\x7d\xc2\ +\xc4\xf9\x00\x00\x9c\x9d\x2b\x0f\x18\xeb\xc4\xee\x3e\x6d\x07\x9f\ +\xd3\x8f\xc2\xce\xe9\x47\x01\xd8\x09\xf5\x17\x2c\xbf\x23\x92\x3c\ +\x7d\xea\x24\x80\x1f\x78\x5d\x92\xc3\xa7\x4e\x62\xee\xcc\xf5\x00\ +\xd6\x96\xda\x15\xe6\x45\xed\x0a\xd3\x9a\xc3\x7c\x7e\x34\x07\x4e\ +\x9d\xc0\x36\x5d\x7d\xc0\x58\x47\x77\xf7\xf1\x03\xc6\x03\xce\xcc\ +\x79\x85\x1d\xea\xee\xaf\x25\xf9\xf8\x80\x21\x0f\x1b\x30\xd6\x5a\ +\xe8\xee\xef\x74\xf7\x13\x93\x5c\x2d\xf3\x7a\x82\xe4\x21\xd9\xb8\ +\x49\x7f\x5c\x55\x3d\xa0\xaa\x96\xad\x96\x03\x00\x56\xdf\x90\x5f\ +\x72\xfc\xf2\x0e\x3f\xa7\x1f\x85\x1d\xd2\x8f\x02\xb0\x43\xea\x2f\ +\x00\x60\x32\xe6\x7a\x00\x00\xc0\x1a\x9b\xc3\x7c\x7e\x34\xcb\xd6\ +\x44\x1d\x3a\x60\xac\x7f\x1a\x30\x16\x70\x56\x87\x0e\x18\xcb\x79\ +\x65\x1d\xbd\x67\xc0\x58\x57\x1c\x30\xd6\x5a\xe9\xee\xcf\x77\xf7\ +\xbd\x92\xdc\x2a\xc9\xd1\x53\xe7\xb3\xc5\x25\x92\xbc\x28\xc9\x07\ +\xab\xea\xe6\x53\x27\x03\x00\xb0\xc5\x6d\x06\x8c\xf5\x85\x1d\x7e\ +\xee\xd0\x01\x73\xd0\x8f\xb2\x8e\xf4\xa3\x00\x6c\xd7\xa1\x03\xc6\ +\x52\x7f\x01\x00\x3b\x62\xae\x07\x00\x00\xac\xa1\xdb\x0c\x18\x6b\ +\xa7\xf3\xf9\xd1\x2c\xdb\xa2\xcb\xe5\x07\x8c\xf5\x89\x01\x63\x01\ +\x67\xe5\xbc\xc2\xee\x1c\x3b\x60\xac\x43\x07\x8c\xb5\x96\xba\xfb\ +\xdd\x49\x6e\x98\xe4\x57\x92\x7c\x75\xe2\x74\xb6\xba\x41\x92\xf7\ +\x54\xd5\xcb\xaa\xea\xb2\x53\x27\x03\x00\xac\xb7\xaa\x3a\x28\xc9\ +\xed\x07\x0c\xf9\x99\x1d\x7e\x4e\x3f\x0a\xbb\xa3\x1f\x05\x60\xbb\ +\xd4\x5f\x00\xc0\x6c\x98\xeb\x01\x00\x00\xeb\x60\x46\xf3\xf9\xd1\ +\x2c\xdb\xa2\xcb\x25\x06\x8c\xf5\xe9\x01\x63\x01\x67\xe5\xbc\xc2\ +\xee\x0c\xf9\xe7\xfe\x32\x03\xc6\x5a\x5b\xdd\x7d\x46\x77\xbf\x30\ +\xc9\x55\x92\x3c\x35\xc9\xf7\x26\x4e\x69\xab\x7b\x67\xe3\xb5\xe7\ +\x4f\xac\xaa\xf3\x4c\x9d\x0c\x00\xb0\xb6\xee\x98\xe4\x62\x03\xc6\ +\xfb\xf8\x0e\x3f\xa7\x1f\x85\xdd\xd1\x8f\x02\xb0\x5d\xea\x2f\x00\ +\x60\x56\xcc\xf5\x00\x00\x80\x35\x30\x97\xf9\xfc\x68\x96\x6d\xd1\ +\xe5\xa2\x03\xc6\x72\xa3\x1c\xc6\xe5\xbc\xc2\xee\x1c\x37\x60\xac\ +\x21\x8b\x99\xb5\xd7\xdd\x27\x74\xf7\xa3\x92\x5c\x2b\xc9\x1b\xa7\ +\xce\x67\x8b\xf3\x24\x79\x52\x92\x4f\x55\xd5\xbd\xaa\xea\x80\x89\ +\xf3\x01\x00\xd6\xcf\x03\x07\x8e\xf7\xd1\x1d\x7e\x4e\x3f\x0a\xbb\ +\xa3\x1f\x05\x60\xbb\xd4\x5f\x00\xc0\x2c\x99\xeb\x01\x00\x00\x2b\ +\x6c\x2e\xf3\xf9\xd1\x2c\xcd\xa2\x4b\x55\x5d\x78\xe0\x90\x5f\x1a\ +\x38\x1e\xb0\xc9\x79\x85\x41\x7c\x6d\xc0\x58\xe7\xad\xaa\x1f\x1b\ +\x30\x1e\x49\xba\xfb\xb8\xee\xbe\x4b\x92\x3b\x25\x39\x76\xea\x7c\ +\xb6\xb8\x5c\x92\x97\x27\x39\xaa\xaa\xae\x3f\x75\x32\x00\xc0\x7a\ +\xa8\xaa\xc3\x92\xdc\x6d\xc0\x90\x67\x24\xf9\xe7\x1d\xe4\x71\xe1\ +\x01\x73\x48\xf4\xa3\xac\x27\xfd\x28\x00\xfb\x4d\xfd\x05\x00\x2c\ +\x03\x73\x3d\x00\x00\x60\x95\xcc\x65\x3e\x3f\xb6\xa5\x59\x74\x49\ +\x72\xbe\x01\x63\x9d\xde\xdd\xa7\x0e\x18\x0f\x38\x33\xe7\x15\x76\ +\xef\xf8\x81\xe3\x79\xed\xf5\x48\xba\xfb\xad\x49\xae\x93\xe4\xe1\ +\x49\xbe\x35\x6d\x36\x67\x72\x8b\x24\x1f\xaa\xaa\x17\x56\xd5\x25\ +\xa6\x4e\x06\x00\x58\x79\xbf\x9f\x64\xc8\x27\x4f\x7e\xa4\xbb\x4f\ +\xdc\xc1\xe7\xf4\xa3\xb0\x7b\xfa\x51\x00\xb6\x43\xfd\x05\x00\x2c\ +\x0d\x73\x3d\x00\x00\x60\x45\xcc\x65\x3e\x3f\xaa\x65\x5a\x74\x39\ +\xd7\x80\xb1\x66\xf7\x2f\x02\x56\x8c\xf3\x0a\xbb\xd4\xdd\x7b\x92\ +\x9c\x32\x60\xc8\x83\x06\x8c\xc5\x8f\xe8\xee\xd3\xba\xfb\x59\x49\ +\xae\x9c\xe4\xb9\x49\x4e\x9f\x38\xa5\xef\x3b\x20\x1b\xaf\x28\x3c\ +\xae\xaa\x1e\x55\x55\x87\x4c\x9d\x10\x00\xb0\x7a\xaa\xea\xa7\x33\ +\xec\xd3\x62\x92\xe4\xad\x3b\xfc\x9c\x7e\x14\x76\x49\x3f\x0a\xc0\ +\x36\xa9\xbf\x00\x80\xa5\x62\xae\x07\x00\x00\x2c\xb3\x99\xcd\xe7\ +\x47\xb5\x4c\x8b\x2e\x43\xfa\xf6\xd4\x09\x00\xfb\xcd\x79\x65\x9d\ +\x7d\x77\xc0\x58\x17\x18\x30\x16\x7b\xd1\xdd\xdf\xe8\xee\x87\x24\ +\xb9\x5e\x92\x77\x4c\x9d\xcf\x16\x17\x4c\xf2\x94\x24\xff\x5a\x55\ +\x77\x99\x3a\x19\x00\x60\x75\x54\xd5\xc5\x92\xbc\x60\x84\xd0\xaf\ +\x1f\x21\xe6\x76\xe9\x47\x59\x67\xfa\x51\x00\xa6\xa0\xfe\x02\x00\ +\x16\xc6\x5c\x0f\x00\x00\x58\x36\x2b\x3e\x9f\x3f\x8b\x75\x5d\x74\ +\x31\x5c\x85\xe5\xe1\xbc\xb2\xce\xce\x33\x60\xac\x6f\x0d\x18\x8b\ +\x73\xd0\xdd\x1f\xeb\xee\xdb\x67\x63\x73\xfa\xdf\xa7\xce\x67\x8b\ +\xab\x24\x79\x7d\x55\xbd\xb5\xaa\x0e\x9b\x3a\x19\x00\x60\xb9\x55\ +\xd5\x41\x49\x5e\x9a\xe4\x32\x03\x87\xfe\xcf\x24\x1f\x18\x38\xe6\ +\x4e\xe8\x47\x59\x67\xfa\x51\x00\xa6\xa0\xfe\x02\x00\x16\xce\x5c\ +\x0f\x00\x00\x58\x06\x6b\x30\x9f\x3f\x8b\x65\x5a\x74\x19\xf2\x29\ +\x82\x07\x0d\x18\x0b\x38\x2b\xe7\x15\x76\x69\xb3\x28\xf1\x3a\xea\ +\x25\xd7\xdd\xaf\x49\x72\x8d\x24\x8f\x4f\x72\xe2\xc4\xe9\x6c\x75\ +\xc7\x24\xc7\x54\xd5\xb3\xaa\xea\x22\x53\x27\x03\x00\x2c\x9f\xaa\ +\x3a\x20\xc9\xf3\x93\xdc\x61\x84\xf0\x7f\xdd\xdd\x7b\x76\xf8\x59\ +\xfd\x28\xec\x92\x7e\x14\x80\x6d\x52\x7f\x01\x00\x2b\xc1\x5c\x0f\ +\x00\x00\x98\xab\x19\xcf\xe7\x47\xb5\x4c\x8b\x2e\xdf\x19\x30\xd6\ +\xf9\xab\x6a\xc8\xa7\x12\x02\x67\xe6\xbc\xc2\xee\x0d\xfa\xf4\xc2\ +\xee\xfe\xd6\x90\xf1\xd8\x7f\xdd\xfd\xdd\xee\xfe\xa3\x24\x57\x4d\ +\xf2\x92\x24\x73\x29\x0a\x0f\x4a\xf2\xb0\x24\x9f\xae\xaa\x87\x54\ +\xd5\xc1\x53\x27\x04\x00\x2c\x87\xcd\x9b\x68\x7f\x9a\xe4\x81\x23\ +\x5d\xe2\x45\xbb\xf8\xac\x7e\x14\x76\x4f\x3f\x0a\xc0\x76\xa8\xbf\ +\x00\x80\x95\x61\xae\x07\x00\x00\xcc\xcd\xcc\xe7\xf3\xa3\x5a\x9a\ +\x45\x97\xee\x3e\x3e\xc9\x19\x03\x86\xbc\xf4\x80\xb1\x80\x2d\x9c\ +\x57\x18\xc4\x90\xaf\x97\x3b\x79\xc0\x58\xec\x50\x77\x7f\xa9\xbb\ +\x7f\x29\xc9\x4d\x92\xbc\x6f\xe2\x74\xb6\xba\x68\x92\x3f\x4f\x72\ +\x74\x55\xdd\x6e\xea\x64\x00\x80\x79\xdb\x7c\xd3\xc3\x8b\x92\xfc\ +\xd6\x48\x97\x78\x73\x77\x7f\x7a\xa7\x1f\xd6\x8f\xc2\x20\xf4\xa3\ +\x00\xec\x37\xf5\x17\x00\xb0\x8a\xcc\xf5\x00\x00\x80\x39\x98\xfb\ +\x7c\x7e\x6c\x4b\xb3\xe8\xb2\xe9\x9b\x03\xc6\xba\xc2\x80\xb1\x80\ +\xb3\x72\x5e\x61\x77\xae\x3c\x60\xac\x2f\x0e\x18\x8b\x5d\xea\xee\ +\x0f\x26\xb9\x79\x92\x5f\x48\xf2\x9f\x13\xa7\xb3\xd5\xb5\x92\xbc\ +\xbd\xaa\x5e\x53\x55\x35\x75\x32\x00\xc0\xfc\x54\xd5\xa5\x93\xbc\ +\x23\xc9\x2f\x8f\x78\x99\xa7\x0c\x10\x43\x3f\x0a\xbb\xa3\x1f\x05\ +\x60\xbb\xd4\x5f\x00\xc0\x4a\x32\xd7\x03\x00\x00\xa6\xb2\x44\xf3\ +\xf9\xd1\x2c\xdb\xa2\xcb\x97\x06\x8c\x75\xad\x01\x63\x01\x67\xe5\ +\xbc\xc2\xee\x0c\xf9\xc5\xa2\xaf\x0e\x18\x8b\x01\x74\xf7\x9e\xee\ +\xfe\xdb\x24\x57\x4b\xf2\x07\x49\x4e\x99\x38\xa5\xad\x0e\x4f\x72\ +\x6c\x55\xfd\x51\x55\x5d\x60\xea\x64\x00\x80\x79\xa8\xaa\xfb\x24\ +\xf9\x48\x92\x5b\x8d\x78\x99\x7f\xec\xee\x7f\x1c\x20\x8e\x7e\x14\ +\x76\x47\x3f\x0a\xc0\x76\xa9\xbf\x00\x80\x95\x65\xae\x07\x00\x00\ +\x2c\xda\x92\xcd\xe7\x47\xb3\x6c\x8b\x2e\x9f\x1f\x30\xd6\x75\x07\ +\x8c\x05\x9c\x95\xf3\x0a\xbb\x73\xcd\x01\x63\x79\x82\xee\x4c\x75\ +\xf7\x49\xdd\xfd\x84\x24\xd7\x48\xf2\xaa\xa9\xf3\xd9\xe2\x90\x24\ +\x8f\x4d\x72\x5c\x55\xfd\x52\x55\x2d\x5b\xcd\x08\x00\x0c\xa4\xaa\ +\x6e\x5e\x55\xef\x48\xf2\xd2\x24\x17\x1f\xf9\x72\x8f\x1d\x28\x8e\ +\x7e\x14\x76\x47\x3f\x0a\xc0\x76\xa9\xbf\x00\x80\x95\x67\xae\x07\ +\x00\x00\x8c\x6d\x49\xe7\xf3\xa3\x59\xb6\xe6\xe6\xb3\x03\xc6\xba\ +\xf9\x80\xb1\x80\xb3\xfa\xec\x80\xb1\x9c\x57\xd6\xd1\x90\x9b\xb8\ +\x9f\x1a\x30\x16\x23\xe8\xee\xcf\x76\xf7\x3d\x93\xdc\x3a\xc9\x47\ +\xa7\xce\x67\x8b\x4b\x26\x79\x71\x92\xf7\x57\xd5\x4d\xa7\x4e\x06\ +\x00\x58\x8c\xaa\xba\x50\x55\x3d\xa0\xaa\xde\x93\xe4\x3d\x49\x6e\ +\xbb\x80\xcb\xfe\x4d\x77\xbf\x7f\xa0\x58\x9f\x1d\x28\x4e\xa2\x1f\ +\x65\x3d\xe9\x47\x01\xd8\xae\xcf\x0e\x18\x4b\xfd\x05\x00\xcc\x9a\ +\xb9\x1e\x00\x00\x30\xa4\x15\x98\xcf\x8f\xe6\xe0\xa9\x13\xd8\xa6\ +\x21\x07\xa3\x57\xab\xaa\x4b\x76\xf7\x97\x07\x8c\x09\xfc\x90\xf3\ +\x0a\x3b\x54\x55\x97\x49\x72\xa5\x01\x43\xfa\x62\xd1\x92\xe8\xee\ +\xa3\xaa\xea\xfa\x49\x1e\x94\x8d\x57\x9f\x8f\xbd\x95\xbd\xbf\x6e\ +\x94\xe4\xbd\x55\xf5\xd2\x24\x8f\xee\xee\xff\x9c\x3a\x21\x80\xb1\ +\x54\xd5\x05\x92\x1c\x34\x75\x1e\xb0\x20\x17\x4a\x72\xbe\x24\x17\ +\x49\x72\xe5\x24\x87\x65\xe3\x0b\xee\x37\x48\x72\xae\x05\xe6\xf1\ +\x5f\x49\x1e\x35\x60\x3c\xfd\x28\xec\x90\x7e\x14\x80\x1d\x52\x7f\ +\x01\x00\x6b\xc7\x5c\x0f\x00\x60\xf7\xcc\xe7\x59\x33\xab\x3a\x9f\ +\x1f\xcd\xb2\x2d\xba\xfc\xeb\xc0\xf1\xfe\x47\x92\x17\x0e\x1c\x13\ +\xd8\xe0\xbc\xc2\xce\x0d\xbd\x91\x3b\xf4\x79\x64\x44\xdd\x7d\x46\ +\x92\x17\x54\xd5\x2b\x92\xfc\x6e\x92\x87\x65\xb1\x85\xec\xbe\xdc\ +\x37\xc9\xe1\x55\xf5\xc7\x49\x9e\xda\xdd\x27\x4f\x9d\x10\xc0\x08\ +\xde\x9d\xe4\xba\x53\x27\x01\x6b\xe6\x37\x07\xfe\x22\xa3\x7e\x14\ +\x76\x4e\x3f\x0a\xc0\x4e\xa8\xbf\x60\xc9\x75\xf7\x33\x92\x3c\x63\ +\xe2\x34\x00\x96\x8e\xb9\x1e\xc0\xe2\xa9\x5d\x61\xe5\x98\xcf\xc3\ +\xe2\x0d\x3d\x9f\x1f\xcd\x81\x53\x27\xb0\x4d\x43\xbf\xf2\xf3\xf0\ +\x81\xe3\x01\x3f\xe4\xbc\xc2\xce\xdd\x67\xc0\x58\xdf\x4b\x72\xec\ +\x80\xf1\x58\x90\xee\x3e\xbe\xbb\x7f\x2b\xc9\xb5\x93\xbc\x79\xea\ +\x7c\xb6\x38\x6f\x92\xdf\x4b\x72\x6c\x55\xdd\x73\xea\x64\x00\x80\ +\xa5\xf7\xca\xee\x7e\xc9\xc0\x31\xf5\xa3\xb0\x73\xfa\x51\x00\x76\ +\x42\xfd\x05\x00\xac\x35\x73\x3d\x00\x00\x60\x49\x8c\x31\x9f\x1f\ +\xcd\x52\x2d\xba\x74\xf7\x37\x92\xfc\xdb\x80\x21\xef\x54\x55\x97\ +\x1c\x30\x1e\xb0\xc9\x79\x85\x9d\xa9\xaa\x8b\x27\xb9\xe3\x80\x21\ +\x3f\xec\xe9\x3c\xcb\xad\xbb\x3f\xd5\xdd\x3f\x93\xe4\xa7\x93\x7c\ +\x72\xea\x7c\xb6\xb8\x42\x92\x57\x54\xd5\x51\x55\xf5\xdf\xa7\x4e\ +\x06\x00\x58\x4a\x9f\x49\xf2\xab\x43\x07\xd5\x8f\xc2\xce\xe8\x47\ +\x01\xd8\x29\xf5\x17\x00\xc0\x06\x73\x3d\x00\x00\x60\xc6\x46\x99\ +\xcf\x8f\x69\xa9\x16\x5d\x36\xbd\x6f\xc0\x58\x07\x25\xf9\xc5\x01\ +\xe3\x01\x67\xe6\xbc\xc2\xf6\xdd\x3b\xc9\xc1\x03\xc6\xfb\xa7\x01\ +\x63\x31\xa1\xee\x7e\x4b\x92\xeb\x24\xf9\x8d\x24\xdf\x9a\x36\x9b\ +\x33\xb9\x65\x92\x0f\x57\xd5\x0b\xaa\xea\xc7\xa7\x4e\x06\x00\x58\ +\x1a\x27\x25\x39\xbc\xbb\x8f\x1f\x29\xbe\x7e\x14\xb6\x4f\x3f\x0a\ +\xc0\x6e\xa8\xbf\x00\x00\x36\x99\xeb\x01\x00\x00\x33\x33\xf6\x7c\ +\x7e\x14\xcb\xb8\xe8\xf2\xf6\x81\xe3\x3d\xb4\xaa\x0e\x19\x38\x26\ +\xb0\xc1\x79\x85\x6d\xa8\xaa\x03\x93\x3c\x74\xe0\xb0\x47\x0d\x1c\ +\x8f\x09\x75\xf7\xf7\xba\xfb\x19\x49\xae\x9a\xe4\x79\x49\xce\x98\ +\x36\xa3\x1f\x38\x20\xc9\xaf\x24\xf9\xb7\xaa\xfa\x4d\x7f\x57\x03\ +\x00\xe7\xe0\x8c\x24\xf7\xe8\xee\x7f\x1d\xf1\x1a\xfa\x51\xd8\x06\ +\xfd\x28\x00\x03\x50\x7f\x01\x00\x6c\x61\xae\x07\x00\x00\xcc\xc4\ +\x22\xe6\xf3\xa3\x58\xc6\x45\x97\x7f\x18\x38\xde\x65\x93\xdc\x77\ +\xe0\x98\xc0\x06\xe7\x15\xb6\xe7\xae\xd9\xb8\xd1\x39\x94\x53\x33\ +\xfc\x80\x99\x19\xe8\xee\xaf\x75\xf7\x11\x49\xae\x97\xe4\x5d\x13\ +\xa7\xb3\xd5\x05\x93\x3c\x2d\xc9\xc7\xaa\xea\x67\xa6\x4e\x06\x00\ +\x98\xad\x5f\xdd\x7c\xaa\xe5\x98\xf4\xa3\xb0\x3d\xfa\x51\x00\x76\ +\x4b\xfd\x05\x00\x70\x36\xcc\xf5\x00\x00\x80\x89\x2d\x62\x3e\x3f\ +\x8a\xa5\x5b\x74\xe9\xee\x2f\x25\xf9\xf0\xc0\x61\x9f\xe8\x09\x05\ +\x30\x3c\xe7\x15\xf6\x5f\x55\x1d\x94\xe4\x0f\x07\x0e\xfb\xce\xee\ +\x3e\x71\xe0\x98\xcc\x48\x77\x1f\xd3\xdd\xb7\x4d\x72\xf7\x24\x9f\ +\x9d\x38\x9d\xad\xae\x9a\xe4\x8d\x55\xf5\x96\xaa\xba\xc6\xd4\xc9\ +\x00\x00\xb3\xf2\x90\xee\x7e\xd1\xd8\x17\xd1\x8f\xc2\xfe\xd3\x8f\ +\x02\x30\x04\xf5\x17\x00\xc0\xbe\x99\xeb\x01\x00\x00\x13\x58\xc8\ +\x7c\x7e\x2c\x4b\xb7\xe8\xb2\xe9\x55\x03\xc7\x3b\x34\xc9\xc3\x06\ +\x8e\x09\x6c\x70\x5e\x61\xff\x3c\x28\xc9\xd0\x37\x0e\x5f\x3f\x70\ +\x3c\x66\xaa\xbb\x5f\x9d\x8d\x3f\x3f\xbf\x9d\xe4\xa4\x89\xd3\xd9\ +\xea\x4e\x49\x8e\xa9\xaa\x67\x54\xd5\x45\xa6\x4e\x06\x00\x98\xdc\ +\x43\xba\xfb\xb9\x0b\xbc\x9e\x7e\x14\xf6\x8f\x7e\x14\x80\xa1\xa8\ +\xbf\x00\x00\xce\x81\xb9\x1e\x00\x00\xb0\x20\x8b\x9e\xcf\x0f\xce\ +\xa2\xcb\x0f\x3d\xb1\xaa\x2e\x3b\x42\x5c\x58\x77\xce\x2b\x9c\x83\ +\xaa\xba\x78\x92\x27\x0f\x1c\xf6\xb4\x8c\x73\xfe\x98\xa9\xee\x3e\ +\xa5\xbb\x9f\x9c\x8d\xa7\x2e\xfd\xf5\xd4\xf9\x6c\x71\x70\x92\x87\ +\x27\x39\xae\xaa\x1e\xbc\xf9\xb4\x68\x00\x60\xbd\x7c\x2f\xc9\x2f\ +\x4c\x70\x13\x4d\x3f\x0a\xe7\x40\x3f\x0a\xc0\xc0\xd4\x5f\x00\x00\ +\xfb\xc1\x5c\x0f\x00\x00\x18\xd1\x54\xf3\xf9\xc1\x2d\xe5\xa2\x4b\ +\x77\x7f\x26\xc9\x7b\x06\x0e\x7b\x81\x24\xcf\x1e\x38\x26\xac\x3d\ +\xe7\x15\xf6\xcb\x73\x92\x5c\x74\xe0\x98\x6f\xea\xee\xaf\x0d\x1c\ +\x93\x25\xd0\xdd\x5f\xec\xee\xfb\x27\xb9\x49\x92\x0f\x4c\x9d\xcf\ +\x16\x17\x4b\xf2\xdc\x24\x47\x57\xd5\x6d\x26\xce\x05\x00\x58\x9c\ +\x6f\x25\xb9\x53\x77\xff\xed\xa2\x2f\xac\x1f\x85\xfd\xa2\x1f\x05\ +\x60\x30\xea\x2f\x00\x80\xed\x31\xd7\x03\x00\x00\x06\xf6\xad\x4c\ +\x34\x9f\x1f\xc3\x52\x2e\xba\x6c\x7a\xd1\x08\x31\xef\x5a\x55\x0f\ +\x1a\x21\x2e\xac\x3b\xe7\x15\xf6\xa2\xaa\x1e\x90\xe4\xee\x23\x84\ +\xfe\xcb\x11\x62\xb2\x44\xba\xfb\x03\x49\x6e\x9a\xe4\xfe\x49\xbe\ +\x38\x71\x3a\x5b\x5d\x3b\xc9\x3b\xab\xea\xd5\x55\x75\xc5\xa9\x93\ +\x01\x00\x46\xf5\xb1\x24\x37\xec\xee\x77\x4c\x98\x83\x7e\x14\xf6\ +\x42\x3f\x0a\xc0\x48\xd4\x5f\x00\x00\xdb\x64\xae\x07\x00\x00\x0c\ +\x60\x0e\xf3\xf9\x41\x2d\xf3\xa2\xcb\x2b\x92\x7c\x63\x84\xb8\xcf\ +\xac\xaa\xc3\x46\x88\x0b\xeb\xcc\x79\x85\xb3\x51\x55\xd7\xc9\x38\ +\x4f\x23\xfc\x7c\x92\x37\x8f\x10\x97\x25\xd3\xdd\x7b\xba\xfb\xaf\ +\x93\x5c\x2d\xc9\x93\x93\x7c\x77\xe2\x94\xb6\xba\x5b\x92\x63\xab\ +\xea\xc9\x55\x75\xfe\xa9\x93\x01\x00\x06\xf7\xbc\x24\x3f\xb1\xf9\ +\x54\xef\x29\xe9\x47\xe1\x6c\xe8\x47\x01\x18\x91\xfa\x0b\x00\x60\ +\x07\xcc\xf5\x00\x00\x80\x5d\x98\xcb\x7c\x7e\x50\x4b\xbb\xe8\xd2\ +\xdd\x27\x27\x79\xce\x08\xa1\xcf\x9b\xe4\x0d\x55\x75\xd1\x11\x62\ +\xc3\x5a\x72\x5e\xe1\xac\xaa\xea\x12\x49\x5e\x97\xe4\x3c\x23\x84\ +\x7f\x46\x77\x9f\x36\x42\x5c\x96\x54\x77\x9f\xd8\xdd\xbf\x9d\xe4\ +\x1a\x49\x5e\x3d\x75\x3e\x5b\xfc\x58\x92\xc7\x25\x39\xae\xaa\xee\ +\x5f\x55\x07\x4c\x9d\x10\x00\xb0\x6b\x5f\x48\xf2\x33\xdd\x7d\xc4\ +\x66\x2f\x38\x29\xfd\x28\x9c\x95\x7e\x14\x80\x31\xa9\xbf\x00\x00\ +\x76\xc7\x5c\x0f\x00\x00\xd8\x86\x59\xcd\xe7\x87\xb6\xb4\x8b\x2e\ +\x9b\x9e\x9d\xe4\xa4\x11\xe2\x56\x92\xd7\x54\xd5\x79\x47\x88\x0d\ +\xeb\xca\x79\x85\x4d\x55\x75\xc1\x24\x6f\x4a\x72\xe8\x08\xe1\x8f\ +\x4f\xf2\x17\x23\xc4\x65\x05\x74\xf7\xbf\x77\xf7\xdd\x93\xdc\x36\ +\xc9\x31\x53\xe7\xb3\xc5\xa5\x92\xbc\x24\xc9\xfb\xab\xea\x26\x53\ +\x27\x03\x00\xec\xc8\xf7\x92\x3c\x35\xc9\xb5\xba\x7b\x6e\x6f\x73\ +\xd0\x8f\xc2\x26\xfd\x28\x00\x0b\xa2\xfe\x02\x00\xd8\x25\x73\x3d\ +\x00\x00\x60\x1f\xe6\x3c\x9f\x1f\xcc\x52\x2f\xba\x74\xf7\x57\xb3\ +\x71\xb3\x7c\x0c\xb7\x4a\xf2\xca\xaa\x3a\x64\xa4\xf8\xb0\x56\x9c\ +\x57\xd8\xb0\x39\x84\x7d\x6d\x92\x1b\x8c\x74\x89\x67\x75\xf7\x89\ +\x23\xc5\x66\x45\x74\xf7\xbb\x92\x5c\x3f\xc9\x83\x93\x7c\x7d\xda\ +\x6c\xce\xe4\xc6\x49\xde\x57\x55\x47\x56\xd5\x65\xa6\x4e\x06\x00\ +\xd8\x2f\x7b\x92\xbc\x2c\xc9\x35\xba\xfb\x51\xdd\x7d\xc2\xd4\x09\ +\xfd\x28\xfd\x28\x6c\xd0\x8f\x02\xb0\x28\xea\x2f\x00\x80\xe1\x98\ +\xeb\x01\x00\x00\x5b\xcc\x7e\x3e\x3f\xa4\xa5\x5e\x74\xd9\xf4\xd4\ +\x24\x63\xfd\x4b\xfa\x99\x24\xaf\xf5\x64\x28\x18\x8c\xf3\xca\x5a\ +\xab\xaa\x0b\x25\x79\x63\x36\x9e\xba\x33\x86\x6f\x64\xe3\x9c\xc1\ +\x39\xea\xee\xd3\xbb\xfb\xf9\x49\xae\x92\xe4\x19\x49\x4e\x9b\x36\ +\xa3\x33\xb9\x5f\x92\x4f\x55\xd5\x6f\x57\xd5\xb9\xa7\x4e\x06\x00\ +\x38\x5b\x27\x27\x79\x41\x92\xc3\xba\xfb\xbe\xdd\xfd\x99\xa9\x13\ +\x3a\x07\xfa\x51\xd6\x9a\x7e\x14\x80\x09\xa8\xbf\x00\x00\x06\x62\ +\xae\x07\x00\x00\x6b\x6f\xd9\xe6\xf3\x83\x58\xfa\x45\x97\xee\xfe\ +\x7a\x92\xdf\x1f\xf1\x12\x77\x4e\xf2\xd6\xaa\xba\xd8\x88\xd7\x80\ +\xb5\xe0\xbc\xb2\xce\xaa\xea\x92\x49\xde\x91\xf1\xbe\x54\x94\x24\ +\x7f\xb8\xea\x1b\xba\x0c\xaf\xbb\xbf\xd5\xdd\xbf\x91\xe4\xda\x49\ +\xfe\x7e\xea\x7c\xb6\x38\x5f\x92\x3f\x48\x72\x6c\x55\xdd\x7d\xea\ +\x64\x00\x80\x24\x1b\x4f\x87\xf9\x40\x92\x87\x24\xb9\x74\x77\xff\ +\x5a\x77\x7f\x72\xe2\x9c\xf6\x8b\x7e\x94\x75\xa6\x1f\x05\x60\x0a\ +\xea\x2f\x00\x80\xe1\x99\xeb\x01\x00\xc0\x5a\x59\xda\xf9\xfc\x50\ +\x96\x7e\xd1\x65\xd3\xb3\x92\x1c\x37\x62\xfc\x5b\x24\xf9\x50\x55\ +\x5d\x67\xc4\x6b\xc0\xba\x70\x5e\x59\x3b\x55\x75\xfd\x24\xef\xcf\ +\xc6\x2b\xa5\xc7\xf2\x99\x24\x7f\x3e\x62\x7c\x56\x5c\x77\x7f\xb2\ +\xbb\xef\x9c\x8d\x27\x62\x8e\xf9\xf7\xf4\x76\x1d\x9a\xe4\x55\x55\ +\xf5\xae\xaa\xba\xee\xd4\xc9\x00\xc0\x1a\xfa\x4a\x92\xbf\x4b\xf2\ +\x6b\x49\x2e\xdb\xdd\x37\xe9\xee\xe7\x76\xf7\xb7\xa6\x4d\x6b\x47\ +\xf4\xa3\xac\x1d\xfd\x28\x00\x13\x53\x7f\x01\x00\x8c\xc0\x5c\x0f\ +\x00\x00\x56\xd6\x2a\xcd\xe7\x77\xed\xe0\xa9\x13\x18\x42\x77\x9f\ +\x5a\x55\x0f\xce\xc6\x93\x09\xc7\x72\x85\x24\x1f\xac\xaa\x47\x26\ +\xf9\xf3\xee\xde\x33\xe2\xb5\x60\x65\x39\xaf\xac\x9b\xaa\xfa\xd5\ +\x24\xcf\x4c\x32\xf6\x6b\x9a\x8f\xe8\xee\x53\x47\xbe\x06\x6b\xa0\ +\xbb\xdf\x5c\x55\xff\x90\xe4\x61\x49\x7e\x37\xc9\x85\x26\x4e\xe9\ +\xfb\x6e\x9d\xe4\xc3\x55\xf5\x17\x49\x9e\xd0\xdd\x5f\x9b\x3a\x21\ +\x80\xfd\x70\x62\x92\xd3\xa7\x4e\x82\xa5\xb0\xc8\xdf\xb7\xdf\x4e\ +\x72\xc6\x96\xff\x7c\x4a\x92\xef\x24\xf9\xaf\x24\x5f\x4f\xf2\xb9\ +\x24\xff\x9e\xe4\x13\x49\x3e\xda\xdd\x9f\x5b\x60\x6e\xa3\xd2\x8f\ +\xb2\x6e\xf4\xa3\x00\x4c\x4d\xfd\x05\x00\x30\x2e\x73\x3d\x00\x80\ +\x33\x31\x9f\x67\x7f\x99\xcf\x2f\x89\x95\x58\x74\x49\x92\xee\x7e\ +\x67\x55\xbd\x30\xc9\x83\x46\xbc\xcc\x8f\x25\xf9\xb3\x24\x3f\x5b\ +\x55\x47\x74\xf7\x67\x46\xbc\x16\xac\x2c\xe7\x95\x75\x50\x55\x97\ +\x4a\xf2\xbc\x24\x3f\xbb\x80\xcb\xbd\xac\xbb\xff\x61\x01\xd7\x61\ +\x4d\x74\xf7\xf7\x92\xfc\x9f\xaa\xfa\x9b\x24\xbf\x9f\x8d\xbf\xaf\ +\xe7\xf0\x26\xc0\x03\xb3\xb1\xad\x7e\xef\xaa\x7a\x52\x36\xbe\xbc\ +\xf0\xbd\x49\x33\x02\xd8\xb7\x5b\x76\xf7\x47\xa6\x4e\x82\xf9\xab\ +\xaa\x23\x92\x3c\x67\x41\x97\x7b\x43\x92\xfb\x77\xf7\x5a\xde\xe4\ +\xd5\x8f\xb2\x0e\xf4\xa3\x00\xcc\x89\xfa\x0b\x00\x60\x5c\xe6\x7a\ +\x00\x00\x3f\x60\x3e\xcf\x7e\x31\x9f\x5f\x1e\x73\x68\x6c\x86\xf4\ +\x5b\xd9\xd8\x6a\x1a\xdb\x1d\x92\x7c\xac\xaa\x9e\x54\x55\xe7\x5f\ +\xc0\xf5\x60\x15\x39\xaf\xac\xa4\xaa\x3a\xb8\xaa\x1e\x9e\xe4\xd8\ +\x2c\xe6\x4b\x45\x5f\x4f\xf2\x1b\x0b\xb8\x0e\x6b\xa8\xbb\xbf\xda\ +\xdd\xbf\x96\xe4\x06\x49\x8e\x9a\x3a\x9f\x2d\x2e\x94\xe4\xe9\xd9\ +\xf8\xfb\xfd\xce\x53\x27\x03\x00\xbb\xd5\xdd\xcf\x4d\xf2\x90\x05\ +\x5d\xee\xbe\x49\x8e\xac\xaa\x83\x16\x74\xbd\x39\xd2\x8f\xb2\x92\ +\xf4\xa3\x00\xcc\x98\xfa\x0b\x00\x60\x64\xe6\x7a\x00\x00\xb0\x7f\ +\xcc\xe7\x97\xc7\x4a\x2d\xba\x74\xf7\x09\xd9\xf8\x03\xb1\x88\xad\ +\xa7\xf3\x24\x79\x62\x92\x4f\x57\xd5\x23\xdc\x30\x87\xed\x71\x5e\ +\x59\x35\x9b\x5f\x28\xba\x5f\x92\x4f\x26\x79\x46\x16\xf7\x7a\xbb\ +\x07\x74\xf7\x57\x16\x74\x2d\xd6\x54\x77\x7f\xa4\xbb\x6f\x9d\xe4\ +\x9e\xd9\x78\x5d\xe2\x5c\x5c\x2d\xc9\x9b\xab\xea\xcd\x9b\xff\x33\ +\x00\x2c\x2d\x37\xd3\x16\x47\x3f\xca\xaa\xd1\x8f\x02\x30\x77\xea\ +\x2f\x00\x80\xc5\x31\xd7\x03\x00\x80\x73\x66\x3e\xbf\x1c\x56\x6a\ +\xd1\x25\x49\xba\xfb\xfd\x49\x1e\xbb\xc0\x4b\x5e\x22\x1b\x4f\x1e\ +\xf8\x5c\x55\xfd\x49\x55\x5d\x75\x81\xd7\x86\xa5\xe6\xbc\xb2\x0a\ +\xaa\xea\x72\x55\xf5\xd8\x24\x9f\x49\x72\x64\x92\x2b\x2d\xf0\xf2\ +\xcf\xed\xee\x37\x2c\xf0\x7a\xac\xb9\xee\x7e\x55\x92\x6b\x24\x79\ +\x42\x92\xef\x4c\x9c\xce\x56\x77\x4e\xf2\xf2\xa9\x93\x00\x80\xdd\ +\x72\x33\x6d\x71\xf4\xa3\xac\x02\xfd\x28\x00\xcb\x64\xb3\xfe\x7a\ +\xdc\x02\x2f\xa9\xfe\x02\x00\xd6\x9a\xb9\x1e\x00\x00\xec\x9b\xf9\ +\xfc\xfc\x1d\x3c\x75\x02\x23\x79\x5a\x92\x1b\x65\xe3\xe9\x04\x8b\ +\x72\xd1\x24\x8f\x4e\xf2\xe8\xaa\xfa\x40\x92\xd7\x26\x79\x63\x92\ +\x8f\x77\xf7\x9e\x05\xe6\x01\xcb\xc6\x79\x65\xe9\x54\xd5\xd5\x93\ +\xdc\x3e\xc9\x3d\x92\xdc\x2a\xc9\x01\x13\xa4\xf1\xe1\x24\xbf\x35\ +\xc1\x75\x59\x73\xdd\x7d\x72\x92\x3f\xa8\xaa\x17\x27\xf9\x93\x24\ +\x3f\x3f\x71\x4a\x00\xb0\x52\xba\xfb\xb9\x55\x95\x24\xcf\x59\xc0\ +\xe5\xee\x9b\x24\x55\x75\xff\xee\x5e\xc4\xd3\xb5\xe7\x46\x3f\xca\ +\xd2\xd1\x8f\x02\xb0\xe4\x9e\x9a\xe4\x86\x51\x7f\x01\x00\x2c\x84\ +\xb9\x1e\x00\x00\xec\x9b\xf9\xfc\xbc\x1d\xb0\x67\xcf\x6a\xde\xc3\ +\xad\xaa\xf3\x25\x39\x2a\xc9\xf5\x27\x4e\xe5\xbf\x92\xbc\x37\xc9\ +\x31\x49\x8e\x4d\xd2\x49\xbe\x9a\xe4\xeb\x49\x4e\xe9\xee\x53\x26\ +\xcc\x0d\x66\xc1\x79\x65\x6e\xaa\xea\x02\x49\xce\xbb\xf9\x73\xe9\ +\x24\x57\xde\xfc\xb9\x66\x92\x5b\x24\xb9\xf8\x74\xd9\x25\x49\xbe\ +\x92\xe4\x46\xdd\xfd\x85\x29\x2e\x5e\x55\x1f\x49\x72\xdd\x81\xc2\ +\xdd\xb6\xbb\xdf\x35\x50\x2c\x26\x50\x55\x37\x49\xf2\xac\x6c\x7c\ +\x49\x74\x55\x7c\xae\xbb\x0f\x9d\x3a\x09\xc6\x53\x55\x83\x35\x41\ +\xdd\x3d\xc5\x97\x4b\x19\xd1\xc0\xbf\xe7\xae\xd7\xdd\x1f\x19\x28\ +\x16\x6b\xa6\xaa\x8e\xc8\x62\x6e\xa6\x25\xc9\x4b\x93\xac\xe5\xcd\ +\x34\xfd\x28\x73\xa3\x1f\xdd\x37\xfd\x28\xc0\xf2\x53\x7f\x01\xc0\ +\x62\xe9\xa3\xd8\xca\x5c\x8f\x65\x64\xae\x07\xb0\xda\xcc\xe7\x99\ +\x0b\xf3\xf9\x79\x5a\xd9\x45\x97\x24\xa9\xaa\x4b\x26\xf9\x40\x92\ +\xcb\x4f\x9d\x0b\xac\xa0\xe3\xbb\xfb\xc2\x43\x05\x73\x5e\x61\xbf\ +\x7d\x2f\xc9\x6d\xba\xfb\xbd\x53\x25\xe0\x86\x38\x3f\xaa\xaa\x0e\ +\x48\x72\xff\x24\x7f\x94\xe4\x52\x13\xa7\x33\x04\x37\xc4\x57\x9c\ +\x1b\xe2\xec\x8b\x1b\x69\xcc\x89\x9b\x69\x8b\xa1\x1f\x85\xfd\xb6\ +\x6a\xfd\x28\xb0\x7b\xff\xb3\xbb\x5f\x3b\x75\x12\x2c\x1f\xf5\x17\ +\x8c\x6a\x47\xf3\xbb\xaa\x7a\x44\x92\xa7\x0f\x9e\x0d\xb0\x53\xaf\ +\xeb\xee\xc3\x87\x08\x64\xae\xc7\x8f\x32\xd7\x63\xd9\x98\xeb\xf1\ +\xa3\xd4\xae\x30\x3b\xbb\xaa\x5d\xcd\xe7\x99\x13\xf3\xf9\xf9\x39\ +\x70\xea\x04\xc6\xd4\xdd\x5f\x4e\x72\xe7\x6c\x3c\x7d\x09\x98\x31\ +\xe7\x15\xf6\xcb\x19\x49\xee\x33\xe5\x97\x8a\xe0\xec\x74\xf7\x9e\ +\xee\x7e\x49\x92\xab\x25\xf9\xe3\x24\xdf\x9d\x38\x25\x00\x58\x09\ +\xdd\xfd\xdc\x24\x0f\x59\xd0\xe5\xee\x9b\xe4\xc8\xaa\x3a\x68\x41\ +\xd7\x9b\x0d\xfd\x28\xec\x17\xfd\x28\x00\x83\x51\x7f\x01\x00\x4c\ +\xc7\x5c\x0f\x00\x00\xce\x9e\xf9\xfc\xfc\xac\xf4\xa2\x4b\x92\x74\ +\xf7\x27\x92\xdc\x3e\xc9\x09\x53\xe7\x02\xec\x9b\xf3\x0a\xe7\xe8\ +\x41\xdd\xfd\xea\xa9\x93\x80\xbd\xe9\xee\x6f\x77\xf7\xe3\x92\x1c\ +\x96\xe4\x35\x53\xe7\x03\x00\xab\xc0\xcd\xb4\xc5\xd0\x8f\xc2\x39\ +\xd2\x8f\x02\x30\x28\xf5\x17\x00\xc0\xb4\xcc\xf5\x00\x00\xe0\xac\ +\xcc\xe7\xe7\x65\xe5\x17\x5d\x92\xa4\xbb\x8f\x49\x72\xc7\xb8\x59\ +\x0e\xb3\xe7\xbc\xc2\x5e\x3d\xa4\xbb\x5f\x3c\x75\x12\xb0\x3f\x7a\ +\xc3\xdd\xb2\xf1\x65\x85\x8f\x4d\x9d\x0f\x00\x2c\x3b\x37\xd3\x16\ +\x43\x3f\x0a\x7b\xa5\x1f\x05\x60\x14\xea\x2f\x00\x80\xe9\x99\xeb\ +\x01\x00\xc0\x99\x99\xcf\xcf\xc7\x5a\x2c\xba\x24\x49\x77\x7f\x20\ +\xc9\x2d\xe2\x35\xe8\x30\x7b\xce\x2b\x9c\xc9\x19\x49\x7e\x71\xb3\ +\x78\x82\xa5\xd2\xdd\xef\x48\x72\xfd\x6c\x14\xfe\xdf\x98\x38\x1d\ +\x00\x58\x6a\x6e\xa6\x2d\x86\x7e\x14\xce\x44\x3f\x0a\xc0\xe8\xd4\ +\x5f\x00\x00\xf3\x60\xae\x07\x00\x00\x3f\x64\x3e\x3f\x0f\x6b\xb3\ +\xe8\x92\x24\xdd\xfd\xb1\x24\xb7\x4c\xf2\xd9\x89\x53\x01\xce\x81\ +\xf3\x0a\x49\x92\x53\x92\xfc\x5c\x77\x1f\x39\x75\x22\xb0\x53\xdd\ +\x7d\xda\x66\xe1\x7f\x95\x24\xcf\x4a\x72\xfa\xc4\x29\x01\xc0\xd2\ +\x72\x33\x6d\x31\xf4\xa3\x90\x44\x3f\x0a\xc0\x02\xa9\xbf\x00\x00\ +\xe6\xc1\x5c\x0f\x00\x00\x7e\xc8\x7c\x7e\x7a\x6b\xb5\xe8\x92\x24\ +\xdd\xfd\xc9\x24\x37\x49\xf2\x2f\x53\xe7\x02\xec\x9b\xf3\xca\x9a\ +\xfb\x62\x92\x5b\x75\xf7\x6b\xa7\x4e\x04\x86\xd0\xdd\xdf\xec\xee\ +\x87\x27\xb9\x76\x92\xb7\x4d\x9d\x0f\x00\x2c\x2b\x37\xd3\x16\x43\ +\x3f\xca\x9a\xd3\x8f\x02\xb0\x70\xea\x2f\x00\x80\xf9\x30\xd7\x03\ +\x00\x80\x0d\xe6\xf3\xd3\x5a\xbb\x45\x97\x24\xe9\xee\xaf\x64\xe3\ +\xc9\x50\x7f\x33\x75\x2e\xc0\xbe\x39\xaf\xac\xa9\x0f\x24\xb9\x61\ +\x77\xff\xf3\xd4\x89\xc0\xd0\xba\xfb\xd8\xee\xfe\xa9\x24\x77\x49\ +\xf2\x6f\x53\xe7\x03\x00\xcb\xc8\xcd\xb4\xc5\xd0\x8f\xb2\xa6\xf4\ +\xa3\x00\x4c\x46\xfd\x05\x00\x30\x2f\xe6\x7a\x00\x00\x60\x3e\x3f\ +\xa5\xb5\x5c\x74\x49\x92\xee\x3e\xb9\xbb\xef\x97\xe4\x11\x49\xbe\ +\x37\x71\x3a\xc0\x3e\x38\xaf\xac\x91\x3d\x49\xfe\x34\x1b\x4f\xce\ +\xfd\xd2\xd4\xc9\xc0\x98\xba\xfb\x8d\x49\xae\x95\xe4\x51\x49\x4e\ +\x98\x38\x1d\x00\x58\x3a\x6e\xa6\x2d\x86\x7e\x94\x35\xa2\x1f\x05\ +\x60\x16\xd4\x5f\x00\x00\xf3\x63\xae\x07\x00\xc0\xba\x33\x9f\x9f\ +\xc6\xda\x2e\xba\x7c\x5f\x77\x3f\x33\x1b\xaf\x42\x3f\x6e\xea\x5c\ +\x80\x7d\x73\x5e\x59\x71\x5f\x4c\x72\x87\xee\x7e\x74\x77\x9f\x3a\ +\x75\x32\xb0\x08\xdd\x7d\x6a\x77\x3f\x35\xc9\x55\x92\xbc\x30\x1b\ +\x5f\xae\x03\x00\xf6\x93\x9b\x69\x8b\xa3\x1f\x65\xc5\xe9\x47\x01\ +\x98\x1d\xf5\x17\x00\xc0\xbc\x98\xeb\x01\x00\xb0\xee\xcc\xe7\x17\ +\x6f\xed\x17\x5d\x92\xa4\xbb\x3f\x9c\xe4\xfa\x49\x9e\x19\x8d\x18\ +\xcc\x9a\xf3\xca\x0a\xda\x93\xe4\xb9\x49\xae\xd9\xdd\x6f\x9f\x3a\ +\x19\x98\x42\x77\x7f\xb5\xbb\x7f\x25\xc9\x0d\x92\xbc\x7b\xea\x7c\ +\x00\x60\x99\xb8\x99\xb6\x38\xfa\x51\x56\x90\x7e\x14\x80\x59\x53\ +\x7f\x01\x00\xcc\x8f\xb9\x1e\x00\x00\xeb\xcc\x7c\x7e\xb1\x2c\xba\ +\x6c\xea\xee\x93\xba\xfb\x11\x49\x6e\x96\xe4\x98\x89\xd3\x01\xf6\ +\xc1\x79\x65\x85\x1c\x93\xe4\x66\xdd\xfd\x90\xee\xfe\xd6\xd4\xc9\ +\xc0\xd4\xba\xfb\xe8\x24\xb7\x4e\x72\xef\x24\x9f\x9f\x38\x1d\x00\ +\x58\x1a\x6e\xa6\x2d\x8e\x7e\x94\x15\xa2\x1f\x05\x60\x29\xa8\xbf\ +\x00\x00\xe6\xc9\x5c\x0f\x00\x80\x75\x65\x3e\xbf\x38\x16\x5d\x7e\ +\x44\x77\xbf\x3f\x1b\x4f\x87\x3a\x22\xc9\xd7\x27\x4e\x07\xd8\x07\ +\xe7\x95\x25\xf6\x85\x24\xbf\x94\xe4\x7a\x9b\x7f\x8e\x81\x4d\xdd\ +\xbd\xa7\xbb\x5f\x91\xe4\xea\x49\x9e\x98\xe4\xe4\x89\x53\x02\x80\ +\xa5\xe0\x66\xda\x62\xe9\x47\x59\x62\xfa\x51\x00\x96\x92\xfa\x0b\ +\x00\x60\x7e\xcc\xf5\x00\x00\x58\x57\xe6\xf3\x8b\x61\xd1\xe5\x6c\ +\x74\xf7\xe9\xdd\xfd\xbc\x24\x57\x4c\xf2\xdb\x49\x8e\x9f\x38\x25\ +\x60\x2f\x9c\x57\x96\xcc\x17\x93\xfc\x66\x92\xab\x76\xf7\x4b\xba\ +\xfb\x8c\xa9\x13\x82\xb9\xea\xee\x93\xbb\xfb\xf7\x92\x5c\x35\xc9\ +\xcb\xa6\xce\x07\x00\x96\x81\x9b\x69\x8b\xa5\x1f\x65\xc9\xe8\x47\ +\x01\x58\x7a\xea\x2f\x00\x80\x79\x32\xd7\x03\x00\x60\x1d\x99\xcf\ +\x8f\xcf\xa2\xcb\x3e\x74\xf7\x89\xdd\xfd\xe4\x24\x97\x4f\xf2\xc8\ +\x78\xd5\x26\xcc\x96\xf3\xca\xcc\x7d\x3c\xc9\x03\x92\x5c\xb1\xbb\ +\x9f\xde\xdd\xa7\x4c\x9d\x10\x2c\x8b\xee\xfe\x8f\xee\xbe\x6f\x92\ +\x9b\x27\xf9\xd0\xd4\xf9\x00\xc0\xdc\xb9\x99\xb6\x78\xfa\x51\x66\ +\x4e\x3f\x0a\xc0\xca\x51\x7f\x01\x00\xcc\x93\xb9\x1e\x00\x00\xeb\ +\xc6\x7c\x7e\x5c\x16\x5d\xf6\x43\x77\x9f\xd0\xdd\x4f\x4b\x72\xa5\ +\x24\x87\x27\x79\x43\x92\xd3\x27\x4d\x0a\x38\x5b\xce\x2b\x33\x72\ +\x62\x92\x17\x27\xb9\x55\x92\x6b\x77\xf7\x8b\xbb\xfb\xd4\x89\x73\ +\x82\xa5\xd5\xdd\xef\x4d\xf2\x13\xd9\xf8\x92\xde\x57\x26\x4e\x07\ +\x00\x66\xcd\xcd\xb4\x69\xe8\x47\x99\x11\xfd\x28\x00\x6b\x41\xfd\ +\x05\x00\x30\x4f\xe6\x7a\x00\x00\xac\x13\xf3\xf9\xf1\x1c\x3c\x75\ +\x02\xcb\xa4\xbb\x4f\x4b\xf2\xba\x24\xaf\xab\xaa\x1f\x4f\x72\xb7\ +\x24\x3f\x97\x8d\xa1\xf1\x21\x53\xe6\x06\x9c\x99\xf3\xca\x44\xbe\ +\x95\xe4\x2d\x49\x5e\x9f\xe4\x0d\xdd\x7d\xd2\xb4\xe9\xc0\x6a\xe9\ +\xee\x33\x92\xbc\xb8\xaa\xfe\x2e\xc9\xef\x24\x79\x44\xfc\x9d\x0e\ +\x00\x67\xab\xbb\x9f\x5b\x55\x49\xf2\x9c\x05\x5c\xee\xbe\x49\x52\ +\x55\xf7\xef\xee\xb5\xff\x62\xa1\x7e\x94\x89\x7c\x2b\xfa\x51\x00\ +\xd6\x94\xfa\x0b\x00\x60\x7e\xcc\xf5\x00\x00\x58\x27\xe6\xf3\xe3\ +\xb0\xe8\xb2\x43\xdd\xfd\xd5\x24\xcf\x4b\xf2\xbc\xaa\x3a\x6f\x92\ +\xdb\x24\xb9\x6d\x92\x9b\x26\xb9\x41\x92\x73\x4f\x97\x1d\xb0\x95\ +\xf3\xca\x88\x8e\x4f\xf2\xbe\x24\xef\x49\xf2\xee\x24\xef\xdd\x1c\ +\xaa\x02\x23\xea\xee\x6f\x27\x79\x4c\x55\xbd\x20\xc9\xd3\x92\xdc\ +\x75\xe2\x94\x00\x60\x96\xdc\x4c\x9b\x9e\x7e\x94\x11\xe9\x47\x01\ +\xe0\x6c\xa8\xbf\x00\x00\xe6\xc5\x5c\x0f\x00\x80\x75\x61\x3e\x3f\ +\x3c\x8b\x2e\x03\xe8\xee\xef\x24\x79\xf3\xe6\x4f\xaa\xea\x5c\x49\ +\xae\x9a\xe4\xb0\x24\xd7\x48\x72\xf9\xcd\x9f\x4b\x26\xb9\x68\x92\ +\x8b\x24\x39\xef\x24\xc9\xc2\x9a\x73\x5e\xd9\x86\x53\x93\x9c\x9c\ +\xe4\xc4\x24\x5f\xcc\xc6\x2b\x95\xbf\x98\xe4\xb8\x24\x9f\x4c\xf2\ +\xf1\x24\x9f\xdf\x7c\x12\x0d\x30\x81\xee\xfe\x4c\x92\xc3\xab\xea\ +\x27\x93\x3c\x3d\xc9\xb5\x26\x4e\x09\x00\x66\xc7\xcd\xb4\xf9\xd0\ +\x8f\xb2\x0d\xfa\x51\x00\x18\x80\xfa\x0b\x00\x60\x3e\xcc\xf5\x00\ +\x00\x58\x07\xe6\xf3\xc3\x3a\x60\xcf\x9e\x3d\x53\xe7\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x39\x70\xea\x04\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\xb1\xe8\x02\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\xc0\x4c\x58\x74\x01\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x60\x16\x2c\xba\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x30\x0b\x16\x5d\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x98\x05\x8b\x2e\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\xcc\x82\x45\x17\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x66\xc1\xa2\x0b\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\xb3\x60\xd1\x05\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x80\x59\xb0\xe8\x02\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\xc0\x2c\x58\x74\x01\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x60\x16\x2c\xba\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x30\x0b\x16\x5d\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x98\x05\x8b\x2e\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\xcc\x82\x45\x17\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x66\xc1\xa2\x0b\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\xb3\x60\xd1\x05\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x80\x59\xb0\xe8\x02\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\xc0\x2c\x58\x74\x01\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x60\x16\x2c\xba\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x30\x0b\x16\x5d\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x98\x05\x8b\x2e\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\xcc\x82\x45\x17\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x66\xc1\xa2\x0b\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\xb3\x60\xd1\x05\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x80\x59\xb0\xe8\x02\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\xc0\x2c\x58\x74\x01\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x60\x16\x2c\xba\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x30\x0b\x16\x5d\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x98\x05\x8b\x2e\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\xcc\x82\x45\x17\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x66\xc1\xa2\x0b\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\xb3\x60\xd1\x05\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x80\x59\xb0\xe8\x02\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\xc0\x2c\x58\x74\x01\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x60\x16\x2c\xba\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x30\x0b\x16\x5d\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x98\x05\x8b\x2e\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\xcc\x82\x45\x17\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x66\xc1\xa2\x0b\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\xb3\x60\xd1\x05\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x80\x59\xb0\xe8\x02\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\xc0\x2c\x58\x74\x01\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x60\x16\x2c\xba\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x30\x0b\x16\x5d\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x98\x05\x8b\x2e\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\xcc\x82\x45\x17\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x66\xc1\xa2\x0b\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\xb3\x60\xd1\x05\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x80\x59\xb0\xe8\x02\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\xc0\x2c\x58\x74\x01\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x60\x16\x2c\xba\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x30\x0b\x16\x5d\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x98\x05\x8b\x2e\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\xcc\xc2\xff\x07\x96\x3f\x31\xf9\xeb\x9b\xbc\xef\x00\ +\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ \x00\x00\x21\x26\ \x89\ \x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ @@ -3307,7 +4129,7 @@ \x77\x61\x72\x65\x00\x41\x64\x6f\x62\x65\x20\x49\x6d\x61\x67\x65\ \x52\x65\x61\x64\x79\x71\xc9\x65\x3c\x00\x00\x00\x00\x49\x45\x4e\ \x44\xae\x42\x60\x82\ -\x00\x00\x20\xc9\ +\x00\x00\x20\x0e\ \x89\ \x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ \x00\x05\xce\x00\x00\x00\x5a\x08\x06\x00\x00\x00\x81\xbf\xc5\xd3\ @@ -3481,7 +4303,7 @@ \x00\x7a\x26\x00\x00\x80\x84\x00\x00\xfa\x00\x00\x00\x80\xe8\x00\ \x00\x75\x30\x00\x00\xea\x60\x00\x00\x3a\x98\x00\x00\x17\x70\x9c\ \xba\x51\x3c\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x2e\x23\x00\ -\x00\x2e\x23\x01\x78\xa5\x3f\x76\x00\x00\x05\xad\x69\x54\x58\x74\ +\x00\x2e\x23\x01\x78\xa5\x3f\x76\x00\x00\x06\x7c\x69\x54\x58\x74\ \x58\x4d\x4c\x3a\x63\x6f\x6d\x2e\x61\x64\x6f\x62\x65\x2e\x78\x6d\ \x70\x00\x00\x00\x00\x00\x3c\x3f\x78\x70\x61\x63\x6b\x65\x74\x20\ \x62\x65\x67\x69\x6e\x3d\x22\xef\xbb\xbf\x22\x20\x69\x64\x3d\x22\ @@ -3522,10 +4344,10 @@ \x65\x3d\x22\x32\x30\x32\x35\x2d\x30\x32\x2d\x31\x36\x54\x31\x38\ \x3a\x31\x37\x3a\x32\x34\x2b\x30\x35\x3a\x33\x30\x22\x20\x78\x6d\ \x70\x3a\x4d\x6f\x64\x69\x66\x79\x44\x61\x74\x65\x3d\x22\x32\x30\ -\x32\x35\x2d\x30\x32\x2d\x31\x36\x54\x31\x38\x3a\x32\x30\x3a\x33\ +\x32\x35\x2d\x30\x32\x2d\x31\x36\x54\x31\x38\x3a\x32\x33\x3a\x31\ \x32\x2b\x30\x35\x3a\x33\x30\x22\x20\x78\x6d\x70\x3a\x4d\x65\x74\ \x61\x64\x61\x74\x61\x44\x61\x74\x65\x3d\x22\x32\x30\x32\x35\x2d\ -\x30\x32\x2d\x31\x36\x54\x31\x38\x3a\x32\x30\x3a\x33\x32\x2b\x30\ +\x30\x32\x2d\x31\x36\x54\x31\x38\x3a\x32\x33\x3a\x31\x32\x2b\x30\ \x35\x3a\x33\x30\x22\x20\x64\x63\x3a\x66\x6f\x72\x6d\x61\x74\x3d\ \x22\x69\x6d\x61\x67\x65\x2f\x70\x6e\x67\x22\x20\x70\x68\x6f\x74\ \x6f\x73\x68\x6f\x70\x3a\x43\x6f\x6c\x6f\x72\x4d\x6f\x64\x65\x3d\ @@ -3533,9 +4355,9 @@ \x43\x50\x72\x6f\x66\x69\x6c\x65\x3d\x22\x73\x52\x47\x42\x20\x49\ \x45\x43\x36\x31\x39\x36\x36\x2d\x32\x2e\x31\x22\x20\x78\x6d\x70\ \x4d\x4d\x3a\x49\x6e\x73\x74\x61\x6e\x63\x65\x49\x44\x3d\x22\x78\ -\x6d\x70\x2e\x69\x69\x64\x3a\x32\x34\x31\x61\x62\x39\x34\x37\x2d\ -\x65\x34\x34\x31\x2d\x37\x33\x34\x34\x2d\x62\x62\x31\x37\x2d\x37\ -\x66\x65\x62\x65\x35\x64\x66\x61\x34\x36\x38\x22\x20\x78\x6d\x70\ +\x6d\x70\x2e\x69\x69\x64\x3a\x64\x31\x35\x65\x37\x37\x62\x39\x2d\ +\x62\x31\x61\x38\x2d\x39\x38\x34\x65\x2d\x38\x62\x32\x36\x2d\x65\ +\x31\x62\x61\x62\x62\x39\x38\x64\x30\x32\x35\x22\x20\x78\x6d\x70\ \x4d\x4d\x3a\x44\x6f\x63\x75\x6d\x65\x6e\x74\x49\x44\x3d\x22\x78\ \x6d\x70\x2e\x64\x69\x64\x3a\x32\x34\x31\x61\x62\x39\x34\x37\x2d\ \x65\x34\x34\x31\x2d\x37\x33\x34\x34\x2d\x62\x62\x31\x37\x2d\x37\ @@ -3548,292 +4370,280 @@ \x3a\x54\x65\x78\x74\x4c\x61\x79\x65\x72\x73\x3e\x20\x3c\x72\x64\ \x66\x3a\x42\x61\x67\x3e\x20\x3c\x72\x64\x66\x3a\x6c\x69\x20\x70\ \x68\x6f\x74\x6f\x73\x68\x6f\x70\x3a\x4c\x61\x79\x65\x72\x4e\x61\ -\x6d\x65\x3d\x22\x43\x6f\x6e\x74\x72\x6f\x6c\x20\x63\x65\x6e\x65\ -\x72\x22\x20\x70\x68\x6f\x74\x6f\x73\x68\x6f\x70\x3a\x4c\x61\x79\ -\x65\x72\x54\x65\x78\x74\x3d\x22\x43\x6f\x6e\x74\x72\x6f\x6c\x20\ -\x63\x65\x6e\x65\x72\x22\x2f\x3e\x20\x3c\x2f\x72\x64\x66\x3a\x42\ -\x61\x67\x3e\x20\x3c\x2f\x70\x68\x6f\x74\x6f\x73\x68\x6f\x70\x3a\ -\x54\x65\x78\x74\x4c\x61\x79\x65\x72\x73\x3e\x20\x3c\x78\x6d\x70\ -\x4d\x4d\x3a\x48\x69\x73\x74\x6f\x72\x79\x3e\x20\x3c\x72\x64\x66\ -\x3a\x53\x65\x71\x3e\x20\x3c\x72\x64\x66\x3a\x6c\x69\x20\x73\x74\ -\x45\x76\x74\x3a\x61\x63\x74\x69\x6f\x6e\x3d\x22\x63\x72\x65\x61\ -\x74\x65\x64\x22\x20\x73\x74\x45\x76\x74\x3a\x69\x6e\x73\x74\x61\ -\x6e\x63\x65\x49\x44\x3d\x22\x78\x6d\x70\x2e\x69\x69\x64\x3a\x32\ -\x34\x31\x61\x62\x39\x34\x37\x2d\x65\x34\x34\x31\x2d\x37\x33\x34\ -\x34\x2d\x62\x62\x31\x37\x2d\x37\x66\x65\x62\x65\x35\x64\x66\x61\ -\x34\x36\x38\x22\x20\x73\x74\x45\x76\x74\x3a\x77\x68\x65\x6e\x3d\ -\x22\x32\x30\x32\x35\x2d\x30\x32\x2d\x31\x36\x54\x31\x38\x3a\x31\ -\x37\x3a\x32\x34\x2b\x30\x35\x3a\x33\x30\x22\x20\x73\x74\x45\x76\ -\x74\x3a\x73\x6f\x66\x74\x77\x61\x72\x65\x41\x67\x65\x6e\x74\x3d\ -\x22\x41\x64\x6f\x62\x65\x20\x50\x68\x6f\x74\x6f\x73\x68\x6f\x70\ -\x20\x32\x36\x2e\x32\x20\x28\x57\x69\x6e\x64\x6f\x77\x73\x29\x22\ -\x2f\x3e\x20\x3c\x2f\x72\x64\x66\x3a\x53\x65\x71\x3e\x20\x3c\x2f\ -\x78\x6d\x70\x4d\x4d\x3a\x48\x69\x73\x74\x6f\x72\x79\x3e\x20\x3c\ -\x2f\x72\x64\x66\x3a\x44\x65\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\ -\x3e\x20\x3c\x2f\x72\x64\x66\x3a\x52\x44\x46\x3e\x20\x3c\x2f\x78\ -\x3a\x78\x6d\x70\x6d\x65\x74\x61\x3e\x20\x3c\x3f\x78\x70\x61\x63\ -\x6b\x65\x74\x20\x65\x6e\x64\x3d\x22\x72\x22\x3f\x3e\x4a\xf9\x12\ -\xac\x00\x00\x10\x2f\x49\x44\x41\x54\x78\x9c\xed\xdd\xed\x51\x1c\ -\x47\xb4\x06\xe0\x57\xb7\x9c\x00\x37\x04\x14\x02\x0a\x01\x85\x80\ -\x43\x40\x21\xa0\x10\x44\x08\x22\x04\x13\x82\x08\xc1\x84\x60\x52\ -\x20\x04\xee\x8f\x66\x2e\x63\x59\x5a\x66\x76\xbe\xba\x67\x9f\xa7\ -\x8a\xc2\x55\x46\x30\xbb\xbd\x7b\x4e\x9f\xb3\xdd\x3d\x1f\x5e\x5e\ -\x5e\x02\x00\x00\x00\x00\x00\x14\xff\xb3\xf5\x05\x00\x00\x00\x00\ -\x00\x40\x4d\x34\xce\x01\x00\x00\x00\x00\xa0\x47\xe3\x1c\x00\x00\ -\x00\x00\x00\x7a\x34\xce\x01\x00\x00\x00\x00\xa0\x47\xe3\x1c\x00\ -\x00\x00\x00\x00\x7a\x34\xce\x01\x00\x00\x00\x00\xa0\x47\xe3\x1c\ -\x00\x00\x00\x00\x00\x7a\x34\xce\x01\x00\x00\x00\x00\xa0\x47\xe3\ -\x1c\x00\x00\x00\x00\x00\x7a\x34\xce\x01\x00\x00\x00\x00\xa0\x47\ -\xe3\x1c\x00\x00\x00\x00\x00\x7a\x34\xce\x01\x00\x00\x00\x00\xa0\ -\x47\xe3\x1c\x00\x00\x00\x00\x00\x7a\x34\xce\x01\x00\x00\x00\x00\ -\xa0\x47\xe3\x1c\x00\x00\x00\x00\x00\x7a\x34\xce\x01\x00\x00\x00\ -\x00\xa0\x47\xe3\x1c\x00\x00\x00\x00\x00\x7a\x34\xce\x01\x00\x00\ -\x00\x00\xa0\x47\xe3\x1c\x00\x00\x00\x00\x00\x7a\xfe\xd8\xfa\x02\ -\x8e\x70\xf9\xfa\xfd\x62\xd3\xab\xd8\xbf\xe7\x24\x77\x0b\xff\x0d\ -\x63\xf9\xe6\xf1\xf5\xfb\xc3\xa6\x57\x91\xdc\x0c\xf8\x99\x35\x5e\ -\x1b\x2d\x3b\x4f\x72\x35\xe0\xe7\x6e\x0f\xfc\xbf\xcb\x78\x5f\xb4\ -\xec\x77\xef\x11\xe3\xba\x9d\xc7\x24\x4f\xaf\x5f\x2d\x91\x27\xdf\ -\xd4\x90\x27\x87\xe4\xc8\xfb\xb4\xf7\x3a\x83\x29\xc4\xa9\x75\x1c\ -\x9a\x7f\x5f\x27\x39\x5b\xf1\x5a\x98\xd7\x63\xde\xcf\x6d\x6a\xb4\ -\xe9\xe6\xa8\xd1\xfa\xbc\xef\xda\x76\xe8\x7d\xa7\x66\xdb\x4e\x95\ -\x35\xdb\x87\x97\x97\x97\xad\xaf\xe1\x3d\x57\x29\x2f\x5a\x2f\xde\ -\x75\x3d\x24\xf9\x3c\xf3\xef\x34\x96\xc3\x74\x41\xfc\x3e\x6f\x8d\ -\x82\xb5\x0c\x0d\x08\x8f\x49\xbe\x66\xfb\x46\x7f\x8d\x2e\x93\xfc\ -\x18\xf0\x73\x1f\x0e\xfc\xbf\x9b\x24\xdf\xe6\xb9\x1c\x36\xf0\xbb\ -\xf8\x69\x5c\xb7\xf7\x94\x32\x3e\x5d\x8c\xad\x8d\x3c\x39\xcc\x56\ -\x79\x72\x48\x8e\xfc\x1c\xb9\x91\x7d\x13\xa7\xb6\x71\xa8\x36\xfb\ -\x91\xb7\x0f\x30\x68\xcf\xd7\xbc\xdf\xac\x55\xa3\x4d\x37\x47\x8d\ -\xd6\xe7\x7d\xd7\xb6\x43\xef\x3b\x35\xdb\xf6\xaa\xaa\xd9\x6a\x5d\ -\x71\x7e\x9e\xf2\x09\xde\xd5\xeb\x7f\xd3\x2e\x63\x39\xde\xc5\xeb\ -\xd7\x4d\x4a\xc0\xb8\x7b\xfd\x7a\xde\xf2\xa2\x7e\x72\x91\x32\x59\ -\xb8\x4b\x49\x38\x55\x7d\x22\x08\xf0\x1b\x5d\x4e\xba\x4e\x89\x5b\ -\xb7\xd9\x7e\x75\x96\x3c\x39\x5e\x0b\x79\x12\xf6\x44\x9c\x82\x36\ -\xa8\xd1\x80\x3d\xa8\xaa\x66\xab\xed\x8c\xf3\xb3\x24\xdf\x93\xfc\ -\x93\x52\x0c\x99\x98\xb5\xcb\x58\xce\xe3\x3c\xe5\xd3\xce\xee\x79\ -\xac\x6d\x3b\xd8\x75\x92\xbf\x53\xe7\xb5\x01\x1c\x72\x9e\xb7\x3c\ -\xb5\xc5\x8a\x21\x79\x72\x1e\xb5\xe7\x49\x68\x99\x38\x05\x6d\x52\ -\xa3\x01\x7b\xb1\x75\xcd\x56\x55\xe3\xfc\x26\xe5\x89\xb8\xde\xfa\ -\x42\x98\xcc\x58\xce\xef\x2c\xff\x6e\x0c\xd4\xa4\xbb\xb6\xbf\x63\ -\xcc\x81\xf6\x9c\xa7\xac\xce\x5a\x73\x4b\xa6\x3c\x39\xbf\x9a\xf3\ -\x24\xb4\x48\x9c\x82\xb6\xa9\xd1\x80\x3d\xd9\xa2\x66\x4b\x52\x47\ -\xe3\xfc\x2c\xc9\x5f\x29\x0f\xde\xa7\xa1\x6d\x33\x96\xcb\xeb\x26\ -\x40\x3f\x52\xdf\x73\xdc\x7d\x12\xf8\x23\xce\xbc\x04\xda\x73\x93\ -\x52\x5c\x2e\x19\x5b\xe5\xc9\xe5\xd5\x9c\x27\xa1\x05\xe2\x14\xec\ -\x8b\x1a\x0d\xd8\x93\x35\x6a\xb6\x7f\xd9\xba\x71\x7e\x91\xf2\x80\ -\x87\xdc\xdd\x98\xba\x19\xcb\x75\x5d\xa6\xac\x02\xaa\x71\xf2\x73\ -\x99\xf2\x5a\xf8\x1e\x05\x17\xd0\x96\xee\x6c\xd0\x25\x62\x97\x3c\ -\xb9\xae\x9a\xf3\x24\xd4\x4a\x9c\x82\xfd\x52\xa3\x01\x7b\xb1\x64\ -\xcd\xf6\x1f\x5b\x36\xce\xbb\x07\xea\xac\xbc\xf6\x19\xcb\x6d\x9c\ -\xa5\xee\xad\x77\xd7\xb1\x65\x1e\x68\xcf\x12\x13\x31\x79\x72\x1b\ -\xb5\xe7\x49\xa8\x89\x38\x05\xa7\x41\x8d\x06\xec\xc1\x6a\xcd\xf3\ -\xad\x1a\xe7\xab\x7e\x3a\xc0\xa2\x8c\xe5\xf6\xbe\xa7\xde\xa6\x40\ -\xff\xcc\x59\xab\x97\x80\x56\x5c\x64\xbe\xf3\xf3\xe4\xc9\xed\xd5\ -\x9c\x27\xa1\x06\xe2\x14\x9c\x16\x35\x1a\xb0\x07\x73\xd6\x6c\xbf\ -\xf5\xc7\xd2\x7f\xe0\x17\xe6\x9c\x98\x3d\xcc\xf0\x3b\xf8\xb5\xc7\ -\x01\x3f\x63\x2c\xe7\x71\x91\xe9\xcf\xe1\xf7\x24\xcf\x49\xee\xa7\ -\x5f\xce\x22\xce\x53\xce\xcb\x7c\x48\xf2\x25\xc9\xd3\xb6\x97\x53\ -\xbd\xe7\xec\xeb\x3d\x31\xe4\xee\xd7\x8f\x29\x8f\x7b\x0f\x7e\x17\ -\x3f\xf7\x36\xae\xad\x38\xf6\xee\xeb\xd7\x29\xe3\x35\x25\xae\xca\ -\x93\xf3\x38\x85\x3c\x09\x5b\x11\xa7\xda\x70\xa8\x36\x1b\x52\xb7\ -\x51\xaf\x2d\xe7\xbf\x6a\xb4\xe3\x79\xdf\xb5\xed\xd0\xfb\x4e\xcd\ -\xb6\x8d\x2d\x6b\xb6\x83\x3e\xbc\xbc\xbc\x2c\xf5\xbb\x7f\xa5\xdb\ -\x32\x7b\xcc\x16\xc0\xa7\x94\x27\xe2\x21\xfb\x6a\xb0\xb4\xca\x58\ -\xce\xef\xf2\xf5\xeb\x2a\xc7\x3d\xaf\xcf\x49\x3e\x67\x5a\x12\x5f\ -\x2b\x20\xdc\xbe\x7e\xed\x71\xec\x2f\x53\x0a\xd0\xf7\x7c\x58\xfa\ -\x42\x2a\x32\xe4\x75\xf5\x39\x26\x28\x2c\xe7\x22\x25\xb6\x5e\x67\ -\x5c\x73\xe8\x39\xc9\xc7\x1c\x17\xab\xe4\xc9\xf9\x6d\x9d\x27\xc5\ -\x32\xf6\x46\x9c\x82\x36\xa8\xd1\xa6\x53\xa3\x41\xfd\xb6\xa8\xd9\ -\xde\xb5\xf6\x51\x2d\x7f\x65\xfc\xc4\xec\x29\xe5\xd3\xcf\x8f\x49\ -\xbe\xa6\x4c\xce\xf6\x18\xc8\x5b\x63\x2c\xe7\xf7\x90\xf2\xbc\x7c\ -\xcc\x71\x85\xf7\x59\xca\x8a\xba\xa5\xdd\x66\xfa\x6a\x84\x9b\x38\ -\x5b\x0f\x58\xcf\x63\xde\xe2\xeb\xed\x88\x7f\x77\x96\xe3\x8f\xf8\ -\x90\x27\xe7\xd7\x4a\x9e\x84\x56\x88\x53\xb0\x1f\x6a\x34\xa0\x75\ -\x5b\xd4\x6c\xef\x5a\xb3\x71\x7e\x93\xf1\x4b\xef\xbb\x27\xec\x6e\ -\xfe\xcb\x61\x02\x63\xb9\xbc\x87\x94\xa6\xc0\xd7\x8c\x2b\x46\x2e\ -\xb2\xfc\x44\xe7\x21\xc9\xa7\x8c\xbf\xb6\x9f\x75\x67\xeb\xfd\x9d\ -\xe3\xb7\xe5\x00\x8c\xf1\x9c\x12\xbb\xbe\x8c\xf8\x37\x37\x19\x7f\ -\x84\x81\x3c\xb9\xbc\x9a\xf3\x24\xb4\x40\x9c\x82\x7d\x51\xa3\x01\ -\x7b\xb1\x56\xcd\x36\xc8\x5a\x8d\xf3\xf3\x8c\x2b\x52\x9e\x53\x82\ -\xfe\x98\x4f\x18\x58\x87\xb1\x5c\xd7\x6d\xc6\x6f\x2b\xff\x96\xe3\ -\xb6\xdc\x8e\xf1\x9c\x72\x6d\x9f\x32\xbd\x78\xea\xce\xd6\x3c\x66\ -\xd5\x13\xc0\x31\xee\x32\x7c\x22\x76\x96\x71\x37\xce\x92\x27\xd7\ -\x55\x6b\x9e\x84\x9a\x89\x53\xb0\x4f\x6a\x34\x60\x4f\x96\xac\xd9\ -\x06\x5b\xab\x71\x3e\xa6\xf3\xff\x98\xb2\x92\xc1\xcd\x16\xea\x64\ -\x2c\xd7\xf7\x98\xe3\x9a\x02\x6b\xe8\xb6\xeb\x4e\x3d\x5b\x3d\x29\ -\x41\xae\xdb\x1a\xb8\xc8\x27\x85\x00\x3d\x77\x29\x2b\x19\x86\x18\ -\xb3\xf5\x4f\x9e\x5c\x5f\xcd\x79\x12\x6a\x24\x4e\xc1\xbe\xa9\xd1\ -\x80\xbd\x58\xaa\x66\x1b\x6c\x8d\xc6\xf9\x79\x86\x5f\x7c\x77\xd3\ -\x26\xe7\xe4\xd5\xc9\x58\x6e\x67\xec\x0d\xcd\xae\x52\x56\x09\xac\ -\xa5\xdb\x1a\xf8\x25\xd3\xc7\xfc\x5b\xca\xe4\x6c\xb1\x33\xaa\x00\ -\x5e\x0d\x3d\x0f\xf4\x22\xc3\x8a\x45\x79\x72\x3b\xb5\xe7\x49\xa8\ -\x85\x38\x05\xa7\x43\x8d\x06\xec\xc1\xdc\x35\xdb\x28\x6b\x34\xce\ -\x87\x6e\x03\x34\x31\xab\x9f\xb1\xdc\xd6\x73\x92\x3f\x33\xfc\x79\ -\xdd\x62\x52\x73\x97\xb7\x9b\x45\x4d\xd1\xdd\xc0\xed\x47\x9c\xad\ -\x07\x2c\x6b\xe8\xd1\x03\x43\x9a\xac\xf2\xe4\xb6\x5a\xc8\x93\xb0\ -\x35\x71\x0a\x4e\x8f\x1a\x0d\x68\xdd\x9c\x35\xdb\x28\x4b\x37\xce\ -\xc7\x9c\x31\x73\x1b\x5b\x00\x6b\x66\x2c\xeb\xd0\x6d\xbb\x1b\xe2\ -\x3a\xdb\x6c\xa7\xeb\xce\xd6\xfb\x98\xe4\x7e\xe2\xef\xba\x4c\x99\ -\x98\x7d\x8f\xb3\xf5\x80\x65\x0c\x8d\x53\xef\x4d\xc2\xe4\xc9\x3a\ -\xb4\x90\x27\x61\x2b\xe2\x14\x9c\x2e\x35\x1a\xd0\xb2\xb9\x6a\xb6\ -\xd1\x96\x6e\x9c\x5f\x65\x58\x41\xf2\x10\x37\x9b\xa9\x9d\xb1\xac\ -\xc7\x7d\x86\x07\x8d\x45\x6e\x8e\x30\xd0\x53\xca\xca\xbf\xcf\x19\ -\xb6\xad\xe6\x90\xeb\x94\x3b\xbb\x3b\x5b\x0f\x98\xdb\x73\x4a\xee\ -\x7a\xcf\x7b\xb1\x47\x9e\xac\x47\x2b\x79\x12\xd6\x26\x4e\x01\x6a\ -\x34\xa0\x45\x73\xd5\x6c\xa3\x2d\xdd\x38\x1f\xda\xe9\x37\x31\xab\ -\x9f\xb1\xac\xcb\xd0\x6d\x76\x35\x6c\xa1\x7b\xc8\xdb\xd6\xc0\x29\ -\xdb\x7d\xcf\x52\xce\xd6\xfb\x3b\x1a\x1d\xc0\xbc\x86\xc4\xa6\xf7\ -\xf2\xa0\x3c\x59\x97\x96\xf2\x24\xac\x45\x9c\x02\x3a\x6a\x34\xa0\ -\x35\x73\xd4\x6c\xa3\xad\xb1\xe2\xfc\x3d\x4f\x19\xf6\xa9\x01\xdb\ -\x32\x96\x75\x19\xfa\x5c\xd7\xd4\x10\xe8\xb6\x06\x4e\x2d\xc6\xce\ -\x93\xfc\x95\xb2\x3d\xd0\x8d\xdd\x80\x39\xcc\x71\x1c\x81\x3c\x59\ -\x97\x16\xf3\x24\x2c\x4d\x9c\x02\x7e\xa6\x46\x03\x5a\xb1\xc9\x11\ -\x72\x4b\x36\xce\xcf\x33\x6c\x89\xfc\xdd\x82\xd7\xc0\x3c\x8c\x65\ -\x9d\x86\x6c\x43\x3f\x4b\x5d\x13\x97\xe7\x94\x55\x0d\x1f\x33\xbd\ -\x28\xbb\x4c\x59\xd9\xf0\x3d\xb6\x06\x02\xd3\x4c\x8d\x93\xf2\x64\ -\x9d\x5a\xcc\x93\xb0\x14\x71\x0a\xf8\x1d\x35\x1a\xd0\x82\x4d\xe6\ -\xec\x4b\x37\xce\x87\xb0\xa2\xa1\x7e\xc6\xb2\x4e\x9b\xdd\x1c\x61\ -\x06\x4f\x29\xe7\xea\xfd\x99\x79\xce\xd6\xfb\x27\xe5\x6c\x3d\x80\ -\x63\x0c\x29\xec\x0e\xc5\x2a\x79\xb2\x4e\x2d\xe7\x49\x98\x9b\x38\ -\xb5\x5f\x3f\x92\xbc\xf8\x6a\xf6\xab\xa6\x1a\x46\x8d\x36\x9c\xf7\ -\x5d\xdb\x5f\x7b\x7d\x5d\xee\xdd\xd4\x9a\xed\x28\x4b\x36\xce\x87\ -\x16\x21\xee\xd6\x5e\x3f\x63\x59\xa7\xe7\x0c\x0b\x0a\x35\x7f\xd2\ -\x7f\x9f\x79\xcf\xd6\xfb\x27\xb6\xdd\x03\xe3\x0d\x89\x1b\x87\xe2\ -\xad\x3c\x59\xa7\x3d\xe4\x49\x98\x8b\x38\x05\x0c\xa5\x46\x03\x6a\ -\x34\xb5\x66\x3b\xca\xd2\x67\x9c\xbf\xc7\x8a\x86\xfd\x30\x96\xdb\ -\x18\x12\x14\x86\xae\x30\xda\x52\x77\xb6\xde\xd4\xed\xc1\xe7\x29\ -\x9f\xfe\xff\x48\x1b\x8f\x1b\xd8\xde\xd0\x1b\x59\x4d\x29\x1c\x13\ -\x79\x72\x2b\x7b\xc9\x93\xb0\x06\x71\x0a\xe8\x53\xa3\x01\xb5\x58\ -\xab\x66\xfb\x8f\xad\x1b\xe7\xc0\x34\x7b\x6a\x08\x3c\x27\xf9\x92\ -\xe4\x53\xe6\x39\x5b\xef\x9f\x94\x15\x0e\x56\x12\x02\x87\x5c\x0f\ -\xfc\x39\x0d\xa5\x36\xed\x29\x4f\x02\xc0\xda\xd4\x68\x40\x0d\x36\ -\xab\xd9\x34\xce\xa1\x6d\xb3\x6f\x43\xa9\xc0\x63\xca\xd9\x7a\x5f\ -\x32\xfd\xf1\xdd\x64\xdf\x67\xeb\x01\xd3\xdc\x64\xf8\x96\xbf\x3d\ -\xc6\xdb\x53\x60\xdc\x00\x60\x3a\x35\x1a\xb0\x95\x4d\x6b\x36\x8d\ -\x73\xa0\x56\x77\x29\x2b\x1b\xe6\x3a\x5b\xef\xef\x38\x5b\x0f\x78\ -\x73\x91\xe1\x05\xdb\xd0\x9b\x4c\x02\x00\xec\x99\x1a\x0d\x58\xd3\ -\xe6\x35\x9b\xc6\x39\xb4\x6d\xef\x93\x8c\xe7\x94\xb3\xf5\x3e\x65\ -\xfa\xd9\x7a\x17\x29\xe7\xea\xfd\x15\xdb\xf2\xe1\xd4\x75\xf1\x60\ -\xe8\x36\xe1\xa9\xf1\x87\xed\xec\x3d\x4f\x02\xc0\xda\xd4\x68\xc0\ -\x1a\xaa\xa8\xd9\xb6\x6e\x9c\x0b\x8c\xfb\x61\x2c\xeb\xf5\xb8\xf5\ -\x05\xcc\xe0\x29\x65\x5b\xe0\xe7\x4c\x7f\x3c\x57\x79\xdb\x1a\xe8\ -\x6c\x3d\x38\x3d\xd7\x19\x3f\x01\x9b\x63\xcb\x9f\x3c\x59\xaf\x3d\ -\xe4\x49\x98\x83\x38\x05\x8c\xa1\x46\x03\x96\xb2\x55\xcd\xf6\x1f\ -\x4b\x36\xce\x87\x6c\xdb\x39\x8f\xa0\xd8\x02\x63\x59\xaf\x21\x2b\ -\xe9\x66\xbf\xab\xf0\x86\x1e\x52\x56\x36\x7c\xc9\xf4\xc7\xf5\x2d\ -\x65\x72\x36\xf4\x26\x13\x40\xdb\xae\x52\x26\x5f\xdf\x33\x2e\x5f\ -\xdd\x0e\xf8\x19\x79\xb2\x5e\xa7\x96\x27\xe1\x77\xc4\x29\x60\x29\ -\x6a\x34\x60\x2e\x4b\xd6\x6c\x47\xf9\x63\xa9\x5f\x9c\xe1\x9d\xfe\ -\xab\xd8\x02\x5d\x3b\x63\x59\xa7\x8b\x81\x3f\xb7\xc7\x1b\xa3\xdd\ -\xa5\x9c\x5f\x75\x9d\x32\xb9\x3a\xd6\x59\x4a\x40\xbe\x4a\x09\xb4\ -\xb3\xdf\x81\x19\x7a\x2e\x33\xfc\x7d\xcb\x3c\x2e\x52\xde\xe7\xc7\ -\x1e\xd7\xf1\x35\xc3\x62\xa8\x3c\x59\xa7\x53\xce\x93\xf0\x33\x71\ -\x6a\xbf\xee\x63\x0e\xdb\xb2\x3d\xed\x7a\x3a\xa5\x1a\xcd\xfb\xae\ -\x6d\x87\xde\x77\x6a\xb6\xf5\xad\x55\xb3\x1d\x65\xc9\xc6\xf9\xd0\ -\x20\x72\x19\x93\xb3\xda\x19\xcb\x3a\x5d\x0d\xfc\xb9\x3d\x4d\xc6\ -\xfa\xba\xb3\xf5\xee\x53\x26\x56\x53\xce\xb1\xbd\xcc\xdb\xeb\xf7\ -\x36\x9a\x28\x2c\xe3\x22\xd3\x8a\x08\xd6\xf5\x98\xe1\x2b\x17\xe4\ -\xc9\x3a\x9d\x7a\x9e\x84\x3e\x71\x6a\xbf\x8c\x17\x35\x39\x95\x1a\ -\xcd\xfb\x6e\xbf\xd4\x6c\x6d\x19\x53\xb3\x1d\x65\xe9\x33\xce\x87\ -\x14\x22\x57\x71\x9e\x5e\x0b\x8c\x65\x7d\x86\x6c\x5f\x7b\x4a\x5d\ -\x13\x8c\x25\x3c\xa5\x9c\xab\xf7\x39\xd3\x1f\xeb\x75\xca\x9d\xdd\ -\x87\xde\xb5\x19\xd8\xa7\xe7\x94\x98\x32\x86\x3c\x59\x1f\x79\x12\ -\xfe\x4d\x9c\x02\xd6\xa2\x46\x03\x96\x76\x4c\xcd\x36\xda\xd2\x8d\ -\xf3\xfb\x81\x3f\x27\x00\xd6\xcf\x58\xd6\xe5\x3a\xc3\xce\x7b\x3a\ -\xa5\xed\x63\x0f\x49\x3e\xa6\x6c\xd3\x99\x72\xb6\xde\x59\xde\xce\ -\xd6\x1b\xba\x5a\x11\xd8\x8f\x6e\x02\x36\x36\x8e\xc8\x93\x75\x91\ -\x27\xe1\xbf\xc4\x29\x60\x6d\x6a\x34\x60\x09\xc7\xd6\x6c\xa3\x2d\ -\xdd\x38\x1f\x5a\x8c\x5c\xc7\xca\x86\xda\x19\xcb\x7a\x74\x93\x86\ -\x21\x4e\xb1\x21\x70\x9b\x32\x39\x9b\xba\x5d\xe7\x3c\xc9\x5f\x29\ -\x37\xa6\x70\xc6\x19\x9c\x86\x6e\x02\x76\xcc\xd1\x1d\xf2\x64\x3d\ -\xe4\x49\xf8\x35\x71\x0a\xd8\x8a\x1a\x0d\x98\xcb\x94\x9a\x6d\xb4\ -\x35\x8e\x6a\x19\xfa\x40\xbe\x2f\x79\x21\x4c\x66\x2c\xeb\x31\xf4\ -\xee\xc2\x4f\x19\xbe\xb2\x68\x6f\x9e\x53\x56\x35\x7c\xca\xf4\xa6\ -\xc8\x65\xca\xd6\xc0\xb1\x77\x75\x06\xda\xf2\x98\x52\xd0\x1d\x3b\ -\x01\x93\x27\xeb\x21\x4f\xc2\xaf\x89\x53\xc0\x96\xd4\x68\xc0\x54\ -\x53\x6b\xb6\xd1\x96\x6e\x9c\x27\xc3\x6f\x9a\x70\x19\x07\xf0\xd7\ -\xce\x58\x6e\xef\x26\xc3\xb7\xa6\xb9\x61\x49\x09\xa6\x9f\x93\xfc\ -\x99\x79\xce\xd6\xfb\x27\xb6\x2f\xc3\x1e\x75\x45\xdc\xd4\xad\x7e\ -\xf2\xe4\xf6\xe4\x49\x38\x4c\x9c\x02\xb6\xa6\x46\x03\x8e\x31\x57\ -\xcd\x36\xca\x5a\x8d\xf3\xa1\xc1\xf0\x26\xc3\x6e\xe4\xc4\x36\x8c\ -\xe5\xb6\xae\x33\xbc\x80\x79\x8e\x86\x40\xdf\x7d\xe6\x3f\x5b\x6f\ -\xca\x1d\xe2\x81\x3a\xdc\x65\x9e\x6d\xc3\xfd\xdf\x27\x4f\x6e\x47\ -\x9e\x84\xf7\x89\x53\x40\x2d\xd4\x68\xc0\x10\x73\xd7\x6c\xa3\xac\ -\xd1\x38\x4f\xc6\x3d\xb8\xef\xf1\x69\x61\xcd\x8c\xe5\x36\xae\x33\ -\x6e\xcb\xec\x6d\x56\xfe\x14\xae\x11\xdd\xd9\x7a\x53\x9b\x25\xe7\ -\x29\xe7\xea\xfd\x88\x33\x40\xa1\x35\x4f\x79\x8b\x05\x5f\x32\x7d\ -\xa5\xd3\xcf\xe4\xc9\x6d\xc8\x93\x30\x9c\x38\x05\xd4\x44\x8d\x06\ -\xfc\x6c\xe9\x9a\x6d\xb0\x0f\x2f\x2f\x2f\x6b\xfd\xad\x1f\x19\xf7\ -\xe9\xdf\x7d\xca\x93\xa3\xa8\xa9\x8f\xb1\x5c\x4f\xf7\xe9\xf9\x98\ -\xd5\x3e\x4f\x29\xc1\xe5\x18\x43\x02\xc2\xe7\xec\xe3\x66\x6a\x97\ -\x29\x85\xe0\x1c\xab\x12\x6e\xf3\xd6\x84\xb9\x4c\x79\x8f\xbc\xe7\ -\xc3\x0c\x7f\xb7\x15\xa7\xf4\xba\x3a\xe4\x26\xef\xaf\x86\x7d\xce\ -\x8a\xe7\xb5\x35\xe2\x98\xf7\xe8\x63\xde\x72\xce\x53\xef\xeb\x31\ -\xeb\x4c\xba\xe4\xc9\xf5\xac\x99\x27\xc5\x32\xf6\x44\x9c\x82\x76\ -\x9c\x52\xfe\x51\xa3\xb1\x35\x35\xdb\x71\x5a\xac\xd9\x06\x59\xb3\ -\x71\x7e\x9e\x72\xf3\x86\x31\x37\x6e\xe8\x6e\x1e\x61\x2b\x6d\x5d\ -\x8c\xe5\x3a\x2e\x53\x56\xf9\x8c\xfd\xb4\x7c\xca\xa4\xe9\x94\x26\ -\x65\x9d\xeb\x94\xe4\x38\x75\x55\x42\xf7\x1a\x7f\x8a\x49\xd9\xcf\ -\x4e\xf1\x75\xf5\x2b\x43\x26\x61\x0f\x29\xcf\x05\x6f\xc6\xae\x24\ -\x4e\xde\xce\xce\xdc\xaa\xc1\x23\x4f\xae\x63\xed\x3c\x29\x96\xb1\ -\x27\xe2\x14\xb4\xe3\x14\xf3\x8f\x1a\x8d\xad\xa8\xd9\x8e\xd3\x62\ -\xcd\x36\xc8\x5a\x47\xb5\x24\x25\x50\x7d\x1d\xf9\x6f\xce\x52\x9e\ -\xf8\xee\x66\x0f\xb6\xdb\xd4\xc1\x58\x2e\xe7\x2c\x25\xe0\x1c\xbb\ -\xc5\xec\x36\xfb\x9a\x30\xad\xe1\x2e\xe5\x06\x13\x73\x9c\xad\xf7\ -\x3d\xc9\x5f\x73\x5c\x14\xf0\xff\xee\x52\x56\x39\x8e\x71\x91\x12\ -\x43\xc7\x34\x84\xe6\x24\x4f\x2e\x47\x9e\x84\x79\x88\x53\x40\xcd\ -\xd4\x68\xd0\x96\x16\x6b\xb6\x41\xd6\x5c\x71\xde\xf9\x96\x69\xe7\ -\xe4\x3d\xa6\x14\x3c\xb6\x46\x2c\x6b\xc8\xf3\x6b\x2c\xe7\x71\xf1\ -\xfa\xfd\x32\xd3\xb6\xa4\xcd\xf1\xa9\xe7\x29\xae\x66\xe8\x3b\x4f\ -\x79\x5d\x5f\xad\xf0\xb7\x4e\x69\x35\xc3\xa9\xbf\xae\x3a\x56\x2f\ -\x4c\xd3\xe2\x2a\x06\x79\x72\x1e\xb5\xe4\xc9\x21\xb1\xec\x6b\x4e\ -\x7b\xac\x6a\xd6\xdf\x0e\xcc\x1b\x71\xaa\x0d\x87\x9e\xdf\x8b\x54\ -\xde\x74\xe0\xa0\xee\x68\x82\x43\x4e\x7d\x2e\x5d\x63\x8d\xe6\x7d\ -\xd7\xb6\x43\xef\x3b\x35\xdb\x34\x2d\xd6\x6c\x07\xfd\xb1\xc1\xdf\ -\xfc\x9a\xb7\xd5\x42\xc7\xb8\xc8\x5b\x01\xc5\x72\x86\x04\x02\x63\ -\x59\x8f\xc7\x24\x7f\x6e\x7d\x11\x3b\xf0\x94\xf2\x3c\x5e\xa6\x24\ -\x4b\xaf\x4f\xa8\x47\x77\x34\xc0\x98\x89\x58\xb7\x8a\x61\xab\x89\ -\x98\x3c\x59\x8f\xb5\xf2\xe4\x7b\x85\x16\xdb\xd9\x73\x53\x69\x0a\ -\x71\xaa\x0d\x87\x6a\xb3\x6f\x99\xe7\x3c\x68\xb6\xf1\x35\xe3\x6e\ -\xd8\x7b\x8a\x6a\xac\xd1\xbc\xef\xda\xe6\x7d\xb7\x9c\x16\x6b\xb6\ -\x83\xd6\x3c\xaa\xa5\xef\x4b\x9c\x8d\xb7\x17\xc6\x72\x7b\x55\x7f\ -\x3a\xd7\xa8\x87\x94\xad\x81\x6e\x82\x05\x75\x69\x71\x0b\xa0\x3c\ -\xb9\x3d\x79\x12\x0e\x13\xa7\x80\x16\xa8\xd1\xa0\x0d\x2d\xd6\x6c\ -\xbf\xb5\x55\xe3\x3c\x31\x41\xdb\x13\x63\xb9\x1d\xcd\x80\x65\xdd\ -\x25\xf9\x98\xf1\x67\x80\x02\xcb\x69\x71\x22\x26\x4f\x6e\x47\x9e\ -\x84\x61\xc4\x29\xa0\x15\x6a\x34\xa8\x5f\x8b\x35\xdb\x2f\x6d\xd9\ -\x38\x4f\xca\x93\x38\xf6\x89\xa4\x4e\xc6\x72\x7d\xb7\x29\x9f\xb8\ -\x6b\x06\x2c\xeb\x39\xe5\xb9\xfe\x18\x5b\xbc\xa1\x16\x2d\x4e\xc4\ -\xe4\xc9\xf5\xc9\x93\x30\x8e\x38\x05\xb4\x42\x8d\x06\xf5\x6b\xb1\ -\x66\xfb\x8f\xad\x1b\xe7\xc9\xdb\xdd\x92\xdf\xbb\x21\x06\xf5\x33\ -\x96\xeb\x78\x4e\x39\xe3\xcd\x27\xec\xeb\x7a\x4a\x59\xb5\xf8\x39\ -\x5e\xe3\x50\x83\x16\x27\x62\xf2\xe4\x3a\xe4\x49\x38\x9e\x38\x05\ -\xb4\x44\x8d\x06\x75\x6b\xb1\x66\xfb\x97\x1a\x1a\xe7\x49\xd9\x46\ -\xfb\x29\x0a\x9c\x3d\x30\x96\xcb\xea\x3e\x55\xbf\xdf\xfa\x42\x4e\ -\xd8\x43\xde\xb6\x06\x5a\xc5\x08\xdb\x6a\x71\x22\x26\x4f\x2e\x4b\ -\x9e\x84\xe9\xc4\x29\xa0\x35\x6a\x34\xa8\x57\x8b\x35\xdb\xff\xab\ -\xa5\x71\x9e\xfc\x7b\xab\x8d\xf3\xf5\xda\x66\x2c\xe7\xd7\x3f\xc7\ -\xcd\x44\xa0\x0e\xdd\x6b\xdc\xdd\xb8\x61\x5b\x2d\x4e\xc4\xe4\xc9\ -\xf9\xc9\x93\x30\x2f\x71\x0a\x68\x91\x1a\x0d\xea\xd4\x62\xcd\x96\ -\xa4\xae\xc6\x79\xe7\x29\xe5\xc9\xfc\xdf\xd7\xef\x8f\xdb\x5e\x0e\ -\x13\x18\xcb\x69\x1e\x53\x1a\x00\xdd\xf3\x67\xeb\x59\x7d\x9e\x53\ -\xc6\xe8\x53\x9c\xad\x07\x5b\x6a\x75\x22\x26\x4f\x4e\x23\x4f\xc2\ -\xf2\xc4\x29\xa0\x35\x6a\x34\xa8\x53\x93\x35\xdb\x1f\x5b\xfd\xe1\ -\x01\x9e\x53\x9e\xd4\xbb\x94\x27\xe8\x32\xc9\xf9\xeb\xf7\xf4\xbe\ -\x53\x3f\x63\xf9\xbe\x87\xde\xf7\xa7\xd7\xef\x56\xcc\xb5\xe3\x31\ -\xe5\x5c\xbd\xab\x24\xdf\x52\x5e\xdf\xc0\xba\xba\x15\x91\xdf\x47\ -\xfc\x9b\x6e\x22\xf6\x39\xdb\xc6\x5c\x79\xf2\x7d\xf2\x24\x6c\x4b\ -\x9c\x02\x5a\xa3\x46\x83\xfa\x34\x57\xb3\x7d\x78\x79\x79\x59\xfb\ -\x6f\x02\x00\x00\x00\x00\x40\xb5\x6a\x3c\xaa\x05\x00\x00\x00\x00\ -\x00\x36\xa3\x71\x0e\x00\x00\x00\x00\x00\x3d\x1a\xe7\x00\x00\x00\ -\x00\x00\xd0\xa3\x71\x0e\x00\x00\x00\x00\x00\x3d\x1a\xe7\x00\x00\ -\x00\x00\x00\xd0\xa3\x71\x0e\x00\x00\x00\x00\x00\x3d\x1a\xe7\x00\ -\x00\x00\x00\x00\xd0\xa3\x71\x0e\x00\x00\x00\x00\x00\x3d\x1a\xe7\ -\x00\x00\x00\x00\x00\xd0\xa3\x71\x0e\x00\x00\x00\x00\x00\x3d\x1a\ -\xe7\x00\x00\x00\x00\x00\xd0\xa3\x71\x0e\x00\x00\x00\x00\x00\x3d\ -\x1a\xe7\x00\x00\x00\x00\x00\xd0\xa3\x71\x0e\x00\x00\x00\x00\x00\ -\x3d\x1a\xe7\x00\x00\x00\x00\x00\xd0\xa3\x71\x0e\x00\x00\x00\x00\ -\x00\x3d\xff\x07\x43\xf6\x9c\xcd\x51\x7c\xd9\x39\x00\x00\x00\x00\ -\x49\x45\x4e\x44\xae\x42\x60\x82\ +\x6d\x65\x3d\x22\x43\x6f\x6e\x74\x72\x6f\x6c\x20\x63\x65\x6e\x74\ +\x65\x72\x22\x20\x70\x68\x6f\x74\x6f\x73\x68\x6f\x70\x3a\x4c\x61\ +\x79\x65\x72\x54\x65\x78\x74\x3d\x22\x43\x6f\x6e\x74\x72\x6f\x6c\ +\x20\x63\x65\x6e\x74\x65\x72\x22\x2f\x3e\x20\x3c\x2f\x72\x64\x66\ +\x3a\x42\x61\x67\x3e\x20\x3c\x2f\x70\x68\x6f\x74\x6f\x73\x68\x6f\ +\x70\x3a\x54\x65\x78\x74\x4c\x61\x79\x65\x72\x73\x3e\x20\x3c\x78\ +\x6d\x70\x4d\x4d\x3a\x48\x69\x73\x74\x6f\x72\x79\x3e\x20\x3c\x72\ +\x64\x66\x3a\x53\x65\x71\x3e\x20\x3c\x72\x64\x66\x3a\x6c\x69\x20\ +\x73\x74\x45\x76\x74\x3a\x61\x63\x74\x69\x6f\x6e\x3d\x22\x63\x72\ +\x65\x61\x74\x65\x64\x22\x20\x73\x74\x45\x76\x74\x3a\x69\x6e\x73\ +\x74\x61\x6e\x63\x65\x49\x44\x3d\x22\x78\x6d\x70\x2e\x69\x69\x64\ +\x3a\x32\x34\x31\x61\x62\x39\x34\x37\x2d\x65\x34\x34\x31\x2d\x37\ +\x33\x34\x34\x2d\x62\x62\x31\x37\x2d\x37\x66\x65\x62\x65\x35\x64\ +\x66\x61\x34\x36\x38\x22\x20\x73\x74\x45\x76\x74\x3a\x77\x68\x65\ +\x6e\x3d\x22\x32\x30\x32\x35\x2d\x30\x32\x2d\x31\x36\x54\x31\x38\ +\x3a\x31\x37\x3a\x32\x34\x2b\x30\x35\x3a\x33\x30\x22\x20\x73\x74\ +\x45\x76\x74\x3a\x73\x6f\x66\x74\x77\x61\x72\x65\x41\x67\x65\x6e\ +\x74\x3d\x22\x41\x64\x6f\x62\x65\x20\x50\x68\x6f\x74\x6f\x73\x68\ +\x6f\x70\x20\x32\x36\x2e\x32\x20\x28\x57\x69\x6e\x64\x6f\x77\x73\ +\x29\x22\x2f\x3e\x20\x3c\x72\x64\x66\x3a\x6c\x69\x20\x73\x74\x45\ +\x76\x74\x3a\x61\x63\x74\x69\x6f\x6e\x3d\x22\x73\x61\x76\x65\x64\ +\x22\x20\x73\x74\x45\x76\x74\x3a\x69\x6e\x73\x74\x61\x6e\x63\x65\ +\x49\x44\x3d\x22\x78\x6d\x70\x2e\x69\x69\x64\x3a\x64\x31\x35\x65\ +\x37\x37\x62\x39\x2d\x62\x31\x61\x38\x2d\x39\x38\x34\x65\x2d\x38\ +\x62\x32\x36\x2d\x65\x31\x62\x61\x62\x62\x39\x38\x64\x30\x32\x35\ +\x22\x20\x73\x74\x45\x76\x74\x3a\x77\x68\x65\x6e\x3d\x22\x32\x30\ +\x32\x35\x2d\x30\x32\x2d\x31\x36\x54\x31\x38\x3a\x32\x33\x3a\x31\ +\x32\x2b\x30\x35\x3a\x33\x30\x22\x20\x73\x74\x45\x76\x74\x3a\x73\ +\x6f\x66\x74\x77\x61\x72\x65\x41\x67\x65\x6e\x74\x3d\x22\x41\x64\ +\x6f\x62\x65\x20\x50\x68\x6f\x74\x6f\x73\x68\x6f\x70\x20\x32\x36\ +\x2e\x32\x20\x28\x57\x69\x6e\x64\x6f\x77\x73\x29\x22\x20\x73\x74\ +\x45\x76\x74\x3a\x63\x68\x61\x6e\x67\x65\x64\x3d\x22\x2f\x22\x2f\ +\x3e\x20\x3c\x2f\x72\x64\x66\x3a\x53\x65\x71\x3e\x20\x3c\x2f\x78\ +\x6d\x70\x4d\x4d\x3a\x48\x69\x73\x74\x6f\x72\x79\x3e\x20\x3c\x2f\ +\x72\x64\x66\x3a\x44\x65\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\x3e\ +\x20\x3c\x2f\x72\x64\x66\x3a\x52\x44\x46\x3e\x20\x3c\x2f\x78\x3a\ +\x78\x6d\x70\x6d\x65\x74\x61\x3e\x20\x3c\x3f\x78\x70\x61\x63\x6b\ +\x65\x74\x20\x65\x6e\x64\x3d\x22\x72\x22\x3f\x3e\x6d\x31\xc1\xa5\ +\x00\x00\x0e\xa5\x49\x44\x41\x54\x78\x9c\xed\xdd\xed\x51\xdd\x48\ +\xb0\x06\xe0\xd7\xb7\x9c\x00\x29\xe0\x10\x70\x08\x76\x08\xde\x10\ +\x20\x04\x1c\x02\x84\x60\x42\x58\x42\x30\x21\x2c\x21\x2c\x21\x5c\ +\x42\xf0\xfd\x31\x9c\x42\xeb\x8b\x91\x8e\x8e\x3e\x46\xa3\xe7\xa9\ +\xa2\xd8\x2a\xaf\x6d\xa1\x39\xee\x9e\x6e\xcd\x8c\x3e\xfc\xfa\xf5\ +\x2b\x00\x00\x00\x00\x00\x40\xf1\x3f\x6b\x5f\x00\x00\x00\x00\x00\ +\x00\xd4\x44\xe3\x1c\x00\x00\x00\x00\x00\x3a\x34\xce\x01\x00\x00\ +\x00\x00\xa0\x43\xe3\x1c\x00\x00\x00\x00\x00\x3a\x34\xce\x01\x00\ +\x00\x00\x00\xa0\x43\xe3\x1c\x00\x00\x00\x00\x00\x3a\x34\xce\x01\ +\x00\x00\x00\x00\xa0\x43\xe3\x1c\x00\x00\x00\x00\x00\x3a\x34\xce\ +\x01\x00\x00\x00\x00\xa0\x43\xe3\x1c\x00\x00\x00\x00\x00\x3a\x34\ +\xce\x01\x00\x00\x00\x00\xa0\x43\xe3\x1c\x00\x00\x00\x00\x00\x3a\ +\x34\xce\x01\x00\x00\x00\x00\xa0\x43\xe3\x1c\x00\x00\x00\x00\x00\ +\x3a\x34\xce\x01\x00\x00\x00\x00\xa0\x43\xe3\x1c\x00\x00\x00\x00\ +\x00\x3a\x34\xce\x01\x00\x00\x00\x00\xa0\x43\xe3\x1c\x00\x00\x00\ +\x00\x00\x3a\x3e\xae\x7d\x01\x95\xf8\x92\xe4\xe2\xe5\xbf\x2f\x92\ +\x9c\xad\x78\x2d\x5b\xf6\x75\x86\x3f\x73\x8f\x63\xf3\xf0\xf2\xfd\ +\xb1\xf3\xdf\x73\xbb\xc9\xeb\x7d\x7e\xcb\x73\x92\xbb\x2c\x77\x3d\ +\x35\xfa\xd9\xf3\xeb\xdf\x53\xc6\xec\x77\x7d\xf7\x96\x65\xfd\x69\ +\x9c\xfa\xc6\x97\xe3\x3c\xa4\xc4\x8d\xc7\xbc\x7d\xbf\xd7\x24\xaf\ +\x2c\xa3\x2f\xf6\xdd\xa7\xe4\x15\xa8\xd1\x1e\xe3\xc4\x1c\x7e\xaf\ +\x0d\x2e\x52\x62\x03\x75\x78\x4c\x99\x17\xbd\x47\x8d\xd0\x4f\x8d\ +\xd0\x86\xdf\xc7\x49\xbc\xaa\xcb\x7b\xf1\x4a\x1d\x37\xad\x9a\xeb\ +\xb8\x55\xec\xb5\x71\x7e\x96\xe4\x5b\xca\xa4\xf8\xdb\xca\xd7\xc2\ +\x7f\x19\x9b\xf2\xb3\x1f\x3c\xa7\x04\xae\x87\xcc\xdb\x60\xb8\xf8\ +\xed\xef\x7d\xcb\xb7\x97\x6b\xb8\x4d\xf2\x34\xe3\xb5\xd4\xaa\xef\ +\xfe\xfc\xa9\xa8\x1e\x72\x6f\x59\xce\x9f\xc6\xc9\x18\x4d\xab\x7b\ +\x3f\x9f\x52\x62\xc7\x7d\xd6\x89\x1d\xf2\x4a\x9d\x79\x65\xcf\x4d\ +\x16\xea\x23\x4e\x2c\xe3\x2c\xf2\xed\xd6\xa8\x11\xfa\xa9\x11\xda\ +\xf0\xfb\x38\x89\x57\xdb\x61\x9c\xa6\x55\x53\x1d\x57\x85\xbd\x1d\ +\xd5\x72\x9e\xe4\x47\x92\x7f\x5f\xbe\x9b\x18\xd7\xc3\xd8\xbc\xed\ +\x50\xc8\xfd\x48\xf2\xbf\x49\xae\xb3\xee\xaa\xa7\xcb\x24\xff\xbc\ +\x5c\x07\xc0\x10\xe7\x29\x2b\x76\xfe\x7d\xf9\xbe\x54\x0c\x93\x57\ +\xde\x56\x5b\x5e\x81\x35\x89\x13\x30\x0d\x35\x02\x40\x7b\xd6\xaa\ +\xe3\xaa\xb2\xa7\xc6\xf9\x75\xca\x60\x5f\x66\xa7\x83\x5d\x31\x63\ +\x33\xcc\x59\x5e\x83\xd6\x9a\x93\xd2\xee\x75\x28\x30\x81\x63\x1c\ +\xe2\xfd\xdc\xb1\x43\x5e\x19\xa6\x96\xbc\x02\x6b\x10\x27\x60\x5a\ +\x6a\x04\x80\x76\x2d\x55\xc7\x55\x67\x0f\x8d\xf3\x8b\x94\xa7\xdf\ +\xce\xa7\xaa\x8f\xb1\x19\xe7\x30\x29\xfd\x27\xeb\x9e\x8b\x77\x9e\ +\xe4\xef\x94\x33\xc5\x9c\xcf\x07\x0c\x75\x96\x12\x3b\xe6\x88\xfd\ +\xf2\xca\x38\xb5\xe4\x15\x58\x82\x38\x01\xf3\x52\x23\x00\xb4\x69\ +\xce\x3a\xae\x5a\xad\x37\xce\x2f\x23\x61\xd7\xca\xd8\x9c\xee\x22\ +\xe5\x1e\x5e\xae\x7c\x1d\x5f\x52\x0a\xd0\x1f\xb1\x62\x0b\x18\xee\ +\x3a\x25\x6e\x4c\x45\x5e\x39\x5d\x2d\x79\x05\xe6\x22\x4e\xc0\x72\ +\xd4\x08\x00\x6d\x9a\xba\x8e\xab\x5a\xcb\x8d\xf3\xcb\x48\xd2\xb5\ +\x32\x36\xd3\x39\x4b\xb9\x97\x35\x6c\xb1\xbf\x8c\xed\xfe\xc0\x71\ +\x2e\x33\xcd\x8a\x05\x79\x65\x3a\x35\xe5\x15\x98\x92\x38\x01\xeb\ +\x50\x23\x00\xb4\x67\xaa\x3a\xae\x7a\xad\x36\xce\x0f\x13\x63\xea\ +\x63\x6c\xe6\x71\x93\x3a\xee\x6b\xf7\x6c\x43\x6f\xb7\x06\x86\xb8\ +\xce\x69\x67\xe5\xc9\x2b\xf3\xa8\x25\xaf\xc0\x14\xc4\x09\x58\x97\ +\x1a\x01\xa0\x3d\xa7\xd6\x71\x9b\xf0\x71\xed\x0b\x98\xc1\x29\x13\ +\xe3\xc7\x24\x4f\x2f\xdf\x99\x9e\xb1\xe9\x77\x91\x32\xb1\x1c\x33\ +\xa1\xbc\x4c\xb9\x3f\x77\x93\x5e\xd1\x38\xe7\x29\x5b\xa1\x1f\x92\ +\x5c\xa5\x8c\xdd\x5e\xdd\xa7\xdc\x87\xad\xe9\x7b\x7a\xfc\x90\x6d\ +\xfe\x5c\x7f\xfa\x2c\x7e\x5f\xf4\x2a\xda\x76\xf1\xf2\x75\x7e\xc4\ +\xef\xf9\x91\xf2\x79\x7a\x3e\xf2\xef\x92\x57\xfa\xb5\x92\x57\x60\ +\x2c\x71\xa2\x1e\x4f\x91\x6f\x6b\x72\x6c\xce\x9d\x82\x1a\xe1\x95\ +\x1a\xa1\x2e\xbf\x7f\x16\xc5\xab\xba\xbc\x17\xaf\x8c\xd3\x74\x96\ +\xac\xe3\x36\xe3\xc3\xaf\x5f\xbf\xd6\xbe\x86\x29\x1d\x5e\xf6\x73\ +\x8c\xfb\xbc\x26\xad\x66\x07\xba\x02\xc6\xe6\x78\xdf\x3a\x5f\xc7\ +\xf8\x9a\xe3\x27\x2b\x3f\xf3\x7e\x53\xe5\x39\xa7\x6d\x6d\xbe\x7d\ +\xf9\xda\xf2\x38\xf6\x05\xcb\x31\xf7\xbd\x66\x7d\x3f\xef\xf7\x94\ +\x31\x85\x3f\xb9\xc8\x71\xab\x10\xee\x52\x8a\xe8\x63\xfe\x7c\x79\ +\xe5\x38\x35\xe5\x15\x31\x84\x25\x88\x13\x70\x1a\x35\x42\x3f\x35\ +\xc2\x7f\xc9\xef\xb0\x7d\x73\xd7\x71\x9b\xd2\xd2\x51\x2d\x87\xb7\ +\xbb\x0e\xf5\x90\xe4\x73\x92\xbf\x52\x26\xc7\x5b\x4e\xd6\xb5\x33\ +\x36\xe3\xdc\xa7\xdc\x83\xcf\x39\x6e\xb2\x35\xc7\xf9\x9d\xb7\x2f\ +\xd7\x32\x76\x55\xc8\x75\xca\xd6\x4c\x2f\x9c\x83\xfd\x78\x4c\x89\ +\x1b\x7f\x65\x58\x1c\xbf\xcc\xf0\xd5\x0d\xf2\xca\x38\x35\xe5\x15\ +\x98\x9b\x38\x01\xf3\x53\x23\x00\xb4\x67\xce\x3a\x6e\x73\x5a\x6a\ +\x9c\xdf\x64\xf8\x40\x7d\x4f\x79\xf2\x6b\xdb\xe5\x32\x8c\xcd\x69\ +\x1e\x53\xee\xc9\xd0\x2d\x48\xe7\x99\xe7\xe5\x3b\xf7\x49\x3e\xbd\ +\x5c\xc7\x98\x62\xf2\xf0\xc2\xb9\x7f\xe2\x6c\x43\xd8\x93\xfb\x94\ +\x18\x36\x24\x6e\x0c\x8d\x5d\xf2\xca\x69\x6a\xc9\x2b\x30\x27\x71\ +\x02\x96\xa1\x46\x00\x68\xd3\x1c\x75\xdc\xe6\xb4\xd2\x38\xff\x92\ +\x61\x4f\xa9\x9f\x53\x9e\x98\xd8\x3a\xb4\x1c\x63\x33\x9d\xdb\x1c\ +\x17\xb4\xe6\x7a\xe2\x77\x9b\xb2\x22\x6b\xec\x99\xb7\x17\x29\xdb\ +\x3e\xff\x4e\xc3\x4f\x25\x81\xff\x38\xac\x5a\xe8\x33\x64\x3b\xa0\ +\xbc\x32\x9d\x5a\xf2\x0a\x4c\x4d\x9c\x80\xe5\xa9\x11\x00\xda\x33\ +\x65\x1d\xb7\x49\xad\x34\xce\x87\x3e\xd9\xf8\x9a\xf2\xc4\x84\xe5\ +\x18\x9b\x69\x3d\xa4\xdc\xab\x21\xe6\x7c\xe2\xf7\x94\x72\x86\xd5\ +\x29\x67\xf6\x7d\x4b\xd9\x9a\x79\x1d\x47\x00\xc0\x1e\x3c\xa4\xbf\ +\x98\x1e\xf2\x12\x4b\x79\x65\x5a\xb5\xe4\x15\x98\x92\x38\x01\xeb\ +\x50\x23\x00\xb4\x67\xaa\x3a\x6e\x93\x5a\x68\x9c\x5f\x64\xd8\xe0\ +\x5c\xc5\xf6\xcb\xa5\x19\x9b\x79\x3c\x66\xd8\x8b\x17\x96\x38\x67\ +\xea\xd0\x70\xb9\xca\xf8\xb3\x40\x6f\x52\xb6\x66\x3a\xdb\x10\xda\ +\x37\x64\x55\xe7\x45\xcf\xaf\xc9\x2b\xd3\xab\x29\xaf\xc0\xa9\xc4\ +\x09\x58\x9f\x1a\x01\xa0\x2d\xa7\xd6\x71\x9b\xd5\x42\xe3\x7c\x48\ +\x22\xbd\xcf\xf8\x2d\x63\x8c\x67\x6c\xe6\x73\x97\x61\x2b\xa4\x96\ +\x9a\x68\xde\xe5\xf5\x6c\xc3\x31\xce\x53\xce\x36\xfc\x99\x46\x9f\ +\x52\x02\x49\xca\x4a\xb4\xbe\x46\xd5\x7b\x13\x2e\x79\x65\x3e\xb5\ +\xe5\x15\x18\x4b\x9c\x80\x7a\xa8\x11\x00\xda\x70\x6a\x1d\xb7\x59\ +\x2d\x34\xce\xfb\xce\xd1\x79\xce\xb0\x55\x54\x4c\xcf\xd8\xcc\x6b\ +\xc8\x0a\x8e\x25\xcf\x99\x7a\x4e\x79\x0a\xf9\x29\xe3\xb7\x3d\x7f\ +\x49\x99\x18\xff\x88\x55\x8d\xd0\xaa\xbe\xf8\xf0\xde\xb6\x6c\x79\ +\x65\x5e\xb5\xe5\x15\x18\x43\x9c\x80\xba\xa8\x11\x00\xda\x70\x4a\ +\x1d\xb7\x59\x5b\x6f\x9c\x5f\xa4\x7f\x60\xee\x32\x7e\x7b\x18\xe3\ +\x19\x9b\xf9\x3d\xa7\x7f\xb5\xd4\x79\x96\x7f\xea\xf7\x94\xf2\xf2\ +\x88\xaf\x19\xbf\x05\xfa\x32\x65\x6b\xa6\xf3\x74\x81\x03\x79\x65\ +\x7e\xb5\xe6\x15\x18\x4a\x9c\x80\x7a\xa9\x11\x00\xd8\x9c\xad\x37\ +\xce\x87\x6c\xd7\xb2\x0d\x73\x1d\xc6\x66\x19\x43\xee\xe1\x5a\xdb\ +\x1a\x1f\x92\x7c\x4e\xd9\x9a\x39\xa6\x40\x3d\x4b\x39\xdb\xf0\xdf\ +\x58\xe1\x08\xc8\x2b\x4b\xa9\x39\xaf\x40\x1f\x71\x02\xea\xa7\x46\ +\x00\x60\x33\x3e\xae\x7d\x01\x27\xea\x5b\xf1\xf4\x98\xf2\x64\x9b\ +\xe5\x19\x9b\x65\x1c\xce\x99\x7a\xef\x7e\xaf\xbd\x9d\xf1\x36\xa5\ +\x48\xbd\xce\xb8\xd5\x21\xe7\x49\xfe\x4e\x99\x64\x7f\x8f\x17\x79\ +\xc1\x5e\xc9\x2b\xcb\xd8\x42\x5e\x81\x3f\x11\x27\xea\x76\x1e\x8d\ +\xce\x9a\x0c\xd9\x65\x34\x27\x35\x02\x35\x13\xaf\xea\xb2\x76\xbc\ +\x62\xc7\xb6\xde\x38\xef\xdb\x8a\xf9\xb0\xc8\x55\xf0\x16\x63\xb3\ +\x9c\x87\xd4\xdf\xe0\x78\x4e\x99\xd0\xde\xa5\x9c\x4d\x38\x66\xb5\ +\xe2\x97\x94\xad\x99\x77\x19\xbf\x42\x05\x58\xdf\xd8\x63\x3e\xe4\ +\x95\xe5\x6c\x21\xaf\xc0\x5b\xc4\x89\xba\x9d\xa7\xac\x14\xa6\x0e\ +\x0f\x59\xbf\x11\xa5\x46\xa0\x56\xe2\x55\x5d\x6a\x88\x57\xec\xf4\ +\xb8\xc6\xd6\x8f\x6a\x91\x34\xd7\x63\x6c\x96\xd3\x77\x2f\x6b\x0a\ +\x6e\x4f\x29\xe7\x1a\x7e\xcd\xf8\x15\x5f\x97\x29\x5b\x33\x9d\x6d\ +\x08\xdb\x73\x96\xfe\xfc\xf0\xa7\x15\x63\xf2\xca\x72\xb6\x94\x57\ +\xa0\x4b\x9c\x80\x6d\x52\x23\x00\xd4\xed\x94\x3a\x6e\xd3\xb6\xde\ +\x38\xef\xd3\xe4\xa0\x35\xc2\xd8\x4c\xa7\xef\x5e\xd6\xf8\x66\xe3\ +\x87\x24\x9f\x32\xcd\xd9\x86\xce\xda\x85\xed\xb8\x4c\x7f\x4c\x1a\ +\x5b\x30\xcb\x2b\xd3\xd9\x62\x5e\x81\x21\xc4\x09\xa8\x9b\x1a\x01\ +\xa0\x4e\x73\xd6\x71\x55\x6b\xbd\x71\x0e\xd4\xed\x36\x65\x72\x3c\ +\x76\xdb\xd5\x79\x92\x9f\x2f\x5f\x8e\x0e\x80\xba\x9d\x67\xd8\x2a\ +\x30\x47\x29\x00\xc0\xbe\xa9\x11\x00\xea\xb1\xeb\x3a\x4e\xe3\x1c\ +\x58\xdb\x73\x92\xab\x24\x9f\x33\x3e\xd0\x7e\xc9\xeb\xd6\x4c\x2b\ +\x21\xa1\x3e\x67\x29\x2f\xf0\x1a\x72\xfe\x70\x93\x2b\x15\x00\x80\ +\xa3\xa8\x11\x00\xd6\xb7\xfb\x3a\xae\xf5\xc6\xb9\x33\x38\xeb\x65\ +\x6c\xa6\xd3\xca\x2a\x8a\xc7\x94\x73\x0d\xff\xca\xf8\x80\x7b\xd8\ +\x9a\x79\x39\xd5\x45\x01\x27\x3b\x4b\x59\xf1\x35\x24\xee\xdf\x9f\ +\xf0\xf7\xc8\x2b\xd3\x69\x25\xaf\xc0\xef\xc4\x09\xd8\x1e\x35\x02\ +\xc0\x3a\x96\xaa\xe3\xaa\xb6\xf5\xc6\xb9\x73\x0a\xeb\x65\x6c\x96\ +\xd3\xf7\xe4\x6f\x6b\x63\x71\x9f\xb2\xb2\xe4\x94\xb3\x0d\x7f\x24\ +\xf9\x27\xce\x36\x84\xb5\x1d\x5e\xd4\x35\x64\xb2\xf5\x94\xf7\xb7\ +\x64\x6f\x2d\x96\x6d\x59\x6b\x79\x85\xfd\xf0\xd9\x84\x76\xa9\x11\ +\x00\x96\x33\x65\x1d\xb7\x69\x1f\xd7\xbe\x80\x13\xf5\x25\xcc\x6f\ +\x29\xe7\xa3\xb1\x3c\x63\xb3\x9c\xbe\x89\xdf\x98\x89\xe5\xda\x9e\ +\x53\x3e\x1f\xf7\x29\x5b\x2b\xc7\xac\x0e\xb9\x48\x79\x3a\x7a\x9f\ +\x32\xc1\x6e\x72\xdb\x10\x93\x51\x40\x4d\xe7\x2c\xe5\xdf\xdf\xb7\ +\x1c\xb7\x72\xf9\xaa\xe7\xd7\xe5\x95\xe5\xb4\x98\x57\xd8\x07\x71\ +\xa2\x6e\xcf\x69\xf4\xfc\xd3\x8d\xda\xe2\x83\x26\x35\x02\x4b\x11\ +\xaf\xea\xf2\x5e\xbc\x52\xc7\x4d\x67\xae\x3a\x6e\xd3\xb6\xde\x38\ +\x7f\xc8\xfb\xff\x48\x2e\x52\x06\x5e\x81\xb7\x3c\x63\xb3\x8c\xb3\ +\xf4\x27\x8a\x2d\x27\xfc\xa7\x94\x20\x7c\x98\x1c\x8f\x49\x8a\xdf\ +\x5e\x7e\xdf\x6d\xca\x53\x50\x9f\x39\xde\xf2\x73\xed\x0b\xd8\xb9\ +\xdb\xf4\xc7\x2a\x79\x65\x19\xad\xe7\x15\xda\x26\x4e\xd4\xed\x70\ +\xe4\x06\x9c\x4a\x8d\xc0\xdc\xc4\xab\xed\x50\xc7\xad\x6b\x48\x1d\ +\xb7\x69\x5b\x3f\xaa\x65\xc8\xd3\x61\xe7\x98\xad\xc3\xd8\x2c\x63\ +\xc8\x3d\x6c\x61\x15\xc5\x43\xca\xc4\xe5\x2a\xe3\xb7\x66\xde\xa4\ +\x6c\xcd\xf4\xb9\x83\xba\x3c\xa6\xac\xf8\xea\x23\xaf\x2c\x63\x2f\ +\x79\x85\x36\x89\x13\xb0\x2f\x6a\x04\x80\xf5\x0c\xad\xe3\x36\x6d\ +\xeb\x8d\xf3\x21\x4f\x35\xbc\x41\x7b\x1d\xc6\x66\x19\x43\x26\x78\ +\x2d\xbd\xa4\xe1\x2e\xc9\xa7\x8c\x0f\xce\xe7\x29\x67\x1b\xfe\x8c\ +\x2d\x5d\x50\x83\x63\x56\xf3\xc8\x2b\xcb\xd8\x5b\x5e\xa1\x2d\xe2\ +\x04\xec\x93\x1a\x01\x60\x59\xbb\xd9\x95\xb1\xf5\xc6\xf9\x73\xfa\ +\x8b\xb7\xb3\x78\x7a\xbc\x06\x63\x33\xbf\xeb\xf4\x9f\x3b\xd5\x62\ +\x73\xe3\x70\xb6\xe1\xa7\x8c\xff\xf9\xbe\xa4\x4c\x8c\x7f\x44\xf1\ +\x0c\x6b\x39\x4c\xb6\x86\xae\x10\x93\x57\xe6\xb7\xd7\xbc\x42\x3b\ +\xc4\x09\xd8\x2f\x35\x02\xc0\x32\x8e\xad\xe3\x36\x6d\xeb\x8d\xf3\ +\x64\x58\x52\xbc\xc9\xb0\x37\xc1\x32\x2d\x63\x33\x9f\x8b\x94\x7b\ +\xd7\xa7\xe5\x06\xc7\x53\x92\xbf\x52\x02\xf6\xd8\x97\x1b\x1d\xde\ +\x14\x7d\x3d\xd5\x45\x01\x83\xdc\x65\xdc\x64\x4b\x5e\x99\x8f\xbc\ +\x42\x2b\xc4\x09\xd8\x37\x35\x02\xc0\x7c\xc6\xd6\x71\x9b\xd5\x4a\ +\xe3\x7c\xc8\x79\x86\x7f\xc7\x53\xe3\xa5\x19\x9b\x79\x9c\xa5\xdc\ +\xb3\x3e\x4f\xd9\x47\x83\xe3\x21\xc9\xe7\x94\xad\x99\xa7\x9c\x6d\ +\xf8\x6f\xca\x4b\x82\x80\xf9\x1c\x8a\xd9\xb1\x67\x91\xca\x2b\xf3\ +\x90\x57\x68\x89\x38\x01\x24\x6a\x04\x80\x29\x9d\x5a\xc7\x6d\x56\ +\x0b\x8d\xf3\xa4\x6c\xc9\xea\x73\x9e\xb2\xed\xca\x04\x79\x59\xc6\ +\x66\x5a\x67\x29\xf7\xaa\x6f\x2b\x7d\x32\xec\xde\xb7\xe4\xb0\x35\ +\x73\xec\xcf\x7d\x9e\x52\x44\xff\x8c\x55\x68\x30\xb5\xa7\x94\xc2\ +\xf5\x73\x4e\x6f\xbc\xca\x2b\xd3\x92\x57\x68\x91\x38\x01\x1c\xa8\ +\x11\x00\xc6\x9b\xb2\x8e\xdb\xa4\x56\x1a\xe7\x77\x19\xf6\x32\xa0\ +\x8b\x94\x37\x66\x4b\x78\xcb\x31\x36\xd3\xb9\x48\x59\xf1\x30\xe4\ +\x1e\x3d\xa4\xdc\xfb\xbd\x79\x4e\x09\xea\x9f\x32\xec\x73\xf7\x96\ +\x2f\x29\x9f\xc5\x9b\x28\xa6\x61\xac\xa7\x94\x7f\x83\x87\x49\xd6\ +\xa1\x60\x9d\x62\x75\x82\xbc\x32\x1d\x79\x85\x56\x89\x13\x40\x97\ +\x1a\x01\x60\x98\x39\xeb\xb8\x4d\x6a\xa5\x71\x9e\x0c\xdf\x82\x75\ +\x9e\x92\xf0\xae\x23\xe1\x2d\xc5\xd8\x9c\xee\x3a\xe5\xde\x0c\xbd\ +\x2f\x63\xdf\x28\xdf\x8a\xa7\x94\x73\xb7\xfe\xca\xb0\xed\xda\x6f\ +\xb9\x8e\xb3\x0d\x79\xf5\x35\xc9\x87\x9d\x7d\x1d\xd3\x24\x7d\x4e\ +\x99\x58\x1d\x7e\xef\xa7\x97\x7b\x76\x9b\xf1\xe7\x8b\xbe\x47\x5e\ +\x39\x9d\xbc\x42\xeb\xc4\x09\xe0\x77\x6a\x04\xd8\x1f\x75\xdc\xfb\ +\x96\xae\xe3\x36\xa7\xa5\xc6\xf9\x63\x8e\xdb\x7e\x75\x13\x93\xe4\ +\xa5\x18\x9b\x71\xce\xf2\x3a\x31\x1b\xf2\xc2\xb6\x83\xef\x11\xe0\ +\x0e\xee\x53\x02\xff\x14\x67\x1b\xc2\xde\x5c\x65\xf8\xa4\xeb\x2c\ +\xcb\x6e\x61\x96\x57\xc6\x91\x57\xd8\x13\x71\x02\xf8\x13\x35\x02\ +\xd0\xb2\x9a\xeb\xb8\xcd\xf9\xb8\xf6\x05\x4c\xec\x36\x65\xd5\xc8\ +\xe5\xc0\xff\xff\x3c\x25\xe1\xdd\xa4\x6c\x45\x78\x48\x49\x9c\x63\ +\x9f\x3e\xef\xdd\x7b\xdb\xde\x8c\xcd\x30\xe7\x29\x81\xeb\xcb\xcb\ +\xd7\xb1\xee\xe3\x0c\xda\xb7\xdc\xa6\x24\x8e\x9b\x0c\xff\x0c\x76\ +\x0d\x39\xfb\x17\x5a\x74\xf5\xf2\x7d\xc8\xbf\x9b\xc3\xa4\xeb\x6b\ +\x96\x69\xb2\xca\x2b\xc3\xd4\x9a\x57\xce\x33\xee\x7a\x18\xe7\x31\ +\xfb\xdc\x62\x2b\x4e\xac\xeb\xf7\xda\xe0\x2c\x0a\xf3\x9a\x3c\xc7\ +\x43\x51\x35\x02\x7f\x22\x5e\xd5\x45\xbc\x3a\x5e\xcd\x75\xdc\xa6\ +\xb4\xd6\x38\x4f\x8e\xfb\x70\x74\x8d\x2d\x28\x79\xf5\xa1\xe7\xd7\ +\x8d\xcd\xbc\x1e\xf3\x7a\x8f\xf9\xff\x9e\xf3\xfa\xe4\xf5\x26\x3e\ +\x53\x30\x54\xcd\x93\x2e\x79\x65\x5e\x73\xe6\x95\xcb\x8c\x6b\x52\ +\x30\xce\xd7\x8c\x3f\xd7\x77\xeb\xc4\x89\xf5\xfc\x5e\x1b\x5c\xa4\ +\xe4\x08\xea\xf0\x90\x12\x1b\xf6\x4e\x8d\xc0\x5b\xc4\xab\xba\x88\ +\x57\xe3\xd4\x5c\xc7\x6d\x46\x4b\x47\xb5\x74\x1d\xb3\x2d\x81\x65\ +\x19\x9b\x79\x3c\xa6\x04\xb8\x3d\xae\x26\x3b\xd6\xe1\x5e\x5d\xc5\ +\x0a\x32\x18\xaa\xe6\xed\x7e\xf2\xca\x3c\xe4\x15\x5a\x22\x4e\x00\ +\x7d\xd4\x08\x40\x8b\x6a\xae\xe3\x36\xa1\xd5\xc6\x79\x62\x82\x5c\ +\x33\x63\x33\xad\xfb\x68\x6e\x8c\x71\x97\xf2\x12\x8c\xb1\x67\x1b\ +\xc2\xde\xd4\x3c\xe9\x92\x57\xa6\x25\xaf\xd0\x22\x71\x02\x18\x42\ +\x8d\x00\xb4\xa6\xe6\x3a\xae\x7a\x2d\x37\xce\x93\xf2\xe1\xf8\xbe\ +\xf6\x45\xf0\x26\x63\x33\x8d\xef\x29\x6f\x85\x37\xa9\x1b\xe7\x39\ +\xe5\x6c\xc3\xcf\x51\x4c\xc3\x10\x35\x4f\xba\xe4\x95\x69\xc8\x2b\ +\xb4\x4c\x9c\x00\x86\x50\x23\x00\xad\xa9\xb9\x8e\xab\x5a\xeb\x8d\ +\xf3\xe4\x35\xe1\xed\xf5\x5c\xc7\x9a\x19\x9b\xf1\x1e\x52\xee\x9d\ +\x17\x81\x4e\xe3\x29\x25\x91\x38\xcf\x0b\xfa\xd5\x3c\xe9\x92\x57\ +\xc6\x93\x57\xd8\x0b\x71\x02\x18\x4a\x8d\x00\xb4\xa4\xe6\x3a\xae\ +\x5a\x7b\x68\x9c\x27\xce\x2b\xab\x99\xb1\x39\x8e\xc9\xdb\xbc\x0e\ +\x8d\xa3\xab\x58\x6d\x09\xef\xa9\x79\xd2\x25\xaf\x1c\x47\x5e\x61\ +\x8f\xc4\x09\xe0\x18\x6a\x04\xa0\x15\x35\xd7\x71\x55\xda\x4b\xe3\ +\xfc\xe0\x2e\xc9\xa7\x94\x2d\xc8\xf7\x2b\x5f\x0b\xff\x65\x6c\xde\ +\x77\x9f\x72\x6f\x3e\xc5\x76\xc1\x25\x1c\x3e\x8f\xb6\x73\xc3\x9f\ +\xd5\x3e\xe9\x92\x57\xde\x27\xaf\x80\x38\x01\x1c\x47\x8d\x00\xb4\ +\xa0\xf6\x3a\xae\x2a\x1f\xd7\xbe\x80\x95\xdc\xbf\x7c\x9d\x25\xf9\ +\x92\xe4\xfc\xe5\xfb\x59\x76\xfc\x61\xa8\x84\xb1\x29\xab\xa0\x9e\ +\x53\x56\x36\x3c\xc6\x56\xe2\xb5\x1c\xce\x36\xbc\x4f\x72\x93\xe4\ +\xdb\xba\x97\x03\x55\xba\x7a\xf9\x7e\x39\xe0\xff\x3d\x4c\xba\x96\ +\x5e\xd9\x2c\xaf\xc8\x2b\xd0\x47\x9c\x00\x86\x52\x23\x00\x2d\xd8\ +\x42\x1d\x57\x85\x0f\xbf\x7e\xfd\x5a\xfb\x1a\x00\x00\x00\x00\x00\ +\xa0\x1a\x7b\x3b\xaa\x05\x00\x00\x00\x00\x00\xde\xa5\x71\x0e\x00\ +\x00\x00\x00\x00\x1d\x1a\xe7\x00\x00\x00\x00\x00\xd0\xa1\x71\x0e\ +\x00\x00\x00\x00\x00\x1d\x1a\xe7\x00\x00\x00\x00\x00\xd0\xa1\x71\ +\x0e\x00\x00\x00\x00\x00\x1d\x1a\xe7\x00\x00\x00\x00\x00\xd0\xa1\ +\x71\x0e\x00\x00\x00\x00\x00\x1d\x1a\xe7\x00\x00\x00\x00\x00\xd0\ +\xa1\x71\x0e\x00\x00\x00\x00\x00\x1d\x1a\xe7\x00\x00\x00\x00\x00\ +\xd0\xa1\x71\x0e\x00\x00\x00\x00\x00\x1d\x1a\xe7\x00\x00\x00\x00\ +\x00\xd0\xa1\x71\x0e\x00\x00\x00\x00\x00\x1d\x1a\xe7\x00\x00\x00\ +\x00\x00\xd0\xa1\x71\x0e\x00\x00\x00\x00\x00\x1d\x1a\xe7\x00\x00\ +\x00\x00\x00\xd0\xa1\x71\x0e\x00\x00\x00\x00\x00\x1d\x1a\xe7\x00\ +\x00\x00\x00\x00\xd0\xa1\x71\x0e\x00\x00\x00\x00\x00\x1d\x1a\xe7\ +\x00\x00\x00\x00\x00\xd0\xa1\x71\x0e\x00\x00\x00\x00\x00\x1d\x1a\ +\xe7\x00\x00\x00\x00\x00\xd0\xf1\x7f\xf8\x46\x19\xa7\x52\x18\xf4\ +\x54\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ \x00\x00\x0f\x14\ \x89\ \x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ @@ -106029,6 +106839,11 @@ \x02\x07\x24\x27\ \x00\x74\ \x00\x68\x00\x75\x00\x6d\x00\x62\x00\x6e\x00\x61\x00\x69\x00\x6c\x00\x2e\x00\x70\x00\x6e\x00\x67\ +\x00\x1d\ +\x07\x55\x6a\x67\ +\x00\x63\ +\x00\x6f\x00\x6e\x00\x74\x00\x72\x00\x6f\x00\x6c\x00\x5f\x00\x63\x00\x65\x00\x6e\x00\x74\x00\x65\x00\x72\x00\x5f\x00\x6c\x00\x6f\ +\x00\x67\x00\x6f\x00\x5f\x00\x62\x00\x6c\x00\x61\x00\x63\x00\x6b\x00\x2e\x00\x70\x00\x6e\x00\x67\ \x00\x0c\ \x07\xcf\xa3\x07\ \x00\x66\ @@ -106395,8 +107210,8 @@ qt_resource_struct_v1 = b"\ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x04\x00\x00\x00\x01\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x60\ -\x00\x00\x00\x0e\x00\x02\x00\x00\x00\x01\x00\x00\x00\x16\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x61\ +\x00\x00\x00\x0e\x00\x02\x00\x00\x00\x01\x00\x00\x00\x17\ \x00\x00\x00\x1e\x00\x02\x00\x00\x00\x01\x00\x00\x00\x0f\ \x00\x00\x00\x44\x00\x02\x00\x00\x00\x01\x00\x00\x00\x05\ \x00\x00\x00\x5e\x00\x02\x00\x00\x00\x01\x00\x00\x00\x06\ @@ -106410,102 +107225,103 @@ \x00\x00\x01\x10\x00\x00\x00\x00\x00\x01\x00\x00\x0b\xd7\ \x00\x00\x01\x2e\x00\x00\x00\x00\x00\x01\x00\x00\x0e\x02\ \x00\x00\x00\x5e\x00\x02\x00\x00\x00\x01\x00\x00\x00\x10\ -\x00\x00\x01\x44\x00\x02\x00\x00\x00\x05\x00\x00\x00\x11\ +\x00\x00\x01\x44\x00\x02\x00\x00\x00\x06\x00\x00\x00\x11\ \x00\x00\x01\x54\x00\x00\x00\x00\x00\x01\x00\x00\x19\x27\ \x00\x00\x01\x7c\x00\x00\x00\x00\x00\x01\x00\x00\x5f\x9c\ \x00\x00\x01\x9c\x00\x00\x00\x00\x00\x01\x00\x00\xab\x63\ -\x00\x00\x01\xba\x00\x00\x00\x00\x00\x01\x00\x00\xcc\x8d\ -\x00\x00\x01\xf8\x00\x00\x00\x00\x00\x01\x00\x00\xed\x5a\ -\x00\x00\x00\x5e\x00\x02\x00\x00\x00\x01\x00\x00\x00\x17\ -\x00\x00\x02\x2c\x00\x02\x00\x00\x00\x48\x00\x00\x00\x18\ -\x00\x00\x02\x3c\x00\x00\x00\x00\x00\x01\x00\x00\xfc\x72\ -\x00\x00\x02\x56\x00\x00\x00\x00\x00\x01\x00\x01\x21\x0c\ -\x00\x00\x02\x7c\x00\x00\x00\x00\x00\x01\x00\x01\x73\x57\ -\x00\x00\x02\x96\x00\x00\x00\x00\x00\x01\x00\x01\x85\x74\ -\x00\x00\x02\xb4\x00\x00\x00\x00\x00\x01\x00\x01\x94\x3c\ -\x00\x00\x02\xea\x00\x00\x00\x00\x00\x01\x00\x01\x97\xca\ -\x00\x00\x03\x16\x00\x00\x00\x00\x00\x01\x00\x01\xbb\x04\ -\x00\x00\x03\x30\x00\x00\x00\x00\x00\x01\x00\x01\xc2\x7d\ -\x00\x00\x03\x58\x00\x00\x00\x00\x00\x01\x00\x01\xc9\xbe\ -\x00\x00\x03\x7c\x00\x00\x00\x00\x00\x01\x00\x01\xd1\xee\ -\x00\x00\x03\xa2\x00\x00\x00\x00\x00\x01\x00\x02\x04\xd7\ -\x00\x00\x03\xce\x00\x00\x00\x00\x00\x01\x00\x02\x34\x7e\ -\x00\x00\x03\xe8\x00\x00\x00\x00\x00\x01\x00\x02\x39\x99\ -\x00\x00\x04\x08\x00\x00\x00\x00\x00\x01\x00\x02\x40\x15\ -\x00\x00\x04\x36\x00\x00\x00\x00\x00\x01\x00\x02\x7b\xd2\ -\x00\x00\x04\x60\x00\x00\x00\x00\x00\x01\x00\x02\xa2\xda\ -\x00\x00\x04\x80\x00\x00\x00\x00\x00\x01\x00\x02\xae\xb4\ -\x00\x00\x04\xbe\x00\x00\x00\x00\x00\x01\x00\x02\xb6\x51\ -\x00\x00\x04\xd4\x00\x00\x00\x00\x00\x01\x00\x02\xde\x71\ -\x00\x00\x04\xee\x00\x00\x00\x00\x00\x01\x00\x02\xf0\x56\ -\x00\x00\x05\x08\x00\x00\x00\x00\x00\x01\x00\x03\x17\xf3\ -\x00\x00\x05\x32\x00\x00\x00\x00\x00\x01\x00\x03\x3f\xc5\ -\x00\x00\x05\x54\x00\x00\x00\x00\x00\x01\x00\x03\x44\x80\ -\x00\x00\x05\x8a\x00\x00\x00\x00\x00\x01\x00\x03\x56\xfc\ -\x00\x00\x05\xa2\x00\x00\x00\x00\x00\x01\x00\x03\x7a\xec\ -\x00\x00\x05\xbe\x00\x00\x00\x00\x00\x01\x00\x03\x7f\xce\ -\x00\x00\x05\xf6\x00\x00\x00\x00\x00\x01\x00\x03\xae\x15\ -\x00\x00\x06\x1a\x00\x00\x00\x00\x00\x01\x00\x03\xb2\x87\ -\x00\x00\x06\x48\x00\x00\x00\x00\x00\x01\x00\x03\xb6\xc1\ -\x00\x00\x06\x66\x00\x00\x00\x00\x00\x01\x00\x03\xbc\x92\ -\x00\x00\x06\x8c\x00\x00\x00\x00\x00\x01\x00\x03\xcd\x0d\ -\x00\x00\x06\xa2\x00\x00\x00\x00\x00\x01\x00\x03\xd7\x81\ -\x00\x00\x06\xb8\x00\x00\x00\x00\x00\x01\x00\x03\xe8\xa8\ -\x00\x00\x06\xcc\x00\x00\x00\x00\x00\x01\x00\x04\x0b\xf0\ -\x00\x00\x06\xf0\x00\x00\x00\x00\x00\x01\x00\x04\x10\x09\ -\x00\x00\x07\x0e\x00\x00\x00\x00\x00\x01\x00\x04\x2a\xdb\ -\x00\x00\x07\x30\x00\x00\x00\x00\x00\x01\x00\x04\x47\x3d\ -\x00\x00\x07\x48\x00\x00\x00\x00\x00\x01\x00\x04\x4c\x57\ -\x00\x00\x07\x86\x00\x00\x00\x00\x00\x01\x00\x04\x59\x8d\ -\x00\x00\x07\xa0\x00\x00\x00\x00\x00\x01\x00\x04\x62\xbb\ -\x00\x00\x07\xc6\x00\x00\x00\x00\x00\x01\x00\x04\x72\x2d\ -\x00\x00\x07\xf8\x00\x00\x00\x00\x00\x01\x00\x04\x8b\xf2\ -\x00\x00\x08\x14\x00\x00\x00\x00\x00\x01\x00\x04\x94\xd0\ -\x00\x00\x08\x3e\x00\x00\x00\x00\x00\x01\x00\x04\xb5\xb4\ -\x00\x00\x08\x68\x00\x00\x00\x00\x00\x01\x00\x04\xd2\xc1\ -\x00\x00\x08\x7e\x00\x00\x00\x00\x00\x01\x00\x04\xd5\xa4\ -\x00\x00\x08\x9e\x00\x00\x00\x00\x00\x01\x00\x04\xd8\xf5\ -\x00\x00\x08\xb8\x00\x00\x00\x00\x00\x01\x00\x04\xde\xe5\ -\x00\x00\x08\xdc\x00\x00\x00\x00\x00\x01\x00\x04\xf0\x84\ -\x00\x00\x08\xf4\x00\x00\x00\x00\x00\x01\x00\x05\x05\x4c\ -\x00\x00\x09\x26\x00\x00\x00\x00\x00\x01\x00\x05\x33\x1f\ -\x00\x00\x09\x52\x00\x00\x00\x00\x00\x01\x00\x05\x58\x73\ -\x00\x00\x09\x70\x00\x00\x00\x00\x00\x01\x00\x05\x67\x43\ -\x00\x00\x09\x92\x00\x00\x00\x00\x00\x01\x00\x05\x6b\x3d\ -\x00\x00\x09\xc0\x00\x00\x00\x00\x00\x01\x00\x05\x7e\xd2\ -\x00\x00\x09\xf2\x00\x00\x00\x00\x00\x01\x00\x05\x97\x08\ -\x00\x00\x0a\x22\x00\x00\x00\x00\x00\x01\x00\x05\xd3\x4e\ -\x00\x00\x0a\x58\x00\x00\x00\x00\x00\x01\x00\x05\xda\x15\ -\x00\x00\x0a\x74\x00\x00\x00\x00\x00\x01\x00\x05\xe1\x5e\ -\x00\x00\x0a\x8a\x00\x00\x00\x00\x00\x01\x00\x05\xe6\x0c\ -\x00\x00\x0a\xb2\x00\x00\x00\x00\x00\x01\x00\x05\xf8\xb4\ -\x00\x00\x0a\xcc\x00\x00\x00\x00\x00\x01\x00\x06\x20\xf6\ -\x00\x00\x0a\xe0\x00\x00\x00\x00\x00\x01\x00\x06\x27\xd4\ -\x00\x00\x0a\xf8\x00\x00\x00\x00\x00\x01\x00\x06\x31\x06\ -\x00\x00\x0b\x10\x00\x00\x00\x00\x00\x01\x00\x06\x3d\xd8\ -\x00\x00\x0b\x2a\x00\x00\x00\x00\x00\x01\x00\x06\x45\x16\ -\x00\x00\x0b\x68\x00\x00\x00\x00\x00\x01\x00\x06\x59\xd6\ -\x00\x00\x0b\x94\x00\x00\x00\x00\x00\x01\x00\x06\x5f\x1c\ -\x00\x00\x0b\xc4\x00\x00\x00\x00\x00\x01\x00\x06\x6d\xef\ -\x00\x00\x0b\xea\x00\x00\x00\x00\x00\x01\x00\x06\x89\xfb\ -\x00\x00\x0c\x16\x00\x00\x00\x00\x00\x01\x00\x06\x8c\x16\ -\x00\x00\x0c\x46\x00\x00\x00\x00\x00\x01\x00\x06\x8d\xeb\ -\x00\x00\x00\x5e\x00\x02\x00\x00\x00\x07\x00\x00\x00\x61\ -\x00\x00\x0c\x68\x00\x01\x00\x00\x00\x01\x00\x06\x98\x44\ -\x00\x00\x0c\x88\x00\x00\x00\x00\x00\x01\x00\x06\x98\xd6\ -\x00\x00\x0c\xa4\x00\x00\x00\x00\x00\x01\x00\x07\xd7\x7e\ -\x00\x00\x0c\xc2\x00\x00\x00\x00\x00\x01\x00\x07\xe5\x11\ -\x00\x00\x0c\xe2\x00\x00\x00\x00\x00\x01\x00\x0f\x8f\x8d\ -\x00\x00\x0c\xfc\x00\x00\x00\x00\x00\x01\x00\x0f\xe2\x1a\ -\x00\x00\x0d\x2c\x00\x00\x00\x00\x00\x01\x00\x19\xb7\x94\ +\x00\x00\x01\xdc\x00\x00\x00\x00\x00\x01\x00\x00\xde\xa3\ +\x00\x00\x01\xfa\x00\x00\x00\x00\x00\x01\x00\x00\xff\xcd\ +\x00\x00\x02\x38\x00\x00\x00\x00\x00\x01\x00\x01\x1f\xdf\ +\x00\x00\x00\x5e\x00\x02\x00\x00\x00\x01\x00\x00\x00\x18\ +\x00\x00\x02\x6c\x00\x02\x00\x00\x00\x48\x00\x00\x00\x19\ +\x00\x00\x02\x7c\x00\x00\x00\x00\x00\x01\x00\x01\x2e\xf7\ +\x00\x00\x02\x96\x00\x00\x00\x00\x00\x01\x00\x01\x53\x91\ +\x00\x00\x02\xbc\x00\x00\x00\x00\x00\x01\x00\x01\xa5\xdc\ +\x00\x00\x02\xd6\x00\x00\x00\x00\x00\x01\x00\x01\xb7\xf9\ +\x00\x00\x02\xf4\x00\x00\x00\x00\x00\x01\x00\x01\xc6\xc1\ +\x00\x00\x03\x2a\x00\x00\x00\x00\x00\x01\x00\x01\xca\x4f\ +\x00\x00\x03\x56\x00\x00\x00\x00\x00\x01\x00\x01\xed\x89\ +\x00\x00\x03\x70\x00\x00\x00\x00\x00\x01\x00\x01\xf5\x02\ +\x00\x00\x03\x98\x00\x00\x00\x00\x00\x01\x00\x01\xfc\x43\ +\x00\x00\x03\xbc\x00\x00\x00\x00\x00\x01\x00\x02\x04\x73\ +\x00\x00\x03\xe2\x00\x00\x00\x00\x00\x01\x00\x02\x37\x5c\ +\x00\x00\x04\x0e\x00\x00\x00\x00\x00\x01\x00\x02\x67\x03\ +\x00\x00\x04\x28\x00\x00\x00\x00\x00\x01\x00\x02\x6c\x1e\ +\x00\x00\x04\x48\x00\x00\x00\x00\x00\x01\x00\x02\x72\x9a\ +\x00\x00\x04\x76\x00\x00\x00\x00\x00\x01\x00\x02\xae\x57\ +\x00\x00\x04\xa0\x00\x00\x00\x00\x00\x01\x00\x02\xd5\x5f\ +\x00\x00\x04\xc0\x00\x00\x00\x00\x00\x01\x00\x02\xe1\x39\ +\x00\x00\x04\xfe\x00\x00\x00\x00\x00\x01\x00\x02\xe8\xd6\ +\x00\x00\x05\x14\x00\x00\x00\x00\x00\x01\x00\x03\x10\xf6\ +\x00\x00\x05\x2e\x00\x00\x00\x00\x00\x01\x00\x03\x22\xdb\ +\x00\x00\x05\x48\x00\x00\x00\x00\x00\x01\x00\x03\x4a\x78\ +\x00\x00\x05\x72\x00\x00\x00\x00\x00\x01\x00\x03\x72\x4a\ +\x00\x00\x05\x94\x00\x00\x00\x00\x00\x01\x00\x03\x77\x05\ +\x00\x00\x05\xca\x00\x00\x00\x00\x00\x01\x00\x03\x89\x81\ +\x00\x00\x05\xe2\x00\x00\x00\x00\x00\x01\x00\x03\xad\x71\ +\x00\x00\x05\xfe\x00\x00\x00\x00\x00\x01\x00\x03\xb2\x53\ +\x00\x00\x06\x36\x00\x00\x00\x00\x00\x01\x00\x03\xe0\x9a\ +\x00\x00\x06\x5a\x00\x00\x00\x00\x00\x01\x00\x03\xe5\x0c\ +\x00\x00\x06\x88\x00\x00\x00\x00\x00\x01\x00\x03\xe9\x46\ +\x00\x00\x06\xa6\x00\x00\x00\x00\x00\x01\x00\x03\xef\x17\ +\x00\x00\x06\xcc\x00\x00\x00\x00\x00\x01\x00\x03\xff\x92\ +\x00\x00\x06\xe2\x00\x00\x00\x00\x00\x01\x00\x04\x0a\x06\ +\x00\x00\x06\xf8\x00\x00\x00\x00\x00\x01\x00\x04\x1b\x2d\ +\x00\x00\x07\x0c\x00\x00\x00\x00\x00\x01\x00\x04\x3e\x75\ +\x00\x00\x07\x30\x00\x00\x00\x00\x00\x01\x00\x04\x42\x8e\ +\x00\x00\x07\x4e\x00\x00\x00\x00\x00\x01\x00\x04\x5d\x60\ +\x00\x00\x07\x70\x00\x00\x00\x00\x00\x01\x00\x04\x79\xc2\ +\x00\x00\x07\x88\x00\x00\x00\x00\x00\x01\x00\x04\x7e\xdc\ +\x00\x00\x07\xc6\x00\x00\x00\x00\x00\x01\x00\x04\x8c\x12\ +\x00\x00\x07\xe0\x00\x00\x00\x00\x00\x01\x00\x04\x95\x40\ +\x00\x00\x08\x06\x00\x00\x00\x00\x00\x01\x00\x04\xa4\xb2\ +\x00\x00\x08\x38\x00\x00\x00\x00\x00\x01\x00\x04\xbe\x77\ +\x00\x00\x08\x54\x00\x00\x00\x00\x00\x01\x00\x04\xc7\x55\ +\x00\x00\x08\x7e\x00\x00\x00\x00\x00\x01\x00\x04\xe8\x39\ +\x00\x00\x08\xa8\x00\x00\x00\x00\x00\x01\x00\x05\x05\x46\ +\x00\x00\x08\xbe\x00\x00\x00\x00\x00\x01\x00\x05\x08\x29\ +\x00\x00\x08\xde\x00\x00\x00\x00\x00\x01\x00\x05\x0b\x7a\ +\x00\x00\x08\xf8\x00\x00\x00\x00\x00\x01\x00\x05\x11\x6a\ +\x00\x00\x09\x1c\x00\x00\x00\x00\x00\x01\x00\x05\x23\x09\ +\x00\x00\x09\x34\x00\x00\x00\x00\x00\x01\x00\x05\x37\xd1\ +\x00\x00\x09\x66\x00\x00\x00\x00\x00\x01\x00\x05\x65\xa4\ +\x00\x00\x09\x92\x00\x00\x00\x00\x00\x01\x00\x05\x8a\xf8\ +\x00\x00\x09\xb0\x00\x00\x00\x00\x00\x01\x00\x05\x99\xc8\ +\x00\x00\x09\xd2\x00\x00\x00\x00\x00\x01\x00\x05\x9d\xc2\ +\x00\x00\x0a\x00\x00\x00\x00\x00\x00\x01\x00\x05\xb1\x57\ +\x00\x00\x0a\x32\x00\x00\x00\x00\x00\x01\x00\x05\xc9\x8d\ +\x00\x00\x0a\x62\x00\x00\x00\x00\x00\x01\x00\x06\x05\xd3\ +\x00\x00\x0a\x98\x00\x00\x00\x00\x00\x01\x00\x06\x0c\x9a\ +\x00\x00\x0a\xb4\x00\x00\x00\x00\x00\x01\x00\x06\x13\xe3\ +\x00\x00\x0a\xca\x00\x00\x00\x00\x00\x01\x00\x06\x18\x91\ +\x00\x00\x0a\xf2\x00\x00\x00\x00\x00\x01\x00\x06\x2b\x39\ +\x00\x00\x0b\x0c\x00\x00\x00\x00\x00\x01\x00\x06\x53\x7b\ +\x00\x00\x0b\x20\x00\x00\x00\x00\x00\x01\x00\x06\x5a\x59\ +\x00\x00\x0b\x38\x00\x00\x00\x00\x00\x01\x00\x06\x63\x8b\ +\x00\x00\x0b\x50\x00\x00\x00\x00\x00\x01\x00\x06\x70\x5d\ +\x00\x00\x0b\x6a\x00\x00\x00\x00\x00\x01\x00\x06\x77\x9b\ +\x00\x00\x0b\xa8\x00\x00\x00\x00\x00\x01\x00\x06\x8c\x5b\ +\x00\x00\x0b\xd4\x00\x00\x00\x00\x00\x01\x00\x06\x91\xa1\ +\x00\x00\x0c\x04\x00\x00\x00\x00\x00\x01\x00\x06\xa0\x74\ +\x00\x00\x0c\x2a\x00\x00\x00\x00\x00\x01\x00\x06\xbc\x80\ +\x00\x00\x0c\x56\x00\x00\x00\x00\x00\x01\x00\x06\xbe\x9b\ +\x00\x00\x0c\x86\x00\x00\x00\x00\x00\x01\x00\x06\xc0\x70\ +\x00\x00\x00\x5e\x00\x02\x00\x00\x00\x07\x00\x00\x00\x62\ +\x00\x00\x0c\xa8\x00\x01\x00\x00\x00\x01\x00\x06\xca\xc9\ +\x00\x00\x0c\xc8\x00\x00\x00\x00\x00\x01\x00\x06\xcb\x5b\ +\x00\x00\x0c\xe4\x00\x00\x00\x00\x00\x01\x00\x08\x0a\x03\ +\x00\x00\x0d\x02\x00\x00\x00\x00\x00\x01\x00\x08\x17\x96\ +\x00\x00\x0d\x22\x00\x00\x00\x00\x00\x01\x00\x0f\xc2\x12\ +\x00\x00\x0d\x3c\x00\x00\x00\x00\x00\x01\x00\x10\x14\x9f\ +\x00\x00\x0d\x6c\x00\x00\x00\x00\x00\x01\x00\x19\xea\x19\ " qt_resource_struct_v2 = b"\ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x04\x00\x00\x00\x01\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x60\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x61\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x0e\x00\x02\x00\x00\x00\x01\x00\x00\x00\x16\ +\x00\x00\x00\x0e\x00\x02\x00\x00\x00\x01\x00\x00\x00\x17\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x00\x1e\x00\x02\x00\x00\x00\x01\x00\x00\x00\x0f\ \x00\x00\x00\x00\x00\x00\x00\x00\ @@ -106533,181 +107349,183 @@ \x00\x00\x01\x94\x6b\x3d\xb9\x6d\ \x00\x00\x00\x5e\x00\x02\x00\x00\x00\x01\x00\x00\x00\x10\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\x44\x00\x02\x00\x00\x00\x05\x00\x00\x00\x11\ +\x00\x00\x01\x44\x00\x02\x00\x00\x00\x06\x00\x00\x00\x11\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\x54\x00\x00\x00\x00\x00\x01\x00\x00\x19\x27\ \x00\x00\x01\x94\x6b\x3d\xb9\x2e\ \x00\x00\x01\x7c\x00\x00\x00\x00\x00\x01\x00\x00\x5f\x9c\ \x00\x00\x01\x94\x6b\x3d\xb9\x76\ \x00\x00\x01\x9c\x00\x00\x00\x00\x00\x01\x00\x00\xab\x63\ +\x00\x00\x01\x95\x76\x0d\x50\xf3\ +\x00\x00\x01\xdc\x00\x00\x00\x00\x00\x01\x00\x00\xde\xa3\ \x00\x00\x01\x94\x6b\x3d\xb9\x58\ -\x00\x00\x01\xba\x00\x00\x00\x00\x00\x01\x00\x00\xcc\x8d\ -\x00\x00\x01\x95\x0e\xcf\x8b\xfa\ -\x00\x00\x01\xf8\x00\x00\x00\x00\x00\x01\x00\x00\xed\x5a\ +\x00\x00\x01\xfa\x00\x00\x00\x00\x00\x01\x00\x00\xff\xcd\ +\x00\x00\x01\x95\x17\xc9\xee\xec\ +\x00\x00\x02\x38\x00\x00\x00\x00\x00\x01\x00\x01\x1f\xdf\ \x00\x00\x01\x95\x0e\xc8\x96\x09\ -\x00\x00\x00\x5e\x00\x02\x00\x00\x00\x01\x00\x00\x00\x17\ +\x00\x00\x00\x5e\x00\x02\x00\x00\x00\x01\x00\x00\x00\x18\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x02\x2c\x00\x02\x00\x00\x00\x48\x00\x00\x00\x18\ +\x00\x00\x02\x6c\x00\x02\x00\x00\x00\x48\x00\x00\x00\x19\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x02\x3c\x00\x00\x00\x00\x00\x01\x00\x00\xfc\x72\ +\x00\x00\x02\x7c\x00\x00\x00\x00\x00\x01\x00\x01\x2e\xf7\ \x00\x00\x01\x94\x6b\x3d\xb9\x2f\ -\x00\x00\x02\x56\x00\x00\x00\x00\x00\x01\x00\x01\x21\x0c\ +\x00\x00\x02\x96\x00\x00\x00\x00\x00\x01\x00\x01\x53\x91\ \x00\x00\x01\x94\x6b\x3d\xb9\x3a\ -\x00\x00\x02\x7c\x00\x00\x00\x00\x00\x01\x00\x01\x73\x57\ +\x00\x00\x02\xbc\x00\x00\x00\x00\x00\x01\x00\x01\xa5\xdc\ \x00\x00\x01\x94\x6b\x3d\xb9\x30\ -\x00\x00\x02\x96\x00\x00\x00\x00\x00\x01\x00\x01\x85\x74\ +\x00\x00\x02\xd6\x00\x00\x00\x00\x00\x01\x00\x01\xb7\xf9\ \x00\x00\x01\x94\x6b\x3d\xb9\x29\ -\x00\x00\x02\xb4\x00\x00\x00\x00\x00\x01\x00\x01\x94\x3c\ +\x00\x00\x02\xf4\x00\x00\x00\x00\x00\x01\x00\x01\xc6\xc1\ \x00\x00\x01\x94\x6b\x3d\xb9\x5b\ -\x00\x00\x02\xea\x00\x00\x00\x00\x00\x01\x00\x01\x97\xca\ +\x00\x00\x03\x2a\x00\x00\x00\x00\x00\x01\x00\x01\xca\x4f\ \x00\x00\x01\x94\x6b\x3d\xb9\x2f\ -\x00\x00\x03\x16\x00\x00\x00\x00\x00\x01\x00\x01\xbb\x04\ +\x00\x00\x03\x56\x00\x00\x00\x00\x00\x01\x00\x01\xed\x89\ \x00\x00\x01\x94\x6b\x3d\xb9\x74\ -\x00\x00\x03\x30\x00\x00\x00\x00\x00\x01\x00\x01\xc2\x7d\ +\x00\x00\x03\x70\x00\x00\x00\x00\x00\x01\x00\x01\xf5\x02\ \x00\x00\x01\x94\x6b\x3d\xb9\x7b\ -\x00\x00\x03\x58\x00\x00\x00\x00\x00\x01\x00\x01\xc9\xbe\ +\x00\x00\x03\x98\x00\x00\x00\x00\x00\x01\x00\x01\xfc\x43\ \x00\x00\x01\x94\x6b\x3d\xb9\x75\ -\x00\x00\x03\x7c\x00\x00\x00\x00\x00\x01\x00\x01\xd1\xee\ +\x00\x00\x03\xbc\x00\x00\x00\x00\x00\x01\x00\x02\x04\x73\ \x00\x00\x01\x94\x6b\x3d\xb9\x73\ -\x00\x00\x03\xa2\x00\x00\x00\x00\x00\x01\x00\x02\x04\xd7\ +\x00\x00\x03\xe2\x00\x00\x00\x00\x00\x01\x00\x02\x37\x5c\ \x00\x00\x01\x94\x6b\x3d\xb9\x4e\ -\x00\x00\x03\xce\x00\x00\x00\x00\x00\x01\x00\x02\x34\x7e\ +\x00\x00\x04\x0e\x00\x00\x00\x00\x00\x01\x00\x02\x67\x03\ \x00\x00\x01\x94\x6b\x3d\xb9\x7d\ -\x00\x00\x03\xe8\x00\x00\x00\x00\x00\x01\x00\x02\x39\x99\ +\x00\x00\x04\x28\x00\x00\x00\x00\x00\x01\x00\x02\x6c\x1e\ \x00\x00\x01\x94\x6b\x3d\xb9\x71\ -\x00\x00\x04\x08\x00\x00\x00\x00\x00\x01\x00\x02\x40\x15\ +\x00\x00\x04\x48\x00\x00\x00\x00\x00\x01\x00\x02\x72\x9a\ \x00\x00\x01\x94\x6b\x3d\xb9\x58\ -\x00\x00\x04\x36\x00\x00\x00\x00\x00\x01\x00\x02\x7b\xd2\ +\x00\x00\x04\x76\x00\x00\x00\x00\x00\x01\x00\x02\xae\x57\ \x00\x00\x01\x94\x6b\x3d\xb9\x38\ -\x00\x00\x04\x60\x00\x00\x00\x00\x00\x01\x00\x02\xa2\xda\ +\x00\x00\x04\xa0\x00\x00\x00\x00\x00\x01\x00\x02\xd5\x5f\ \x00\x00\x01\x94\x6b\x3d\xb9\x6d\ -\x00\x00\x04\x80\x00\x00\x00\x00\x00\x01\x00\x02\xae\xb4\ +\x00\x00\x04\xc0\x00\x00\x00\x00\x00\x01\x00\x02\xe1\x39\ \x00\x00\x01\x94\x6b\x3d\xb9\x50\ -\x00\x00\x04\xbe\x00\x00\x00\x00\x00\x01\x00\x02\xb6\x51\ +\x00\x00\x04\xfe\x00\x00\x00\x00\x00\x01\x00\x02\xe8\xd6\ \x00\x00\x01\x94\x6b\x3d\xb9\x5f\ -\x00\x00\x04\xd4\x00\x00\x00\x00\x00\x01\x00\x02\xde\x71\ +\x00\x00\x05\x14\x00\x00\x00\x00\x00\x01\x00\x03\x10\xf6\ \x00\x00\x01\x94\x6b\x3d\xb9\x6f\ -\x00\x00\x04\xee\x00\x00\x00\x00\x00\x01\x00\x02\xf0\x56\ +\x00\x00\x05\x2e\x00\x00\x00\x00\x00\x01\x00\x03\x22\xdb\ \x00\x00\x01\x94\x6b\x3d\xb9\x76\ -\x00\x00\x05\x08\x00\x00\x00\x00\x00\x01\x00\x03\x17\xf3\ +\x00\x00\x05\x48\x00\x00\x00\x00\x00\x01\x00\x03\x4a\x78\ \x00\x00\x01\x94\x6b\x3d\xb9\x4e\ -\x00\x00\x05\x32\x00\x00\x00\x00\x00\x01\x00\x03\x3f\xc5\ +\x00\x00\x05\x72\x00\x00\x00\x00\x00\x01\x00\x03\x72\x4a\ \x00\x00\x01\x94\x6b\x3d\xb9\x4e\ -\x00\x00\x05\x54\x00\x00\x00\x00\x00\x01\x00\x03\x44\x80\ +\x00\x00\x05\x94\x00\x00\x00\x00\x00\x01\x00\x03\x77\x05\ \x00\x00\x01\x94\x6b\x3d\xb9\x75\ -\x00\x00\x05\x8a\x00\x00\x00\x00\x00\x01\x00\x03\x56\xfc\ +\x00\x00\x05\xca\x00\x00\x00\x00\x00\x01\x00\x03\x89\x81\ \x00\x00\x01\x94\x6b\x3d\xb9\x37\ -\x00\x00\x05\xa2\x00\x00\x00\x00\x00\x01\x00\x03\x7a\xec\ +\x00\x00\x05\xe2\x00\x00\x00\x00\x00\x01\x00\x03\xad\x71\ \x00\x00\x01\x94\x6b\x3d\xb9\x6c\ -\x00\x00\x05\xbe\x00\x00\x00\x00\x00\x01\x00\x03\x7f\xce\ +\x00\x00\x05\xfe\x00\x00\x00\x00\x00\x01\x00\x03\xb2\x53\ \x00\x00\x01\x94\x6b\x3d\xb7\xf7\ -\x00\x00\x05\xf6\x00\x00\x00\x00\x00\x01\x00\x03\xae\x15\ +\x00\x00\x06\x36\x00\x00\x00\x00\x00\x01\x00\x03\xe0\x9a\ \x00\x00\x01\x94\x6b\x3d\xb9\x5e\ -\x00\x00\x06\x1a\x00\x00\x00\x00\x00\x01\x00\x03\xb2\x87\ +\x00\x00\x06\x5a\x00\x00\x00\x00\x00\x01\x00\x03\xe5\x0c\ \x00\x00\x01\x94\x6b\x3d\xb9\x55\ -\x00\x00\x06\x48\x00\x00\x00\x00\x00\x01\x00\x03\xb6\xc1\ +\x00\x00\x06\x88\x00\x00\x00\x00\x00\x01\x00\x03\xe9\x46\ \x00\x00\x01\x94\x6b\x3d\xb9\x52\ -\x00\x00\x06\x66\x00\x00\x00\x00\x00\x01\x00\x03\xbc\x92\ +\x00\x00\x06\xa6\x00\x00\x00\x00\x00\x01\x00\x03\xef\x17\ \x00\x00\x01\x94\x6b\x3d\xb9\x6d\ -\x00\x00\x06\x8c\x00\x00\x00\x00\x00\x01\x00\x03\xcd\x0d\ +\x00\x00\x06\xcc\x00\x00\x00\x00\x00\x01\x00\x03\xff\x92\ \x00\x00\x01\x94\x6b\x3d\xb9\x4d\ -\x00\x00\x06\xa2\x00\x00\x00\x00\x00\x01\x00\x03\xd7\x81\ +\x00\x00\x06\xe2\x00\x00\x00\x00\x00\x01\x00\x04\x0a\x06\ \x00\x00\x01\x94\x6b\x3d\xb9\x2f\ -\x00\x00\x06\xb8\x00\x00\x00\x00\x00\x01\x00\x03\xe8\xa8\ +\x00\x00\x06\xf8\x00\x00\x00\x00\x00\x01\x00\x04\x1b\x2d\ \x00\x00\x01\x94\x6b\x3d\xb9\x48\ -\x00\x00\x06\xcc\x00\x00\x00\x00\x00\x01\x00\x04\x0b\xf0\ +\x00\x00\x07\x0c\x00\x00\x00\x00\x00\x01\x00\x04\x3e\x75\ \x00\x00\x01\x94\x6b\x3d\xb7\xf7\ -\x00\x00\x06\xf0\x00\x00\x00\x00\x00\x01\x00\x04\x10\x09\ +\x00\x00\x07\x30\x00\x00\x00\x00\x00\x01\x00\x04\x42\x8e\ \x00\x00\x01\x94\x6b\x3d\xb9\x51\ -\x00\x00\x07\x0e\x00\x00\x00\x00\x00\x01\x00\x04\x2a\xdb\ +\x00\x00\x07\x4e\x00\x00\x00\x00\x00\x01\x00\x04\x5d\x60\ \x00\x00\x01\x94\x6b\x3d\xb9\x52\ -\x00\x00\x07\x30\x00\x00\x00\x00\x00\x01\x00\x04\x47\x3d\ +\x00\x00\x07\x70\x00\x00\x00\x00\x00\x01\x00\x04\x79\xc2\ \x00\x00\x01\x94\x6b\x3d\xb9\x54\ -\x00\x00\x07\x48\x00\x00\x00\x00\x00\x01\x00\x04\x4c\x57\ +\x00\x00\x07\x88\x00\x00\x00\x00\x00\x01\x00\x04\x7e\xdc\ \x00\x00\x01\x94\x6b\x3d\xb9\x7c\ -\x00\x00\x07\x86\x00\x00\x00\x00\x00\x01\x00\x04\x59\x8d\ +\x00\x00\x07\xc6\x00\x00\x00\x00\x00\x01\x00\x04\x8c\x12\ \x00\x00\x01\x94\x6b\x3d\xb9\x4a\ -\x00\x00\x07\xa0\x00\x00\x00\x00\x00\x01\x00\x04\x62\xbb\ +\x00\x00\x07\xe0\x00\x00\x00\x00\x00\x01\x00\x04\x95\x40\ \x00\x00\x01\x94\x6b\x3d\xb9\x32\ -\x00\x00\x07\xc6\x00\x00\x00\x00\x00\x01\x00\x04\x72\x2d\ +\x00\x00\x08\x06\x00\x00\x00\x00\x00\x01\x00\x04\xa4\xb2\ \x00\x00\x01\x94\x6b\x3d\xb9\x30\ -\x00\x00\x07\xf8\x00\x00\x00\x00\x00\x01\x00\x04\x8b\xf2\ +\x00\x00\x08\x38\x00\x00\x00\x00\x00\x01\x00\x04\xbe\x77\ \x00\x00\x01\x94\x6b\x3d\xb9\x6e\ -\x00\x00\x08\x14\x00\x00\x00\x00\x00\x01\x00\x04\x94\xd0\ +\x00\x00\x08\x54\x00\x00\x00\x00\x00\x01\x00\x04\xc7\x55\ \x00\x00\x01\x94\x6b\x3d\xb9\x32\ -\x00\x00\x08\x3e\x00\x00\x00\x00\x00\x01\x00\x04\xb5\xb4\ +\x00\x00\x08\x7e\x00\x00\x00\x00\x00\x01\x00\x04\xe8\x39\ \x00\x00\x01\x94\x6b\x3d\xb9\x33\ -\x00\x00\x08\x68\x00\x00\x00\x00\x00\x01\x00\x04\xd2\xc1\ +\x00\x00\x08\xa8\x00\x00\x00\x00\x00\x01\x00\x05\x05\x46\ \x00\x00\x01\x94\x6b\x3d\xb9\x5d\ -\x00\x00\x08\x7e\x00\x00\x00\x00\x00\x01\x00\x04\xd5\xa4\ +\x00\x00\x08\xbe\x00\x00\x00\x00\x00\x01\x00\x05\x08\x29\ \x00\x00\x01\x94\x6b\x3d\xb9\x4e\ -\x00\x00\x08\x9e\x00\x00\x00\x00\x00\x01\x00\x04\xd8\xf5\ +\x00\x00\x08\xde\x00\x00\x00\x00\x00\x01\x00\x05\x0b\x7a\ \x00\x00\x01\x94\x6b\x3d\xb9\x58\ -\x00\x00\x08\xb8\x00\x00\x00\x00\x00\x01\x00\x04\xde\xe5\ +\x00\x00\x08\xf8\x00\x00\x00\x00\x00\x01\x00\x05\x11\x6a\ \x00\x00\x01\x94\x6b\x3d\xb9\x6e\ -\x00\x00\x08\xdc\x00\x00\x00\x00\x00\x01\x00\x04\xf0\x84\ +\x00\x00\x09\x1c\x00\x00\x00\x00\x00\x01\x00\x05\x23\x09\ \x00\x00\x01\x94\x6b\x3d\xb9\x6c\ -\x00\x00\x08\xf4\x00\x00\x00\x00\x00\x01\x00\x05\x05\x4c\ +\x00\x00\x09\x34\x00\x00\x00\x00\x00\x01\x00\x05\x37\xd1\ \x00\x00\x01\x94\x6b\x3d\xb7\xf7\ -\x00\x00\x09\x26\x00\x00\x00\x00\x00\x01\x00\x05\x33\x1f\ +\x00\x00\x09\x66\x00\x00\x00\x00\x00\x01\x00\x05\x65\xa4\ \x00\x00\x01\x94\x6b\x3d\xb9\x4e\ -\x00\x00\x09\x52\x00\x00\x00\x00\x00\x01\x00\x05\x58\x73\ +\x00\x00\x09\x92\x00\x00\x00\x00\x00\x01\x00\x05\x8a\xf8\ \x00\x00\x01\x94\x6b\x3d\xb9\x71\ -\x00\x00\x09\x70\x00\x00\x00\x00\x00\x01\x00\x05\x67\x43\ +\x00\x00\x09\xb0\x00\x00\x00\x00\x00\x01\x00\x05\x99\xc8\ \x00\x00\x01\x94\x6b\x3d\xb9\x6b\ -\x00\x00\x09\x92\x00\x00\x00\x00\x00\x01\x00\x05\x6b\x3d\ +\x00\x00\x09\xd2\x00\x00\x00\x00\x00\x01\x00\x05\x9d\xc2\ \x00\x00\x01\x94\x6b\x3d\xb9\x39\ -\x00\x00\x09\xc0\x00\x00\x00\x00\x00\x01\x00\x05\x7e\xd2\ +\x00\x00\x0a\x00\x00\x00\x00\x00\x00\x01\x00\x05\xb1\x57\ \x00\x00\x01\x94\x6b\x3d\xb9\x39\ -\x00\x00\x09\xf2\x00\x00\x00\x00\x00\x01\x00\x05\x97\x08\ +\x00\x00\x0a\x32\x00\x00\x00\x00\x00\x01\x00\x05\xc9\x8d\ \x00\x00\x01\x94\x6b\x3d\xb9\x57\ -\x00\x00\x0a\x22\x00\x00\x00\x00\x00\x01\x00\x05\xd3\x4e\ +\x00\x00\x0a\x62\x00\x00\x00\x00\x00\x01\x00\x06\x05\xd3\ \x00\x00\x01\x94\x6b\x3d\xb9\x57\ -\x00\x00\x0a\x58\x00\x00\x00\x00\x00\x01\x00\x05\xda\x15\ +\x00\x00\x0a\x98\x00\x00\x00\x00\x00\x01\x00\x06\x0c\x9a\ \x00\x00\x01\x94\x6b\x3d\xb9\x75\ -\x00\x00\x0a\x74\x00\x00\x00\x00\x00\x01\x00\x05\xe1\x5e\ +\x00\x00\x0a\xb4\x00\x00\x00\x00\x00\x01\x00\x06\x13\xe3\ \x00\x00\x01\x94\x6b\x3d\xb9\x6c\ -\x00\x00\x0a\x8a\x00\x00\x00\x00\x00\x01\x00\x05\xe6\x0c\ +\x00\x00\x0a\xca\x00\x00\x00\x00\x00\x01\x00\x06\x18\x91\ \x00\x00\x01\x94\x6b\x3d\xb9\x30\ -\x00\x00\x0a\xb2\x00\x00\x00\x00\x00\x01\x00\x05\xf8\xb4\ +\x00\x00\x0a\xf2\x00\x00\x00\x00\x00\x01\x00\x06\x2b\x39\ \x00\x00\x01\x94\x6b\x3d\xb9\x49\ -\x00\x00\x0a\xcc\x00\x00\x00\x00\x00\x01\x00\x06\x20\xf6\ +\x00\x00\x0b\x0c\x00\x00\x00\x00\x00\x01\x00\x06\x53\x7b\ \x00\x00\x01\x94\x6b\x3d\xb9\x7c\ -\x00\x00\x0a\xe0\x00\x00\x00\x00\x00\x01\x00\x06\x27\xd4\ +\x00\x00\x0b\x20\x00\x00\x00\x00\x00\x01\x00\x06\x5a\x59\ \x00\x00\x01\x94\x6b\x3d\xb9\x5c\ -\x00\x00\x0a\xf8\x00\x00\x00\x00\x00\x01\x00\x06\x31\x06\ +\x00\x00\x0b\x38\x00\x00\x00\x00\x00\x01\x00\x06\x63\x8b\ \x00\x00\x01\x94\x6b\x3d\xb9\x6d\ -\x00\x00\x0b\x10\x00\x00\x00\x00\x00\x01\x00\x06\x3d\xd8\ +\x00\x00\x0b\x50\x00\x00\x00\x00\x00\x01\x00\x06\x70\x5d\ \x00\x00\x01\x94\x6b\x3d\xb9\x50\ -\x00\x00\x0b\x2a\x00\x00\x00\x00\x00\x01\x00\x06\x45\x16\ +\x00\x00\x0b\x6a\x00\x00\x00\x00\x00\x01\x00\x06\x77\x9b\ \x00\x00\x01\x94\x6b\x3d\xb9\x56\ -\x00\x00\x0b\x68\x00\x00\x00\x00\x00\x01\x00\x06\x59\xd6\ +\x00\x00\x0b\xa8\x00\x00\x00\x00\x00\x01\x00\x06\x8c\x5b\ \x00\x00\x01\x94\x6b\x3d\xb9\x7e\ -\x00\x00\x0b\x94\x00\x00\x00\x00\x00\x01\x00\x06\x5f\x1c\ +\x00\x00\x0b\xd4\x00\x00\x00\x00\x00\x01\x00\x06\x91\xa1\ \x00\x00\x01\x94\x6b\x3d\xb9\x29\ -\x00\x00\x0b\xc4\x00\x00\x00\x00\x00\x01\x00\x06\x6d\xef\ +\x00\x00\x0c\x04\x00\x00\x00\x00\x00\x01\x00\x06\xa0\x74\ \x00\x00\x01\x94\x6b\x3d\xb9\x2f\ -\x00\x00\x0b\xea\x00\x00\x00\x00\x00\x01\x00\x06\x89\xfb\ +\x00\x00\x0c\x2a\x00\x00\x00\x00\x00\x01\x00\x06\xbc\x80\ \x00\x00\x01\x94\x6b\x3d\xb9\x48\ -\x00\x00\x0c\x16\x00\x00\x00\x00\x00\x01\x00\x06\x8c\x16\ +\x00\x00\x0c\x56\x00\x00\x00\x00\x00\x01\x00\x06\xbe\x9b\ \x00\x00\x01\x94\x6b\x3d\xb9\x7c\ -\x00\x00\x0c\x46\x00\x00\x00\x00\x00\x01\x00\x06\x8d\xeb\ +\x00\x00\x0c\x86\x00\x00\x00\x00\x00\x01\x00\x06\xc0\x70\ \x00\x00\x01\x94\x6b\x3d\xb9\x70\ -\x00\x00\x00\x5e\x00\x02\x00\x00\x00\x07\x00\x00\x00\x61\ +\x00\x00\x00\x5e\x00\x02\x00\x00\x00\x07\x00\x00\x00\x62\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x0c\x68\x00\x01\x00\x00\x00\x01\x00\x06\x98\x44\ +\x00\x00\x0c\xa8\x00\x01\x00\x00\x00\x01\x00\x06\xca\xc9\ \x00\x00\x01\x94\x6b\x3d\xb9\x37\ -\x00\x00\x0c\x88\x00\x00\x00\x00\x00\x01\x00\x06\x98\xd6\ +\x00\x00\x0c\xc8\x00\x00\x00\x00\x00\x01\x00\x06\xcb\x5b\ \x00\x00\x01\x94\x6b\x3d\xb9\x68\ -\x00\x00\x0c\xa4\x00\x00\x00\x00\x00\x01\x00\x07\xd7\x7e\ +\x00\x00\x0c\xe4\x00\x00\x00\x00\x00\x01\x00\x08\x0a\x03\ \x00\x00\x01\x94\x6b\x3d\xb9\x73\ -\x00\x00\x0c\xc2\x00\x00\x00\x00\x00\x01\x00\x07\xe5\x11\ +\x00\x00\x0d\x02\x00\x00\x00\x00\x00\x01\x00\x08\x17\x96\ \x00\x00\x01\x94\x6b\x3d\xb9\x37\ -\x00\x00\x0c\xe2\x00\x00\x00\x00\x00\x01\x00\x0f\x8f\x8d\ +\x00\x00\x0d\x22\x00\x00\x00\x00\x00\x01\x00\x0f\xc2\x12\ \x00\x00\x01\x94\x6b\x3d\xb9\x72\ -\x00\x00\x0c\xfc\x00\x00\x00\x00\x00\x01\x00\x0f\xe2\x1a\ +\x00\x00\x0d\x3c\x00\x00\x00\x00\x00\x01\x00\x10\x14\x9f\ \x00\x00\x01\x94\x6b\x3d\xb9\x66\ -\x00\x00\x0d\x2c\x00\x00\x00\x00\x00\x01\x00\x19\xb7\x94\ +\x00\x00\x0d\x6c\x00\x00\x00\x00\x00\x01\x00\x19\xea\x19\ \x00\x00\x01\x94\x6b\x3d\xb9\x60\ " diff --git a/src/ui/settings_screen/README.md b/src/ui/settings_screen/README.md deleted file mode 100644 index aedec14d..00000000 --- a/src/ui/settings_screen/README.md +++ /dev/null @@ -1,64 +0,0 @@ -# Settings Screen - -This directory contains the settings screen widgets for the application. Each settings screen widget is dynamically loaded and integrated into the main settings screen. - -## Structure - -Each settings screen widget should be placed in its own subfolder within the `settings_screen` directory. The subfolder should contain the following files: - -- `.ui`: The UI file for the widget, created using Qt Designer. -- `.py`: The backend Python file for the widget. - -The naming convention for the files should follow the pattern `.ui` and `.py`, where `` is the name of the widget. - -## Example - -For a widget named `example_widget`, the structure should be as follows: - -settings_screen/ example_widget/ example_widget.ui example_widget.py - -### example_widget.ui - -The `.ui` file should define the layout and components of the widget using Qt Designer. - -### example_widget.py - -The `.py` file should define the backend logic for the widget. The class name in the `.py` file should match the widget name in title case with underscores removed. For example, the class name for `example_widget.py` should be `ExampleWidget`. - -```python -from PyQt5 import uic -from PyQt5.QtWidgets import QWidget - -class ExampleWidget(QWidget): - def __init__(self, parent, settings_screen): - super(ExampleWidget, self).__init__(parent) - self.mainSettingsWidget = settings_screen # Reference to the main settings widget - uic.loadUi('src/ui/settings_screen/example_widget/example_widget.ui', self) - - # Example of connecting a button to a function - self.exampleButton = self.findChild(QPushButton, 'exampleButton') - self.exampleButton.clicked.connect(self.example_function) - - def example_function(self): - # Placeholder for example function logic - print("Example button clicked") - - def go_back(self): - # Logic to go back to the settings screen - print("Back button clicked") - self.mainSettingsWidget.stackedWidget.setCurrentWidget(self.mainSettingsWidget.mainSettingsPage) -``` - - - -### Adding a New Settings Widget -To add a new settings widget, follow these steps: - -1. Create a new subfolder in the settings_screen directory with the name of the widget. -2. Create the .ui file using Qt Designer and save it in the subfolder. -3. Create the .py file with the backend logic and save it in the subfolder. -4. Ensure the class name in the .py file matches the widget name in title case with underscores removed. -5. The new widget will be dynamically loaded and integrated into the main settings screen. - - -By following this structure, you can easily add new settings widgets to the application and ensure they are dynamically loaded and integrated into the main settings screen. \ No newline at end of file diff --git a/src/ui/settings_screen/__pycache__/settings_screen.cpython-313.pyc b/src/ui/settings_screen/__pycache__/settings_screen.cpython-313.pyc deleted file mode 100644 index b5986b50..00000000 Binary files a/src/ui/settings_screen/__pycache__/settings_screen.cpython-313.pyc and /dev/null differ diff --git a/src/ui/settings_screen/network_settings/__pycache__/network_settings.cpython-313.pyc b/src/ui/settings_screen/network_settings/__pycache__/network_settings.cpython-313.pyc deleted file mode 100644 index 0c135185..00000000 Binary files a/src/ui/settings_screen/network_settings/__pycache__/network_settings.cpython-313.pyc and /dev/null differ diff --git a/src/ui/settings_screen/network_settings/network_settings.py b/src/ui/settings_screen/network_settings/network_settings.py deleted file mode 100644 index ff40ed5a..00000000 --- a/src/ui/settings_screen/network_settings/network_settings.py +++ /dev/null @@ -1,83 +0,0 @@ -from PyQt5 import uic -from PyQt5.QtWidgets import QWidget, QPushButton, QStackedWidget - -class NetworkSettings(QWidget): - def __init__(self, parent, settings_screen): - super(NetworkSettings, self).__init__(parent) - self.mainSettingsWidget = settings_screen # Reference to the main settings widget - uic.loadUi('src/ui/settings_screen/network_settings/network_settings.ui', self) - - # Find buttons by their object names - self.staticIPSettingsCancelButton = self.findChild(QPushButton, 'staticIPSettingsCancelButton') - self.staticIPGatewayKeyboardButton = self.findChild(QPushButton, 'staticIPGatewayKeyboardButton') - self.staticIPKeyboardButton = self.findChild(QPushButton, 'staticIPKeyboardButton') - self.staticIPNameServerKeyboardButton = self.findChild(QPushButton, 'staticIPNameServerKeyboardButton') - self.deleteStaticIPSettingsButton = self.findChild(QPushButton, 'deleteStaticIPSettingsButton') - self.staticIPSettingsDoneButton = self.findChild(QPushButton, 'staticIPSettingsDoneButton') - self.wifiSettingsCancelButton = self.findChild(QPushButton, 'wifiSettingsCancelButton') - self.wifiSettingsDoneButton = self.findChild(QPushButton, 'wifiSettingsDoneButton') - self.wifiSettingsSSIDKeyboardButton = self.findChild(QPushButton, 'wifiSettingsSSIDKeyboardButton') - self.networkInfoButton = self.findChild(QPushButton, 'networkInfoButton') - self.configureStaticIPButton = self.findChild(QPushButton, 'configureStaticIPButton') - self.networkSettingsBackButton = self.findChild(QPushButton, 'networkSettingsBackButton') - self.configureWifiButton = self.findChild(QPushButton, 'configureWifiButton') - self.networkInfoBackButton = self.findChild(QPushButton, 'networkInfoBackButton') - - # Find pages by their object names - self.stackedWidget = self.findChild(QStackedWidget, 'stackedWidget') - self.networkSettingsPage = self.findChild(QWidget, 'networkSettingsPage') - self.staticIPSettingsPage = self.findChild(QWidget, 'staticIPSettingsPage') - self.wifiSettingsPage = self.findChild(QWidget, 'wifiSettingsPage') - self.networkInfoPage = self.findChild(QWidget, 'networkInfoPage') - - # Check if buttons and pages are found - if not all([self.staticIPSettingsCancelButton, self.staticIPGatewayKeyboardButton, self.staticIPKeyboardButton, self.staticIPNameServerKeyboardButton, self.deleteStaticIPSettingsButton, self.staticIPSettingsDoneButton, self.wifiSettingsCancelButton, self.wifiSettingsDoneButton, self.wifiSettingsSSIDKeyboardButton, self.networkInfoButton, self.configureStaticIPButton, self.networkSettingsBackButton, self.configureWifiButton, self.networkInfoBackButton, self.stackedWidget, self.networkSettingsPage, self.staticIPSettingsPage, self.wifiSettingsPage, self.networkInfoPage]): - raise ValueError("One or more buttons or pages not found in the UI file") - - # Connect buttons to their respective functions - self.staticIPSettingsCancelButton.clicked.connect(self.cancel_network_settings) - self.staticIPSettingsDoneButton.clicked.connect(self.save_network_settings) - self.wifiSettingsCancelButton.clicked.connect(self.cancel_network_settings) - self.wifiSettingsDoneButton.clicked.connect(self.save_network_settings) - self.networkSettingsBackButton.clicked.connect(self.go_back_to_settings_screen) - self.networkInfoBackButton.clicked.connect(self.go_back) - self.networkInfoButton.clicked.connect(self.show_network_info) - self.configureStaticIPButton.clicked.connect(self.show_static_ip_settings) - self.configureWifiButton.clicked.connect(self.show_wifi_settings) - - # Set the default screen to networkSettingsPage - self.stackedWidget.setCurrentWidget(self.networkSettingsPage) - - def save_network_settings(self): - # Placeholder for save network settings logic - print("Save Network Settings button clicked") - - def cancel_network_settings(self): - # Placeholder for cancel network settings logic - print("Cancel Network Settings button clicked") - self.stackedWidget.setCurrentWidget(self.networkSettingsPage) - - def go_back_to_settings_screen(self): - # Logic to go back to the settings screen - print("Back to settings screen button clicked") - self.mainSettingsWidget.stackedWidget.setCurrentWidget(self.mainSettingsWidget.mainSettingsPage) - - def go_back(self): - # Logic to go back to the network settings page - print("Back to network settings page button clicked") - self.stackedWidget.setCurrentWidget(self.networkSettingsPage) - - def show_network_info(self): - # Logic to switch to the networkInfoPage - print("Network Info button clicked") - self.stackedWidget.setCurrentWidget(self.networkInfoPage) - - def show_static_ip_settings(self): - # Logic to switch to the staticIPSettingsPage - print("Static IP Settings button clicked") - self.stackedWidget.setCurrentWidget(self.staticIPSettingsPage) - - def show_wifi_settings(self): - # Logic to switch to the wifiSettingsPage - print("WiFi Settings button clicked") - self.stackedWidget.setCurrentWidget(self.wifiSettingsPage) diff --git a/src/ui/settings_screen/network_settings/network_settings.ui b/src/ui/settings_screen/network_settings/network_settings.ui deleted file mode 100644 index dd614a8a..00000000 --- a/src/ui/settings_screen/network_settings/network_settings.ui +++ /dev/null @@ -1,1638 +0,0 @@ - - - network_settings - - - - 0 - 0 - 800 - 480 - - - - Form - - - - - 0 - 0 - 800 - 480 - - - - background-color: rgb(40, 40, 40); - - - 3 - - - - - - 0 - 0 - 801 - 100 - - - - - 0 - 70 - - - - - Gotham - 16 - - - - QPushButton { - border: 1px solid rgb(87, 87, 87); - background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); - - -} - -QPushButton:pressed { - background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, - stop: 0 #dadbde, stop: 1 #f6f7fa); -} - -QPushButton:flat { - border: none; /* no border for a flat push button */ -} - -QPushButton:default { - border-color: navy; /* make the default button prominent */ -} - -QPushButton:focus { - outline: none; -} - - - Network Info - - - - 40 - 40 - - - - - - - 0 - 200 - 801 - 100 - - - - - 0 - 70 - - - - - Gotham - 16 - - - - QPushButton { - border: 1px solid rgb(87, 87, 87); - background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); - - -} - -QPushButton:pressed { - background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, - stop: 0 #dadbde, stop: 1 #f6f7fa); -} - -QPushButton:flat { - border: none; /* no border for a flat push button */ -} - -QPushButton:default { - border-color: navy; /* make the default button prominent */ -} - -QPushButton:focus { - outline: none; -} - - - Configure Static IP - - - - 40 - 40 - - - - - - - 0 - 360 - 801 - 121 - - - - - 0 - 0 - - - - - Gotham - 16 - - - - QPushButton { - border: 1px solid rgb(87, 87, 87); - background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); - - -} - -QPushButton:pressed { - background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, - stop: 0 #dadbde, stop: 1 #f6f7fa); -} - -QPushButton:flat { - border: none; /* no border for a flat push button */ -} - -QPushButton:default { - border-color: navy; /* make the default button prominent */ -} - - - - - - - :/Navigation/img/Navigation/arrows-4.png:/Navigation/img/Navigation/arrows-4.png - - - - 50 - 50 - - - - false - - - false - - - false - - - false - - - - - - 0 - 100 - 801 - 100 - - - - - 0 - 70 - - - - - Gotham - 16 - - - - QPushButton { - border: 1px solid rgb(87, 87, 87); - background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); - - -} - -QPushButton:pressed { - background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, - stop: 0 #dadbde, stop: 1 #f6f7fa); -} - -QPushButton:flat { - border: none; /* no border for a flat push button */ -} - -QPushButton:default { - border-color: navy; /* make the default button prominent */ -} - -QPushButton:focus { - outline: none; -} - - - Configure WiFi - - - - 40 - 40 - - - - - - - - - 400 - 360 - 411 - 121 - - - - - Gotham - 16 - - - - Qt::NoContextMenu - - - QPushButton { - border: 1px solid rgb(87, 87, 87); - background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); - - -} - -QPushButton:pressed { - background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, - stop: 0 #dadbde, stop: 1 #f6f7fa); -} - -QPushButton:flat { - border: none; /* no border for a flat push button */ -} - -QPushButton:default { - border-color: navy; /* make the default button prominent */ -} - -QPushButton:focus { - outline: none; -} - - - Cancel - - - - 50 - 50 - - - - - - - 0 - 120 - 801 - 211 - - - - - 16 - - - - - - 690 - 80 - 111 - 51 - - - - - MS Shell Dlg 2 - 16 - 75 - true - - - - Qt::NoContextMenu - - - QPushButton { - border: 1px solid rgb(0, 0, 0); - background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); - - -} - -QPushButton:pressed { - background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, - stop: 0 #dadbde, stop: 1 #f6f7fa); -} - -QPushButton:flat { - border: none; /* no border for a flat push button */ -} - -QPushButton:default { - border-color: navy; /* make the default button prominent */ -} - -QPushButton:focus { - outline: none; -} - - - ... - - - - 40 - 40 - - - - - - - 10 - 90 - 110 - 30 - - - - - MS Shell Dlg 2 - 16 - 50 - false - - - - Qt::NoContextMenu - - - color: rgb(255, 255, 255); - - - Gateway - - - - - true - - - - 690 - 10 - 111 - 51 - - - - - MS Shell Dlg 2 - 16 - 75 - true - - - - Qt::NoContextMenu - - - false - - - QPushButton { - border: 1px solid rgb(0, 0, 0); - background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); - - -} - -QPushButton:pressed { - background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, - stop: 0 #dadbde, stop: 1 #f6f7fa); -} - -QPushButton:flat { - border: none; /* no border for a flat push button */ -} - -QPushButton:default { - border-color: navy; /* make the default button prominent */ -} - -QPushButton:focus { - outline: none; -} - - - ... - - - - 40 - 40 - - - - - - - 10 - 20 - 151 - 30 - - - - - MS Shell Dlg 2 - 16 - 50 - false - - - - Qt::NoContextMenu - - - color: rgb(255, 255, 255); - - - IP Address - - - - - - 690 - 150 - 111 - 51 - - - - - MS Shell Dlg 2 - 16 - 75 - true - - - - Qt::NoContextMenu - - - QPushButton { - border: 1px solid rgb(0, 0, 0); - background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); - - -} - -QPushButton:pressed { - background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, - stop: 0 #dadbde, stop: 1 #f6f7fa); -} - -QPushButton:flat { - border: none; /* no border for a flat push button */ -} - -QPushButton:default { - border-color: navy; /* make the default button prominent */ -} - -QPushButton:focus { - outline: none; -} - - - ... - - - - 40 - 40 - - - - - - - 10 - 160 - 181 - 30 - - - - - MS Shell Dlg 2 - 16 - 50 - false - - - - Qt::NoContextMenu - - - color: rgb(255, 255, 255); - - - Name Servers - - - - - - true - - - - 640 - 0 - 171 - 91 - - - - - MS Shell Dlg 2 - 18 - 75 - true - - - - Qt::NoContextMenu - - - false - - - QPushButton { - border: 1px solid rgb(0, 0, 0); - background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); - - -} - -QPushButton:pressed { - background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, - stop: 0 #dadbde, stop: 1 #f6f7fa); -} - -QPushButton:flat { - border: none; /* no border for a flat push button */ -} - -QPushButton:default { - border-color: navy; /* make the default button prominent */ -} - -QPushButton:focus { - outline: none; -} - - - - - - - :/Icons/img/icons/delete.png:/Icons/img/icons/delete.png - - - - 40 - 40 - - - - - - - 0 - 360 - 401 - 121 - - - - - Gotham - 16 - - - - Qt::NoContextMenu - - - QPushButton { - border: 1px solid rgb(87, 87, 87); - background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); - - -} - -QPushButton:pressed { - background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, - stop: 0 #dadbde, stop: 1 #f6f7fa); -} - -QPushButton:flat { - border: none; /* no border for a flat push button */ -} - -QPushButton:default { - border-color: navy; /* make the default button prominent */ -} - -QPushButton:focus { - outline: none; -} - - - Done - - - - 40 - 40 - - - - - - - 210 - 10 - 301 - 71 - - - - - Gotham - 16 - - - - Qt::NoContextMenu - - - QScrollBar:vertical { - border: 1px solid black; -border-radius: 5px; - background-color: rgb(40,40,40); - width: 60px; - margin: 67px 0 67px 0; - } - -/* Sets up the color and height of handle */ -QScrollBar::handle:vertical { -border-radius: 5px; -background: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); -min-height: 7px; -} - - -QScrollBar::add-line:vertical { - border: 1px solid black; -background: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); - height:65px; -border-radius: 5px; - subcontrol-position: bottom; - subcontrol-origin: margin; - } - - QScrollBar::sub-line:vertical { - border: 1px solid black; -background: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); - height: 65px; -border-radius: 5px; - subcontrol-position: top; - subcontrol-origin: margin; - } - -QScrollBar::up-arrow:vertical { - image: url(./templates/img/arrows.png); - width: 40px; - height: 40px; - padding: 5px; - } -QScrollBar::down-arrow:vertical { - image: url(./templates/img/arrows-5.png); - width: 40px; - height: 40px; - padding: 5px; - } - -/* need this to get rid of crosshatching on scrollbar background */ -QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { -background: none; -} - -QComboBox { -border: 1px solid black; - padding: 0px 18px 0px 3px; - min-width: 6em; - -} - -QComboBox::item { - color: rgb(0, 0, 0); -} - -QComboBox:editable { - background: white; -} - -QComboBox:!editable, QComboBox::drop-down:editable { -background: white; -} - -/* QComboBox gets the "on" state when the popup is open */ -QComboBox:!editable:on, QComboBox::drop-down:editable:on { -background: white; -} - - -QComboBox::drop-down { -border-left: 1px solid black; -border-right: 1px solid black; - background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); - width: 60px; - height: 70px; - -} - -QComboBox::down-arrow { - -image: url(./templates/img/arrows-5.png); -width: 30px; -height: 30px; - -} - - -QComboBox QAbstractItemView { - selection-background-color: rgb(40, 40, 40); - background: white; -} - - - false - - - 8 - - - - 30 - 30 - - - - - - - 20 - 30 - 171 - 30 - - - - - MS Shell Dlg 2 - 16 - 50 - false - - - - Qt::NoContextMenu - - - color: rgb(255, 255, 255); - - - Interface - - - - - - - - 0 - 130 - 161 - 41 - - - - - Gotham - 16 - - - - QCheckBox { - color: rgb(255, 255, 255); -} - -QCheckBox::indicator { - width: 25px; - height: 25px; -} - -QCheckBox::indicator:checked { - image: url(./templates/img/check-box.png); -} -QCheckBox::indicator:unchecked { - image: url(./templates/img/blank-check-box.png); -} - - - - - - - Hidden - - - - 40 - 40 - - - - false - - - - - - 400 - 360 - 401 - 121 - - - - - Gotham - 16 - - - - QPushButton { - border: 1px solid rgb(87, 87, 87); - background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); - - -} - -QPushButton:pressed { - background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, - stop: 0 #dadbde, stop: 1 #f6f7fa); -} - -QPushButton:flat { - border: none; /* no border for a flat push button */ -} - -QPushButton:default { - border-color: navy; /* make the default button prominent */ -} - -QPushButton:focus { - outline: none; -} - - - Cancel - - - - 40 - 40 - - - - - - - 0 - 70 - 701 - 51 - - - - - Gotham - 16 - - - - QScrollBar:vertical { - border: 1px solid black; -border-radius: 5px; - background-color: rgb(40,40,40); - width: 60px; - margin: 67px 0 67px 0; - } - -/* Sets up the color and height of handle */ -QScrollBar::handle:vertical { -border-radius: 5px; -background: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); -min-height: 7px; -} - - -QScrollBar::add-line:vertical { - border: 1px solid black; -background: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); - height:65px; -border-radius: 5px; - subcontrol-position: bottom; - subcontrol-origin: margin; - } - - QScrollBar::sub-line:vertical { - border: 1px solid black; -background: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); - height: 65px; -border-radius: 5px; - subcontrol-position: top; - subcontrol-origin: margin; - } - -QScrollBar::up-arrow:vertical { - image: url(:/Navigation/img/Navigation/arrows.png); - width: 40px; - height: 40px; - padding: 5px; - } -QScrollBar::down-arrow:vertical { - image: url(:/Navigation/img/Navigation/arrows-5.png); - width: 40px; - height: 40px; - padding: 5px; - } - -/* need this to get rid of crosshatching on scrollbar background */ -QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { -background: none; -} - -QComboBox { -border: 1px solid black; - padding: 0px 18px 0px 3px; - min-width: 6em; - -} - -QComboBox::item { - color: rgb(0, 0, 0); -} - -QComboBox:editable { - background: white; -} - -QComboBox:!editable, QComboBox::drop-down:editable { -background: white; -} - -/* QComboBox gets the "on" state when the popup is open */ -QComboBox:!editable:on, QComboBox::drop-down:editable:on { -background: white; -} - - -QComboBox::drop-down { -border-left: 1px solid black; -border-right: 1px solid black; - background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); - width: 60px; - height: 50px; - -} - -QComboBox::down-arrow { - -image: url(:/Navigation/img/Navigation/arrows-5.png); -width: 30px; -height: 30px; - -} - - -QComboBox QAbstractItemView { - selection-background-color: rgb(40, 40, 40); - background: white; -} - - - false - - - 8 - - - - 30 - 30 - - - - - - - 0 - 0 - 461 - 51 - - - - - Gotham Light - 16 - 50 - false - - - - color: rgb(255, 255, 255); - - - Enter SSID: - - - - - - 0 - 360 - 401 - 121 - - - - - Gotham - 16 - - - - QPushButton { - border: 1px solid rgb(87, 87, 87); - background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); - - -} - -QPushButton:pressed { - background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, - stop: 0 #dadbde, stop: 1 #f6f7fa); -} - -QPushButton:flat { - border: none; /* no border for a flat push button */ -} - -QPushButton:default { - border-color: navy; /* make the default button prominent */ -} - -QPushButton:focus { - outline: none; -} - - - Done - - - - 40 - 40 - - - - - - - 700 - 70 - 101 - 51 - - - - - Gotham - 16 - - - - QPushButton { - border: 1px solid rgb(0, 0, 0); - background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); - - -} - -QPushButton:pressed { - background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, - stop: 0 #dadbde, stop: 1 #f6f7fa); -} - -QPushButton:flat { - border: none; /* no border for a flat push button */ -} - -QPushButton:default { - border-color: navy; /* make the default button prominent */ -} - -QPushButton:focus { - outline: none; -} - - - ... - - - - 40 - 40 - - - - - - - 10 - 180 - 791 - 31 - - - - - Gotham Light - 16 - 50 - false - - - - color: rgb(255, 255, 255); - - - Enter Password: - - - - - - - - 0 - 390 - 801 - 91 - - - - - 0 - 0 - - - - - Gotham - 16 - - - - QPushButton { - border: 1px solid rgb(87, 87, 87); - background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); - - -} - -QPushButton:pressed { - background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, - stop: 0 #dadbde, stop: 1 #f6f7fa); -} - -QPushButton:flat { - border: none; /* no border for a flat push button */ -} - -QPushButton:default { - border-color: navy; /* make the default button prominent */ -} - - - - - - - :/Navigation/img/Navigation/arrows-4.png:/Navigation/img/Navigation/arrows-4.png - - - - 50 - 50 - - - - false - - - false - - - false - - - false - - - - - - 10 - 10 - 501 - 371 - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - - - - Gotham - 16 - 50 - false - - - - color: rgb(255, 255, 255); -background-color: rgba(255, 255, 255, 0); - - - mDNS URL: - - - - - - - - Gotham - 16 - 50 - false - - - - color: rgb(255, 255, 255); -background-color: rgba(255, 255, 255, 0); - - - Wi-Fi AP - - - - - - - - Gotham - 16 - 50 - false - - - - color: rgb(255, 255, 255); -background-color: rgba(255, 255, 255, 0); - - - Wi-Fi MAC: - - - - - - - - Gotham - 16 - 50 - false - - - - color: rgb(255, 255, 255); -background-color: rgba(255, 255, 255, 0); - - - Wi-Fi IP - - - - - - - - Gotham - 16 - 50 - false - - - - color: rgb(255, 255, 255); -background-color: rgba(255, 255, 255, 0); - - - WiFi: - - - - - - - - Gotham - 16 - 50 - false - - - - color: rgb(255, 255, 255); -background-color: rgba(255, 255, 255, 0); - - - WiFi - - - - - - - - Gotham - 16 - 50 - false - - - - color: rgb(255, 255, 255); -background-color: rgba(255, 255, 255, 0); - - - Ethernet MAC: - - - - - - - - Gotham - 16 - 50 - false - - - - color: rgb(255, 255, 255); -background-color: rgba(255, 255, 255, 0); - - - Ethernet IP - - - - - - - - Gotham - 16 - 50 - false - - - - color: rgb(255, 255, 255); -background-color: rgba(255, 255, 255, 0); - - - Ethernet: - - - - - - - - Gotham - 16 - 50 - false - - - - color: rgb(255, 255, 255); -background-color: rgba(255, 255, 255, 0); - - - Ethernet - - - - - - - - Gotham - 16 - 50 - false - - - - color: rgb(255, 255, 255); -background-color: rgba(255, 255, 255, 0); - - - Hostname: - - - - - - - - Gotham - 16 - 50 - false - - - - color: rgb(255, 255, 255); -background-color: rgba(255, 255, 255, 0); - - - WiFi - - - - - - - - - 530 - 80 - 250 - 250 - - - - - 20 - - - - background-color: rgb(255, 255, 255); - - - - - - - - - 530 - 80 - 250 - 250 - - - - - 20 - - - - - - - - - - true - - - - - - 530 - 50 - 171 - 26 - - - - - Gotham - 12 - 50 - false - - - - color: rgb(255, 255, 255); -background-color: rgba(255, 255, 255, 0); - - - Scan to connenct: - - - - - - - - - - diff --git a/src/ui/settings_screen/settings_screen.py b/src/ui/settings_screen/settings_screen.py deleted file mode 100644 index 9b2f21d2..00000000 --- a/src/ui/settings_screen/settings_screen.py +++ /dev/null @@ -1,171 +0,0 @@ -import os -import importlib.util -from PyQt5 import uic -from PyQt5.QtWidgets import QWidget, QPushButton, QStackedWidget, QVBoxLayout, QScrollArea -from PyQt5.QtGui import QFont - -class SettingsScreen(QWidget): - def __init__(self, main_window): - """ - Initialize the SettingsScreen widget. - - Args: - main_window (QMainWindow): The main window of the application. - """ - super(SettingsScreen, self).__init__() - self.main_window = main_window - uic.loadUi('src/ui/settings_screen/settings_screen.ui', self) - - # Find the stacked widget - self.stackedWidget = self.findChild(QStackedWidget, 'mainSettingsStackedWidget') - - # Find the main settings page - self.mainSettingsPage = self.findChild(QWidget, 'mainSettingsPage') - - # Find the scroll area and its contents - self.scrollArea = self.findChild(QScrollArea, 'scrollArea') - self.scrollAreaWidgetContents = self.scrollArea.findChild(QWidget, 'scrollAreaWidgetContents') - self.verticalLayout = self.scrollAreaWidgetContents.findChild(QVBoxLayout, 'verticalLayout') - - # Add the back button to the main layout - self.settingsBackButton = self.findChild(QPushButton, 'settingsBackButton') - self.settingsBackButton.clicked.connect(self.go_back) - self.verticalLayout.insertWidget(0, self.settingsBackButton) - - # Find the new buttons - self.restorePrintSettingsButton = self.findChild(QPushButton, 'restorePrintSettingsButton') - self.restoreFactoryDefaultsButton = self.findChild(QPushButton, 'restoreFactoryDefaultsButton') - self.restartButton = self.findChild(QPushButton, 'restartButton') - - # Connect the new buttons to their respective functions - self.restorePrintSettingsButton.clicked.connect(self.restore_print_settings) - self.restoreFactoryDefaultsButton.clicked.connect(self.restore_factory_defaults) - self.restartButton.clicked.connect(self.restart_system) - - # Scan the "Settings Screen" folder for subfolders containing .ui files - self.load_settings_widgets() - - # Set the default page to mainSettingsPage - self.stackedWidget.setCurrentWidget(self.mainSettingsPage) - - def go_back(self): - """ - Switch back to the main menu screen. - """ - self.main_window.switch_screen(self.main_window.menu_screen) - - def load_settings_widgets(self): - """ - Load settings widgets from subfolders in the "Settings Screen" folder. - """ - settings_folder = 'src/ui/settings_screen' - for subfolder in os.listdir(settings_folder): - subfolder_path = os.path.join(settings_folder, subfolder) - if os.path.isdir(subfolder_path): - ui_file = os.path.join(subfolder_path, f'{subfolder}.ui') - py_file = os.path.join(subfolder_path, f'{subfolder}.py') - if os.path.exists(ui_file) and os.path.exists(py_file): - print(f"Loading widget: {subfolder}") - # Create a button for the subfolder - button = QPushButton(subfolder.replace('_', ' ').title()) - button.setMinimumHeight(100) - button.setFont(QFont("Gotham Light", 16)) - button.setStyleSheet(""" - QPushButton { - border: 1px solid rgb(87, 87, 87); - background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); - } - QPushButton:pressed { - background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #dadbde, stop: 1 #f6f7fa); - } - QPushButton:flat { - border: none; /* no border for a flat push button */ - } - QPushButton:default { - border-color: navy; /* make the default button prominent */ - } - """) - button.clicked.connect(lambda _, sf=subfolder: self.load_widget(sf)) - self.verticalLayout.addWidget(button) - - # Load the widget and add it to the stacked widget - widget_instance = self.create_widget_instance(ui_file, py_file) - page = QWidget() - layout = QVBoxLayout(page) - layout.setContentsMargins(0, 0, 0, 0) # Remove margins - layout.setSpacing(0) # Remove spacing - layout.addWidget(widget_instance) - self.stackedWidget.addWidget(page) - print(f"Added widget: {widget_instance.objectName()}") - - # Ensure the mainSettingsPage is set as the default page after loading all widgets - self.stackedWidget.setCurrentWidget(self.mainSettingsPage) - - # Ensure the restart button is at the bottom of the button list - self.verticalLayout.addWidget(self.restartButton) - - def load_widget(self, widget_name): - """ - Switch to the specified widget in the stacked widget. - - Args: - widget_name (str): The name of the widget to switch to. - """ - print(f"Switching to widget: {widget_name}") - for i in range(self.stackedWidget.count()): - widget = self.stackedWidget.widget(i) - if widget.findChild(QWidget, widget_name): - self.stackedWidget.setCurrentWidget(widget) - print(f"Switched to widget: {widget_name}") - break - - def create_widget_instance(self, ui_file, py_file): - """ - Create an instance of a widget from the specified .ui and .py files. - - Args: - ui_file (str): The path to the .ui file. - py_file (str): The path to the .py file. - - Returns: - QWidget: An instance of the dynamically loaded widget. - """ - class DynamicWidget(QWidget): - def __init__(self, parent): - super(DynamicWidget, self).__init__(parent) - uic.loadUi(ui_file, self) - self.setObjectName(os.path.basename(ui_file).split('.')[0]) - self.load_backend(py_file, parent) - - def load_backend(self, py_file, parent): - spec = importlib.util.spec_from_file_location("module.name", py_file) - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - # Assuming the class name in the .py file is the same as the subfolder name - class_name = os.path.basename(py_file).split('.')[0].title().replace('_', '') - backend_class = getattr(module, class_name) - backend_instance = backend_class(self, parent) - self.backend = backend_instance - - return DynamicWidget(self) - - def restore_print_settings(self): - """ - Restore the print settings to their default values. - """ - print("Restoring print settings to default values.") - # Add logic to restore print settings - - def restore_factory_defaults(self): - """ - Restore the system to factory default settings. - """ - print("Restoring system to factory default settings.") - # Add logic to restore factory default settings - - def restart_system(self): - """ - Restart the system. - """ - print("Restarting the system.") - # Add logic to restart the system \ No newline at end of file diff --git a/src/ui/settings_screen/settings_screen.ui b/src/ui/settings_screen/settings_screen.ui deleted file mode 100644 index d1ae80d9..00000000 --- a/src/ui/settings_screen/settings_screen.ui +++ /dev/null @@ -1,344 +0,0 @@ - - - Form - - - - 0 - 0 - 800 - 480 - - - - Form - - - background-color: rgb(40, 40, 40); - - - - - 0 - 0 - 800 - 480 - - - - 0 - - - - - - 0 - 0 - 800 - 480 - - - - - 16 - - - - QScrollBar:vertical { - border: 1px solid black; -border-radius: 5px; - background-color: rgb(40,40,40); - width: 80px; - margin: 70px 0 70px 0; - } - QScrollBar::handle:vertical { -border-radius: 5px; -background: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); -min-height: 20px; - } - QScrollBar::add-line:vertical { - border: 1px solid black; -background: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); - height:65px; -border-radius: 5px; - subcontrol-position: bottom; - subcontrol-origin: margin; - } - - QScrollBar::sub-line:vertical { - border: 1px solid black; -background: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); - height: 65px; -border-radius: 5px; - subcontrol-position: top; - subcontrol-origin: margin; - } - -QScrollBar::up-arrow:vertical { - image: url(:/Navigation/img/Navigation/arrows.png); - width: 40px; - height: 40px; - padding: 5px; - } -QScrollBar::down-arrow:vertical { - image: url(:/Navigation/img/Navigation/arrows-5.png); - width: 40px; - height: 40px; - padding: 5px; - } - - QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { - background: none; - } - - - true - - - - - 0 - 0 - 798 - 478 - - - - - 0 - - - 0 - - - 0 - - - 3 - - - 0 - - - - - - 0 - 100 - - - - - Gotham Light - 16 - - - - QPushButton { - border: 1px solid rgb(87, 87, 87); - background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); - - -} - -QPushButton:pressed { - background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, - stop: 0 #dadbde, stop: 1 #f6f7fa); -} - -QPushButton:flat { - border: none; /* no border for a flat push button */ -} - -QPushButton:default { - border-color: navy; /* make the default button prominent */ -} - - - - - - - :/Navigation/img/Navigation/arrows-4.png:/Navigation/img/Navigation/arrows-4.png - - - - 70 - 70 - - - - false - - - false - - - false - - - false - - - - - - - - 0 - 100 - - - - - Gotham - 16 - - - - QPushButton { - border: 1px solid rgb(87, 87, 87); - background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); - - -} - -QPushButton:pressed { - background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, - stop: 0 #dadbde, stop: 1 #f6f7fa); -} - -QPushButton:flat { - border: none; /* no border for a flat push button */ -} - -QPushButton:default { - border-color: navy; /* make the default button prominent */ -} - -QPushButton:focus { - outline: none; -} - - - Restore Print Settings - - - - 40 - 40 - - - - - - - - - 0 - 100 - - - - - Gotham - 16 - - - - QPushButton { - border: 1px solid rgb(87, 87, 87); - background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); - - -} - -QPushButton:pressed { - background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, - stop: 0 #dadbde, stop: 1 #f6f7fa); -} - -QPushButton:flat { - border: none; /* no border for a flat push button */ -} - -QPushButton:default { - border-color: navy; /* make the default button prominent */ -} - -QPushButton:focus { - outline: none; -} - - - Restore Factory Defaults - - - - 40 - 40 - - - - - - - - - 0 - 100 - - - - - Gotham - 16 - - - - QPushButton { - border: 1px solid rgb(87, 87, 87); - background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); - - -} - -QPushButton:pressed { - background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, - stop: 0 #dadbde, stop: 1 #f6f7fa); -} - -QPushButton:flat { - border: none; /* no border for a flat push button */ -} - -QPushButton:default { - border-color: navy; /* make the default button prominent */ -} - -QPushButton:focus { - outline: none; -} - - - Restart - - - - 40 - 40 - - - - - - - - - - - - - - - diff --git a/src/ui/settings_screen/software_update/__pycache__/software_update.cpython-313.pyc b/src/ui/settings_screen/software_update/__pycache__/software_update.cpython-313.pyc deleted file mode 100644 index 0fde5b87..00000000 Binary files a/src/ui/settings_screen/software_update/__pycache__/software_update.cpython-313.pyc and /dev/null differ diff --git a/src/ui/settings_screen/software_update/software_update.py b/src/ui/settings_screen/software_update/software_update.py deleted file mode 100644 index 3cfcb142..00000000 --- a/src/ui/settings_screen/software_update/software_update.py +++ /dev/null @@ -1,34 +0,0 @@ -from PyQt5 import uic -from PyQt5.QtWidgets import QWidget, QPushButton - -class SoftwareUpdate(QWidget): - def __init__(self, parent, settings_screen): - super(SoftwareUpdate, self).__init__(parent) - self.mainSettingsWidget = settings_screen # Reference to the main settings widget - uic.loadUi('src/ui/settings_screen/software_update/software_update.ui', self) - - # Find buttons by their object names - self.softwareUpdateBackButton = self.findChild(QPushButton, 'softwareUpdateBackButton') - self.performUpdateButton = self.findChild(QPushButton, 'performUpdateButton') - - # Check if buttons are found - if not all([self.softwareUpdateBackButton, self.performUpdateButton]): - raise ValueError("One or more buttons not found in the UI file") - - # Connect buttons to their respective functions - self.softwareUpdateBackButton.clicked.connect(self.go_back_to_settings_screen) - self.performUpdateButton.clicked.connect(self.update_software) - - def go_back_to_settings_screen(self): - """ - Return to the settings screen. - """ - print("Back to settings screen button clicked") - self.mainSettingsWidget.stackedWidget.setCurrentWidget(self.mainSettingsWidget.mainSettingsPage) - - def update_software(self): - """ - Update the software. - """ - print("Updating software.") - # Add logic to update software diff --git a/src/ui/settings_screen/software_update/software_update.ui b/src/ui/settings_screen/software_update/software_update.ui deleted file mode 100644 index c3594925..00000000 --- a/src/ui/settings_screen/software_update/software_update.ui +++ /dev/null @@ -1,341 +0,0 @@ - - - Form - - - - 0 - 0 - 800 - 480 - - - - Form - - - -background-color: rgb(40, 40, 40); - - - - - 0 - 0 - 801 - 481 - - - - 0 - - - - - - 400 - 360 - 401 - 121 - - - - - 0 - 0 - - - - - MS Shell Dlg 2 - 16 - - - - QPushButton { - border: 1px solid rgb(87, 87, 87); - background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); - - -} - -QPushButton:pressed { - background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, - stop: 0 #dadbde, stop: 1 #f6f7fa); -} - -QPushButton:flat { - border: none; /* no border for a flat push button */ -} - -QPushButton:default { - border-color: navy; /* make the default button prominent */ -} - - - - - - - ../../../../../TwinDragon600x600CM4TouchUI/octoprint_TwinDragon600x600CM4TouchUI/templates/img/arrows-4.png../../../../../TwinDragon600x600CM4TouchUI/octoprint_TwinDragon600x600CM4TouchUI/templates/img/arrows-4.png - - - - 70 - 70 - - - - false - - - false - - - false - - - false - - - - - - 0 - 0 - 801 - 391 - - - - - MS Shell Dlg 2 - 16 - - - - - -QScrollBar:vertical { - border: 1px solid black; -border-radius: 5px; -background: rgb(40,40,40); -width: 62px; -} - -/* Sets up the color and height of handle */ -QScrollBar::handle:vertical { -border-radius: 5px; -background: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); -min-height: 20px; -} - - - - -/* This remvoes the bottom button by setting the height to 0px */ -QScrollBar::add-line:vertical { -height: 0px; -subcontrol-position: bottom; -subcontrol-origin: margin; -} - -/* This remvoes the top button by setting the height to 0px */ -QScrollBar::sub-line:vertical { -height: 0px; -subcontrol-position: top; -subcontrol-origin: margin; -} - -/* -QScrollBar::up-arrow:vertical, QScrollBar::down-arrow:vertical { -border: 2px solid grey; -width: 5px; -height: 5px; -background: white; -} - - -/* need this to get rid of crosshatching on scrollbar background */ -QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { -background: none; -} - -QListView::item { -color: rgb(255, 255, 255); -border-bottom: 1px solid rgb(255, 255, 255); -} - -QListView { - show-decoration-selected: 1; /* make the selection span the entire width of the view */ -} - -QListView::item:selected { -outline: none; -} - - -QListView::item:selected:focus { - outline: none; -} - - - - - - - 0 - 360 - 401 - 121 - - - - - 0 - 0 - - - - - MS Shell Dlg 2 - 16 - - - - QPushButton { - border: 1px solid rgb(87, 87, 87); - background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); - - -} - -QPushButton:pressed { - background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, - stop: 0 #dadbde, stop: 1 #f6f7fa); -} - -QPushButton:flat { - border: none; /* no border for a flat push button */ -} - -QPushButton:default { - border-color: navy; /* make the default button prominent */ -} - - - Update - - - - ../../../../../TwinDragon600x600CM4TouchUI/octoprint_TwinDragon600x600CM4TouchUI/templates/img/update-arrows.png../../../../../TwinDragon600x600CM4TouchUI/octoprint_TwinDragon600x600CM4TouchUI/templates/img/update-arrows.png - - - - 70 - 70 - - - - false - - - false - - - false - - - false - - - updateListWidget - softwareUpdateBackButton - performUpdateButton - - - - - - 0 - 0 - 801 - 481 - - - - - Gotham - 16 - - - - QTextEdit{ -background-color: rgb(40, 40, 40); -color: rgb(255, 255, 255); -} - - -QScrollBar:vertical { - border: 1px solid black; -border-radius: 5px; -background: rgb(40,40,40); -width: 30px; -} - -/* Sets up the color and height of handle */ -QScrollBar::handle:vertical { -border-radius: 5px; -background: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0.188, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(255, 255, 255, 255)); -min-height: 20px; -} - - - - -/* This remvoes the bottom button by setting the height to 0px */ -QScrollBar::add-line:vertical { -height: 0px; -subcontrol-position: bottom; -subcontrol-origin: margin; -} - -/* This remvoes the top button by setting the height to 0px */ -QScrollBar::sub-line:vertical { -height: 0px; -subcontrol-position: top; -subcontrol-origin: margin; -} - -/* -QScrollBar::up-arrow:vertical, QScrollBar::down-arrow:vertical { -border: 2px solid grey; -width: 5px; -height: 5px; -background: white; -} - - -/* need this to get rid of crosshatching on scrollbar background */ -QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { -background: none; -} - - - - true - - - <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> -<html><head><meta name="qrichtext" content="1" /><style type="text/css"> -p, li { white-space: pre-wrap; } -</style></head><body style=" font-family:'Gotham'; font-size:16pt; font-weight:400; font-style:normal;"> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:10pt;">Software Update Starting, Please Wait....</span></p></body></html> - - - - - - - - diff --git a/src/ui/tab_screen/tab_screen.py b/src/ui/tab_screen/tab_screen.py new file mode 100644 index 00000000..612c7834 --- /dev/null +++ b/src/ui/tab_screen/tab_screen.py @@ -0,0 +1,61 @@ +from PyQt5 import uic +from PyQt5.QtWidgets import QWidget, QTabWidget, QVBoxLayout +from ui.home_screen.home_screen import HomeScreen +from ui.control_screen.control_screen import ControlScreen + + +class TabScreen(QWidget): + def __init__(self, main_window): + super(TabScreen, self).__init__() + self.main_window = main_window + # Load the .ui file for tab screen + try: + uic.loadUi('src/ui/tab_screen/tab_screen.ui', self) + print("TabScreen UI loaded successfully") + except Exception as e: + print(f"Failed to load TabScreen UI: {e}") + + # Find the QTabWidget + self.tabWidget = self.findChild(QTabWidget, 'tabWidget') + + # Populate the tabs using the existing containers in the UI file + self.load_home_tab() + self.load_parameters_tab() + self.load_control_tab() + + def load_home_tab(self): + # Find the existing home tab + home_tab = self.findChild(QWidget, 'home_tab') + if home_tab: + self.home_screen = HomeScreen(self.main_window) + self.main_window.home_screen = self.home_screen # Store reference in main_window + layout = QVBoxLayout(home_tab) + layout.addWidget(self.home_screen) + home_tab.setLayout(layout) + else: + print("Home tab not found in TabScreen UI") + + def load_parameters_tab(self): + # Locate the existing parameters tab container (set objectName to "parameters_tab" in Qt Designer) + parameters_tab = self.findChild(QWidget, 'parameters_tab') + if parameters_tab: + from ui.parameters_screen.parameters_screen import ParametersScreen + self.parameters_screen = ParametersScreen(self.main_window) + layout = QVBoxLayout(parameters_tab) + layout.addWidget(self.parameters_screen) + parameters_tab.setLayout(layout) + else: + print("Parameters tab not found in TabScreen UI") + + def load_control_tab(self): + # Find the existing control tab by its object name as set in Qt Designer (e.g., "controlTab") + control_tab = self.findChild(QWidget, 'control_tab') + if control_tab: + # Import ControlScreen only when needed + self.control_screen = ControlScreen(self.main_window) + self.main_window.control_screen = self.control_screen # Store reference in main_window + layout = QVBoxLayout(control_tab) + layout.addWidget(self.control_screen) + control_tab.setLayout(layout) + else: + print("Control tab not found in TabScreen UI") \ No newline at end of file diff --git a/src/ui/tab_screen/tab_screen.ui b/src/ui/tab_screen/tab_screen.ui new file mode 100644 index 00000000..06d03285 --- /dev/null +++ b/src/ui/tab_screen/tab_screen.ui @@ -0,0 +1,554 @@ + + + Form + + + + 0 + 0 + 1333 + 714 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 5 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 71 + + + + + 16777215 + 71 + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 20 + + + 5 + + + 5 + + + 5 + + + 5 + + + + + + 50 + 50 + + + + + 50 + 50 + + + + + Gotham + 16 + 50 + false + false + false + + + + border: 1px solid rgb(87, 87, 87); + border-radius: 10px; + background-color: qlineargradient(spread:pad, x1:0, y1:0.523, x2:0, y2:0.534, stop:0 rgba(130, 203, 117, 255), stop:1 rgba(66, 191, 85, 255)); + + + + + + Qt::AlignCenter + + + + + + + + 200 + 0 + + + + + Gotham + 16 + 50 + false + false + false + + + + +background-color: rgba(255, 255, 255, 0); + + + STATUS + + + true + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + Qt::Vertical + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + 600 + 40 + + + + + 600 + 40 + + + + + + + :/Logos & Branding/img/Logos/control_center_logo_black.png + + + true + + + + + + + + + + Qt::Vertical + + + + + + + + 150 + 0 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + 10 + 0 + 131 + 59 + + + + + 100 + 0 + + + + + Gotham + 12 + 50 + false + false + false + + + + +background-color: rgba(255, 255, 255, 0); + + + Current Action + + + true + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + + Qt::Vertical + + + + + + + + 0 + 0 + + + + + 150 + 0 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + 0 + 10 + 100 + 16 + + + + + 100 + 0 + + + + + Gotham + 10 + 50 + false + false + false + + + + +background-color: rgba(255, 255, 255, 0); + + + Laser Status + + + true + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + 0 + 30 + 100 + 16 + + + + + 100 + 0 + + + + + Gotham + 10 + 50 + false + false + false + + + + +background-color: rgba(255, 255, 255, 0); + + + Scanner Status + + + true + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + 100 + 30 + 15 + 15 + + + + + 15 + 15 + + + + + 15 + 15 + + + + + Gotham + 16 + 50 + false + false + false + + + + border: 1px solid rgb(87, 87, 87); + background-color: qlineargradient(spread:pad, x1:0, y1:0.523, x2:0, y2:0.534, stop:0 rgba(130, 203, 117, 255), stop:1 rgba(66, 191, 85, 255)); + + + + + + Qt::AlignCenter + + + + + + 100 + 10 + 15 + 15 + + + + + 15 + 15 + + + + + 15 + 15 + + + + + Gotham + 16 + 50 + false + false + false + + + + border: 1px solid rgb(87, 87, 87); + background-color: qlineargradient(spread:pad, x1:0, y1:0.523, x2:0, y2:0.534, stop:0 rgba(130, 203, 117, 255), stop:1 rgba(66, 191, 85, 255)); + + + + + + Qt::AlignCenter + + + + + + + + + + + + 15 + + + + QTabBar::tab { + width: 80%; /* Adjust the percentage as needed */ + height: 200%; /* Adjust the percentage as needed */ + + +} + + + QTabWidget::West + + + QTabWidget::Rounded + + + 0 + + + + 40 + 40 + + + + + + :/Icons/img/icons/house.png:/Icons/img/icons/house.png + + + Home + + + + + + :/Icons/img/icons/Motion.png:/Icons/img/icons/Motion.png + + + Control + + + + + + :/Icons/img/icons/settings-1.png:/Icons/img/icons/settings-1.png + + + Parameters + + + + + + :/Icons/img/icons/settings.png:/Icons/img/icons/settings.png + + + Settings + + + + + + + + + + + + + + diff --git a/src/utils/helpers.py b/src/utils/helpers.py index 3c304c83..84aff455 100644 --- a/src/utils/helpers.py +++ b/src/utils/helpers.py @@ -16,4 +16,54 @@ def convert_to_percentage(value, total): """Convert a value to a percentage of a total.""" if total == 0: return 0 - return (value / total) * 100 \ No newline at end of file + return (value / total) * 100 + +from PyQt5.QtCore import QRunnable, QThreadPool, pyqtSignal, QObject + +class WorkerSignals(QObject): + finished = pyqtSignal() + error = pyqtSignal(Exception) + +class Worker(QRunnable): + def __init__(self, func, *args, **kwargs): + super().__init__() + self.func = func + self.args = args + self.kwargs = kwargs + self.signals = WorkerSignals() + self.is_running = False # Custom attribute to track running state + + def run(self): + self.is_running = True + try: + self.func(*self.args, **self.kwargs) + except Exception as e: + self.signals.error.emit(e) + finally: + self.is_running = False + self.signals.finished.emit() + +# Keep a reference to the workers to prevent them from being garbage collected +workers = {} + +def run_async(func): + """ + Function decorator to make methods run in a QRunnable + """ + from functools import wraps + + @wraps(func) + def async_func(*args, **kwargs): + if func in workers and workers[func].is_running: + print(f"Worker for {func.__name__} is already running.") + return workers[func] + + worker = Worker(func, *args, **kwargs) + worker.signals.error.connect(lambda e: print(f"Error in thread: {e}")) + worker.signals.finished.connect(lambda: workers.pop(func, None)) # Remove the worker from the dictionary when finished + + workers[func] = worker # Keep a reference to the worker + QThreadPool.globalInstance().start(worker) + return worker + + return async_func \ No newline at end of file