├── plugin ├── requirements.txt ├── shui │ ├── __init__.py │ ├── utils │ │ ├── controls │ │ │ ├── __init__.py │ │ │ └── GCodeActionsControl.py │ │ ├── __init__.py │ │ ├── PrusaGcodeParser.py │ │ ├── CuraGCodeParser.py │ │ ├── WifiUart.py │ │ ├── FileSaver.py │ │ ├── PrinterControlTab.py │ │ ├── YandexSender.py │ │ ├── ConsoleTab.py │ │ ├── WifiSender.py │ │ ├── AlisaTab.py │ │ ├── qoi │ │ │ └── qoi_reader.py │ │ ├── TelegramTab.py │ │ ├── FileTab.py │ │ ├── Core.py │ │ └── SetupDialog.py │ ├── PyQt_API.py │ ├── langs.json │ └── MainUI.py ├── shui.bat ├── setup.bat ├── __init__.py ├── prusha.py ├── plugin.json ├── config.json ├── ShuiPlugin.py └── shui_prusa.gcode ├── shui.stl ├── .gitignore ├── README.md ├── scripts └── ShuiPreview.py └── shui_prusa.gcode /plugin/requirements.txt: -------------------------------------------------------------------------------- 1 | PyQt5 2 | -------------------------------------------------------------------------------- /plugin/shui/__init__.py: -------------------------------------------------------------------------------- 1 | from .MainUI import MainWidget 2 | -------------------------------------------------------------------------------- /plugin/shui.bat: -------------------------------------------------------------------------------- 1 | @echo on 2 | python "%~dp0\prusha.py" "%1" 3 | -------------------------------------------------------------------------------- /shui.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ipcn/shui-slicers-plugin/HEAD/shui.stl -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /plugin/config_local.json 3 | *__pycache__* 4 | Makefile 5 | .idea -------------------------------------------------------------------------------- /plugin/setup.bat: -------------------------------------------------------------------------------- 1 | @echo on 2 | pip install -r "%~dp0\requirements.txt" 3 | pause 4 | 5 | -------------------------------------------------------------------------------- /plugin/shui/utils/controls/__init__.py: -------------------------------------------------------------------------------- 1 | from .GCodeActionsControl import GCodeActionsControl -------------------------------------------------------------------------------- /plugin/__init__.py: -------------------------------------------------------------------------------- 1 | from . import ShuiPlugin 2 | 3 | 4 | def getMetaData(): 5 | return {} 6 | 7 | 8 | def register(app): 9 | return { 10 | "output_device": ShuiPlugin.ShuiPlugin() 11 | } 12 | -------------------------------------------------------------------------------- /plugin/prusha.py: -------------------------------------------------------------------------------- 1 | import os 2 | from shui.MainUI import qt_application 3 | from shui.utils import StartMode 4 | appStartMode=StartMode.UNKNOWN 5 | 6 | if os.getenv('START_MODE')=='STANDALONE': 7 | appStartMode=StartMode.STANDALONE 8 | else: 9 | appStartMode=StartMode.PRUSA 10 | 11 | qt_application(appStartMode) -------------------------------------------------------------------------------- /plugin/shui/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .SetupDialog import SetupDialog 2 | from .FileTab import FileTab 3 | from .ConsoleTab import ConsoleTab 4 | from .PrinterControlTab import PrinterControlTab 5 | from .WifiUart import ConnectionThread 6 | from .Core import StartMode 7 | from .FileSaver import FileSaver 8 | from .WifiSender import WifiSender 9 | from .TelegramTab import TelegramTab 10 | from .AlisaTab import AlisaTab 11 | -------------------------------------------------------------------------------- /plugin/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "SHUI Plugin Connection", 3 | "author": "Vyacheslav Shubin", 4 | "version": "24.05.26", 5 | "description": "SHUI Uploader", 6 | "api": 5, 7 | "supported_sdk_versions": [ 8 | "7.5.0", 9 | "7.6.0", 10 | "7.7.0", 11 | "7.8.0", 12 | "8.0.0", 13 | "8.1.0", 14 | "8.2.0" 15 | ], 16 | "i18n-catalog": "cura" 17 | } 18 | -------------------------------------------------------------------------------- /plugin/shui/utils/PrusaGcodeParser.py: -------------------------------------------------------------------------------- 1 | from .Core import GCodeSource, PreviewGenerator 2 | 3 | class PrusaGCodeParser(GCodeSource): 4 | def __init__(self, app, preview, fileName): 5 | super().__init__(app, preview, PreviewGenerator()) 6 | self.fileName=fileName 7 | 8 | def parse(self): 9 | # load oroginal gcode 10 | with open(self.fileName, "r", encoding="utf-8") as g_file: 11 | self.gcode=g_file.readlines() 12 | self.parseGcode() 13 | -------------------------------------------------------------------------------- /plugin/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "printers":[ 3 | ], 4 | 5 | "proxy": {"enabled":false, "host": "proxy.host.ru", "port": 8080, "user": "user", "password": "user_password" }, 6 | 7 | "yandex": { "key": "AQAAAAAX_________", "override": true }, 8 | 9 | "telegram" : {"key": "", "chat_id": ""}, 10 | 11 | "language": "ru", 12 | 13 | "snippets": { 14 | "OFF": "M104S0|M140S0", 15 | "PLA": "M104S190|M140S60", 16 | "ABS": "M104S230|M140S110", 17 | "PETG": "M104S230|M140S70", 18 | "OPT1": "M117 OPT1", 19 | "OPT2": "M117 OPT2", 20 | "OPT3": "M117 OPT3", 21 | "OPT4": "M117 OPT4" 22 | }, 23 | "templates": { 24 | "move_xy_relative": "G91|G1{0}{1}F1000|G90", 25 | "move_z_relative": "G91|G1Z{0}F1000|G90" 26 | } 27 | } -------------------------------------------------------------------------------- /plugin/shui/PyQt_API.py: -------------------------------------------------------------------------------- 1 | # API to access PyQt modules either from PyQt6 or PyQt5 2 | try: 3 | from PyQt6 import QtCore, QtWidgets, QtNetwork, QtGui 4 | from PyQt6.QtCore import Qt, QSize, QByteArray 5 | from PyQt6.QtGui import QPixmap, QImage, QGuiApplication, QClipboard 6 | from PyQt6.QtNetwork import QNetworkAccessManager, QNetworkProxy, QNetworkRequest, QNetworkReply, QHttpMultiPart, QHttpPart 7 | except Exception as e: 8 | try: 9 | from PyQt5 import QtCore, QtWidgets, QtNetwork, QtGui 10 | from PyQt5.QtCore import Qt, QSize, QByteArray 11 | from PyQt5.QtGui import QPixmap, QImage, QGuiApplication, QClipboard 12 | from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkProxy, QNetworkRequest, QNetworkReply, QHttpMultiPart, QHttpPart 13 | except Exception as e: 14 | print("Cannot find either PyQt6 or PyQt5") 15 | raise 16 | -------------------------------------------------------------------------------- /plugin/shui/utils/CuraGCodeParser.py: -------------------------------------------------------------------------------- 1 | from .Core import GCodeSource, PreviewGenerator 2 | 3 | class CuraGCodeParser(GCodeSource): 4 | def __init__(self, app, preview): 5 | super().__init__(app, preview, PreviewGenerator()) 6 | 7 | def parse(self): 8 | # get original gcode 9 | from UM.Application import Application 10 | app_instance=Application.getInstance() 11 | gcode_dict = getattr(app_instance.getController().getScene(), "gcode_dict", None) 12 | gcode = gcode_dict.get(app_instance.getMultiBuildPlateModel().activeBuildPlate, None) 13 | self.gcode=[] 14 | for d in gcode: 15 | lines = d.split("\n") 16 | for d in lines: 17 | self.gcode.append(d + "\n") 18 | # parse gcode 19 | self.parseGcode(False) 20 | # get scene snapshot and store in preview 21 | large_size = self.gen.large_size 22 | from cura.Snapshot import Snapshot 23 | # QImage qimg 24 | qimg = Snapshot.snapshot(width = large_size, height = large_size) 25 | if qimg is not None: 26 | self.preview.setImage(qimg) 27 | pass 28 | -------------------------------------------------------------------------------- /plugin/ShuiPlugin.py: -------------------------------------------------------------------------------- 1 | from UM.Application import Application 2 | from UM.Logger import Logger 3 | from UM.OutputDevice.OutputDevice import OutputDevice 4 | from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin 5 | from UM.i18n import i18nCatalog 6 | 7 | from .shui.MainUI import (cura_application) 8 | 9 | class NetOutputDevice(OutputDevice): 10 | def __init__(self): 11 | catalog = i18nCatalog("uranium") 12 | id_str="save_to_network_device" 13 | descr_str="SHUI Uploader" 14 | super().__init__(id_str) 15 | self.setName(id_str) 16 | self.setPriority(2) 17 | self._preferences = Application.getInstance().getPreferences() 18 | self.setShortDescription(catalog.i18nc("@action:button", descr_str)) 19 | self.setDescription(catalog.i18nc("@properties:tooltip", descr_str)) 20 | self.setIconName("save") 21 | Logger.log("w", "Output device started") 22 | 23 | def requestWrite(self, nodes, file_name=None, limit_mimetypes=None, file_handler=None, **kwargs): 24 | try: 25 | self.writeStarted.emit(self) 26 | fn=Application.getInstance().getPrintInformation().jobName.strip() 27 | # i=fn.find("_") 28 | # if i!=-1: 29 | # fn=fn[i+1:] 30 | self.w=cura_application(output_file_name=fn+".gcode") 31 | pass 32 | except Exception as e: 33 | Logger.log("e", "SHUI error: " + str(e)) 34 | pass 35 | 36 | class ShuiPlugin(OutputDevicePlugin): 37 | def __init__(self): 38 | super().__init__() 39 | self.getOutputDeviceManager().addOutputDevice(NetOutputDevice()) 40 | 41 | def start(self): 42 | Logger.log("w", "ShuiPlugin started") 43 | pass 44 | 45 | def stop(self): 46 | Logger.log("w", "ShuiPlugin stopped") 47 | pass 48 | -------------------------------------------------------------------------------- /plugin/shui/utils/WifiUart.py: -------------------------------------------------------------------------------- 1 | from ..PyQt_API import (QtCore) 2 | import socket 3 | 4 | class ConnectionThread(QtCore.QThread): 5 | 6 | address=None 7 | connected=False 8 | 9 | def __init__(self, app): 10 | QtCore.QThread.__init__(self) 11 | self.app=app 12 | self.sock=None 13 | 14 | def send(self, message): 15 | message=message.replace('|', '\n') 16 | message=message+"\n\r" 17 | if self.sock: 18 | try: 19 | self.sock.send(message.encode()) 20 | except Exception as e: 21 | self.app.onUartMessage.emit("Error: "+str(e)) 22 | pass 23 | 24 | def notifyConnect(self, c): 25 | self.connected = c; 26 | self.app.onUartConnect.emit(c) 27 | pass 28 | 29 | def disconnect(self): 30 | if self.isRunning(): 31 | self.exit(0) 32 | if self.connected: 33 | self.notifyConnect(False) 34 | self.sock.close() 35 | # self.sock=None 36 | pass 37 | 38 | def connect(self, address): 39 | if (self.address!=address) or not self.connected: 40 | self.disconnect() 41 | self.address=address 42 | self.app.onUartMessage.emit("Connecting... "+address) 43 | self.start() 44 | pass 45 | 46 | def run(self): 47 | try: 48 | reading=False 49 | self.sock = socket.socket() 50 | self.sock.connect((self.address, 8080)) 51 | self.notifyConnect(True) 52 | reading=True 53 | buf=bytearray() 54 | while(True): 55 | data = self.sock.recv(1024) 56 | for a in data: 57 | if a==10: 58 | self.app.onUartRow.emit(buf.decode('utf-8')) 59 | buf=bytearray() 60 | else: 61 | buf.append(a) 62 | except Exception as e: 63 | if self.connected or not reading: 64 | self.app.onUartMessage.emit("Error: "+str(e)) 65 | self.disconnect() 66 | pass 67 | -------------------------------------------------------------------------------- /plugin/shui/utils/FileSaver.py: -------------------------------------------------------------------------------- 1 | from ..PyQt_API import (QtCore) 2 | 3 | 4 | class GCodeSaver(QtCore.QObject): 5 | def __init__(self, app): 6 | super().__init__() 7 | self.app=app 8 | 9 | def makeBytes(self, rows): 10 | d=[] 11 | for r in rows: 12 | d+=r.encode() 13 | return bytearray(d) 14 | 15 | class NetworkSender(GCodeSaver): 16 | 17 | def __init__(self, app, file_name): 18 | super().__init__(app) 19 | if len(file_name) == 0: 20 | self.fileName="SHUIWIFI" 21 | else: 22 | self.fileName=file_name 23 | 24 | def onUploadProgress(self, bytes_sent, bytes_total): 25 | if bytes_sent==0 and bytes_total==0: 26 | self.app.onProgress.emit(0, 1) 27 | else: 28 | self.app.onProgress.emit(bytes_sent, bytes_total) 29 | self.app.onMessage.emit("{}: {:d}/{:d}".format(self.app.getLang("sent"), bytes_sent, bytes_total)) 30 | pass 31 | 32 | def onSslError(self, reply, sslerror): 33 | pass 34 | 35 | class FileSaver(GCodeSaver): 36 | 37 | def save(self, rows, filename = None): 38 | state = True 39 | try: 40 | import os 41 | dir = self.app.config.get("saveFileDir") 42 | if not dir and self.app.inputFileName: 43 | dir = os.path.dirname(os.path.abspath(self.app.inputFileName)) 44 | if dir: 45 | filename = os.path.join(dir, filename) 46 | filename = self.app.selectFileDialog(self.app.getLang("save-to-file"), dir, filename) 47 | if filename: 48 | self.app.saveFileDir("saveFileDir", None, filename) 49 | i=0 50 | c=len(rows)/100 51 | with open(filename, "w", encoding="utf-8") as out_file: 52 | for r in rows: 53 | if i%100==0: 54 | self.app.onProgress.emit(i/100, c) 55 | i=i+1 56 | out_file.write(r) 57 | self.app.onProgress.emit(1, 1) 58 | out_file.close() 59 | self.app.onMessage.emit(self.app.getLang("success")) 60 | else: 61 | state = False 62 | except Exception as e: 63 | self.app.onMessage.emit("{0}: {1}".format(self.app.getLang("error"), str(e))) 64 | print(str(e)) 65 | state = False 66 | self.app.onUploadFinished.emit(state) 67 | pass 68 | -------------------------------------------------------------------------------- /plugin/shui/utils/PrinterControlTab.py: -------------------------------------------------------------------------------- 1 | from ..PyQt_API import (QtCore, QtWidgets) 2 | from .controls import GCodeActionsControl 3 | from .Core import (StartMode, UiTab) 4 | import re 5 | 6 | class PrinterControlTab(UiTab): 7 | rows=[] 8 | def __init__(self, app): 9 | super().__init__(app) 10 | self.view_connect = True 11 | self.uartControls=[] 12 | self.app.onUartRow.connect(self.addRow) 13 | self.app.onUartMessage.connect(self.addRow) 14 | self.title = self.app.getLang("printer") 15 | self.axesControl=GCodeActionsControl(self, self.app) 16 | 17 | self.teConsoleOutput = QtWidgets.QTextEdit(self) 18 | self.teConsoleOutput.setReadOnly(True) 19 | self.teConsoleOutput.setStyleSheet("*{background-color: black; color:rgb(0,255,0)}") 20 | self.leHotendTemp = QtWidgets.QLineEdit(self) 21 | self.leHotendTemp.setReadOnly(True) 22 | self.leBedTemp = QtWidgets.QLineEdit(self) 23 | self.leBedTemp.setReadOnly(True) 24 | 25 | 26 | self.mainLayout=QtWidgets.QHBoxLayout() 27 | self.rightLayout=QtWidgets.QVBoxLayout() 28 | self.leftLayout=QtWidgets.QVBoxLayout() 29 | self.leftLayout.addWidget(self.axesControl) 30 | self.leftLayout.addStretch() 31 | self.mainLayout.addLayout(self.leftLayout) 32 | self.mainLayout.addStretch() 33 | self.mainLayout.addLayout(self.rightLayout) 34 | self.temperatureViewLayout=QtWidgets.QHBoxLayout() 35 | self.temperatureViewLayout.addWidget(self.leHotendTemp) 36 | self.temperatureViewLayout.addWidget(self.leBedTemp) 37 | self.rightLayout.addLayout(self.temperatureViewLayout) 38 | self.rightLayout.addWidget(self.teConsoleOutput) 39 | self.rightLayout.addStretch() 40 | 41 | self.setLayout(self.mainLayout) 42 | #ok T0:24.00 /0.00 B:84.09 /70.00 T0:24.00 /0.00 T1:116.75 /0.00 @:0 B@:0 @0:0 @1:0 43 | def filterRow(self, row): 44 | if row[:5]=='ok T0': 45 | match = re.search(r'T0\:(\d+\.\d+)\s*\/(\d+\.\d+)\s*B\:(\d+\.\d+)\s*\/(\d+\.\d+)', row) 46 | if match: 47 | self.leHotendTemp.setText("T:{0}/{1}".format(match[1], match[2])) 48 | self.leBedTemp.setText("B:{0}/{1}".format(match[3], match[4])) 49 | return False 50 | return True 51 | 52 | def addRow(self, row): 53 | if self.filterRow(row): 54 | self.rows.append(row) 55 | if len(self.rows)>20: 56 | self.rows.pop(0) 57 | self.teConsoleOutput.setText("\n".join(self.rows)) 58 | self.teConsoleOutput.verticalScrollBar().setValue(self.teConsoleOutput.verticalScrollBar().maximum()) 59 | pass 60 | 61 | -------------------------------------------------------------------------------- /plugin/shui/utils/YandexSender.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | import json 3 | from .FileSaver import NetworkSender 4 | from ..PyQt_API import (QtCore) 5 | from ..PyQt_API import (QHttpMultiPart, QHttpPart, QNetworkRequest, QNetworkAccessManager, QNetworkReply, QNetworkProxy) 6 | 7 | 8 | class YaPhase(Enum): 9 | NONE=0 10 | REQUEST_URL = 0 11 | UPLOAD = 1 12 | 13 | 14 | class YandexSender(NetworkSender): 15 | phase=YaPhase.NONE 16 | 17 | def makeRequest(self, phase, url): 18 | self.app.networkManager.setProxy(self.app.proxy) 19 | self.phase=phase 20 | req=QNetworkRequest(QtCore.QUrl(url)) 21 | req.setRawHeader(b'Accept', b'application/json') 22 | req.setRawHeader(b'Authorization', ('OAuth '+self.app.config["yandex"]["key"]).encode()) 23 | return req 24 | 25 | def joinReply(self, rep): 26 | rep.finished.connect(self.handleResponse) 27 | rep.uploadProgress.connect(self.onUploadProgress) 28 | rep.sslErrors.connect(self.onSslError) 29 | pass 30 | 31 | def save(self, rows): 32 | self.rows = rows 33 | if self.app.config["yandex"]["override"]: 34 | ovr="overwrite=true&" 35 | else: 36 | ovr="" 37 | self.app.onMessage.emit(self.app.getLang("connecting")) 38 | self.request = self.makeRequest(YaPhase.REQUEST_URL, "https://cloud-api.yandex.net/v1/disk/resources/upload?"+ovr+"path=app:/"+self.fileName) 39 | self.reply = self.app.networkManager.get(self.request) 40 | self.joinReply(self.reply) 41 | pass 42 | 43 | def upload(self, params): 44 | self.request = self.makeRequest(YaPhase.UPLOAD, params["href"]) 45 | self.request.setRawHeader(b'Content-Type', b'application/octet-stream') 46 | self.rows=self.makeBytes(self.rows) 47 | self.reply = self.app.networkManager.put(self.request, self.rows) 48 | self.joinReply(self.reply) 49 | pass 50 | 51 | def doneResponse(self): 52 | self.reply=None 53 | self.postData=None 54 | 55 | def handleResponse(self): 56 | er = self.reply.error() 57 | if er == QNetworkReply.NetworkError.NoError: 58 | if self.phase==YaPhase.REQUEST_URL: 59 | self.app.onMessage.emit(self.app.getLang("prepared")) 60 | jresp=json.loads(str(self.reply.readAll(), 'utf-8')) 61 | self.doneResponse() 62 | self.upload(jresp) 63 | elif self.phase==YaPhase.UPLOAD: 64 | self.app.onMessage.emit(self.app.getLang("success")) 65 | self.doneResponse() 66 | self.app.onUploadFinished.emit(True) 67 | else: 68 | if self.reply.rawHeader(b'Content-Type')==b'application/json': 69 | jresp=json.loads(str(self.reply.readAll(), 'utf-8')) 70 | self.app.onMessage.emit("{0}: {1}".format(self.app.getLang("error"), jresp["message"])) 71 | else: 72 | self.app.onMessage.emit("{0} {1}:{2}".format(self.app.getLang("error"), er, self.reply.errorString())) 73 | self.doneResponse() 74 | self.app.onUploadFinished.emit(False) 75 | pass 76 | 77 | -------------------------------------------------------------------------------- /plugin/shui/utils/ConsoleTab.py: -------------------------------------------------------------------------------- 1 | from ..PyQt_API import (QtCore, QtWidgets) 2 | from .Core import (StartMode, UiTab) 3 | 4 | 5 | class ConsoleTab(UiTab): 6 | def __init__(self, app): 7 | super().__init__(app) 8 | self.view_connect = True 9 | self.title = self.app.getLang("terminal") 10 | self.app.onUartRow.connect(self.addRow) 11 | self.app.onUartMessage.connect(self.addRow) 12 | self.app.onUartConnect.connect(self.onUartConnect) 13 | self.rows=[] 14 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Fixed, QtWidgets.QSizePolicy.Policy.Fixed) 15 | sizePolicy.setHorizontalStretch(0) 16 | sizePolicy.setVerticalStretch(0) 17 | sizePolicy.setHeightForWidth(self.sizePolicy().hasHeightForWidth()) 18 | self.setSizePolicy(sizePolicy) 19 | 20 | self.teConsoleOutput = QtWidgets.QTextEdit(self) 21 | self.teConsoleOutput.setReadOnly(True) 22 | 23 | self.teConsoleOutput.setStyleSheet("*{background-color: black; color:rgb(0,255,0)}") 24 | self.slGCodeMessage = QtWidgets.QLineEdit(self) 25 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Fixed) 26 | sizePolicy.setHorizontalStretch(0) 27 | sizePolicy.setVerticalStretch(0) 28 | sizePolicy.setHeightForWidth(self.slGCodeMessage.sizePolicy().hasHeightForWidth()) 29 | self.slGCodeMessage.setSizePolicy(sizePolicy) 30 | self.teConsoleOutput.setSizePolicy(sizePolicy) 31 | 32 | self.btSenb = QtWidgets.QPushButton() 33 | self.btSenb.setMaximumSize(QtCore.QSize(100, 16777215)) 34 | self.btSenb.setMinimumWidth(100) 35 | self.btSenb.setText(self.app.getLang("send")) 36 | self.btSenb.setDisabled(True) 37 | 38 | 39 | self.mainLayout = QtWidgets.QVBoxLayout(self) 40 | self.mainLayout.addWidget(self.teConsoleOutput) 41 | #self.setLayout(self.mainLayout) 42 | 43 | self.sendLayout = QtWidgets.QHBoxLayout() 44 | self.sendLayout.addWidget(self.slGCodeMessage) 45 | self.sendLayout.addWidget(self.btSenb) 46 | self.sendLayout.setContentsMargins(0, 0, 0, 0) 47 | self.mainLayout.addLayout(self.sendLayout) 48 | 49 | self.btSenb.clicked.connect(self.doSend) 50 | self.addRow(self.app.getLang("title")) 51 | pass 52 | 53 | def keyPressEvent(self, event): 54 | super().keyPressEvent(event) 55 | self.doSendKeyPress(event) 56 | 57 | def addRow(self, row): 58 | self.rows.append(row) 59 | if len(self.rows)>20: 60 | self.rows.pop(0) 61 | self.teConsoleOutput.setText("\n".join(self.rows)) 62 | self.teConsoleOutput.verticalScrollBar().setValue(self.teConsoleOutput.verticalScrollBar().maximum()) 63 | pass 64 | 65 | def onUartConnect(self, state): 66 | self.btSenb.setDisabled(not state) 67 | if state: 68 | self.addRow("Connected") 69 | else: 70 | self.addRow("Disconnected") 71 | pass 72 | 73 | def doSend(self): 74 | self.addRow(self.slGCodeMessage.text()) 75 | self.app.wifiUart.send(self.slGCodeMessage.text()) 76 | pass 77 | 78 | def doSendKeyPress(self, event): 79 | if (event.key() == QtCore.Qt.Key.Key_Enter) or (event.key() == QtCore.Qt.Key.Key_Return): 80 | if (self.app.wifiUart.connected): 81 | self.doSend() 82 | pass 83 | 84 | -------------------------------------------------------------------------------- /plugin/shui/utils/WifiSender.py: -------------------------------------------------------------------------------- 1 | from .FileSaver import NetworkSender 2 | from ..PyQt_API import (QtCore) 3 | from ..PyQt_API import (QHttpMultiPart, QHttpPart, QNetworkRequest, QNetworkAccessManager, QNetworkReply, QNetworkProxy) 4 | 5 | class WifiSender(NetworkSender): 6 | reply=None 7 | request=None 8 | postData=None 9 | 10 | def save(self, rows, **kwargs): 11 | if len(self.app.config["printers"]) <= 0: 12 | self.app.onMessage.emit("{}: {}".format(self.app.getLang("error"), self.app.getLang("error-no-printers"))) 13 | self.app.onUploadFinished.emit(False) 14 | return 15 | 16 | self.app.wifiUart.disconnect() 17 | try: 18 | rows=self.makeBytes(rows) 19 | # calculate simple crc 20 | crc = 0 21 | for ch in rows: 22 | crc = crc ^ ch 23 | crc = crc & 0xFF 24 | 25 | ip = self.app.config["printers"][self.app.selectedPrinter]["ip"] 26 | esp32 = self.app.config["printers"][self.app.selectedPrinter]["esp32"] 27 | request = QNetworkRequest(QtCore.QUrl("http://%s/upload" % ip)) 28 | post_data = None 29 | if "start" in kwargs: 30 | if kwargs["start"]: 31 | request.setRawHeader(b'Start-Printing', b'1') 32 | if esp32: 33 | post_data = rows 34 | request.setRawHeader(b'Content-Type', b'application/octet-stream') 35 | request.setRawHeader(b'File-Name', self.fileName.encode()) 36 | request.setRawHeader(b'CRC', str(crc).encode()) 37 | else: 38 | post_data = QHttpMultiPart(QHttpMultiPart.ContentType.FormDataType) 39 | part = QHttpPart() 40 | part.setHeader(QNetworkRequest.KnownHeaders.ContentDispositionHeader, 41 | "form-data; name=\"file\"; filename=\"%s\"" % self.fileName) 42 | part.setBody(rows) 43 | post_data.append(part) 44 | request.setRawHeader(b'Content-Type', b'multipart/form-data; boundary='+post_data.boundary()) 45 | request.setRawHeader(b'CRC', str(crc).encode()) 46 | self.postData=post_data 47 | 48 | self.app.onMessage.emit(self.app.getLang("connecting")) 49 | proxy=QNetworkProxy() 50 | proxy.setType(QNetworkProxy.ProxyType.NoProxy) 51 | self.app.networkManager.setProxy(proxy) 52 | self.reply = self.app.networkManager.post(request, post_data) 53 | self.reply.finished.connect(self.handleResponse) 54 | self.reply.uploadProgress.connect(self.onUploadProgress) 55 | self.reply.sslErrors.connect(self.onSslError) 56 | except Exception as e: 57 | self.app.onMessage.emit("{0}: {1}".format(self.app.getLang("error"), str(e))) 58 | print(str(e)) 59 | self.postData=None 60 | self.reply=None 61 | self.app.onUploadFinished.emit(False) 62 | pass 63 | 64 | def handleResponse(self): 65 | state = True 66 | er = self.reply.error() 67 | if er == QNetworkReply.NetworkError.NoError: 68 | self.app.onMessage.emit(self.app.getLang("success")) 69 | else: 70 | self.app.onMessage.emit("{0} {1}:{2}".format(self.app.getLang("error"), er, self.reply.errorString())) 71 | state = False 72 | 73 | self.reply=None 74 | self.postData=None 75 | self.app.onUploadFinished.emit(state) 76 | pass 77 | -------------------------------------------------------------------------------- /plugin/shui/langs.json: -------------------------------------------------------------------------------- 1 | { 2 | "ru": { 3 | "inherited":["en"], 4 | "lang": { 5 | "close": "Закрыть", 6 | "discard": "Отменить", 7 | "save": "Сохранить", 8 | "send": "Отправить", 9 | "reload": "Обновить", 10 | "select": "Выбрать", 11 | "connect": "Подключить", 12 | "disconnect": "Отключить", 13 | "preview": "Превью", 14 | "terminate": "Прервать", 15 | "run": "Выполнить", 16 | "ok": "OK", 17 | 18 | "load-preview": "Превью из", 19 | "from-file": "файла...", 20 | "from-clipboard": "буфер обмена", 21 | 22 | "setup": "Настройка", 23 | "file": "Файл", 24 | "terminal": "Терминал", 25 | "printer": "Принтер", 26 | "language": "Язык", 27 | "proxy": "Прокси", 28 | "yandex": "Яндекс", 29 | "override": "Перезаписывать", 30 | "telegram": "Телеграм", 31 | "alisa": "Алиса", 32 | 33 | "printer-name": "Новый принтер", 34 | "printer-new": "<новый>", 35 | "printer-apply": "Изменить", 36 | "printer-delete": "Удалить", 37 | "printers-options": "Настройки принтеров", 38 | "plugin-options": "Настройки плагина", 39 | "network-options": "Настройки сети", 40 | "auto-close": "Закрыть окно по окончании", 41 | "native-file-dialog": "Системное окно выбора файла", 42 | "preview-aspect-ratio": "Сохранять пропорции", 43 | 44 | "start-printing": "Запустить печать", 45 | "output-name": "Файл", 46 | 47 | "prepared": "Подготовлено", 48 | "success": "Успешно", 49 | "connecting": "Соединение...", 50 | "sent": "Передано", 51 | "error": "Ошибка", 52 | "error-unsupported-action": "Недопустимое действие", 53 | "error-no-printers": "Нет настроенных принтеров", 54 | 55 | "action": "Действие", 56 | "open-file": "Открыть файл", 57 | "save-to-file": "Сохранить файл", 58 | "send-to-printer": "Передать на принтер", 59 | "print-to-printer": "Отправить на печать", 60 | "send-to-yandex": "Сохранить на Яндекс диске" 61 | } 62 | }, 63 | "ua": { 64 | "inherited":["en", "ru"], 65 | "lang": { 66 | 67 | } 68 | }, 69 | "en": { 70 | "lang": { 71 | "title": "SHUI WiFi Plugin", 72 | "close": "Close", 73 | "discard": "Discard", 74 | "save": "Save", 75 | "send": "Send", 76 | "reload": "Reload", 77 | "select": "Select", 78 | "connect": "Connect", 79 | "disconnect": "Disconnect", 80 | "preview": "Preview", 81 | "terminate": "Terminate", 82 | "run": "Run", 83 | "ok": "OK", 84 | 85 | "load-preview": "Preview from", 86 | "from-file": "file...", 87 | "from-clipboard": "clipboard", 88 | 89 | "setup": "Setup", 90 | "file": "File", 91 | "terminal": "Terminal", 92 | "printer": "Printer", 93 | "language": "Language", 94 | "proxy": "Proxy", 95 | "yandex": "Yandex", 96 | "override": "Override", 97 | "telegram": "Telegram", 98 | "alisa": "Alisa", 99 | "telegram-bot-welcome": "Telegram bot console", 100 | 101 | "printer-name": "New printer", 102 | "printer-new": "", 103 | "printer-ip": "192.168.", 104 | "printer-esp32": "Esp32", 105 | "printer-apply": "Apply", 106 | "printer-delete": "Delete", 107 | "printers-options": "Printers options", 108 | "plugin-options": "Plugin options", 109 | "network-options": "Network options", 110 | "auto-close": "Close window on success", 111 | "native-file-dialog": "System file selection window", 112 | "preview-aspect-ratio": "Preserve aspect ratio", 113 | 114 | "start-printing": "Start printing", 115 | "output-name": "File", 116 | 117 | "prepared": "Prepared", 118 | "success": "Success", 119 | "connecting": "Connecting...", 120 | "sent": "Sent", 121 | "error": "Error", 122 | "error-unsupported-action": "Unsupported action", 123 | "error-no-printers": "No printers configured", 124 | 125 | "action": "Action", 126 | "open-file": "Open file", 127 | "save-to-file": "Save to file", 128 | "send-to-printer": "Load to printer", 129 | "print-to-printer": "Send to printer and print", 130 | "send-to-yandex": "Send to Yandex disk" 131 | } 132 | } 133 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SHUI Wifi multislicer plugin 2 | 3 | * Git: https://github.com/vyacheslav-shubin/slicers-plugin.git 4 | * SHUI firmware: https://github.com/vyacheslav-shubin/shui.git 5 | * TG: https://t.me/+8eQiBVI29zw1YTU6 6 | 7 | История изменений: 8 | 9 | v22.05.00 10 | * Начальная версия плагина на основе оригинальной версии v2.0.1 из 11 | https://github.com/vyacheslav-shubin/slicers-plugin 12 | * Добавлен оригинальный скрипт пост-процессинга для генерации превью из 13 | https://github.com/vyacheslav-shubin/shui 14 | 15 | v22.05.04 16 | * Добавлен диалог настройки конфигурации и управления списком принтеров. 17 | * Добавлен файл requirements.txt со списком необходимых пакетов python. 18 | * Удален каталог release с бинарными файлами старых релизов. 19 | * Незначительные фиксы для облегчения переноса плагина в Cura5 с PyQt6. 20 | * Собраны версии плагина и скрипта для Cura4 и Cura5. 21 | 22 | v22.07.07 23 | * Единая версия плагина и скрипта превью для разных версий Cura (4.x и 5.0). 24 | * Добавлена генерация превью в самом плагине (скрипт превью можно не использовать). 25 | * Подстановка имени файла при сохранении файла на диске. 26 | * Свежие изменения из исходного плагина (поддержка сценариев Алисы). 27 | 28 | v22.09.19 29 | * Добавлена поддержка превью в формате PNG/JPG/QOI для Prusa Slicer 2.5. 30 | * Исправлены незначительные ошибки. 31 | 32 | v22.11.20 33 | * Немного изменена основная вкладка передачи файла на печать: 34 | * Список действий перенесен с кнопки OK в основное окно. 35 | * Предотвращает отображение списка действий на другом мониторе. 36 | * Добавлена опция автоматического закрытия окна по окончании передачи файла. 37 | * Решена проблема с обрезанием длинных сообщений об ошибках. 38 | * Исправлены незначительные ошибки. 39 | * Из requiremets.txt удален модуль qoi из-за зависимости от MS VC++ tools на Windows. 40 | * Добавлен requiremets-all.txt со всеми опциональными модулями, включая qoi. 41 | 42 | v22.12.11 43 | * Немного изменена структура окна настройки плагина, добавлены новые опции: 44 | * Автоматическое закрытие окна плагина по завершении передачи файла 45 | * Использование системного окна для выбора файла 46 | * Исправлены незначительные ошибки. 47 | 48 | v23.02.09 49 | * Добавлено запоминание выбранного принтера и каталога для сохранения файла. 50 | * Отменено обрезание префикса (вида CFFFP_) в имени файла, добавляемого слайсером Cura 51 | * вместо этого можно выключить соответствующую опцию в настройках слайсера 52 | "Добвить префикс принтера к имени задачия" / "Add machine prefix to job name" 53 | * Исправлены незначительные ошибки. 54 | 55 | v23.05.20 56 | * Переделана внутренняя работа с картинками превью, извлекаемыми из G-кода. 57 | * Добавлена загрузка картинки превью из внещнего файла или буфера обмена. 58 | * Поддерживается распознавание превью в формате SHUI в сохраненных файлах. 59 | * Убрана зависимость от библиотеки Pillow, чтобы новая функцинальность работала в Cura. 60 | * Исправлены незначительные ошибки. 61 | 62 | v24.05.26 63 | * Добавлены скрипты для удобства установки и запуска плагина: 64 | * setup.bat - установка необходимых модулей python 65 | * shui.bat - запуск плагина из командной строки или проводника 66 | * Упрощение установки и запуска плагина как для слайсеров, так и отдельным приложением: 67 | * Для установки плагина достаточно установть python и запустить скрипт setup.bat по полному пути. 68 | * В настройках слайсера достаточно прописать полный путь к скрипту shui.bat. 69 | * Для запуска плагина достаточно вызвать скрипт shui.bat по полному пути и указать gcode-файл. 70 | Можно запускать скрипт shui.bat из проводника или сделать ярлык для него, 71 | или просто перетаскивать на него мышью любой gcode-файл для передачи на принтер. 72 | * Добавлена чек-сумма CRC при передаче файла по WiFi для проверки целостности переданного файла 73 | на стороне принтера (поддерживается в свежих версиях прошивки SHUI). 74 | * Добавлена внутренняя реализация поддержки формата QOI в превью (спасибо Владимиру Шумову). 75 | Превью в формате QOI теперь показывается в любом случае, независимо от установленных модулей. 76 | Отпала необходимость во внешнем модуле QOI и удален файл requirements-all.txt. 77 | * Переключена зависимость от PyQT5 на PyQT6 по умолчанию, как более актуальную версию. 78 | * В основном окне кнопка "OK" переименована на "Выполнить" ("Run"), как более соответствующее 79 | навание для выполнения выбранного действия. 80 | * Исправлены незначительные ошибки. 81 | -------------------------------------------------------------------------------- /plugin/shui/utils/controls/GCodeActionsControl.py: -------------------------------------------------------------------------------- 1 | from ...PyQt_API import QtCore, QtGui, QtWidgets 2 | 3 | 4 | class ShuiToolButton(QtWidgets.QToolButton): 5 | def __init__(self, parent, app): 6 | super().__init__(parent) 7 | self.app=app 8 | self.setMinimumSize(QtCore.QSize(50, 40)) 9 | 10 | 11 | class ShuiSnippetToolButton(ShuiToolButton): 12 | onSnippet = QtCore.pyqtSignal(object) 13 | snippet=None 14 | 15 | def __init__(self, parent, app, snippet): 16 | super().__init__(parent, app) 17 | self.snippet=snippet 18 | self.clicked.connect(lambda: self.onSnippet.emit(self.snippet)) 19 | 20 | 21 | 22 | class GCodeActionsControl(QtWidgets.QWidget): 23 | step=1 24 | uartControls=[] 25 | def __init__(self, parent, app): 26 | super().__init__(parent) 27 | self.app=app 28 | self.app.onUartConnect.connect(self.onUartConnect) 29 | self.gridLayout = QtWidgets.QGridLayout(self) 30 | 31 | self.tbMoveYPlus=ShuiToolButton(self, app) 32 | self.tbMoveYPlus.setText("Y+") 33 | self.tbMoveYMinus=ShuiToolButton(self, app) 34 | self.tbMoveYMinus.setText("Y-") 35 | 36 | self.tbMoveXPlus=ShuiToolButton(self, app) 37 | self.tbMoveXPlus.setText("X+") 38 | self.tbMoveXMinus=ShuiToolButton(self, app) 39 | self.tbMoveXMinus.setText("X-") 40 | 41 | self.tbMoveZMinus=ShuiToolButton(self, app) 42 | self.tbMoveZMinus.setText("Z-") 43 | 44 | self.tbMoveZPlus=ShuiToolButton(self, app) 45 | self.tbMoveZPlus.setText("Z+") 46 | 47 | self.tbStep=ShuiToolButton(self, app) 48 | self.tbStep.setText(str(self.step)) 49 | 50 | 51 | self.gridLayout.addWidget(self.tbMoveYPlus, 0, 1, 1, 1) 52 | self.gridLayout.addWidget(self.tbMoveXMinus, 1, 0, 1, 1) 53 | self.gridLayout.addWidget(self.tbMoveXPlus, 1, 2, 1, 1) 54 | self.gridLayout.addWidget(self.tbMoveYMinus, 2, 1, 1, 1) 55 | 56 | self.gridLayout.addWidget(self.tbMoveZPlus, 0, 3, 1, 1) 57 | self.gridLayout.addWidget(self.tbMoveZMinus, 2, 3, 1, 1) 58 | 59 | 60 | if "snippets" in self.app.config: 61 | idx=0 62 | self.snippet_btns = [] 63 | for s in self.app.config["snippets"]: 64 | btn=ShuiSnippetToolButton(self, app, self.app.config["snippets"][s]) 65 | btn.setText(s) 66 | self.gridLayout.addWidget(btn, int(3+(idx/4)), int((idx%4)), 1, 1) 67 | self.snippet_btns.append(btn) 68 | btn.onSnippet.connect(self.onSnippet) 69 | idx=idx+1 70 | if idx==8: 71 | break 72 | 73 | self.uartControls=self.uartControls + [self.tbMoveXPlus,self.tbMoveYPlus,self.tbMoveZPlus,self.tbMoveXMinus, self.tbMoveYMinus,self.tbMoveZMinus]+self.snippet_btns 74 | 75 | for c in self.uartControls: 76 | c.setDisabled(True) 77 | 78 | self.gridLayout.addWidget(self.tbStep, 1, 1, 1, 1) 79 | 80 | self.tbStep.clicked.connect(lambda: self.onStep(1)) 81 | 82 | self.tbMoveYPlus.clicked.connect(lambda: self.onMoveXY("Y", 1)) 83 | self.tbMoveYMinus.clicked.connect(lambda: self.onMoveXY("Y", -1)) 84 | self.tbMoveXPlus.clicked.connect(lambda: self.onMoveXY("X", 1)) 85 | self.tbMoveXMinus.clicked.connect(lambda: self.onMoveXY("X", -1)) 86 | self.tbMoveZPlus.clicked.connect(lambda: self.onMoveZ(1)) 87 | self.tbMoveZMinus.clicked.connect(lambda: self.onMoveZ(-1)) 88 | 89 | def onSnippet(self, snippet): 90 | self.app.onUartMessage.emit(snippet) 91 | self.app.wifiUart.send(snippet) 92 | pass 93 | 94 | def onMoveXY(self, axis, direct): 95 | self.onSnippet(self.app.config["templates"]["move_xy_relative"].format(axis, str(direct*self.step))) 96 | pass 97 | 98 | def onMoveZ(self, direct): 99 | self.onSnippet(self.app.config["templates"]["move_z_relative"].format(str(direct*self.step))) 100 | pass 101 | 102 | def onStep(self, event): 103 | if self.step==1: 104 | self.step=10 105 | elif self.step==10: 106 | self.step=50 107 | elif self.step==50: 108 | self.step=1 109 | self.tbStep.setText(str(self.step)) 110 | 111 | def onUartConnect(self, state): 112 | for c in self.uartControls: 113 | c.setDisabled(not state) 114 | pass 115 | -------------------------------------------------------------------------------- /scripts/ShuiPreview.py: -------------------------------------------------------------------------------- 1 | from ..Script import Script 2 | from cura.Snapshot import Snapshot 3 | import os 4 | import base64 5 | 6 | try: 7 | from PyQt6.QtCore import Qt 8 | except Exception: 9 | from PyQt5.QtCore import Qt 10 | 11 | class ShuiPreview(Script): 12 | def __init__(self): 13 | super().__init__() 14 | 15 | def getSettingDataString(self): 16 | return """{ 17 | "name": "Generate SHUI Preview", 18 | "key": "shui_preview", 19 | "metadata": {}, 20 | "version": 2, 21 | "settings": { 22 | "size": { 23 | "label": "Size", 24 | "description": "Size of small preview", 25 | "type": "enum", 26 | "options": { 27 | "50": "small", 28 | "100": "big", 29 | "0": "none" 30 | }, 31 | "default_value": "50" 32 | }, 33 | "format": { 34 | "label": "Format", 35 | "description": "Format preview code", 36 | "type": "enum", 37 | "options": { 38 | "shui": "SHUI", 39 | "mks": "MKS" 40 | }, 41 | "default_value": "shui" 42 | } 43 | } 44 | }""" 45 | 46 | def addScreenshot(self, img, width, height, img_type): 47 | result = "" 48 | b_image = img.scaled(width, height, Qt.AspectRatioMode.KeepAspectRatio) 49 | img_size = b_image.size() 50 | result += img_type 51 | datasize = 0 52 | for i in range(img_size.height()): 53 | for j in range(img_size.width()): 54 | pixel_color = b_image.pixelColor(j, i) 55 | r = pixel_color.red() >> 3 56 | g = pixel_color.green() >> 2 57 | b = pixel_color.blue() >> 3 58 | rgb = (r << 11) | (g << 5) | b 59 | strHex = "%x" % rgb 60 | if len(strHex) == 3: 61 | strHex = '0' + strHex[0:3] 62 | elif len(strHex) == 2: 63 | strHex = '00' + strHex[0:2] 64 | elif len(strHex) == 1: 65 | strHex = '000' + strHex[0:1] 66 | if strHex[2:4] != '': 67 | result += strHex[2:4] 68 | datasize += 2 69 | if strHex[0:2] != '': 70 | result += strHex[0:2] 71 | datasize += 2 72 | if datasize >= 50: 73 | datasize = 0 74 | result += '\rM10086 ;' 75 | if i == img_size.height() - 1: 76 | result += "\r" 77 | return result 78 | 79 | def generate_mks(self, sz, data): 80 | image = Snapshot.snapshot(width = 900, height = 900) 81 | if sz == "50": 82 | pv = self.addScreenshot(image, 50, 50, ";simage:") 83 | else: 84 | pv = self.addScreenshot(image, 100, 100, ";simage:") 85 | pv = pv + self.addScreenshot(image, 200, 200, ";;gimage:") 86 | data.insert(0, pv) 87 | return data 88 | 89 | def generate_shui(self, sz, data): 90 | image = Snapshot.snapshot(width = 900, height = 900) 91 | pv="" 92 | if sz == "50": 93 | pv=pv+";SHUI PREVIEW 50x50\n" + self.generate(image, 50) 94 | else: 95 | pv=pv+";SHUI PREVIEW 100x100\n" + self.generate(image, 100) 96 | pv=pv+self.generate(image, 200) 97 | data.insert(0, pv) 98 | return data 99 | 100 | def execute(self, data): 101 | sz=self.getSettingValueByKey("size") 102 | if sz=="0": 103 | return data 104 | fmt=self.getSettingValueByKey("format") 105 | if fmt=="shui": 106 | return self.generate_shui(sz, data) 107 | if fmt=="mks": 108 | return self.generate_mks(sz, data) 109 | return data 110 | 111 | def generate(self, img, size): 112 | result = "" 113 | b_image = img.scaled(size, size, Qt.AspectRatioMode.KeepAspectRatio) 114 | img_size = b_image.size() 115 | datasize = 0 116 | for i in range(img_size.height()): 117 | row = bytearray() 118 | for j in range(img_size.width()): 119 | pixel_color = b_image.pixelColor(j, i) 120 | r = pixel_color.red() >> 3 121 | g = pixel_color.green() >> 2 122 | b = pixel_color.blue() >> 3 123 | rgb = (r << 11) | (g << 5) | b 124 | row.append((rgb >> 8) & 0xFF) 125 | row.append(rgb & 0xFF) 126 | result += ";" + base64.b64encode(row).decode('utf-8') + "\n" 127 | return result 128 | -------------------------------------------------------------------------------- /plugin/shui/utils/AlisaTab.py: -------------------------------------------------------------------------------- 1 | from ..PyQt_API import (QtCore, QtWidgets, QtGui) 2 | from ..PyQt_API import (QNetworkRequest, QNetworkAccessManager, QNetworkReply, QNetworkProxy) 3 | from .Core import (StartMode, UiTab) 4 | 5 | class AlisaTab(UiTab): 6 | rlist=[] 7 | rows=[] 8 | def __init__(self, app): 9 | super().__init__(app) 10 | self.title = self.app.getLang("alisa") 11 | 12 | self.teConsoleOutput = QtWidgets.QTextEdit(self) 13 | self.teConsoleOutput.setReadOnly(True) 14 | 15 | self.teConsoleOutput.setStyleSheet("*{background-color: black; color:rgb(0,255,0)}") 16 | 17 | self.btReload = QtWidgets.QPushButton() 18 | self.btReload.setMaximumSize(QtCore.QSize(100, 16777215)) 19 | self.btReload.setMinimumWidth(200) 20 | self.btReload.setText(self.app.getLang("reload")) 21 | 22 | 23 | self.mainLayout = QtWidgets.QHBoxLayout() 24 | self.scenarioLayout = QtWidgets.QVBoxLayout() 25 | self.mainLayout.addLayout(self.scenarioLayout) 26 | self.scenarioLayout.addWidget(self.btReload) 27 | 28 | self.mainLayout.addWidget(self.teConsoleOutput) 29 | 30 | self.scenarioWidget = QtWidgets.QWidget() 31 | self.scenarioButtonsLayout = QtWidgets.QVBoxLayout(self.scenarioWidget) 32 | 33 | self.scrolArea = QtWidgets.QScrollArea() 34 | self.scrolArea.setWidget(self.scenarioWidget) 35 | self.scrolArea.setWidgetResizable(True) 36 | self.scenarioLayout.addWidget(self.scrolArea) 37 | 38 | self.setLayout(self.mainLayout) 39 | 40 | self.btReload.clicked.connect(self.loadScenarios) 41 | self.loadScenarios() 42 | 43 | def onSslError(self, reply, sslerror): 44 | pass 45 | 46 | def loadScenarios(self): 47 | for w in self.rlist: 48 | self.scenarioButtonsLayout.removeWidget(w) 49 | self.rlist=[] 50 | self.req=QNetworkRequest(QtCore.QUrl("https://api.iot.yandex.net/v1.0/user/info")) 51 | self.req.setRawHeader(b'Accept', b'application/json') 52 | self.req.setRawHeader(b'Authorization', ('OAuth '+self.app.config["yandex"]["key"]).encode()) 53 | self.reply = self.app.networkManager.get(self.req) 54 | 55 | def handleResponse(): 56 | import json 57 | er = self.reply.error() 58 | if er == QNetworkReply.NetworkError.NoError: 59 | jresp=json.loads(bytes(self.reply.readAll()).decode()) 60 | if "scenarios" in jresp: 61 | self.addScenarioButtons(jresp["scenarios"]) 62 | 63 | self.req=None 64 | self.reply=None 65 | pass 66 | 67 | self.reply.finished.connect(handleResponse) 68 | self.reply.sslErrors.connect(self.onSslError) 69 | 70 | def addRow(self, row): 71 | self.rows.append(row) 72 | if len(self.rows)>20: 73 | self.rows.pop(0) 74 | self.teConsoleOutput.setText("\n".join(self.rows)) 75 | self.teConsoleOutput.verticalScrollBar().setValue(self.teConsoleOutput.verticalScrollBar().maximum()) 76 | pass 77 | 78 | def callScenario(self, id): 79 | self.req=QNetworkRequest(QtCore.QUrl("https://api.iot.yandex.net/v1.0/scenarios/" + str(id) + "/actions")) 80 | self.req.setRawHeader(b'Accept', b'application/json') 81 | self.req.setRawHeader(b'Authorization', ('OAuth '+self.app.config["yandex"]["key"]).encode()) 82 | self.reply = self.app.networkManager.post(self.req, None) 83 | def handleResponse(): 84 | import json 85 | er = self.reply.error() 86 | if er == QNetworkReply.NetworkError.NoError: 87 | jresp=json.loads(bytes(self.reply.readAll()).decode()) 88 | self.addRow(json.dumps(jresp)) 89 | self.req=None 90 | self.reply=None 91 | pass 92 | 93 | self.reply.finished.connect(handleResponse) 94 | self.reply.sslErrors.connect(self.onSslError) 95 | pass 96 | 97 | def addScenarioButtons(self, scenarios): 98 | 99 | class ScenarioButton(QtWidgets.QPushButton): 100 | def __init__(self, owner, id): 101 | super(ScenarioButton, self).__init__() 102 | self.id=id 103 | self.owner=owner 104 | def click(self): 105 | self.owner.callScenario(self.id) 106 | pass 107 | 108 | 109 | for scenario in scenarios: 110 | edit = QtWidgets.QLineEdit() 111 | edit.setText(scenario["id"]) 112 | edit.setReadOnly(True) 113 | self.scenarioButtonsLayout.addWidget(edit) 114 | self.rlist.append(edit) 115 | button = ScenarioButton(self, scenario["id"]) 116 | button.setText(scenario["name"]) 117 | button.clicked.connect(button.click) 118 | self.scenarioButtonsLayout.addWidget(button) 119 | self.rlist.append(button) 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /plugin/shui/utils/qoi/qoi_reader.py: -------------------------------------------------------------------------------- 1 | import struct 2 | 3 | class Header: 4 | def __init__(self, input): 5 | self.magic = None #char magic[4]; // magic 6 | self.width = None #uint32_t width in pixels(BE) 7 | self.height = None #uint32_t height in pixels(BE) 8 | self.channels = None #channels; // 3 = RGB, 4 = RGBA 9 | self.colorspace = None #uint8_t colorspace; // 0 = sRGB with linear alpha // 1 = all channels linear 10 | self.magic, self.width, self.height, self.channels, self.colorspace = struct.unpack(">4sIIcc", input[0:14]) 11 | self.magic = self.magic.decode() 12 | if self.magic!="qoif": 13 | raise Exception("Is not qoi stream") 14 | 15 | self.size = 14 16 | pass 17 | 18 | class QOIReader: 19 | def __init__(self, input): 20 | self.input = input 21 | self.header = Header(input) 22 | self.palette = [(0, 0, 0, 255) for i in range(64)] 23 | self.cursor = self.header.size 24 | self.readed = 0 25 | self.prev = (0, 0, 0, 255) 26 | self.wanted = self.header.width * self.header.height 27 | self.pixel_writer = None 28 | 29 | def set_color(self, color, save_pallete=True): 30 | self.pixel_writer(color) 31 | 32 | if save_pallete: 33 | def hash(color): 34 | r, g, b, a = color 35 | return (r * 3 + g * 5 + b * 7 + a * 11) & 0b111111 36 | self.palette[hash(color)] = color 37 | self.readed = self.readed + 1 38 | self.prev = color 39 | pass 40 | 41 | def rgb(self): 42 | self.set_color( 43 | ( 44 | self.input[self.cursor], 45 | self.input[self.cursor + 1], 46 | self.input[self.cursor + 2], 47 | 255 48 | ) 49 | ) 50 | self.cursor = self.cursor + 3 51 | pass 52 | 53 | def rgba(self): 54 | self.set_color( 55 | ( 56 | self.input[self.cursor], 57 | self.input[self.cursor + 1], 58 | self.input[self.cursor + 2], 59 | self.input[self.cursor + 3] 60 | ) 61 | ) 62 | self.cursor = self.cursor + 4 63 | pass 64 | 65 | def index(self, tag): 66 | color = self.palette[tag & 0b111111] 67 | self.set_color(color, False) 68 | pass 69 | 70 | def diff(self, tag): 71 | dr = ((tag & 0b00110000) >> 4) - 2 72 | dg = ((tag & 0b00001100) >> 2) - 2 73 | db = ((tag & 0b00000011) >> 0) - 2 74 | (r, g, b, a) = self.prev 75 | self.set_color((r + dr, g + dg, b + db, a)) 76 | pass 77 | 78 | def luma(self, tag): 79 | dg = (tag & 0b00111111) - 32 80 | drg = (self.input[self.cursor] & 0b11110000) >> 4 81 | dbg = (self.input[self.cursor] & 0b00001111) >> 0 82 | (r, g, b, a) = self.prev 83 | self.set_color((r + dg - 8 + drg, g + dg, b + dg - 8 + dbg, a)) 84 | self.cursor = self.cursor + 1 85 | pass 86 | 87 | def run(self, tag): 88 | for i in range((tag & 0b00111111) + 1): 89 | self.set_color(self.prev, False) 90 | pass 91 | 92 | def read_chunk(self): 93 | tag = self.input[self.cursor] 94 | self.cursor = self.cursor + 1 95 | if tag==0 and (self.input[self.cursor:self.cursor+7]==b'\0\0\0\0\0\0\1'): 96 | return False 97 | if tag == 0b11111110: 98 | self.rgb() 99 | elif tag == 0b11111111: 100 | self.rgba() 101 | else: 102 | tag_mark = tag & 0b11000000 103 | if tag_mark == 0b00000000: 104 | self.index(tag) 105 | elif tag_mark == 0b01000000: 106 | self.diff(tag) 107 | elif tag_mark == 0b10000000: 108 | self.luma(tag) 109 | elif tag_mark == 0b11000000: 110 | self.run(tag) 111 | return True 112 | 113 | def do_read(self): 114 | while self.readed < self.wanted and self.read_chunk(): 115 | pass 116 | pass 117 | 118 | def image_writer(self, color): 119 | self.img.putpixel( 120 | (self.readed % self.header.width, int(self.readed / self.header.width)), 121 | color 122 | ) 123 | 124 | def array_writer(self, color): 125 | self.img[self.readed] = color 126 | 127 | def asImage(self): 128 | from PIL import Image 129 | 130 | self.img = Image.new( 131 | mode = "RGB" if self.header.channels == 3 else "RGBA", 132 | size = (self.header.width, self.header.height) 133 | ) 134 | 135 | self.pixel_writer = self.image_writer 136 | self.do_read() 137 | 138 | return self.img 139 | 140 | def asArray(self): 141 | self.img = [None for i in range(self.header.width * self.header.height)] 142 | self.pixel_writer = self.array_writer 143 | self.do_read() 144 | return self.img 145 | 146 | -------------------------------------------------------------------------------- /plugin/shui/utils/TelegramTab.py: -------------------------------------------------------------------------------- 1 | from ..PyQt_API import (QtCore, QtWidgets) 2 | import json 3 | from .Core import (StartMode, UiTab) 4 | from ..PyQt_API import (QNetworkRequest, QNetworkAccessManager, QNetworkReply, QNetworkProxy) 5 | 6 | class TelegramTab(UiTab): 7 | rows = [] 8 | tg = None 9 | pooling_data={"limit":1, "offset":-1, "timeout":120} 10 | 11 | def __init__(self, app): 12 | super().__init__(app) 13 | self.title = self.app.getLang("telegram") 14 | self.tg_config = app.config.get("telegram") 15 | 16 | self.teConsoleOutput = QtWidgets.QTextEdit(self) 17 | self.teConsoleOutput.setReadOnly(True) 18 | 19 | self.teConsoleOutput.setStyleSheet("*{background-color: black; color:rgb(255,255,0)}") 20 | self.slGCodeMessage = QtWidgets.QLineEdit(self) 21 | 22 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Fixed) 23 | sizePolicy.setHorizontalStretch(0) 24 | sizePolicy.setVerticalStretch(0) 25 | sizePolicy.setHeightForWidth(self.slGCodeMessage.sizePolicy().hasHeightForWidth()) 26 | 27 | self.slGCodeMessage.setSizePolicy(sizePolicy) 28 | self.teConsoleOutput.setSizePolicy(sizePolicy) 29 | 30 | self.btSenb = QtWidgets.QPushButton() 31 | self.btSenb.setMaximumSize(QtCore.QSize(100, 16777215)) 32 | self.btSenb.setMinimumWidth(100) 33 | self.btSenb.setText(self.app.getLang("send")) 34 | #self.btSenb.setDisabled(True) 35 | 36 | 37 | self.mainLayout = QtWidgets.QVBoxLayout(self) 38 | self.mainLayout.addWidget(self.teConsoleOutput) 39 | #self.setLayout(self.mainLayout) 40 | 41 | self.sendLayout = QtWidgets.QHBoxLayout() 42 | self.sendLayout.addWidget(self.slGCodeMessage) 43 | self.sendLayout.addWidget(self.btSenb) 44 | self.sendLayout.setContentsMargins(0, 0, 0, 0) 45 | self.mainLayout.addLayout(self.sendLayout) 46 | 47 | self.btSenb.clicked.connect(self.doSend) 48 | self.addRow(self.app.getLang("telegram-bot-welcome")) 49 | 50 | #self.tg=TgClient(self.app) 51 | #self.tg.listen() 52 | #self.tg.onMessage.connect(self.onMessage) 53 | self.pooling() 54 | 55 | def pooling(self): 56 | tg_key = self.tg_config.get("key") 57 | if not tg_key: 58 | return 59 | try: 60 | import json 61 | tg_url="https://api.telegram.org/bot"+tg_key+"/getUpdates" 62 | self.req=QNetworkRequest(QtCore.QUrl(tg_url)) 63 | self.req.setRawHeader(b'Content-Type', b'application/json') 64 | post_data=json.dumps(self.pooling_data).encode("utf-8") 65 | self.reply = self.app.networkManager.post(self.req, post_data) 66 | def handleResponse(): 67 | er = self.reply.error() 68 | if er == QNetworkReply.NetworkError.NoError: 69 | jresp=json.loads(bytes(self.reply.readAll()).decode()) 70 | for result_json in jresp["result"]: 71 | self.pooling_data["offset"]=result_json["update_id"]+1 72 | if "message" in result_json: 73 | self.onMessage(result_json["message"]) 74 | self.req=None 75 | self.reply=None 76 | self.pooling() 77 | pass 78 | 79 | self.reply.finished.connect(handleResponse) 80 | self.reply.sslErrors.connect(self.onSslError) 81 | except Exception as err: 82 | self.showMessage("", str(err)) 83 | pass 84 | 85 | def onSslError(self, reply, sslerror): 86 | pass 87 | 88 | def onMessage(self, message): 89 | text = message.get("text") 90 | if text: 91 | name="" 92 | sender_chat=message.get("sender_chat") 93 | if sender_chat!=None: 94 | name=sender_chat.get("title") 95 | else: 96 | name_from=message.get("from") 97 | if name_from: 98 | fn=name_from.get("first_name") 99 | ln=name_from.get("last_name") 100 | if fn: 101 | name=fn 102 | if ln: 103 | if name: 104 | name=name+" " 105 | name=name+ln 106 | self.showMessage(name, text) 107 | pass 108 | 109 | def showMessage(self, name, text): 110 | self.addRow("{0}: {1}".format(name, text)) 111 | 112 | def start(self): 113 | pass 114 | 115 | def __del__(self): 116 | # if self.tg: 117 | # self.tg.kill() 118 | pass 119 | 120 | def keyPressEvent(self, event): 121 | if not self.doSendKeyPress(event): 122 | super().keyPressEvent(event) 123 | 124 | 125 | def addRow(self, row): 126 | self.rows.append(row) 127 | if len(self.rows)>20: 128 | self.rows.pop(0) 129 | self.teConsoleOutput.setText("\n".join(self.rows)) 130 | self.teConsoleOutput.verticalScrollBar().setValue(self.teConsoleOutput.verticalScrollBar().maximum()) 131 | pass 132 | 133 | def doSend(self): 134 | text=self.slGCodeMessage.text() 135 | if (len(text)>0): 136 | tg_url="https://api.telegram.org/bot"+self.tg_config.get("key")+"/sendMessage" 137 | self.req_sm=QNetworkRequest(QtCore.QUrl(tg_url)) 138 | self.req_sm.setRawHeader(b'Content-Type', b'application/json') 139 | post_data=json.dumps({"chat_id":self.tg_config.get("chat_id"), "text":text}).encode("utf-8") 140 | self.reply_sm = self.app.networkManager.post(self.req_sm, post_data) 141 | 142 | def handleResponse(): 143 | self.req_sm=None 144 | self.reply_sm=None 145 | pass 146 | 147 | self.reply_sm.finished.connect(handleResponse) 148 | self.reply_sm.sslErrors.connect(self.onSslError) 149 | 150 | self.slGCodeMessage.setSelection(0, len(text)) 151 | self.addRow(text) 152 | pass 153 | 154 | def doSendKeyPress(self, event): 155 | if (event.key() == QtCore.Qt.Key.Key_Enter) or (event.key() == QtCore.Qt.Key.Key_Return): 156 | self.doSend() 157 | return True 158 | return False 159 | -------------------------------------------------------------------------------- /plugin/shui/utils/FileTab.py: -------------------------------------------------------------------------------- 1 | import os 2 | from ..PyQt_API import (QtCore, QtWidgets, QtGui) 3 | from .Core import (StartMode, UiTab, Preview) 4 | 5 | class FileTab(UiTab): 6 | preview_size = 200 7 | preview = None 8 | parser = None 9 | locked = False 10 | 11 | def __init__(self, app): 12 | super().__init__(app) 13 | self.title = self.app.getLang("file") 14 | self.app.onUploadFinished.connect(self.onFinished) 15 | self.app.onProgress.connect(self.onProgress) 16 | self.app.onMessage.connect(self.onMessage) 17 | 18 | # preview picture 19 | self.bigPic = QtWidgets.QLabel() 20 | self.bigPic.setFixedWidth(self.preview_size) 21 | self.bigPic.setFixedHeight(self.preview_size) 22 | 23 | # preview buttons 24 | self.previewLabel = QtWidgets.QLabel(self.app.getLang("load-preview")) 25 | self.loadPreviewButton = QtWidgets.QPushButton(self.app.getLang("from-file")) 26 | self.pastePreviewButton = QtWidgets.QPushButton(self.app.getLang("from-clipboard")) 27 | 28 | # self.cbStartPrinting = QtWidgets.QCheckBox(self.app.getLang("start-printing")) 29 | # self.cbStartPrinting.setChecked(True) 30 | 31 | # print options 32 | self.cbAutoClose = QtWidgets.QCheckBox(self.app.getLang("auto-close")) 33 | self.cbAutoClose.setChecked(self.app.config.get("autoClose", False)) 34 | 35 | # print filename 36 | self.leFileName = QtWidgets.QLineEdit() 37 | self.leFileName.setMaxLength(64) 38 | 39 | # print actions 40 | # actionsLabel = QtWidgets.QLabel(self.app.getLang("action")) 41 | self.actionsSelect = QtWidgets.QComboBox() 42 | self.actionsMap = self.makeActionsMap() 43 | actions = [self.app.getLang(act) for act in self.actionsMap.keys()] 44 | self.actionsSelect.addItems(actions) 45 | self.actionsSelect.setCurrentIndex(0) 46 | 47 | # print progress 48 | self.progress=QtWidgets.QProgressBar() 49 | self.progress.setMaximum(100) 50 | self.progress.setValue(0) 51 | 52 | # print log 53 | self.progress_label=QtWidgets.QLabel() 54 | self.progress_label.setWordWrap(True) 55 | self.progress_label.setText("---") 56 | self.okButton = QtWidgets.QPushButton(self.app.getLang("run")) 57 | 58 | # left area layout 59 | previewButtons = QtWidgets.QHBoxLayout() 60 | previewButtons.addWidget(self.previewLabel) 61 | previewButtons.addWidget(self.loadPreviewButton) 62 | previewButtons.addWidget(self.pastePreviewButton) 63 | 64 | leftArea = QtWidgets.QVBoxLayout() 65 | leftArea.addWidget(self.bigPic) 66 | leftArea.addStretch() 67 | leftArea.addLayout(previewButtons) 68 | 69 | # right area layout 70 | fileNameLayout = QtWidgets.QHBoxLayout() 71 | fileNameLayout.addWidget(QtWidgets.QLabel(self.app.getLang("output-name"))) 72 | fileNameLayout.addWidget(self.leFileName) 73 | 74 | if self.app.startMode!=StartMode.CURA: 75 | self.btFileSelect = QtWidgets.QToolButton() 76 | self.btFileSelect.setText(self.app.getLang("select")) 77 | fileNameLayout.addWidget(self.btFileSelect) 78 | self.btFileSelect.clicked.connect(self.selectFile) 79 | 80 | actionsLayout = QtWidgets.QHBoxLayout() 81 | # actionsLayout.addWidget(actionsLabel) 82 | actionsLayout.addWidget(self.actionsSelect) 83 | actionsLayout.addStretch() 84 | 85 | buttonsLayout = QtWidgets.QHBoxLayout() 86 | buttonsLayout.addStretch() 87 | buttonsLayout.addWidget(self.okButton) 88 | 89 | rightArea = QtWidgets.QVBoxLayout() 90 | rightArea.addLayout(fileNameLayout) 91 | # rightArea.addWidget(self.cbStartPrinting) 92 | rightArea.addLayout(actionsLayout) 93 | rightArea.addWidget(self.cbAutoClose) 94 | rightArea.addWidget(self.progress) 95 | rightArea.addWidget(self.progress_label) 96 | rightArea.addStretch() 97 | rightArea.addLayout(buttonsLayout) 98 | 99 | # main layout 100 | mainLayout=QtWidgets.QHBoxLayout() 101 | self.setLayout(mainLayout) 102 | mainLayout.addLayout(leftArea) 103 | mainLayout.addLayout(rightArea) 104 | 105 | # button actions 106 | self.loadPreviewButton.clicked.connect(self.onLoadPreview) 107 | self.pastePreviewButton.clicked.connect(self.onPastePreview) 108 | self.okButton.clicked.connect(self.onOk) 109 | 110 | self.startPrint = False 111 | self.preview = Preview() 112 | self.preview_mode = self.app.config.get("preview", "small") 113 | 114 | self.loadSource() 115 | pass 116 | 117 | def makeActionsMap(self): 118 | actions = { 119 | "print-to-printer": self.onSendToWifi, 120 | "send-to-printer": self.onSendToWifi, 121 | "save-to-file": self.onSaveToFile, 122 | } 123 | yandex_config = self.app.config.get("yandex") 124 | if yandex_config and (yandex_config.get("key")!="") \ 125 | and yandex_config.get("enabled", True): 126 | actions["send-to-yandex"] = self.onSendToYandexDisk 127 | return actions 128 | 129 | def selectFile(self): 130 | dir = self.app.config.get("loadFileDir") 131 | if not dir and self.app.inputFileName: 132 | dir = os.path.dirname(os.path.abspath(self.app.inputFileName)) 133 | if self.app.selectFile(dir): 134 | self.app.saveFileDir("loadFileDir", None, self.app.inputFileName) 135 | self.loadSource() 136 | pass 137 | 138 | def onLoadPreview(self): 139 | pattern = "Image Files (*.png *.jpg *.jpeg *.gif *.tif *.tiff *.bmp);;All Files (*)" 140 | try: 141 | dir = self.app.config.get("loadImageDir") 142 | if not dir: 143 | dir = self.app.config.get("loadFileDir") 144 | if not dir and self.app.inputFileName: 145 | dir = os.path.dirname(os.path.abspath(self.app.inputFileName)) 146 | filename = self.app.selectFileDialog(self.app.getLang("open-file"), dir, None, pattern) 147 | if filename: 148 | self.app.saveFileDir("loadImageDir", None, filename) 149 | self.preview.loadFromFile(filename) 150 | self.showPreview() 151 | except Exception as err: 152 | self.onErrorMessage(str(err)) 153 | # raise 154 | pass 155 | 156 | def onPastePreview(self): 157 | try: 158 | self.preview.loadFromClipboard() 159 | self.showPreview() 160 | except Exception as err: 161 | self.onErrorMessage(str(err)) 162 | # raise 163 | pass 164 | 165 | def onOk(self, a): 166 | if self.locked: 167 | if self.sender is not None and self.sender.reply is not None: 168 | if self.sender.reply.isRunning(): 169 | self.sender.reply.abort() 170 | else: 171 | idx = self.actionsSelect.currentIndex() 172 | actions = list(self.actionsMap.keys()) 173 | actionName = actions[idx] 174 | self.startPrint = (actionName == "print-to-printer") 175 | action = self.actionsMap.get(actionName) 176 | if action == None: 177 | self.onErrorMessage(self.app.getLang("error-unsupported-action")) 178 | else: 179 | self.onMessage("---") 180 | action() 181 | pass 182 | 183 | def onProgress(self, current, max): 184 | self.progress.setMaximum(int(max)) 185 | self.progress.setValue(int(current)) 186 | pass 187 | 188 | def onMessage(self, message): 189 | self.progress_label.setText(message) 190 | pass 191 | 192 | def onErrorMessage(self, message): 193 | self.onMessage("{}: {}".format(self.app.getLang("error"), message)) 194 | pass 195 | 196 | def onSaveToFile(self): 197 | try: 198 | self.onProgress(0, 1) 199 | filename = self.leFileName.text() 200 | from .FileSaver import FileSaver 201 | self.lockUILock(True) 202 | fileSaver=FileSaver(self.app) 203 | # self.sender=fileSaver 204 | rows = self.parser.getProcessedGcode() 205 | fileSaver.save(rows, filename) 206 | except Exception as e: 207 | self.onErrorMessage(str(e)) 208 | self.onFinished(False) 209 | # raise 210 | 211 | def onSendToYandexDisk(self): 212 | try: 213 | self.onProgress(0, 1) 214 | from .YandexSender import YandexSender 215 | self.lockUILock(True) 216 | wifiSender=YandexSender(self.app, self.leFileName.text()) 217 | self.sender=wifiSender 218 | rows = self.parser.getProcessedGcode() 219 | wifiSender.save(rows) 220 | except Exception as e: 221 | self.onErrorMessage(str(e)) 222 | self.onFinished(False) 223 | 224 | def onSendToWifi(self): 225 | try: 226 | self.onProgress(0, 1) 227 | from .WifiSender import WifiSender 228 | wifiSender=WifiSender(self.app, self.leFileName.text()) 229 | self.lockUILock(True) 230 | rows = self.parser.getProcessedGcode() 231 | wifiSender.save(rows, start=self.startPrint) 232 | self.sender=wifiSender 233 | except Exception as e: 234 | self.onErrorMessage(str(e)) 235 | self.onFinished(False) 236 | 237 | def lockUILock(self, locked): 238 | if locked: 239 | self.okButton.setText(self.app.getLang("terminate")) 240 | else: 241 | self.okButton.setText(self.app.getLang("run")) 242 | self.locked=locked 243 | pass 244 | 245 | def showPreview(self): 246 | keep_aspect_ratio = self.app.config.get("keepPreviewAspectRatio") 247 | # QPixmap pixmap 248 | pixmap = self.preview.getVisualPixmap(self.preview_size, self.preview_size, keep_aspect_ratio) 249 | if pixmap is not None: 250 | self.bigPic.setPixmap(pixmap) 251 | if True: 252 | qimg = self.preview.getImage() 253 | fmt = self.preview.getFormat() 254 | if False: 255 | if qimg: 256 | self.onMessage("{}: {}".format(self.app.getLang("info"), "loaded preview: " + str(fmt))) 257 | else: 258 | self.onMessage("{}: {}".format(self.app.getLang("info"), "no preview")) 259 | pass 260 | 261 | def loadSource(self): 262 | # choose proper gcode parser 263 | if self.app.startMode==StartMode.PRUSA or self.app.startMode==StartMode.STANDALONE: 264 | if self.app.inputFileName is None or not os.path.exists(self.app.inputFileName): 265 | return 266 | from .PrusaGcodeParser import PrusaGCodeParser 267 | self.parser=PrusaGCodeParser(self.app, self.preview, self.app.inputFileName) 268 | elif self.app.startMode==StartMode.CURA: 269 | from .CuraGCodeParser import CuraGCodeParser 270 | self.parser=CuraGCodeParser(self.app, self.preview) 271 | 272 | # parse gcode and extract preview 273 | self.bigPic.clear() 274 | self.parser.parse() 275 | #self.preview.loadFromParser(self.parser) 276 | self.showPreview() 277 | 278 | # set uptput file naem 279 | if self.app.outputFileName is not None: 280 | self.leFileName.setText(self.app.outputFileName) 281 | pass 282 | 283 | def onFinished(self, state): 284 | self.lockUILock(False) 285 | self.sender=None 286 | autoClose = self.cbAutoClose.isChecked() 287 | if state and autoClose and self.app.mainWidget: 288 | self.app.mainWidget.doClose() 289 | pass 290 | -------------------------------------------------------------------------------- /plugin/shui/MainUI.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from sys import argv 4 | import json 5 | 6 | #from PyQt5 import (QtCore, QtWidgets) 7 | #from PyQt5.QtNetwork import (QNetworkAccessManager, QNetworkProxy) 8 | from .PyQt_API import (QtCore, QtWidgets) 9 | from .PyQt_API import (QNetworkAccessManager, QNetworkProxy) 10 | from .utils import (ConnectionThread, Core, SetupDialog, ConsoleTab, FileTab, PrinterControlTab, TelegramTab, AlisaTab) 11 | 12 | class App(QtCore.QObject): 13 | wifiUart=None 14 | config=None 15 | plugin=None 16 | version="?.?.?" 17 | name="SHUI WiFi Plugin" 18 | title=None 19 | selectedPrinter = 0 20 | startMode = Core.StartMode.UNKNOWN 21 | outputFileName=None 22 | inputFileName=None 23 | 24 | onProgress = QtCore.pyqtSignal(object, object) 25 | onMessage = QtCore.pyqtSignal(object) 26 | onUploadFinished = QtCore.pyqtSignal(object) 27 | onUartRow = QtCore.pyqtSignal(object) 28 | onUartMessage = QtCore.pyqtSignal(object) 29 | onUartConnect = QtCore.pyqtSignal(object) 30 | 31 | def __init__(self, appStartMode, **kwargs): 32 | super().__init__() 33 | self.startMode=appStartMode 34 | self.inputFileName=None 35 | if appStartMode==Core.StartMode.PRUSA: 36 | self.startMode = Core.StartMode.PRUSA 37 | self.outputFileName = os.getenv('SLIC3R_PP_OUTPUT_NAME') 38 | if self.outputFileName is not None: 39 | self.outputFileName=os.path.basename(self.outputFileName) 40 | if len(sys.argv) > 1: 41 | self.inputFileName=sys.argv[1] 42 | elif appStartMode==Core.StartMode.STANDALONE: 43 | if len(sys.argv) > 1: 44 | self.inputFileName=sys.argv[1] 45 | pass 46 | elif appStartMode==Core.StartMode.CURA: 47 | self.inputFileName = "shui_cura.gcode" 48 | if "output_file_name" in kwargs: 49 | self.outputFileName = kwargs["output_file_name"] 50 | 51 | if not self.inputFileName: 52 | self.inputFileName = os.path.join(os.path.dirname(os.path.abspath(__file__)),"..", "shui_prusa.gcode") 53 | if not self.outputFileName: 54 | self.outputFileName = os.path.basename(self.inputFileName) 55 | self.wifiUart = ConnectionThread(self) 56 | 57 | config_file_name="config_local.json" if os.getenv('USER')=='shubin' else "config.json" 58 | self.config_file=os.path.join(os.path.dirname(os.path.abspath(__file__)),"..", config_file_name) 59 | self.config = self.loadConfig() 60 | 61 | plugin_file_name="plugin_local.json" if os.getenv('USER')=='shubin' else "plugin.json" 62 | self.plugin_file=os.path.join(os.path.dirname(os.path.abspath(__file__)),"..", plugin_file_name) 63 | self.plugin = self.loadPluginConfig() 64 | if self.plugin and self.plugin.get("version"): 65 | self.name = self.plugin.get("name", self.name) 66 | self.version = self.plugin.get("version", self.version) 67 | 68 | self.lang_file=os.path.join(os.path.dirname(os.path.abspath(__file__)),"langs.json") 69 | self.langs_cfg = self.loadLang() 70 | 71 | selected=self.config["language"] 72 | self.lang={} 73 | if "inherited" in self.langs_cfg[selected]: 74 | for inh in self.langs_cfg[selected]["inherited"]: 75 | self.lang=self.langs_cfg[inh]["lang"] 76 | self.lang.update(self.langs_cfg[selected]["lang"]) 77 | 78 | self.name = self.getLang("title", self.name) 79 | self.title = "{} (v{})".format(self.name, self.version) 80 | 81 | self.selectedPrinter = self.config.get("selectedPrinter", 0) 82 | printers = self.config.get("printers") 83 | if not printers or self.selectedPrinter < 0 or self.selectedPrinter >= len(printers): 84 | self.selectedPrinter = 0 85 | 86 | self.proxy=QNetworkProxy() 87 | if "proxy" in self.config: 88 | proxy_config=self.config["proxy"] 89 | if proxy_config["enabled"]: 90 | if "host" in proxy_config: 91 | self.proxy.setHostName(proxy_config["host"]) 92 | if "port" in proxy_config: 93 | self.proxy.setPort(proxy_config["port"]) 94 | if "user" in proxy_config: 95 | self.proxy.setUser(proxy_config["user"]) 96 | if "password" in proxy_config: 97 | self.proxy.setPassword(proxy_config["password"]) 98 | self.proxy.setType(QNetworkProxy.ProxyType.HttpProxy) 99 | 100 | self.networkManager = QNetworkAccessManager() 101 | self.networkManager.setProxy(self.proxy) 102 | self.mainWidget = None 103 | 104 | pass 105 | 106 | def selectFileDialog(self, title, dir=None, filename=None, pattern=None): 107 | options = QtWidgets.QFileDialog.Option(0) 108 | if not self.config.get("nativeFileDialog", True): 109 | options |= QtWidgets.QFileDialog.Option.DontUseNativeDialog 110 | if not pattern: 111 | pattern = "GCODE Files (*.gcode *.gco);;All Files (*)" 112 | if filename: 113 | filename, _ = QtWidgets.QFileDialog.getSaveFileName(self.mainWidget, title, filename, pattern, options=options) 114 | else: 115 | filename, _ = QtWidgets.QFileDialog.getOpenFileName(self.mainWidget, title, dir, pattern, options=options) 116 | return filename 117 | 118 | def selectFile(self, dir=None): 119 | fileName = self.selectFileDialog(self.getLang("open-file"), dir) 120 | if fileName: 121 | self.inputFileName=fileName 122 | self.outputFileName = os.path.basename(self.inputFileName) 123 | return True 124 | return False 125 | 126 | def saveFileDir(self, key, dir, filename): 127 | if not dir and filename: 128 | dir = os.path.dirname(os.path.abspath(filename)) 129 | if dir: 130 | self.config[key] = dir 131 | self.saveConfig() 132 | 133 | def getLang(self, text, default = None): 134 | if text and self.lang and text in self.lang: 135 | return self.lang[text] 136 | if default is not None: 137 | return default 138 | return text 139 | 140 | def makePrinterItem(self, p): 141 | item = "{} ({})".format(p.get("name", "?"), p.get("ip", "?")) 142 | # if p.get("esp32", False): 143 | # item += " / " + self.getLang("esp32") 144 | return item 145 | 146 | def loadLang(self): 147 | with open(self.lang_file, encoding="utf-8") as lf: 148 | langs_cfg=json.load(lf) 149 | lf.close() 150 | return langs_cfg 151 | 152 | def loadPluginConfig(self): 153 | with open(self.plugin_file, encoding="utf-8") as jf: 154 | cfg=json.load(jf) 155 | jf.close() 156 | return cfg 157 | 158 | def loadConfig(self): 159 | with open(self.config_file, encoding="utf-8") as jf: 160 | cfg=json.load(jf) 161 | jf.close() 162 | return cfg 163 | 164 | def saveConfig(self): 165 | with open(self.config_file, "w", encoding="utf-8") as jf: 166 | json.dump(self.config, jf, indent=4, ensure_ascii=False) 167 | jf.close() 168 | pass 169 | 170 | def onUpdateConfig(self): 171 | self.saveConfig() 172 | if self.mainWidget: 173 | self.mainWidget.updatePrinters() 174 | 175 | class MainWidget(QtWidgets.QDialog): 176 | def __init__(self, app): 177 | super().__init__() 178 | self.app=app 179 | self.app.mainWidget = self 180 | self.setWindowTitle(self.app.title) 181 | self.setBaseSize(500, 300) 182 | # self.setFixedSize(500, 300) 183 | self.setSizeGripEnabled(False) 184 | 185 | self.mainLayout = QtWidgets.QVBoxLayout(self) 186 | self.mainLayout.setContentsMargins(2, 2, 2, 2) 187 | self.mainLayout.setSpacing(0) 188 | 189 | self.printerSelectLayout = QtWidgets.QHBoxLayout() 190 | self.cbPrinterSelect = QtWidgets.QComboBox(self) 191 | self.cbPrinterSelect.currentIndexChanged.connect(self.printerChanged) 192 | self.updatePrinters() 193 | 194 | self.btConnect = QtWidgets.QPushButton(self) 195 | self.btConnect.setMaximumSize(QtCore.QSize(100, 16777215)) 196 | 197 | self.btSetup = QtWidgets.QPushButton(self) 198 | self.btSetup.setMaximumSize(QtCore.QSize(100, 16777215)) 199 | self.btSetup.setText(self.app.getLang("setup")) 200 | 201 | self.btClose = QtWidgets.QPushButton(self) 202 | self.btClose.setMaximumSize(QtCore.QSize(100, 16777215)) 203 | self.btClose.setText(self.app.getLang("close")) 204 | 205 | self.printerSelectLayout.addWidget(self.cbPrinterSelect) 206 | self.printerSelectLayout.addWidget(self.btConnect) 207 | self.printerSelectLayout.addWidget(self.btSetup) 208 | self.printerSelectLayout.addWidget(self.btClose) 209 | 210 | self.printerSelectLayout.setContentsMargins(2, 2, 2, 2) 211 | 212 | self.tabWidget = QtWidgets.QTabWidget(self) 213 | self.tabWidget.currentChanged.connect(self.tabChanged) 214 | 215 | self.mainLayout.addLayout(self.printerSelectLayout) 216 | self.mainLayout.addWidget(self.tabWidget) 217 | 218 | self.makeTabs() 219 | 220 | self.btSetup.clicked.connect(self.doSetup) 221 | self.btConnect.clicked.connect(self.doConnect) 222 | self.btClose.clicked.connect(self.doClose) 223 | self.app.onUartConnect.connect(self.doOnConnect) 224 | self.doOnConnect(False) 225 | pass 226 | 227 | def tabChanged(self, index): 228 | self.btConnect.setVisible(self.tabs[index].view_connect) 229 | pass 230 | 231 | def doClose(self): 232 | self.close() 233 | pass 234 | 235 | def makeTabs(self): 236 | self.tabs = [] 237 | 238 | tab = FileTab(self.app) 239 | self.tabs.append(tab) 240 | self.tabWidget.addTab(tab, tab.title) 241 | 242 | tab = PrinterControlTab(self.app) 243 | self.tabs.append(tab) 244 | self.tabWidget.addTab(tab, tab.title) 245 | 246 | tab = ConsoleTab(self.app) 247 | self.tabs.append(tab) 248 | self.tabWidget.addTab(tab, tab.title) 249 | 250 | tg_config=self.app.config.get("telegram") 251 | if tg_config and tg_config.get("enabled", False) and tg_config.get("key") and tg_config.get("chat_id"): 252 | tab = TelegramTab(self.app) 253 | self.tabs.append(tab) 254 | self.tabWidget.addTab(tab, tab.title) 255 | yandex_config=self.app.config.get("yandex") 256 | if yandex_config and yandex_config.get("enabled", False) and yandex_config.get("key"): 257 | tab = AlisaTab(self.app) 258 | self.tabs.append(tab) 259 | self.tabWidget.addTab(tab, tab.title) 260 | pass 261 | 262 | def doSetup(self): 263 | dialog = SetupDialog(self, self.app) 264 | dialog.exec() 265 | 266 | def doConnect(self): 267 | if self.app.wifiUart.connected: 268 | self.app.wifiUart.disconnect() 269 | elif self.app.config["printers"] and self.app.selectedPrinter >= 0: 270 | self.app.wifiUart.connect(self.app.config["printers"][self.app.selectedPrinter]["ip"]) 271 | pass 272 | 273 | def doOnConnect(self, connected): 274 | if connected: 275 | self.btConnect.setText(self.app.getLang("disconnect")) 276 | else: 277 | self.btConnect.setText(self.app.getLang("connect")) 278 | 279 | def updatePrinters(self): 280 | printers = self.app.config.get("printers", []) 281 | items = [self.app.makePrinterItem(p) for p in printers] 282 | idx = self.app.selectedPrinter 283 | self.cbPrinterSelect.clear() 284 | self.cbPrinterSelect.addItems(items) 285 | if (idx < 0): 286 | idx = 0 287 | if (idx >= len(printers)): 288 | idx = len(printers) - 1 289 | self.cbPrinterSelect.setCurrentIndex(idx) 290 | self.printerChanged(idx) 291 | pass 292 | 293 | def printerChanged(self, index): 294 | if index >= 0: 295 | self.app.selectedPrinter = index 296 | self.app.config["selectedPrinter"] = index 297 | self.app.saveConfig() 298 | pass 299 | 300 | def makeForm(startMode, **kwargs): 301 | app=App(startMode, **kwargs) 302 | form = MainWidget(app) 303 | form.show() 304 | printers = app.config.get("printers", []) 305 | if len(printers) <= 0: 306 | form.doSetup() 307 | return form 308 | 309 | def cura_application(**kwargs): 310 | return makeForm(Core.StartMode.CURA, **kwargs) 311 | 312 | def qt_application(startMode): 313 | import sys 314 | application = QtWidgets.QApplication(sys.argv) 315 | form = makeForm(startMode) 316 | sys.exit(application.exec()) 317 | -------------------------------------------------------------------------------- /plugin/shui/utils/Core.py: -------------------------------------------------------------------------------- 1 | import base64 2 | from enum import Enum 3 | from ..PyQt_API import (Qt, QtWidgets, Qt, QGuiApplication, QImage, QPixmap, QSize) 4 | 5 | class StartMode(Enum): 6 | UNKNOWN = 0 7 | CURA = 1 8 | PRUSA = 2 9 | STANDALONE = 3 10 | 11 | PreviewModes = { 12 | "none": 0, 13 | "small": 50, 14 | "big": 100 15 | } 16 | 17 | class UiTab(QtWidgets.QWidget): 18 | view_connect=False 19 | def __init__(self, app): 20 | super().__init__() 21 | self.app=app 22 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Fixed, QtWidgets.QSizePolicy.Policy.Fixed) 23 | sizePolicy.setHorizontalStretch(0) 24 | sizePolicy.setVerticalStretch(0) 25 | sizePolicy.setHeightForWidth(self.sizePolicy().hasHeightForWidth()) 26 | self.setSizePolicy(sizePolicy) 27 | 28 | class GCodeSource: 29 | def __init__(self, app, preview, preview_gen): 30 | self.app = app 31 | self.preview = preview 32 | self.gcode = None 33 | self.rows = [] 34 | self.gen = preview_gen 35 | self.preview = preview 36 | pass 37 | 38 | def getKeepAspectRatio(self): 39 | return self.app.config.get("keepPreviewAspectRatio") 40 | 41 | def getPreviewMode(self): 42 | return self.app.config.get("preview") 43 | 44 | def getProcessedGcode(self): 45 | preview_mode = self.getPreviewMode() 46 | keep_acpect_ratio = self.getKeepAspectRatio() 47 | return self.gen.generateAllGcode(self.rows, self.preview, preview_mode, keep_acpect_ratio) 48 | 49 | def makeImageForSHUI(self, data, out_size): 50 | image = self.gen.restoreShuiPreview(data, out_size) 51 | return image 52 | 53 | def makeImageForQOI(self, data): 54 | # format = "QOI" 55 | # qimg = QImage() 56 | # qimg.loadFromData(data, format) 57 | # import qoi 58 | # im = qoi.decode(data) 59 | # print(im) 60 | # format = QImage.Format.Format_ARGB32 61 | # format = QImage.Format.Format_RGBA8888 62 | # qimg = QImage(im, im.shape[0], im.shape[1], format) 63 | 64 | qimg = None 65 | from .qoi.qoi_reader import QOIReader 66 | reader = QOIReader(data) 67 | if reader and reader.header and reader.header.width and reader.header.height: 68 | arr = reader.asArray() 69 | if arr: 70 | # chans = int.from_bytes(reader.header.channels, "big") 71 | # print("reader.header.channels", chans) 72 | # print("reader.header.width", reader.header.width) 73 | # print("reader.header.height", reader.header.height) 74 | # print("reader.header.count", reader.header.width * reader.header.height) 75 | # print("reader.header.size", reader.header.width * reader.header.height * chans) 76 | im = [] 77 | for i in range(0, len(arr)): 78 | t = arr[i] 79 | for j in range(0, len(t)): 80 | im.append(t[j]) 81 | im = bytes(im) 82 | format = QImage.Format.Format_RGBA8888 83 | qimg = QImage(im, reader.header.width, reader.header.height, format) 84 | return qimg 85 | 86 | def makeImageForQOI_PIL(self, data): 87 | from PIL import Image 88 | import qoi 89 | im = qoi.decode(data) 90 | img = Image.fromarray(im) 91 | qimg = self.convertPILtoQImage(img) 92 | return qimg 93 | 94 | def convertPILtoQImage(self, img): 95 | # img = img.convert("RGBA") 96 | data = img.tobytes("raw","RGBA") 97 | format = QImage.Format.Format_ARGB32 98 | qimg = QImage(data, img.size[0], img.size[1], format) 99 | return qimg 100 | 101 | def parseGcode(self, extract=True): 102 | self.image_format=None 103 | self.rows=[] 104 | 105 | # clear initial preview 106 | self.preview.setImage(None) 107 | preview_mode = self.getPreviewMode() 108 | img_size=PreviewModes.get(preview_mode, 0) 109 | preserve=(img_size <= 0) 110 | 111 | thumbs=[] 112 | current_thumb=None 113 | image_format=None 114 | shui_thumb_size=0 115 | shui_thumb_line=0 116 | 117 | # parse gcode lines and extract thumpbnails 118 | index=0 119 | for d in self.gcode: 120 | # raise Exception("loop: index=" + str(index) + " len=" + str(len(self.gcode)) + " str=" + d) 121 | index+=1 122 | if current_thumb is None: 123 | if (d.startswith("; thumbnail") and not d.startswith("; thumbnails")) or d.startswith(";SHUI PREVIEW"): 124 | if d.startswith("; thumbnail begin") or d.startswith("; thumbnail_PNG begin"): 125 | image_format="PNG" 126 | elif d.startswith("; thumbnail_JPG begin"): 127 | image_format="JPG" 128 | elif d.startswith("; thumbnail_QOI begin"): 129 | image_format="QOI" 130 | elif d.startswith(";SHUI PREVIEW 100x100"): 131 | image_format="SHUI-100" 132 | shui_thumb_size=100+200 133 | shui_thumb_line=0 134 | elif d.startswith(";SHUI PREVIEW 50x50"): 135 | image_format="SHUI-50" 136 | shui_thumb_size=50+200 137 | shui_thumb_line=0 138 | else: 139 | # unsupported preview image format 140 | print("Unsupported preview image format: " + d) 141 | continue 142 | current_thumb = {"format": image_format, "base64": "", "bytes": bytearray(), "start_row": index - 1} 143 | thumbs.append(current_thumb) 144 | continue 145 | if (current_thumb is not None) and (d.startswith("; thumbnail end") or d.startswith("; thumbnail_"+image_format+" end")): 146 | current_thumb["end_row"] = index - 1 147 | current_thumb=None 148 | continue 149 | if (current_thumb is not None) and (shui_thumb_size > 0) and (shui_thumb_line >= shui_thumb_size): 150 | current_thumb["end_row"] = index - 1 151 | shui_thumb_size = 0 152 | shui_thumb_line = 0 153 | current_thumb=None 154 | if current_thumb is not None: 155 | if (shui_thumb_size > 0): 156 | s=d.strip()[1:] 157 | b=base64.b64decode(s) 158 | current_thumb["bytes"]+=b 159 | shui_thumb_line += 1 160 | else: 161 | s=d.strip()[2:] 162 | current_thumb["base64"]+=s 163 | # keep all or only non-thumpbnail rows 164 | if preserve or (current_thumb is None): 165 | self.rows.append(d) 166 | 167 | # skip handling thumbnails if requested 168 | if not extract: 169 | return 170 | 171 | # covert thubnails to image and save largest in preview 172 | for t in thumbs: 173 | qimg = None 174 | try: 175 | # konvert to PIL Image 176 | data = t["bytes"] 177 | format = t["format"] 178 | if len(t["base64"]) > 0: 179 | data = base64.b64decode(t["base64"]) 180 | if (t["format"] == "SHUI-100"): 181 | qimg = self.makeImageForSHUI(data, 100) 182 | elif (t["format"] == "SHUI-50"): 183 | qimg = self.makeImageForSHUI(data, 50) 184 | elif (t["format"] == "QOI"): 185 | qimg = self.makeImageForQOI(data) 186 | else: 187 | qimg = QImage() 188 | if not qimg.loadFromData(data, format): 189 | print("Failed create preview of ", format) 190 | qimg = None 191 | except Exception as e: 192 | print("Exception in creating preview:", str(e)) 193 | raise 194 | continue 195 | 196 | # save largest image in preview 197 | if qimg is not None: 198 | pr = self.preview.getImage() 199 | if (pr is None) or (pr.height() < qimg.height()): 200 | self.preview.setImage(qimg, t["format"]) 201 | pass 202 | 203 | def parse(self) -> None: ... 204 | 205 | class Preview: 206 | # QImage image 207 | image = None 208 | format = None 209 | 210 | def getAspectRatioMode(self, keep_aspect_ratio=True): 211 | ratio_mode = Qt.AspectRatioMode.KeepAspectRatio 212 | if not keep_aspect_ratio: 213 | ratio_mode = Qt.AspectRatioMode.IgnoreAspectRatio 214 | return ratio_mode 215 | 216 | def getFormat(self): 217 | return self.format 218 | 219 | def setImage(self, qimg, format=None): 220 | self.image = qimg 221 | self.format = format 222 | 223 | def getImage(self): 224 | return self.image 225 | 226 | def getScaledImage(self, width, height, ratio=Qt.AspectRatioMode.KeepAspectRatio): 227 | qimg = self.image.scaled(width, height, ratio) 228 | return qimg 229 | 230 | def getVisualPixmap(self, width, height, keep_aspect_ratio=True): 231 | pixmap = None 232 | if self.image: 233 | ratio_mode = self.getAspectRatioMode(keep_aspect_ratio) 234 | qimg = self.getScaledImage(width, height, ratio_mode) 235 | pixmap = QPixmap.fromImage(qimg) 236 | else: 237 | pixmap = QPixmap(QSize(width, height)) 238 | pixmap.fill(Qt.GlobalColor.black) 239 | return pixmap 240 | 241 | def loadFromFile(self, path): 242 | qimg = QImage() 243 | if qimg.load(path): 244 | format = str(qimg.format()) 245 | self.setImage(qimg, format) 246 | else: 247 | raise Exception("Cannot read image from file") 248 | pass 249 | 250 | def loadFromClipboard(self): 251 | clipboard = QGuiApplication.clipboard() 252 | mimeData = clipboard.mimeData() 253 | if mimeData.hasImage(): 254 | # QImage qimg 255 | qimg = mimeData.imageData() 256 | if qimg: 257 | self.setImage(qimg, "cliboard") 258 | else: 259 | raise Exception("No image in clipboard") 260 | pass 261 | 262 | class PreviewGenerator: 263 | large_size = 200 264 | def generateAllGcode(self, gcode_rows, preview, preview_mode, keep_sapect_ratio): 265 | rows=[] 266 | # generate preview if required 267 | out_size=PreviewModes.get(preview_mode, 0) 268 | generate=(out_size > 0) 269 | if generate: 270 | ratio_mode = preview.getAspectRatioMode(keep_sapect_ratio) 271 | if preview.getImage() is not None: 272 | self.generateHeaderPreview(out_size, rows) 273 | self.generateImagePreview(out_size, preview, ratio_mode, rows) 274 | self.generateImagePreview(self.large_size, preview, ratio_mode, rows) 275 | self.generateFooterPreview(out_size, rows) 276 | # copy filtered gcode 277 | if gcode_rows is not None: 278 | for d in gcode_rows: 279 | rows.append(d) 280 | return rows 281 | 282 | def generateHeaderPreview(self, out_size, rows): 283 | rows.append(";SHUI PREVIEW {}x{}\n".format(out_size, out_size)) 284 | 285 | def generateFooterPreview(self, out_size, rows): 286 | rows.append(";End of SHUI PREVIEW\n") 287 | 288 | def generateImagePreview(self, out_size, preview, ratio_mode, rows): 289 | qimg = preview.getScaledImage(out_size, out_size, ratio_mode) 290 | img_size = qimg.size() 291 | # calculate margins to center image inside bounds 292 | y_size = img_size.height() 293 | x_size = img_size.width() 294 | y_shift = (out_size - y_size) // 2 295 | x_shift = (out_size - x_size) // 2 296 | if y_shift < 0: y_shift = 0 297 | if x_shift < 0: x_shift = 0 298 | # itarate over all pixels inside bounds 299 | for y in range(out_size): 300 | row = bytearray() 301 | for x in range(out_size): 302 | # use black for pixels outside image 303 | y_idx = y - y_shift 304 | x_idx = x - x_shift 305 | r = 0 306 | g = 0 307 | b = 0 308 | # get real color for pixels inside image 309 | if (0 <= y_idx < y_size) and (0 <= x_idx < x_size): 310 | pixel_color = qimg.pixelColor(x_idx, y_idx) 311 | r = pixel_color.red() >> 3 312 | g = pixel_color.green() >> 2 313 | b = pixel_color.blue() >> 3 314 | # convert to double-byte RGB565 315 | rgb = (r << 11) | (g << 5) | b 316 | row.append((rgb >> 8) & 0xFF) 317 | row.append(rgb & 0xFF) 318 | row = ";" + base64.b64encode(row).decode('utf-8') + "\n" 319 | rows.append(row) 320 | 321 | def restoreShuiPreview(self, data, out_size): 322 | full_size = (out_size*out_size + self.large_size*self.large_size) * 2 323 | if len(data) < full_size: 324 | raise Exception("Too few bytes for SHUI preview: " + str(len(data))) 325 | # drop small head part 326 | data = data[out_size*out_size*2:] 327 | buf = bytearray(self.large_size*self.large_size*4) 328 | # parse big tail part 329 | for i in range(0, self.large_size): 330 | for j in range(0, self.large_size): 331 | idx = (i*self.large_size + j)*2 332 | rgb = (data[idx] & 0xFF) << 8 | (data[idx+1] & 0xFF) 333 | r = ((rgb >> 11) & 0b11111) << 3 334 | g = ((rgb >> 5) & 0b111111) << 2 335 | b = ((rgb) & 0b11111) << 3 336 | #data.append(r) 337 | #data.append(g) 338 | #data.append(b) 339 | k=(i*self.large_size + j)*4 340 | buf[k] = b 341 | buf[k+1] = g 342 | buf[k+2] = r 343 | buf[k+3] = 0xFF 344 | qimg = QImage(buf, self.large_size, self.large_size, QImage.Format.Format_RGB32) 345 | return qimg 346 | -------------------------------------------------------------------------------- /plugin/shui/utils/SetupDialog.py: -------------------------------------------------------------------------------- 1 | from ..PyQt_API import (QtCore, QtWidgets, QtGui) 2 | from .Core import (PreviewModes) 3 | 4 | class SetupDialog(QtWidgets.QDialog): 5 | def __init__(self, parent, app): 6 | super().__init__(parent) 7 | self.app=app 8 | self.title = "{} - {}".format(self.app.getLang("setup"), self.app.title) 9 | self.setWindowTitle(self.title) 10 | 11 | self.config = self.makeConfig() 12 | owner = self 13 | 14 | self.printerIdx = 0 15 | self.printer = None 16 | 17 | # printers list with buttons 18 | # self.printersLabel = QtWidgets.QLabel(self.app.getLang("printers-list"), owner) 19 | self.printersList = QtWidgets.QListWidget(owner) 20 | self.printersList.setMaximumHeight(4 * 20) 21 | 22 | printerListLayout = QtWidgets.QVBoxLayout() 23 | # printerListLayout.addWidget(self.printersLabel) 24 | printerListLayout.addWidget(self.printersList) 25 | 26 | # printer property with buttons 27 | # self.printerLabel = QtWidgets.QLabel(self.app.getLang("printer-property"), owner) 28 | self.nameEditInput = QtWidgets.QLineEdit(owner) 29 | self.ipEditInput = QtWidgets.QLineEdit(owner) 30 | self.esp32EditCheck = QtWidgets.QCheckBox(self.app.getLang("esp32"), owner) 31 | self.savePrinterButton = QtWidgets.QPushButton(self.app.getLang("printer-apply"), owner) 32 | self.delPrinterButton = QtWidgets.QPushButton(self.app.getLang("printer-delete"), owner) 33 | 34 | printerButtonsLayout = QtWidgets.QHBoxLayout() 35 | printerButtonsLayout.addWidget(self.delPrinterButton) 36 | printerButtonsLayout.addWidget(self.savePrinterButton) 37 | 38 | # edit printers box 39 | editPrinterLayout = QtWidgets.QVBoxLayout() 40 | # editPrinterLayout.addLayout(langsLayout) 41 | editPrinterLayout.addStretch() 42 | # editPrinterLayout.addWidget(self.printerLabel) 43 | editPrinterLayout.addWidget(self.nameEditInput) 44 | editPrinterAddrLayout = QtWidgets.QHBoxLayout() 45 | editPrinterAddrLayout.addWidget(self.ipEditInput) 46 | editPrinterAddrLayout.addWidget(self.esp32EditCheck) 47 | editPrinterLayout.addLayout(editPrinterAddrLayout) 48 | editPrinterLayout.addLayout(printerButtonsLayout) 49 | 50 | printersBoxLayout = QtWidgets.QHBoxLayout() 51 | printersBoxLayout.addLayout(printerListLayout) 52 | printersBoxLayout.addLayout(editPrinterLayout) 53 | printersBox = QtWidgets.QGroupBox(self.app.getLang("printers-options"), owner) 54 | printersBox.setLayout(printersBoxLayout) 55 | 56 | # edit printers actions 57 | self.savePrinterButton.clicked.connect(self.onSavePrinter) 58 | self.delPrinterButton.clicked.connect(self.onDeletePrinter) 59 | self.printersList.currentRowChanged.connect(self.onSelectPrinter) 60 | self.nameEditInput.textChanged.connect(self.onPrinterChanged) 61 | self.ipEditInput.textChanged.connect(self.onPrinterChanged) 62 | self.esp32EditCheck.stateChanged.connect(self.onPrinterChanged) 63 | 64 | # plugin options: languages & preview 65 | langsLabel = QtWidgets.QLabel(self.app.getLang("language"), owner) 66 | self.langsSelect = QtWidgets.QComboBox(owner) 67 | previewLabel = QtWidgets.QLabel(self.app.getLang("preview"), owner) 68 | self.previewSelect = QtWidgets.QComboBox(owner) 69 | self.previewAspectRatioCheck = QtWidgets.QCheckBox(self.app.getLang("preview-aspect-ratio"), owner) 70 | self.previewAspectRatioCheck.clicked.connect(self.onChangedPreviewAspectRatio) 71 | self.autoCloseCheck = QtWidgets.QCheckBox(self.app.getLang("auto-close"), owner) 72 | self.autoCloseCheck.clicked.connect(self.onChangedAutoClose) 73 | 74 | optsLayout = QtWidgets.QHBoxLayout() 75 | optsLayout.addWidget(langsLabel) 76 | optsLayout.addWidget(self.langsSelect) 77 | optsLayout.addWidget(previewLabel) 78 | optsLayout.addWidget(self.previewSelect) 79 | optsLayout.addWidget(self.previewAspectRatioCheck) 80 | optsLayout.addWidget(self.autoCloseCheck) 81 | optsLayout.addStretch() 82 | optsBox = QtWidgets.QGroupBox(self.app.getLang("plugin-options"), owner) 83 | optsBox.setLayout(optsLayout) 84 | 85 | # proxy 86 | self.proxyCheck = QtWidgets.QCheckBox(self.app.getLang("proxy"), owner) 87 | self.proxyCheck.clicked.connect(self.onProxyEnabled) 88 | self.proxyHostEdit = QtWidgets.QLineEdit(owner) 89 | self.proxyHostEdit.setPlaceholderText("host") 90 | self.proxyPortEdit = QtWidgets.QLineEdit(owner) 91 | self.proxyPortEdit.setPlaceholderText("port") 92 | self.proxyUserEdit = QtWidgets.QLineEdit(owner) 93 | self.proxyUserEdit.setPlaceholderText("user") 94 | self.proxyPassEdit = QtWidgets.QLineEdit(owner) 95 | self.proxyPassEdit.setPlaceholderText("password") 96 | proxyLayout = QtWidgets.QHBoxLayout() 97 | proxyLayout.addWidget(self.proxyCheck) 98 | proxyLayout.addWidget(self.proxyHostEdit) 99 | proxyLayout.addWidget(self.proxyPortEdit) 100 | proxyLayout.addWidget(self.proxyUserEdit) 101 | proxyLayout.addWidget(self.proxyPassEdit) 102 | 103 | # yandex 104 | self.yandexCheck = QtWidgets.QCheckBox(self.app.getLang("yandex"), owner) 105 | self.yandexCheck.clicked.connect(self.onYandexEnabled) 106 | self.yandexKeyEdit = QtWidgets.QLineEdit(owner) 107 | self.yandexKeyEdit.setPlaceholderText("token") 108 | self.yandexOverrideCheck = QtWidgets.QCheckBox(self.app.getLang("override"), owner) 109 | yandexLayout = QtWidgets.QHBoxLayout() 110 | yandexLayout.addWidget(self.yandexCheck) 111 | yandexLayout.addWidget(self.yandexKeyEdit) 112 | yandexLayout.addWidget(self.yandexOverrideCheck) 113 | 114 | # telegram 115 | self.telegramCheck = QtWidgets.QCheckBox(self.app.getLang("telegram"), owner) 116 | self.telegramCheck.clicked.connect(self.onTelegramEnabled) 117 | self.telegramKeyEdit = QtWidgets.QLineEdit(owner) 118 | self.telegramKeyEdit.setPlaceholderText("bot token") 119 | self.telegramChatIdEdit = QtWidgets.QLineEdit(owner) 120 | self.telegramChatIdEdit.setPlaceholderText("chat id") 121 | telegramLayout = QtWidgets.QHBoxLayout() 122 | telegramLayout.addWidget(self.telegramCheck) 123 | telegramLayout.addWidget(self.telegramKeyEdit) 124 | telegramLayout.addWidget(self.telegramChatIdEdit) 125 | 126 | # network box 127 | networkLayout = QtWidgets.QVBoxLayout() 128 | networkLayout.addLayout(proxyLayout) 129 | networkLayout.addLayout(yandexLayout) 130 | networkLayout.addLayout(telegramLayout) 131 | networkBox = QtWidgets.QGroupBox(self.app.getLang("network-options"), owner) 132 | networkBox.setLayout(networkLayout) 133 | 134 | # save & discard buttons 135 | self.statusLabel = QtWidgets.QLabel("", owner) 136 | self.saveButton = QtWidgets.QPushButton(self.app.getLang("save"), owner) 137 | self.discardButton = QtWidgets.QPushButton(self.app.getLang("discard"), owner) 138 | buttonsLayout = QtWidgets.QHBoxLayout() 139 | buttonsLayout.addWidget(self.statusLabel) 140 | buttonsLayout.addStretch() 141 | buttonsLayout.addWidget(self.discardButton) 142 | buttonsLayout.addWidget(self.saveButton) 143 | 144 | self.saveButton.clicked.connect(self.onSaveData) 145 | self.discardButton.clicked.connect(self.onDiscardData) 146 | 147 | # main layout 148 | mainLayout=QtWidgets.QVBoxLayout() 149 | mainLayout.addWidget(printersBox) 150 | mainLayout.addWidget(optsBox) 151 | mainLayout.addWidget(networkBox) 152 | # mainLayout.addLayout(langsLayout) 153 | # mainLayout.addLayout(printersBoxLayout) 154 | # mainLayout.addLayout(networkLayout) 155 | mainLayout.addLayout(buttonsLayout) 156 | self.setLayout(mainLayout) 157 | 158 | self.showData() 159 | pass 160 | 161 | def makeConfig(self): 162 | cfg = { 163 | "printers": [], 164 | "language": "en", 165 | "preview": "small", 166 | "autoClose": False, 167 | "nativeFileDialog": True, 168 | "keepPreviewAspectRatio": True, 169 | "proxy": {"enabled":False, "host":"host.proxy.ru", "port": 8080, "user": "user", "password":"password"}, 170 | "yandex": {"enabled":False, "key":"key", "override":False}, 171 | "telegram": {"enabled":False, "key":"key", "chat_id":"chat_id"}, 172 | } 173 | if self.app.config: 174 | for key in cfg.keys(): 175 | val = self.app.config.get(key) 176 | if val is not None: 177 | if key == "printers": 178 | cfg[key] = [{k:p.get(k) for k in p.keys()} for p in val] 179 | else: 180 | cfg[key] = val 181 | return cfg 182 | 183 | def showError(self, text): 184 | self.statusLabel.setText(text) 185 | 186 | def showData(self): 187 | langs = list(self.app.langs_cfg.keys()) 188 | lang = self.config.get("language", "en") 189 | self.langsSelect.clear() 190 | self.langsSelect.addItems(langs) 191 | try: 192 | lang_idx = langs.index(lang) 193 | self.langsSelect.setCurrentIndex(lang_idx) 194 | except: 195 | pass 196 | 197 | preview_modes = list(PreviewModes.keys()) 198 | preview_mode = self.config.get("preview", "small") 199 | self.previewSelect.clear() 200 | self.previewSelect.addItems(preview_modes) 201 | try: 202 | preview_idx = preview_modes.index(preview_mode) 203 | self.previewSelect.setCurrentIndex(preview_idx) 204 | except: 205 | pass 206 | 207 | self.autoCloseCheck.setChecked(self.config.get("autoClose", False)) 208 | self.previewAspectRatioCheck.setChecked(self.config.get("keepPreviewAspectRatio", True)) 209 | 210 | cfg = self.config.get("proxy") 211 | self.proxyCheck.setChecked(cfg.get("enabled", False)) 212 | self.proxyHostEdit.setText(cfg.get("host", "")) 213 | self.proxyPortEdit.setText(str(cfg.get("port", ""))) 214 | self.proxyUserEdit.setText(cfg.get("user", "")) 215 | self.proxyPassEdit.setText(cfg.get("password", "")) 216 | self.onProxyEnabled() 217 | 218 | cfg = self.config.get("yandex") 219 | self.yandexCheck.setChecked(cfg.get("enabled", False)) 220 | self.yandexKeyEdit.setText(cfg.get("key", "")) 221 | self.yandexOverrideCheck.setChecked(cfg.get("override", False)) 222 | self.onYandexEnabled() 223 | 224 | cfg = self.config.get("telegram") 225 | self.telegramCheck.setChecked(cfg.get("enabled", False)) 226 | self.telegramKeyEdit.setText(cfg.get("key", "")) 227 | self.telegramChatIdEdit.setText(cfg.get("chat_id", "")) 228 | self.onTelegramEnabled() 229 | 230 | self.updatePrinters() 231 | 232 | def saveData(self): 233 | self.config["language"] = self.langsSelect.currentText() 234 | self.config["preview"] = self.previewSelect.currentText() 235 | self.config["autoClose"] = self.autoCloseCheck.isChecked() 236 | self.config["keepPreviewAspectRatio"] = self.previewAspectRatioCheck.isChecked() 237 | 238 | self.config["proxy"] = { 239 | "enabled": self.proxyCheck.isChecked(), 240 | "host": self.proxyHostEdit.text(), 241 | "port": int(self.proxyPortEdit.text()), 242 | "user": self.proxyUserEdit.text(), 243 | "password": self.proxyPassEdit.text(), 244 | } 245 | self.config["yandex"] = { 246 | "enabled": self.yandexCheck.isChecked(), 247 | "key": self.yandexKeyEdit.text(), 248 | "override": self.yandexOverrideCheck.isChecked(), 249 | } 250 | self.config["telegram"] = { 251 | "enabled": self.telegramCheck.isChecked(), 252 | "key": self.telegramKeyEdit.text(), 253 | "chat_id": self.telegramChatIdEdit.text(), 254 | } 255 | 256 | if not self.app.config: 257 | self.app.config = {} 258 | for key in list(self.config.keys()): 259 | self.app.config[key] = self.config[key] 260 | self.app.onUpdateConfig() 261 | 262 | def doClose(self): 263 | self.close() 264 | 265 | def onDiscardData(self): 266 | self.doClose() 267 | pass 268 | 269 | def onSaveData(self): 270 | self.showError("") 271 | try: 272 | self.saveData() 273 | except Exception as e: 274 | self.showError(str(e)) 275 | return 276 | self.doClose() 277 | 278 | def updatePrinters(self): 279 | items = [] 280 | self.printersList.clear() 281 | printers = self.config.get("printers", []) 282 | for p in self.config["printers"]: 283 | item = self.app.makePrinterItem(p) 284 | self.printersList.addItem(item) 285 | self.printersList.addItem(self.app.getLang("printer-new")) 286 | self.selectPrinter(self.printerIdx) 287 | pass 288 | 289 | def selectPrinter(self, idx): 290 | self.printerIdx = idx 291 | deletable = False 292 | saveable = False 293 | printers = self.config.get("printers", []) 294 | if self.printerIdx >= 0 and self.printerIdx < len(printers): 295 | self.printer = self.config["printers"][self.printerIdx] 296 | deletable = True 297 | else: 298 | self.printer = { 299 | "name": self.app.getLang("printer-name"), 300 | "ip": self.app.getLang("printer-ip"), 301 | "esp32": True 302 | } 303 | saveable = True 304 | 305 | self.nameEditInput.setText(self.printer["name"]) 306 | self.ipEditInput.setText(self.printer["ip"]) 307 | self.esp32EditCheck.setChecked(self.printer["esp32"]) 308 | 309 | self.printersList.setCurrentRow(self.printerIdx) 310 | self.savePrinterButton.setEnabled(saveable) 311 | self.delPrinterButton.setEnabled(deletable) 312 | pass 313 | 314 | def onPrinterChanged(self): 315 | self.savePrinterButton.setEnabled(True) 316 | pass 317 | 318 | def onSelectPrinter(self): 319 | idx = self.printersList.currentRow() 320 | if (idx >= 0): 321 | self.selectPrinter(idx) 322 | pass 323 | 324 | def onSavePrinter(self): 325 | self.printer["name"] = self.nameEditInput.text() 326 | self.printer["ip"] = self.ipEditInput.text() 327 | self.printer["esp32"] = self.esp32EditCheck.isChecked() 328 | 329 | if not (self.printer["name"] and self.printer["ip"]): 330 | return 331 | printers = self.config.get("printers", []) 332 | if self.printerIdx >= len(self.config["printers"]): 333 | printers.append(self.printer) 334 | 335 | # self.app.onUpdateConfig() 336 | self.updatePrinters() 337 | pass 338 | 339 | def onDeletePrinter(self): 340 | printers = self.config.get("printers", []) 341 | if self.printerIdx >= 0 and self.printerIdx < len(printers): 342 | printers.pop(self.printerIdx) 343 | else: 344 | return 345 | # self.app.onUpdateConfig() 346 | self.updatePrinters() 347 | pass 348 | 349 | def onProxyEnabled(self): 350 | enabled = self.proxyCheck.isChecked() 351 | self.proxyHostEdit.setEnabled(enabled) 352 | self.proxyPortEdit.setEnabled(enabled) 353 | self.proxyUserEdit.setEnabled(enabled) 354 | self.proxyPassEdit.setEnabled(enabled) 355 | 356 | def onYandexEnabled(self): 357 | enabled = self.yandexCheck.isChecked() 358 | self.yandexKeyEdit.setEnabled(enabled) 359 | self.yandexOverrideCheck.setEnabled(enabled) 360 | 361 | def onTelegramEnabled(self): 362 | enabled = self.telegramCheck.isChecked() 363 | self.telegramKeyEdit.setEnabled(enabled) 364 | self.telegramChatIdEdit.setEnabled(enabled) 365 | 366 | def onChangedAutoClose(self): 367 | self.autoClose = self.autoCloseCheck.isChecked() 368 | 369 | def onChangedPreviewAspectRatio(self): 370 | self.previewAspectRatio = self.previewAspectRatioCheck.isChecked() 371 | -------------------------------------------------------------------------------- /shui_prusa.gcode: -------------------------------------------------------------------------------- 1 | ; generated by PrusaSlicer 2.4.0+linux-x64 on 2022-02-25 at 14:16:00 UTC 2 | 3 | 4 | ; 5 | ; thumbnail begin 100x100 7844 6 | ; iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAWwklEQVR4Ad1d+1NVV5a+/8toptVogv 7 | ; GJRMSg+AJEQcAHEEWCiE80KCpiFBHlIfLwiaKiRo2PHpOJjrFjzGimnXamnZTdRaacUDNWNdPFTPED 8 | ; Vbeq7w+n73fTCxeb/TyXS+xO1S4rl7PPOXs9v732d/YOJCQkeGLbvn37y/Pnzz9qaWl5On/+/CB+Ky 9 | ; 4ufnX69OknaCtXruyV9aNG16L/6tWre/BbVlZWH/4f98X9df3xTLp248aN3fTboUOHnuM3/Dt79uyQ 10 | ; rv+uXbu6cO2+ffte0Bj4uNLT0/t174DnijLAuEkGGKOuP8aNvvxa3KehoeEZyUA2hoBJEOLfdTfkgo 11 | ; DQaCDUcD0JRfZ3URBQokrZaLK/kyBwD5nQuGHI/s5loBojGQaULf6dj5ErU/V30TAC1MEkCJUH0bV8 12 | ; oDJl8iazNK5sKFWmLJNh6AQhCkVmODLP1kUBUQYmgzYZBvoHOjo6vj18+PBvbQShumFNTc1/2AhCZW 13 | ; l1dXX/jr42gpAZxsmTJ5/YCkL0JigAMqAxqDxX1mDd9Nz6+vp/sw2H3DDgZejX2tr6a/QNPHjw4JeX 14 | ; L1/+Ghani8uqF7pz586XX4T/w8BsB8KVcubMmcd4B7yUizKoHThw4PvHjx9/Rgbl2h/PvH///j9cv3 15 | ; 79K78yuHHjxn3IAO9gii4ypbS1tf360aNHN9E/AK2Ul5f/p6t2eYhDf9tkR417GPrr4rZKmeRhlZWV 16 | ; vydLc7FwPgaXkM2VSSFu/fr1/yMmcZsxcBng3QMyAeluiJvQ4HmIMyU7lSCoP092sFSdYXAEw73KFg 17 | ; mqYj0PQTokqAIvGANHgjrDUBlAQHQfnaXJoJzYZMlOJgiVJ5BhyJ7Bk7EqX3HDkD2DC0KmNNEwxGfY 18 | ; gBedYfD3k+XsgOmGUIIJyolNZmkuCEZmGPyeNolbNAyTIMQmQ4Iyz9Z5kYgEVZ5tVAjXJBL+pUuXHr 19 | ; oiGFIi+qOvOEmzaaTEa9eufeWK4rg13759+157e/s/u8R3lQxscxw1ksGVK1d+pfI6K4WQRQBFYUAu 20 | ; yY4arODChQvf3L1793NTblIJBJCWUJirMCjE4fmEJF2RIN4ZMrh58+Y/qSajugbPhgwIhZlkEFAJgk 21 | ; /S8vLy/mhb9uCWwUOGbbITvYNCnGwyalImD3EuZQ/qT/kK/+KZprKHzKA5etVVMZQKoTgnxnpTslMJ 22 | ; QiZknaXpyi+2SFAUBL+3DRLUlV9s6mE68EJQWWUYAZkgdALXWZoNltfVw3BvU+LW1cNsyy/c+/h72o 23 | ; IXnWHYgBcRYHAZBDBwlzqUzNJgKS4IRmZpLghGZml8kmYz4xeRoCuKE5EgZGgTkkTlcQPGcwNINkh4 24 | ; rgiGbohkh6Svg3KqhpdA0kbpwhXFkWE0NTX9hvq7zNK5R0AGKJ34AS6QARAUZOBnDDAEyODhw4e3Iq 25 | ; UTCBTFNVdISAjm6tWrD3APPygIL9/Z2fkQtSA/AiXPBgpyLXuQQvFMKMOPDGgMHNa6ygBGDEhOMgjg 26 | ; B5eyB2mVu7epxq8SBA9x4mTURhA8xNmWPbgg+CTNVKWQjUEMcS71MBl4QZ+A6H6mG+piva7sIROEeI 27 | ; 2p7CEKQrzGBgmawItNPUwneJuVUV3ODuisn9/Qpg5FQpFZmkv5RWUY/HedF6GPDAnaJm4VErQtv+iQ 28 | ; oAm8BEw3xItt2bLlRxcEI1oax/W2SU8sz/NJmk2eEdfgXWpx1HgfP+V1PkXAuG1CqrZ0AkECgVDpxM 29 | ; 8CFJIdoSA/izfNzc1PCYG4IhgSKhZ/kHhFckRyYoKXOnvGoIbfRKHeunXrHiFJPzJAHYxk4Kt0Qi8C 30 | ; JQAFnThx4jtXFMRD3LFjx/7VFQVxLz137ty3rgtgaOSlgJXtdfu/v3Wo7OXZDxNDn344zbtXNNX7Zp 31 | ; 263V4zzTu+Yrp3eXtu7+3G3b9vPfrTEq2rDCh819bW/lY2GbVSiCzOudCAZJM0l8mniQZkQoKUuK82 32 | ; H3z+RWV+z4PNyUGd8G3bvfKMvl9Wl3a3H2956ocGJObnvykakOrvMJaOvRu6h0sJOuXUln30KhoakG 33 | ; xlNEA3NK2k8SabcwwHDci2/IJnygyjIjclGGtFiO3axoX9uWnzgjYGzZtsihChASG+vik0IJfyC7e0 34 | ; E/U1zyGYkVQEb1+Xzgy1bcrt9ZMnKccgz7YMBw3oi7/8B2HaMla4UkADIhTlhwZ0dNPKPz7aPl+pjC 35 | ; 8LJ3l3CyePiGKQr7KXuMkAbRAN6Pjx4//ihwbEJ2kVFRU/uJQ9SJktChqQTX9A1DNr52jDU8visV5O 36 | ; /Fhvc/I473bBxJFRyoY5wZK1djIQaUBwCl80INkkTVfjF5sNDUgX+koyU4KApTrBHM2cGFoUHzfQp2 37 | ; DWBO9U5tveg4+mxFwp98sW9o8oDUintFjTgKpzZ4Vs4/q+gtR+/oxFM6d4R5bP7P989XsxVwrgsWxl 38 | ; NGoaEBDRz0EDEtfgEaJ0XnG/aLJ3cunbQ/LFp1uW9CGu0yTt3OmTT8qWJvVfzH0n5kq5UbezixufDa 39 | ; /tr4IGdL1604+weNXA/3HNJG9D4mhvTsLUSL4QhQ0ofOZgRRcXxOKZk7za1LExTfgwIMgA1Q5aLzEZ 40 | ; tJEGBAqNXxoQvAvK9EsDgldc35b1/9p4HfaMktlve4kJ8QP9chLjIsIW80XHpiV9XBjJ70+PKPDKin 41 | ; djppSt2SnBYaEB8TjnhwYkJi3br58G+qfGe1+WzAzZDvzjxfGD+mcsmBtsLlrUK6IroKDSD1cMmvhC 42 | ; gW1LxsUk4QMKE3q1WQBT0oBECGtT9iBlmmhAJlZK/Up94r6RFyf9vSNvWsSruDEUpUwOiegK4a9x3d 43 | ; I+bhjz3p/q7Zk3JibwmD/HtDL6RtGAUILQJW7kitoFf+9tWDAleHRpXEgW/yHs641VLziCAbqCsEV0 44 | ; 9VnJnCCfyM0Kh72qgrReKHA4FQIjUUUgUQYBCNWVAhMtDUi2Bl+5ZLp2UJS4k2ZMi9yjcFVu7/7smf 45 | ; 3XV8m9pSZr+qC1DQi7MGm8NOHv+Wh5L/fs0rxlvfsWjBu2hL8qZYYStJwfbhoQEj4t3rgmbbwE6lC/ 46 | ; 2p39fzYDo5DEDaOqePkfLhTMCMriP7xNFAYlfFHYd/atfdXW1PCMPBsKlCG24VQIGtGAiL8cgDD90o 47 | ; Bg5aABESfJlQKzbemskK46C684LYQPLCxRAqdJZOeplu9qM6cEVfEf3sKfS+hK9C6AiI15mX0yBUaT 48 | ; 8JckTVXKwEgDMpU9yDJVNCAbhvnCObNDrXmJ2sSNOJ49fYyXHp4vyMJHZ/Gc4MX2U0/Is1PnpwQR/1 49 | ; UWLXoXxt2wY/1LWTkFlVsRHu9M8VcPAyynMCvKUFw7Qn6OigYkFiJtaEBFK7L6THUoDLyqZNXA4g/C 50 | ; R2ny0PgPiy5MSxpEA1qXk9anSvjwrt3LZg36CitrQTiHSNDVnS0L+wuy0gaNz0897NNVE6WhelhpQD 51 | ; ovUn1n51KHQkMoO3W46gU3jF1rc3qac6cFRWGLISl78aJ+JHyVt3xZkdNbvuW1IKBwEja/Doht75rM 52 | ; Pk4Dqi7f9BLh0bYe1rx2Yd+w04BAgYmGBmTyCrg11i1kfwMxgdOA9ny89aWsnA7igpjwd+Rn9LZkTQ 53 | ; zZJnxVOQVEB5TUyfuLVhf0ALGZlIJxHTuw64WMBqQyaCMNiJdO/FBgUId6tGnWn1QvTXD2w8S3lYgG 54 | ; KOzSqZan3GNl4YMn/AGLXLOq5+T6jP/VJXwOjynhy+DxxfqqSC0O16OfyTtq0t8ZoAHZlo+UCqE4B0 55 | ; jMaUC2KArWemH9Iu2y6qXl70QSNyW9zMT3lAU/hI8rB8te8gGpJnyUwMlLW5saf7Nz+bw+1YRP9C60 56 | ; 3FlxXkf2+CHXnipe1G/KgWhAcJnzkgYSd2Nj4zObJV5fNCBTkdFmAUkV/2Gh5QvjQqqCH2pD+ytf5z 57 | ; CK/6JFw1tAA6I8husx4WvInhpUJXzuXZBB296tXfd8TA7h9QeKsoasHUVFA5IVEW1oQEfy5zqzPriF 58 | ; kjEcKM17pUI0VE7nhqGK/2JIWpqe2r83e5Yy4YO5wr/C6lw1yVkZLWUF3bY0oCELVCRUWyIzmowGZK 59 | ; pDmRos9MzHBT28/KIr+CFhdtTuecENY9um0u6mkqU9onfhvbKSZwwSypbchVJ4fHnvum4ug848N2XU 60 | ; FS3uuXC+IzoaEDZwsa1DiTdEv6a1+lzxRfhFzy0b71XPf8vbnzLaOxj+tyl9jNcZziFfFQ32ANB5xB 61 | ; qUCEkJCGwsLowYEfi/3LNV5XQx4aMetm/Z+/249ni44d12bvyom2QAr7VVxrnl74UObsjvdqUB0RRh 62 | ; gAYECk40NKAHdZv/S/eiLRljvRXTRnsL49/1khOmRPp9kDDdSwlb/4rkKaGG4oyeCzkThli0CEmRwK 63 | ; sXjAkrcqyXGT9uYEEKxvTtjY5HgMcodNL1Ku+SzdhvX7nwdXtjzbMjNdUDoMW0ZMyNAxXohh2lP/gh 64 | ; lKMh4UtpQKZNXwYl7jAux2KP7mXr0sZ5i2bESfvzfIXwYRP/kfDTE+IG+kMBIBMMhL3yjL7DB14vE6 65 | ; smfAiPqC5vXv86TOOTCyLdocZmo4xrYSS1c2FcsLnh8MAnG9HsBiSlAZluSDUYDF73soB9KTMmDfSD 66 | ; xSF5k0Bg0TzpQdjncoYiKzH+k1Ufa6x/dm/jB0MMguYLnA4L71KV03H95cLEEDzHZGCDPH/xWO9AaU 67 | ; E3H4P4TYotFYq/qy8aEErVphe+UjKvn7uvbL2DwocpPBAk5QgFCtEV/PCOKI9weDwc5XSEKHjFsZqq 68 | ; 56bdgFRUqKhpQPiNBAFsb/Pi8CAOnyvyUvtU+N/0rcaAkMMFP8zYuSB0BT+qhyEMk2e3Vm7tArryU0 69 | ; 7Hc/bkp/fU1xyw+ihUtxuQLxoQbijSgCjs2LTqim0DNCDwofZkTAtGa6FQXtGC+CEJXzZjp/Z5w44f 70 | ; kCtpkobweGqZ/XvAK45kTg4eKit+6bLmQx4NGhDJcFhpQJuXpzkxzBGGrp498R2v4wCSNqTLGR6YW3 71 | ; SGFXY0bYxXn/oLryUzLtSWMc67mT90mVa2RCsr0XOPPVbzU8K3qUNRO5c9IRKiOs+e/tamSiEz7JjR 72 | ; gFC2drVoYnjwNfi05JkhWPSt/Ils4OO99TNHeWnxE8JgYLJXvDqvpz3sVUd2b+vKT54c2vHBKM8GHq 73 | ; uWaOldbL8fgXE0pI6JJG68O9+Hxc9uQFBk1DQgrk28gI49aGpI4ICZlOxyl2X2LU98x9uSNNoriB/l 74 | ; LQrPU8gYVF9hqbxLnPDRjF1FgLAJUfCKUy1Ng/KVnx2RxMRt+jQwwB+m28/QZdaqi/8IeypLI9IDeS 75 | ; I8QBQ2JnwydAWFo3wzyLPnTgu55izAWSTupoY6pcB1UwSbTXCMNCCbT7EQGqJVCDXg/quflHaDP4Ui 76 | ; IZZh96+cG1SFRHF2rVvhw31JEGA/fl5oh6bgFXvT3gseqdjcNVK7AYkhzYkGNJwK8etdsnLK8cwJQ6 77 | ; 7F58yNOW6J+/DajAicNX1fKTYYNBCU392AxB2RAthJh2hAuipv7pz4n1Uh1Di6wvziaG31809X+iNL 78 | ; Ux2qZtPqH4GEotkNyO+OSJA5IDntaDSEBqS6IbD7SAqeOFkyeAylcHevzUlwBhu8DkVh24UQLvv4yH 79 | ; VHJJG660wDGqnv9JBYP86d35uVHB+STfjubFvcxwWBxO96/6qi7FeyMG2zMqpbO7L5NFBH3Q3IYpqK 80 | ; BnSiOLUv1srAOvvG7AV9NGDAYyTwhrRfhCd9E7xD4RCzr2TVKy4IJH1br4NXmBK3amVU9X2l2Dhijc 81 | ; luQEQDOlq5reve2th/YhyBuwWZA4aB0nbN7u1dzZ/seLFzw+sFJDQTSZsaENknq39K3LZLDLLdgGxm 82 | ; 2zw/qHYD8kUDwoNFGtDRJRNirhBqoJueP7L3d/QdO/dYIC6buhq8onHpxD81VZW/8LOR8nDtBhQTGh 83 | ; Bw98q500OqD2Zi1cDrQi0K1V6EJ9vqMMIfQlRrY90zKi66oCBK3CgO4uN+lzmGGOJiRgPCb3vzF/WN 84 | ; ROiKpvE6FNGAdJu+6GRAAnTZF1K2B7HNvpBaGpBoSXzTl/ayFa/8cJZi3UwLSKaVUdMCks0UwWU3IC 85 | ; 0NyIa/Sze8eL7j0ckNmT1vklKoDmVaQFKtjNocJ0FtOA4Fk+2IFKEB4cggPzSgttaWp43rMntG4iN8 86 | ; k1dUZ0yOwFmXHEFGiEpFVVXV70xVXJW3oR8/FMx2zYR7G3LMsNCAgB46DlV837j03dDdwkneN+tGVh 87 | ; nwikMlOf999tSJJxicy/tTrMfnZNEcCobzTfweCoampQHZWgcPcdgNqK3xyLNjWwu6T2RPHBHFnF02 88 | ; 3tsRmeRt6oKHux6HgSYebOZq4TzE+TkYjYc4jCEqGpA4eHK/i2eOP2nakt9dt3hC6Gq46CcyE6NpuB 89 | ; 9Yj0cK03tO1h98JuJ6W0K4CrzY7oik2vtF/DpZ522q8ssQGpDO0myOkxjY5a21+enW7Hl962ePCZc7 90 | ; 3opQ+10VhNoZlnZRkV33/t95yxMnhCrLNmg3wTERwk3gxbRBgk3iNtGAZKfcSRUiszRaH3A5TkL8hg 91 | ; 4LSuVrcnrqNhd0b89I6N+a9Ja3b+6oiLWD0IA5A9r+eeHfU0Z7ZUmjvOKkcaHKgrQeJOoFsxNCLgiG 92 | ; GwZVXrln22yCIzt4xUUGsk8PYkIDMglCtDT0xwvILA783A8SpnmzWeObyMgOBXNJmmQYWKugmbrLGC 93 | ; gEcRnYfKUsyiBmNCDXPRUhkGh2A8KLg9lOH9W7CIMa+mA1LppDwfANOdWy3igakItQVbsB2RyHgcbd 94 | ; u7Cw8A8uiz80Bh7iXI/DEMGLSANylQH6DzsNyGYRX1d+sfkOXraSxj3OBgmqNsERDU1lGKrvyF1oQK 95 | ; qzsJxoQDbb+PEbyg7EkkE53nTf2dlsgqMjhJvqUDaGYZO4VcdhcIM20YBUSNCaBiTekA+cn9HnuhsQ 96 | ; WZoLgpFZmg2C0RmG6Nmm/uIUgdOAbHOVeDbjEBqQa9LmuwH5SdoiBcaVsYFBY1du6m+rTNEwwPigQ8 97 | ; FcN3OOKQ3IhZOk2g3IdTDYCQcrcqbcpFIIhEgozhUW0yQNsPivjgYkEwRZhOtuQLJJminZyQTBvcLl 98 | ; MAASBA9xLmUPUQZ+aEAy6i7eKaAapM1uQOI1NufN6sovprKHKAgRKZnKHuI1OhqQTga68otNPUyXs5 99 | ; 1oQDYIRkc4s0Fx4nXc0mwX0VSGYQteVMdh2JZfYkoDwg35gViuuwHRpMglnIiGARqQK5FZDEE23q/z 100 | ; JD80IHGKoIL7RoXwG0a7GxDqUIQgXInMeGmsxMloQLaNHwrmp3QC5b0RNCDC6SINyIVCAwtFYY0fCu 101 | ; YCrcmqcQ/XxR80Stw4o+TnpgFR/5jQgEwuL5uk2S7+cEFwr7Ate4iCIK9wRYImGpDJ22WoLyY0IN3f 102 | ; TV9hmb6DFwUhy1emepgJ/o4EDUiXuGNGA6IbUghyKb/ILM20kqYzDBtBiGOQrYy6lF/EsofKs1VNdT 103 | ; DaIBqQy3ES3NKiPRSsurr6e1tByAxD3A3IlQaEagHRgFzGoDoYzRa8cBng4MkWkQZkcxPRUoEeaPHF 104 | ; tRYGwUV7KBh2A3r8+PFnZO2u/SHUaGlAOBvkjaMBuZQ9yDrIutDf1tV5f8pXsE7X4zDQVDQgW8MaTh 105 | ; oQPHQIDcjmhjoakKnsIRMEWZRN2UMUhIjYbJHgm0oDwvVDUJbO0mwKeLoCm24ljZqpHmYqvwwHDUhX 106 | ; FbClAamQIDdomQz+DA+3XKARnVcKAAAAAElFTkSuQmCC 107 | ; thumbnail end 108 | ; 109 | 110 | ; 111 | ; thumbnail begin 200x200 14596 112 | ; iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAYAAACtWK6eAAAqiUlEQVR4Ae1d6XNVx5XnD4ltDGYzIK 113 | ; OYTdJDQmIHsRgjFrPLYgmIRSwGhMNig8UuFECAMQZj4w1DnDge42BPCJl4EibUhEl5ppgUFaqiquED 114 | ; H/RBFX3Qhzvv90jLTXP73l5O971PaKpOTRKe7rvv9DndZ/mdX/fIZDKBjlRXV98/fvz4zffee+/Gyp 115 | ; UrW8I+U1VV1Rr3GVOpqKjo2Lx58108G98xadKk9rjPTJ8+vY3yHfA89vvWrl17L+4zeBe8E+U74Huj 116 | ; dACB7tlnsCaU34/vjNMBPnPkyJFbPnQgW2Po4MyZM99BTHTQQ/WDKoYpfn779u138Pl9+/bdjvs81a 117 | ; Lw4sJRVQwzTGcwFGod4NlxnxcdlVoHKkbH2w2Fo/KOBxuL06uNo/ZQfSEdw+QFJw7zYBsjVdktZEa6 118 | ; e/fu720dVTRMHSUzR7XVAX8iQK86OqBwVPwddKhqmGE6sN2smD3p6gBiElXEOojubqGiWB3j4g1T92 119 | ; 9F42KOumDBggcmf2uyKLyRMh3oOio+y5xc1zB5we823axYaG3j5LyjmuiAIiLRddQeUS9kultEGZqO 120 | ; s9nsFnG/SeUU4BcFBkqlAx1HpTBMmQ7wm+J0wIfKKmGliuhGFbxR60Ywst+kGlX0iFsUCsPkRSUmpt 121 | ; otZKJyKsJ4KRdF/H1xMbFuzqcr/Kko04FNaK2iAxVH1cn5THUQZec9eA92sVuESVRMTL1byIT/HtFR 122 | ; XRqm7Hv4mNilYfKC75Q5qkvDFI00bLOyyfl0JCqqgA56sF3a5Y4ZZaT8UetrUZiIMbEv5xR1wMfEFD 123 | ; mfqQ7wnbwd2OR8OiJGFRQ5n67wjgodMKfJnSBffPHFV59//vnXeCkfhskLvq+pqelP169f/7nPReEF 124 | ; CoEOvsz+n2m93Ebwe/fs2fOXGzduXKHMd3QEv/nTTz/9BnpwEVqr6GDHjh3/c+3atV9++OGHv0lKBx 125 | ; 999NG/wg7YhtUDHoMXOnfu3O9cH2dhwjz3gw8+uG7T0DEVdsRCB/hu1eSVUljOx96BKiHX0QE2JqwB 126 | ; swMXuZ+KDi5duvTN+fPnf+dbB+wkhQ4uXrx4nYX/PfhjjLqho7IofJXMdedVtij8Yqgkry50wHIxm5 127 | ; K4iYSFlTYlcVPD5ENrm5K46TqIOmAl8R7il7uEiUQZJi8uYSJsUaKKEa5hIjLD5MUlTIRJVM6nWxKn 128 | ; MkxRB66jiqgGNN6vh8yATBs6cYapWiFy5aiqFSKx0kbpqKrFCBcwEVEHcc91FVWoFiNcRRViA1q2Dp 129 | ; GddCqYiI5hikZKARPRNUxeqGAiYYapstjUeC5eB6rhE2V1z7QBTRlV6DSgY6Emup3XuEUx2YVUGjqq 130 | ; hmkS11M4qm3p0gYmwnRgA1ehiCpsG9C2UYVJA1oZzWsSE5vuFnHP0jlqKWvqJskrJVyF1wH+v6oOKO 131 | ; EqJo5K2YA2RYmbnoLKDgLRiYldwVVUHdVkt1A1UtWYWGV2xlQHKpU2V3AVnahCZXbGRgcq9mXTgNZy 132 | ; EFHpYTFxWOmSSilMomAi4r+76ojHxcSu4SpxjuoDrhK3WblGRsSVxCngKtoOwhuheNS62i3CROaoPu 133 | ; EqYTGxD8MM0wHvqKazMyYSVhKnMEwdCXNU09kZUYwdhC0Q8+Dm5uY/+jJMXlhMjO7ryZMn/6AS/lEK 134 | ; HxOzTjx1STROmKOiE3/69OnvKHI+XR0wR2V6oMj5dIQ5KnTQ2Nh4i0oHVg4CwQsAw3P16tVf4aVcd1 135 | ; 7DpK6u7m/AckE5UJJvLBd0AIjGt99++wvfDsoEOyZwTIBqpAFTl5QOqHGFPWxfiO0W+M8+YSJsUfjS 136 | ; JUILXzARJnwxAt/vEybCdMAXI5gOfMBEmPAIYGxWPmEiEDHUrKmp+T+q/p2Rg0SVLl3DRMIMk1eCD5 137 | ; gIWxRZ6dIHTAQiK0b4gIkwkeV8vvBcspyPon8H0XYQlbmRrk77o5KIu4KJxBmm7DNdnfZH9vtswada 138 | ; DqJL+0MJE1FdFF4oYSI6hsnrgBImIupAxejiSuK2OlA5Hag3K90GtI2jOqf9sYWJhC2Kzk6QFtofip 139 | ; iYgvbHVgdpoP0xaUCbRhXeaX90jYv3fpuyXdK0P6YwEfa34uyMyTukgfbHFN1A1YDucrQ/+CxVTV33 140 | ; qHUBV9GdcaCGq+gmry7gKrwOVH4TdQOalPaHOsnWpf1xAVdRmXFwCVdRcVRXOCrRSKN04BIVEAcTYe 141 | ; KL9kcWVXTT/oTszL7gKmmg/ZHlVb7gKnlD+5ME5Y0YE6eJ9sdXJzhNtD/YrJ5U2h+R+oiFYN20P5mH 142 | ; ISVP++MbLoPfe+DAgT8zHSRJedNN+5NC2h8sCKP98bVzMmFwlTTQ/ly+fPkaRUncRAdJ0/6wk+vKlS 143 | ; u/TgvtD3Qgpf1xDZMOg6tEUWG6NEyR9sdXiMPrgIWVNiVxE0ma9ke0O1EHuiVx03UQdZAK2p+w73AN 144 | ; Ewn7DlEHrmEi4neklfbH9WYVV4ywhYno6kD8DryflPbHFbu6Lu0P9VGrWiFyARNRMcwwHVA7qk6FyN 145 | ; VmlQbaH5UGdGQnXbehQ2GYopG6ov1RXWwqmIiuYfI6oHRUE7gKZVRh0oCmjip0GtBatD+mFSZK2h+T 146 | ; mFh1t4haINsZhzTQ/tjAVahpf0x+AyXtj+pm0+VpfyhvqdKFiYiLYnsK6sJEqAwzTJ9J0v6YRBWppv 147 | ; 2hzCFUYCLUhik+VzUmdnXnSj7R/rhCBehEFami/fEBV4nbDdJA++MaFRBXEndlmKKRRm1WPml/4nTg 148 | ; nfYnLCb2sShMZDFxN+2P20lCUcKiCl4HPpARYb+XCq5iRdrAe3DaaH98dcJltD8+4SrMUaGDY8eOxT 149 | ; KWu9BB0rQ/PPURo/2hgKuQ0v4kgWOCpIH2BwvCaH98gT15wY6JNdCh/akaWxIsnPCoLJ9cHKyd+lDw 150 | ; 36dWPBQVHQBTx66R8wX2FHXQTfsjLApfuvSNhIWkifYH+hdpf+AEr80oDhqqioKT84uC84tGBl8vHx 151 | ; H89idm8vmrI3PPwfPgRHj+ork/hJpbtmz5q0+YCJPU0v7w8b8PmIhomGJN3Cftj6xC5AvPJcJVxpZl 152 | ; crv+wcXj27/e8lLrb2pLO0wdQVe+XT+2/Vj1+HZ8P97DpCRuaothOZ9pSdzaQVQqRGmi/XExkaiSiP 153 | ; ui/bn49vGb9fMmtONk8OUMcYITCu/z5oLx7acONzgDXqaO9keX8qar0/7EnZAuaX8uH9p256v6uQ/S 154 | ; 4hSRDpM9zS417vq+uYnmKjvdBrRNVKFN+6O7G1JBp21ofyioMG1Kl7YwESa1S+c/uLRzeQvCmXxwDF 155 | ; EQ8v1ib+29tYtmGYdcFLQ/OndOatH+mBq4KUxE/NukaH8o4Co2Mw6oIjXOLcorZ4iTd5eWdahUx3gD 156 | ; p2hA60YVkbQ/NnfaxTmbDuWN7m4R9ZvSQvujooOu6BiioBqGpD5KD08U7Y/KjEPYpB3lO6hU2pKm/U 157 | ; EJFSXVfDJ2U0FSv2NmuKOkjvbHBbhNlCjotK9bqpKm/ZGVxHFqfLz0yXAMUbAhoK/CNhEf5fqozSpH 158 | ; 2sCM1Of1aUzSQPvDh1BJ0/40LatsyyeDdnWa7H91alvStD/MLh6j/XFN1pAJ8eCkaX+gA/x+Rvvjm/ 159 | ; Lm5YkVHdc2TnnineMRR8mWhudNm5g87Q+Ol27an0dpf3w6KXING+jHv7w6NDg8tX/w/rwXUm3wJoKQ 160 | ; S6fSZSOppv1heYgvmAiTJGl/KCpUny/5cbCh9NlgVcmzwetjnwt+NuP54NsVw1Nr8Gl1krAGNCvtJ0 161 | ; L7EzVpZ9rQ0ZGkaX+AV7KtUB3POsOKkU8Hc4oGBOUlI4PK0uHBpjH9gr2T+uYcJ60GbyI4YedPKXey 162 | ; WRrT/lDDRMJe6Emk/UE508ZYEFLh1KgZ2TOYXvzCI88enSkOVpYPzJ0mb88alFqDNxF04betXdlCSf 163 | ; uj0oBWpv2xTVx1cVQuHNUErkIFE0GYQNHXwMlRPbJX7tSQfdfcssJg65g+wf4p/YOrNcNSa/S6AogN 164 | ; wI9UtD8qdq1F+2OaF9jCVShof2zgKjYwEQjmMSgNBUjZuLh87KiioLbi+WDX+D5dKoFHdYuC+ki1lZ 165 | ; BXtD8mA1mUt1TpQqddQkUQl6MCFvcOOE12TR7YcXjqgC5zmgDwqBtVmPb5tODuOo20NNH+UOYQqo5K 166 | ; ARX5YumLsZ+BA0ZhmfCO55uP3tw9Y2g7TpOuksC/vW15/tL+uIarxFXaXNP+RE1OwlgBwLNZfJaIby 167 | ; nvlUu240q3cERUxkRH5nWABH7J6ILOBD7fy8H4zXGbVTftT8pofygScVa+XVDUJ5jzz2QbpdtfVcef 168 | ; JnBMUQfiKTujdKjWM9MsLMQMiypSTfvjE67CGjpJ0v6g0oa42GaxcWr8NLu7wzleKh7c+XyWbGPnV0 169 | ; m2v1wzrh1juFE5n+4z0yrYjFh4ydP+7N+//89UlU9S2h+8VFK0P6DcSYL2B6eG7Tw4C6miyrdItmHQ 170 | ; Ksk2egaonMW9OysH53MCzxcq2FV6qaH94SftVq9e/fckaX+wi/um/aGc2bjwSmEwoSzaqCeOGpHrli 171 | ; PZvrSwMPaZcNy4YST+mR/NH5I6B4gTjAbk1oJb+27an4wcruLjdihX5duwZFsU3WRb95nNMwfmXQLP 172 | ; mFPyjvbHxUQiJEnaHwz0uJ70UxlD3bqquuWNGS+2q2KvVJ6Zr3iuDzfPeyAL7W3Bp12e9ofqqIXY4q 173 | ; h0BKFDWLec18GGdWvusWRbBXvFT+zJJB/xXCzMkolNVNHlaX9EmIiJo1KUb2FsZ2cP1vobdMv5ZFtW 174 | ; utRNtnUS+HzBc8X9HtOoItZBTO60CzNS0wTeBe2PjoPBmGwHmvZP7pcr326uMDM4JNs7N62NhFegdL 175 | ; u+or8y9koVz6XzzCQl7mRkohtVeKX9UYWJMKGGq+g4KkUi/mm20sTKtxNKhloBCFG6Pbh+SewGxZJt 176 | ; leEpOD7Y3KOeBx011C5sYSXmtCbwcb9D/E2qUUWitD+yZ7u+pSqu0gZlUw808c9fMHqIck9DFBfJtu 177 | ; yZfGhdu2j2gzQn8CphoygqUUXqaH98wVVkjkqRiJ+bU5Abg60sLow1YhMAoWqyXVM+2LgcHJbzpRnP 178 | ; hXUzsYNu2p8YR2Vh5KHd9Xcoy7cuehomu6Zuso3TJC7nm1X2YurwXKYOwkSZ9scnowjzYJ72x6apYy 179 | ; qnal9udbVwDEAYJQAQvj55cIeJwbkYnsJQ0oYVS++rPDMteK6NlcOt7QC2DxtktD/YsHtgt0gL7Y9P 180 | ; mAiEAkf1mQLkQ9bTYJLL+RoP3do7f0yricGJ5WCZ6OC5XDwz7Q7CaH/AjfUY7Y9PmAhEhfbH5ffb4q 181 | ; iuLR8WHMkaxbpMT2ODC8v5bAwubngKoovnwnVr1M90ISrTlVG2KKYXbONOhPYnCq7i+nYoyoEmlG9f 182 | ; ygwxMrioYoQNgFAn98Eorgs8VxIJ/MzSH5ttlPlM+0MJE4FQDzSx8q2ucXy1qrTjk2Nv3Y46qW0NLi 183 | ; r3Yc55+sDu27oDWSolZvZMX+VgbFjoM+nYAjntj+2chwlchep2KAhF+XZree/HBppsjOPwgrIOlwDC 184 | ; sNxHhKvoJttx+RRzbh2MmK2cnFUQlGXUQyydPl/e0f7oJvBUfFQQGOieTavuRemAN7h3FbBXrgGELP 185 | ; eRjSgw0cFz6STwPgaydkwpULIFkwa0MppXFybCnIsKrmKC57Ilhg4TQD4uvL68JU4HLg3OBM/1Rd2U 186 | ; tvfPvv1dVI8rH/FcKJbMGRXvIKYN6Lym/ZHF7z6uLkOvII4Kc+Hsma0Hqyc/SIvBwRGph6fi8FzMMI 187 | ; 9urL7ngmD7+EvPB0tmTYvcrBKj/Qk7qpOm/aEghlYVnCbvvFV/J8xR+UWhBhBCdJ4pii88l1ghcjGQ 188 | ; 9frs0a0qtD+mV1rkNe2PGFNTEEMfmtpf2+Aub198f9Wyh6MAMi4mKgChjRHz4iL3Yc/kw2ExtGYYMQ 189 | ; pk8Pk5BUFlSWFoVJFq2h+fcBUo4tzxw7ds7w6HAawp6RlsKutlZHD4/tP7d/531OyMicGphEQ6oERR 190 | ; XOQ+yNGiDJOioojcY31Z785nppr2B+35a9eu/RIv5fv6MltiaH6gCeVb2/7Dz/dtvBuXc+kanIoRb1 191 | ; n2yn02o+4Sz7U7m/t8rNC8/LDm8XvQKSuKjdMGBDOFcjuiGuCoUkX7w4ihQfsTFlq4EigbXWkbJQOW 192 | ; zjriY0sexfLYMBCqJPC6AEKZEfM539H9e2+bDmSpkmEvKS8Idox7LhveDFZ6JnNuynl+IBaWFD/3SO 193 | ; +Dz3dgi6mh/eHhIK5hIp3KIOCjwhEN58BAk6zJZINYRQJ/fueaeyrlYFMAoSzns8FzqeQ+VWUvBvXj 194 | ; +gS/UAxDKYsmWLeNZc92bmiyBrTYKX8iaH8ocFSisl0jVr/csfj++tXROuCHp1TxXHhmVLnbBkCokv 195 | ; tUTR7b3jh7mFXeZyIHpvTrDK34U0MW2qea9ocKJgKhjF/DDM4lYhUJ/NH6NfeidKCb++CZzbs2RYay 196 | ; tvmUbAiJVYgOrFnQ8tkif+hd5B0IrXQb0N5pf3SOKwo8lw8+qrhdEzrYv6/hNmt4mRgcysFxJ6pu7q 197 | ; MykGUDIBRDOp7qFf/92Cw/w1Insg3BV4t6BSuWLjJqQDul/bGtJ5vS/uDUADjOxwLwBieeJuKddjYG 198 | ; d23jlLa99dE7vwsAoS5GjN848Pcyqtf6qe5HbuEcq0b376jfVGfdgCal/RF3C5MX4kVnIIuSGFpXYH 199 | ; Cs4SW7pcoGsZoGPJdqPoU8LQoZgXVyuRaAktSNL2h/+3gTWQNa587JSNofF0NTcQNZPnBUKgKDgxHH 200 | ; LYoJYhW9l21TCtvjYmJ877vNTZ3Xp6kMT1HjuRD3R+mAsmjymI4qegfrss5x5vjRmy4a0Nq0P5lMPD 201 | ; E0hYTBRPC/U+KokEi/l+1znJw5MDj18sDg9MuDcsbwyYIhwVc1Q5WfE9bwsjE4NnD1SlG/QBYTizmf 202 | ; CYAwrgoFwTO3j+kdHKzsH1xZXBh8s/yH5366cEiugbpq/I/bZcaDUJQaLc2cA6XcTZXD23ByUBEXho 203 | ; kS7Q9CqCRof9j9IpCmZZVttopFbF2fXfAVRU8HK7OyuviZYE3mmWDdqJ65ngckp/isoNkFY4NhxD2X 204 | ; AkAou0FKLIlHUb26wnNtqOgbbB79UC/12V17VfHTwbIRTwVLi54NyjNyYmgXp0cO8pPpGbyxfE7Lie 205 | ; P2NxOrSmppf16aMrEdvQIbpZ7LdnXrSnsGNdlFXTSiZzCneEAwveSFYGJmWFCRXWDImExRMD4zIpg6 206 | ; amgwt7QgWFHWP2cUkIOV/YKfL3YHIOQ79mE3SGHXAvURbsnC9WlROZ8pgDB2HcZkOprrV//vwVVz/r 207 | ; 57xdyWmeUjOkojpvSocw8e8nOkfu0dl41mmUhpfz755JNvL168eN0XTISJbW8DCSRi5OUjnwpmZzvi 208 | ; 4zXnkmFs1ZNHtb/1yuhWhBqUVwgwACEcBYs+q3ig9PPYJXEJzPWNE/6B7/jkjdp7cSVxSjwXC3kB9G 209 | ; OUN1H8ZLb4N1EuvvJCbgNZO66g/WzTgf/AfZOu7paJEjgEcIW4UrCT9oeVLsU6sWvaH4qLaBAmVWdr 210 | ; 45XFQ4zegS9d1s2d3EoNIIQRryztG0wqeVFqmNB52AWgqngu3QnAuOui8b/xpX0+onBRQGE52U8XT7 211 | ; 9/uGFPLh+luh1KVYxof1x68LxpE9ttkzvWVY2KkfmFDdstxGIEFYBQdVFwasARZM9HOfjdNzfejdus 212 | ; WO6jOmOB94aD83AV0Q7wnSx53fnqrFZqx2DUSTg1DtavvxMWudjCRFQkii8hEdofUFrazm5Aueiqls 213 | ; XEyHyTEQ4Jw6iZVt4RV4xg7OsqZVXV/oO4KJcPbbujqocvdi9rYQNZMjEdnsI7nFhU1oH8CogFJnAI 214 | ; VPAonUI8NTZUjmjDqRGViLsiE0wl7Q92TFvnyCk42zyK4kGKi5FhcDWL5kt/D06FM68M1XonFTwXKy 215 | ; m+ffTArTTguXwLX8nbU7vknmoDmholTk77Y3u9MzsqAbOgUPTWiYMjv08lfJNhr2x6MVF4Ln52Bndt 216 | ; 2OC54kqfaWRfZ5U8hFTobZiE77ZkgiZ8Cc5pf1g9+dLO5S1Uyn59yYxWmaMij9B5Fg/2owJF8s+UwV 217 | ; V4UKIungt5C65ko8RzuRLGYYxTY1e2t9F05JDVCWB656R32p84D+bhKrvrVrRQKv1S467vZXguhBkq 218 | ; 0268IFehxn3hmcvnR8/O2A5kqeC5dq5e0pIU+7qYiFM2oHXIBPlEXLc665z2B41AakTuhdWVbVGOum 219 | ; Vsv+BMCq4whhGjpxG3y9kwEH5VP/fBlrrHDY/fMTetqmnxzb7OEvFt8yY8QCLuogoVF/5Hsas4cxAm 220 | ; PEwkivbHxSwHcoyXJ4ZX2hjAb+fkQR1J31nBBNWzuATehhCODU/xOhBLl3wC/67mddQ6wk4NOMe+jS 221 | ; vvusRRMQlDiTP7TD3tjwswGwRlSDyfHbXovp48efIPbLeYPKasI01XGOuyGpoQwiGBP9Sw5y9RO+ay 222 | ; 8cOcbRpIxEGdtLFyeNuxIwdu+WS4STXtT65CFUL743pWgI2EYscEjgnKwZHKH7U2BscLkk2EJ+iNXJ 223 | ; hbkHO6y4sKtZDBENeEcKgUgicsLIF3NZnJ46iattXeoe5ZqApOy6tXr/4qdbQ/eBmR9gf/bnu9mYog 224 | ; HwEpM3YL2WUopgb3ZXbh38r+zdaK3pHo4G0VvYJ9k/vmigM8ZFwmrhndxQTe5Tw/uwseifjPGnbd3r 225 | ; Jly199wkQ6N2Nu7evq6v7Gwn/bU8wZ7Q84a10sSJjgQhpmcDKCbdHgok4T7IhvTOiTQwe/OvxHucty 226 | ; qooHBjP+iRCelBWgghs21949/NpP7m6dM7a1bnTvHDK4fkyvXCPzqsLJ4prRHeVgjBG4CnPDcFTsvX 227 | ; 3ARJgthvElUFzbYeQgKnMjUAhl30NVmMFFVdqYwb05sW8ORcrv+Dgx9mVDBaCDl4x8NphSPCQUni6r 228 | ; qU8eNSxYUDo42FzxXLBzHEq3BbHv7OKWWtfCEvHVZX07ZDgq0VZchFxRszNUjuqM9sdHeBVncGEE28 229 | ; zgVpcPCF4rfzgkBKh73aiH8yTLs/JS8SAlHcgUjtNqsSajuwqrIcOIURA/mwpOX5waCKnicFTiZkU1 230 | ; 48FHMCpVMp1rO4wcRHaMycTVKKaq8ADCKOg02/FrRvULlpT0DaYVF4SeGOJzVEuX1BOAMMYT+9+8zW 231 | ; bUfd0B+Nt/nhpvTujbiaPSrRBRocRN+RJSRfuDcqZPh5AJfy2ybMZBdVEAtMTkI6pESHjR/ERVKK4i 232 | ; pcu+HobnEq9PmzppQrtPUKKIozLtiNuQCVLdOyOLKrQdxIb2x3V5V0d4gxNj4riTEP8OzBPQv7Lnq8 233 | ; LddQGEDM8VRfXK8Fyu8hJqHJW46aqixE1xVFHPU6209ZB5mc0LpclBmLAhoSnj1CYnURX7uG5mqyo8 234 | ; 30WyfWVFaQdm1GU7Jr7PVa7nEkfFjFQFJa6S89k6atSzndD++KAKtRGERzB+jLoCK1U/b0I7nAe7Ng 235 | ; zONH/SIW9TTeDR03h99uiOsE3IVW+DlW83zyxrRSJuOwsUJTIyQZOcz0T4yUnRURFaO6H9SbuDuBZV 236 | ; MuzNY/vlGpFfK/Q32DOpGe55wanB7oJHIu4DRwURUeIi1avr7xfzm8dof/hbeSiOsSfdQSBK16eNKg 237 | ; 7Wjnk+ODptgPIzXZ0aLBHfOnNUK3BUvhlFYKR79uz5CyBLDDKUBO3PuXPnfhdL+2P7RRsq0zXqmaSo 238 | ; sK+/uWBc68cLhgSU36sqPI6qYV3131Rof1wIi2DA0cZof3xf5yel/cFuQdnQ+cmkbgfhJYp9HTo/29 239 | ; x081DVcO+X0fA4qsZdW7/nKW98wER4w+TzXqrboVQl7JYqp7Q/VeVd30FUGE94QWjEO4lYusS1Zj5n 240 | ; V0QclVjN0y2JUxhmErQ/UXCVSNofm9uhkICmjVGDSlhIsi7TUxtAyOAkYaVLlIB9dMZ5HFXcQJPLOy 241 | ; dVG9AuaX9UbqlyRvvz8fxkYmqXwkIScOxOzxRqAwibq8dGli4/W+p2U2F3waviqJhQkgmaNKC7JO1P 242 | ; w/SCwOVi+xY+JJlYUfbYQFYcgBBd6X3LZ96PSj5d4df4RNwER8WM1JZM0LYBrQsTiXI0p7Q/KswQ1e 243 | ; UFSvX9fBD0BmpG9gyqp1a0RQ1kAeL+TtWgx4amcNcGBqu2r5jfIlsUV/g1nhi6+fC+W7Y9LtOoQgcJ 244 | ; HiU6t0OJf5cq2p9Vry6+31XCrBzV6YQRud8li4nZPPmW8l65wSmcKlsreuUmEHHXxsKiPvL72LPNPx 245 | ; f9DYajChtoshGdqIIaRyU6qkoCbwNXMaL9CbsdipfOeHF+aRv1oicprKcRFRNPyBYocP/IzoWTH2yv 246 | ; KmtdM7W4bUoM+zx1Z1yFGJpC4qIKlzgqFUdNlPaHP2pZTP1Y6XL0i7krz9Jo7KbCgxLDqDB1Z2eonU 247 | ; PEUblutoVFFb5wVBAxr+BpfxjVa2ppfwCl2DVpQJcr+SKZZrMgfEn81KlT/646O4Mr3SjDKkYMXVva 248 | ; p8MnjooZKYsqTpw48UeqipeJo/K0PxQVL6e0P7kXz54iulSg+eQoOAFwpcI777zze1APhelAdAxqiD 249 | ; rDUdVNLPzHe++c+j0MxSdMhDlJY2PjrRs3blyh7lmoSqppf0C1ImvobBnXL/h6WdeoaMkEsHTQgGK4 250 | ; CjD63UumtqExiJOGweipvxOlY1a+ZQNNCG1NJydtNkm29jztjw+YCBMZ7Y8tVN/IQfgjlS/byZJXnC 251 | ; LgjerqTuJTogaafMBEwgyTT9SfWNoflbJdWENnUfmQHF9UPhlhWkXEUYUZoCx5dWGYspPCB+1PXM5n 252 | ; W0lzRvsT1tBZN+b5LpuP+BATYmgXd07qEEO7pv1Rgat4pf3RjSt5L180e2Yr8pFuJ9EXHkelSwxNde 253 | ; ekyK6iS/tjAxPhndOk8Rh3bYexg5jQ/oQpljVsfvrahru7pwzq6HYSNRFxVDa7sA34NIpdRccOTGAi 254 | ; TFJL+0NBI897MO7v2D+nuK3bSaJFJIamorzRBZ9S4ajCHFUlL0gd7Q/FbiGTzmefOf1dw+yStlMzB3 255 | ; bZ2REbiSKGphCVOyfDJu2oJC20P3HPfoz2h3q3CBMWE5890XTzjRlD29N0G2vSokoMTbpZhYBPVYih 256 | ; qR01KdofWUm8k/YHL+C6JCfz4ObGg7f2zh/TmvRtrGkQdmpsqBzR5gNHxTYrPnmF06hM2rl0VJ2BJm 257 | ; od4FTD+0hpf1yShIUJFqCpqelPHx/f96cdKbpX0KdgXmRDac+sczwVHN629o4u1SuFwCivXLnya2YH 258 | ; SdP+uIxgZBJK+4PFoKb90V0YKOPy5cvXkLzvzeYl9RW9g7NVg4Nvunhu8kOF6qlg+7xxDz55//x13z 259 | ; ARZpxYdxhmUrQ/LN95Imh/VBdFhKvAKJpPHL95eEP1ve0TB3YAndoVHQUX9RzLhlM4NdaOG9x+sH7d 260 | ; HaaDKCpMl4bJF2TSRPvjWwfs+yJpfygaOlESRXPKHPX9987fOLlj/Z3d0wrb4Shho6z56BgN2RMDE4 261 | ; brxg5qb9xWe+f9985JdeAKJhJmmKIj+MhJ4xrQvmh/ZH0+Ke2PbUNHdVGiTinW0Dl/5tR3R2rntbw2 262 | ; +tncKCtAj1cWF+aVY2BojDkGZG/twntwjLiFdwETYeurMmmXBtofG5hInA5UbqlSpv2x9WDdSTv2N8 263 | ; xRD73x+veLxw7r4C/LPDNrUPDF0nSWh+EUIKZem3kmqB7+o9ydhwvHF+XodnQqRFQwESYmk3aUUYVJ 264 | ; A9oV7U9YeVvLQdgPso2JbeEq/FE7r2pW67RRQ4MVZf1zjsKcpWn6gOAD4VJOn4KEGwwiDZxTQGaP7B 265 | ; tMLh4S2JYubWAiomGaGJktmSBztG7aH8mi2O5+YQ2dMZmioKp0SM5Z2MkCeXNCn5zDnJszOLeTf6Vw 266 | ; JbOOfLN8WHBpYWEOQIjvYiVaSM2IHwVzRzyXcwrcd2iyKFH6NEleKZERplGFC9ofnUpb6mh/bEnCwi 267 | ; TqqAXtDruUc03Zc7nba3nZVtErd/c5rhqAnH55YPDu7MG5q5pBT/TZosIc3+6FuQWPSPPMgTlnA4Pi 268 | ; 9rG9g/VZZ1hd8kywsujphw4x8pncPeq4GXdCydBHqH2oMURMVGAi1IYpGpsJ7Q9lDpFq2p+4mNg1XC 269 | ; UuecV3Nmf//eyJozdrZoxrm591murSAUFt6XO53T5M1o0KFzhDziFKng0Wl/QNFpT0D2aXDAqqp1W0 270 | ; nTl+9KYsXPGlA5nzmeR8uqJD++OiAR03OUkBV3FK+0OtEF5kjqq6W4zJjMzJpMywYMaowhzXbmXmxd 271 | ; x/52VcZoT0HcJi4rjSJbUOwmYcKEYUVEVG++MLrhJH+2PbcCSj/QGjhcvdQibMGNB9PXDgwJ99LIpM 272 | ; Bzq0P5TSWRLP6qChoeG/VCftqCRNtD9nz579N0b7Q3F6O6f98SE4NUC5w67u8k15Ax2o0v641AHW4N 273 | ; KlS98kRfsDbjRG++M6gggTOElqaX82bdp01zcaWCxdgnJGt9JGsSj87/YJE2HChxiu6T5l69BN+8OJ 274 | ; rHTpE88lg6voVNooFyVMB64dVZbzddP+0PTvIN5of1wbJi8qBNuUixL2GdeOGndSuN6sVOAqaaD90S 275 | ; mJh4kx7U/czmja0NExzLhnMnoaSkfVmbSjhomIeu2m/VHTq81m5ZX2x+aoNS1digZl6qg2pUtbmAhv 276 | ; mCbGTgETyRgYJi9UUYVpA9oZ7Y/ObiETm5h40iT9O+0yEiM1PWopRkBtZhxkVK8mOkgL7Y9JVEHRfN 277 | ; WNKhKh/VFNXqnhKroxMSWOKmyRk6K8SQvtT5I6SD3tT5wHUxsmE9WY2CUqQHXGwSVcRWWzcglXUZ2c 278 | ; dAlXUaL9EcMmn7Q/YaeTS8MUjVR21ProJUTFxDY5n64OZMmrD7hKVFShW4wwlaioopP2B//gs3YuLg 279 | ; KLiX03ucTJSX5X8wVXEZNXnzgqZqR8KM1T3vim/WE6SB3tD5gkWHveN0wCC3Ds2LGbDKqSBOUNdMBT 280 | ; 3iShA1AfMR246N3o6CDsRHEtjPYHUJEkaX8uXLjw20dof+ApaaD9gZP6PD2YsGIEFgUG4vME5ReG6Y 281 | ; CiJK4rYbQ/PmEibB2YDhjtj2+ONkb7AwoqtlGljvbHtKFjY5h8vuMLJsIvCq8D36FuWM6XJO0Pfr9N 282 | ; SdxWB7G0P65hIrJFYWLa0LE1TP7ffDhqFFzF12aVdtofW5iIiljT/lAz7KlWyVzARMIMU/bbXMBEwn 283 | ; QQFUa4pP1RqRC5dFTVBrSrloNqA1qJ9ofCg01Kl9SOalIhCpuctNGBLlzFFe2Pzu+hjCpMGtDUUYVO 284 | ; AzoWakJBhUlJ+2PiqKq7RdTf28bESdP+2MJVxJK4iQ6Spv2BOKH9MZ1xMNktZGIaE+uQhKkYqa6jUt 285 | ; P+mGxWUVSvJjowiSqeKNofFQ92ETvqxsQu4Co6VJimixInOptV2mh/qPJZnc3qiaT9iTpqXRkmr4M4 286 | ; R9WZnTHVQdTJ6Moww3Qg+406szMmkje0PywmjipdujDSsBkHn3AVGe2PL7iKbHIyDbQ/vuAqss2KCq 287 | ; 7ihPbHJ1QjTbQ/hw4d+k+fOComrGTK0/74hKvwUUUaaH+YDigiGBLaH8ATuml/kqX9gTEy2p8kdCDS 288 | ; /vjGckEQckEHqaf98cWJJJYuk6b9wfe7jPtlwoeVaaL9SUoHeUH74/qIlyXi3bQ/fmAiYTrgHdKXo8 289 | ; oa0KYlcVG6aX8sF0WmfFcwkTAdhO2SPmh/4ipEOiVxE9Gl/THZrIxpf+J2BRd4Lt3SJSVMhH+majEi 290 | ; LbQ/1JuVDlzFRVSh24D2QvtjWrqkgk6ngfbHtHSZNO0PBUxE1IHuqUQVVZg2oE2jCiXaH7yQTemSiv 291 | ; bHZgeioP2xyWtsYmIquIrNnZMUcBXbqCK1tD8UMaxJTEwNV0kD7Y/ujAM1KsDEUdNE+0ORy+hEFaml 292 | ; /XEFV0kb7Y+qDqirYWmg/VF5tku4ihLtjxgT+6L9kVFhUu8WMomKibtpf/zT/mCzEml/fNxSlbe0Pz 293 | ; 4G9sWYGDrwwcUkLgLvqBRUr7pGylfaeMobXwwzaaP9gR100v7AKMEkAUYPynKojpGC9gd0Lz4XhZdu 294 | ; 2p+HOgBkiNH++EJDMEkD7Q82ZeDZHqP9AX4H4jqsCZMw2h9fMBG2MGmi/XEd2slEpP3xrQMWViZN+w 295 | ; MdPEL7w+JPnzCRTCa8QuSb9kcsXfqEifCLwuvAJ0yEN0z+1EiS9gffRwUTMdFBXtD+uISJyAyT/7ck 296 | ; aH94I3ANE5EZpkwHLml/ohrQPjarVNP+RP1oPoHvarQ/qqVLFzARcX2TpP1RaUC72qxUG9BeaX904S 297 | ; pUMJGw32NC+2MbE5vAVaJK4jaGqeN0rmh/VJ2OOvzX6fMp0f7Yln9tS5eUtD8mCuYdNQ20PyY6oKT9 298 | ; MY0qbBvQFGSCun0+bdofsaHj0jB5MU1eKW+pMqHCTAPtDyUqoCvQ/ujYz/8Drubc3t1SGwcAAAAASU 299 | ; VORK5CYII= 300 | ; thumbnail end 301 | ; 302 | ; 303 | 304 | ; external perimeters extrusion width = 0.45mm 305 | ; perimeters extrusion width = 0.45mm 306 | ; infill extrusion width = 0.45mm 307 | ; solid infill extrusion width = 0.45mm 308 | ; top infill extrusion width = 0.40mm 309 | ; first layer extrusion width = 0.42mm 310 | 311 | M117 Shui test file 312 | M300 313 | M300 314 | M300 315 | M117 Finished 316 | 317 | -------------------------------------------------------------------------------- /plugin/shui_prusa.gcode: -------------------------------------------------------------------------------- 1 | ; generated by PrusaSlicer 2.4.0+linux-x64 on 2022-02-25 at 14:16:00 UTC 2 | 3 | 4 | ; 5 | ; thumbnail begin 100x100 7844 6 | ; iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAWwklEQVR4Ad1d+1NVV5a+/8toptVogv 7 | ; GJRMSg+AJEQcAHEEWCiE80KCpiFBHlIfLwiaKiRo2PHpOJjrFjzGimnXamnZTdRaacUDNWNdPFTPED 8 | ; Vbeq7w+n73fTCxeb/TyXS+xO1S4rl7PPOXs9v732d/YOJCQkeGLbvn37y/Pnzz9qaWl5On/+/CB+Ky 9 | ; 4ufnX69OknaCtXruyV9aNG16L/6tWre/BbVlZWH/4f98X9df3xTLp248aN3fTboUOHnuM3/Dt79uyQ 10 | ; rv+uXbu6cO2+ffte0Bj4uNLT0/t174DnijLAuEkGGKOuP8aNvvxa3KehoeEZyUA2hoBJEOLfdTfkgo 11 | ; DQaCDUcD0JRfZ3URBQokrZaLK/kyBwD5nQuGHI/s5loBojGQaULf6dj5ErU/V30TAC1MEkCJUH0bV8 12 | ; oDJl8iazNK5sKFWmLJNh6AQhCkVmODLP1kUBUQYmgzYZBvoHOjo6vj18+PBvbQShumFNTc1/2AhCZW 13 | ; l1dXX/jr42gpAZxsmTJ5/YCkL0JigAMqAxqDxX1mDd9Nz6+vp/sw2H3DDgZejX2tr6a/QNPHjw4JeX 14 | ; L1/+Ghani8uqF7pz586XX4T/w8BsB8KVcubMmcd4B7yUizKoHThw4PvHjx9/Rgbl2h/PvH///j9cv3 15 | ; 79K78yuHHjxn3IAO9gii4ypbS1tf360aNHN9E/AK2Ul5f/p6t2eYhDf9tkR417GPrr4rZKmeRhlZWV 16 | ; vydLc7FwPgaXkM2VSSFu/fr1/yMmcZsxcBng3QMyAeluiJvQ4HmIMyU7lSCoP092sFSdYXAEw73KFg 17 | ; mqYj0PQTokqAIvGANHgjrDUBlAQHQfnaXJoJzYZMlOJgiVJ5BhyJ7Bk7EqX3HDkD2DC0KmNNEwxGfY 18 | ; gBedYfD3k+XsgOmGUIIJyolNZmkuCEZmGPyeNolbNAyTIMQmQ4Iyz9Z5kYgEVZ5tVAjXJBL+pUuXHr 19 | ; oiGFIi+qOvOEmzaaTEa9eufeWK4rg13759+157e/s/u8R3lQxscxw1ksGVK1d+pfI6K4WQRQBFYUAu 20 | ; yY4arODChQvf3L1793NTblIJBJCWUJirMCjE4fmEJF2RIN4ZMrh58+Y/qSajugbPhgwIhZlkEFAJgk 21 | ; /S8vLy/mhb9uCWwUOGbbITvYNCnGwyalImD3EuZQ/qT/kK/+KZprKHzKA5etVVMZQKoTgnxnpTslMJ 22 | ; QiZknaXpyi+2SFAUBL+3DRLUlV9s6mE68EJQWWUYAZkgdALXWZoNltfVw3BvU+LW1cNsyy/c+/h72o 23 | ; IXnWHYgBcRYHAZBDBwlzqUzNJgKS4IRmZpLghGZml8kmYz4xeRoCuKE5EgZGgTkkTlcQPGcwNINkh4 24 | ; rgiGbohkh6Svg3KqhpdA0kbpwhXFkWE0NTX9hvq7zNK5R0AGKJ34AS6QARAUZOBnDDAEyODhw4e3Iq 25 | ; UTCBTFNVdISAjm6tWrD3APPygIL9/Z2fkQtSA/AiXPBgpyLXuQQvFMKMOPDGgMHNa6ygBGDEhOMgjg 26 | ; B5eyB2mVu7epxq8SBA9x4mTURhA8xNmWPbgg+CTNVKWQjUEMcS71MBl4QZ+A6H6mG+piva7sIROEeI 27 | ; 2p7CEKQrzGBgmawItNPUwneJuVUV3ODuisn9/Qpg5FQpFZmkv5RWUY/HedF6GPDAnaJm4VErQtv+iQ 28 | ; oAm8BEw3xItt2bLlRxcEI1oax/W2SU8sz/NJmk2eEdfgXWpx1HgfP+V1PkXAuG1CqrZ0AkECgVDpxM 29 | ; 8CFJIdoSA/izfNzc1PCYG4IhgSKhZ/kHhFckRyYoKXOnvGoIbfRKHeunXrHiFJPzJAHYxk4Kt0Qi8C 30 | ; JQAFnThx4jtXFMRD3LFjx/7VFQVxLz137ty3rgtgaOSlgJXtdfu/v3Wo7OXZDxNDn344zbtXNNX7Zp 31 | ; 263V4zzTu+Yrp3eXtu7+3G3b9vPfrTEq2rDCh819bW/lY2GbVSiCzOudCAZJM0l8mniQZkQoKUuK82 32 | ; H3z+RWV+z4PNyUGd8G3bvfKMvl9Wl3a3H2956ocGJObnvykakOrvMJaOvRu6h0sJOuXUln30KhoakG 33 | ; xlNEA3NK2k8SabcwwHDci2/IJnygyjIjclGGtFiO3axoX9uWnzgjYGzZtsihChASG+vik0IJfyC7e0 34 | ; E/U1zyGYkVQEb1+Xzgy1bcrt9ZMnKccgz7YMBw3oi7/8B2HaMla4UkADIhTlhwZ0dNPKPz7aPl+pjC 35 | ; 8LJ3l3CyePiGKQr7KXuMkAbRAN6Pjx4//ihwbEJ2kVFRU/uJQ9SJktChqQTX9A1DNr52jDU8visV5O 36 | ; /Fhvc/I473bBxJFRyoY5wZK1djIQaUBwCl80INkkTVfjF5sNDUgX+koyU4KApTrBHM2cGFoUHzfQp2 37 | ; DWBO9U5tveg4+mxFwp98sW9o8oDUintFjTgKpzZ4Vs4/q+gtR+/oxFM6d4R5bP7P989XsxVwrgsWxl 38 | ; NGoaEBDRz0EDEtfgEaJ0XnG/aLJ3cunbQ/LFp1uW9CGu0yTt3OmTT8qWJvVfzH0n5kq5UbezixufDa 39 | ; /tr4IGdL1604+weNXA/3HNJG9D4mhvTsLUSL4QhQ0ofOZgRRcXxOKZk7za1LExTfgwIMgA1Q5aLzEZ 40 | ; tJEGBAqNXxoQvAvK9EsDgldc35b1/9p4HfaMktlve4kJ8QP9chLjIsIW80XHpiV9XBjJ70+PKPDKin 41 | ; djppSt2SnBYaEB8TjnhwYkJi3br58G+qfGe1+WzAzZDvzjxfGD+mcsmBtsLlrUK6IroKDSD1cMmvhC 42 | ; gW1LxsUk4QMKE3q1WQBT0oBECGtT9iBlmmhAJlZK/Up94r6RFyf9vSNvWsSruDEUpUwOiegK4a9x3d 43 | ; I+bhjz3p/q7Zk3JibwmD/HtDL6RtGAUILQJW7kitoFf+9tWDAleHRpXEgW/yHs641VLziCAbqCsEV0 44 | ; 9VnJnCCfyM0Kh72qgrReKHA4FQIjUUUgUQYBCNWVAhMtDUi2Bl+5ZLp2UJS4k2ZMi9yjcFVu7/7smf 45 | ; 3XV8m9pSZr+qC1DQi7MGm8NOHv+Wh5L/fs0rxlvfsWjBu2hL8qZYYStJwfbhoQEj4t3rgmbbwE6lC/ 46 | ; 2p39fzYDo5DEDaOqePkfLhTMCMriP7xNFAYlfFHYd/atfdXW1PCMPBsKlCG24VQIGtGAiL8cgDD90o 47 | ; Bg5aABESfJlQKzbemskK46C684LYQPLCxRAqdJZOeplu9qM6cEVfEf3sKfS+hK9C6AiI15mX0yBUaT 48 | ; 8JckTVXKwEgDMpU9yDJVNCAbhvnCObNDrXmJ2sSNOJ49fYyXHp4vyMJHZ/Gc4MX2U0/Is1PnpwQR/1 49 | ; UWLXoXxt2wY/1LWTkFlVsRHu9M8VcPAyynMCvKUFw7Qn6OigYkFiJtaEBFK7L6THUoDLyqZNXA4g/C 50 | ; R2ny0PgPiy5MSxpEA1qXk9anSvjwrt3LZg36CitrQTiHSNDVnS0L+wuy0gaNz0897NNVE6WhelhpQD 51 | ; ovUn1n51KHQkMoO3W46gU3jF1rc3qac6cFRWGLISl78aJ+JHyVt3xZkdNbvuW1IKBwEja/Doht75rM 52 | ; Pk4Dqi7f9BLh0bYe1rx2Yd+w04BAgYmGBmTyCrg11i1kfwMxgdOA9ny89aWsnA7igpjwd+Rn9LZkTQ 53 | ; zZJnxVOQVEB5TUyfuLVhf0ALGZlIJxHTuw64WMBqQyaCMNiJdO/FBgUId6tGnWn1QvTXD2w8S3lYgG 54 | ; KOzSqZan3GNl4YMn/AGLXLOq5+T6jP/VJXwOjynhy+DxxfqqSC0O16OfyTtq0t8ZoAHZlo+UCqE4B0 55 | ; jMaUC2KArWemH9Iu2y6qXl70QSNyW9zMT3lAU/hI8rB8te8gGpJnyUwMlLW5saf7Nz+bw+1YRP9C60 56 | ; 3FlxXkf2+CHXnipe1G/KgWhAcJnzkgYSd2Nj4zObJV5fNCBTkdFmAUkV/2Gh5QvjQqqCH2pD+ytf5z 57 | ; CK/6JFw1tAA6I8husx4WvInhpUJXzuXZBB296tXfd8TA7h9QeKsoasHUVFA5IVEW1oQEfy5zqzPriF 58 | ; kjEcKM17pUI0VE7nhqGK/2JIWpqe2r83e5Yy4YO5wr/C6lw1yVkZLWUF3bY0oCELVCRUWyIzmowGZK 59 | ; pDmRos9MzHBT28/KIr+CFhdtTuecENY9um0u6mkqU9onfhvbKSZwwSypbchVJ4fHnvum4ug848N2XU 60 | ; FS3uuXC+IzoaEDZwsa1DiTdEv6a1+lzxRfhFzy0b71XPf8vbnzLaOxj+tyl9jNcZziFfFQ32ANB5xB 61 | ; qUCEkJCGwsLowYEfi/3LNV5XQx4aMetm/Z+/249ni44d12bvyom2QAr7VVxrnl74UObsjvdqUB0RRh 62 | ; gAYECk40NKAHdZv/S/eiLRljvRXTRnsL49/1khOmRPp9kDDdSwlb/4rkKaGG4oyeCzkThli0CEmRwK 63 | ; sXjAkrcqyXGT9uYEEKxvTtjY5HgMcodNL1Ku+SzdhvX7nwdXtjzbMjNdUDoMW0ZMyNAxXohh2lP/gh 64 | ; lKMh4UtpQKZNXwYl7jAux2KP7mXr0sZ5i2bESfvzfIXwYRP/kfDTE+IG+kMBIBMMhL3yjL7DB14vE6 65 | ; smfAiPqC5vXv86TOOTCyLdocZmo4xrYSS1c2FcsLnh8MAnG9HsBiSlAZluSDUYDF73soB9KTMmDfSD 66 | ; xSF5k0Bg0TzpQdjncoYiKzH+k1Ufa6x/dm/jB0MMguYLnA4L71KV03H95cLEEDzHZGCDPH/xWO9AaU 67 | ; E3H4P4TYotFYq/qy8aEErVphe+UjKvn7uvbL2DwocpPBAk5QgFCtEV/PCOKI9weDwc5XSEKHjFsZqq 68 | ; 56bdgFRUqKhpQPiNBAFsb/Pi8CAOnyvyUvtU+N/0rcaAkMMFP8zYuSB0BT+qhyEMk2e3Vm7tArryU0 69 | ; 7Hc/bkp/fU1xyw+ihUtxuQLxoQbijSgCjs2LTqim0DNCDwofZkTAtGa6FQXtGC+CEJXzZjp/Z5w44f 70 | ; kCtpkobweGqZ/XvAK45kTg4eKit+6bLmQx4NGhDJcFhpQJuXpzkxzBGGrp498R2v4wCSNqTLGR6YW3 71 | ; SGFXY0bYxXn/oLryUzLtSWMc67mT90mVa2RCsr0XOPPVbzU8K3qUNRO5c9IRKiOs+e/tamSiEz7JjR 72 | ; gFC2drVoYnjwNfi05JkhWPSt/Ils4OO99TNHeWnxE8JgYLJXvDqvpz3sVUd2b+vKT54c2vHBKM8GHq 73 | ; uWaOldbL8fgXE0pI6JJG68O9+Hxc9uQFBk1DQgrk28gI49aGpI4ICZlOxyl2X2LU98x9uSNNoriB/l 74 | ; LQrPU8gYVF9hqbxLnPDRjF1FgLAJUfCKUy1Ng/KVnx2RxMRt+jQwwB+m28/QZdaqi/8IeypLI9IDeS 75 | ; I8QBQ2JnwydAWFo3wzyLPnTgu55izAWSTupoY6pcB1UwSbTXCMNCCbT7EQGqJVCDXg/quflHaDP4Ui 76 | ; IZZh96+cG1SFRHF2rVvhw31JEGA/fl5oh6bgFXvT3gseqdjcNVK7AYkhzYkGNJwK8etdsnLK8cwJQ6 77 | ; 7F58yNOW6J+/DajAicNX1fKTYYNBCU392AxB2RAthJh2hAuipv7pz4n1Uh1Di6wvziaG31809X+iNL 78 | ; Ux2qZtPqH4GEotkNyO+OSJA5IDntaDSEBqS6IbD7SAqeOFkyeAylcHevzUlwBhu8DkVh24UQLvv4yH 79 | ; VHJJG660wDGqnv9JBYP86d35uVHB+STfjubFvcxwWBxO96/6qi7FeyMG2zMqpbO7L5NFBH3Q3IYpqK 80 | ; BnSiOLUv1srAOvvG7AV9NGDAYyTwhrRfhCd9E7xD4RCzr2TVKy4IJH1br4NXmBK3amVU9X2l2Dhijc 81 | ; luQEQDOlq5reve2th/YhyBuwWZA4aB0nbN7u1dzZ/seLFzw+sFJDQTSZsaENknq39K3LZLDLLdgGxm 82 | ; 2zw/qHYD8kUDwoNFGtDRJRNirhBqoJueP7L3d/QdO/dYIC6buhq8onHpxD81VZW/8LOR8nDtBhQTGh 83 | ; Bw98q500OqD2Zi1cDrQi0K1V6EJ9vqMMIfQlRrY90zKi66oCBK3CgO4uN+lzmGGOJiRgPCb3vzF/WN 84 | ; ROiKpvE6FNGAdJu+6GRAAnTZF1K2B7HNvpBaGpBoSXzTl/ayFa/8cJZi3UwLSKaVUdMCks0UwWU3IC 85 | ; 0NyIa/Sze8eL7j0ckNmT1vklKoDmVaQFKtjNocJ0FtOA4Fk+2IFKEB4cggPzSgttaWp43rMntG4iN8 86 | ; k1dUZ0yOwFmXHEFGiEpFVVXV70xVXJW3oR8/FMx2zYR7G3LMsNCAgB46DlV837j03dDdwkneN+tGVh 87 | ; nwikMlOf999tSJJxicy/tTrMfnZNEcCobzTfweCoampQHZWgcPcdgNqK3xyLNjWwu6T2RPHBHFnF02 88 | ; 3tsRmeRt6oKHux6HgSYebOZq4TzE+TkYjYc4jCEqGpA4eHK/i2eOP2nakt9dt3hC6Gq46CcyE6NpuB 89 | ; 9Yj0cK03tO1h98JuJ6W0K4CrzY7oik2vtF/DpZ522q8ssQGpDO0myOkxjY5a21+enW7Hl962ePCZc7 90 | ; 3opQ+10VhNoZlnZRkV33/t95yxMnhCrLNmg3wTERwk3gxbRBgk3iNtGAZKfcSRUiszRaH3A5TkL8hg 91 | ; 4LSuVrcnrqNhd0b89I6N+a9Ja3b+6oiLWD0IA5A9r+eeHfU0Z7ZUmjvOKkcaHKgrQeJOoFsxNCLgiG 92 | ; GwZVXrln22yCIzt4xUUGsk8PYkIDMglCtDT0xwvILA783A8SpnmzWeObyMgOBXNJmmQYWKugmbrLGC 93 | ; gEcRnYfKUsyiBmNCDXPRUhkGh2A8KLg9lOH9W7CIMa+mA1LppDwfANOdWy3igakItQVbsB2RyHgcbd 94 | ; u7Cw8A8uiz80Bh7iXI/DEMGLSANylQH6DzsNyGYRX1d+sfkOXraSxj3OBgmqNsERDU1lGKrvyF1oQK 95 | ; qzsJxoQDbb+PEbyg7EkkE53nTf2dlsgqMjhJvqUDaGYZO4VcdhcIM20YBUSNCaBiTekA+cn9HnuhsQ 96 | ; WZoLgpFZmg2C0RmG6Nmm/uIUgdOAbHOVeDbjEBqQa9LmuwH5SdoiBcaVsYFBY1du6m+rTNEwwPigQ8 97 | ; FcN3OOKQ3IhZOk2g3IdTDYCQcrcqbcpFIIhEgozhUW0yQNsPivjgYkEwRZhOtuQLJJminZyQTBvcLl 98 | ; MAASBA9xLmUPUQZ+aEAy6i7eKaAapM1uQOI1NufN6sovprKHKAgRKZnKHuI1OhqQTga68otNPUyXs5 99 | ; 1oQDYIRkc4s0Fx4nXc0mwX0VSGYQteVMdh2JZfYkoDwg35gViuuwHRpMglnIiGARqQK5FZDEE23q/z 100 | ; JD80IHGKoIL7RoXwG0a7GxDqUIQgXInMeGmsxMloQLaNHwrmp3QC5b0RNCDC6SINyIVCAwtFYY0fCu 101 | ; YCrcmqcQ/XxR80Stw4o+TnpgFR/5jQgEwuL5uk2S7+cEFwr7Ate4iCIK9wRYImGpDJ22WoLyY0IN3f 102 | ; TV9hmb6DFwUhy1emepgJ/o4EDUiXuGNGA6IbUghyKb/ILM20kqYzDBtBiGOQrYy6lF/EsofKs1VNdT 103 | ; DaIBqQy3ES3NKiPRSsurr6e1tByAxD3A3IlQaEagHRgFzGoDoYzRa8cBng4MkWkQZkcxPRUoEeaPHF 104 | ; tRYGwUV7KBh2A3r8+PFnZO2u/SHUaGlAOBvkjaMBuZQ9yDrIutDf1tV5f8pXsE7X4zDQVDQgW8MaTh 105 | ; oQPHQIDcjmhjoakKnsIRMEWZRN2UMUhIjYbJHgm0oDwvVDUJbO0mwKeLoCm24ljZqpHmYqvwwHDUhX 106 | ; FbClAamQIDdomQz+DA+3XKARnVcKAAAAAElFTkSuQmCC 107 | ; thumbnail end 108 | ; 109 | 110 | ; 111 | ; thumbnail begin 200x200 14596 112 | ; iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAYAAACtWK6eAAAqiUlEQVR4Ae1d6XNVx5XnD4ltDGYzIK 113 | ; OYTdJDQmIHsRgjFrPLYgmIRSwGhMNig8UuFECAMQZj4w1DnDge42BPCJl4EibUhEl5ppgUFaqiquED 114 | ; H/RBFX3Qhzvv90jLTXP73l5O971PaKpOTRKe7rvv9DndZ/mdX/fIZDKBjlRXV98/fvz4zffee+/Gyp 115 | ; UrW8I+U1VV1Rr3GVOpqKjo2Lx58108G98xadKk9rjPTJ8+vY3yHfA89vvWrl17L+4zeBe8E+U74Huj 116 | ; dACB7tlnsCaU34/vjNMBPnPkyJFbPnQgW2Po4MyZM99BTHTQQ/WDKoYpfn779u138Pl9+/bdjvs81a 117 | ; Lw4sJRVQwzTGcwFGod4NlxnxcdlVoHKkbH2w2Fo/KOBxuL06uNo/ZQfSEdw+QFJw7zYBsjVdktZEa6 118 | ; e/fu720dVTRMHSUzR7XVAX8iQK86OqBwVPwddKhqmGE6sN2smD3p6gBiElXEOojubqGiWB3j4g1T92 119 | ; 9F42KOumDBggcmf2uyKLyRMh3oOio+y5xc1zB5we823axYaG3j5LyjmuiAIiLRddQeUS9kultEGZqO 120 | ; s9nsFnG/SeUU4BcFBkqlAx1HpTBMmQ7wm+J0wIfKKmGliuhGFbxR60Ywst+kGlX0iFsUCsPkRSUmpt 121 | ; otZKJyKsJ4KRdF/H1xMbFuzqcr/Kko04FNaK2iAxVH1cn5THUQZec9eA92sVuESVRMTL1byIT/HtFR 122 | ; XRqm7Hv4mNilYfKC75Q5qkvDFI00bLOyyfl0JCqqgA56sF3a5Y4ZZaT8UetrUZiIMbEv5xR1wMfEFD 123 | ; mfqQ7wnbwd2OR8OiJGFRQ5n67wjgodMKfJnSBffPHFV59//vnXeCkfhskLvq+pqelP169f/7nPReEF 124 | ; CoEOvsz+n2m93Ebwe/fs2fOXGzduXKHMd3QEv/nTTz/9BnpwEVqr6GDHjh3/c+3atV9++OGHv0lKBx 125 | ; 999NG/wg7YhtUDHoMXOnfu3O9cH2dhwjz3gw8+uG7T0DEVdsRCB/hu1eSVUljOx96BKiHX0QE2JqwB 126 | ; swMXuZ+KDi5duvTN+fPnf+dbB+wkhQ4uXrx4nYX/PfhjjLqho7IofJXMdedVtij8Yqgkry50wHIxm5 127 | ; K4iYSFlTYlcVPD5ENrm5K46TqIOmAl8R7il7uEiUQZJi8uYSJsUaKKEa5hIjLD5MUlTIRJVM6nWxKn 128 | ; MkxRB66jiqgGNN6vh8yATBs6cYapWiFy5aiqFSKx0kbpqKrFCBcwEVEHcc91FVWoFiNcRRViA1q2Dp 129 | ; GddCqYiI5hikZKARPRNUxeqGAiYYapstjUeC5eB6rhE2V1z7QBTRlV6DSgY6Emup3XuEUx2YVUGjqq 130 | ; hmkS11M4qm3p0gYmwnRgA1ehiCpsG9C2UYVJA1oZzWsSE5vuFnHP0jlqKWvqJskrJVyF1wH+v6oOKO 131 | ; EqJo5K2YA2RYmbnoLKDgLRiYldwVVUHdVkt1A1UtWYWGV2xlQHKpU2V3AVnahCZXbGRgcq9mXTgNZy 132 | ; EFHpYTFxWOmSSilMomAi4r+76ojHxcSu4SpxjuoDrhK3WblGRsSVxCngKtoOwhuheNS62i3CROaoPu 133 | ; EqYTGxD8MM0wHvqKazMyYSVhKnMEwdCXNU09kZUYwdhC0Q8+Dm5uY/+jJMXlhMjO7ryZMn/6AS/lEK 134 | ; HxOzTjx1STROmKOiE3/69OnvKHI+XR0wR2V6oMj5dIQ5KnTQ2Nh4i0oHVg4CwQsAw3P16tVf4aVcd1 135 | ; 7DpK6u7m/AckE5UJJvLBd0AIjGt99++wvfDsoEOyZwTIBqpAFTl5QOqHGFPWxfiO0W+M8+YSJsUfjS 136 | ; JUILXzARJnwxAt/vEybCdMAXI5gOfMBEmPAIYGxWPmEiEDHUrKmp+T+q/p2Rg0SVLl3DRMIMk1eCD5 137 | ; gIWxRZ6dIHTAQiK0b4gIkwkeV8vvBcspyPon8H0XYQlbmRrk77o5KIu4KJxBmm7DNdnfZH9vtswada 138 | ; DqJL+0MJE1FdFF4oYSI6hsnrgBImIupAxejiSuK2OlA5Hag3K90GtI2jOqf9sYWJhC2Kzk6QFtofip 139 | ; iYgvbHVgdpoP0xaUCbRhXeaX90jYv3fpuyXdK0P6YwEfa34uyMyTukgfbHFN1A1YDucrQ/+CxVTV33 140 | ; qHUBV9GdcaCGq+gmry7gKrwOVH4TdQOalPaHOsnWpf1xAVdRmXFwCVdRcVRXOCrRSKN04BIVEAcTYe 141 | ; KL9kcWVXTT/oTszL7gKmmg/ZHlVb7gKnlD+5ME5Y0YE6eJ9sdXJzhNtD/YrJ5U2h+R+oiFYN20P5mH 142 | ; ISVP++MbLoPfe+DAgT8zHSRJedNN+5NC2h8sCKP98bVzMmFwlTTQ/ly+fPkaRUncRAdJ0/6wk+vKlS 143 | ; u/TgvtD3Qgpf1xDZMOg6tEUWG6NEyR9sdXiMPrgIWVNiVxE0ma9ke0O1EHuiVx03UQdZAK2p+w73AN 144 | ; Ewn7DlEHrmEi4neklfbH9WYVV4ywhYno6kD8DryflPbHFbu6Lu0P9VGrWiFyARNRMcwwHVA7qk6FyN 145 | ; VmlQbaH5UGdGQnXbehQ2GYopG6ov1RXWwqmIiuYfI6oHRUE7gKZVRh0oCmjip0GtBatD+mFSZK2h+T 146 | ; mFh1t4haINsZhzTQ/tjAVahpf0x+AyXtj+pm0+VpfyhvqdKFiYiLYnsK6sJEqAwzTJ9J0v6YRBWppv 147 | ; 2hzCFUYCLUhik+VzUmdnXnSj7R/rhCBehEFami/fEBV4nbDdJA++MaFRBXEndlmKKRRm1WPml/4nTg 148 | ; nfYnLCb2sShMZDFxN+2P20lCUcKiCl4HPpARYb+XCq5iRdrAe3DaaH98dcJltD8+4SrMUaGDY8eOxT 149 | ; KWu9BB0rQ/PPURo/2hgKuQ0v4kgWOCpIH2BwvCaH98gT15wY6JNdCh/akaWxIsnPCoLJ9cHKyd+lDw 150 | ; 36dWPBQVHQBTx66R8wX2FHXQTfsjLApfuvSNhIWkifYH+hdpf+AEr80oDhqqioKT84uC84tGBl8vHx 151 | ; H89idm8vmrI3PPwfPgRHj+ork/hJpbtmz5q0+YCJPU0v7w8b8PmIhomGJN3Cftj6xC5AvPJcJVxpZl 152 | ; crv+wcXj27/e8lLrb2pLO0wdQVe+XT+2/Vj1+HZ8P97DpCRuaothOZ9pSdzaQVQqRGmi/XExkaiSiP 153 | ; ui/bn49vGb9fMmtONk8OUMcYITCu/z5oLx7acONzgDXqaO9keX8qar0/7EnZAuaX8uH9p256v6uQ/S 154 | ; 4hSRDpM9zS417vq+uYnmKjvdBrRNVKFN+6O7G1JBp21ofyioMG1Kl7YwESa1S+c/uLRzeQvCmXxwDF 155 | ; EQ8v1ib+29tYtmGYdcFLQ/OndOatH+mBq4KUxE/NukaH8o4Co2Mw6oIjXOLcorZ4iTd5eWdahUx3gD 156 | ; p2hA60YVkbQ/NnfaxTmbDuWN7m4R9ZvSQvujooOu6BiioBqGpD5KD08U7Y/KjEPYpB3lO6hU2pKm/U 157 | ; EJFSXVfDJ2U0FSv2NmuKOkjvbHBbhNlCjotK9bqpKm/ZGVxHFqfLz0yXAMUbAhoK/CNhEf5fqozSpH 158 | ; 2sCM1Of1aUzSQPvDh1BJ0/40LatsyyeDdnWa7H91alvStD/MLh6j/XFN1pAJ8eCkaX+gA/x+Rvvjm/ 159 | ; Lm5YkVHdc2TnnineMRR8mWhudNm5g87Q+Ol27an0dpf3w6KXING+jHv7w6NDg8tX/w/rwXUm3wJoKQ 160 | ; S6fSZSOppv1heYgvmAiTJGl/KCpUny/5cbCh9NlgVcmzwetjnwt+NuP54NsVw1Nr8Gl1krAGNCvtJ0 161 | ; L7EzVpZ9rQ0ZGkaX+AV7KtUB3POsOKkU8Hc4oGBOUlI4PK0uHBpjH9gr2T+uYcJ60GbyI4YedPKXey 162 | ; WRrT/lDDRMJe6Emk/UE508ZYEFLh1KgZ2TOYXvzCI88enSkOVpYPzJ0mb88alFqDNxF04betXdlCSf 163 | ; uj0oBWpv2xTVx1cVQuHNUErkIFE0GYQNHXwMlRPbJX7tSQfdfcssJg65g+wf4p/YOrNcNSa/S6AogN 164 | ; wI9UtD8qdq1F+2OaF9jCVShof2zgKjYwEQjmMSgNBUjZuLh87KiioLbi+WDX+D5dKoFHdYuC+ki1lZ 165 | ; BXtD8mA1mUt1TpQqddQkUQl6MCFvcOOE12TR7YcXjqgC5zmgDwqBtVmPb5tODuOo20NNH+UOYQqo5K 166 | ; ARX5YumLsZ+BA0ZhmfCO55uP3tw9Y2g7TpOuksC/vW15/tL+uIarxFXaXNP+RE1OwlgBwLNZfJaIby 167 | ; nvlUu240q3cERUxkRH5nWABH7J6ILOBD7fy8H4zXGbVTftT8pofygScVa+XVDUJ5jzz2QbpdtfVcef 168 | ; JnBMUQfiKTujdKjWM9MsLMQMiypSTfvjE67CGjpJ0v6g0oa42GaxcWr8NLu7wzleKh7c+XyWbGPnV0 169 | ; m2v1wzrh1juFE5n+4z0yrYjFh4ydP+7N+//89UlU9S2h+8VFK0P6DcSYL2B6eG7Tw4C6miyrdItmHQ 170 | ; Ksk2egaonMW9OysH53MCzxcq2FV6qaH94SftVq9e/fckaX+wi/um/aGc2bjwSmEwoSzaqCeOGpHrli 171 | ; PZvrSwMPaZcNy4YST+mR/NH5I6B4gTjAbk1oJb+27an4wcruLjdihX5duwZFsU3WRb95nNMwfmXQLP 172 | ; mFPyjvbHxUQiJEnaHwz0uJ70UxlD3bqquuWNGS+2q2KvVJ6Zr3iuDzfPeyAL7W3Bp12e9ofqqIXY4q 173 | ; h0BKFDWLec18GGdWvusWRbBXvFT+zJJB/xXCzMkolNVNHlaX9EmIiJo1KUb2FsZ2cP1vobdMv5ZFtW 174 | ; utRNtnUS+HzBc8X9HtOoItZBTO60CzNS0wTeBe2PjoPBmGwHmvZP7pcr326uMDM4JNs7N62NhFegdL 175 | ; u+or8y9koVz6XzzCQl7mRkohtVeKX9UYWJMKGGq+g4KkUi/mm20sTKtxNKhloBCFG6Pbh+SewGxZJt 176 | ; leEpOD7Y3KOeBx011C5sYSXmtCbwcb9D/E2qUUWitD+yZ7u+pSqu0gZlUw808c9fMHqIck9DFBfJtu 177 | ; yZfGhdu2j2gzQn8CphoygqUUXqaH98wVVkjkqRiJ+bU5Abg60sLow1YhMAoWqyXVM+2LgcHJbzpRnP 178 | ; hXUzsYNu2p8YR2Vh5KHd9Xcoy7cuehomu6Zuso3TJC7nm1X2YurwXKYOwkSZ9scnowjzYJ72x6apYy 179 | ; qnal9udbVwDEAYJQAQvj55cIeJwbkYnsJQ0oYVS++rPDMteK6NlcOt7QC2DxtktD/YsHtgt0gL7Y9P 180 | ; mAiEAkf1mQLkQ9bTYJLL+RoP3do7f0yricGJ5WCZ6OC5XDwz7Q7CaH/AjfUY7Y9PmAhEhfbH5ffb4q 181 | ; iuLR8WHMkaxbpMT2ODC8v5bAwubngKoovnwnVr1M90ISrTlVG2KKYXbONOhPYnCq7i+nYoyoEmlG9f 182 | ; ygwxMrioYoQNgFAn98Eorgs8VxIJ/MzSH5ttlPlM+0MJE4FQDzSx8q2ucXy1qrTjk2Nv3Y46qW0NLi 183 | ; r3Yc55+sDu27oDWSolZvZMX+VgbFjoM+nYAjntj+2chwlchep2KAhF+XZree/HBppsjOPwgrIOlwDC 184 | ; sNxHhKvoJttx+RRzbh2MmK2cnFUQlGXUQyydPl/e0f7oJvBUfFQQGOieTavuRemAN7h3FbBXrgGELP 185 | ; eRjSgw0cFz6STwPgaydkwpULIFkwa0MppXFybCnIsKrmKC57Ilhg4TQD4uvL68JU4HLg3OBM/1Rd2U 186 | ; tvfPvv1dVI8rH/FcKJbMGRXvIKYN6Lym/ZHF7z6uLkOvII4Kc+Hsma0Hqyc/SIvBwRGph6fi8FzMMI 187 | ; 9urL7ngmD7+EvPB0tmTYvcrBKj/Qk7qpOm/aEghlYVnCbvvFV/J8xR+UWhBhBCdJ4pii88l1ghcjGQ 188 | ; 9frs0a0qtD+mV1rkNe2PGFNTEEMfmtpf2+Aub198f9Wyh6MAMi4mKgChjRHz4iL3Yc/kw2ExtGYYMQ 189 | ; pk8Pk5BUFlSWFoVJFq2h+fcBUo4tzxw7ds7w6HAawp6RlsKutlZHD4/tP7d/531OyMicGphEQ6oERR 190 | ; XOQ+yNGiDJOioojcY31Z785nppr2B+35a9eu/RIv5fv6MltiaH6gCeVb2/7Dz/dtvBuXc+kanIoRb1 191 | ; n2yn02o+4Sz7U7m/t8rNC8/LDm8XvQKSuKjdMGBDOFcjuiGuCoUkX7w4ihQfsTFlq4EigbXWkbJQOW 192 | ; zjriY0sexfLYMBCqJPC6AEKZEfM539H9e2+bDmSpkmEvKS8Idox7LhveDFZ6JnNuynl+IBaWFD/3SO 193 | ; +Dz3dgi6mh/eHhIK5hIp3KIOCjwhEN58BAk6zJZINYRQJ/fueaeyrlYFMAoSzns8FzqeQ+VWUvBvXj 194 | ; +gS/UAxDKYsmWLeNZc92bmiyBrTYKX8iaH8ocFSisl0jVr/csfj++tXROuCHp1TxXHhmVLnbBkCokv 195 | ; tUTR7b3jh7mFXeZyIHpvTrDK34U0MW2qea9ocKJgKhjF/DDM4lYhUJ/NH6NfeidKCb++CZzbs2RYay 196 | ; tvmUbAiJVYgOrFnQ8tkif+hd5B0IrXQb0N5pf3SOKwo8lw8+qrhdEzrYv6/hNmt4mRgcysFxJ6pu7q 197 | ; MykGUDIBRDOp7qFf/92Cw/w1Insg3BV4t6BSuWLjJqQDul/bGtJ5vS/uDUADjOxwLwBieeJuKddjYG 198 | ; d23jlLa99dE7vwsAoS5GjN848Pcyqtf6qe5HbuEcq0b376jfVGfdgCal/RF3C5MX4kVnIIuSGFpXYH 199 | ; Cs4SW7pcoGsZoGPJdqPoU8LQoZgXVyuRaAktSNL2h/+3gTWQNa587JSNofF0NTcQNZPnBUKgKDgxHH 200 | ; LYoJYhW9l21TCtvjYmJ877vNTZ3Xp6kMT1HjuRD3R+mAsmjymI4qegfrss5x5vjRmy4a0Nq0P5lMPD 201 | ; E0hYTBRPC/U+KokEi/l+1znJw5MDj18sDg9MuDcsbwyYIhwVc1Q5WfE9bwsjE4NnD1SlG/QBYTizmf 202 | ; CYAwrgoFwTO3j+kdHKzsH1xZXBh8s/yH5366cEiugbpq/I/bZcaDUJQaLc2cA6XcTZXD23ByUBEXho 203 | ; kS7Q9CqCRof9j9IpCmZZVttopFbF2fXfAVRU8HK7OyuviZYE3mmWDdqJ65ngckp/isoNkFY4NhxD2X 204 | ; AkAou0FKLIlHUb26wnNtqOgbbB79UC/12V17VfHTwbIRTwVLi54NyjNyYmgXp0cO8pPpGbyxfE7Lie 205 | ; P2NxOrSmppf16aMrEdvQIbpZ7LdnXrSnsGNdlFXTSiZzCneEAwveSFYGJmWFCRXWDImExRMD4zIpg6 206 | ; amgwt7QgWFHWP2cUkIOV/YKfL3YHIOQ79mE3SGHXAvURbsnC9WlROZ8pgDB2HcZkOprrV//vwVVz/r 207 | ; 57xdyWmeUjOkojpvSocw8e8nOkfu0dl41mmUhpfz755JNvL168eN0XTISJbW8DCSRi5OUjnwpmZzvi 208 | ; 4zXnkmFs1ZNHtb/1yuhWhBqUVwgwACEcBYs+q3ig9PPYJXEJzPWNE/6B7/jkjdp7cSVxSjwXC3kB9G 209 | ; OUN1H8ZLb4N1EuvvJCbgNZO66g/WzTgf/AfZOu7paJEjgEcIW4UrCT9oeVLsU6sWvaH4qLaBAmVWdr 210 | ; 45XFQ4zegS9d1s2d3EoNIIQRryztG0wqeVFqmNB52AWgqngu3QnAuOui8b/xpX0+onBRQGE52U8XT7 211 | ; 9/uGFPLh+luh1KVYxof1x68LxpE9ttkzvWVY2KkfmFDdstxGIEFYBQdVFwasARZM9HOfjdNzfejdus 212 | ; WO6jOmOB94aD83AV0Q7wnSx53fnqrFZqx2DUSTg1DtavvxMWudjCRFQkii8hEdofUFrazm5Aueiqls 213 | ; XEyHyTEQ4Jw6iZVt4RV4xg7OsqZVXV/oO4KJcPbbujqocvdi9rYQNZMjEdnsI7nFhU1oH8CogFJnAI 214 | ; VPAonUI8NTZUjmjDqRGViLsiE0wl7Q92TFvnyCk42zyK4kGKi5FhcDWL5kt/D06FM68M1XonFTwXKy 215 | ; m+ffTArTTguXwLX8nbU7vknmoDmholTk77Y3u9MzsqAbOgUPTWiYMjv08lfJNhr2x6MVF4Ln52Bndt 216 | ; 2OC54kqfaWRfZ5U8hFTobZiE77ZkgiZ8Cc5pf1g9+dLO5S1Uyn59yYxWmaMij9B5Fg/2owJF8s+UwV 217 | ; V4UKIungt5C65ko8RzuRLGYYxTY1e2t9F05JDVCWB656R32p84D+bhKrvrVrRQKv1S467vZXguhBkq 218 | ; 0268IFehxn3hmcvnR8/O2A5kqeC5dq5e0pIU+7qYiFM2oHXIBPlEXLc665z2B41AakTuhdWVbVGOum 219 | ; Vsv+BMCq4whhGjpxG3y9kwEH5VP/fBlrrHDY/fMTetqmnxzb7OEvFt8yY8QCLuogoVF/5Hsas4cxAm 220 | ; PEwkivbHxSwHcoyXJ4ZX2hjAb+fkQR1J31nBBNWzuATehhCODU/xOhBLl3wC/67mddQ6wk4NOMe+jS 221 | ; vvusRRMQlDiTP7TD3tjwswGwRlSDyfHbXovp48efIPbLeYPKasI01XGOuyGpoQwiGBP9Sw5y9RO+ay 222 | ; 8cOcbRpIxEGdtLFyeNuxIwdu+WS4STXtT65CFUL743pWgI2EYscEjgnKwZHKH7U2BscLkk2EJ+iNXJ 223 | ; hbkHO6y4sKtZDBENeEcKgUgicsLIF3NZnJ46iattXeoe5ZqApOy6tXr/4qdbQ/eBmR9gf/bnu9mYog 224 | ; HwEpM3YL2WUopgb3ZXbh38r+zdaK3pHo4G0VvYJ9k/vmigM8ZFwmrhndxQTe5Tw/uwseifjPGnbd3r 225 | ; Jly199wkQ6N2Nu7evq6v7Gwn/bU8wZ7Q84a10sSJjgQhpmcDKCbdHgok4T7IhvTOiTQwe/OvxHucty 226 | ; qooHBjP+iRCelBWgghs21949/NpP7m6dM7a1bnTvHDK4fkyvXCPzqsLJ4prRHeVgjBG4CnPDcFTsvX 227 | ; 3ARJgthvElUFzbYeQgKnMjUAhl30NVmMFFVdqYwb05sW8ORcrv+Dgx9mVDBaCDl4x8NphSPCQUni6r 228 | ; qU8eNSxYUDo42FzxXLBzHEq3BbHv7OKWWtfCEvHVZX07ZDgq0VZchFxRszNUjuqM9sdHeBVncGEE28 229 | ; zgVpcPCF4rfzgkBKh73aiH8yTLs/JS8SAlHcgUjtNqsSajuwqrIcOIURA/mwpOX5waCKnicFTiZkU1 230 | ; 48FHMCpVMp1rO4wcRHaMycTVKKaq8ADCKOg02/FrRvULlpT0DaYVF4SeGOJzVEuX1BOAMMYT+9+8zW 231 | ; bUfd0B+Nt/nhpvTujbiaPSrRBRocRN+RJSRfuDcqZPh5AJfy2ybMZBdVEAtMTkI6pESHjR/ERVKK4i 232 | ; pcu+HobnEq9PmzppQrtPUKKIozLtiNuQCVLdOyOLKrQdxIb2x3V5V0d4gxNj4riTEP8OzBPQv7Lnq8 233 | ; LddQGEDM8VRfXK8Fyu8hJqHJW46aqixE1xVFHPU6209ZB5mc0LpclBmLAhoSnj1CYnURX7uG5mqyo8 234 | ; 30WyfWVFaQdm1GU7Jr7PVa7nEkfFjFQFJa6S89k6atSzndD++KAKtRGERzB+jLoCK1U/b0I7nAe7Ng 235 | ; zONH/SIW9TTeDR03h99uiOsE3IVW+DlW83zyxrRSJuOwsUJTIyQZOcz0T4yUnRURFaO6H9SbuDuBZV 236 | ; MuzNY/vlGpFfK/Q32DOpGe55wanB7oJHIu4DRwURUeIi1avr7xfzm8dof/hbeSiOsSfdQSBK16eNKg 237 | ; 7Wjnk+ODptgPIzXZ0aLBHfOnNUK3BUvhlFYKR79uz5CyBLDDKUBO3PuXPnfhdL+2P7RRsq0zXqmaSo 238 | ; sK+/uWBc68cLhgSU36sqPI6qYV3131Rof1wIi2DA0cZof3xf5yel/cFuQdnQ+cmkbgfhJYp9HTo/29 239 | ; x081DVcO+X0fA4qsZdW7/nKW98wER4w+TzXqrboVQl7JYqp7Q/VeVd30FUGE94QWjEO4lYusS1Zj5n 240 | ; V0QclVjN0y2JUxhmErQ/UXCVSNofm9uhkICmjVGDSlhIsi7TUxtAyOAkYaVLlIB9dMZ5HFXcQJPLOy 241 | ; dVG9AuaX9UbqlyRvvz8fxkYmqXwkIScOxOzxRqAwibq8dGli4/W+p2U2F3waviqJhQkgmaNKC7JO1P 242 | ; w/SCwOVi+xY+JJlYUfbYQFYcgBBd6X3LZ96PSj5d4df4RNwER8WM1JZM0LYBrQsTiXI0p7Q/KswQ1e 243 | ; UFSvX9fBD0BmpG9gyqp1a0RQ1kAeL+TtWgx4amcNcGBqu2r5jfIlsUV/g1nhi6+fC+W7Y9LtOoQgcJ 244 | ; HiU6t0OJf5cq2p9Vry6+31XCrBzV6YQRud8li4nZPPmW8l65wSmcKlsreuUmEHHXxsKiPvL72LPNPx 245 | ; f9DYajChtoshGdqIIaRyU6qkoCbwNXMaL9CbsdipfOeHF+aRv1oicprKcRFRNPyBYocP/IzoWTH2yv 246 | ; KmtdM7W4bUoM+zx1Z1yFGJpC4qIKlzgqFUdNlPaHP2pZTP1Y6XL0i7krz9Jo7KbCgxLDqDB1Z2eonU 247 | ; PEUblutoVFFb5wVBAxr+BpfxjVa2ppfwCl2DVpQJcr+SKZZrMgfEn81KlT/646O4Mr3SjDKkYMXVva 248 | ; p8MnjooZKYsqTpw48UeqipeJo/K0PxQVL6e0P7kXz54iulSg+eQoOAFwpcI777zze1APhelAdAxqiD 249 | ; rDUdVNLPzHe++c+j0MxSdMhDlJY2PjrRs3blyh7lmoSqppf0C1ImvobBnXL/h6WdeoaMkEsHTQgGK4 250 | ; CjD63UumtqExiJOGweipvxOlY1a+ZQNNCG1NJydtNkm29jztjw+YCBMZ7Y8tVN/IQfgjlS/byZJXnC 251 | ; LgjerqTuJTogaafMBEwgyTT9SfWNoflbJdWENnUfmQHF9UPhlhWkXEUYUZoCx5dWGYspPCB+1PXM5n 252 | ; W0lzRvsT1tBZN+b5LpuP+BATYmgXd07qEEO7pv1Rgat4pf3RjSt5L180e2Yr8pFuJ9EXHkelSwxNde 253 | ; ekyK6iS/tjAxPhndOk8Rh3bYexg5jQ/oQpljVsfvrahru7pwzq6HYSNRFxVDa7sA34NIpdRccOTGAi 254 | ; TFJL+0NBI897MO7v2D+nuK3bSaJFJIamorzRBZ9S4ajCHFUlL0gd7Q/FbiGTzmefOf1dw+yStlMzB3 255 | ; bZ2REbiSKGphCVOyfDJu2oJC20P3HPfoz2h3q3CBMWE5890XTzjRlD29N0G2vSokoMTbpZhYBPVYih 256 | ; qR01KdofWUm8k/YHL+C6JCfz4ObGg7f2zh/TmvRtrGkQdmpsqBzR5gNHxTYrPnmF06hM2rl0VJ2BJm 257 | ; od4FTD+0hpf1yShIUJFqCpqelPHx/f96cdKbpX0KdgXmRDac+sczwVHN629o4u1SuFwCivXLnya2YH 258 | ; SdP+uIxgZBJK+4PFoKb90V0YKOPy5cvXkLzvzeYl9RW9g7NVg4Nvunhu8kOF6qlg+7xxDz55//x13z 259 | ; ARZpxYdxhmUrQ/LN95Imh/VBdFhKvAKJpPHL95eEP1ve0TB3YAndoVHQUX9RzLhlM4NdaOG9x+sH7d 260 | ; HaaDKCpMl4bJF2TSRPvjWwfs+yJpfygaOlESRXPKHPX9987fOLlj/Z3d0wrb4Shho6z56BgN2RMDE4 261 | ; brxg5qb9xWe+f9985JdeAKJhJmmKIj+MhJ4xrQvmh/ZH0+Ke2PbUNHdVGiTinW0Dl/5tR3R2rntbw2 262 | ; +tncKCtAj1cWF+aVY2BojDkGZG/twntwjLiFdwETYeurMmmXBtofG5hInA5UbqlSpv2x9WDdSTv2N8 263 | ; xRD73x+veLxw7r4C/LPDNrUPDF0nSWh+EUIKZem3kmqB7+o9ydhwvHF+XodnQqRFQwESYmk3aUUYVJ 264 | ; A9oV7U9YeVvLQdgPso2JbeEq/FE7r2pW67RRQ4MVZf1zjsKcpWn6gOAD4VJOn4KEGwwiDZxTQGaP7B 265 | ; tMLh4S2JYubWAiomGaGJktmSBztG7aH8mi2O5+YQ2dMZmioKp0SM5Z2MkCeXNCn5zDnJszOLeTf6Vw 266 | ; JbOOfLN8WHBpYWEOQIjvYiVaSM2IHwVzRzyXcwrcd2iyKFH6NEleKZERplGFC9ofnUpb6mh/bEnCwi 267 | ; TqqAXtDruUc03Zc7nba3nZVtErd/c5rhqAnH55YPDu7MG5q5pBT/TZosIc3+6FuQWPSPPMgTlnA4Pi 268 | ; 9rG9g/VZZ1hd8kywsujphw4x8pncPeq4GXdCydBHqH2oMURMVGAi1IYpGpsJ7Q9lDpFq2p+4mNg1XC 269 | ; UuecV3Nmf//eyJozdrZoxrm591murSAUFt6XO53T5M1o0KFzhDziFKng0Wl/QNFpT0D2aXDAqqp1W0 270 | ; nTl+9KYsXPGlA5nzmeR8uqJD++OiAR03OUkBV3FK+0OtEF5kjqq6W4zJjMzJpMywYMaowhzXbmXmxd 271 | ; x/52VcZoT0HcJi4rjSJbUOwmYcKEYUVEVG++MLrhJH+2PbcCSj/QGjhcvdQibMGNB9PXDgwJ99LIpM 272 | ; Bzq0P5TSWRLP6qChoeG/VCftqCRNtD9nz579N0b7Q3F6O6f98SE4NUC5w67u8k15Ax2o0v641AHW4N 273 | ; KlS98kRfsDbjRG++M6gggTOElqaX82bdp01zcaWCxdgnJGt9JGsSj87/YJE2HChxiu6T5l69BN+8OJ 274 | ; rHTpE88lg6voVNooFyVMB64dVZbzddP+0PTvIN5of1wbJi8qBNuUixL2GdeOGndSuN6sVOAqaaD90S 275 | ; mJh4kx7U/czmja0NExzLhnMnoaSkfVmbSjhomIeu2m/VHTq81m5ZX2x+aoNS1digZl6qg2pUtbmAhv 276 | ; mCbGTgETyRgYJi9UUYVpA9oZ7Y/ObiETm5h40iT9O+0yEiM1PWopRkBtZhxkVK8mOkgL7Y9JVEHRfN 277 | ; WNKhKh/VFNXqnhKroxMSWOKmyRk6K8SQvtT5I6SD3tT5wHUxsmE9WY2CUqQHXGwSVcRWWzcglXUZ2c 278 | ; dAlXUaL9EcMmn7Q/YaeTS8MUjVR21ProJUTFxDY5n64OZMmrD7hKVFShW4wwlaioopP2B//gs3YuLg 279 | ; KLiX03ucTJSX5X8wVXEZNXnzgqZqR8KM1T3vim/WE6SB3tD5gkWHveN0wCC3Ds2LGbDKqSBOUNdMBT 280 | ; 3iShA1AfMR246N3o6CDsRHEtjPYHUJEkaX8uXLjw20dof+ApaaD9gZP6PD2YsGIEFgUG4vME5ReG6Y 281 | ; CiJK4rYbQ/PmEibB2YDhjtj2+ONkb7AwoqtlGljvbHtKFjY5h8vuMLJsIvCq8D36FuWM6XJO0Pfr9N 282 | ; SdxWB7G0P65hIrJFYWLa0LE1TP7ffDhqFFzF12aVdtofW5iIiljT/lAz7KlWyVzARMIMU/bbXMBEwn 283 | ; QQFUa4pP1RqRC5dFTVBrSrloNqA1qJ9ofCg01Kl9SOalIhCpuctNGBLlzFFe2Pzu+hjCpMGtDUUYVO 284 | ; AzoWakJBhUlJ+2PiqKq7RdTf28bESdP+2MJVxJK4iQ6Spv2BOKH9MZ1xMNktZGIaE+uQhKkYqa6jUt 285 | ; P+mGxWUVSvJjowiSqeKNofFQ92ETvqxsQu4Co6VJimixInOptV2mh/qPJZnc3qiaT9iTpqXRkmr4M4 286 | ; R9WZnTHVQdTJ6Moww3Qg+406szMmkje0PywmjipdujDSsBkHn3AVGe2PL7iKbHIyDbQ/vuAqss2KCq 287 | ; 7ihPbHJ1QjTbQ/hw4d+k+fOComrGTK0/74hKvwUUUaaH+YDigiGBLaH8ATuml/kqX9gTEy2p8kdCDS 288 | ; /vjGckEQckEHqaf98cWJJJYuk6b9wfe7jPtlwoeVaaL9SUoHeUH74/qIlyXi3bQ/fmAiYTrgHdKXo8 289 | ; oa0KYlcVG6aX8sF0WmfFcwkTAdhO2SPmh/4ipEOiVxE9Gl/THZrIxpf+J2BRd4Lt3SJSVMhH+majEi 290 | ; LbQ/1JuVDlzFRVSh24D2QvtjWrqkgk6ngfbHtHSZNO0PBUxE1IHuqUQVVZg2oE2jCiXaH7yQTemSiv 291 | ; bHZgeioP2xyWtsYmIquIrNnZMUcBXbqCK1tD8UMaxJTEwNV0kD7Y/ujAM1KsDEUdNE+0ORy+hEFaml 292 | ; /XEFV0kb7Y+qDqirYWmg/VF5tku4ihLtjxgT+6L9kVFhUu8WMomKibtpf/zT/mCzEml/fNxSlbe0Pz 293 | ; 4G9sWYGDrwwcUkLgLvqBRUr7pGylfaeMobXwwzaaP9gR100v7AKMEkAUYPynKojpGC9gd0Lz4XhZdu 294 | ; 2p+HOgBkiNH++EJDMEkD7Q82ZeDZHqP9AX4H4jqsCZMw2h9fMBG2MGmi/XEd2slEpP3xrQMWViZN+w 295 | ; MdPEL7w+JPnzCRTCa8QuSb9kcsXfqEifCLwuvAJ0yEN0z+1EiS9gffRwUTMdFBXtD+uISJyAyT/7ck 296 | ; aH94I3ANE5EZpkwHLml/ohrQPjarVNP+RP1oPoHvarQ/qqVLFzARcX2TpP1RaUC72qxUG9BeaX904S 297 | ; pUMJGw32NC+2MbE5vAVaJK4jaGqeN0rmh/VJ2OOvzX6fMp0f7Yln9tS5eUtD8mCuYdNQ20PyY6oKT9 298 | ; MY0qbBvQFGSCun0+bdofsaHj0jB5MU1eKW+pMqHCTAPtDyUqoCvQ/ujYz/8Drubc3t1SGwcAAAAASU 299 | ; VORK5CYII= 300 | ; thumbnail end 301 | ; 302 | ; 303 | 304 | ; external perimeters extrusion width = 0.45mm 305 | ; perimeters extrusion width = 0.45mm 306 | ; infill extrusion width = 0.45mm 307 | ; solid infill extrusion width = 0.45mm 308 | ; top infill extrusion width = 0.40mm 309 | ; first layer extrusion width = 0.42mm 310 | 311 | M117 Shui test file 312 | M300 313 | M300 314 | M300 315 | M117 Finished 316 | 317 | --------------------------------------------------------------------------------