2021-03-15 15:31:52 +00:00
#!/usr/bin/env python3
"""
2021-03-20 01:59:53 +00:00
* nxdt_host . py
2021-03-15 15:31:52 +00:00
*
2024-04-12 10:47:36 +01:00
* Copyright ( c ) 2020 - 2024 , DarkMatterCore < pabloacurielz @gmail.com > .
2021-03-15 15:31:52 +00:00
*
* This file is part of nxdumptool ( https : / / github . com / DarkMatterCore / nxdumptool ) .
*
2022-03-31 03:37:23 +01:00
* nxdumptool is free software : you can redistribute it and / or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation , either version 3 of the License , or
* ( at your option ) any later version .
2021-03-15 15:31:52 +00:00
*
2022-03-31 03:37:23 +01:00
* nxdumptool is distributed in the hope that it will be useful ,
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
* GNU General Public License for more details .
2021-03-15 15:31:52 +00:00
*
* You should have received a copy of the GNU General Public License
2022-03-31 03:37:23 +01:00
* along with this program . If not , see < https : / / www . gnu . org / licenses / > .
2021-03-15 15:31:52 +00:00
"""
2021-03-20 01:59:53 +00:00
# This script depends on PyUSB and tqdm.
# Optionally, comtypes may also be installed under Windows to provide taskbar progress functionality.
# Use `pip -r requirements.txt` under Linux or MacOS to install these dependencies.
# Windows users may just double-click `windows_install_deps.py` to achieve the same result.
2021-03-15 15:31:52 +00:00
# libusb needs to be installed as well. PyUSB uses it as its USB backend. Otherwise, a NoBackend exception will be raised while calling PyUSB functions.
# Under Windows, the recommended way to do this is by installing the libusb driver with Zadig (https://zadig.akeo.ie). This is a common step in Switch modding guides.
# Under MacOS, use `brew install libusb` to install libusb via Homebrew.
2021-03-16 05:08:38 +00:00
# Under Linux, you should be good to go from the start. If not, just use the package manager from your distro to install libusb.
2021-03-15 15:31:52 +00:00
2023-10-19 22:56:38 +01:00
from __future__ import annotations
2023-06-03 16:27:24 +01:00
2021-06-04 01:19:19 +01:00
import sys
2021-03-15 15:31:52 +00:00
import os
import platform
import threading
import traceback
import logging
import queue
import shutil
import time
import struct
import usb . core
import usb . util
2021-03-16 03:28:06 +00:00
import warnings
2023-05-24 20:05:34 +01:00
import base64
2021-03-15 15:31:52 +00:00
import tkinter as tk
2021-03-16 03:28:06 +00:00
import tkinter . ttk as ttk
2021-03-15 15:31:52 +00:00
from tkinter import filedialog , messagebox , font , scrolledtext
2021-03-16 03:28:06 +00:00
from tqdm import tqdm
2021-03-15 15:31:52 +00:00
2021-06-04 01:19:19 +01:00
from argparse import ArgumentParser
2023-05-24 20:05:34 +01:00
from io import BufferedWriter
2023-11-26 21:10:42 +00:00
from typing import Generator , Any , Callable
2023-05-24 20:05:34 +01:00
2021-03-15 15:31:52 +00:00
# Scaling factors.
WINDOWS_SCALING_FACTOR = 96.0
SCALE = 1.0
# Window size.
WINDOW_WIDTH = 500
WINDOW_HEIGHT = 470
# Application version.
2024-08-26 10:17:23 +01:00
APP_VERSION = ' 0.5 '
2021-03-15 15:31:52 +00:00
# Copyright year.
2024-04-12 10:47:36 +01:00
COPYRIGHT_YEAR = ' 2020-2024 '
2021-03-15 15:31:52 +00:00
# USB VID/PID pair.
USB_DEV_VID = 0x057E
USB_DEV_PID = 0x3000
# USB manufacturer and product strings.
USB_DEV_MANUFACTURER = ' DarkMatterCore '
USB_DEV_PRODUCT = ' nxdumptool '
# USB timeout (milliseconds).
2023-11-11 20:39:41 +00:00
USB_TRANSFER_TIMEOUT = 10000
2021-03-15 15:31:52 +00:00
# USB transfer block size.
USB_TRANSFER_BLOCK_SIZE = 0x800000
2021-03-17 17:25:30 +00:00
# USB transfer threshold. Used to determine whether a progress bar should be displayed or not.
2021-03-20 01:59:53 +00:00
USB_TRANSFER_THRESHOLD = ( USB_TRANSFER_BLOCK_SIZE * 4 )
2021-03-17 17:25:30 +00:00
2021-03-15 15:31:52 +00:00
# USB command header/status magic word.
USB_MAGIC_WORD = b ' NXDT '
# Supported USB ABI version.
2023-05-24 20:05:34 +01:00
USB_ABI_VERSION_MAJOR = 1
2023-11-11 20:50:02 +00:00
USB_ABI_VERSION_MINOR = 2
2021-03-15 15:31:52 +00:00
# USB command header size.
USB_CMD_HEADER_SIZE = 0x10
# USB command IDs.
2023-11-11 20:39:41 +00:00
USB_CMD_START_SESSION = 0
USB_CMD_SEND_FILE_PROPERTIES = 1
USB_CMD_CANCEL_FILE_TRANSFER = 2
USB_CMD_SEND_NSP_HEADER = 3
USB_CMD_END_SESSION = 4
USB_CMD_START_EXTRACTED_FS_DUMP = 5
USB_CMD_END_EXTRACTED_FS_DUMP = 6
2021-03-15 15:31:52 +00:00
# USB command block sizes.
2023-11-11 20:39:41 +00:00
USB_CMD_BLOCK_SIZE_START_SESSION = 0x10
USB_CMD_BLOCK_SIZE_SEND_FILE_PROPERTIES = 0x320
USB_CMD_BLOCK_SIZE_START_EXTRACTED_FS_DUMP = 0x310
2021-03-15 15:31:52 +00:00
# Max filename length (file properties).
USB_FILE_PROPERTIES_MAX_NAME_LENGTH = 0x300
# USB status codes.
USB_STATUS_SUCCESS = 0
USB_STATUS_INVALID_MAGIC_WORD = 4
USB_STATUS_UNSUPPORTED_CMD = 5
USB_STATUS_UNSUPPORTED_ABI_VERSION = 6
USB_STATUS_MALFORMED_CMD = 7
USB_STATUS_HOST_IO_ERROR = 8
2021-06-04 01:19:19 +01:00
# Script title.
2023-05-24 20:05:34 +01:00
SCRIPT_TITLE = f ' { USB_DEV_PRODUCT } host script v { APP_VERSION } '
2021-06-04 01:19:19 +01:00
# Copyright text.
2023-05-24 20:05:34 +01:00
COPYRIGHT_TEXT = f ' Copyright (c) { COPYRIGHT_YEAR } , { USB_DEV_MANUFACTURER } '
2021-06-04 01:19:19 +01:00
2021-03-20 01:59:53 +00:00
# Messages displayed as labels.
2023-05-24 20:05:34 +01:00
SERVER_START_MSG = f ' Please connect a Nintendo Switch console running { USB_DEV_PRODUCT } . '
SERVER_STOP_MSG = f ' Exit { USB_DEV_PRODUCT } on your console or disconnect it at any time to stop the server. '
2021-03-20 01:59:53 +00:00
2021-03-15 15:31:52 +00:00
# Default directory paths.
2023-11-26 21:10:42 +00:00
INITIAL_DIR = os . path . dirname ( os . path . abspath ( os . path . expanduser ( os . path . expandvars ( sys . argv [ 0 ] ) ) ) )
DEFAULT_DIR = os . path . join ( INITIAL_DIR , USB_DEV_PRODUCT )
2021-03-15 15:31:52 +00:00
2021-03-20 01:59:53 +00:00
# Application icon (PNG).
# Embedded to load it as the icon for all windows using PhotoImage (which doesn't support ICO files) + wm_iconphoto.
# iconbitmap supports external ICO files, but it's not capable of setting the icon to all child windows.
2021-03-15 15:31:52 +00:00
APP_ICON = b ' iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAAEnQAABJ0Ad5mH3gAABfVelRYdFJhdyBw ' + \
b ' cm9maWxlIHR5cGUgZXhpZgAAeNrNmll23TiWRf8xihoC+mY4uGjWqhnU8GsfPjV2SJGpcuRHWbYoU3wkcJvTAHTnf/77uv/iT83Ru1xar6NWz5888oiTH7p//RnP9+Dz ' + \
b ' 8/39T3j7/tt59/Fj5Jg4ptcv2nz71OR8+fzA+zOC/X7e9bffxP52o/cnx9ch6cn6ef06SM7H1/mQ32403i6oo7dfh2pvH1jHfw7l7V/9fZLP/92vJ3IjSrvwoBTjSSF5 ' + \
b ' vnc9PTGyNNLkWPkeUos6E54zTd9dqm/TUJS/C2r6m/Pe/xq04H6Skb9LSMrvD9ONfg1w/TiG786H8pfz6eMx8bcRpfnx5Pjr+Z1C+zKdt3/37n7vUbCZxcyVMNe3Sb1P ' + \
b ' 5fmJC0lgTs/HKl+Nf4Wf2/M1+Op++uWog02NGF8rjBBJ1Q057DDDDec5rrAYYo4nkqsY44rpOdfJ3YjryWh2Kaccbmwkd6dOohcpT5yNH2MJz3PH87gVOg/egStj4GaB ' + \
b ' Tzxf7v2Hf/r17Y3uVbmH4PtHrBhXfKotKIpJ37mKhIT7XkflCfD711//qJ8SGSxPmDsTnN5et7ASPmsruSfRiQsLx1cDhrbfbkCIeHZhMLRDDr6GVEINvsXYQiCOnfxM ' + \
b ' Rh6Ty9FIQSglbkYZc0qV5PSoZ/OZFp5rY4mv0+AViSh0XCM1dB+5yrnk6mjUTg3NkkoupdTSSi+jzJpqrqXW2qqAb7bUciutttZ6G2321HMvvfbWex99uhFHAhjLqKON ' + \
b ' PsaYk4dO7jz59OyTExYtWbZi1Zp1GzYX5bPyKquutvoaa7odd9pgx6677b7HniccSunkU0497fQzzryU2k0333LrbbffcedH1p6sut9y9jVz/zpr4S1rJMw9Octc9J41 ' + \
b ' Trf2fosgOCnKGRmLOZDxpgxQ0FE58z3kHJU5p5z5EemKEhllUXJ2UMbIYD4hlhs+cveZuS95c/T9n+Yt/po5p9T9JzLnlLpvMvc1b99kbc+HpoQPAJvaUEH1ifa7+czY ' + \
b ' +ev9x3H1G/ztva67kjHDsvayWhbBy9nftc4+3aW76o+u1IW2opW567ypnJ3LYZxU2GqdYIfbitqUKJc4WzzdGhNdZvGMGrkfM4/ReNiu3MbWbpRQs23ExkDRQrqyK5fq ' + \
b ' GNw9ELW9b0nH1M8rEm+qIfdnijODD4GrKLVfjzBMBct3dORT/xlhmPkz7jq91rPSbDdNgs9wZ1p2bmagJ9+U1tqcPmuOthJRFwrO6HicTfD+7cFSEz873vn2kbUqc3J0 ' + \
b ' aND0x+2t5VOpkFGXjRNj7+GOsW17Er7/zYXu80pCr2Buisf27LYpo3RKvx7wOZTFndTCnIX62oECm2O0WnakKm04qhfRxtOmctLGvJWP0AIUXYWbmLZx19Op7Kk49G12 ' + \
b ' 5rrz9IxK6bOvUgaU/RTeAJHzKwu/HX0Y0LjRPrvr5/yZiM/sDCt707SdqjJqZd1BRkhLK/tanOX6e9q9Lz2n51GVq/ovpf8cnf+bX3wc05k/aRP3Xv3/tE2c+uSzTbhB ' + \
b ' nmPZ1EwL/cKPBWBpKVvuE4hNP+i1pii3YK3tMm7ZvucK44UOGL3lgpr5tkABf+RbqiOSAY8G6ZR/b5kgw5Sg86YHdqpGBVCR5bZZbKBdxjjkz+zWku2Eu65DbL26kBG0 ' + \
b ' vr4tgp8c3dsPdf5a1UlVvegBlfVUWQN5/r0k16znMrApfRS6CDtOoPYNFpjvdwUAlm5UJAB+ln6On1P2n3HY0/G0lOmIDYqdtpg7AbB57h7wU6eGVZL9bQ4th5O+BSb3 ' + \
b ' 1xM/Od5nyLYGdFC+r+yfTuTrPNyfTeTr0f2TCcENVsdEd08U2+phk7qn8vK8hj4Y9LiBGWf1Lp7dZ52DuB4yRGE37ibwLPmBfku75+lmjiWcCs/2fxmU/VtQrP41uRDk ' + \
b ' /ZOQfDm6/1toApNl1IiMvgwj4dVZuwtqQR1wdJ0oz7LvyAfd0wDGdK7EDdzFdOvtGIuoqgAAim9gRWX46epfAOgdYukEwlnH1e/iruf4QVMD0fthcaReSUAwxWIQyJi9 ' + \
b ' nGCWIBSQcKV8Q53DVfgDzoGnz9oM/sbUBelBLJ9glDNigFPnFnLkrolw435yOwUABUl72ee4dOoGtxaCaujBt4uRMvS3dkX77aSB2NiIwFysIysqQqs1/Pqeq0UY+5wc ' + \
b ' HMKjA8fINcEnVTGZy9DFgHUHGAnOOAHXjnXAZa3rsWQL/XhOAwtxZ7Bm2q6dm2Lp6NJSEl1T850r7F0e+bLvRNj1xfnhqzIycJFhj14zBDwQZgD6yODRoS7psMmnSGXr ' + \
b ' zONSuIVRoDlDH6HXgOC5dq34sZ+qGz7XFjP3mQe9AgOag/lHhkAzGmk1oRF6B0Q/+0bIiRhtOzXcWVAM6CViGFaJi3FssDQfwDJRDO6SROgv1dY3pUJPVLQW/BHFxDFT ' + \
b ' FCSU6OeabCSKg8YhZYHMCimWZMk800liU5GIll0mVUOg8uGz+41vZkr/joufo/t3F/xyDIQEVbN7RIGAErDZhsnmJjj4tVAHoAKy3DjlCONaje6n1FCgy/qizcmHwbKl ' + \
b ' 9u017IFkbTO2FTdKQy4HT9u2SWkiF6Xi4evmUST+pEtFp9uVz+bJDtU+JPn8IbeT6hkDYbWl/zdCi4SQBGuw56EzubB6qUqgJlNp5IEoIijwAbgd4nxoLPR/v7PVSRmj ' + \
b ' NAa07IA80rV6aj7QbSiDsrzwC90ASF7o3zMVWrv2ZAwz0QRtoblb4IqIfWmI3OloR7R5u63FgK67OW1GoaSOG7Kwr/Br8xLoBCoTNkqBwttIQL+mH3CIvxfwj0sq8xaP ' + \
b ' FqLb8GfoklFvjNJKt1KafHpg1blvkqq+fL5fBHp+KolQxIZd9yrIPPuhIciunPlmVlDXuWAyGZFgz9YJ+k7x1DvAfpQYTMCH8RuNoJA18QGKxi4Sad5JyxSjaSe4nipR ' + \
b ' 1lO5e0HagJ6cw/oh0XuPz5MbFyLiXNmEftWG76BZCCMG8cI9eINwOwYjQEu4wYk63ATDgE84iA4qjJiaKwfrkoaLFBtJQNEpw2Oh5TKmwx8qEWRBbKsIJqXSTEonebSv ' + \
b ' R5XndajiMlKmEK27cG88F30UPEzBM+pTKRUYD4DexZM3Ve65FX0W8buUptWMNNpd+OaZKNDnPBVtOHDw5+4GHOOCLOxAY92GgaVUDl71FLKHuGAuC7wMydoITBnVT+3h ' + \
b ' bTHH6PguRgU2SHCaK+HfIoS+xWsgSIJUguQr1WUZsKVVAbZkYHfBOFPCqboAThIgsMSgGTh+xAabkLXUAfDrFwOqeKCQt6cIKI57wKyCOgX1ib4NLQ65FZj+TuS9gNZG ' + \
b ' Z6GSMT8L1ouHpEfoKI+OaKcMQDk6JE+D+0B244Ncl3NojsqYTdOcUESlxZCk0B74OpIWBJDdJAuvDiWiST2EunuooD7lAAJM0o+5Ks4qiFLMQ1no45xwrzZHb6hbUBx5 ' + \
b ' E0icdYGY9ItWO0BemNQglVXglKSlCPTRpOguHExiNfjUrnAdnXQaDo5fbH4/PbSCfircm3DVI9eMmmYqF4dAhTvanYDSFIQZuEM5FNrbqH+jFcLssNKsfU2g1XhWGL7h ' + \
b ' LyYTpf6CFTzhRp041NauBDjqRKHsaQmKESINEXdxMPHU7emeQpThJzKcITYwHALsosC0CD4dfQ77+cJjgbPr8fmRokXZ0M0t91tTMcRCYjB0SMbwRICQukI0IHciaNdW ' + \
b ' XQ50YIq5IjbgYUxqU/P4HQEoPpPxtlCvp50MRUdpGp4KvTglGrw/EfNp8IJ7QALmpUymske1IbDAFEudWq/6fWPMOmatUaEcICN8GdkxmpZJ0kfmEAGkkKobsDTgnivm ' + \
b ' F672xwt36RoyV7GXi67ONF1H+EG0j/ajRMA4arVEB1YcCjVolbkibUGfTW2UYyPuyCAL0gHaS5uETV1Jrsamnce0chfOv6ZUusMyQHONhxJciLK1oSUlGgFWobmQOSiI ' + \
b ' Z0YL2YaKo53KAS/3Isf0/MjEgRuRSsAR78iATWtSjAFlbWQ7JxwJvNVoVJqdXjmRHxNaYx30REoEGl23kIH0mjGFxs8vddgRLDuiqORcDr0P2oFFpNBqLEV+k6Az84Iw ' + \
b ' qrTODVRUVR35i+gKQQKNauPBsGOIeF2ESUAeqiAgzRthKi8hO1A1XJgDgIXvwGpjI9zwozDG9YJoxi3FhQwe4BETooZMGoNjB6wj14FWjIl0zE1+LkXYpt+uS2DDzyCp ' + \
b ' BHQ2CLNTgGvtsJKnTWk2OKzS5bEJ4yRYOy5FEtuXkqX7wSNkMLqRukJwYr6to1smZZWIIVlLgBXOgZyhFCit8vi5LEwE5PHrdBz22LwbmAuLmCF0Bd2HGhLNwa1oOzPA ' + \
b ' AL3AkAvsOFpEADFuFLryfFLsRJc6rdRRm91nHFWbyFkEn0Q/WSSihZ5c9HeEZS5qOlBHNPRRELRUbwiPvQ/FXevMDkoryGPGVVKBlMh3BZsq6ECv7v4wJEAIHcJMRV1h ' + \
b ' whdkXX4rUwxDHA52JiCBQkO0T1NRe3wdIsc3gKNoHVInrjZdKD2IePbmVz/Q0FAyyR/SzdkFWp51WuopwYAbpa2soM8zKHuwQQP48khC5BHyo05MD7/zOJUXROI+DE+L ' + \
b ' GUC1bGq70fkBLV5F9VRXw+JBRwjPY8eeqgxDNIxCpxi9bjFrEQVPBz1pYbnhNbBwkEWmRWbW8jNXaChwFyo9KbIkNns0eQN8vNrMoGywm7Q7f7XsTFLhnXQAXijswVKo ' + \
b ' OqCdRa8dK07DD7nIXbX4hrCF5R+Jh5hRQzioGXEskYZI9MpLRCBSjjycWsInimvTQ4hYr4HfxI3sdmgowhE6HUK8vYvLNr8Js6JYFWnpraA9MooUNaYOSowaxTk2+HIj ' + \
b ' ovQsu3sa3H8GrThxAu6Su5Oz1kw9jIdYKXL2iDcgI+AX0ddJ0g0fhI1AHgHWonuwQ1IoyNkSEEdjw5fVZvWIniNYikPVcX30yOZF3zbgH6NxshbwRfUTgsNCkBtbFf8E ' + \
b ' qjvicvSfBXUzFqwqg45APc2O9MWN02SdSyYNh97a5Rw8AyYDp4l41lJFKLc7KH1p0bOZkjC1NCyPTiljSxDQuC64E8YlMnnD9xC874unEGIEAlJMmxUbMUr90EY0iRoV ' + \
b ' 54lJApPQeBIxSAhYFgFidN+QfB705aEe0C+oKDwWmqqm4Dq2OXuECxI/ILIT2hiA0pC1SntUj/Ghydx3RnEZyj/SnfdZrQhoaa+iBEYO6BCyVP14VNmcWr0xUrsgfGAv ' + \
b ' qWG3dpI6bg4MwBcDLhQHMKkpJ7yG5PE5D+5RHUSSaMp3UPekMi9jeFvrHFE6Q1wfI7XlzRaqGRmHPo1cPxxsg+KaA/latI1UaT0ZWbhsUvxRq6EobCRI6xQ4JRAI05Uf ' + \
b ' fcwF2g3fMreEFj0LUiBYcEWlJG1Oz0oP+TXSM9khT07ORsDDwADXJOaWdib2QzyYaSe3HWgROgxOpOfAJxIOAUMh+Hy+QT7IEmRNP9qho0iWCF4r11zP7eGr6yDRABcB ' + \
b ' wQXkRyox1wGPAuxaadH+QEJndoppyrwXif5UZPu0IE0zVEvMHYTsFDx5B5NuQKtHesF8x2t5OC5qlw25VWlumahrxG/no94zqU1MlKdXugPpE/EN+OawZEqwIQEhPUSA ' + \
b ' EVKh1XAQoA7YjLzARWGAeLQWl5ggN7zy2s5j0agphBRlov13v6SdUZGhaLOJUWpdGXu+pX33kjcNezEJiAUBSZ1V+sEF2mYtVAXSAJdVkp8IgSM0RqZy540SJInae57i ' + \
b ' TPJpSHGpDa5BtWGCKUwXEbMVPh0JhYzhN2RtG+gSxAP+GhMCfUl20iJ005otjPOst8EOSDp69aFUNKRJEjf4BYGCZpYW0j4pKgJMXEGbbSAaMS4H2GuPP1jcfenFgaa9 ' + \
b ' V/omOd+11IdsGP0pPuxDHjiybmpzWqXJ6SNdNbtIjSWU8VLbo2hIqKmX/GyqI5TFJYpwlRZOcceQe4Nj5ChE4ScxQ7UOpUuNxFioDRgZNVPhcvoWXHJKTEe70+jP6iIg ' + \
b ' 21Bqviw18KKUpJuRFquQVKFu5M6UbaJ4gwAfwwKAO6l0uAtOBLqIUp8FxdSaXq/IWh0iGRQAiIizFqbyQKkbSATugLHuBfnA7EY6MHQHll9UbkQxou40ZqgVG6y4FRSx ' + \
b ' jqhVuvcBOTwpNHG1OoSEDjStTptvewF1oDCJb5Q6g1gx4rYORifQu2BWk/1RiC1ECIDMhag1fWzAqtFhFxAhSytCdEUFGmA0LEWHgZ8lOgPBtQ8dCQlKh8pZXpvOOd6L ' + \
b ' Yyf/0Em77lJS2E65gOcXSS6Ypq0zYXO0pa1Wlrs2wAfLNPCbOOkQNwSBK26+T/pZLzI1D4ZqbVrbWaiUIKB5xC+dtkA4mK61OPDRMslgEPmTlOtawQcYlEMnytQDKqSE ' + \
b ' SIGFE22svSQi+9appmXluiTlPK4Wa6i3UagwkDktWRaPF0GBEUF6flBy2Lbt4VRtr09k/fTIjw2yUlkIaGCSvuszArCI9Ug4CBKYKxGxKInHw/Mx7dHDUWAGwD2f0KoM ' + \
b ' B8UfB+bHgLSkRTrUXAEHAWDRGU0XyJp02GsVFz0SI1aip0PrE7dx2ypPTU2PipiNppt30T5oF0pXy0j1VCJSgRGAyHetNLe5qBaMDJHHb1+i3xHS0lX75Rtk3g0EEypj ' + \
b ' wLTnTr9rZXS5Sq7nAFOBfWTWOgQFQfKsNaSL8sS7R62SNWhMIhqqQ260pReiaFPGxCgBNngBcQwc0Wp6wUpmXHI9CW0QjIZbozLG0JYxrA6nlO4PGIor5bkUpMJRHMar ' + \
b ' UM21avkLHwPiZE5UZDPWf+NHSQK1GJ4WQFNsyXLUyx4YDENi6lWQVR2+qWgLH76EQIEphBMoCjpA82BBGD3Za+EMFXfQrkQE7/KAICC8NkhFC1GQfDBhxOhJ7bTAfk2M ' + \
b ' AkWngULHJwKdNUDxwM9M4CT2JUlaAzviFWw/Bt9BcohF2PhEkBJ4hNVkZJFqGJeolRjQtjBOocbAZuFui/5uVBzapqjZ4nEVB4jjLDjDo+0H7CmOg6AHDWde6cW26MCI ' + \
b ' 5IroeOygBBcy/NrQsqw2Vey45dPUFLRGz3MDtmODKuA/kgEhDv3R21o26ZjvHEEuvfSFuiooOMRGQJchwt0Vl/WREQC54fz6JX5p0ktau9lEJWp3bdF6cDTF25t28SqQ ' + \
b ' gZ/V4maU99muDS1mapntWZmOaraVaxQ1HoBlPIU3uCGwcwXzExm9MVEY6axlHJXgCDTtQrDlV2sapD+W5vpsnT0LgBnLBlPlCDY8eyjkFYkADBdGBsC3560lB4cXxOXB ' + \
b ' ljKiBLH70YuW6+D1bWDmlJdGGhJrICRBHAD1xJ17YQO+RcuI5pAKBhQSjCz+lNOAmpIQZAXCUGuGBBlyZZ79WQwJqDJkEc2ADpVz9Xh31zFVoCoEQGFi9lAJIF/TksIi ' + \
b ' LsEip8Jr9xbN1bTvFTTMRg4x1nD2I1od8HXAeAl9ujLPbTXr7UcsbIN2NTKtPaeNvQcJDQGpFT1llda+IT3r+H08vDbpouGxCbTxiYhO2FLqFv+2ABB0GXqj4GwpKTyj ' + \
b ' gKGhtWjHpQjI8yQXtapWtcun3QK9B1YfXkdLzqyl0qM3hgJPuZTT1ittvWoPsNJRG0UlRqHaXYg0d3kWGTFa3tLTFtB51hweFYrZR76T3KLqQmoExEgfWptCLAwyyQBp ' + \
b ' kUApgQ9ksrSAsX+92tJ/vuOLTMb3FwTA1hp1pN6itj95KualQwlYZIlIvaVIg1AKciOGw2CCeHomerchLvVejMP7+gymoGGR7ttXvVBT8vzLxlhTkl+O2SPK1332uOfz ' + \
b ' +pAq/zgwRFt56LIyAGRtc+OR37b+uzRPfb1XEwCzIHwZH5Nar93pLSPmfnmlghsdHkOD4LRFjA1jnNPP3vpwP309hGzwlNcLQyj58XphSDtmTasT2y2sIi0BpC9L4hWI ' + \
b ' twAYn68MQfr7eUMFMbwrk6MrvuwqjtcLKHBsY1J//BoLR/dnE/k6D/cnE/mc0Od83D+b0OfR/dlEvs7D/Xwi303ocz7uzyf0+zzcn03k6zzcjzeyx7+uuP9vLeK1Tf9P ' + \
2021-03-20 02:36:04 +00:00
b ' JvQfaZEp644NXCjW1tzRe3Ifr2xGre9rCH7cAuDCAD98ddN98w4nYpgC+19UQVjzl9u7BwAAAAZiS0dEAP8A/wD/oL2nkwAAAAd0SU1FB+MGEQkHB+UVPj0AAAVvSURB ' + \
b ' VGhD1ZltTFtVHMafe1sGbDBSXgMffM22jNdFNpnoPqgbAiEzcbiYLDH7YEbUbLwtDkURF1xgkQE6JJmwOEJiposm24RsiuAcjC2uQAuomQq4ibqU18FgpbSec+69cAct ' + \
b ' a3tvIfyS5px7Tlue5/Tp/5xeOL1eb4MjeB7akCAYkndhymgAz60SJ5yHt1nhRdpPIzdgWKOFRhhWDV5sF0LEawJ10CcmYdL4s9viteBwLCYKJq2X6uIp9g3QlQ8ORMe2 ' + \
b ' ZEz3/woN5ydOOI+w8kR8bCTGOQ6rbI4/aCUsNCCK1z+1Heb+34h4f3HCedjKE9GlRPwEMeEp8ZT7DUixefoFTN/83e2Vp7Epi/a8eMqcgRUUGzmCgRUWGzn8SoyNHJ7W ' + \
b ' +ZUWGzm8IWXXiouNHK4aPjZ3NimJsuiNYm954N3dYSnLLZ7CneTWuvS5S7EpIV9YSse1Ztju3cMUOSr4anhYJyagDQpCU1MzMjPz0HDuS4T6+kCr0yEufht7DaXz+k+w ' + \
b ' DA/j9tQUUtJ2i6OuY/8o4QB5tZG4c+Ei/tM9hFH/cPy7Ogy3Qx7DAB+AqLNnUV9/BtBqYHopHf94B+MaMUu5erWJXdNxaJSdkJw2IImXqo09gv/qRrh5EL7V5bBU1CBk ' + \
b ' ZgYppEiEX77E5ierq1FQ8BYmPz/Fruk4nVeCUwak2FDxjqpNhHUUm1PTEffEMxh97lk2drfxB9bu2/cGAjtbMHkgHzsTtmDqzUPQGVrYuFIeaEAem8VKZWvrFbEHNDY2 ' + \
b ' sdY6OsratjY9ekmUNLtTYXo4mrV9JDp0XCmLGqDind2kTCaT2APGxsaEjlWoVpTx8XFwQcGsz4WEsGs1cGhAio0am9T69Y8iNjgElqpa6NovwVJ5il3TcaXYNUDFazhe ' + \
b ' lbPNpk1RqCt6H4OPxyHU1IvnX89G6GAfu64rKmTzSlhgQIpNWcxGVc42tbU1GIpMgPfRAjS0tJLo3EXD5Rb4FL9Lxp9k8+4yQx73GXAnNgEv7mQVaD50LCg3Z7bvt38/ ' + \
b ' 8vOL2DVt/bIy7b7OWcxEp46U6dmdmIlnK6/OqZJtYoRUUlrVhor3I/pyjD3CJyDFRs0jcVhYGL5rFjYwNaHi18CG7K4eTJOWdyc27tDR0YqSUiFC7iKJzzX0wEJ0Wkmh ' + \
b ' 4dkXdol+SQ0Nj4g915mNDRFPV56Kp/BVkRsUxUZDdtTOziuoOlGBvRl7kZOXhabmenFWwGBoIx+18AdpX3o4izw2Fpl4Cj9EBChZ+XPnv0LlZyexdctmTJPdVbdmNQLW ' + \
b ' rsXRjyvBiaKHR0bwYWk5+m7eYi19DJGjtDPYi40cLiYmwW31hYV5+KW3D/m5WYiN3SqOAuvWPYLCDwpw3WDAseJyNka/A1+c+RolRR+xa2eQYpNNqs38lZdYOOICERHh ' + \
b ' rK2vb2CtxI0bfYiOUvZrbbHYyFFkQIIjf0xNHhQbOYoMDAwMsDYlJZm1EjRChq5u8co1ZmMzr9o4QpGBwsISvPrKyzh+ogbt7S3Ysycdhw+/g9On63D+4vfis+awEWHe ' + \
b ' 3vS/BfaRx2bGCfEURQYoA7f+JhUoHsXln8Ci1aL7jz9RUnEcb2cfYIIlzGYz+slzm3+8wErokSPviTMCrsRGjqIqJJGWtgMHD2bD31+4OWY0diEmJpr148U7EVlZGdiR ' + \
b ' lIRvvm3AHVJuMzNeQ2LidjZHoTd3Fqs2jlDFgBocotXGhZWXUBwhJdDYeJEClkdX3g3xlGUzIFWb+WcbV1kWA/Jq42rm57PkBtytNo5YUgNqxUbOkhlQMzZylsSA2rGR ' + \
b ' 43EDnoiNHI8a8FRs5HjMgCdjI8cj7+rp2MhR/Z3p7b5gyzRyjN0ei80cwP+bQrjkWSh1LgAAAABJRU5ErkJggg== '
2021-03-15 15:31:52 +00:00
2021-03-20 01:59:53 +00:00
# Taskbar Type Library (TLB). Used under Windows 7 or greater.
2023-11-26 21:10:42 +00:00
TASKBAR_LIB_PATH = os . path . join ( INITIAL_DIR , ' TaskbarLib.tlb ' )
2021-03-17 21:19:44 +00:00
2021-03-20 01:59:53 +00:00
TASKBAR_LIB = b ' TVNGVAIAAQAAAAAACQQAAAAAAABBAAAAAQAAAAAAAAAOAAAA/////wAAAAAAAAAATgAAADMDAAAAAAAA/////xgAAAAgAAAAgAAAAP////8AAAAAAAAAAGQAAADIAAAA ' + \
2021-03-17 21:19:44 +00:00
b ' LAEAAJABAAD0AQAAWAIAALwCAAAgAwAAhAMAAOgDAABMBAAAsAQAABQFAAB8AQAAeAUAAP////8PAAAA/////wAAAAD/////DwAAAP////8AAAAA/////w8AAABMCAAA ' + \
b ' EAAAAP////8PAAAA9AYAAIAAAAD/////DwAAAHQHAADYAAAA/////w8AAABcCAAAAAIAAP////8PAAAAXAoAAEQHAAD/////DwAAAP////8AAAAA/////w8AAACgEQAA ' + \
b ' iAAAAP////8PAAAAKBIAACAAAAD/////DwAAAEgSAABUAAAA/////w8AAACcEgAAJAAAAP////8PAAAA/////wAAAAD/////DwAAAP////8AAAAA/////w8AAAAjIgAA ' + \
b ' wBIAAAAAAAAAAAAAAwAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAGAAAAAAAAAD/////AAAAAAAAAAD/////AQAgAAQAAABkAAAAAQADAAAAAAD///// ' + \
b ' IyIBAKgTAAAAAAAAAAAAAAMAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAB4AAAAAAAAADAAAAAAAAAA/////wAAAAAAAAAA/////wAADAAEAAAA/////wAAAAAAAAAA ' + \
b ' /////yYhAgAwFAAAAAAAAAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/////wAAAABEAAAAAAAAAP////8AAAAAAAAAAP////8AAAAAEAAAAAgAAAAAAAAA ' + \
b ' AAAAAP////8hIQMAMBQAAAAAAAAAAAAAAwAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAP////8AAAAAVAAAAAAAAAD/////AAAAAAAAAAD/////AAAAABAAAAD///// ' + \
b ' AAAAAAAAAAD/////IyIEALQUAAAAAAAAAAAAAAMAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAACQAAAAAAAAAMwBAAAAAAAA/////wAAAAAAAAAA/////wEAJAAEAAAA ' + \
b ' AAAAAAIACAAAAAAA/////yEhBQD0FAAAAAAAAAAAAAADAAAAAAAAAAAABgAAAAAAAAAAAAAAAAAAAAAA/////wAAAAAgAgAAAAAAAP////8AAAAAAAAAAP////8AAAAA ' + \
b ' HAIAAP////8AAAAAAAAAAP////8jIgYAuBUAAAAAAAAAAAAAAwAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAKgAAAAAAAAAsAIAAAAAAAD/////AAAAAAAAAAD///// ' + \
b ' AQBUAAQAAACQAQAAAwAJAAAAAAD/////ICEHALwYAAAAAAAAAAAAAAMAAAAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAD/////AAAAABQDAAAAAAAA/////wAAAAAAAAAA ' + \
b ' /////wAAAAAEAAAA/////wAAAAAAAAAA/////yYhCABgGQAAAAAAAAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/////wAAAADcAwAAAAAAAP////8AAAAA ' + \
b ' AAAAAP////8AAAAABAAAAFAAAAAIAAAAAAAAAP////8hIQkAYBkAAAAAAAAAAAAAAwAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAP////8AAAAA8AMAAAAAAAD///// ' + \
b ' AAAAAAAAAAD/////AAAAAAgAAAD/////AAAAAAAAAAD/////JyEKAKQZAAAAAAAAAAAAAAMAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAD/////AAAAACAEAAAAAAAA ' + \
b ' /////wAAAAAAAAAA/////wAAAAAEAAAA/////wAAAAAAAAAA/////yAhCwDoGQAAAAAAAAAAAAADAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAA/////wAAAAAMBQAA ' + \
b ' AAAAAP////8AAAAAAAAAAP////8AAAAABAAAAP////8AAAAAAAAAAP////8hIQwALBoAAAAAAAAAAAAAAwAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAP////8AAAAA ' + \
b ' oAYAAAAAAAD/////AAAAAAAAAAD/////AAAAABAAAAD/////AAAAAAAAAAD/////JSINALAaAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAAA ' + \
b ' AgAAACwHAAAAAAAA/////wAAAAAAAAAA/////wEAAAAEAAAAAAAAAAAAAAAAAAAA/////3gAAACQAAAA/////////////////////8AAAAD///////////////////// ' + \
b ' ////////////////qAAAAP////////////////////8AAAAA/////////////////////0gAAAAYAAAA//////////////////////////8wAAAAQvY7aMrpJEG+Q2cG ' + \
b ' Wy+mU/7/////////Zbp33nxR0RGi2gAA+Hc86f//////////Y7p33nxR0RGi2gAA+Hc86f//////////ZLp33nxR0RGi2gAA+Hc86f//////////QvP9Vm390BGVigBg ' + \
b ' l8mgkAAAAAD/////AAAAAAAAAADAAAAAAAAARmQAAABgAAAAlUktYDqxm0Kmbhk15E9DF5ABAAD/////kfsa6iiehkuQ6Z6fil7vr1gCAAD/////RPP9Vm390BGVigBg ' + \
b ' l8mgkBQFAAD/////WAIAAAEAAAD/////////////////////6AIAAP/////////////////////////////////////////////////////UBgAA4AUAAP////////// ' + \
b ' //////////8sBwAAIAQAAP/////YBAAARAQAAP/////oAQAAoAYAAPgAAAC8BAAA/////3wEAAAMBAAA////////////////qAQAAP///////////////1AGAACsAwAA ' + \
b ' KAMAAP/////wBAAA3AMAAP//////////nAIAAP///////////////5gFAAC4BQAAxAYAAP///////////////0QFAAD///////////////8YAAAA3AAAAP////+MAAAA ' + \
b ' SAEAAAADAAAYBwAA/////8wBAACwAgAADAUAAOgGAAD//////////4wGAAD/////CAIAAP////9cAQAA//////////+YAQAAAAAAALABAAD///////////////////// ' + \
b ' /////1gEAAD8BgAA////////////////////////////////////////////////tAYAAP////////////////////+ABQAA/////2wEAACUAwAA/////zQBAAAgAgAA ' + \
b ' //////////////////////////8IAQAA/////1ACAAA8AgAAaAUAAIgCAAD/////FAMAAMgDAABEAAAA//////////8KANOVVGFza2JhckxpYldXAAAAAP////8MOD29 ' + \
b ' SVRhc2tiYXJMaXN0ZAAAAP////8IOFuISVVua25vd27IAAAA/////wQ4f/VHVUlELAEAAP////8rOALfX19NSURMX19fTUlETF9pdGZfVGFza2JhckxpYl8wMDA3XzAw ' + \
b ' MDFfMDAwMVcsAQAA/////wUQQDFEYXRhMVdXVywBAAD/////BRBBMURhdGEyV1dXLAEAAP////8FEEIxRGF0YTNXV1csAQAA/////wUQQzFEYXRhNFdXV2QAAAD///// ' + \
b ' DgC+jlF1ZXJ5SW50ZXJmYWNlV1f//////////wQAmzNyaWlk//////////8JAPb2cHB2T2JqZWN0V1dXZAAAAP////8GALW4QWRkUmVmV1dkAAAA/////wcAb2FSZWxl ' + \
b ' YXNlVwAAAACgAAAABgDBGUhySW5pdFdXAAAAAP////8GAM/CQWRkVGFiV1f//////////wQAL8Fod25kAAAAAP////8JAEcJRGVsZXRlVGFiV1dXAAAAAP////8LANKD ' + \
b ' QWN0aXZhdGVUYWJXAAAAAP////8OANQ5U2V0QWN0aXZhdGVBbHRXV5ABAAD/////DThF70lUYXNrYmFyTGlzdDJXV1eQAQAA/////xQAmQVNYXJrRnVsbHNjcmVlbldp ' + \
b ' bmRvd///////////CwBN92ZGdWxsc2NyZWVuV/QBAAD/////Djhw9nRhZ1RIVU1CQlVUVE9OV1f0AQAA/////wYQedlkd01hc2tXV/QBAAD/////AxB4nmlJZFf0AQAA ' + \
b ' /////wcQqxNpQml0bWFwV/QBAAD/////BRCeTWhJY29uV1dX9AEAAP////8FEPsMc3pUaXBXV1f0AQAAcAEAAAcQL4Bkd0ZsYWdzV1gCAAD/////DThG70lUYXNrYmFy ' + \
b ' TGlzdDNXV1dYAgAA/////xAAk5hTZXRQcm9ncmVzc1ZhbHVl/////1QAAAAMAAJpdWxsQ29tcGxldGVk/////7QAAAAIAMIbdWxsVG90YWy8AgAA/////wc4feJUQlBG ' + \
b ' TEFHV7wCAAD/////DzCpRlRCUEZfTk9QUk9HUkVTU1e8AgAA/////xIwqDxUQlBGX0lOREVURVJNSU5BVEVXV7wCAADIAAAACzDDqVRCUEZfTk9STUFMV7wCAAD///// ' + \
b ' CjCnMVRCUEZfRVJST1JXV7wCAAD/////CzDtTVRCUEZfUEFVU0VEV1gCAABEAwAAEACoU1NldFByb2dyZXNzU3RhdGX//////////wgAfld0YnBGbGFncyADAAD///// ' + \
b ' CDgsBXdpcmVIV05EhAMAAGQDAAAQOMPUX1JlbW90YWJsZUhhbmRsZYQDAAD/////CBAfY2ZDb250ZXh06AMAAP////8VOJRaX19NSURMX0lXaW5UeXBlc18wMDA5V1dX ' + \
b ' 6AMAAP////8HEJdKaElucHJvY1foAwAA/////wcQWpVoUmVtb3RlV4QDAAD/////ARBsEHVXV1dYAgAAdAIAAAsAHgFSZWdpc3RlclRhYlf//////////wcAHDZod25k ' + \
b ' VGFiV///////////BwAjEWh3bmRNRElXWAIAAJQEAAANABymVW5yZWdpc3RlclRhYldXV1gCAAD/////CwAWi1NldFRhYk9yZGVyV/////9gAgAAEACrAmh3bmRJbnNl ' + \
b ' cnRCZWZvcmVMBAAAgAEAAAg4x5VUQkFURkxBR0wEAAD/////FTDLIlRCQVRGX1VTRU1ESVRIVU1CTkFJTFdXV0wEAAD/////FzC5uFRCQVRGX1VTRU1ESUxJVkVQUkVW ' + \
b ' SUVXV1gCAAD/////DAB661NldFRhYkFjdGl2Zf//////////CQBqkXRiYXRGbGFnc1dXV1gCAAD/////EgAzhVRodW1iQmFyQWRkQnV0dG9uc1dX//////////8IALRQ ' + \
b ' Y0J1dHRvbnP/////IAEAAAcAtchwQnV0dG9uV1gCAAD/////FQAObVRodW1iQmFyVXBkYXRlQnV0dG9uc1dXV1gCAAAgBQAAFADLRVRodW1iQmFyU2V0SW1hZ2VMaXN0 ' + \
b ' //////////8EAI17aGltbFgCAAD/////DgBloFNldE92ZXJsYXlJY29uV1f/////fAMAAA4AJ6twc3pEZXNjcmlwdGlvbldXWAIAAP////8TAJr4U2V0VGh1bWJuYWls ' + \
b ' VG9vbHRpcFf/////BAYAAAYAy6Fwc3pUaXBXV7AEAABsBgAABziayXRhZ1JFQ1RXsAQAADQGAAAEEOV7bGVmdLAEAADMBQAAAxA12nRvcFewBAAAJAYAAAUQDRVyaWdo ' + \
b ' dFdXV7AEAAD/////BhBIe2JvdHRvbVdXWAIAADAAAAAQANtfU2V0VGh1bWJuYWlsQ2xpcP/////wAwAABwDDlXByY0NsaXBXFAUAAMwCAAALOBMKVGFza2Jhckxpc3RX ' + \
b ' HAD+fwAAAAAdAP9/LAEAAB0A/3/IAAAAGgD/fxAAAAAaAABAGAAAgBoA/n8gAAAAHAD+fxAAAAAdAAMAvAIAAB0A/3/oAwAAHQD/f4QDAAAaAP9/SAAAAB0A/38gAwAA ' + \
b ' HQADAEwEAAAdAP9/9AEAABoA/39oAAAAHQD/f7AEAAAaAP9/eAAAABEAEYABAAgACAAAAAAAAAASABKAAQAIAAQBAAAAAAAACAA+AAAAQ3JlYXRlZCBieSBNSURMIHZl ' + \
b ' cnNpb24gOC4wMS4wNjIyIGF0IE1vbiBKYW4gMTggMTk6MTQ6MDcgMjAzOAoTAP///39XVxMAbgIBCFdXGAAAAAAAAAD/////MAAAAEQAAAAAAAAASAAAAEwAAAAMAAAA ' + \
b ' qAAAABgAAAAZABmAAAAAAAwANAAJBAAAAAAAACQAAQAZABmAAAAAABAARAAJBAEAAQAAAAMAA4BwAQAAAQAAACQAAgAZABmAAAAAABQARAAJBAIAAQAAAAMAA4BwAQAA ' + \
b ' AQAAACQAAwAZABmAAAAAABgARAAJBAMAAQAAAAMAA4BwAQAAAQAAACQABAAZABmAAAAAABwARAAJBAQAAQAAAAMAA4BwAQAAAQAAAAAAAWABAAFgAgABYAMAAWAEAAFg ' + \
b ' SAEAAFwBAACAAQAAmAEAALABAAAAAAAAGAAAADwAAABgAAAAhAAAAGAAAAAwAAAAGQAZgAAAAAAAAGwACQQAAAIAAAAYAAAA+AAAAAEAAAAoAAAACAEAAAIAAAAYAAEA ' + \
b ' EwATgAAAAAAEADQACQQBAAAAAAAYAAIAEwATgAAAAAAIADQACQQCAAAAAAAAAABgAQAAYAIAAGDcAAAAIAEAADQBAAAAAAAAMAAAAEgAAABQAAAAFAAAABMAE4AAAAAA ' + \
b ' AAAkAAAAAAAUAAEAEgASgAAAAAAAACQABAAAABQAAgASABKAAAAAAAAAJAAGAAAAFAADAAAAAAAAAAAAAAA4AAgAAAAAAABAAQAAQAIAAEADAABAjAAAAKAAAAC0AAAA ' + \
b ' yAAAAAAAAAAUAAAAKAAAADwAAAAwAAAAMAAAABkAGYAAAAAAIABUAAkEAAACAAAAAwADgHABAAABAAAAAwADgAgCAAABAAAAAAACYOgBAAAAAAAAeAAAABQAAAATABOA ' + \
b ' AAAAAAAAJAAAAAAAFAABABcAE4AAAAAAAAAkAAQAAAAUAAIAFwATgAAAAAAAACQACAAAABQAAwANAA2AAAAAAAAAJAAMAAAAFAAEADAAAAAAAAAAAAA4ABAAAAAUAAUA ' + \
b ' EwATgAAAAAAAACQAGAIAAAAAAEABAABAAgAAQAMAAEAEAABABQAAQDwCAABQAgAAYAIAAHQCAACIAgAAnAIAAAAAAAAUAAAAKAAAADwAAABQAAAAZAAAAHACAAA8AAAA ' + \
b ' GQAZgAAAAAAkAGQACQQAAAMAAAADAAOAcAEAAAEAAAAVABWA6AIAAAEAAAAVABWAAAMAAAEAAAAwAAEAGQAZgAAAAAAoAFQACQQBAAIAAAADAAOAcAEAAAEAAAA4AAAA ' + \
b ' yAMAAAEAAAAwAAIAGQAZgAAAAAAsAFQACQQCAAIAAAADAAOAlAQAAAEAAABYAAAAqAQAAAEAAAAkAAMAGQAZgAAAAAAwAEQACQQDAAEAAAADAAOAlAQAAAEAAAAwAAQA ' + \
b ' GQAZgAAAAAA0AFQACQQEAAIAAAADAAOAlAQAAAEAAAADAAOA8AQAAAEAAAA8AAUAGQAZgAAAAAA4AGQACQQFAAMAAAADAAOAlAQAAAEAAAADAAOAqAQAAAEAAABgAAAA ' + \
b ' gAUAAAEAAAA8AAYAGQAZgAAAAAA8AGwACQQGAAMAAAADAAOAcAEAAAEAAAAXABOAuAUAAAEAAABwAAAAzAUAAAEAAAA8AAcAGQAZgAAAAABAAGwACQQHAAMAAAADAAOA ' + \
b ' cAEAAAEAAAAXABOAuAUAAAEAAABwAAAAzAUAAAEAAAAwAAgAGQAZgAAAAABEAFQACQQIAAIAAAADAAOAcAEAAAEAAAANAA2AJAYAAAEAAAA8AAkAGQAZgAAAAABIAGQA ' + \
b ' CQQJAAMAAAADAAOAcAEAAAEAAAANAA2AdAIAAAEAAAAfAP7/UAYAAAEAAAAwAAoAGQAZgAAAAABMAFQACQQKAAIAAAADAAOAcAEAAAEAAAAfAP7/jAYAAAEAAAAwAAsA ' + \
b ' GQAZgAAAAABQAFwACQQLAAIAAAADAAOAcAEAAAEAAACAAAAAGAcAAAEAAAAAAANgAQADYAIAA2ADAANgBAADYAUAA2AGAANgBwADYAgAA2AJAANgCgADYAsAA2DMAgAA ' + \
b ' rAMAAHwEAAC8BAAA2AQAAGgFAACYBQAA4AUAAAQGAAA0BgAAbAYAAPwGAAAAAAAAPAAAAGwAAACcAAAAwAAAAPAAAAAsAQAAaAEAAKQBAADUAQAAEAIAAEACAABkAAAA ' + \
b ' FAAAABYAA4AAAAAAAgA0AAAAAIwUAAEAFgADgAAAAAACADQAAQAAjBQAAgAWAAOAAAAAAAIANAACAACMFAADABYAA4AAAAAAAgA0AAQAAIwUAAQAFgADgAAAAAACADQA ' + \
b ' CAAAjAAAAEABAABAAgAAQAMAAEAEAABAKAMAAEQDAABkAwAAfAMAAJQDAAAAAAAAFAAAACgAAAA8AAAAUAAAACgAAAAUAAAAAwADgAAAAAAAACQAAAAAABQAAQBAAAAA ' + \
b ' AAAAAAAAJAAEAAAAAAAAQAEAAEAMBAAAbAQAAAAAAAAUAAAAKAAAABQAAAADAAOAAAAAAAAAJAAAAAAAFAABAAMAA4AAAAAAAAAkAAAAAAAAAABAAQAAQEQEAABYBAAA ' + \
b ' AAAAABQAAAAoAAAAFAAAABYAA4AAAAAAAgA0AAEAAIwUAAEAFgADgAAAAAACADQAAgAAjAAAAEABAABAIAUAAEQFAAAAAAAAFAAAAFAAAAAUAAAAAwADgAAAAAAAACQA ' + \
b ' AAAAABQAAQADAAOAAAAAAAAAJAAEAAAAFAACAAMAA4AAAAAAAAAkAAgAAAAUAAMAAwADgAAAAAAAACQADAAAAAAAAEABAABAAgAAQAMAAEC0BgAAxAYAANQGAADoBgAA ' + \
b ' AAAAABQAAAAoAAAAPAAAAA== '
2023-05-24 20:05:34 +01:00
# Global variables used throughout the code.
g_cliMode : bool = False
g_outputDir : str = ' '
2023-11-26 21:10:42 +00:00
g_logLevelIntVar : tk . IntVar | None = None
2023-05-24 20:05:34 +01:00
g_osType : str = ' '
g_osVersion : str = ' '
g_isWindows : bool = False
g_isWindowsVista : bool = False
g_isWindows7 : bool = False
2023-11-26 21:10:42 +00:00
g_tkRoot : tk . Tk | None = None
g_tkCanvas : tk . Canvas | None = None
g_tkDirText : tk . Text | None = None
g_tkChooseDirButton : tk . Button | None = None
g_tkServerButton : tk . Button | None = None
g_tkTipMessage : int = 0
g_tkScrolledTextLog : scrolledtext . ScrolledText | None = None
g_tkVerboseCheckbox : tk . Checkbutton | None = None
2023-05-24 20:05:34 +01:00
2023-11-26 21:10:42 +00:00
g_logger : logging . Logger | None = None
2023-05-24 20:05:34 +01:00
2023-11-26 21:10:42 +00:00
g_stopEvent : threading . Event | None = None
2023-05-24 20:05:34 +01:00
g_tlb : Any = None
g_taskbar : Any = None
g_usbEpIn : Any = None
g_usbEpOut : Any = None
g_usbEpMaxPacketSize : int = 0
g_nxdtVersionMajor : int = 0
g_nxdtVersionMinor : int = 0
g_nxdtVersionMicro : int = 0
g_nxdtAbiVersionMajor : int = 0
g_nxdtAbiVersionMinor : int = 0
g_nxdtGitCommit : str = ' '
g_nspTransferMode : bool = False
g_nspSize : int = 0
g_nspHeaderSize : int = 0
g_nspRemainingSize : int = 0
2023-11-26 21:10:42 +00:00
g_nspFile : BufferedWriter | None = None
2023-05-24 20:05:34 +01:00
g_nspFilePath : str = ' '
2021-03-15 15:31:52 +00:00
# Reference: https://beenje.github.io/blog/posts/logging-to-a-tkinter-scrolledtext-widget.
class LogQueueHandler ( logging . Handler ) :
2023-11-26 21:10:42 +00:00
def __init__ ( self , log_queue : queue . Queue ) - > None :
2021-03-15 15:31:52 +00:00
super ( ) . __init__ ( )
self . log_queue = log_queue
2022-07-05 02:04:28 +01:00
2023-05-24 20:05:34 +01:00
def emit ( self , record : logging . LogRecord ) - > None :
2021-06-04 01:19:19 +01:00
if g_cliMode :
msg = self . format ( record )
print ( msg )
else :
self . log_queue . put ( record )
2021-03-15 15:31:52 +00:00
# Reference: https://beenje.github.io/blog/posts/logging-to-a-tkinter-scrolledtext-widget.
class LogConsole :
2023-11-26 21:10:42 +00:00
def __init__ ( self , scrolled_text : scrolledtext . ScrolledText | None = None ) - > None :
assert g_logger is not None
2023-05-24 20:05:34 +01:00
2021-03-15 15:31:52 +00:00
self . scrolled_text = scrolled_text
2021-06-04 01:19:19 +01:00
self . frame = ( self . scrolled_text . winfo_toplevel ( ) if self . scrolled_text else None )
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
# Create a logging handler using a queue.
2023-05-24 20:05:34 +01:00
self . log_queue : queue . Queue = queue . Queue ( )
2021-03-15 15:31:52 +00:00
self . queue_handler = LogQueueHandler ( self . log_queue )
2023-06-03 16:27:24 +01:00
2021-03-15 15:31:52 +00:00
#formatter = logging.Formatter('[%(asctime)s] -> %(message)s')
formatter = logging . Formatter ( ' %(message)s ' )
self . queue_handler . setFormatter ( formatter )
2023-06-03 16:27:24 +01:00
2021-06-04 01:19:19 +01:00
g_logger . addHandler ( self . queue_handler )
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
# Start polling messages from the queue.
2023-05-24 20:05:34 +01:00
if self . frame :
self . frame . after ( 100 , self . poll_log_queue )
2022-07-05 02:04:28 +01:00
2023-05-24 20:05:34 +01:00
def display ( self , record : logging . LogRecord ) - > None :
2021-06-04 01:19:19 +01:00
if self . scrolled_text :
2021-06-06 00:04:42 +01:00
msg = self . queue_handler . format ( record )
2021-06-04 01:19:19 +01:00
self . scrolled_text . configure ( state = ' normal ' )
self . scrolled_text . insert ( tk . END , msg + ' \n ' , record . levelname )
self . scrolled_text . configure ( state = ' disabled ' )
self . scrolled_text . yview ( tk . END )
2022-07-05 02:04:28 +01:00
2023-05-24 20:05:34 +01:00
def poll_log_queue ( self ) - > None :
2021-03-15 15:31:52 +00:00
# Check every 100 ms if there is a new message in the queue to display.
while True :
try :
record = self . log_queue . get ( block = False )
except queue . Empty :
break
else :
self . display ( record )
2022-07-05 02:04:28 +01:00
2023-05-24 20:05:34 +01:00
if self . frame :
self . frame . after ( 100 , self . poll_log_queue )
2021-03-15 15:31:52 +00:00
2021-03-16 03:28:06 +00:00
# Loosely based on tk.py from tqdm.
2021-03-17 21:19:44 +00:00
class ProgressBarWindow :
global g_tlb , g_taskbar
2022-07-05 02:04:28 +01:00
2023-11-26 21:10:42 +00:00
def __init__ ( self , bar_format : str = ' ' , tk_parent : Any = None , window_title : str = ' ' , window_resize : bool = False , window_protocol : Callable | None = None ) - > None :
2023-05-24 20:05:34 +01:00
self . n : int = 0
self . total : int = 0
2024-08-26 10:17:23 +01:00
self . divisor : float = 1.0
2023-05-24 20:05:34 +01:00
self . total_div : float = 0
self . prefix : str = ' '
self . unit : str = ' B '
2021-03-16 03:28:06 +00:00
self . bar_format = bar_format
2023-05-24 20:05:34 +01:00
self . start_time : float = 0
2024-08-26 10:17:23 +01:00
self . prev_iter_time : float = 0
self . prev_n : int = 0
2023-05-24 20:05:34 +01:00
self . hwnd : int = 0
2022-07-05 02:04:28 +01:00
2021-03-16 03:28:06 +00:00
self . tk_parent = tk_parent
2021-06-04 01:19:19 +01:00
self . tk_window = ( tk . Toplevel ( self . tk_parent ) if self . tk_parent else None )
self . withdrawn = False
2023-11-26 21:10:42 +00:00
self . tk_text_var : tk . StringVar | None = None
self . tk_n_var : tk . DoubleVar | None = None
self . tk_pbar : ttk . Progressbar | None = None
2022-07-05 02:04:28 +01:00
2023-11-26 21:10:42 +00:00
self . pbar : tqdm | None = None
2022-07-05 02:04:28 +01:00
2021-06-04 01:19:19 +01:00
if self . tk_window :
self . tk_window . withdraw ( )
self . withdrawn = True
2022-07-05 02:04:28 +01:00
2023-05-24 20:05:34 +01:00
if window_title :
self . tk_window . title ( window_title )
2021-06-04 01:19:19 +01:00
self . tk_window . resizable ( window_resize , window_resize )
2023-05-24 20:05:34 +01:00
if window_protocol :
self . tk_window . protocol ( ' WM_DELETE_WINDOW ' , window_protocol )
2022-07-05 02:04:28 +01:00
2021-06-04 01:19:19 +01:00
pbar_frame = ttk . Frame ( self . tk_window , padding = 5 )
pbar_frame . pack ( )
2022-07-05 02:04:28 +01:00
2021-06-04 01:19:19 +01:00
self . tk_text_var = tk . StringVar ( self . tk_window )
tk_label = ttk . Label ( pbar_frame , textvariable = self . tk_text_var , wraplength = 600 , anchor = ' center ' , justify = ' center ' )
tk_label . pack ( )
2022-07-05 02:04:28 +01:00
2021-06-04 01:19:19 +01:00
self . tk_n_var = tk . DoubleVar ( self . tk_window , value = 0 )
self . tk_pbar = ttk . Progressbar ( pbar_frame , variable = self . tk_n_var , length = 450 )
self . tk_pbar . configure ( maximum = 100 , mode = ' indeterminate ' )
self . tk_pbar . pack ( )
2022-07-05 02:04:28 +01:00
2023-11-26 21:10:42 +00:00
def __del__ ( self ) - > None :
if self . tk_window :
2023-05-24 20:05:34 +01:00
self . tk_parent . after ( 0 , self . tk_window . destroy )
2022-07-05 02:04:28 +01:00
2024-08-26 10:17:23 +01:00
def start ( self , total : int , n : int = 0 , prefix : str = ' ' ) - > None :
if ( total < = 0 ) or ( n < 0 ) :
2023-05-24 20:05:34 +01:00
raise Exception ( ' Invalid arguments! ' )
2022-07-05 02:04:28 +01:00
2021-03-17 21:19:44 +00:00
self . n = n
2021-03-16 15:44:48 +00:00
self . total = total
self . prefix = prefix
2024-08-26 10:17:23 +01:00
# Get progress bar unit and unit divisor. These will be used to display and calculate size values using a specific size unit (B, KiB, MiB, GiB).
unit_and_divisor = utilsGetSizeUnitAndDivisor ( self . total )
self . unit = unit_and_divisor [ 0 ]
self . divisor = float ( unit_and_divisor [ 1 ] )
self . total_div = ( float ( self . total ) / self . divisor )
2021-06-04 01:19:19 +01:00
if self . tk_pbar :
self . tk_pbar . configure ( maximum = self . total_div , mode = ' determinate ' )
self . start_time = time . time ( )
else :
2024-08-26 10:17:23 +01:00
n_div = ( float ( self . n ) / self . divisor )
2021-06-04 01:19:19 +01:00
self . pbar = tqdm ( initial = n_div , total = self . total_div , unit = self . unit , dynamic_ncols = True , desc = self . prefix , bar_format = self . bar_format )
2022-07-05 02:04:28 +01:00
2023-05-24 20:05:34 +01:00
def update ( self , n : int ) - > None :
2021-03-16 15:44:48 +00:00
cur_n = ( self . n + n )
2023-05-24 20:05:34 +01:00
if cur_n > self . total :
return
2022-07-05 02:04:28 +01:00
2024-08-26 10:17:23 +01:00
cur_time = time . time ( )
cur_n_div = ( float ( cur_n ) / self . divisor )
msg = self . _format_speed ( cur_time , cur_n )
if not msg :
self . n = cur_n
return
2021-06-04 01:19:19 +01:00
if self . tk_window :
2023-11-26 21:10:42 +00:00
assert self . tk_text_var is not None
assert self . tk_n_var is not None
2023-05-24 20:05:34 +01:00
2024-08-26 10:17:23 +01:00
msg = tqdm . format_meter ( n = cur_n_div , total = self . total_div , elapsed = ( cur_time - self . start_time ) , prefix = self . prefix , unit = self . unit , bar_format = msg )
2022-07-05 02:04:28 +01:00
2021-06-04 01:19:19 +01:00
self . tk_text_var . set ( msg )
self . tk_n_var . set ( cur_n_div )
2022-07-05 02:04:28 +01:00
2021-06-04 01:19:19 +01:00
if self . withdrawn :
2023-05-24 20:05:34 +01:00
self . tk_window . geometry ( f ' + { self . tk_parent . winfo_x ( ) } + { self . tk_parent . winfo_y ( ) } ' )
2021-06-04 01:19:19 +01:00
self . tk_window . deiconify ( )
self . tk_window . grab_set ( )
2022-07-05 02:04:28 +01:00
2021-06-04 01:19:19 +01:00
if g_taskbar :
self . hwnd = int ( self . tk_window . wm_frame ( ) , 16 )
g_taskbar . ActivateTab ( self . hwnd )
g_taskbar . SetProgressState ( self . hwnd , g_tlb . TBPF_NORMAL )
2022-07-05 02:04:28 +01:00
2021-06-04 01:19:19 +01:00
self . withdrawn = False
2022-07-05 02:04:28 +01:00
2023-05-24 20:05:34 +01:00
if g_taskbar :
g_taskbar . SetProgressValue ( self . hwnd , cur_n , self . total )
2021-06-04 01:19:19 +01:00
else :
2023-11-26 21:10:42 +00:00
assert self . pbar is not None
2024-08-26 10:17:23 +01:00
self . pbar . bar_format = msg
self . pbar . n = ( float ( self . n ) / self . divisor )
self . pbar . update ( float ( n ) / self . divisor )
2022-07-05 02:04:28 +01:00
2021-03-20 01:59:53 +00:00
self . n = cur_n
2022-07-05 02:04:28 +01:00
2024-08-26 10:17:23 +01:00
def _format_speed ( self , cur_time : float , cur_n : int ) - > str :
# Short-circuit: return immediately if our unit is set to MiB. We'll let tqdm do its thing.
if self . unit == ' MiB ' :
2024-10-07 23:57:01 +01:00
return self . bar_format . replace ( ' __custom_rate_fmt__ ' , ' {rate_fmt} ' )
2024-08-26 10:17:23 +01:00
# I absolutely hate to roll out my own speed calculation for the UI, but tqdm offers no way to use different units for the progress/total/rate values.
# Please forgive me.
last_iter_time = ( cur_time - self . prev_iter_time )
if ( self . prev_n > 0 ) and ( last_iter_time != cur_time ) and ( last_iter_time < 1.0 ) and ( cur_n < self . total ) :
return ' '
rate = ( float ( cur_n - self . prev_n ) / 1048576.0 )
if last_iter_time != cur_time :
rate / = last_iter_time
self . prev_n = cur_n
self . prev_iter_time = cur_time
2024-10-07 23:57:01 +01:00
return self . bar_format . replace ( ' __custom_rate_fmt__ ' , f ' { rate : .2f } MiB/s ' )
2024-08-26 10:17:23 +01:00
2023-05-24 20:05:34 +01:00
def end ( self ) - > None :
2021-03-16 15:44:48 +00:00
self . n = 0
self . total = 0
2024-08-26 10:17:23 +01:00
self . divisor = 1
2021-03-17 21:19:44 +00:00
self . total_div = 0
2021-03-16 15:44:48 +00:00
self . prefix = ' '
self . unit = ' B '
self . start_time = 0
2024-08-26 10:17:23 +01:00
self . prev_iter_time = 0
self . prev_n = 0
2022-07-05 02:04:28 +01:00
2021-06-04 01:19:19 +01:00
if self . tk_window :
2023-11-26 21:10:42 +00:00
assert self . tk_pbar is not None
2023-05-24 20:05:34 +01:00
2021-06-04 01:19:19 +01:00
if g_taskbar :
g_taskbar . SetProgressState ( self . hwnd , g_tlb . TBPF_NOPROGRESS )
g_taskbar . UnregisterTab ( self . hwnd )
2022-07-05 02:04:28 +01:00
2021-06-04 01:19:19 +01:00
self . tk_window . grab_release ( )
2022-07-05 02:04:28 +01:00
2021-06-04 01:19:19 +01:00
self . tk_window . withdraw ( )
self . withdrawn = True
2022-07-05 02:04:28 +01:00
2021-06-04 01:19:19 +01:00
self . tk_pbar . configure ( maximum = 100 , mode = ' indeterminate ' )
else :
2023-11-26 21:10:42 +00:00
assert self . pbar is not None
2021-06-04 01:19:19 +01:00
self . pbar . close ( )
self . pbar = None
print ( )
2022-07-05 02:04:28 +01:00
2023-05-24 20:05:34 +01:00
def set_prefix ( self , prefix ) - > None :
2021-03-16 15:44:48 +00:00
self . prefix = prefix
2021-03-16 03:28:06 +00:00
2023-11-26 21:10:42 +00:00
g_progressBarWindow : ProgressBarWindow | None = None
2023-05-24 20:05:34 +01:00
2023-06-03 16:27:24 +01:00
def eprint ( * args , * * kwargs ) - > None :
print ( * args , file = sys . stderr , * * kwargs )
def utilsLogException ( exception_str : str ) - > None :
2023-06-04 19:58:46 +01:00
# Always print exception information to the terminal output.
2023-06-03 16:27:24 +01:00
eprint ( exception_str )
2023-06-04 19:58:46 +01:00
# Only print exception information to our logger if we're not in CLI mode.
if ( not g_cliMode ) and ( g_logger is not None ) :
2023-06-03 16:27:24 +01:00
g_logger . debug ( exception_str )
2023-05-24 20:05:34 +01:00
def utilsGetPath ( path_arg : str , fallback_path : str , is_file : bool , create : bool = False ) - > str :
2021-06-04 01:19:19 +01:00
path = os . path . abspath ( os . path . expanduser ( os . path . expandvars ( path_arg if path_arg else fallback_path ) ) )
2022-07-05 02:04:28 +01:00
2023-05-24 20:05:34 +01:00
if not is_file and create :
os . makedirs ( path , exist_ok = True )
2022-07-05 02:04:28 +01:00
2021-06-04 01:19:19 +01:00
if not os . path . exists ( path ) or ( is_file and os . path . isdir ( path ) ) or ( not is_file and os . path . isfile ( path ) ) :
2023-05-24 20:05:34 +01:00
raise Exception ( f ' Error: " { path } " points to an invalid file/directory. ' )
2022-07-05 02:04:28 +01:00
2021-06-04 01:19:19 +01:00
return path
2023-05-24 20:05:34 +01:00
def utilsIsValueAlignedToEndpointPacketSize ( value : int ) - > bool :
2021-03-15 15:31:52 +00:00
return bool ( ( value & ( g_usbEpMaxPacketSize - 1 ) ) == 0 )
2023-11-11 20:39:41 +00:00
def utilsResetNspInfo ( delete : bool = False ) - > None :
2021-03-17 21:19:44 +00:00
global g_nspTransferMode , g_nspSize , g_nspHeaderSize , g_nspRemainingSize , g_nspFile , g_nspFilePath
2022-07-05 02:04:28 +01:00
2023-11-11 20:39:41 +00:00
if g_nspFile :
g_nspFile . close ( )
if delete :
os . remove ( g_nspFilePath )
2021-03-15 15:31:52 +00:00
# Reset NSP transfer mode info.
g_nspTransferMode = False
g_nspSize = 0
g_nspHeaderSize = 0
g_nspRemainingSize = 0
g_nspFile = None
2023-05-24 20:05:34 +01:00
g_nspFilePath = ' '
2021-03-15 15:31:52 +00:00
2023-11-26 21:10:42 +00:00
def utilsGetSizeUnitAndDivisor ( size : int ) - > tuple [ str , int ] :
2021-03-15 15:31:52 +00:00
size_suffixes = [ ' B ' , ' KiB ' , ' MiB ' , ' GiB ' ]
size_suffixes_count = len ( size_suffixes )
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
float_size = float ( size )
2023-05-24 20:05:34 +01:00
ret = ( size_suffixes [ 0 ] , 1 )
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
for i in range ( size_suffixes_count ) :
if ( float_size < pow ( 1024 , i + 1 ) ) or ( ( i + 1 ) > = size_suffixes_count ) :
ret = ( size_suffixes [ i ] , pow ( 1024 , i ) )
break
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
return ret
2023-05-24 20:05:34 +01:00
def usbGetDeviceEndpoints ( ) - > bool :
2021-03-15 15:31:52 +00:00
global g_usbEpIn , g_usbEpOut , g_usbEpMaxPacketSize
2022-07-05 02:04:28 +01:00
2023-11-26 21:10:42 +00:00
assert g_logger is not None
2023-05-24 20:05:34 +01:00
2023-11-26 21:10:42 +00:00
cur_dev : Generator [ usb . core . Device , Any , None ] | None = None
prev_dev : usb . core . Device | None = None
2021-03-15 15:31:52 +00:00
usb_ep_in_lambda = lambda ep : usb . util . endpoint_direction ( ep . bEndpointAddress ) == usb . util . ENDPOINT_IN
usb_ep_out_lambda = lambda ep : usb . util . endpoint_direction ( ep . bEndpointAddress ) == usb . util . ENDPOINT_OUT
2021-03-20 01:59:53 +00:00
usb_version = 0
2022-07-05 02:04:28 +01:00
2023-05-24 20:05:34 +01:00
if g_cliMode :
2024-08-26 10:17:23 +01:00
g_logger . info ( SERVER_START_MSG )
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
while True :
# Check if the user decided to stop the server.
2024-08-26 10:17:23 +01:00
if not g_cliMode :
assert g_stopEvent is not None
if g_stopEvent . is_set ( ) :
g_stopEvent . clear ( )
return False
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
# Find a connected USB device with a matching VID/PID pair.
2023-05-24 20:05:34 +01:00
# Using == here to compare both device instances would also compare the backend, so we'll just compare certain elements manually.
2023-11-26 21:10:42 +00:00
try :
cur_dev = usb . core . find ( find_all = False , idVendor = USB_DEV_VID , idProduct = USB_DEV_PID )
except :
if not g_cliMode :
utilsLogException ( traceback . format_exc ( ) )
2023-12-17 18:03:08 +00:00
g_logger . error ( ' Fatal error ocurred while enumerating USB devices. ' )
2023-11-26 21:10:42 +00:00
if g_isWindows :
2023-12-17 18:03:08 +00:00
g_logger . error ( ' Try reinstalling the libusbK driver using Zadig. ' )
2023-11-26 21:10:42 +00:00
return False
if ( not isinstance ( cur_dev , usb . core . Device ) ) or ( isinstance ( prev_dev , usb . core . Device ) and ( cur_dev . bus == prev_dev . bus ) and ( cur_dev . address == prev_dev . address ) ) :
2021-03-15 15:31:52 +00:00
time . sleep ( 0.1 )
continue
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
# Update previous device.
prev_dev = cur_dev
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
# Check if the product and manufacturer strings match the ones used by nxdumptool.
2023-05-24 20:05:34 +01:00
# TODO: enable product string check whenever we're ready for a release.
2021-03-15 15:31:52 +00:00
#if (cur_dev.manufacturer != USB_DEV_MANUFACTURER) or (cur_dev.product != USB_DEV_PRODUCT):
if cur_dev . manufacturer != USB_DEV_MANUFACTURER :
2023-05-24 20:05:34 +01:00
g_logger . error ( f ' Invalid manufacturer/product strings! (bus { cur_dev . bus } , address { cur_dev . address } ). ' )
2021-03-15 15:31:52 +00:00
time . sleep ( 0.1 )
continue
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
# Reset device.
cur_dev . reset ( )
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
# Set default device configuration, then get the active configuration descriptor.
cur_dev . set_configuration ( )
cfg = cur_dev . get_active_configuration ( )
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
# Get default interface descriptor.
intf = cfg [ ( 0 , 0 ) ]
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
# Retrieve endpoints.
g_usbEpIn = usb . util . find_descriptor ( intf , custom_match = usb_ep_in_lambda )
g_usbEpOut = usb . util . find_descriptor ( intf , custom_match = usb_ep_out_lambda )
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
if ( g_usbEpIn is None ) or ( g_usbEpOut is None ) :
2023-05-24 20:05:34 +01:00
g_logger . error ( f ' Invalid endpoint addresses! (bus { cur_dev . bus } , address { cur_dev . address } ). ' )
2021-03-15 15:31:52 +00:00
time . sleep ( 0.1 )
continue
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
# Save endpoint max packet size and USB version.
g_usbEpMaxPacketSize = g_usbEpIn . wMaxPacketSize
usb_version = cur_dev . bcdUSB
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
break
2022-07-05 02:04:28 +01:00
2023-05-24 20:05:34 +01:00
g_logger . debug ( f ' Successfully retrieved USB endpoints! (bus { cur_dev . bus } , address { cur_dev . address } ). ' )
g_logger . debug ( f ' Max packet size: 0x { g_usbEpMaxPacketSize : X } (USB { usb_version >> 8 } . { ( usb_version & 0xFF ) >> 4 } ). \n ' )
2022-07-05 02:04:28 +01:00
2023-05-24 20:05:34 +01:00
if g_cliMode :
2024-08-26 10:17:23 +01:00
g_logger . info ( SERVER_STOP_MSG )
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
return True
2023-05-24 20:05:34 +01:00
def usbRead ( size : int , timeout : int = - 1 ) - > bytes :
rd = b ' '
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
try :
# Convert read data to a bytes object for easier handling.
rd = bytes ( g_usbEpIn . read ( size , timeout ) )
2021-06-04 01:19:19 +01:00
except usb . core . USBError :
2023-05-24 20:05:34 +01:00
if not g_cliMode :
2023-06-03 16:27:24 +01:00
utilsLogException ( traceback . format_exc ( ) )
2023-11-26 21:10:42 +00:00
if g_logger is not None :
g_logger . error ( ' \n USB timeout triggered or console disconnected. ' )
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
return rd
2023-05-24 20:05:34 +01:00
def usbWrite ( data : bytes , timeout : int = - 1 ) - > int :
2021-03-15 15:31:52 +00:00
wr = 0
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
try :
wr = g_usbEpOut . write ( data , timeout )
2021-06-04 01:19:19 +01:00
except usb . core . USBError :
2023-05-24 20:05:34 +01:00
if not g_cliMode :
2023-06-03 16:27:24 +01:00
utilsLogException ( traceback . format_exc ( ) )
2023-11-26 21:10:42 +00:00
if g_logger is not None :
g_logger . error ( ' \n USB timeout triggered or console disconnected. ' )
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
return wr
2023-05-24 20:05:34 +01:00
def usbSendStatus ( code : int ) - > bool :
2023-11-26 21:10:42 +00:00
status = struct . pack ( ' <4sIH6x ' , USB_MAGIC_WORD , code , g_usbEpMaxPacketSize )
2023-05-24 20:05:34 +01:00
return bool ( usbWrite ( status , USB_TRANSFER_TIMEOUT ) == len ( status ) )
2021-03-15 15:31:52 +00:00
2023-05-24 20:05:34 +01:00
def usbHandleStartSession ( cmd_block : bytes ) - > int :
global g_nxdtVersionMajor , g_nxdtVersionMinor , g_nxdtVersionMicro , g_nxdtAbiVersionMajor , g_nxdtAbiVersionMinor , g_nxdtGitCommit
2022-07-05 02:04:28 +01:00
2023-11-26 21:10:42 +00:00
assert g_logger is not None
2023-05-24 20:05:34 +01:00
if g_cliMode :
print ( )
g_logger . debug ( f ' Received StartSession ( { USB_CMD_START_SESSION : 02X } ) command. ' )
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
# Parse command block.
2023-05-24 20:05:34 +01:00
( g_nxdtVersionMajor , g_nxdtVersionMinor , g_nxdtVersionMicro , abi_version , git_commit ) = struct . unpack_from ( ' <BBBB8s ' , cmd_block , 0 )
g_nxdtGitCommit = git_commit . decode ( ' utf-8 ' ) . strip ( ' \x00 ' )
# Unpack ABI version.
g_nxdtAbiVersionMajor = ( ( abi_version >> 4 ) & 0x0F )
g_nxdtAbiVersionMinor = ( abi_version & 0x0F )
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
# Print client info.
2023-05-24 20:05:34 +01:00
g_logger . info ( f ' Client info: { USB_DEV_PRODUCT } v { g_nxdtVersionMajor } . { g_nxdtVersionMinor } . { g_nxdtVersionMicro } , USB ABI v { g_nxdtAbiVersionMajor } . { g_nxdtAbiVersionMinor } (commit { g_nxdtGitCommit } ). \n ' )
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
# Check if we support this ABI version.
2023-05-24 20:05:34 +01:00
if ( g_nxdtAbiVersionMajor != USB_ABI_VERSION_MAJOR ) or ( g_nxdtAbiVersionMinor != USB_ABI_VERSION_MINOR ) :
2021-06-04 01:19:19 +01:00
g_logger . error ( ' Unsupported ABI version! ' )
2021-03-15 15:31:52 +00:00
return USB_STATUS_UNSUPPORTED_ABI_VERSION
2022-07-05 02:04:28 +01:00
2023-05-24 20:05:34 +01:00
# Return status code.
2021-03-15 15:31:52 +00:00
return USB_STATUS_SUCCESS
2023-05-24 20:05:34 +01:00
def usbHandleSendFileProperties ( cmd_block : bytes ) - > int | None :
2021-03-17 21:19:44 +00:00
global g_nspTransferMode , g_nspSize , g_nspHeaderSize , g_nspRemainingSize , g_nspFile , g_nspFilePath , g_outputDir , g_tkRoot , g_progressBarWindow
2022-07-05 02:04:28 +01:00
2023-11-26 21:10:42 +00:00
assert g_logger is not None
assert g_progressBarWindow is not None
2023-05-24 20:05:34 +01:00
if g_cliMode and not g_nspTransferMode :
print ( )
g_logger . debug ( f ' Received SendFileProperties ( { USB_CMD_SEND_FILE_PROPERTIES : 02X } ) command. ' )
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
# Parse command block.
2023-05-24 20:05:34 +01:00
( file_size , filename_length , nsp_header_size , raw_filename ) = struct . unpack_from ( f ' <QII { USB_FILE_PROPERTIES_MAX_NAME_LENGTH } s ' , cmd_block , 0 )
2021-03-15 15:31:52 +00:00
filename = raw_filename . decode ( ' utf-8 ' ) . strip ( ' \x00 ' )
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
# Print info.
2023-05-24 20:05:34 +01:00
dbg_str = f ' File size: 0x { file_size : X } | Filename length: 0x { filename_length : X } '
if nsp_header_size > 0 :
dbg_str + = f ' | NSP header size: 0x { nsp_header_size : X } '
g_logger . debug ( dbg_str + ' . ' )
2022-07-05 02:04:28 +01:00
2021-03-20 01:59:53 +00:00
file_type_str = ( ' file ' if ( not g_nspTransferMode ) else ' NSP file entry ' )
2022-07-05 02:04:28 +01:00
2023-05-24 20:05:34 +01:00
if not g_cliMode or ( g_cliMode and not g_nspTransferMode ) :
g_logger . info ( f ' Receiving { file_type_str } : " { filename } " . ' )
2024-08-26 10:17:23 +01:00
# Perform sanity checks.
2021-03-17 17:25:30 +00:00
if ( not g_nspTransferMode ) and file_size and ( nsp_header_size > = file_size ) :
2021-06-04 01:19:19 +01:00
g_logger . error ( ' NSP header size must be smaller than the full NSP size! \n ' )
2021-03-15 15:31:52 +00:00
return USB_STATUS_MALFORMED_CMD
2022-07-05 02:04:28 +01:00
2021-03-17 17:25:30 +00:00
if g_nspTransferMode and nsp_header_size :
2021-06-04 01:19:19 +01:00
g_logger . error ( ' Received non-zero NSP header size during NSP transfer mode! \n ' )
2021-03-15 15:31:52 +00:00
return USB_STATUS_MALFORMED_CMD
2022-07-05 02:04:28 +01:00
2021-03-17 17:25:30 +00:00
if ( not filename_length ) or ( filename_length > USB_FILE_PROPERTIES_MAX_NAME_LENGTH ) :
2021-06-04 01:19:19 +01:00
g_logger . error ( ' Invalid filename length! \n ' )
2021-03-15 15:31:52 +00:00
return USB_STATUS_MALFORMED_CMD
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
# Enable NSP transfer mode (if needed).
2021-03-17 17:25:30 +00:00
if ( not g_nspTransferMode ) and file_size and nsp_header_size :
2021-03-15 15:31:52 +00:00
g_nspTransferMode = True
g_nspSize = file_size
g_nspHeaderSize = nsp_header_size
2021-03-16 15:44:48 +00:00
g_nspRemainingSize = ( file_size - nsp_header_size )
2021-03-15 15:31:52 +00:00
g_nspFile = None
2023-05-24 20:05:34 +01:00
g_nspFilePath = ' '
2021-06-04 01:19:19 +01:00
g_logger . debug ( ' NSP transfer mode enabled! \n ' )
2022-07-05 02:04:28 +01:00
2024-08-26 10:17:23 +01:00
# Perform additional sanity checks and get a file object to work with.
2021-03-17 17:25:30 +00:00
if ( not g_nspTransferMode ) or ( g_nspFile is None ) :
2021-03-15 15:31:52 +00:00
# Generate full, absolute path to the destination file.
fullpath = os . path . abspath ( g_outputDir + os . path . sep + filename )
2023-11-26 21:10:42 +00:00
printable_fullpath = ( fullpath [ 4 : ] if g_isWindows else fullpath )
2023-11-23 21:33:13 +00:00
2021-03-15 15:31:52 +00:00
# Get parent directory path.
dirpath = os . path . dirname ( fullpath )
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
# Create full directory tree.
os . makedirs ( dirpath , exist_ok = True )
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
# Make sure the output filepath doesn't point to an existing directory.
2021-03-17 17:25:30 +00:00
if os . path . exists ( fullpath ) and ( not os . path . isfile ( fullpath ) ) :
2021-03-15 15:31:52 +00:00
utilsResetNspInfo ( )
2023-11-26 21:10:42 +00:00
g_logger . error ( f ' Output filepath points to an existing directory! ( " { printable_fullpath } " ). \n ' )
2021-03-15 15:31:52 +00:00
return USB_STATUS_HOST_IO_ERROR
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
# Make sure we have enough free space.
2023-11-26 21:10:42 +00:00
( _ , _ , free_space ) = shutil . disk_usage ( dirpath )
2021-03-15 15:31:52 +00:00
if free_space < = file_size :
utilsResetNspInfo ( )
2021-06-04 01:19:19 +01:00
g_logger . error ( ' Not enough free space available in output volume! \n ' )
2021-03-15 15:31:52 +00:00
return USB_STATUS_HOST_IO_ERROR
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
# Get file object.
file = open ( fullpath , " wb " )
2022-07-05 02:04:28 +01:00
2021-03-17 17:25:30 +00:00
if g_nspTransferMode :
2021-03-15 15:31:52 +00:00
# Update NSP file object.
g_nspFile = file
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
# Update NSP file path.
g_nspFilePath = fullpath
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
# Write NSP header padding right away.
file . write ( b ' \0 ' * g_nspHeaderSize )
else :
# Retrieve what we need using global variables.
file = g_nspFile
fullpath = g_nspFilePath
dirpath = os . path . dirname ( fullpath )
2023-11-26 21:10:42 +00:00
printable_fullpath = ( fullpath [ 4 : ] if g_isWindows else fullpath )
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
# Check if we're dealing with an empty file or with the first SendFileProperties command from a NSP.
2021-03-17 17:25:30 +00:00
if ( not file_size ) or ( g_nspTransferMode and file_size == g_nspSize ) :
2021-03-15 15:31:52 +00:00
# Close file (if needed).
2023-05-24 20:05:34 +01:00
if not g_nspTransferMode :
file . close ( )
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
# Let the command handler take care of sending the status response for us.
return USB_STATUS_SUCCESS
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
# Send status response before entering the data transfer stage.
usbSendStatus ( USB_STATUS_SUCCESS )
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
# Start data transfer stage.
2024-08-26 10:17:23 +01:00
g_logger . debug ( f ' Data transfer started. { " Saving " if file_type_str == " file " else " Writing " } { file_type_str } to: " { printable_fullpath } " . ' )
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
offset = 0
blksize = USB_TRANSFER_BLOCK_SIZE
2022-07-05 02:04:28 +01:00
2021-03-17 21:19:44 +00:00
# Check if we should use the progress bar window.
2021-03-17 17:25:30 +00:00
use_pbar = ( ( ( not g_nspTransferMode ) and ( file_size > USB_TRANSFER_THRESHOLD ) ) or ( g_nspTransferMode and ( g_nspSize > USB_TRANSFER_THRESHOLD ) ) )
if use_pbar :
2021-06-04 01:19:19 +01:00
if g_cliMode :
2021-06-06 00:04:42 +01:00
# We're not using dynamic tqdm prefixes under CLI mode.
2021-06-04 01:19:19 +01:00
prefix = ' '
else :
2023-11-26 21:10:42 +00:00
prefix = f ' Current { file_type_str } : " { os . path . basename ( filename ) } " . \n '
2021-06-04 01:19:19 +01:00
prefix + = ' Use your console to cancel the file transfer if you wish to do so. '
2022-07-05 02:04:28 +01:00
2021-03-17 17:25:30 +00:00
if ( not g_nspTransferMode ) or g_nspRemainingSize == ( g_nspSize - g_nspHeaderSize ) :
if not g_nspTransferMode :
# Set current progress to zero and the maximum value to the provided file size.
pbar_n = 0
pbar_file_size = file_size
else :
# Set current progress to the NSP header size and the maximum value to the provided NSP size.
pbar_n = g_nspHeaderSize
pbar_file_size = g_nspSize
2022-07-05 02:04:28 +01:00
2021-03-20 01:59:53 +00:00
# Display progress bar window.
2024-08-26 10:17:23 +01:00
g_progressBarWindow . start ( pbar_file_size , pbar_n , prefix )
2021-03-16 15:44:48 +00:00
else :
2021-03-17 21:19:44 +00:00
# Set current prefix (holds the filename for the current NSP file entry).
g_progressBarWindow . set_prefix ( prefix )
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
def cancelTransfer ( ) :
# Cancel file transfer.
2023-11-26 21:10:42 +00:00
if g_nspTransferMode :
utilsResetNspInfo ( True )
else :
file . close ( )
os . remove ( fullpath )
if use_pbar and ( g_progressBarWindow is not None ) :
2023-05-24 20:05:34 +01:00
g_progressBarWindow . end ( )
2022-07-05 02:04:28 +01:00
2021-03-16 15:44:48 +00:00
# Start transfer process.
2021-03-17 17:25:30 +00:00
start_time = time . time ( )
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
while offset < file_size :
# Update block size (if needed).
diff = ( file_size - offset )
2021-03-17 17:25:30 +00:00
if blksize > diff : blksize = diff
2022-07-05 02:04:28 +01:00
2021-03-17 17:25:30 +00:00
# Set block size and handle Zero-Length Termination packet (if needed).
rd_size = blksize
2023-05-24 20:05:34 +01:00
if ( ( offset + blksize ) > = file_size ) and utilsIsValueAlignedToEndpointPacketSize ( blksize ) :
rd_size + = 1
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
# Read current chunk.
chunk = usbRead ( rd_size , USB_TRANSFER_TIMEOUT )
2023-05-24 20:05:34 +01:00
if not chunk :
g_logger . error ( f ' Failed to read 0x { rd_size : X } -byte long data chunk! ' )
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
# Cancel file transfer.
cancelTransfer ( )
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
# Returning None will make the command handler exit right away.
return None
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
chunk_size = len ( chunk )
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
# Check if we're dealing with a CancelFileTransfer command.
if chunk_size == USB_CMD_HEADER_SIZE :
2023-11-26 21:10:42 +00:00
( magic , cmd_id , _ ) = struct . unpack_from ( ' <4sII ' , chunk , 0 )
2021-03-15 15:31:52 +00:00
if ( magic == USB_MAGIC_WORD ) and ( cmd_id == USB_CMD_CANCEL_FILE_TRANSFER ) :
# Cancel file transfer.
cancelTransfer ( )
2022-07-05 02:04:28 +01:00
2023-05-24 20:05:34 +01:00
g_logger . debug ( f ' Received CancelFileTransfer ( { USB_CMD_CANCEL_FILE_TRANSFER : 02X } ) command. ' )
2021-06-04 01:19:19 +01:00
g_logger . warning ( ' Transfer cancelled. ' )
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
# Let the command handler take care of sending the status response for us.
return USB_STATUS_SUCCESS
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
# Write current chunk.
file . write ( chunk )
file . flush ( )
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
# Update current offset.
offset = ( offset + chunk_size )
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
# Update remaining NSP data size.
2023-05-24 20:05:34 +01:00
if g_nspTransferMode :
g_nspRemainingSize - = chunk_size
2022-07-05 02:04:28 +01:00
2021-03-17 21:19:44 +00:00
# Update progress bar window (if needed).
2023-05-24 20:05:34 +01:00
if use_pbar :
g_progressBarWindow . update ( chunk_size )
2022-07-05 02:04:28 +01:00
2021-03-17 17:25:30 +00:00
elapsed_time = round ( time . time ( ) - start_time )
2023-05-24 20:05:34 +01:00
g_logger . debug ( f ' File transfer successfully completed in { tqdm . format_interval ( elapsed_time ) } ! \n ' )
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
# Close file handle (if needed).
2023-05-24 20:05:34 +01:00
if not g_nspTransferMode :
file . close ( )
2022-07-05 02:04:28 +01:00
2021-03-17 21:19:44 +00:00
# Hide progress bar window (if needed).
2023-05-24 20:05:34 +01:00
if use_pbar and ( ( not g_nspTransferMode ) or ( not g_nspRemainingSize ) ) :
g_progressBarWindow . end ( )
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
return USB_STATUS_SUCCESS
2023-11-11 20:39:41 +00:00
def usbHandleCancelFileTransfer ( cmd_block : bytes ) - > int :
2023-11-26 21:10:42 +00:00
assert g_logger is not None
2023-11-11 20:39:41 +00:00
g_logger . debug ( f ' Received CancelFileTransfer ( { USB_CMD_START_SESSION : 02X } ) command. ' )
if g_nspTransferMode :
2024-08-26 10:17:23 +01:00
if ( g_nspSize > USB_TRANSFER_THRESHOLD ) and ( g_progressBarWindow is not None ) :
g_progressBarWindow . end ( )
2023-11-11 20:39:41 +00:00
utilsResetNspInfo ( True )
2024-08-26 10:17:23 +01:00
2023-11-11 20:39:41 +00:00
g_logger . warning ( ' Transfer cancelled. ' )
return USB_STATUS_SUCCESS
else :
g_logger . error ( ' Unexpected transfer cancellation. ' )
return USB_STATUS_MALFORMED_CMD
2023-05-24 20:05:34 +01:00
def usbHandleSendNspHeader ( cmd_block : bytes ) - > int :
2021-03-15 15:31:52 +00:00
global g_nspTransferMode , g_nspHeaderSize , g_nspRemainingSize , g_nspFile , g_nspFilePath
2022-07-05 02:04:28 +01:00
2023-11-26 21:10:42 +00:00
assert g_logger is not None
assert g_nspFile is not None
2023-05-24 20:05:34 +01:00
2021-03-15 15:31:52 +00:00
nsp_header_size = len ( cmd_block )
2022-07-05 02:04:28 +01:00
2023-05-24 20:05:34 +01:00
g_logger . debug ( f ' Received SendNspHeader ( { USB_CMD_SEND_NSP_HEADER : 02X } ) command. ' )
2022-07-05 02:04:28 +01:00
2023-05-24 20:05:34 +01:00
# Validity checks.
2021-03-17 17:25:30 +00:00
if not g_nspTransferMode :
2021-06-04 01:19:19 +01:00
g_logger . error ( ' Received NSP header out of NSP transfer mode! \n ' )
2021-03-15 15:31:52 +00:00
return USB_STATUS_MALFORMED_CMD
2022-07-05 02:04:28 +01:00
2021-03-17 17:25:30 +00:00
if g_nspRemainingSize :
2023-05-24 20:05:34 +01:00
g_logger . error ( f ' Received NSP header before receiving all NSP data! (missing 0x { g_nspRemainingSize : X } byte[s]). \n ' )
2021-03-15 15:31:52 +00:00
return USB_STATUS_MALFORMED_CMD
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
if nsp_header_size != g_nspHeaderSize :
2023-05-24 20:05:34 +01:00
g_logger . error ( f ' NSP header size mismatch! (0x { nsp_header_size : X } != 0x { g_nspHeaderSize : X } ). \n ' )
2021-03-15 15:31:52 +00:00
return USB_STATUS_MALFORMED_CMD
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
# Write NSP header.
g_nspFile . seek ( 0 )
g_nspFile . write ( cmd_block )
2022-07-05 02:04:28 +01:00
2023-05-24 20:05:34 +01:00
g_logger . debug ( f ' Successfully wrote 0x { nsp_header_size : X } -byte long NSP header to " { g_nspFilePath } " . \n ' )
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
# Disable NSP transfer mode.
utilsResetNspInfo ( )
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
return USB_STATUS_SUCCESS
2023-05-24 20:05:34 +01:00
def usbHandleEndSession ( cmd_block : bytes ) - > int :
2023-11-26 21:10:42 +00:00
assert g_logger is not None
2023-05-24 20:05:34 +01:00
g_logger . debug ( f ' Received EndSession ( { USB_CMD_END_SESSION : 02X } ) command. ' )
2021-03-15 15:31:52 +00:00
return USB_STATUS_SUCCESS
2023-11-11 20:39:41 +00:00
def usbHandleStartExtractedFsDump ( cmd_block : bytes ) - > int :
2023-11-26 21:10:42 +00:00
assert g_logger is not None
2023-11-11 20:39:41 +00:00
g_logger . debug ( f ' Received StartExtractedFsDump ( { USB_CMD_START_EXTRACTED_FS_DUMP : 02X } ) command. ' )
if g_nspTransferMode :
g_logger . error ( ' StartExtractedFsDump received mid NSP transfer. ' )
return USB_STATUS_MALFORMED_CMD
# Parse command block.
( extracted_fs_size , extracted_fs_root_path ) = struct . unpack_from ( f ' <Q { USB_FILE_PROPERTIES_MAX_NAME_LENGTH } s ' , cmd_block , 0 )
extracted_fs_root_path = extracted_fs_root_path . decode ( ' utf-8 ' ) . strip ( ' \x00 ' )
g_logger . info ( f ' Starting extracted FS dump (size 0x { extracted_fs_size : X } , output relative path " { extracted_fs_root_path } " ). ' )
# Return status code.
return USB_STATUS_SUCCESS
def usbHandleEndExtractedFsDump ( cmd_block : bytes ) - > int :
2023-11-26 21:10:42 +00:00
assert g_logger is not None
2023-11-11 20:39:41 +00:00
g_logger . debug ( f ' Received EndExtractedFsDump ( { USB_CMD_END_EXTRACTED_FS_DUMP : 02X } ) command. ' )
g_logger . info ( f ' Finished extracted FS dump. ' )
return USB_STATUS_SUCCESS
2023-05-24 20:05:34 +01:00
def usbCommandHandler ( ) - > None :
2023-11-26 21:10:42 +00:00
assert g_logger is not None
2023-05-24 20:05:34 +01:00
2021-03-15 15:31:52 +00:00
cmd_dict = {
2023-11-11 20:39:41 +00:00
USB_CMD_START_SESSION : usbHandleStartSession ,
USB_CMD_SEND_FILE_PROPERTIES : usbHandleSendFileProperties ,
USB_CMD_CANCEL_FILE_TRANSFER : usbHandleCancelFileTransfer ,
USB_CMD_SEND_NSP_HEADER : usbHandleSendNspHeader ,
USB_CMD_END_SESSION : usbHandleEndSession ,
USB_CMD_START_EXTRACTED_FS_DUMP : usbHandleStartExtractedFsDump ,
USB_CMD_END_EXTRACTED_FS_DUMP : usbHandleEndExtractedFsDump
2021-03-15 15:31:52 +00:00
}
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
# Get device endpoints.
2021-03-17 17:25:30 +00:00
if not usbGetDeviceEndpoints ( ) :
2021-06-04 01:19:19 +01:00
if not g_cliMode :
# Update UI.
uiToggleElements ( True )
2021-03-15 15:31:52 +00:00
return
2022-07-05 02:04:28 +01:00
2021-06-04 01:19:19 +01:00
if not g_cliMode :
# Update UI.
2023-11-26 21:10:42 +00:00
assert g_tkCanvas is not None
assert g_tkServerButton is not None
2021-06-04 01:19:19 +01:00
g_tkCanvas . itemconfigure ( g_tkTipMessage , state = ' normal ' , text = SERVER_STOP_MSG )
g_tkServerButton . configure ( state = ' disabled ' )
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
# Reset NSP info.
utilsResetNspInfo ( )
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
while True :
# Read command header.
cmd_header = usbRead ( USB_CMD_HEADER_SIZE )
2023-05-24 20:05:34 +01:00
if ( not cmd_header ) or ( len ( cmd_header ) != USB_CMD_HEADER_SIZE ) :
g_logger . error ( f ' Failed to read 0x { USB_CMD_HEADER_SIZE : X } -byte long command header! ' )
2021-03-15 15:31:52 +00:00
break
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
# Parse command header.
( magic , cmd_id , cmd_block_size ) = struct . unpack_from ( ' <4sII ' , cmd_header , 0 )
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
# Read command block right away (if needed).
# nxdumptool expects us to read it right after sending the command header.
2023-05-24 20:05:34 +01:00
cmd_block : bytes = b ' '
2021-03-17 17:25:30 +00:00
if cmd_block_size :
2021-03-15 15:31:52 +00:00
# Handle Zero-Length Termination packet (if needed).
2021-03-17 17:25:30 +00:00
if utilsIsValueAlignedToEndpointPacketSize ( cmd_block_size ) :
2021-03-15 15:31:52 +00:00
rd_size = ( cmd_block_size + 1 )
else :
rd_size = cmd_block_size
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
cmd_block = usbRead ( rd_size , USB_TRANSFER_TIMEOUT )
2023-05-24 20:05:34 +01:00
if ( not cmd_block ) or ( len ( cmd_block ) != cmd_block_size ) :
g_logger . error ( f ' Failed to read 0x { cmd_block_size : X } -byte long command block for command ID { cmd_id : 02X } ! ' )
2021-03-15 15:31:52 +00:00
break
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
# Verify magic word.
if magic != USB_MAGIC_WORD :
2021-06-04 01:19:19 +01:00
g_logger . error ( ' Received command header with invalid magic word! \n ' )
2021-03-15 15:31:52 +00:00
usbSendStatus ( USB_STATUS_INVALID_MAGIC_WORD )
continue
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
# Get command handler function.
cmd_func = cmd_dict . get ( cmd_id , None )
if cmd_func is None :
2023-05-24 20:05:34 +01:00
g_logger . error ( f ' Received command header with unsupported ID { cmd_id : 02X } . \n ' )
2021-03-15 15:31:52 +00:00
usbSendStatus ( USB_STATUS_UNSUPPORTED_CMD )
continue
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
# Verify command block size.
2021-03-17 17:25:30 +00:00
if ( cmd_id == USB_CMD_START_SESSION and cmd_block_size != USB_CMD_BLOCK_SIZE_START_SESSION ) or \
( cmd_id == USB_CMD_SEND_FILE_PROPERTIES and cmd_block_size != USB_CMD_BLOCK_SIZE_SEND_FILE_PROPERTIES ) or \
2023-11-11 20:39:41 +00:00
( cmd_id == USB_CMD_SEND_NSP_HEADER and not cmd_block_size ) or \
( cmd_id == USB_CMD_START_EXTRACTED_FS_DUMP and cmd_block_size != USB_CMD_BLOCK_SIZE_START_EXTRACTED_FS_DUMP ) :
2023-05-24 20:05:34 +01:00
g_logger . error ( f ' Invalid command block size for command ID { cmd_id : 02X } ! (0x { cmd_block_size : X } ). \n ' )
2021-12-01 12:18:31 +00:00
usbSendStatus ( USB_STATUS_MALFORMED_CMD )
2021-03-15 15:31:52 +00:00
continue
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
# Run command handler function.
# Send status response afterwards. Bail out if requested.
status = cmd_func ( cmd_block )
2021-03-17 17:25:30 +00:00
if ( status is None ) or ( not usbSendStatus ( status ) ) or ( cmd_id == USB_CMD_END_SESSION ) or ( status == USB_STATUS_UNSUPPORTED_ABI_VERSION ) :
2021-03-15 15:31:52 +00:00
break
2022-07-05 02:04:28 +01:00
2021-06-04 01:19:19 +01:00
g_logger . info ( ' \n Stopping server. ' )
2022-07-05 02:04:28 +01:00
2021-06-04 01:19:19 +01:00
if not g_cliMode :
# Update UI.
uiToggleElements ( True )
2021-03-15 15:31:52 +00:00
2023-05-24 20:05:34 +01:00
def uiStopServer ( ) - > None :
2021-03-15 15:31:52 +00:00
# Signal the shared stop event.
2023-11-26 21:10:42 +00:00
assert g_stopEvent is not None
2021-03-15 15:31:52 +00:00
g_stopEvent . set ( )
2023-05-24 20:05:34 +01:00
def uiStartServer ( ) - > None :
2021-03-15 15:31:52 +00:00
global g_outputDir
2022-07-05 02:04:28 +01:00
2023-11-26 21:10:42 +00:00
assert g_tkDirText is not None
2023-05-24 20:05:34 +01:00
2021-03-15 15:31:52 +00:00
g_outputDir = g_tkDirText . get ( ' 1.0 ' , tk . END ) . strip ( )
if not g_outputDir :
# We should never reach this, honestly.
messagebox . showerror ( ' Error ' , ' You must provide an output directory! ' , parent = g_tkRoot )
return
2022-07-05 02:04:28 +01:00
2023-11-26 21:10:42 +00:00
# Unconditionally enable 32-bit paths on Windows.
if g_isWindows :
g_outputDir = ' \\ \\ ? \\ ' + g_outputDir
2021-03-15 15:31:52 +00:00
# Make sure the full directory tree exists.
try :
os . makedirs ( g_outputDir , exist_ok = True )
except :
2023-06-03 16:27:24 +01:00
utilsLogException ( traceback . format_exc ( ) )
2021-03-15 15:31:52 +00:00
messagebox . showerror ( ' Error ' , ' Unable to create full output directory tree! ' , parent = g_tkRoot )
return
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
# Update UI.
uiToggleElements ( False )
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
# Create background server thread.
server_thread = threading . Thread ( target = usbCommandHandler , daemon = True )
server_thread . start ( )
2023-05-24 20:05:34 +01:00
def uiToggleElements ( flag : bool ) - > None :
2023-11-26 21:10:42 +00:00
assert g_tkRoot is not None
assert g_tkChooseDirButton is not None
assert g_tkServerButton is not None
assert g_tkCanvas is not None
assert g_tkVerboseCheckbox is not None
2023-05-24 20:05:34 +01:00
if flag :
2021-03-15 15:31:52 +00:00
g_tkRoot . protocol ( ' WM_DELETE_WINDOW ' , uiHandleExitProtocol )
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
g_tkChooseDirButton . configure ( state = ' normal ' )
g_tkServerButton . configure ( text = ' Start server ' , command = uiStartServer , state = ' normal ' )
g_tkCanvas . itemconfigure ( g_tkTipMessage , state = ' hidden ' , text = ' ' )
2023-06-03 16:27:24 +01:00
g_tkVerboseCheckbox . configure ( state = ' normal ' )
2021-03-15 15:31:52 +00:00
else :
2023-11-26 21:10:42 +00:00
assert g_tkScrolledTextLog is not None
2023-05-24 20:05:34 +01:00
2021-03-15 15:31:52 +00:00
g_tkRoot . protocol ( ' WM_DELETE_WINDOW ' , uiHandleExitProtocolStub )
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
g_tkChooseDirButton . configure ( state = ' disabled ' )
g_tkServerButton . configure ( text = ' Stop server ' , command = uiStopServer , state = ' normal ' )
g_tkCanvas . itemconfigure ( g_tkTipMessage , state = ' normal ' , text = SERVER_START_MSG )
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
g_tkScrolledTextLog . configure ( state = ' normal ' )
g_tkScrolledTextLog . delete ( ' 1.0 ' , tk . END )
g_tkScrolledTextLog . configure ( state = ' disabled ' )
2023-06-03 16:27:24 +01:00
g_tkVerboseCheckbox . configure ( state = ' disabled ' )
2023-05-24 20:05:34 +01:00
def uiChooseDirectory ( ) - > None :
2021-03-15 15:31:52 +00:00
dir = filedialog . askdirectory ( parent = g_tkRoot , title = ' Select an output directory ' , initialdir = INITIAL_DIR , mustexist = True )
2023-05-24 20:05:34 +01:00
if dir :
uiUpdateDirectoryField ( os . path . abspath ( dir ) )
2021-03-15 15:31:52 +00:00
2023-05-24 20:05:34 +01:00
def uiUpdateDirectoryField ( path : str ) - > None :
2023-11-26 21:10:42 +00:00
assert g_tkDirText is not None
2021-03-15 15:31:52 +00:00
g_tkDirText . configure ( state = ' normal ' )
g_tkDirText . delete ( ' 1.0 ' , tk . END )
2023-05-24 20:05:34 +01:00
g_tkDirText . insert ( ' 1.0 ' , path )
2021-03-15 15:31:52 +00:00
g_tkDirText . configure ( state = ' disabled ' )
2023-05-24 20:05:34 +01:00
def uiHandleExitProtocol ( ) - > None :
2023-11-26 21:10:42 +00:00
assert g_tkRoot is not None
2021-03-16 15:54:17 +00:00
g_tkRoot . destroy ( )
2021-03-15 15:31:52 +00:00
2023-05-24 20:05:34 +01:00
def uiHandleExitProtocolStub ( ) - > None :
2021-03-15 15:31:52 +00:00
pass
2023-05-24 20:05:34 +01:00
def uiScaleMeasure ( measure : int ) - > int :
2021-03-15 15:31:52 +00:00
return round ( float ( measure ) * SCALE )
2023-06-03 16:27:24 +01:00
def uiHandleVerboseCheckbox ( ) - > None :
2023-11-26 21:10:42 +00:00
assert g_logger is not None
assert g_logLevelIntVar is not None
2023-06-03 16:27:24 +01:00
g_logger . setLevel ( g_logLevelIntVar . get ( ) )
2023-05-24 20:05:34 +01:00
def uiInitialize ( ) - > None :
2023-06-03 16:27:24 +01:00
global SCALE , g_logLevelIntVar
global g_tkRoot , g_tkCanvas , g_tkDirText , g_tkChooseDirButton , g_tkServerButton , g_tkTipMessage , g_tkScrolledTextLog , g_tkVerboseCheckbox
2021-06-04 01:19:19 +01:00
global g_stopEvent , g_tlb , g_taskbar , g_progressBarWindow
2022-07-05 02:04:28 +01:00
2021-06-04 01:19:19 +01:00
# Setup thread event.
g_stopEvent = threading . Event ( )
2022-07-05 02:04:28 +01:00
2021-03-20 01:59:53 +00:00
# Enable high DPI scaling under Windows (if possible).
2023-05-24 20:05:34 +01:00
# This will remove the blur caused by bilineal filtering when automatic scaling is carried out by Windows itself.
2021-03-15 15:31:52 +00:00
dpi_aware = False
2021-03-20 01:59:53 +00:00
if g_isWindowsVista :
2021-03-15 15:31:52 +00:00
try :
2021-03-20 01:59:53 +00:00
import ctypes
2021-03-15 15:31:52 +00:00
dpi_aware = ( ctypes . windll . user32 . SetProcessDPIAware ( ) == 1 )
2023-05-24 20:05:34 +01:00
if not dpi_aware :
dpi_aware = ( ctypes . windll . shcore . SetProcessDpiAwareness ( 1 ) == 0 )
2021-03-15 15:31:52 +00:00
except :
2023-06-03 16:27:24 +01:00
utilsLogException ( traceback . format_exc ( ) )
2022-07-05 02:04:28 +01:00
2021-03-20 01:59:53 +00:00
# Enable taskbar features under Windows (if possible).
del_tlb = False
2022-07-05 02:04:28 +01:00
2021-03-20 01:59:53 +00:00
if g_isWindows7 :
try :
import comtypes . client as cc
2022-07-05 02:04:28 +01:00
2021-07-27 16:00:09 +01:00
tlb_fp = open ( TASKBAR_LIB_PATH , ' wb ' )
2021-03-20 01:59:53 +00:00
tlb_fp . write ( base64 . b64decode ( TASKBAR_LIB ) )
tlb_fp . close ( )
del_tlb = True
2022-07-05 02:04:28 +01:00
2021-12-01 12:18:31 +00:00
g_tlb = cc . GetModule ( TASKBAR_LIB_PATH )
2022-07-05 02:04:28 +01:00
2021-03-20 01:59:53 +00:00
g_taskbar = cc . CreateObject ( ' { 56FDF344-FD6D-11D0-958A-006097C9A090} ' , interface = g_tlb . ITaskbarList3 )
g_taskbar . HrInit ( )
except :
2023-06-03 16:27:24 +01:00
utilsLogException ( traceback . format_exc ( ) )
2022-07-05 02:04:28 +01:00
2023-05-24 20:05:34 +01:00
if del_tlb :
os . remove ( TASKBAR_LIB_PATH )
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
# Create root Tkinter object.
2023-05-24 20:05:34 +01:00
g_tkRoot = tk . Tk ( className = SCRIPT_TITLE )
2021-06-04 01:19:19 +01:00
g_tkRoot . title ( SCRIPT_TITLE )
2021-03-20 01:59:53 +00:00
g_tkRoot . protocol ( ' WM_DELETE_WINDOW ' , uiHandleExitProtocol )
g_tkRoot . resizable ( False , False )
2022-07-05 02:04:28 +01:00
2021-03-20 01:59:53 +00:00
# Set window icon.
try :
2023-05-24 20:05:34 +01:00
icon_image = tk . PhotoImage ( data = base64 . b64decode ( APP_ICON ) )
2021-03-20 01:59:53 +00:00
g_tkRoot . wm_iconphoto ( True , icon_image )
except :
2023-06-03 16:27:24 +01:00
utilsLogException ( traceback . format_exc ( ) )
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
# Get screen resolution.
screen_width_px = g_tkRoot . winfo_screenwidth ( )
screen_height_px = g_tkRoot . winfo_screenheight ( )
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
# Get pixel density (DPI).
screen_dpi = round ( g_tkRoot . winfo_fpixels ( ' 1i ' ) )
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
# Update scaling factor (if needed).
2023-05-24 20:05:34 +01:00
if g_isWindowsVista and dpi_aware :
SCALE = ( float ( screen_dpi ) / WINDOWS_SCALING_FACTOR )
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
# Determine window size.
window_width_px = uiScaleMeasure ( WINDOW_WIDTH )
window_height_px = uiScaleMeasure ( WINDOW_HEIGHT )
2022-07-05 02:04:28 +01:00
2023-05-24 20:05:34 +01:00
# Retrieve and configure the default font.
default_font = font . nametofont ( ' TkDefaultFont ' )
default_font_family = ( ' Segoe UI ' if g_isWindows else ' sans-serif ' )
default_font_size = ( - 12 if g_isWindows else - 10 ) # Measured in pixels. Reference: https://docs.python.org/3/library/tkinter.font.html
default_font . configure ( family = default_font_family , size = uiScaleMeasure ( default_font_size ) , weight = font . NORMAL )
""" print(screen_width_px, screen_height_px)
print ( screen_dpi )
print ( window_width_px , window_height_px )
print ( default_font . cget ( ' family ' ) , default_font . cget ( ' size ' ) ) """
2021-03-15 15:31:52 +00:00
# Center window.
pos_hor = int ( ( screen_width_px / 2 ) - ( window_width_px / 2 ) )
pos_ver = int ( ( screen_height_px / 2 ) - ( window_height_px / 2 ) )
2023-05-24 20:05:34 +01:00
g_tkRoot . geometry ( f ' { window_width_px } x { window_height_px } + { pos_hor } + { pos_ver } ' )
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
# Create canvas and fill it with window elements.
g_tkCanvas = tk . Canvas ( g_tkRoot , width = window_width_px , height = window_height_px )
g_tkCanvas . pack ( )
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
g_tkCanvas . create_text ( uiScaleMeasure ( 60 ) , uiScaleMeasure ( 30 ) , text = ' Output directory: ' , anchor = tk . CENTER )
2022-07-05 02:04:28 +01:00
2023-05-24 20:05:34 +01:00
g_tkDirText = tk . Text ( g_tkRoot , height = 1 , width = 45 , font = default_font , wrap = ' none ' , state = ' disabled ' , bg = ' #F0F0F0 ' )
2021-06-04 01:19:19 +01:00
uiUpdateDirectoryField ( g_outputDir )
2021-03-15 15:31:52 +00:00
g_tkCanvas . create_window ( uiScaleMeasure ( 260 ) , uiScaleMeasure ( 30 ) , window = g_tkDirText , anchor = tk . CENTER )
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
g_tkChooseDirButton = tk . Button ( g_tkRoot , text = ' Choose ' , width = 10 , command = uiChooseDirectory )
g_tkCanvas . create_window ( uiScaleMeasure ( 450 ) , uiScaleMeasure ( 30 ) , window = g_tkChooseDirButton , anchor = tk . CENTER )
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
g_tkServerButton = tk . Button ( g_tkRoot , text = ' Start server ' , width = 15 , command = uiStartServer )
2023-05-24 20:05:34 +01:00
g_tkCanvas . create_window ( uiScaleMeasure ( int ( WINDOW_WIDTH / 2 ) ) , uiScaleMeasure ( 70 ) , window = g_tkServerButton , anchor = tk . CENTER )
2022-07-05 02:04:28 +01:00
2023-05-24 20:05:34 +01:00
g_tkTipMessage = g_tkCanvas . create_text ( uiScaleMeasure ( int ( WINDOW_WIDTH / 2 ) ) , uiScaleMeasure ( 100 ) , anchor = tk . CENTER )
2021-03-15 15:31:52 +00:00
g_tkCanvas . itemconfigure ( g_tkTipMessage , state = ' hidden ' , text = ' ' )
2022-07-05 02:04:28 +01:00
2023-05-24 20:05:34 +01:00
g_tkScrolledTextLog = scrolledtext . ScrolledText ( g_tkRoot , height = 20 , width = 65 , font = default_font , wrap = tk . WORD , state = ' disabled ' )
2021-03-15 15:31:52 +00:00
g_tkScrolledTextLog . tag_config ( ' DEBUG ' , foreground = ' gray ' )
2021-03-17 21:19:44 +00:00
g_tkScrolledTextLog . tag_config ( ' INFO ' , foreground = ' black ' )
2021-03-15 15:31:52 +00:00
g_tkScrolledTextLog . tag_config ( ' WARNING ' , foreground = ' orange ' )
g_tkScrolledTextLog . tag_config ( ' ERROR ' , foreground = ' red ' )
2023-05-24 20:05:34 +01:00
g_tkScrolledTextLog . tag_config ( ' CRITICAL ' , foreground = ' red ' , underline = True )
g_tkCanvas . create_window ( uiScaleMeasure ( int ( WINDOW_WIDTH / 2 ) ) , uiScaleMeasure ( 280 ) , window = g_tkScrolledTextLog , anchor = tk . CENTER )
2022-07-05 02:04:28 +01:00
2021-06-04 01:19:19 +01:00
g_tkCanvas . create_text ( uiScaleMeasure ( 5 ) , uiScaleMeasure ( WINDOW_HEIGHT - 10 ) , text = COPYRIGHT_TEXT , anchor = tk . W )
2022-07-05 02:04:28 +01:00
2023-06-03 16:27:24 +01:00
g_logLevelIntVar = tk . IntVar ( )
g_tkVerboseCheckbox = tk . Checkbutton ( g_tkRoot , text = ' Verbose output ' , variable = g_logLevelIntVar , onvalue = logging . DEBUG , offvalue = logging . INFO , command = uiHandleVerboseCheckbox )
g_tkCanvas . create_window ( uiScaleMeasure ( WINDOW_WIDTH - 55 ) , uiScaleMeasure ( WINDOW_HEIGHT - 10 ) , window = g_tkVerboseCheckbox , anchor = tk . CENTER )
2021-03-17 17:25:30 +00:00
# Initialize console logger.
2021-03-15 15:31:52 +00:00
console = LogConsole ( g_tkScrolledTextLog )
2022-07-05 02:04:28 +01:00
2021-03-20 01:59:53 +00:00
# Initialize progress bar window object.
2024-08-26 10:17:23 +01:00
bar_format = ' {desc} \n \n {percentage:.2f} % - {n:.2f} / {total:.2f} {unit} \n Elapsed time: {elapsed} . Remaining time: {remaining} . \n Speed: __custom_rate_fmt__. '
2021-03-17 21:19:44 +00:00
g_progressBarWindow = ProgressBarWindow ( bar_format , g_tkRoot , ' File transfer ' , False , uiHandleExitProtocolStub )
2022-07-05 02:04:28 +01:00
2021-03-15 15:31:52 +00:00
# Enter Tkinter main loop.
2021-03-20 01:59:53 +00:00
g_tkRoot . lift ( )
2021-03-15 15:31:52 +00:00
g_tkRoot . mainloop ( )
2023-05-24 20:05:34 +01:00
def cliInitialize ( ) - > None :
2023-11-26 21:10:42 +00:00
global g_progressBarWindow , g_outputDir
2022-07-05 02:04:28 +01:00
2023-11-26 21:10:42 +00:00
assert g_logger is not None
2023-05-24 20:05:34 +01:00
2021-06-04 01:19:19 +01:00
# Initialize console logger.
console = LogConsole ( )
2022-07-05 02:04:28 +01:00
2021-06-04 01:19:19 +01:00
# Initialize progress bar window object.
2024-08-26 10:17:23 +01:00
bar_format = ' {percentage:.2f} % | {bar} | {n:.2f} / {total:.2f} {unit} [ {elapsed} < {remaining} , __custom_rate_fmt__] '
2021-06-04 01:19:19 +01:00
g_progressBarWindow = ProgressBarWindow ( bar_format )
2022-07-05 02:04:28 +01:00
2021-06-04 01:19:19 +01:00
# Print info.
2023-11-26 21:10:42 +00:00
g_logger . info ( f ' \n { SCRIPT_TITLE } . { COPYRIGHT_TEXT } . ' )
g_logger . info ( f ' Output directory: " { g_outputDir } " . \n ' )
# Unconditionally enable 32-bit paths on Windows.
if g_isWindows :
g_outputDir = ' \\ \\ ? \\ ' + g_outputDir
2022-07-05 02:04:28 +01:00
2021-06-04 01:19:19 +01:00
# Start USB command handler directly.
usbCommandHandler ( )
2023-05-24 20:05:34 +01:00
def main ( ) - > int :
2021-06-04 01:19:19 +01:00
global g_cliMode , g_outputDir , g_osType , g_osVersion , g_isWindows , g_isWindowsVista , g_isWindows7 , g_logger
2022-07-05 02:04:28 +01:00
2021-03-20 01:59:53 +00:00
# Disable warnings.
warnings . filterwarnings ( " ignore " )
2022-07-05 02:04:28 +01:00
2021-06-04 01:19:19 +01:00
# Parse command line arguments.
2023-11-26 21:10:42 +00:00
parser = ArgumentParser ( description = f ' { SCRIPT_TITLE } . { COPYRIGHT_TEXT } . ' )
2023-06-03 16:27:24 +01:00
parser . add_argument ( ' -c ' , ' --cli ' , required = False , action = ' store_true ' , default = False , help = ' Start the script in CLI mode. ' )
2023-11-26 21:10:42 +00:00
parser . add_argument ( ' -o ' , ' --outdir ' , required = False , type = str , metavar = ' DIR ' , help = f ' Path to output directory. Defaults to " { DEFAULT_DIR } " . ' )
2023-06-03 16:27:24 +01:00
parser . add_argument ( ' -v ' , ' --verbose ' , required = False , action = ' store_true ' , default = False , help = ' Enable verbose output. ' )
2021-06-04 01:19:19 +01:00
args = parser . parse_args ( )
2022-07-05 02:04:28 +01:00
2021-06-04 01:19:19 +01:00
# Update global flags.
g_cliMode = args . cli
g_outputDir = utilsGetPath ( args . outdir , DEFAULT_DIR , False , True )
2022-07-05 02:04:28 +01:00
2021-03-20 01:59:53 +00:00
# Get OS information.
g_osType = platform . system ( )
g_osVersion = platform . version ( )
2022-07-05 02:04:28 +01:00
2021-03-20 01:59:53 +00:00
# Get Windows information.
g_isWindows = ( g_osType == ' Windows ' )
g_isWindowsVista = g_isWindows7 = False
if g_isWindows :
win_ver = g_osVersion . split ( ' . ' )
win_ver_major = int ( win_ver [ 0 ] )
win_ver_minor = int ( win_ver [ 1 ] )
2022-07-05 02:04:28 +01:00
2021-03-20 01:59:53 +00:00
g_isWindowsVista = ( win_ver_major > = 6 )
g_isWindows7 = ( True if ( win_ver_major > 6 ) else ( win_ver_major == 6 and win_ver_minor > 0 ) )
2022-07-05 02:04:28 +01:00
2021-06-04 01:19:19 +01:00
# Setup logging mechanism.
2023-06-03 16:27:24 +01:00
logging . basicConfig ( level = ( logging . DEBUG if args . verbose else logging . INFO ) )
2021-06-04 01:19:19 +01:00
g_logger = logging . getLogger ( )
if len ( g_logger . handlers ) :
2023-06-03 16:27:24 +01:00
# Remove stderr output handler from logger. We'll control standard output on our own.
2021-06-04 01:19:19 +01:00
log_stderr = g_logger . handlers [ 0 ]
g_logger . removeHandler ( log_stderr )
2022-07-05 02:04:28 +01:00
2021-06-04 01:19:19 +01:00
if g_cliMode :
# Initialize CLI.
cliInitialize ( )
else :
# Initialize UI.
uiInitialize ( )
2021-03-20 01:59:53 +00:00
2023-05-24 20:05:34 +01:00
return 0
2021-03-15 15:31:52 +00:00
if __name__ == " __main__ " :
2023-05-24 20:05:34 +01:00
ret : int = 1
2021-03-15 15:31:52 +00:00
try :
2023-05-24 20:05:34 +01:00
ret = main ( )
2021-03-15 15:31:52 +00:00
except KeyboardInterrupt :
2023-05-24 20:05:34 +01:00
time . sleep ( 0.2 )
print ( ' \n Script interrupted. ' )
2023-11-26 21:10:42 +00:00
except :
2023-06-03 16:27:24 +01:00
utilsLogException ( traceback . format_exc ( ) )
2023-05-24 20:05:34 +01:00
try :
sys . exit ( ret )
except SystemExit :
os . _exit ( ret )