diff --git a/.github/FlashGBX_Ubuntu.png b/.github/FlashGBX_Ubuntu.png index da1c6b1..8f8d7e0 100644 Binary files a/.github/FlashGBX_Ubuntu.png and b/.github/FlashGBX_Ubuntu.png differ diff --git a/.github/FlashGBX_Windows.png b/.github/FlashGBX_Windows.png index 192077c..469eeb4 100644 Binary files a/.github/FlashGBX_Windows.png and b/.github/FlashGBX_Windows.png differ diff --git a/.github/FlashGBX_macOS.png b/.github/FlashGBX_macOS.png index b204336..da1f797 100644 Binary files a/.github/FlashGBX_macOS.png and b/.github/FlashGBX_macOS.png differ diff --git a/FlashGBX/DataTransfer.py b/FlashGBX/DataTransfer.py index 9076eb1..03914e0 100644 --- a/FlashGBX/DataTransfer.py +++ b/FlashGBX/DataTransfer.py @@ -1,16 +1,16 @@ # -*- coding: utf-8 -*- # UTF-8 import sys, traceback -from PySide2.QtCore import QThread, Signal +import PySide2 -class DataTransfer(QThread): +class DataTransfer(PySide2.QtCore.QThread): CONFIG = None FINISHED = False - updateProgress = Signal(object) + updateProgress = PySide2.QtCore.Signal(object) def __init__(self, config=None): - QThread.__init__(self) + PySide2.QtCore.QThread.__init__(self) if config is not None: self.CONFIG = config self.FINISHED = False diff --git a/FlashGBX/FlashGBX.py b/FlashGBX/FlashGBX.py index 9e7610b..94eaaee 100644 --- a/FlashGBX/FlashGBX.py +++ b/FlashGBX/FlashGBX.py @@ -1,1867 +1,192 @@ # -*- coding: utf-8 -*- # UTF-8 -import sys, threading, os, glob, time, re, json, platform, subprocess, zlib, argparse, math, struct, statistics, requests, webbrowser, pkg_resources -from PySide2 import QtCore, QtWidgets, QtGui -from zipfile import * -from datetime import datetime -from .RomFileDMG import * -from .RomFileAGB import * -from .PocketCamera import * -from . import hw_GBxCartRW -hw_devices = [hw_GBxCartRW] +import sys, os, glob, re, json, zlib, argparse, zipfile, traceback, platform, datetime +from . import Util -APPNAME = "FlashGBX" -VERSION_PEP440 = "1.3" -VERSION = "v{:s}".format(VERSION_PEP440) - -class FlashGBX(QtWidgets.QWidget): - global APPNAME, VERSION, VERSION_PEP440 +def ReadConfigFiles(args): + reset = args['argparsed'].reset + settings = Util.IniSettings(ini_file=args["config_path"] + "/settings.ini") + config_version = settings.value("ConfigVersion") + if not os.path.exists(args["config_path"]): os.makedirs(args["config_path"]) + fc_files = glob.glob("{0:s}/fc_*.txt".format(args["config_path"])) + if config_version is not None and len(fc_files) == 0: + print("No flash cartridge type configuration files found in {:s}. Resetting configuration...".format(args["config_path"])) + settings.clear() + os.rename(args["config_path"] + "/settings.ini", args["config_path"] + "/settings.ini_" + datetime.datetime.now().strftime("%Y%m%d%H%M%S") + ".bak") + config_version = False # extracts the config.zip again + elif reset: + settings.clear() + print("All configuration has been reset.") - AGB_Header_ROM_Sizes = [ "4 MB", "8 MB", "16 MB", "32 MB" ] - AGB_Header_ROM_Sizes_Map = [ 0x400000, 0x800000, 0x1000000, 0x2000000 ] - AGB_Header_Save_Types = [ "None", "4K EEPROM (512 Bytes)", "64K EEPROM (8 KB)", "256K SRAM (32 KB)", "512K SRAM (64 KB)", "1M SRAM (128 KB)", "512K FLASH (64 KB)", "1M FLASH (128 KB)" ] - AGB_Global_CRC32 = 0 + settings.setValue("ConfigVersion", Util.VERSION) + return (config_version, fc_files) + +def LoadConfig(args): + app_path = args['app_path'] + config_path = args['config_path'] + ret = [] + flashcarts = { "DMG":{}, "AGB":{} } - DMG_Header_Features = { 0x00:'ROM ONLY', 0x01:'MBC1', 0x02:'MBC1+RAM', 0x03:'MBC1+RAM+BATTERY', 0x05:'MBC2', 0x06:'MBC2+BATTERY', 0x08:'ROM+RAM', 0x09:'ROM+RAM+BATTERY', 0x0B:'MMM01', 0x0C:'MMM01+RAM', 0x0D:'MMM01+RAM+BATTERY', 0x0F:'MBC3+TIMER+BATTERY', 0x10:'MBC3+TIMER+RAM+BATTERY', 0x11:'MBC3', 0x12:'MBC3+RAM', 0x13:'MBC3+RAM+BATTERY', 0x15:'MBC4', 0x16:'MBC4+RAM', 0x17:'MBC4+RAM+BATTERY', 0x19:'MBC5', 0x1A:'MBC5+RAM', 0x1B:'MBC5+RAM+BATTERY', 0x1C:'MBC5+RUMBLE', 0x1D:'MBC5+RUMBLE+RAM', 0x1E:'MBC5+RUMBLE+RAM+BATTERY', 0x20:'MBC6', 0x22:'MBC7+RAM+BATTERY', 0x55:'Game Genie', 0x56:'Game Genie v3.0', 0xFC:'POCKET CAMERA', 0xFD:'BANDAI TAMA5', 0xFE:'HuC3', 0xFF:'HuC1+RAM+BATTERY' } - DMG_Header_Features_MBC = [ 0, 1, 1, 1, 2, 2, 0, 0, 0, 0, 0, 3, 3, 3, 3, 3, 4, 4, 4, 5, 5, 5, 5, 5, 5, 6, 7, 0, 0, 0, 0, 0, 0 ] - DMG_Header_ROM_Sizes = [ "32 KB", "64 KB", "128 KB", "256 KB", "512 KB", "1 MB", "1.1 MB", "1.2 MB", "1.5 MB", "2 MB", "4 MB", "8 MB" ] - DMG_Header_ROM_Sizes_Map = [ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x52, 0x53, 0x54, 0x06, 0x07, 0x08 ] - DMG_Header_ROM_Sizes_Flasher_Map = [ 2, 4, 8, 16, 32, 64, 72, 80, 96, 128, 256, 512 ] # Number of ROM banks - DMG_Header_RAM_Sizes = [ "None", "4K SRAM (512 Bytes)", "16K SRAM (2 KB)", "64K SRAM (8 KB)", "256K SRAM (32 KB)", "512K SRAM (64 KB)", "1M SRAM (128 KB)" ] - DMG_Header_RAM_Sizes_Map = [ 0x00, 0x01, 0x01, 0x02, 0x03, 0x05, 0x04 ] - DMG_Header_RAM_Sizes_Flasher_Map = [ 0, 0x200, 0x800, 0x2000, 0x8000, 0x10000, 0x20000 ] # RAM size in bytes - DMG_Header_SGB = { 0x00:'No support', 0x03:'Supported' } - DMG_Header_CGB = { 0x00:'No support', 0x80:'Supported', 0xC0:'Required' } - - CONN = None - SETTINGS = None - DEVICES = {} - FLASHCARTS = { "DMG":{}, "AGB":{} } - CONFIG_PATH = "" - TBPROG = None # Windows 7+ Taskbar Progress Bar - PROGRESS = {} - MUTEX = threading.Lock() - - def __init__(self, args): - app_path = args['app_path'] - QtWidgets.QWidget.__init__(self) - self.setStyleSheet("QMessageBox { messagebox-text-interaction-flags: 5; }") - self.setWindowIcon(QtGui.QIcon(app_path + "/res/icon.ico")) - self.setWindowTitle("{:s} {:s}".format(APPNAME, VERSION)) - if hasattr(QtGui, "Qt"): - self.setWindowFlags(self.windowFlags() | QtGui.Qt.MSWindowsFixedSizeDialogHint) + # Settings and Config + (config_version, fc_files) = ReadConfigFiles(args=args) + if config_version != Util.VERSION: + # Rename old files that have since been replaced/renamed/merged + deprecated_files = [ "fc_AGB_M36L0R705.txt", "fc_AGB_TEST.txt", "fc_DMG_TEST.txt", "fc_AGB_Nintendo_E201850.txt", "fc_AGB_Nintendo_E201868.txt", "config.ini", "fc_DMG_MX29LV320ABTC.txt" ] + for file in deprecated_files: + if os.path.exists(config_path + "/" + file): + os.rename(config_path + "/" + file, config_path + "/" + file + "_" + datetime.datetime.now().strftime("%Y%m%d%H%M%S") + ".bak") - # Create the QtWidgets.QVBoxLayout that lays out the whole form - self.layout = QtWidgets.QGridLayout() - self.layout.setSizeConstraint(QtWidgets.QLayout.SetFixedSize) - self.layout_left = QtWidgets.QVBoxLayout() - self.layout_right = QtWidgets.QVBoxLayout() - self.layout.setContentsMargins(-1, 8, -1, 8) - - # Cartridge Information GroupBox - self.grpDMGCartridgeInfo = self.GuiCreateGroupBoxDMGCartInfo() - self.grpAGBCartridgeInfo = self.GuiCreateGroupBoxAGBCartInfo() - self.grpAGBCartridgeInfo.setVisible(False) - self.layout_left.addWidget(self.grpDMGCartridgeInfo) - self.layout_left.addWidget(self.grpAGBCartridgeInfo) - - # Actions - self.grpActions = QtWidgets.QGroupBox("Options") - self.grpActionsLayout = QtWidgets.QVBoxLayout() - self.grpActionsLayout.setContentsMargins(-1, 3, -1, -1) - - rowActionsMode = QtWidgets.QHBoxLayout() - self.lblMode = QtWidgets.QLabel("Mode: ") - rowActionsMode.addWidget(self.lblMode) - self.optDMG = QtWidgets.QRadioButton("&Game Boy") - self.connect(self.optDMG, QtCore.SIGNAL("clicked()"), self.SetMode) - self.optAGB = QtWidgets.QRadioButton("Game Boy &Advance") - self.connect(self.optAGB, QtCore.SIGNAL("clicked()"), self.SetMode) - rowActionsMode.addWidget(self.optDMG) - rowActionsMode.addWidget(self.optAGB) - - rowActionsGeneral1 = QtWidgets.QHBoxLayout() - self.btnHeaderRefresh = QtWidgets.QPushButton("Read &Information") - self.btnHeaderRefresh.setStyleSheet("min-height: 17px;") - self.connect(self.btnHeaderRefresh, QtCore.SIGNAL("clicked()"), self.ReadCartridge) - rowActionsGeneral1.addWidget(self.btnHeaderRefresh) - - rowActionsGeneral2 = QtWidgets.QHBoxLayout() - self.btnBackupROM = QtWidgets.QPushButton("Backup &ROM") - self.btnBackupROM.setStyleSheet("min-height: 17px;") - self.connect(self.btnBackupROM, QtCore.SIGNAL("clicked()"), self.BackupROM) - rowActionsGeneral2.addWidget(self.btnBackupROM) - self.btnBackupRAM = QtWidgets.QPushButton("Backup &Save Data") - self.btnBackupRAM.setStyleSheet("min-height: 17px;") - self.connect(self.btnBackupRAM, QtCore.SIGNAL("clicked()"), self.BackupRAM) - rowActionsGeneral2.addWidget(self.btnBackupRAM) - - self.cmbDMGCartridgeTypeResult.currentIndexChanged.connect(self.CartridgeTypeChanged) - - rowActionsGeneral3 = QtWidgets.QHBoxLayout() - self.btnFlashROM = QtWidgets.QPushButton("&Flash ROM") - self.btnFlashROM.setStyleSheet("min-height: 17px;") - self.connect(self.btnFlashROM, QtCore.SIGNAL("clicked()"), self.FlashROM) - rowActionsGeneral3.addWidget(self.btnFlashROM) - self.btnRestoreRAM = QtWidgets.QPushButton("Writ&e Save Data") - self.mnuRestoreRAM = QtWidgets.QMenu() - self.mnuRestoreRAM.addAction("&Restore from save data file", self.WriteRAM) - self.mnuRestoreRAM.addAction("&Erase cartridge save data", lambda: self.WriteRAM(erase=True)) - self.btnRestoreRAM.setMenu(self.mnuRestoreRAM) - self.btnRestoreRAM.setStyleSheet("min-height: 17px;") - rowActionsGeneral3.addWidget(self.btnRestoreRAM) - - self.grpActionsLayout.setSpacing(4) - self.grpActionsLayout.addLayout(rowActionsMode) - self.grpActionsLayout.addLayout(rowActionsGeneral1) - self.grpActionsLayout.addLayout(rowActionsGeneral2) - self.grpActionsLayout.addLayout(rowActionsGeneral3) - self.grpActions.setLayout(self.grpActionsLayout) - - self.layout_right.addWidget(self.grpActions) - - # Transfer Status - self.grpStatus = QtWidgets.QGroupBox("Transfer Status") - grpStatusLayout = QtWidgets.QVBoxLayout() - grpStatusLayout.setContentsMargins(-1, 3, -1, -1) - - rowStatus1a = QtWidgets.QHBoxLayout() - self.lblStatus1a = QtWidgets.QLabel("Data transferred:") - rowStatus1a.addWidget(self.lblStatus1a) - self.lblStatus1aResult = QtWidgets.QLabel("–") - rowStatus1a.addWidget(self.lblStatus1aResult) - grpStatusLayout.addLayout(rowStatus1a) - rowStatus2a = QtWidgets.QHBoxLayout() - self.lblStatus2a = QtWidgets.QLabel("Transfer rate:") - rowStatus2a.addWidget(self.lblStatus2a) - self.lblStatus2aResult = QtWidgets.QLabel("–") - rowStatus2a.addWidget(self.lblStatus2aResult) - grpStatusLayout.addLayout(rowStatus2a) - rowStatus3a = QtWidgets.QHBoxLayout() - self.lblStatus3a = QtWidgets.QLabel("Time elapsed:") - rowStatus3a.addWidget(self.lblStatus3a) - self.lblStatus3aResult = QtWidgets.QLabel("–") - rowStatus3a.addWidget(self.lblStatus3aResult) - grpStatusLayout.addLayout(rowStatus3a) - rowStatus4a = QtWidgets.QHBoxLayout() - self.lblStatus4a = QtWidgets.QLabel("Ready.") - rowStatus4a.addWidget(self.lblStatus4a) - self.lblStatus4aResult = QtWidgets.QLabel("") - rowStatus4a.addWidget(self.lblStatus4aResult) - grpStatusLayout.addLayout(rowStatus4a) - - rowStatus2 = QtWidgets.QHBoxLayout() - self.prgStatus = QtWidgets.QProgressBar() - self.SetProgressBars(min=0, max=1, value=0) - rowStatus2.addWidget(self.prgStatus) - btnText = "Stop" - self.btnCancel = QtWidgets.QPushButton(btnText) - self.btnCancel.setEnabled(False) - btnWidth = self.btnCancel.fontMetrics().boundingRect(btnText).width() + 15 - if platform.system() == "Darwin": btnWidth += 12 - self.btnCancel.setMaximumWidth(btnWidth) - self.connect(self.btnCancel, QtCore.SIGNAL("clicked()"), self.AbortOperation) - rowStatus2.addWidget(self.btnCancel) - - grpStatusLayout.addLayout(rowStatus2) - self.grpStatus.setLayout(grpStatusLayout) - - self.layout_right.addWidget(self.grpStatus) - - self.layout.addLayout(self.layout_left, 0, 0) - self.layout.addLayout(self.layout_right, 0, 1) - - # List devices - self.layout_devices = QtWidgets.QHBoxLayout() - self.lblDevice = QtWidgets.QLabel() - self.cmbDevice = QtWidgets.QComboBox() - self.cmbDevice.setStyleSheet("QComboBox { border: 0; margin: 0; padding: 0; max-width: 0px; }"); - self.layout_devices.addWidget(self.lblDevice) - self.layout_devices.addWidget(self.cmbDevice) - self.layout_devices.addStretch() - - self.btnCameraViewer = QtWidgets.QPushButton("GB &Camera") - self.connect(self.btnCameraViewer, QtCore.SIGNAL("clicked()"), self.ShowPocketCameraWindow) - #rowActionsGeneral1.addWidget(self.btnCameraViewer) - - btnText = "C&onfig" - self.btnConfig = QtWidgets.QPushButton(btnText) - btnWidth = self.btnConfig.fontMetrics().boundingRect(btnText).width() + 24 - if platform.system() == "Darwin": btnWidth += 12 - self.btnConfig.setMaximumWidth(btnWidth) - self.mnuConfig = QtWidgets.QMenu() - self.mnuConfig.addAction("Check for &updates at application startup", lambda: [ self.SETTINGS.setValue("UpdateCheck", str(self.mnuConfig.actions()[0].isChecked()).lower().replace("true", "enabled").replace("false", "disabled")), self.UpdateCheck() ]) - self.mnuConfig.addAction("&Append date && time to filename of save data backups", lambda: self.SETTINGS.setValue("SaveFileNameAddDateTime", str(self.mnuConfig.actions()[1].isChecked()).lower().replace("true", "enabled").replace("false", "disabled"))) - self.mnuConfig.addAction("Prefer §or erase over full chip erase when both available", lambda: self.SETTINGS.setValue("PreferSectorErase", str(self.mnuConfig.actions()[2].isChecked()).lower().replace("true", "enabled").replace("false", "disabled"))) - self.mnuConfig.addAction("&Verify flash after writing", lambda: self.SETTINGS.setValue("VerifyFlash", str(self.mnuConfig.actions()[3].isChecked()).lower().replace("true", "enabled").replace("false", "disabled"))) - self.mnuConfig.addAction("Use &fast read mode (experimental)", lambda: self.SETTINGS.setValue("FastReadMode", str(self.mnuConfig.actions()[4].isChecked()).lower().replace("true", "enabled").replace("false", "disabled"))) # GBxCart RW - self.mnuConfig.addSeparator() - self.mnuConfig.addAction("Show &configuration directory", self.OpenConfigDir) - self.mnuConfig.actions()[0].setCheckable(True) - self.mnuConfig.actions()[1].setCheckable(True) - self.mnuConfig.actions()[2].setCheckable(True) - self.mnuConfig.actions()[3].setCheckable(True) - self.mnuConfig.actions()[4].setCheckable(True) # GBxCart RW - self.btnConfig.setMenu(self.mnuConfig) - - #self.btnScan = QtWidgets.QPushButton("&Device Scan") - #self.connect(self.btnScan, QtCore.SIGNAL("clicked()"), self.FindDevices) - self.btnConnect = QtWidgets.QPushButton("&Connect") - self.connect(self.btnConnect, QtCore.SIGNAL("clicked()"), self.ConnectDevice) - self.layout_devices.addWidget(self.btnCameraViewer) - self.layout_devices.addWidget(self.btnConfig) - #self.layout_devices.addWidget(self.btnScan) - self.layout_devices.addWidget(self.btnConnect) - - self.layout.addLayout(self.layout_devices, 1, 0, 1, 0) - - # Disable widgets - self.optAGB.setEnabled(False) - self.optDMG.setEnabled(False) - self.btnHeaderRefresh.setEnabled(False) - self.btnBackupROM.setEnabled(False) - self.btnFlashROM.setEnabled(False) - self.btnBackupRAM.setEnabled(False) - self.btnRestoreRAM.setEnabled(False) - self.btnConnect.setEnabled(False) - self.grpDMGCartridgeInfo.setEnabled(False) - self.grpAGBCartridgeInfo.setEnabled(False) - - # Set the VBox layout as the window's main layout - self.setLayout(self.layout) - - # Read config, find devices and connect - self.InitConfig(args) - - # Show app window first, then do update check - qt_app.processEvents() - QtCore.QTimer.singleShot(1, lambda: [ self.UpdateCheck(), self.FindDevices() ]) - - def InitConfig(self, args): - app_path = args['app_path'] - self.CONFIG_PATH = args['config_path'] - - # Settings and Config - deprecated_files = [ "fc_AGB_M36L0R705.txt", "config.ini" ] - (config_version, fc_files) = self.ReadConfig(reset=args['argparsed'].reset) - if config_version != VERSION: - # Rename old files that have since been replaced/renamed/merged - deprecated_files = [ "fc_AGB_M36L0R705.txt", "fc_AGB_TEST.txt", "fc_DMG_TEST.txt", "fc_AGB_Nintendo_E201850.txt", "fc_AGB_Nintendo_E201868.txt", "config.ini" ] - for file in deprecated_files: - if os.path.exists(self.CONFIG_PATH + "/" + file): - os.rename(self.CONFIG_PATH + "/" + file, self.CONFIG_PATH + "/" + file + "_" + datetime.now().strftime("%Y%m%d%H%M%S") + ".bak") + rf_list = "" + if os.path.exists(app_path + "/res/config.zip"): + with zipfile.ZipFile(app_path + "/res/config.zip") as zip: + for zfile in zip.namelist(): + if os.path.exists(config_path + "/" + zfile): + zfile_crc = zip.getinfo(zfile).CRC + with open(config_path + "/" + zfile, "rb") as ofile: buffer = ofile.read() + ofile_crc = zlib.crc32(buffer) & 0xFFFFFFFF + if zfile_crc == ofile_crc: continue + os.rename(config_path + "/" + zfile, config_path + "/" + zfile + "_" + datetime.datetime.now().strftime("%Y%m%d%H%M%S") + ".bak") + rf_list += zfile + "\n" + zip.extract(zfile, config_path + "/") - rf_list = "" - if os.path.exists(app_path + "/res/config.zip"): - with ZipFile(app_path + "/res/config.zip") as zip: - for zfile in zip.namelist(): - if os.path.exists(self.CONFIG_PATH + "/" + zfile): - zfile_crc = zip.getinfo(zfile).CRC - with open(self.CONFIG_PATH + "/" + zfile, "rb") as ofile: buffer = ofile.read() - ofile_crc = zlib.crc32(buffer) & 0xFFFFFFFF - if zfile_crc == ofile_crc: continue - os.rename(self.CONFIG_PATH + "/" + zfile, self.CONFIG_PATH + "/" + zfile + "_" + datetime.now().strftime("%Y%m%d%H%M%S") + ".bak") - rf_list += zfile + "\n" - zip.extract(zfile, self.CONFIG_PATH + "/") - - if rf_list != "": QtWidgets.QMessageBox.information(self, "{:s} {:s}".format(APPNAME, VERSION), "The application was recently updated and some config files have been updated as well. You will find backup copies of them in your configuration directory.\n\nUpdated files:\n" + rf_list[:-1], QtWidgets.QMessageBox.Ok) - fc_files = glob.glob("{0:s}/fc_*.txt".format(self.CONFIG_PATH)) - else: - print("WARNING: {:s} not found. This is required to load new flash cartridge type configurations after updating.\n".format(app_path + "/res/config.zip")) - - # Read flash cart types - for file in fc_files: - with open(file, encoding='utf-8') as f: - data = f.read() - specs_int = re.sub("(0x[\dA-F]+)", lambda m: str(int(m.group(1), 16)), data) # hex numbers to int numbers, otherwise not valid json - try: - specs = json.loads(specs_int) - except: - print("WARNING: Flash chip config file “{:s}” could not be parsed and needs to be fixed before it can be used.".format(os.path.basename(file))) - continue - for name in specs["names"]: - if not specs["type"] in self.FLASHCARTS: continue # only DMG and AGB are supported right now - self.FLASHCARTS[specs["type"]][name] = specs - - self.mnuConfig.actions()[0].setChecked(self.SETTINGS.value("UpdateCheck") == "enabled") - self.mnuConfig.actions()[1].setChecked(self.SETTINGS.value("SaveFileNameAddDateTime") == "enabled") - self.mnuConfig.actions()[2].setChecked(self.SETTINGS.value("PreferSectorErase") == "enabled") - self.mnuConfig.actions()[3].setChecked(self.SETTINGS.value("VerifyFlash") == "enabled") - self.mnuConfig.actions()[4].setChecked(self.SETTINGS.value("FastReadMode") == "enabled") # GBxCart RW - - def ReadConfig(self, reset=False): - self.SETTINGS = QtCore.QSettings(self.CONFIG_PATH + "/settings.ini", QtCore.QSettings.IniFormat) - config_version = self.SETTINGS.value("ConfigVersion") - if not os.path.exists(self.CONFIG_PATH): os.makedirs(self.CONFIG_PATH) - fc_files = glob.glob("{0:s}/fc_*.txt".format(self.CONFIG_PATH)) - if config_version is not None and len(fc_files) == 0: - print("FAIL: No flash cartridge type configuration files found in {:s}. Resetting configuration...\n".format(self.CONFIG_PATH)) - self.SETTINGS.clear() - os.rename(self.CONFIG_PATH + "/settings.ini", self.CONFIG_PATH + "/settings.ini_" + datetime.now().strftime("%Y%m%d%H%M%S") + ".bak") - config_version = False # extracts the config.zip again - elif reset: - self.SETTINGS.clear() - print("All configuration has been reset.\n") - - self.SETTINGS.setValue("ConfigVersion", VERSION) - return (config_version, fc_files) + if rf_list != "": + ret.append([1, "The application was recently updated and some flashcart handler files have been updated as well. You will find backup copies of them in your configuration directory.\n\nUpdated files:\n" + rf_list[:-1]]) + fc_files = glob.glob("{0:s}/fc_*.txt".format(config_path)) + else: + ret.append([2, "{:s} not found. This is required to load new flash cartridge type configurations after updating.\n".format(app_path + "/res/config.zip")]) - def GuiCreateGroupBoxDMGCartInfo(self): - self.grpDMGCartridgeInfo = QtWidgets.QGroupBox("Game Boy Cartridge Information") - self.grpDMGCartridgeInfo.setMinimumWidth(280) - group_layout = QtWidgets.QVBoxLayout() - group_layout.setContentsMargins(-1, 5, -1, -1) - - rowHeaderTitle = QtWidgets.QHBoxLayout() - lblHeaderTitle = QtWidgets.QLabel("Game Title/Code:") - lblHeaderTitle.setContentsMargins(0, 1, 0, 1) - rowHeaderTitle.addWidget(lblHeaderTitle) - self.lblHeaderTitleResult = QtWidgets.QLabel("") - rowHeaderTitle.addWidget(self.lblHeaderTitleResult) - group_layout.addLayout(rowHeaderTitle) - - rowHeaderSGB = QtWidgets.QHBoxLayout() - lblHeaderSGB = QtWidgets.QLabel("Super Game Boy:") - lblHeaderSGB.setContentsMargins(0, 1, 0, 1) - rowHeaderSGB.addWidget(lblHeaderSGB) - self.lblHeaderSGBResult = QtWidgets.QLabel("") - rowHeaderSGB.addWidget(self.lblHeaderSGBResult) - group_layout.addLayout(rowHeaderSGB) - - rowHeaderCGB = QtWidgets.QHBoxLayout() - lblHeaderCGB = QtWidgets.QLabel("Game Boy Color:") - lblHeaderCGB.setContentsMargins(0, 1, 0, 1) - rowHeaderCGB.addWidget(lblHeaderCGB) - self.lblHeaderCGBResult = QtWidgets.QLabel("") - rowHeaderCGB.addWidget(self.lblHeaderCGBResult) - group_layout.addLayout(rowHeaderCGB) - - rowHeaderLogoValid = QtWidgets.QHBoxLayout() - lblHeaderLogoValid = QtWidgets.QLabel("Nintendo Logo:") - lblHeaderLogoValid.setContentsMargins(0, 1, 0, 1) - rowHeaderLogoValid.addWidget(lblHeaderLogoValid) - self.lblHeaderLogoValidResult = QtWidgets.QLabel("") - rowHeaderLogoValid.addWidget(self.lblHeaderLogoValidResult) - group_layout.addLayout(rowHeaderLogoValid) - - rowHeaderChecksum = QtWidgets.QHBoxLayout() - lblHeaderChecksum = QtWidgets.QLabel("Header Checksum:") - lblHeaderChecksum.setContentsMargins(0, 1, 0, 1) - rowHeaderChecksum.addWidget(lblHeaderChecksum) - self.lblHeaderChecksumResult = QtWidgets.QLabel("") - rowHeaderChecksum.addWidget(self.lblHeaderChecksumResult) - group_layout.addLayout(rowHeaderChecksum) - - rowHeaderROMChecksum = QtWidgets.QHBoxLayout() - lblHeaderROMChecksum = QtWidgets.QLabel("ROM Checksum:") - lblHeaderROMChecksum.setContentsMargins(0, 1, 0, 1) - rowHeaderROMChecksum.addWidget(lblHeaderROMChecksum) - self.lblHeaderROMChecksumResult = QtWidgets.QLabel("") - rowHeaderROMChecksum.addWidget(self.lblHeaderROMChecksumResult) - group_layout.addLayout(rowHeaderROMChecksum) - - rowChipManufacturer = QtWidgets.QHBoxLayout() - self.lblChipManufacturer = QtWidgets.QLabel("Chip Manufacturer:") - self.lblChipManufacturer.setContentsMargins(0, 1, 0, 1) - rowChipManufacturer.addWidget(self.lblChipManufacturer) - self.lblChipManufacturerResult = QtWidgets.QLabel("") - rowChipManufacturer.addWidget(self.lblChipManufacturerResult) - group_layout.addLayout(rowChipManufacturer) - self.lblChipManufacturer.setVisible(False) - self.lblChipManufacturerResult.setVisible(False) - - rowChipID = QtWidgets.QHBoxLayout() - self.lblChipID = QtWidgets.QLabel("Chip ID:") - self.lblChipID.setContentsMargins(0, 1, 0, 1) - rowChipID.addWidget(self.lblChipID) - self.lblChipIDResult = QtWidgets.QLabel("") - rowChipID.addWidget(self.lblChipIDResult) - group_layout.addLayout(rowChipID) - self.lblChipID.setVisible(False) - self.lblChipIDResult.setVisible(False) - - rowHeaderROMSize = QtWidgets.QHBoxLayout() - lblHeaderROMSize = QtWidgets.QLabel("ROM Size:") - rowHeaderROMSize.addWidget(lblHeaderROMSize) - self.cmbHeaderROMSizeResult = QtWidgets.QComboBox() - self.cmbHeaderROMSizeResult.setStyleSheet("combobox-popup: 0;"); - self.cmbHeaderROMSizeResult.view().setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) - self.cmbHeaderROMSizeResult.addItems(self.DMG_Header_ROM_Sizes) - self.cmbHeaderROMSizeResult.setCurrentIndex(self.cmbHeaderROMSizeResult.count() - 1) - rowHeaderROMSize.addWidget(self.cmbHeaderROMSizeResult) - group_layout.addLayout(rowHeaderROMSize) - - rowHeaderRAMSize = QtWidgets.QHBoxLayout() - lblHeaderRAMSize = QtWidgets.QLabel("Save Type:") - rowHeaderRAMSize.addWidget(lblHeaderRAMSize) - self.cmbHeaderRAMSizeResult = QtWidgets.QComboBox() - self.cmbHeaderRAMSizeResult.setStyleSheet("combobox-popup: 0;"); - self.cmbHeaderRAMSizeResult.view().setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) - self.cmbHeaderRAMSizeResult.addItems(self.DMG_Header_RAM_Sizes) - self.cmbHeaderRAMSizeResult.setCurrentIndex(self.cmbHeaderRAMSizeResult.count() - 1) - rowHeaderRAMSize.addWidget(self.cmbHeaderRAMSizeResult) - group_layout.addLayout(rowHeaderRAMSize) - - rowHeaderFeatures = QtWidgets.QHBoxLayout() - lblHeaderFeatures = QtWidgets.QLabel("Features:") - rowHeaderFeatures.addWidget(lblHeaderFeatures) - self.cmbHeaderFeaturesResult = QtWidgets.QComboBox() - self.cmbHeaderFeaturesResult.setStyleSheet("combobox-popup: 0;"); - self.cmbHeaderFeaturesResult.view().setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) - self.cmbHeaderFeaturesResult.addItems(list(self.DMG_Header_Features.values())) - rowHeaderFeatures.addWidget(self.cmbHeaderFeaturesResult) - group_layout.addLayout(rowHeaderFeatures) - - rowCartridgeType = QtWidgets.QHBoxLayout() - lblCartridgeType = QtWidgets.QLabel("Type:") - rowCartridgeType.addWidget(lblCartridgeType) - self.cmbDMGCartridgeTypeResult = QtWidgets.QComboBox() - self.cmbDMGCartridgeTypeResult.setStyleSheet("max-width: 260px;") - self.cmbDMGCartridgeTypeResult.setStyleSheet("combobox-popup: 0;"); - self.cmbDMGCartridgeTypeResult.view().setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) - rowCartridgeType.addWidget(self.cmbDMGCartridgeTypeResult) - group_layout.addLayout(rowCartridgeType) - - self.grpDMGCartridgeInfo.setLayout(group_layout) - - return self.grpDMGCartridgeInfo - - def GuiCreateGroupBoxAGBCartInfo(self): - self.grpAGBCartridgeInfo = QtWidgets.QGroupBox("Game Boy Advance Cartridge Information") - self.grpAGBCartridgeInfo.setMinimumWidth(280) - group_layout = QtWidgets.QVBoxLayout() - group_layout.setContentsMargins(-1, 5, -1, -1) - - rowAGBHeaderTitle = QtWidgets.QHBoxLayout() - lblAGBHeaderTitle = QtWidgets.QLabel("Game Title:") - lblAGBHeaderTitle.setContentsMargins(0, 1, 0, 1) - rowAGBHeaderTitle.addWidget(lblAGBHeaderTitle) - self.lblAGBHeaderTitleResult = QtWidgets.QLabel("") - rowAGBHeaderTitle.addWidget(self.lblAGBHeaderTitleResult) - group_layout.addLayout(rowAGBHeaderTitle) - - rowAGBHeaderCode = QtWidgets.QHBoxLayout() - lblAGBHeaderCode = QtWidgets.QLabel("Game Code:") - lblAGBHeaderCode.setContentsMargins(0, 1, 0, 1) - rowAGBHeaderCode.addWidget(lblAGBHeaderCode) - self.lblAGBHeaderCodeResult = QtWidgets.QLabel("") - rowAGBHeaderCode.addWidget(self.lblAGBHeaderCodeResult) - group_layout.addLayout(rowAGBHeaderCode) - - rowAGBHeaderVersion = QtWidgets.QHBoxLayout() - lblAGBHeaderVersion = QtWidgets.QLabel("Revision:") - lblAGBHeaderVersion.setContentsMargins(0, 1, 0, 1) - rowAGBHeaderVersion.addWidget(lblAGBHeaderVersion) - self.lblAGBHeaderVersionResult = QtWidgets.QLabel("") - rowAGBHeaderVersion.addWidget(self.lblAGBHeaderVersionResult) - group_layout.addLayout(rowAGBHeaderVersion) - - rowAGBHeaderLogoValid = QtWidgets.QHBoxLayout() - lblAGBHeaderLogoValid = QtWidgets.QLabel("Nintendo Logo:") - lblAGBHeaderLogoValid.setContentsMargins(0, 1, 0, 1) - rowAGBHeaderLogoValid.addWidget(lblAGBHeaderLogoValid) - self.lblAGBHeaderLogoValidResult = QtWidgets.QLabel("") - rowAGBHeaderLogoValid.addWidget(self.lblAGBHeaderLogoValidResult) - group_layout.addLayout(rowAGBHeaderLogoValid) - - rowAGBHeader96h = QtWidgets.QHBoxLayout() - lblAGBHeader96h = QtWidgets.QLabel("Cartridge Identifier:") - lblAGBHeader96h.setContentsMargins(0, 1, 0, 1) - rowAGBHeader96h.addWidget(lblAGBHeader96h) - self.lblAGBHeader96hResult = QtWidgets.QLabel("") - rowAGBHeader96h.addWidget(self.lblAGBHeader96hResult) - group_layout.addLayout(rowAGBHeader96h) - - rowAGBHeaderChecksum = QtWidgets.QHBoxLayout() - lblAGBHeaderChecksum = QtWidgets.QLabel("Header Checksum:") - lblAGBHeaderChecksum.setContentsMargins(0, 1, 0, 1) - rowAGBHeaderChecksum.addWidget(lblAGBHeaderChecksum) - self.lblAGBHeaderChecksumResult = QtWidgets.QLabel("") - rowAGBHeaderChecksum.addWidget(self.lblAGBHeaderChecksumResult) - group_layout.addLayout(rowAGBHeaderChecksum) - - rowAGBHeaderROMChecksum = QtWidgets.QHBoxLayout() - lblAGBHeaderROMChecksum = QtWidgets.QLabel("ROM Checksum:") - lblAGBHeaderROMChecksum.setContentsMargins(0, 1, 0, 1) - rowAGBHeaderROMChecksum.addWidget(lblAGBHeaderROMChecksum) - self.lblAGBHeaderROMChecksumResult = QtWidgets.QLabel("") - rowAGBHeaderROMChecksum.addWidget(self.lblAGBHeaderROMChecksumResult) - group_layout.addLayout(rowAGBHeaderROMChecksum) - - rowAGBHeaderROMSize = QtWidgets.QHBoxLayout() - lblAGBHeaderROMSize = QtWidgets.QLabel("ROM Size:") - rowAGBHeaderROMSize.addWidget(lblAGBHeaderROMSize) - self.cmbAGBHeaderROMSizeResult = QtWidgets.QComboBox() - self.cmbAGBHeaderROMSizeResult.setStyleSheet("combobox-popup: 0;"); - self.cmbAGBHeaderROMSizeResult.view().setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) - self.cmbAGBHeaderROMSizeResult.addItems(self.AGB_Header_ROM_Sizes) - self.cmbAGBHeaderROMSizeResult.setCurrentIndex(self.cmbAGBHeaderROMSizeResult.count() - 1) - rowAGBHeaderROMSize.addWidget(self.cmbAGBHeaderROMSizeResult) - group_layout.addLayout(rowAGBHeaderROMSize) - - rowAGBHeaderRAMSize = QtWidgets.QHBoxLayout() - lblAGBHeaderRAMSize = QtWidgets.QLabel("Save Type:") - rowAGBHeaderRAMSize.addWidget(lblAGBHeaderRAMSize) - self.cmbAGBSaveTypeResult = QtWidgets.QComboBox() - self.cmbAGBSaveTypeResult.setStyleSheet("combobox-popup: 0;"); - self.cmbAGBSaveTypeResult.view().setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) - self.cmbAGBSaveTypeResult.addItems(self.AGB_Header_Save_Types) - self.cmbAGBSaveTypeResult.setCurrentIndex(self.cmbAGBSaveTypeResult.count() - 1) - rowAGBHeaderRAMSize.addWidget(self.cmbAGBSaveTypeResult) - group_layout.addLayout(rowAGBHeaderRAMSize) - - rowAGBCartridgeType = QtWidgets.QHBoxLayout() - lblAGBCartridgeType = QtWidgets.QLabel("Type:") - rowAGBCartridgeType.addWidget(lblAGBCartridgeType) - self.cmbAGBCartridgeTypeResult = QtWidgets.QComboBox() - self.cmbAGBCartridgeTypeResult.setStyleSheet("max-width: 260px;") - self.cmbAGBCartridgeTypeResult.setStyleSheet("combobox-popup: 0;"); - self.cmbAGBCartridgeTypeResult.view().setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) - self.cmbAGBCartridgeTypeResult.currentIndexChanged.connect(self.CartridgeTypeChanged) - rowAGBCartridgeType.addWidget(self.cmbAGBCartridgeTypeResult) - group_layout.addLayout(rowAGBCartridgeType) - - self.grpAGBCartridgeInfo.setLayout(group_layout) - return self.grpAGBCartridgeInfo - - def UpdateCheck(self): - update_check = self.SETTINGS.value("UpdateCheck") - if update_check is None: - answer = QtWidgets.QMessageBox.question(self, "{:s} {:s}".format(APPNAME, VERSION), "Welcome to {:s} {:s} by Lesserkuma!\nWould you like to automatically check for new versions at application startup?".format(APPNAME, VERSION), QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.Yes) - if answer == QtWidgets.QMessageBox.Yes: - self.SETTINGS.setValue("UpdateCheck", "enabled") - self.mnuConfig.actions()[0].setChecked(True) - update_check = "enabled" - else: - self.SETTINGS.setValue("UpdateCheck", "disabled") - - if update_check and update_check.lower() == "enabled": - if ".dev" in VERSION_PEP440: - type = "test " - url = "https://test.pypi.org/pypi/FlashGBX/json" - site = "https://test.pypi.org/project/FlashGBX/" - else: - type = "" - url = "https://pypi.org/pypi/FlashGBX/json" - site = "https://github.com/lesserkuma/FlashGBX" + # Read flash cart types + for file in fc_files: + with open(file, encoding='utf-8') as f: + data = f.read() + specs_int = re.sub("(0x[0-9A-F]+)", lambda m: str(int(m.group(1), 16)), data) # hex numbers to int numbers, otherwise not valid json try: - ret = requests.get(url, allow_redirects=True, timeout=1.5) - except requests.exceptions.ConnectTimeout as e: - print("ERROR: Update check failed due to a connection timeout. Please check your internet connection.", e, sep="\n") - ret = False - except requests.exceptions.ConnectionError as e: - print("ERROR: Update check failed due to a connection error. Please check your network connection.", e, sep="\n") - ret = False - except Exception as e: - print("ERROR: An unexpected error occured while querying the latest version information from PyPI.", e, sep="\n") - ret = False - - if ret is not False and ret.status_code == 200: - ret = ret.content - try: - ret = json.loads(ret) - if 'info' in ret and 'version' in ret['info']: - if pkg_resources.parse_version(ret['info']['version']) == pkg_resources.parse_version(VERSION_PEP440): - print("You are using the latest {:s}version of {:s}.".format(type, APPNAME)) - elif pkg_resources.parse_version(ret['info']['version']) > pkg_resources.parse_version(VERSION_PEP440): - msg_text = "A new {:s}version of {:s} has been released!\nVersion {:s} is now available.".format(type, APPNAME, ret['info']['version']) - print(msg_text) - msgbox = QtWidgets.QMessageBox(parent=self, icon=QtWidgets.QMessageBox.Question, windowTitle="{:s} Update Check".format(APPNAME), text=msg_text) - button_open = msgbox.addButton(" Open &website ", QtWidgets.QMessageBox.ActionRole) - button_cancel = msgbox.addButton("&OK", QtWidgets.QMessageBox.RejectRole) - msgbox.setDefaultButton(button_open) - msgbox.setEscapeButton(button_cancel) - answer = msgbox.exec() - if msgbox.clickedButton() == button_open: - webbrowser.open(site) - else: - print("This version of {:s} ({:s}) seems to be newer than the latest {:s}release ({:s}). Please check for updates manually.".format(APPNAME, VERSION_PEP440, type, ret['info']['version'])) - else: - print("ERROR: Update check failed due to missing version information in JSON data from PyPI.") - except json.decoder.JSONDecodeError: - print("ERROR: Update check failed due to malformed JSON data from PyPI.") - except Exception as e: - print("ERROR: An unexpected error occured while querying the latest version information from PyPI.", e, sep="\n") - elif ret is not False: - print("ERROR: Failed to check for updates (HTTP status {:d}).".format(ret.status_code)) + specs = json.loads(specs_int) + except: + ret.append([2, "The flashchip handler file “{:s}” could not be parsed and needs to be fixed before it can be used.".format(os.path.basename(file))]) + continue + for name in specs["names"]: + if not specs["type"] in flashcarts: continue # only DMG and AGB are supported right now + flashcarts[specs["type"]][name] = specs - def DisconnectDevice(self): - try: - devname = self.CONN.GetFullName() - self.CONN.Close() - print("Disconnected from {:s}".format(devname)) - except: - pass - - self.CONN = None - #self.btnScan.show() - self.optAGB.setEnabled(False) - self.optDMG.setEnabled(False) - self.grpDMGCartridgeInfo.setEnabled(False) - self.grpAGBCartridgeInfo.setEnabled(False) - self.btnCancel.setEnabled(False) - self.btnHeaderRefresh.setEnabled(False) - self.btnBackupROM.setEnabled(False) - self.btnFlashROM.setEnabled(False) - self.btnBackupRAM.setEnabled(False) - self.btnRestoreRAM.setEnabled(False) - self.btnConnect.setText("Connect") - self.lblDevice.setText("Disconnected.") - - def OpenConfigDir(self): - path = 'file://{0:s}'.format(self.CONFIG_PATH) - try: - if platform.system() == "Windows": - os.startfile(path) - elif platform.system() == "Darwin": - subprocess.Popen(["open", path]) - else: - subprocess.Popen(["xdg-open", path]) - except: - QtWidgets.QMessageBox.information(self, "{:s} {:s}".format(APPNAME, VERSION), "Your configuration files are stored in\n" + path, QtWidgets.QMessageBox.Ok) - - def ConnectDevice(self): - if self.CONN is not None: - self.DisconnectDevice() - return True - else: - if self.cmbDevice.count() > 0: - index = self.cmbDevice.currentText() - else: - index = self.lblDevice.text() - - if index not in self.DEVICES: - self.FindDevices(True) - return - - dev = self.DEVICES[index] - ret = dev.Initialize(self.FLASHCARTS) - msg = "" - - if ret is False: - self.CONN = None - if self.cmbDevice.count() == 0: self.lblDevice.setText("No connection.") - return False - - elif isinstance(ret, list): - for i in range(0, len(ret)): - status = ret[i][0] - text = ret[i][1] - if status == 0: - msg += text + "\n" - elif status == 1: - msgbox = QtWidgets.QMessageBox(parent=self, icon=QtWidgets.QMessageBox.Information, windowTitle="{:s} {:s}".format(APPNAME, VERSION), text=text, standardButtons=QtWidgets.QMessageBox.Ok) - if not '\n' in text: msgbox.setTextFormat(QtCore.Qt.RichText) - msgbox.exec() - elif status == 2: - msgbox = QtWidgets.QMessageBox(parent=self, icon=QtWidgets.QMessageBox.Warning, windowTitle="{:s} {:s}".format(APPNAME, VERSION), text=text, standardButtons=QtWidgets.QMessageBox.Ok) - if not '\n' in text: msgbox.setTextFormat(QtCore.Qt.RichText) - msgbox.exec() - elif status == 3: - msgbox = QtWidgets.QMessageBox(parent=self, icon=QtWidgets.QMessageBox.Critical, windowTitle="{:s} {:s}".format(APPNAME, VERSION), text=text, standardButtons=QtWidgets.QMessageBox.Ok) - if not '\n' in text: msgbox.setTextFormat(QtCore.Qt.RichText) - msgbox.exec() - self.CONN = None - return False - - if dev.IsConnected(): - qt_app.processEvents() - self.CONN = dev - #self.btnScan.hide() - self.optDMG.setAutoExclusive(False) - self.optAGB.setAutoExclusive(False) - if "DMG" in self.CONN.GetSupprtedModes(): - self.optDMG.setEnabled(True) - self.optDMG.setChecked(False) - if "AGB" in self.CONN.GetSupprtedModes(): - self.optAGB.setEnabled(True) - self.optAGB.setChecked(False) - self.optAGB.setAutoExclusive(True) - self.optDMG.setAutoExclusive(True) - self.btnConnect.setText("&Disconnect") - self.cmbDevice.setStyleSheet("QComboBox { border: 0; margin: 0; padding: 0; max-width: 0px; }"); - self.lblDevice.setText(dev.GetFullName()) - print("\nConnected to " + dev.GetFullName()) - self.grpDMGCartridgeInfo.setEnabled(True) - self.grpAGBCartridgeInfo.setEnabled(True) - self.grpActions.setEnabled(True) - self.btnCancel.setEnabled(False) - self.SetProgressBars(min=0, max=1, value=0) - - if self.CONN.GetMode() == "DMG": - self.cmbDMGCartridgeTypeResult.clear() - self.cmbDMGCartridgeTypeResult.addItems(self.CONN.GetSupportedCartridgesDMG()[0]) - self.grpAGBCartridgeInfo.setVisible(False) - self.grpDMGCartridgeInfo.setVisible(True) - elif self.CONN.GetMode() == "AGB": - self.cmbAGBCartridgeTypeResult.clear() - self.cmbAGBCartridgeTypeResult.addItems(self.CONN.GetSupportedCartridgesAGB()[0]) - self.grpDMGCartridgeInfo.setVisible(False) - self.grpAGBCartridgeInfo.setVisible(True) - - print(msg, end="") - return True - return False - - def FindDevices(self, connectToFirst=False): - if self.CONN is not None: - self.DisconnectDevice() - self.lblDevice.setText("Searching...") - #self.btnScan.setEnabled(False) - self.btnConnect.setEnabled(False) - qt_app.processEvents() - time.sleep(0.05) - - global hw_devices - for hw_device in hw_devices: - dev = hw_device.GbxDevice() - ret = dev.Initialize(self.FLASHCARTS) - if ret is False: - self.CONN = None - elif isinstance(ret, list): - for i in range(0, len(ret)): - status = ret[i][0] - msg = ret[i][1] - if status == 3: - QtWidgets.QMessageBox.critical(self, "{:s} {:s}".format(APPNAME, VERSION), msg, QtWidgets.QMessageBox.Ok) - self.CONN = None - - if dev.IsConnected(): - self.DEVICES[dev.GetFullName()] = dev - dev.Close() - - self.cmbDevice.setStyleSheet("QComboBox { border: 0; margin: 0; padding: 0; max-width: 0px; }"); - - if len(self.DEVICES) == 0: - self.lblDevice.setText("No devices found.") - self.lblDevice.setStyleSheet(""); - self.cmbDevice.clear() - self.btnConnect.setEnabled(False) - elif len(self.DEVICES) == 1 or (connectToFirst and len(self.DEVICES) > 1): - self.lblDevice.setText(list(self.DEVICES.keys())[0]) - self.lblDevice.setStyleSheet(""); - self.ConnectDevice() - self.cmbDevice.clear() - self.btnConnect.setEnabled(True) - else: - self.lblDevice.setText("Select device:") - self.cmbDevice.clear() - self.cmbDevice.addItems(self.DEVICES.keys()) - self.cmbDevice.setCurrentIndex(0) - self.cmbDevice.setStyleSheet(""); - self.btnConnect.setEnabled(True) - - #self.btnScan.setEnabled(True) - self.btnConnect.setEnabled(True) - - if len(self.DEVICES) == 0: return False - return True - - def AbortOperation(self): - self.CONN.CANCEL = True - - def FinishOperation(self): - if self.lblStatus2aResult.text() == "Pending...": self.lblStatus2aResult.setText("–") - self.lblStatus4aResult.setText("") - self.grpDMGCartridgeInfo.setEnabled(True) - self.grpAGBCartridgeInfo.setEnabled(True) - self.grpActions.setEnabled(True) - self.btnCancel.setEnabled(False) - - dontShowAgain = str(self.SETTINGS.value("SkipFinishMessage")).lower() == "enabled" - - msgbox = QtWidgets.QMessageBox(parent=self, icon=QtWidgets.QMessageBox.Information, windowTitle="{:s} {:s}".format(APPNAME, VERSION), text="Operation complete!", standardButtons=QtWidgets.QMessageBox.Ok) - cb = QtWidgets.QCheckBox("Don’t show this message again.", checked=False) - msgbox.setCheckBox(cb) - - if self.CONN.INFO["last_action"] == 4: # Flash ROM - self.CONN.INFO["last_action"] = 0 - self.ReadCartridge(resetStatus=False) - self.lblStatus4a.setText("Done!") - if "verified" in self.PROGRESS and self.PROGRESS["verified"] == True: - msgbox.setText("The ROM was flashed and verified successfully!") - else: - msgbox.setText("ROM flashing complete!") - if not dontShowAgain: - msgbox.exec() - dontShowAgain = cb.isChecked() - - elif self.CONN.INFO["last_action"] == 1: # Backup ROM - self.CONN.INFO["last_action"] = 0 - - if self.CONN.GetMode() == "DMG": - if self.CONN.INFO["rom_checksum"] == self.CONN.INFO["rom_checksum_calc"]: - self.lblHeaderROMChecksumResult.setText("Valid (0x{:04X})".format(self.CONN.INFO["rom_checksum"])) - self.lblHeaderROMChecksumResult.setStyleSheet("QLabel { color: green; }"); - self.lblStatus4a.setText("Done!") - #msgbox = QtWidgets.QMessageBox(parent=self, icon=QtWidgets.QMessageBox.Information, windowTitle="{:s} {:s}".format(APPNAME, VERSION), text="The ROM was dumped successfully!", standardButtons=QtWidgets.QMessageBox.Ok) - msgbox.setText("The ROM backup is complete and the checksum was verified successfully!") - if not dontShowAgain: - msgbox.exec() - dontShowAgain = cb.isChecked() - else: - self.lblHeaderROMChecksumResult.setText("Invalid (0x{:04X}≠0x{:04X})".format(self.CONN.INFO["rom_checksum_calc"], self.CONN.INFO["rom_checksum"])) - self.lblHeaderROMChecksumResult.setStyleSheet("QLabel { color: red; }"); - self.lblStatus4a.setText("Done.") - QtWidgets.QMessageBox.warning(self, "{:s} {:s}".format(APPNAME, VERSION), "The ROM was dumped, but the checksum is not correct. This may indicate a bad dump, however this can be normal for some reproduction cartridges, prototypes and patched games.\nWhen dumping from a flash cartridge, manually selecting MBC5 before dumping may also help.", QtWidgets.QMessageBox.Ok) - elif self.CONN.GetMode() == "AGB": - if self.AGB_Global_CRC32 == self.CONN.INFO["rom_checksum_calc"]: - self.lblAGBHeaderROMChecksumResult.setText("Valid (0x{:06X})".format(self.AGB_Global_CRC32)) - self.lblAGBHeaderROMChecksumResult.setStyleSheet("QLabel { color: green; }"); - self.lblStatus4a.setText("Done!") - msgbox.setText("The ROM backup is complete and the checksum was verified successfully!") - if not dontShowAgain: - msgbox.exec() - dontShowAgain = cb.isChecked() - - elif self.AGB_Global_CRC32 == 0: - self.lblAGBHeaderROMChecksumResult.setText("0x{:06X}".format(self.CONN.INFO["rom_checksum_calc"])) - self.lblAGBHeaderROMChecksumResult.setStyleSheet(self.lblHeaderCGBResult.styleSheet()) - self.lblStatus4a.setText("Done!") - QtWidgets.QMessageBox.information(self, "{:s} {:s}".format(APPNAME, VERSION), "The ROM backup is complete!", QtWidgets.QMessageBox.Ok) - else: - self.lblAGBHeaderROMChecksumResult.setText("Invalid (0x{:06X}≠0x{:06X})".format(self.CONN.INFO["rom_checksum_calc"], self.AGB_Global_CRC32)) - self.lblAGBHeaderROMChecksumResult.setStyleSheet("QLabel { color: red; }"); - self.lblStatus4a.setText("Done.") - QtWidgets.QMessageBox.warning(self, "{:s} {:s}".format(APPNAME, VERSION), "The ROM backup is complete, but the checksum doesn’t match the known database entry. This may indicate a bad dump, however this can be normal for some reproduction cartridges, prototypes and patched games.", QtWidgets.QMessageBox.Ok) - - elif self.CONN.INFO["last_action"] == 2: # Backup RAM - self.lblStatus4a.setText("Done!") - self.CONN.INFO["last_action"] = 0 - if self.CONN.INFO["transferred"] == 131072: # 128 KB - with open(self.CONN.INFO["last_path"], "rb") as file: temp = file.read() - if temp[0x1FFB1:0x1FFB6] == b'Magic': - answer = QtWidgets.QMessageBox.question(self, "{:s} {:s}".format(APPNAME, VERSION), "Game Boy Camera save data was detected.\nWould you like to load it with the GB Camera Viewer now?", QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.Yes) - if answer == QtWidgets.QMessageBox.Yes: - self.CAMWIN = None - self.CAMWIN = PocketCameraWindow(self, icon=self.windowIcon(), file=self.CONN.INFO["last_path"]) - self.CAMWIN.setAttribute(QtCore.Qt.WA_DeleteOnClose, True) - self.CAMWIN.setModal(True) - self.CAMWIN.run() - return - - msgbox.setText("The save data backup is complete!") - if not dontShowAgain: - msgbox.exec() - dontShowAgain = cb.isChecked() - - elif self.CONN.INFO["last_action"] == 3: # Restore RAM - self.lblStatus4a.setText("Done!") - self.CONN.INFO["last_action"] = 0 - if "save_erase" in self.CONN.INFO and self.CONN.INFO["save_erase"]: - msg_text = "The save data was erased." - del(self.CONN.INFO["save_erase"]) - else: - msg_text = "The save data was restored!" - msgbox.setText(msg_text) - if not dontShowAgain: - msgbox.exec() - dontShowAgain = cb.isChecked() - - else: - self.lblStatus4a.setText("Ready.") - self.CONN.INFO["last_action"] = 0 - - if dontShowAgain: self.SETTINGS.setValue("SkipFinishMessage", "enabled") - self.SetProgressBars(min=0, max=1, value=1) - - def CartridgeTypeAutoDetect(self): - cart_type = 0 - cart_text = "" - - if self.CONN.CheckROMStable() is False: - QtWidgets.QMessageBox.critical(self, "{:s} {:s}".format(APPNAME, VERSION), "Unstable ROM reading detected. Please make sure you selected the correct mode and that the cartridge contacts are clean.", QtWidgets.QMessageBox.Ok) - return 0 - - if self.CONN.GetMode() in self.FLASHCARTS and len(self.FLASHCARTS[self.CONN.GetMode()]) == 0: - QtWidgets.QMessageBox.critical(self, "{:s} {:s}".format(APPNAME, VERSION), "No flash cartridge type configuration files found. Try to restart the application with the “--reset” switch to reset the configuration.", QtWidgets.QMessageBox.Ok) - return 0 - - msgbox = QtWidgets.QMessageBox(parent=self, icon=QtWidgets.QMessageBox.Question, windowTitle="{:s} {:s}".format(APPNAME, VERSION), text="Would you like " + APPNAME + " to try and auto-detect the flash cartridge type?\n(Official game cartridges can not be re-written.)", standardButtons=QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, defaultButton=QtWidgets.QMessageBox.Yes) - cb = QtWidgets.QCheckBox("Limit voltage to 3.3V", checked=True) - if self.CONN.GetMode() == "DMG": - msgbox.setCheckBox(cb) - answer = msgbox.exec() - limitVoltage = cb.isChecked() - if answer == QtWidgets.QMessageBox.No: - return 0 - else: - detected = self.CONN.AutoDetectFlash(limitVoltage) - if len(detected) == 0: - msgbox = QtWidgets.QMessageBox(parent=self, icon=QtWidgets.QMessageBox.Question, windowTitle="{:s} {:s}".format(APPNAME, VERSION), text="No pre-configured flash cartridge type was detected. You can still try and manually select one from the list -- look for similar PCB text and/or flash chip markings. However, chances are this cartridge is currently not supported for flashing with " + APPNAME + ".\n\nWould you like " + APPNAME + " to run a flash chip query? This may help adding support for your flash cartridge in the future.", standardButtons=QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, defaultButton=QtWidgets.QMessageBox.Yes) - if self.CONN.GetMode() == "DMG": - msgbox.setCheckBox(cb) - answer = msgbox.exec() - if self.CONN.GetMode() == "DMG": - limitVoltage = cb.isChecked() - else: - limitVoltage = False - - if answer == QtWidgets.QMessageBox.Yes: - (flash_id, cfi_s, cfi) = self.CONN.CheckFlashChip(limitVoltage) - if cfi_s == "": - QtWidgets.QMessageBox.information(self, "{:s} {:s}".format(APPNAME, VERSION), "Flash chip query result:
" + flash_id + "There was no Common Flash Interface (CFI) response from the cartridge. If a flash chip exists on the cartridge PCB, it may be too old or require unique unlocking and handling.", QtWidgets.QMessageBox.Ok) - else: - QtWidgets.QMessageBox.information(self, "{:s} {:s}".format(APPNAME, VERSION), "Flash chip query result:
" + flash_id + "
" + str(cfi_s) + "", QtWidgets.QMessageBox.Ok) - with open(self.CONFIG_PATH + "/cfi.bin", "wb") as f: f.write(cfi['raw']) - return 0 - else: - cart_type = detected[0] - size_undetected = False - if self.CONN.GetMode() == "DMG": - cart_types = self.CONN.GetSupportedCartridgesDMG() - size = cart_types[1][detected[0]]["flash_size"] - for i in range(0, len(detected)): - if size != cart_types[1][detected[i]]["flash_size"]: - size_undetected = True - cart_text += "- " + cart_types[0][detected[i]] + "\n" - elif self.CONN.GetMode() == "AGB": - cart_types = self.CONN.GetSupportedCartridgesAGB() - size = cart_types[1][detected[0]]["flash_size"] - for i in range(0, len(detected)): - if size != cart_types[1][detected[i]]["flash_size"]: - size_undetected = True - cart_text += "- " + cart_types[0][detected[i]] + "\n" - - if size_undetected: - (flashid, cfi_s, cfi) = self.CONN.CheckFlashChip(limitVoltage=limitVoltage, cart_type=cart_types[1][cart_type]) - if isinstance(cfi, dict) and 'device_size' in cfi: - for i in range(0, len(detected)): - if cfi['device_size'] == cart_types[1][detected[i]]["flash_size"]: - cart_type = detected[i] - size_undetected = False - break - - if len(detected) == 1: - msg_text = "The following flash cartridge type was detected:\n" + cart_text + "\nThe supported ROM size is up to {:d} MB unless specified otherwise.".format(int(cart_types[1][cart_type]['flash_size'] / 1024 / 1024)) - else: - if size_undetected is True: - msg_text = "The following flash cartridge type variants were detected:\n" + cart_text + "\nThe first one will now be auto-selected, but you might need to adjust the selection.\n\nIMPORTANT: While these cartridges share the same electronic signature, their supported ROM size can differ. As the size can not be detected automatically at this time, please select it manually." - else: - msg_text = "The following flash cartridge type variants were detected:\n" + cart_text + "\nAll from this list should work the same. The first name/alias will now be auto-selected.\n\nThe supported ROM size is up to {:d} MB.".format(int(cart_types[1][cart_type]['flash_size'] / 1024 / 1024)) - - msgbox = QtWidgets.QMessageBox(parent=self, icon=QtWidgets.QMessageBox.Question, windowTitle="{:s} {:s}".format(APPNAME, VERSION), text=msg_text) - button_ok = msgbox.addButton("&OK", QtWidgets.QMessageBox.ActionRole) - button_cancel = msgbox.addButton("&Cancel", QtWidgets.QMessageBox.RejectRole) - button_cfi = msgbox.addButton(" Run flash chip &query ", QtWidgets.QMessageBox.ActionRole) - msgbox.setDefaultButton(button_ok) - msgbox.setEscapeButton(button_cancel) - answer = msgbox.exec() - if msgbox.clickedButton() == button_cfi: - (flash_id, cfi_s, cfi) = self.CONN.CheckFlashChip(limitVoltage=limitVoltage, cart_type=cart_types[1][cart_type]) - if cfi_s == "": - QtWidgets.QMessageBox.information(self, "{:s} {:s}".format(APPNAME, VERSION), "Flash chip query result:
" + flash_id + "There was no Common Flash Interface (CFI) response from the cartridge. If a flash chip exists on the cartridge PCB, it may be too old or require unique unlocking and handling.", QtWidgets.QMessageBox.Ok) - else: - QtWidgets.QMessageBox.information(self, "{:s} {:s}".format(APPNAME, VERSION), "Flash chip query result:
" + flash_id + "
" + str(cfi_s) + "", QtWidgets.QMessageBox.Ok) - with open(self.CONFIG_PATH + "/cfi.bin", "wb") as f: f.write(cfi['raw']) - elif msgbox.clickedButton() == button_cancel: return 0 - - return cart_type - - def CartridgeTypeChanged(self, index): - if self.CONN.GetMode() == "DMG": - cart_types = self.CONN.GetSupportedCartridgesDMG() - if cart_types[1][index] == "AUTODETECT": # special keyword - cart_type = self.CartridgeTypeAutoDetect() - if (cart_type == 1): cart_type = 0 - self.cmbDMGCartridgeTypeResult.setCurrentIndex(cart_type) - elif cart_types[1][index] == "RETAIL": # special keyword - pass - else: - for i in range(0, len(self.DMG_Header_ROM_Sizes_Flasher_Map)): - if cart_types[1][index]["flash_size"] == (self.DMG_Header_ROM_Sizes_Flasher_Map[i] * 0x4000): - self.cmbHeaderROMSizeResult.setCurrentIndex(i) - - elif self.CONN.GetMode() == "AGB": - cart_types = self.CONN.GetSupportedCartridgesAGB() - if cart_types[1][index] == "AUTODETECT": # special keyword - cart_type = self.CartridgeTypeAutoDetect() - if (cart_type == 1): cart_type = 0 - self.cmbAGBCartridgeTypeResult.setCurrentIndex(cart_type) - elif cart_types[1][index] == "RETAIL": # special keyword - pass - else: - self.cmbAGBHeaderROMSizeResult.setCurrentIndex(self.AGB_Header_ROM_Sizes_Map.index(cart_types[1][index]["flash_size"])) - - def BackupROM(self): - if not self.CheckDeviceAlive(): return - mbc = self.DMG_Header_Features_MBC[self.cmbHeaderFeaturesResult.currentIndex()] - rom_banks = self.DMG_Header_ROM_Sizes_Flasher_Map[self.cmbHeaderROMSizeResult.currentIndex()] - - fast_read_mode = self.SETTINGS.value("FastReadMode") - if fast_read_mode and fast_read_mode.lower() == "enabled": - fast_read_mode = True - else: - fast_read_mode = False - - rom_size = 0 - if self.CONN.GetMode() == "DMG": - if mbc == 1 and ("MOMOCOL" in self.lblHeaderTitleResult.text() or "BOMCOL" in self.lblHeaderTitleResult.text()): - mbc = 1.1 - setting_name = "LastDirRomDMG" - last_dir = self.SETTINGS.value(setting_name) - if last_dir is None: last_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DocumentsLocation) - path = self.lblHeaderTitleResult.text().strip().encode('ascii', 'ignore').decode('ascii') - if path == "": path = "ROM" - path = re.sub(r"[<>:\"/\\|\?\*]", "_", path) - if self.CONN.INFO["cgb"] == 0xC0 or self.CONN.INFO["cgb"] == 0x80: - path = path + ".gbc" - elif self.CONN.INFO["sgb"] == 0x03: - path = path + ".sgb" - else: - path = path + ".gb" - path = QtWidgets.QFileDialog.getSaveFileName(self, "Backup ROM", last_dir + "/" + path, "Game Boy ROM File (*.gb *.sgb *.gbc);;All Files (*.*)")[0] - - elif self.CONN.GetMode() == "AGB": - setting_name = "LastDirRomAGB" - last_dir = self.SETTINGS.value(setting_name) - if last_dir is None: last_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DocumentsLocation) - path = self.lblAGBHeaderTitleResult.text().strip().encode('ascii', 'ignore').decode('ascii') - if path == "": path = self.lblAGBHeaderCodeResult.text().strip().encode('ascii', 'ignore').decode('ascii') - if path == "": path = "ROM" - path = re.sub(r"[<>:\"/\\|\?\*]", "_", path) - rom_size = self.AGB_Header_ROM_Sizes_Map[self.cmbAGBHeaderROMSizeResult.currentIndex()] - path = path + ".gba" - path = QtWidgets.QFileDialog.getSaveFileName(self, "Backup ROM", last_dir + "/" + path, "Game Boy Advance ROM File (*.gba *.srl);;All Files (*.*)")[0] - - if (path == ""): return - - self.SETTINGS.setValue(setting_name, os.path.dirname(path)) - self.lblHeaderROMChecksumResult.setStyleSheet(self.lblHeaderCGBResult.styleSheet()) - self.lblAGBHeaderROMChecksumResult.setStyleSheet(self.lblHeaderCGBResult.styleSheet()) - - self.CONN.BackupROM(fncSetProgress=self.SetProgress, path=path, mbc=mbc, rom_banks=rom_banks, agb_rom_size=rom_size, fast_read_mode=fast_read_mode) - - def FlashROM(self, dpath=""): - if not self.CheckDeviceAlive(): return - path = "" - if dpath != "": - #text = "The following ROM file will now be written to the flash cartridge:\n" + dpath - #answer = QtWidgets.QMessageBox.question(self, "{:s} {:s}".format(APPNAME, VERSION), text, QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel, QtWidgets.QMessageBox.Ok) - #if answer == QtWidgets.QMessageBox.Cancel: return - path = dpath - - if self.CONN.GetMode() == "DMG": - setting_name = "LastDirRomDMG" - last_dir = self.SETTINGS.value(setting_name) - if last_dir is None: last_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DocumentsLocation) - carts = self.CONN.GetSupportedCartridgesDMG()[1] - cart_type = self.cmbDMGCartridgeTypeResult.currentIndex() - elif self.CONN.GetMode() == "AGB": - setting_name = "LastDirRomAGB" - last_dir = self.SETTINGS.value(setting_name) - if last_dir is None: last_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DocumentsLocation) - carts = self.CONN.GetSupportedCartridgesAGB()[1] - cart_type = self.cmbAGBCartridgeTypeResult.currentIndex() - else: - return - - if cart_type == 0: - cart_type = self.CartridgeTypeAutoDetect() - if (cart_type == 1): cart_type = 0 - if self.CONN.GetMode() == "DMG": - self.cmbDMGCartridgeTypeResult.setCurrentIndex(cart_type) - elif self.CONN.GetMode() == "AGB": - self.cmbAGBCartridgeTypeResult.setCurrentIndex(cart_type) - if cart_type == 0: return - - while path == "": - if self.CONN.GetMode() == "DMG": - path = QtWidgets.QFileDialog.getOpenFileName(self, "Flash ROM", last_dir, "Game Boy ROM File (*.gb *.gbc *.sgb *.bin);;All Files (*.*)")[0] - elif self.CONN.GetMode() == "AGB": - path = QtWidgets.QFileDialog.getOpenFileName(self, "Flash ROM", last_dir, "Game Boy Advance ROM File (*.gba *.srl);;All Files (*.*)")[0] - - if (path == ""): return - - self.SETTINGS.setValue(setting_name, os.path.dirname(path)) - - if os.path.getsize(path) > 0x2000000: # reject too large files to avoid exploding RAM - QtWidgets.QMessageBox.critical(self, "{:s} {:s}".format(APPNAME, VERSION), "Files bigger than 32 MB are not supported.", QtWidgets.QMessageBox.Ok) - return - - with open(path, "rb") as file: buffer = file.read() - rom_size = len(buffer) - if rom_size > carts[cart_type]['flash_size']: - answer = QtWidgets.QMessageBox.warning(self, "{:s} {:s}".format(APPNAME, VERSION), "The selected flash cartridge type seems to support ROMs that are up to " + str(int(carts[cart_type]['flash_size'] / 1024 / 1024)) + " MB in size, but the file you selected is " + str(os.path.getsize(path)/1024/1024) + " MB. You can still give it a try, but it’s possible that it’s too large.", QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel, QtWidgets.QMessageBox.Cancel) - if answer == QtWidgets.QMessageBox.Cancel: return - - override_voltage = False - if 'voltage_variants' in carts[cart_type] and carts[cart_type]['voltage'] == 3.3: - msgbox = QtWidgets.QMessageBox(parent=self, icon=QtWidgets.QMessageBox.Question, windowTitle="{:s} {:s}".format(APPNAME, VERSION), text="The selected flash cartridge type usually flashes fine with 3.3V, however sometimes it may require 5V. Which mode should be used?") - button_3_3v = msgbox.addButton(" Use &3.3V (safer) ", QtWidgets.QMessageBox.ActionRole) - button_5v = msgbox.addButton("Use &5V", QtWidgets.QMessageBox.ActionRole) - button_cancel = msgbox.addButton("&Cancel", QtWidgets.QMessageBox.RejectRole) - msgbox.setDefaultButton(button_3_3v) - msgbox.setEscapeButton(button_cancel) - answer = msgbox.exec() - if msgbox.clickedButton() == button_5v: - override_voltage = 5 - elif msgbox.clickedButton() == button_cancel: return - - reverse_sectors = False - if 'sector_reversal' in carts[cart_type]: - msgbox = QtWidgets.QMessageBox(parent=self, icon=QtWidgets.QMessageBox.Question, windowTitle="{:s} {:s}".format(APPNAME, VERSION), text="The selected flash cartridge type is reported to sometimes have reversed sectors. If the cartridge is not working after flashing, try reversed sectors.") - button_normal = msgbox.addButton("Normal", QtWidgets.QMessageBox.ActionRole) - button_reversed = msgbox.addButton("Reversed", QtWidgets.QMessageBox.ActionRole) - button_cancel = msgbox.addButton("&Cancel", QtWidgets.QMessageBox.RejectRole) - msgbox.setDefaultButton(button_normal) - msgbox.setEscapeButton(button_cancel) - answer = msgbox.exec() - if msgbox.clickedButton() == button_reversed: - reverse_sectors = True - elif msgbox.clickedButton() == button_cancel: return - - prefer_sector_erase = False - if 'chip_erase' in carts[cart_type]['commands'] and 'sector_erase' in carts[cart_type]['commands']: - prefer_sector_erase = self.SETTINGS.value("PreferSectorErase") - if prefer_sector_erase and prefer_sector_erase.lower() == "enabled": - prefer_sector_erase = True - else: - prefer_sector_erase = False - - fast_read_mode = self.SETTINGS.value("FastReadMode") - if fast_read_mode and fast_read_mode.lower() == "enabled": - fast_read_mode = True - else: - fast_read_mode = False - - verify_flash = self.SETTINGS.value("VerifyFlash") - if verify_flash and verify_flash.lower() == "enabled": - verify_flash = True - else: - verify_flash = False - - try: - if self.CONN.GetMode() == "DMG": - hdr = RomFileDMG(path).GetHeader() - elif self.CONN.GetMode() == "AGB": - hdr = RomFileAGB(path).GetHeader() - if not hdr["logo_correct"]: - answer = QtWidgets.QMessageBox.warning(self, "{:s} {:s}".format(APPNAME, VERSION), "Warning: The ROM file you selected will not boot on actual hardware due to invalid logo data.", QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel, QtWidgets.QMessageBox.Cancel) - if answer == QtWidgets.QMessageBox.Cancel: return - if not hdr["header_checksum_correct"]: - answer = QtWidgets.QMessageBox.warning(self, "{:s} {:s}".format(APPNAME, VERSION), "Warning: The ROM file you selected will not boot on actual hardware due to an invalid header checksum (expected 0x{:02X} instead of 0x{:02X}).".format(hdr["header_checksum_calc"], hdr["header_checksum"]), QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel, QtWidgets.QMessageBox.Cancel) - if answer == QtWidgets.QMessageBox.Cancel: return - except: - QtWidgets.QMessageBox.critical(self, "{:s} {:s}".format(APPNAME, VERSION), "The file you selected could not be read.", QtWidgets.QMessageBox.Ok) - return - - self.CONN.FlashROM(fncSetProgress=self.SetProgress, path=path, cart_type=cart_type, override_voltage=override_voltage, prefer_sector_erase=prefer_sector_erase, reverse_sectors=reverse_sectors, fast_read_mode=fast_read_mode, verify_flash=verify_flash) - buffer = None - - def BackupRAM(self): - if not self.CheckDeviceAlive(): return - if self.CONN.GetMode() == "DMG": - setting_name = "LastDirSaveDataDMG" - last_dir = self.SETTINGS.value(setting_name) - if last_dir is None: last_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DocumentsLocation) - path = self.lblHeaderTitleResult.text().strip().encode('ascii', 'ignore').decode('ascii') - if path == "": path = "ROM" - features = self.DMG_Header_Features_MBC[self.cmbHeaderFeaturesResult.currentIndex()] - save_type = self.DMG_Header_RAM_Sizes_Flasher_Map[self.cmbHeaderRAMSizeResult.currentIndex()] - if save_type == 0: - QtWidgets.QMessageBox.warning(self, "{:s} {:s}".format(APPNAME, VERSION), "Please select the correct save data size.", QtWidgets.QMessageBox.Ok) - return - elif self.CONN.GetMode() == "AGB": - setting_name = "LastDirSaveDataAGB" - last_dir = self.SETTINGS.value(setting_name) - if last_dir is None: last_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DocumentsLocation) - path = self.lblAGBHeaderTitleResult.text().strip().encode('ascii', 'ignore').decode('ascii') - if path == "": path = self.lblAGBHeaderCodeResult.text().strip().encode('ascii', 'ignore').decode('ascii') - if path == "": path = "ROM" - features = 0 - save_type = self.cmbAGBSaveTypeResult.currentIndex() - if save_type == 0: - QtWidgets.QMessageBox.warning(self, "{:s} {:s}".format(APPNAME, VERSION), "The save type was not selected or auto-detection failed.", QtWidgets.QMessageBox.Ok) - return - else: - return - - add_date_time = self.SETTINGS.value("SaveFileNameAddDateTime") - if add_date_time and add_date_time.lower() == "enabled": - path = re.sub(r"[<>:\"/\\|\?\*]", "_", path) + "_" + datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + ".sav" - else: - path = re.sub(r"[<>:\"/\\|\?\*]", "_", path) + ".sav" - - path = QtWidgets.QFileDialog.getSaveFileName(self, "Backup Save Data", last_dir + "/" + path, "Save Data File (*.sav);;All Files (*.*)")[0] - - if (path == ""): return - - self.SETTINGS.setValue(setting_name, os.path.dirname(path)) - self.CONN.BackupRAM(fncSetProgress=self.SetProgress, path=path, mbc=features, save_type=save_type) - - def WriteRAM(self, dpath="", erase=False): - if not self.CheckDeviceAlive(): return - if self.CONN.GetMode() == "DMG": - setting_name = "LastDirSaveDataDMG" - last_dir = self.SETTINGS.value(setting_name) - if last_dir is None: last_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DocumentsLocation) - if dpath == "": path = self.lblHeaderTitleResult.text().strip().encode('ascii', 'ignore').decode('ascii') - features = self.DMG_Header_Features_MBC[self.cmbHeaderFeaturesResult.currentIndex()] - save_type = self.DMG_Header_RAM_Sizes_Flasher_Map[self.cmbHeaderRAMSizeResult.currentIndex()] - if save_type == 0: - QtWidgets.QMessageBox.warning(self, "{:s} {:s}".format(APPNAME, VERSION), "Please select the correct save data size.", QtWidgets.QMessageBox.Ok) - return - elif self.CONN.GetMode() == "AGB": - setting_name = "LastDirSaveDataAGB" - last_dir = self.SETTINGS.value(setting_name) - if last_dir is None: last_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DocumentsLocation) - if dpath == "": path = self.lblAGBHeaderTitleResult.text().strip().encode('ascii', 'ignore').decode('ascii') - features = 0 - save_type = self.cmbAGBSaveTypeResult.currentIndex() - if save_type == 0: - QtWidgets.QMessageBox.critical(self, "{:s} {:s}".format(APPNAME, VERSION), "The save type was not selected or auto-detection failed.", QtWidgets.QMessageBox.Ok) - return - else: - return - - if dpath != "": - text = "The following save data file will now be written to the cartridge:\n" + dpath - answer = QtWidgets.QMessageBox.question(self, "{:s} {:s}".format(APPNAME, VERSION), text, QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel, QtWidgets.QMessageBox.Ok) - if answer == QtWidgets.QMessageBox.Cancel: return - path = dpath - self.SETTINGS.setValue(setting_name, os.path.dirname(path)) - elif erase: - answer = QtWidgets.QMessageBox.warning(self, "{:s} {:s}".format(APPNAME, VERSION), "The save data on your cartridge will now be erased.", QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel, QtWidgets.QMessageBox.Cancel) - if answer == QtWidgets.QMessageBox.Cancel: return - else: - path = path + ".sav" - path = QtWidgets.QFileDialog.getOpenFileName(self, "Restore Save Data", last_dir + "/" + path, "Save Data File (*.sav);;All Files (*.*)")[0] - if not path == "": self.SETTINGS.setValue(setting_name, os.path.dirname(path)) - if (path == ""): return - if os.path.getsize(path) > 0x100000: # reject too large files to avoid exploding RAM - QtWidgets.QMessageBox.critical(self, "{:s} {:s}".format(APPNAME, VERSION), "Files bigger than 1 MB are not supported.", QtWidgets.QMessageBox.Ok) - return - - self.CONN.RestoreRAM(fncSetProgress=self.SetProgress, path=path, mbc=features, save_type=save_type, erase=erase) - - def CheckDeviceAlive(self, setMode=False): - if self.CONN is not None: - mode = self.CONN.GetMode() - if self.CONN.DEVICE is not None: - if not self.CONN.IsConnected(): - self.DisconnectDevice() - self.DEVICES = {} - dontShowAgain = str(self.SETTINGS.value("AutoReconnect")).lower() == "enabled" - if not dontShowAgain: - cb = QtWidgets.QCheckBox("Always try to reconnect without asking", checked=False) - msgbox = QtWidgets.QMessageBox(parent=self, icon=QtWidgets.QMessageBox.Question, windowTitle="{:s} {:s}".format(APPNAME, VERSION), text="The connection to the device was lost. Do you want to try and reconnect to the first device found? The cartridge information will also be reset and read again.", standardButtons=QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, defaultButton=QtWidgets.QMessageBox.Yes) - msgbox.setCheckBox(cb) - answer = msgbox.exec() - dontShowAgain = cb.isChecked() - if dontShowAgain: self.SETTINGS.setValue("AutoReconnect", "enabled") - if answer == QtWidgets.QMessageBox.No: - return False - if self.FindDevices(True): - if setMode is not False: mode = setMode - if mode == "DMG": self.optDMG.setChecked(True) - elif mode == "AGB": self.optAGB.setChecked(True) - self.SetMode() - return True - else: - return False - else: - return True - return False - - def SetMode(self): - setTo = False - mode = self.CONN.GetMode() - if mode == "DMG": - if self.optDMG.isChecked(): return - setTo = "AGB" - elif mode == "AGB": - if self.optAGB.isChecked(): return - setTo = "DMG" - else: # mode not set yet - if self.optDMG.isChecked(): - setTo = "DMG" - elif self.optAGB.isChecked(): - setTo = "AGB" - - voltageWarning = "" - if self.CONN.CanSetVoltageAutomatically(): # device can switch in software - dontShowAgain = str(self.SETTINGS.value("SkipModeChangeWarning")).lower() == "enabled" - elif self.CONN.CanSetVoltageManually(): # device has a physical switch - voltageWarning = "\n\nImportant: Also make sure your device is set to the correct voltage!" - dontShowAgain = False - else: # no voltage switching supported - dontShowAgain = False - - if not dontShowAgain and mode is not None: - cb = QtWidgets.QCheckBox("Don’t show this message again.", checked=False) - msgbox = QtWidgets.QMessageBox(parent=self, icon=QtWidgets.QMessageBox.Warning, windowTitle="{:s} {:s}".format(APPNAME, VERSION), text="The mode will now be changed to " + {"DMG":"Game Boy", "AGB":"Game Boy Advance"}[setTo] + " mode. To be safe, cartridges should only be exchanged while the device is not powered on." + voltageWarning, standardButtons=QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel, defaultButton=QtWidgets.QMessageBox.Ok) - if self.CONN.CanSetVoltageAutomatically(): msgbox.setCheckBox(cb) - answer = msgbox.exec() - dontShowAgain = cb.isChecked() - if answer == QtWidgets.QMessageBox.Cancel: - if mode == "DMG": self.optDMG.setChecked(True) - if mode == "AGB": self.optAGB.setChecked(True) - return False - if dontShowAgain: self.SETTINGS.setValue("SkipModeChangeWarning", "enabled") - - if not self.CheckDeviceAlive(setMode=setTo): return - - if self.optDMG.isChecked() and (mode == "AGB" or mode == None): - self.CONN.SetMode("DMG") - elif self.optAGB.isChecked() and (mode == "DMG" or mode == None): - self.CONN.SetMode("AGB") - - self.ReadCartridge() - qt_app.processEvents() - self.btnHeaderRefresh.setEnabled(True) - self.btnBackupROM.setEnabled(True) - self.btnFlashROM.setEnabled(True) - self.btnBackupRAM.setEnabled(True) - self.btnRestoreRAM.setEnabled(True) - self.grpDMGCartridgeInfo.setEnabled(True) - self.grpAGBCartridgeInfo.setEnabled(True) - - def ReadCartridge(self, resetStatus=True): - if not self.CheckDeviceAlive(): return - data = self.CONN.ReadInfo() - - if data == False or len(data) == 0: - self.DisconnectDevice() - return False - - if self.CONN.GetMode() == "DMG": - self.cmbDMGCartridgeTypeResult.clear() - self.cmbDMGCartridgeTypeResult.addItems(self.CONN.GetSupportedCartridgesDMG()[0]) - self.cmbDMGCartridgeTypeResult.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents) - if "flash_type" in data: - self.cmbDMGCartridgeTypeResult.setCurrentIndex(data["flash_type"]) - - self.lblHeaderTitleResult.setText(data['game_title']) - if data['sgb'] in self.DMG_Header_SGB: - self.lblHeaderSGBResult.setText(self.DMG_Header_SGB[data['sgb']]) - else: - self.lblHeaderSGBResult.setText("Unknown (0x{:02X})".format(data['sgb'])) - if data['cgb'] in self.DMG_Header_CGB: - self.lblHeaderCGBResult.setText(self.DMG_Header_CGB[data['cgb']]) - else: - self.lblHeaderCGBResult.setText("Unknown (0x{:02X})".format(data['cgb'])) - if data['logo_correct']: - self.lblHeaderLogoValidResult.setText("OK") - self.lblHeaderLogoValidResult.setStyleSheet(self.lblHeaderCGBResult.styleSheet()) - else: - self.lblHeaderLogoValidResult.setText("Invalid") - self.lblHeaderLogoValidResult.setStyleSheet("QLabel { color: red; }"); - if data['header_checksum_correct']: - self.lblHeaderChecksumResult.setText("Valid (0x{:02X})".format(data['header_checksum'])) - self.lblHeaderChecksumResult.setStyleSheet(self.lblHeaderCGBResult.styleSheet()) - else: - self.lblHeaderChecksumResult.setText("Invalid (0x{:02X})".format(data['header_checksum'])) - self.lblHeaderChecksumResult.setStyleSheet("QLabel { color: red; }"); - self.lblHeaderROMChecksumResult.setText("0x{:04X}".format(data['rom_checksum'])) - self.lblHeaderROMChecksumResult.setStyleSheet(self.lblHeaderCGBResult.styleSheet()) - for i in range(0, len(self.DMG_Header_ROM_Sizes_Map)): - if data["rom_size_raw"] == self.DMG_Header_ROM_Sizes_Map[i]: - self.cmbHeaderROMSizeResult.setCurrentIndex(i) - for i in range(0, len(self.DMG_Header_RAM_Sizes_Map)): - if data["ram_size_raw"] == self.DMG_Header_RAM_Sizes_Map[i]: - self.cmbHeaderRAMSizeResult.setCurrentIndex(i) - i = 0 - for k, v in self.DMG_Header_Features.items(): - if data["features_raw"] == k: - self.cmbHeaderFeaturesResult.setCurrentIndex(i) - if k == 0x05 or k == 0x06: self.cmbHeaderRAMSizeResult.setCurrentIndex(1) # MBC2 Save - i += 1 - - if data['empty'] == True: # defaults - self.lblHeaderTitleResult.setText("(No ROM data detected)") - self.lblHeaderTitleResult.setStyleSheet("QLabel { color: red; }"); - #self.lblHeaderSGBResult.setText("") - #self.lblHeaderCGBResult.setText("") - #self.lblHeaderLogoValidResult.setText("") - #self.lblHeaderChecksumResult.setText("") - #self.lblHeaderROMChecksumResult.setText("") - self.cmbHeaderROMSizeResult.setCurrentIndex(11) - self.cmbHeaderRAMSizeResult.setCurrentIndex(0) - self.cmbHeaderFeaturesResult.setCurrentIndex(0) - else: - self.lblHeaderTitleResult.setStyleSheet(self.lblHeaderCGBResult.styleSheet()) - - self.grpAGBCartridgeInfo.setVisible(False) - self.grpDMGCartridgeInfo.setVisible(True) - - elif self.CONN.GetMode() == "AGB": - self.cmbAGBCartridgeTypeResult.clear() - self.cmbAGBCartridgeTypeResult.addItems(self.CONN.GetSupportedCartridgesAGB()[0]) - self.cmbAGBCartridgeTypeResult.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents) - if "flash_type" in data: - self.cmbAGBCartridgeTypeResult.setCurrentIndex(data["flash_type"]) - - self.lblAGBHeaderTitleResult.setText(data['game_title']) - self.lblAGBHeaderCodeResult.setText(data['game_code']) - self.lblAGBHeaderVersionResult.setText(str(data['version'])) - if data['logo_correct']: - self.lblAGBHeaderLogoValidResult.setText("OK") - self.lblAGBHeaderLogoValidResult.setStyleSheet(self.lblAGBHeaderCodeResult.styleSheet()) - else: - self.lblAGBHeaderLogoValidResult.setText("Invalid") - self.lblAGBHeaderLogoValidResult.setStyleSheet("QLabel { color: red; }"); - - if data['96h_correct']: - self.lblAGBHeader96hResult.setText("OK") - self.lblAGBHeader96hResult.setStyleSheet(self.lblAGBHeaderCodeResult.styleSheet()) - else: - self.lblAGBHeader96hResult.setText("Invalid") - self.lblAGBHeader96hResult.setStyleSheet("QLabel { color: red; }"); - - if data['header_checksum_correct']: - self.lblAGBHeaderChecksumResult.setText("Valid (0x{:02X})".format(data['header_checksum'])) - self.lblAGBHeaderChecksumResult.setStyleSheet(self.lblAGBHeaderCodeResult.styleSheet()) - else: - self.lblAGBHeaderChecksumResult.setText("Invalid (0x{:02X})".format(data['header_checksum'])) - self.lblAGBHeaderChecksumResult.setStyleSheet("QLabel { color: red; }"); - self.lblAGBHeaderROMChecksumResult.setStyleSheet(self.lblHeaderCGBResult.styleSheet()) - self.lblAGBHeaderROMChecksumResult.setText("Not available") - self.AGB_Global_CRC32 = 0 - - db_agb_entry = None - - if os.path.exists("{0:s}/db_AGB.json".format(self.CONFIG_PATH)): - with open("{0:s}/db_AGB.json".format(self.CONFIG_PATH)) as f: - db_agb = f.read() - db_agb = json.loads(db_agb) - if data["header_sha1"] in db_agb.keys(): - db_agb_entry = db_agb[data["header_sha1"]] - else: - self.lblAGBHeaderROMChecksumResult.setText("Not in database") - else: - print("FAIL: Database for Game Boy Advance titles not found in " + "{0:s}/db_AGB.json".format(self.CONFIG_PATH)) - - if db_agb_entry != None: - self.cmbAGBHeaderROMSizeResult.setCurrentIndex(self.AGB_Header_ROM_Sizes_Map.index(db_agb_entry['rs'])) - if data["rom_size_calc"] < 0x400000: - self.lblAGBHeaderROMChecksumResult.setText("In database (0x{:06X})".format(db_agb_entry['rc'])) - self.AGB_Global_CRC32 = db_agb_entry['rc'] - - elif data["rom_size"] != 0: - if not data["rom_size"] in self.AGB_Header_ROM_Sizes_Map: - data["rom_size"] = 0x2000000 - self.cmbAGBHeaderROMSizeResult.setCurrentIndex(self.AGB_Header_ROM_Sizes_Map.index(data["rom_size"])) - else: - self.cmbAGBHeaderROMSizeResult.setCurrentIndex(0) - - if data["save_type"] == None: - self.cmbAGBSaveTypeResult.setCurrentIndex(0) - if db_agb_entry != None: - if db_agb_entry['st'] < len(self.AGB_Header_Save_Types): - self.cmbAGBSaveTypeResult.setCurrentIndex(db_agb_entry['st']) - - if data['empty'] == True: # defaults - self.lblAGBHeaderTitleResult.setText("(No ROM data detected)") - self.lblAGBHeaderTitleResult.setStyleSheet("QLabel { color: red; }"); - #self.lblAGBHeaderCodeResult.setText("") - #self.lblAGBHeaderVersionResult.setText("") - #self.lblAGBHeaderLogoValidResult.setText("") - #self.lblAGBHeader96hResult.setText("") - #self.lblAGBHeaderChecksumResult.setText("") - #self.lblAGBHeaderROMChecksumResult.setText("") - self.cmbAGBHeaderROMSizeResult.setCurrentIndex(3) - self.cmbAGBSaveTypeResult.setCurrentIndex(0) - else: - self.lblAGBHeaderTitleResult.setStyleSheet(self.lblHeaderCGBResult.styleSheet()) - - self.grpDMGCartridgeInfo.setVisible(False) - self.grpAGBCartridgeInfo.setVisible(True) - - if resetStatus: - self.lblStatus1aResult.setText("–") - self.lblStatus2aResult.setText("–") - self.lblStatus3aResult.setText("–") - self.lblStatus4a.setText("Ready.") - self.grpStatus.setTitle("Transfer Status") - self.FinishOperation() - - if self.CONN.CheckROMStable() is False: - QtWidgets.QMessageBox.warning(self, "{:s} {:s}".format(APPNAME, VERSION), "Unstable ROM reading detected. Please make sure that the cartridge contacts are very clean and that you selected the correct mode.", QtWidgets.QMessageBox.Ok) - return - - if not data['logo_correct'] and data['empty'] == False: - QtWidgets.QMessageBox.warning(self, "{:s} {:s}".format(APPNAME, VERSION), "The Nintendo Logo check failed which usually means that the cartridge couldn’t be read correctly. Please make sure that the cartridge contacts are very clean and that you selected the correct mode.", QtWidgets.QMessageBox.Ok) - - if data['game_title'][:11] == "YJencrypted": - QtWidgets.QMessageBox.warning(self, "{:s} {:s}".format(APPNAME, VERSION), "This cartridge may be protected against reading or writing a ROM. If you don’t want to risk this cartridge to render itself unusable, please do not try to write a new ROM to it.", QtWidgets.QMessageBox.Ok) - - def formatFileSize(self, size): - size = size / 1024 - if size < 1024: - return "{:.1f} KB".format(size) - else: - return "{:.2f} MB".format(size/1024) - - def formatProgressTime(self, sec): - if int(sec) == 1: - return "{:d} second".format(int(sec)) - elif sec < 60: - return "{:d} seconds".format(int(sec)) - elif int(sec) == 60: - return "1 minute" - else: - min = int(sec / 60) - sec = int(sec % 60) - s = str(min) + " " - if min == 1: - s = s + "minute" - else: - s = s + "minutes" - s = s + ", " + str(sec) + " " - if sec == 1: - s = s + "second" - else: - s = s + "seconds" - return s - - def SetProgress(self, args): - self.MUTEX.acquire(1) - try: - if not "method" in self.PROGRESS: self.PROGRESS = {} - now = time.time() - if args["action"] == "INITIALIZE": - self.PROGRESS["method"] = args["method"] - self.PROGRESS["size"] = args["size"] - if "pos" in args: - self.PROGRESS["pos"] = args["pos"] - else: - self.PROGRESS["pos"] = 0 - if "time_start" in args: - self.PROGRESS["time_start"] = args["time_start"] - else: - self.PROGRESS["time_start"] = now - self.PROGRESS["time_last_emit"] = now - self.PROGRESS["time_last_update_speed"] = now - self.PROGRESS["time_left"] = 0 - self.PROGRESS["speed"] = 0 - self.PROGRESS["speeds"] = [] - self.PROGRESS["bytes_last_update_speed"] = 0 - if args["method"] == "ROM_READ": - self.grpStatus.setTitle("Transfer Status (Backup ROM)") - elif args["method"] == "ROM_WRITE": - self.grpStatus.setTitle("Transfer Status (Flash ROM)") - elif args["method"] == "ROM_WRITE_VERIFY": - self.grpStatus.setTitle("Transfer Status (Verify Flash)") - elif args["method"] == "SAVE_READ": - self.grpStatus.setTitle("Transfer Status (Backup Save Data)") - elif args["method"] == "SAVE_WRITE": - self.grpStatus.setTitle("Transfer Status (Write Save Data)") - self.UpdateProgress(self.PROGRESS) - - if args["action"] == "ABORT": - self.UpdateProgress(args) - self.grpStatus.setTitle("Transfer Status") - self.PROGRESS = {} - - elif args["action"] in ("ERASE", "SECTOR_ERASE"): - if "time_start" in self.PROGRESS: - args["time_elapsed"] = now - self.PROGRESS["time_start"] - elif "time_start" in args: - args["time_elapsed"] = now - args["time_start"] - args["pos"] = 1 - args["size"] = 0 - self.UpdateProgress(args) - - elif self.PROGRESS == {}: - return - - elif args["action"] == "UPDATE_POS": - self.PROGRESS["pos"] = args["pos"] - self.UpdateProgress(self.PROGRESS) - - elif args["action"] in ("READ", "WRITE"): - if "method" not in self.PROGRESS: return - elif args["action"] in ("READ") and self.PROGRESS["method"] in ("SAVE_WRITE", "ROM_WRITE"): return - elif args["action"] in ("WRITE") and self.PROGRESS["method"] in ("SAVE_READ", "ROM_READ", "ROM_WRITE_VERIFY"): return - if self.PROGRESS["pos"] >= self.PROGRESS["size"]: return - - self.PROGRESS["pos"] += args["bytes_added"] - if (now - self.PROGRESS["time_last_emit"]) > 0.05: - self.PROGRESS["time_elapsed"] = now - self.PROGRESS["time_start"] - if (now - self.PROGRESS["time_last_update_speed"]) > 0.25: - time_delta = now - self.PROGRESS["time_last_update_speed"] - pos_delta = self.PROGRESS["pos"] - self.PROGRESS["bytes_last_update_speed"] - if time_delta > 0: - speed = (pos_delta / time_delta) / 1024 - self.PROGRESS["speeds"].append(speed) - if len(self.PROGRESS["speeds"]) > 256: self.PROGRESS["speeds"].pop(0) - self.PROGRESS["speed"] = statistics.median(self.PROGRESS["speeds"]) - self.PROGRESS["time_last_update_speed"] = now - self.PROGRESS["bytes_last_update_speed"] = self.PROGRESS["pos"] - - if "skipping" in args and args["skipping"] is True: - self.PROGRESS["speed"] = 0 - self.PROGRESS["skipping"] = True - else: - self.PROGRESS["skipping"] = False - - if self.PROGRESS["speed"] > 0: - total_speed = statistics.mean(self.PROGRESS["speeds"]) - self.PROGRESS["time_left"] = (self.PROGRESS["size"] - self.PROGRESS["pos"]) / 1024 / total_speed - - self.UpdateProgress(self.PROGRESS) - self.PROGRESS["time_last_emit"] = now - - elif args["action"] == "FINISHED": - self.PROGRESS["pos"] = self.PROGRESS["size"] - self.UpdateProgress(self.PROGRESS) - qt_app.processEvents() - self.PROGRESS["action"] = args["action"] - self.PROGRESS["bytes_last_update_speed"] = self.PROGRESS["size"] - self.PROGRESS["time_elapsed"] = now - self.PROGRESS["time_start"] - self.PROGRESS["time_last_emit"] = now - self.PROGRESS["time_last_update_speed"] = now - self.PROGRESS["time_left"] = 0 - self.PROGRESS["speed"] = (self.PROGRESS["size"] / self.PROGRESS["time_elapsed"]) / 1024 - self.PROGRESS["bytes_last_emit"] = self.PROGRESS["size"] - if "verified" in args: self.PROGRESS["verified"] = (args["verified"] == True) - - if self.PROGRESS["speed"] > self.PROGRESS["size"] / 1024: - self.PROGRESS["speed"] = self.PROGRESS["size"] / 1024 - - self.UpdateProgress(self.PROGRESS) - del(self.PROGRESS["method"]) - - finally: - self.MUTEX.release() - - def UpdateProgress(self, args): - if args is None: return - - if "error" in args: - self.lblStatus4a.setText("Failed!") - self.grpDMGCartridgeInfo.setEnabled(True) - self.grpAGBCartridgeInfo.setEnabled(True) - self.grpActions.setEnabled(True) - self.btnCancel.setEnabled(False) - msgbox = QtWidgets.QMessageBox(parent=self, icon=QtWidgets.QMessageBox.Critical, windowTitle="{:s} {:s}".format(APPNAME, VERSION), text=str(args["error"]), standardButtons=QtWidgets.QMessageBox.Ok) - if not '\n' in str(args["error"]): msgbox.setTextFormat(QtCore.Qt.RichText) - msgbox.exec() - return - - self.grpDMGCartridgeInfo.setEnabled(False) - self.grpAGBCartridgeInfo.setEnabled(False) - self.grpActions.setEnabled(False) - - pos = 0 - size = 0 - speed = 0 - elapsed = 0 - left = 0 - if "pos" in args: pos = args["pos"] - if "size" in args: size = args["size"] - if "speed" in args: speed = args["speed"] - if "time_elapsed" in args: elapsed = args["time_elapsed"] - if "time_left" in args: left = args["time_left"] - - if "action" in args: - if args["action"] == "ERASE": - self.lblStatus1aResult.setText("Pending...") - self.lblStatus2aResult.setText("Pending...") - self.lblStatus3aResult.setText(self.formatProgressTime(elapsed)) - self.lblStatus4a.setText("Erasing flash...") - self.lblStatus4aResult.setText("") - self.btnCancel.setEnabled(args["abortable"]) - self.SetProgressBars(min=0, max=size, value=pos) - elif args["action"] == "SECTOR_ERASE": - if elapsed >= 1: - self.lblStatus3aResult.setText(self.formatProgressTime(elapsed)) - self.lblStatus4a.setText("Erasing sector...") - self.lblStatus4aResult.setText("") - self.btnCancel.setEnabled(args["abortable"]) - self.SetProgressBars(min=0, max=size, value=pos) - elif args["action"] == "ABORTING": - self.lblStatus1aResult.setText("–") - self.lblStatus2aResult.setText("–") - self.lblStatus3aResult.setText("–") - self.lblStatus4a.setText("Stopping... Please wait.") - self.lblStatus4aResult.setText("") - self.btnCancel.setEnabled(args["abortable"]) - self.SetProgressBars(min=0, max=size, value=pos) - elif args["action"] == "FINISHED": - self.FinishOperation() - elif args["action"] == "ABORT": - wd = 10 - while self.CONN.WORKER.isRunning(): - time.sleep(0.1) - wd -= 1 - if wd == 0: break - pass - self.CONN.CANCEL = False - self.grpDMGCartridgeInfo.setEnabled(True) - self.grpAGBCartridgeInfo.setEnabled(True) - self.grpActions.setEnabled(True) - self.lblStatus1aResult.setText("–") - self.lblStatus2aResult.setText("–") - self.lblStatus3aResult.setText("–") - self.lblStatus4a.setText("Stopped.") - self.lblStatus4aResult.setText("") - self.btnCancel.setEnabled(False) - self.SetProgressBars(min=0, max=1, value=0) - self.btnCancel.setEnabled(False) - - if "info_type" in args.keys() and "info_msg" in args.keys(): - if args["info_type"] == "msgbox_critical": - msgbox = QtWidgets.QMessageBox(parent=self, icon=QtWidgets.QMessageBox.Critical, windowTitle="{:s} {:s}".format(APPNAME, VERSION), text=args["info_msg"], standardButtons=QtWidgets.QMessageBox.Ok) - if not '\n' in args["info_msg"]: msgbox.setTextFormat(QtCore.Qt.RichText) - msgbox.exec() - elif args["info_type"] == "msgbox_information": - msgbox = QtWidgets.QMessageBox(parent=self, icon=QtWidgets.QMessageBox.Information, windowTitle="{:s} {:s}".format(APPNAME, VERSION), text=args["info_msg"], standardButtons=QtWidgets.QMessageBox.Ok) - if not '\n' in args["info_msg"]: msgbox.setTextFormat(QtCore.Qt.RichText) - msgbox.exec() - elif args["info_type"] == "label": - self.lblStatus4a.setText(args["info_msg"]) - - return - - else: - self.SetProgressBars(min=0, max=size, value=pos) - self.btnCancel.setEnabled(True) - self.lblStatus1aResult.setText(self.formatFileSize(pos)) - if speed > 0: - self.lblStatus2aResult.setText("{:.2f} KB/s".format(speed)) - else: - self.lblStatus2aResult.setText("Pending...") - if left > 0: - self.lblStatus4aResult.setText(self.formatProgressTime(left)) - else: - self.lblStatus4aResult.setText("Pending...") - if elapsed > 0: - self.lblStatus3aResult.setText(self.formatProgressTime(elapsed)) - - if speed == 0 and "skipping" in args and args["skipping"] is True: - self.lblStatus4aResult.setText("Pending...") - self.lblStatus4a.setText("Time left:") - - def SetProgressBars(self, min=0, max=100, value=0, setPause=None): - self.prgStatus.setMinimum(min) - self.prgStatus.setMaximum(max) - self.prgStatus.setValue(value) - if self.TBPROG is not None: - if not value > max: - self.TBPROG.setRange(min, max) - self.TBPROG.setValue(value) - if value != min and value != max: - self.TBPROG.setVisible(True) - else: - self.TBPROG.setVisible(False) - if setPause is not None: - self.TBPROG.setPaused(setPause) - else: - self.TBPROG.setPaused(False) - - def ShowPocketCameraWindow(self): - self.CAMWIN = None - self.CAMWIN = PocketCameraWindow(self, icon=self.windowIcon()) - self.CAMWIN.setAttribute(QtCore.Qt.WA_DeleteOnClose, True) - self.CAMWIN.setModal(True) - self.CAMWIN.run() - - def dragEnterEvent(self, e): - if self._dragEventHover(e): - e.accept() - else: - e.ignore() - - def dragMoveEvent(self, e): - if self._dragEventHover(e): - e.accept() - else: - e.ignore() - - def _dragEventHover(self, e): - if self.btnHeaderRefresh.isEnabled() and self.grpActions.isEnabled() and e.mimeData().hasUrls: - for url in e.mimeData().urls(): - if platform.system() == 'Darwin': - fn = str(NSURL.URLWithString_(str(url.toString())).filePathURL().path()) - else: - fn = str(url.toLocalFile()) - - fn_split = os.path.splitext(os.path.abspath(fn)) - if fn_split[1] == ".sav": - return True - elif self.CONN.GetMode() == "DMG" and fn_split[1] in (".gb", ".sgb", ".gbc", ".bin"): - return True - elif self.CONN.GetMode() == "AGB" and fn_split[1] in (".gba", ".srl"): - return True - else: - return False - return False - - def dropEvent(self, e): - if self.btnHeaderRefresh.isEnabled() and self.grpActions.isEnabled() and e.mimeData().hasUrls: - e.setDropAction(QtCore.Qt.CopyAction) - e.accept() - for url in e.mimeData().urls(): - if platform.system() == 'Darwin': - fn = str(NSURL.URLWithString_(str(url.toString())).filePathURL().path()) - else: - fn = str(url.toLocalFile()) - - fn_split = os.path.splitext(os.path.abspath(fn)) - if fn_split[1] in (".gb", ".sgb", ".gbc", ".bin", ".gba", ".srl"): - self.FlashROM(fn) - elif fn_split[1] == ".sav": - self.WriteRAM(fn) - else: - e.ignore() - - def closeEvent(self, event): - self.DisconnectDevice() - event.accept() - - def run(self): - self.layout.update() - self.layout.activate() - screen = QtGui.QGuiApplication.screens()[0] - screenGeometry = screen.geometry() - x = (screenGeometry.width() - self.width()) / 2 - y = (screenGeometry.height() - self.height()) / 2 - self.move(x, y) - self.setAcceptDrops(True) - self.show() - - # Taskbar Progress on Windows only - try: - from PySide2.QtWinExtras import QWinTaskbarButton, QtWin - myappid = 'lesserkuma.flashgbx' - QtWin.setCurrentProcessExplicitAppUserModelID(myappid) - taskbar_button = QWinTaskbarButton() - self.TBPROG = taskbar_button.progress() - self.TBPROG.setRange(0, 100) - taskbar_button.setWindow(self.windowHandle()) - self.TBPROG.setVisible(False) - except ImportError: - pass - - qt_app.exec_() + return { "flashcarts":flashcarts, "config_ret":ret } +class ArgParseCustomFormatter(argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter): pass def main(portableMode=False): + if platform.system() == "Windows": os.system("color") + os.environ['QT_MAC_WANTS_LAYER'] = '1' + + print("{:s} {:s} by Lesserkuma".format(Util.APPNAME, Util.VERSION)) + print("\nDISCLAIMER: This software is provided as-is and the developer is not responsible for any damage that is caused by the use of it. Use at your own risk!") + print("\nFor troubleshooting please visit https://github.com/lesserkuma/FlashGBX\n") + if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'): app_path = os.path.dirname(sys.executable) else: app_path = os.path.dirname(os.path.abspath(__file__)) - cp = { "subdir":app_path + "/config", "appdata":QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.AppConfigLocation) } + try: + from . import FlashGBX_GUI + from PySide2 import QtCore + cp = { "subdir":app_path + "/config", "appdata":QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.AppConfigLocation) } + except: + cp = { "subdir":app_path + "/config" } if portableMode: cfgdir_default = "subdir" else: cfgdir_default = "appdata" - parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) - parser.add_argument("--reset", help="clears all settings such as last used directory information", action="store_true") - parser.add_argument("--cfgdir", choices=["appdata", "subdir"], type = str.lower, default=cfgdir_default, help="sets the config directory to either the OS-provided local app config directory (" + cp['appdata'] + "), or a subdirectory of this application (" + cp['subdir'].replace("\\", "/") + ")") - args = parser.parse_args() - config_path = cp[args.cfgdir] - - print("{:s} {:s} by Lesserkuma".format(APPNAME, VERSION)) - print("\nDISCLAIMER: This software is provided as-is and the developer is not responsible for any damage that is caused by the use of it. Use at your own risk!") - print("\nFor troubleshooting please visit https://github.com/lesserkuma/FlashGBX") - - app = FlashGBX({"app_path":app_path, "config_path":config_path, "argparsed":args}) - app.run() + examples = "\nexamples:\n" + \ + " Backup the ROM of a Game Boy Advance cartridge:\n\t{:s} --mode agb --action backup-rom\n\n".format(sys.argv[0]) + \ + " Backup Save Data from a Game Boy cartridge:\n\t{:s} --mode dmg --action backup-save\n\n".format(sys.argv[0]) + \ + " Write a Game Boy Advance ROM relying on auto-detecting the flash cartridge:\n\t{:s} --mode agb --action flash-rom ROM.gba\n\n".format(sys.argv[0]) + \ + " Extract Game Boy Camera pictures as .png files from a save data file:\n\t{:s} --mode dmg --action gbcamera-extract --gbcamera-outfile-format png --path GAMEBOYCAMERA.sav\n\n".format(sys.argv[0]) -qt_app = QtWidgets.QApplication(sys.argv) -qt_app.setApplicationName(APPNAME) + parser = argparse.ArgumentParser(formatter_class=ArgParseCustomFormatter, epilog=examples) + try: + parser._action_groups[1].title = "general arguments" + except: + pass + parser.add_argument("--cli", help="force command line interface mode", action="store_true") + parser.add_argument("--reset", help="clears all settings such as last used directory information", action="store_true") + parser.add_argument("--debug", help="enable debug messages used for development", action="store_true") + + parser.add_argument_group('') + ap_config = parser.add_argument_group('configuration arguments') + if "appdata" in cp: ap_config.add_argument("--cfgdir", choices=["appdata", "subdir"], type=str.lower, default=cfgdir_default, help="sets the config directory to either the OS-provided local app config directory (" + cp['appdata'] + "), or a subdirectory of this application (" + cp['subdir'].replace("\\", "/") + ")") + + ap_cli1 = parser.add_argument_group('main command line interface arguments') + ap_cli1.add_argument("--mode", choices=["dmg", "agb"], type=str.lower, default=None, help="set cartridge mode to \"dmg\" (Game Boy) or \"agb\" (Game Boy Advance)") + ap_cli1.add_argument("--action", choices=["info", "backup-rom", "flash-rom", "backup-save", "restore-save", "erase-save", "gbcamera-extract", "debug-probe-save"], type=str.lower, default=None, help="select program action") + ap_cli1.add_argument("--overwrite", action="store_true", help="overwrite without asking if target file already exists") + ap_cli1.add_argument("path", nargs="?", default="auto", help="target or source file path (optional when reading, required when writing)") + + ap_cli2 = parser.add_argument_group('optional command line interface arguments') + ap_cli2.add_argument("--dmg-romsize", choices=["auto", "32kb", "64kb", "128kb", "256kb", "512kb", "1mb", "2mb", "4mb", "8mb"], type=str.lower, default="auto", help="set size of Game Boy cartridge ROM data") + ap_cli2.add_argument("--dmg-mbc", choices=["auto", "1", "2", "3", "5", "6", "7"], type=str.lower, default="auto", help="set memory bank controller type of Game Boy cartridge") + ap_cli2.add_argument("--dmg-savesize", choices=["auto", "4k", "16k", "64k", "256k", "512k", "1m", "eeprom2k", "eeprom4k", "tama5"], type=str.lower, default="auto", help="set size of Game Boy cartridge save data") + ap_cli2.add_argument("--agb-romsize", choices=["auto", "4mb", "8mb", "16mb", "32mb", "64mb"], type=str.lower, default="auto", help="set size of Game Boy Advance cartridge ROM data") + ap_cli2.add_argument("--agb-savetype", choices=["auto", "eeprom4k", "eeprom64k", "sram256k", "sram512k", "sram1m", "flash512k", "flash1m"], type=str.lower, default="auto", help="set type of Game Boy cartridge save data") + ap_cli2.add_argument("--store-rtc", action="store_true", help="store RTC register values if supported") + ap_cli2.add_argument("--ignore-bad-header", action="store_true", help="don’t stop if invalid data found in cartridge header data") + ap_cli2.add_argument("--fast-read-mode", action="store_true", help="enable experimental fast read mode for GBxCart RW v1.3") + ap_cli2.add_argument("--flashcart-handler", type=str, default="autodetect", help="name of flash cart; see txt files in config directory") + ap_cli2.add_argument("--prefer-chip-erase", action="store_true", help="prefer full chip erase over sector erase when both available") + ap_cli2.add_argument("--reversed-sectors", action="store_true", help="use reversed flash sectors if possible") + ap_cli2.add_argument("--force-5v", action="store_true", help="force 5V when writing Game Boy flash cartridges") + ap_cli2.add_argument("--no-verify-flash", action="store_true", help="do not verify written ROM data") + ap_cli2.add_argument("--save-filename-add-datetime", action="store_true", help="adds a timestamp to the file name of save data backups") + ap_cli2.add_argument("--gbcamera-palette", choices=["grayscale", "dmg", "sgb", "cgb1", "cgb2", "cgb3"], type=str.lower, default="grayscale", help="sets the palette of pictures extracted from Game Boy Camera saves") + ap_cli2.add_argument("--gbcamera-outfile-format", choices=["png", "bmp", "gif", "jpg"], type=str.lower, default="png", help="sets the file format of saved pictures extracted from Game Boy Camera saves") + args = parser.parse_args() + + if "appdata" in cp: + config_path = cp[args.cfgdir] + else: + config_path = cp["subdir"] + + if args.mode is not None or args.action is not None: + args.cli = True + + if args.debug == True: + Util.DEBUG = True + + args = {"app_path":app_path, "config_path":config_path, "argparsed":args} + args.update(LoadConfig(args)) + + app = None + exc = None + if not args["argparsed"].cli: + try: + from . import FlashGBX_GUI + app = FlashGBX_GUI.FlashGBX_GUI(args) + except: + exc = traceback.format_exc() + app = None + + if app is None: + from . import FlashGBX_CLI + if args["argparsed"].action is None: + parser.print_help() + print("\n\n{:s}ERROR: GUI mode couldn’t be launched, but the application can be run in CLI mode.\n Command line switches are explained above.{:s}\n".format(Util.ANSI.RED, Util.ANSI.RESET)) + if exc is not None: print("{:s}{:s}{:s}".format(Util.ANSI.YELLOW, exc, Util.ANSI.RESET)) + + print("Now running in CLI mode.\n") + app = FlashGBX_CLI.FlashGBX_CLI(args) + try: + app.run() + except KeyboardInterrupt: + print("\n\nProgram stopped.") + return + + app.run() + + else: + from . import FlashGBX_CLI + print("Now running in CLI mode.\n") + app = FlashGBX_CLI.FlashGBX_CLI(args) + try: + app.run() + except KeyboardInterrupt: + print("\n\nProgram stopped.") diff --git a/FlashGBX/FlashGBX_CLI.py b/FlashGBX/FlashGBX_CLI.py new file mode 100644 index 0000000..c090bc3 --- /dev/null +++ b/FlashGBX/FlashGBX_CLI.py @@ -0,0 +1,955 @@ +# -*- coding: utf-8 -*- +# UTF-8 +import datetime, shutil, platform, os, json, math, traceback, re, time +try: + # pylint: disable=import-error + import readline + readline.set_completer_delims('\t\n=') + readline.parse_and_bind("tab:complete") +except: + pass + +from .RomFileDMG import RomFileDMG +from .RomFileAGB import RomFileAGB +from .PocketCamera import PocketCamera +from .Util import APPNAME, VERSION, ANSI +from . import Util +from . import hw_GBxCartRW +hw_devices = [hw_GBxCartRW] + +class FlashGBX_CLI(): + ARGS = {} + CONFIG_PATH = "" + FLASHCARTS = { "DMG":{}, "AGB":{} } + CONN = None + DEVICE = None + PROGRESS = None + + def __init__(self, args): + self.ARGS = args + self.CONFIG_PATH = args['config_path'] + self.FLASHCARTS = args["flashcarts"] + self.PROGRESS = Util.Progress(self.UpdateProgress) + + global prog_bar_part_char + if platform.system() == "Windows": + prog_bar_part_char = [" ", " ", " ", " ", "▌", "▌", "▌", "▌"] + else: + prog_bar_part_char = [" ", "▏", "▎", "▍", "▌", "▋", "▊", "▉"] + + def run(self): + config_ret = self.ARGS["config_ret"] + for i in range(0, len(config_ret)): + if config_ret[i][0] < 1: + print(config_ret[i][1]) + elif config_ret[i][0] == 2: + print("{:s}{:s}{:s}".format(ANSI.YELLOW, config_ret[i][1], ANSI.RESET)) + elif config_ret[i][0] == 2: + print("{:s}{:s}{:s}".format(ANSI.RED, config_ret[i][1], ANSI.RESET)) + + args = self.ARGS["argparsed"] + config_path = Util.formatPathOS(self.CONFIG_PATH) + print("Configuration directory: {:s}\n".format(config_path)) + + # Ask interactively if no args set + if args.action is None: + actions = ["info", "backup-rom", "flash-rom", "backup-save", "restore-save", "erase-save", "gbcamera-extract", "debug-probe-save"] + print("Select Operation:\n 1) Read Cartridge Information\n 2) Backup ROM\n 3) Flash ROM\n 4) Backup Save Data\n 5) Restore Save Data\n 6) Erase Save Data\n 7) Extract Game Boy Camera Pictures\n") + args.action = input("Enter number 1-7 [1]: ").lower().strip() + print("") + try: + args.action = actions[int(args.action) - 1] + except: + if args.action == "": + args.action = "info" + else: + print("Canceled.") + return + + if args.action is None or args.action not in ("gbcamera-extract"): + if not self.FindDevices(): + print("No devices found.") + return + else: + if not self.ConnectDevice(): + print("Couldn’t connect to the device.") + return + print("Connected to {:s}\n".format(self.DEVICE[0])) + + if args.action == "gbcamera-extract": + if args.path == "auto": + args.path = input("Enter file path of Game Boy Camera save data file: ").strip().replace("\"", "") + print("") + if args.path == "": + print("Canceled.") + return + + pc = PocketCamera() + if pc.LoadFile(args.path) != False: + palettes = [ "grayscale", "dmg", "sgb", "cgb1", "cgb2", "cgb3" ] + pc.SetPalette(palettes.index(args.gbcamera_palette)) + file = os.path.splitext(args.path)[0] + "/IMG_PC00.png" + if os.path.isfile(os.path.dirname(file)): + print("\n{:s}Can’t save pictures at location “{:s}”.{:s}\n".format(ANSI.RED, os.path.abspath(os.path.dirname(file)), ANSI.RESET)) + return + if not os.path.isdir(os.path.dirname(file)): + os.makedirs(os.path.dirname(file)) + for i in range(0, 31): + file = os.path.splitext(args.path)[0] + "/IMG_PC{:02d}".format(i) + "." + args.gbcamera_outfile_format + pc.ExportPicture(i, file) + print("The pictures from “{:s}” were extracted to “{:s}”.".format(os.path.abspath(args.path), Util.formatPathOS(os.path.abspath(os.path.dirname(file)), end_sep=True) + "IMG_PC**.{:s}".format(args.gbcamera_outfile_format))) + else: + print("\n{:s}Couldn’t parse the save data file.{:s}\n".format(ANSI.RED, ANSI.RESET)) + return + + if args.mode is None: + print("Select Cartridge Mode:\n 1) Game Boy or Game Boy Color\n 2) Game Boy Advance\n") + answer = input("Enter number 1-2 [2]: ").lower().strip() + print("") + if answer == "1": + args.mode = "dmg" + elif answer == "2" or answer == "": + args.mode = "agb" + else: + print("Canceled.") + return + print("") + + if args.mode == "dmg": + print("Cartridge Mode: Game Boy") + self.CONN.SetMode("DMG") + else: + print("Cartridge Mode: Game Boy Advance") + self.CONN.SetMode("AGB") + time.sleep(0.2) + + header = self.CONN.ReadInfo() + (bad_read, s_header, header) = self.ReadCartridge(header) + if s_header == "": + print("\n{:s}Couldn’t read cartridge header. Please try again.{:s}\n".format(ANSI.RED, ANSI.RESET)) + return + elif bad_read and not args.ignore_bad_header: + print("\n{:s}Invalid data was detected which usually means that the cartridge couldn’t be read correctly. Please make sure you selected the correct mode and that the cartridge contacts are clean. This check can be disabled with the command line switch “--ignore-bad-header”.{:s}\n".format(ANSI.RED, ANSI.RESET)) + print("Cartridge Information:") + print(s_header) + return + + print("\nCartridge Information:") + print(s_header) + + if args.action == "backup-rom": + self.BackupROM(args, header) + + elif args.action == "backup-save": + self.BackupRestoreRAM(args, header) + + elif args.action == "restore-save": + if args.path == "auto": + args.path = input("Enter file path of save data file: ").strip().replace("\"", "") + print("") + if args.path == "": + print("Canceled.") + return + self.BackupRestoreRAM(args, header) + + elif args.action == "erase-save": + self.BackupRestoreRAM(args, header) + + elif args.action == "debug-probe-save": + self.BackupRestoreRAM(args, header) + + elif args.action == "flash-rom": + if args.path == "auto": + args.path = input("Enter file path of ROM file: ").strip().replace("\"", "") + print("") + if args.path == "": + print("Canceled.") + return + self.FlashROM(args, header) + + if args.action != "info": + print("") + + self.DisconnectDevice() + return 0 + + def UpdateProgress(self, args): + if args is None: return + + if "error" in args: + print("{:s}{:s}{:s}".format(ANSI.RED, args["error"], ANSI.RESET)) + return + + pos = 0 + size = 0 + speed = 0 + elapsed = 0 + left = 0 + if "pos" in args: pos = args["pos"] + if "size" in args: size = args["size"] + if "speed" in args: speed = args["speed"] + if "time_elapsed" in args: elapsed = args["time_elapsed"] + if "time_left" in args: left = args["time_left"] + + if "action" in args: + if args["action"] == "INITIALIZE": + if args["method"] == "ROM_WRITE_VERIFY": + print("\n\nThe newly written ROM data will now be checked for errors.\n") + elif args["action"] == "ERASE": + print("\033[KPlease wait while the flash chip is being erased... (Elapsed time: {:s})".format(Util.formatProgressTime(elapsed)), end="\r") + elif args["action"] == "SECTOR_ERASE": + print("\033[KErasing flash sector at address 0x{:X}...".format(args["sector_pos"]), end="\r") + elif args["action"] == "ABORTING": + print("\nStopping...") + pass + elif args["action"] == "FINISHED": + print("\n") + self.FinishOperation() + elif args["action"] == "ABORT": + print("\nOperation stopped.\n") + if "info_type" in args.keys() and "info_msg" in args.keys(): + if args["info_type"] == "msgbox_critical": + print(ANSI.RED + args["info_msg"] + ANSI.RESET) + elif args["info_type"] == "msgbox_information": + print(args["info_msg"]) + elif args["info_type"] == "label": + print(args["info_msg"]) + return + elif args["action"] == "PROGRESS": + # pv style progress status + prog_str = "{:s}/{:s} {:s} [{:s}KB/s] [{:s}] {:s}% ETA {:s} ".format(Util.formatFileSize(size=pos).replace(" ", "").replace("Bytes", "B").replace("Byte", "B").rjust(8), Util.formatFileSize(size=size).replace(" ", "").replace("Bytes", "B"), Util.formatProgressTimeShort(elapsed), "{:.2f}".format(speed).rjust(6), "%PROG_BAR%", "{:d}".format(int(pos/size*100)).rjust(3), Util.formatProgressTimeShort(left)) + prog_width = shutil.get_terminal_size((80, 20))[0] - (len(prog_str) - 10) + progress = min(1, max(0, pos/size)) + whole_width = math.floor(progress * prog_width) + remainder_width = (progress * prog_width) % 1 + part_width = math.floor(remainder_width * 8) + try: + part_char = prog_bar_part_char[part_width] + if (prog_width - whole_width - 1) < 0: part_char = "" + prog_bar = "█" * whole_width + part_char + " " * (prog_width - whole_width - 1) + print(prog_str.replace("%PROG_BAR%", prog_bar), end="\r") + except UnicodeEncodeError: + prog_bar = "#" * whole_width + " " * (prog_width - whole_width) + print(prog_str.replace("%PROG_BAR%", prog_bar), end="\r", flush=True) + except: + pass + + def FinishOperation(self): + if self.CONN.INFO["last_action"] == 4: # Flash ROM + self.CONN.INFO["last_action"] = 0 + if "verified" in self.PROGRESS.PROGRESS and self.PROGRESS.PROGRESS["verified"] == True: + print("{:s}The ROM was flashed and verified successfully!{:s}".format(ANSI.GREEN, ANSI.RESET)) + else: + print("ROM flashing complete!") + + elif self.CONN.INFO["last_action"] == 1: # Backup ROM + self.CONN.INFO["last_action"] = 0 + if self.CONN.GetMode() == "DMG": + print("Checksum: 0x{:04X}".format(self.CONN.INFO["rom_checksum_calc"])) + print("SHA-1: {:s}\n".format(self.CONN.INFO["file_sha1"])) + if self.CONN.INFO["rom_checksum"] == self.CONN.INFO["rom_checksum_calc"]: + print("{:s}The ROM backup is complete and the checksum was verified successfully!{:s}".format(ANSI.GREEN, ANSI.RESET)) + elif "DMG-MMSA-JPN" in self.ARGS["argparsed"].flashcart_handler: + print("The ROM backup is complete!") + else: + print("{:s}The ROM was dumped, but the checksum is not correct. This may indicate a bad dump, however this can be normal for some reproduction prototypes, patched games and intentional overdumps.{:s}".format(ANSI.YELLOW, ANSI.RESET)) + elif self.CONN.GetMode() == "AGB": + print("CRC32: 0x{:08X}".format(self.CONN.INFO["rom_checksum_calc"])) + print("SHA-1: {:s}\n".format(self.CONN.INFO["file_sha1"])) + if Util.AGB_Global_CRC32 == self.CONN.INFO["rom_checksum_calc"]: + print("{:s}The ROM backup is complete and the checksum was verified successfully!{:s}".format(ANSI.GREEN, ANSI.RESET)) + elif Util.AGB_Global_CRC32 == 0: + print("The ROM backup is complete! As there is no known checksum for this ROM in the database, verification was skipped.") + else: + print("{:s}The ROM backup is complete, but the checksum doesn’t match the known database entry. This may indicate a bad dump, however this can be normal for some reproduction cartridges, prototypes, patched games and intentional overdumps.{:s}".format(ANSI.YELLOW, ANSI.RESET)) + + elif self.CONN.INFO["last_action"] == 2: # Backup RAM + self.CONN.INFO["last_action"] = 0 + if not "debug" in self.ARGS and self.CONN.INFO["transferred"] == 131072: # 128 KB + with open(self.CONN.INFO["last_path"], "rb") as file: temp = file.read() + if temp[0x1FFB1:0x1FFB6] == b'Magic': + answer = input("Game Boy Camera save data was detected.\nWould you like to extract all pictures to “{:s}” now? [Y/n]: ".format(Util.formatPathOS(os.path.abspath(os.path.splitext(self.CONN.INFO["last_path"])[0]), end_sep=True) + "IMG_PC**.{:s}".format(self.ARGS["argparsed"].gbcamera_outfile_format))).strip().lower() + if answer != "n": + pc = PocketCamera() + if pc.LoadFile(self.CONN.INFO["last_path"]) != False: + palettes = [ "grayscale", "dmg", "sgb", "cgb1", "cgb2", "cgb3" ] + pc.SetPalette(palettes.index(self.ARGS["argparsed"].gbcamera_palette)) + file = os.path.splitext(self.CONN.INFO["last_path"])[0] + "/IMG_PC00.png" + if os.path.isfile(os.path.dirname(file)): + print("Can’t save pictures at location “{:s}”.".format(os.path.abspath(os.path.dirname(file)))) + return + if not os.path.isdir(os.path.dirname(file)): + os.makedirs(os.path.dirname(file)) + for i in range(0, 31): + file = os.path.splitext(self.CONN.INFO["last_path"])[0] + "/IMG_PC{:02d}".format(i) + "." + self.ARGS["argparsed"].gbcamera_outfile_format + pc.ExportPicture(i, file) + print("The pictures were extracted.") + print("") + + print("The save data backup is complete!") + + elif self.CONN.INFO["last_action"] == 3: # Restore RAM + self.CONN.INFO["last_action"] = 0 + if "save_erase" in self.CONN.INFO and self.CONN.INFO["save_erase"]: + print("The save data was erased.") + del(self.CONN.INFO["save_erase"]) + else: + print("The save data was restored!") + + else: + self.CONN.INFO["last_action"] = 0 + + def FindDevices(self, connectToFirst=False): + global hw_devices + for hw_device in hw_devices: + dev = hw_device.GbxDevice() + ret = dev.Initialize(self.FLASHCARTS) + if ret is False: + self.CONN = None + elif isinstance(ret, list): + if len(ret) > 0: print("\n") + for i in range(0, len(ret)): + status = ret[i][0] + msg = re.sub('<[^<]+?>', '', ret[i][1]) + if status == 3: + print("{:s}{:s}{:s}".format(ANSI.RED, msg.replace("\n\n", "\n"), ANSI.RESET)) + self.CONN = None + + if dev.IsConnected(): + self.DEVICE = (dev.GetFullName(), dev) + dev.Close() + break + + if self.DEVICE is None: return False + return True + + def ConnectDevice(self): + dev = self.DEVICE[1] + ret = dev.Initialize(self.FLASHCARTS) + + if ret is False: + print("\n{:s}An error occured while trying to connect to the device.{:s}".format(ANSI.RED, ANSI.RESET)) + traceback.print_stack() + self.CONN = None + return False + + elif isinstance(ret, list): + for i in range(0, len(ret)): + status = ret[i][0] + msg = re.sub('<[^<]+?>', '', ret[i][1]) + if status == 0: + print("\n" + msg) + elif status == 1: + print("{:s}".format(msg)) + elif status == 2: + print("{:s}{:s}{:s}".format(ANSI.YELLOW, msg, ANSI.RESET)) + elif status == 3: + print("{:s}{:s}{:s}".format(ANSI.RED, msg, ANSI.RESET)) + self.CONN = None + return False + + self.CONN = dev + return True + + def DisconnectDevice(self): + try: + devname = self.CONN.GetFullName() + self.CONN.Close() + print("Disconnected from {:s}".format(devname)) + except: + pass + self.CONN = None + + def ReadCartridge(self, data): + bad_read = False + str = "" + if self.CONN.GetMode() == "DMG": + str += "Game Title/Code: {:s}\n".format(data["game_title"]) + str += "Super Game Boy: " + if data['sgb'] in Util.DMG_Header_SGB: + str += "{:s}\n".format(Util.DMG_Header_SGB[data['sgb']]) + else: + str += "Unknown (0x{:02X})\n".format(data['sgb']) + bad_read = True + str += "Game Boy Color: " + if data['cgb'] in Util.DMG_Header_CGB: + str += "{:s}\n".format(Util.DMG_Header_CGB[data['cgb']]) + else: + str += "Unknown (0x{:02X})\n".format(data['cgb']) + bad_read = True + if data["logo_correct"]: + str += "Nintendo Logo: OK\n" + else: + str += "Nintendo Logo: {:s}Invalid{:s}\n".format(ANSI.RED, ANSI.RESET) + bad_read = True + if data['header_checksum_correct']: + str += "Header Checksum: Valid (0x{:02X})\n".format(data['header_checksum']) + else: + str += "Header Checksum: {:s}Invalid (0x{:02X}){:s}\n".format(ANSI.RED, data['header_checksum'], ANSI.RESET) + bad_read = True + str += "ROM Checksum: 0x{:04X}\n".format(data['rom_checksum']) + try: + str += "ROM Size: {:s}\n".format(Util.DMG_Header_ROM_Sizes[data['rom_size_raw']]) + except: + str += "ROM Size: {:s}Not detected{:s}\n".format(ANSI.RED, ANSI.RESET) + bad_read = True + + try: + if data['features_raw'] == 0x06: # MBC2 + str += "Save Type: {:s}\n".format(Util.DMG_Header_RAM_Sizes[1]) + elif data['features_raw'] == 0x22 and data["game_title"] in ("KORO2 KIRBYKKKJ", "KIRBY TNT__KTNE"): # MBC7 Kirby + str += "Save Type: {:s}\n".format(Util.DMG_Header_RAM_Sizes[Util.DMG_Header_RAM_Sizes_Map.index(0x101)]) + elif data['features_raw'] == 0x22 and data["game_title"] in ("CMASTER____KCEJ"): # MBC7 Command Master + str += "Save Type: {:s}\n".format(Util.DMG_Header_RAM_Sizes[Util.DMG_Header_RAM_Sizes_Map.index(0x102)]) + elif data['features_raw'] == 0xFD: # TAMA5 + str += "Save Type: {:s}\n".format(Util.DMG_Header_RAM_Sizes[Util.DMG_Header_RAM_Sizes_Map.index(0x103)]) + else: + str += "Save Type: {:s}\n".format(Util.DMG_Header_RAM_Sizes[Util.DMG_Header_RAM_Sizes_Map.index(data['ram_size_raw'])]) + except: + str += "Save Type: Not detected\n" + + try: + str += "Mapper Type: {:s}\n".format(Util.DMG_Header_Features[data['features_raw']]) + except: + str += "Mapper Type: {:s}Not detected{:s}\n".format(ANSI.RED, ANSI.RESET) + bad_read = True + + if data['logo_correct'] and not self.CONN.IsSupportedMbc(data["features_raw"]): + print("{:s}\nWARNING: This cartridge uses a Memory Bank Controller that may not be completely supported yet. A future version of {:s} may add support for it.{:s}".format(ANSI.YELLOW, APPNAME, ANSI.RESET)) + if data['logo_correct'] and data['game_title'] == "NP M-MENU MENU" and self.ARGS["argparsed"].flashcart_handler == "autodetect": + cart_types = self.CONN.GetSupportedCartridgesDMG() + for i in range(0, len(cart_types[0])): + if "DMG-MMSA-JPN" in cart_types[0][i]: + self.ARGS["argparsed"].flashcart_handler = cart_types[0][i] + + elif self.CONN.GetMode() == "AGB": + str += "Game Title: {:s}\n".format(data["game_title"]) + str += "Game Code: {:s}\n".format(data["game_code"]) + str += "Revision: {:d}\n".format(data["version"]) + if data["logo_correct"]: + str += "Nintendo Logo: OK\n" + else: + str += "Nintendo Logo: {:s}Invalid{:s}\n".format(ANSI.RED, ANSI.RESET) + bad_read = True + if data["96h_correct"]: + str += "Cartridge Identifier: OK\n" + else: + str += "Cartridge Identifier: {:s}Invalid{:s}\n".format(ANSI.RED, ANSI.RESET) + bad_read = True + if data['header_checksum_correct']: + str += "Header Checksum: Valid (0x{:02X})\n".format(data['header_checksum']) + else: + str += "Header Checksum: {:s}Invalid (0x{:02X}){:s}\n".format(ANSI.RED, data['header_checksum'], ANSI.RESET) + bad_read = True + + str += "ROM Checksum: " + Util.AGB_Global_CRC32 = 0 + db_agb_entry = None + if os.path.exists("{0:s}/db_AGB.json".format(self.CONFIG_PATH)): + with open("{0:s}/db_AGB.json".format(self.CONFIG_PATH)) as f: + db_agb = f.read() + db_agb = json.loads(db_agb) + if data["header_sha1"] in db_agb.keys(): + db_agb_entry = db_agb[data["header_sha1"]] + else: + str += "Not in database\n" + else: + str += "FAIL: Database for Game Boy Advance titles not found in {:s}/db_AGB.json\n".format(self.CONFIG_PATH) + + if db_agb_entry != None: + if data["rom_size_calc"] < 0x400000: + str += "In database (0x{:06X})\n".format(db_agb_entry['rc']) + Util.AGB_Global_CRC32 = db_agb_entry['rc'] + str += "ROM Size: {:d} MB\n".format(int(db_agb_entry['rs']/1024/1024)) + data['rom_size'] = db_agb_entry['rs'] + + elif data["rom_size"] != 0: + if not data["rom_size"] in Util.AGB_Header_ROM_Sizes_Map: + data["rom_size"] = 0x2000000 + str += "ROM Size: {:d} MB\n".format(int(data["rom_size"]/1024/1024)) + else: + str += "ROM Size: Not detected\n" + bad_read = True + + stok = False + if data["save_type"] == None: + if db_agb_entry != None: + if db_agb_entry['st'] < len(Util.AGB_Header_Save_Types): + stok = True + str += "Save Type: {:s}\n".format(Util.AGB_Header_Save_Types[db_agb_entry['st']]) + data["save_type"] = db_agb_entry['st'] + if stok is False: + str += "Save Type: Not detected\n" + + if data['logo_correct'] and isinstance(db_agb_entry, dict) and "rs" in db_agb_entry and db_agb_entry['rs'] == 0x4000000 and not self.CONN.IsSupported3dMemory(): + print("{:s}\nWARNING: This cartridge uses a Memory Bank Controller that may not be completely supported yet. A future version of the {:s} device firmware may add support for it.{:s}".format(ANSI.YELLOW, self.CONN.GetName(), ANSI.RESET)) + + return (bad_read, str, data) + + def CartridgeTypeAutoDetect(self, limitVoltage=True, knownCartCFI=False): + cart_type = 0 + cart_text = "" + + print("Now attempting to auto-detect the flash cartridge type...") + if self.CONN.CheckROMStable() is False: + print("{:s}Unstable ROM reading detected. Please make sure you selected the correct mode and that the cartridge contacts are clean.{:s}".format(ANSI.RED, ANSI.RESET)) + return -1 + + if self.CONN.GetMode() in self.FLASHCARTS and len(self.FLASHCARTS[self.CONN.GetMode()]) == 0: + print("{:s}No flash cartridge type configuration files found. Try to restart the application with the “--reset” command line switch to reset the configuration.{:s}".format(ANSI.RED, ANSI.RESET)) + return -2 + + detected = self.CONN.AutoDetectFlash(limitVoltage) + if len(detected) == 0: + print("\n{:s}No pre-configured flash cartridge type was detected.{:s} You can still manually specify one using the “--flashcart-handler” command line switch -- look for similar PCB text and/or flash chip markings. However, chances are this cartridge is currently not supported for flashing with {:s}.\n".format(ANSI.YELLOW, ANSI.RESET, APPNAME)) + + (flash_id, cfi_s, cfi) = self.CONN.CheckFlashChip(limitVoltage) + if cfi_s == "": + print("Flash chip query result:\n" + flash_id + "\nThere was no Common Flash Interface (CFI) response from the cartridge. Please clean the cartridge contacts and make sure that the cartridge is seated correctly. If a flash chip exists on the cartridge PCB, it may be too old or require unique unlocking and handling.") + else: + print("Flash chip query result:\n" + flash_id + "\n" + str(cfi_s)) + with open(self.CONFIG_PATH + "/cfi.bin", "wb") as f: f.write(cfi['raw']) + + else: + cart_type = detected[0] + size_undetected = False + sectors_undetected = False + if self.CONN.GetMode() == "DMG": cart_types = self.CONN.GetSupportedCartridgesDMG() + elif self.CONN.GetMode() == "AGB": cart_types = self.CONN.GetSupportedCartridgesAGB() + size = cart_types[1][detected[0]]["flash_size"] + if "sector_size" in cart_types[1][detected[0]]: + sectors = cart_types[1][detected[0]]["sector_size"] + else: + sectors = [] + + for i in range(0, len(detected)): + if size != cart_types[1][detected[i]]["flash_size"]: + size_undetected = True + if "sector_size_from_cfi" not in cart_types[1][detected[i]] and "sector_size" in cart_types[1][detected[i]] and sectors != cart_types[1][detected[i]]["sector_size"]: + sectors_undetected = True + cart_text += "- " + cart_types[0][detected[i]] + "\n" + + if size_undetected: + (_, cfi_s, cfi) = self.CONN.CheckFlashChip(limitVoltage=limitVoltage, cart_type=cart_types[1][cart_type]) + if isinstance(cfi, dict) and 'device_size' in cfi: + for i in range(0, len(detected)): + if cfi['device_size'] == cart_types[1][detected[i]]["flash_size"]: + cart_type = detected[i] + size_undetected = False + break + + if len(detected) == 1: + msg_text = "The following flash cartridge type was detected:\n" + cart_text + "\nThe supported ROM size is up to {:d} MB.".format(int(cart_types[1][cart_type]['flash_size'] / 1024 / 1024)) + else: + if size_undetected is True: + msg_text = "Your cartridge responds to flash commands also used by:\n" + cart_text + "\nHowever, you may need to manually adjust the ROM size selection.\n\nIMPORTANT: While these cartridges share the same electronic signature, their supported ROM size can differ. As the size can not be detected automatically at this time, please select it manually." + else: + msg_text = "Your cartridge responds to flash commands also used by:\n" + cart_text + "\nThe supported ROM size is up to {:d} MB.".format(int(cart_types[1][cart_type]['flash_size'] / 1024 / 1024)) + + if sectors_undetected and "sector_size_from_cfi" not in cart_types[1][cart_type]: + msg_text = msg_text + "\n\n{:s}IMPORTANT:{:s} While these share most of their attributes, some of them can not be automatically detected. If you encounter any errors while writing a ROM, please manually select the correct type based on the flash chip markings of your cartridge. Unchecking the “Prefer sector erase mode” config option can also help.".format(ANSI.RED, ANSI.RESET) + + print(msg_text) + if knownCartCFI: + (flash_id, cfi_s, cfi) = self.CONN.CheckFlashChip(limitVoltage=limitVoltage, cart_type=cart_types[1][cart_type]) + if cfi_s == "": + print("\nFlash chip query result:\n" + flash_id + "\nThere was no Common Flash Interface (CFI) response from the cartridge. If a flash chip exists on the cartridge PCB, it may be too old or require unique unlocking and handling.") + else: + print("\nFlash chip query result:\n" + flash_id + "\n" + str(cfi_s)) + with open(self.CONFIG_PATH + "/cfi.bin", "wb") as f: f.write(cfi['raw']) + + return cart_type + + def BackupROM(self, args, header): + mbc = 1 + rom_banks = 1 + fast_read_mode = args.fast_read_mode is True + + if self.CONN.GetMode() == "DMG": + if args.dmg_mbc == "auto": + try: + mbc = header["features_raw"] + if mbc == 0: mbc = 5 + except: + print("{:s}Couldn’t determine MBC type, will try to use MBC5. It can also be manually set with the “--dmg-mbc” command line switch.{:s}".format(ANSI.YELLOW, ANSI.RESET)) + mbc = 5 + else: + mbc = int(args.dmg_mbc) + if mbc == 2: mbc = 0x06 + elif mbc == 3: mbc = 0x13 + elif mbc == 5: mbc = 0x19 + elif mbc == 6: mbc = 0x20 + elif mbc == 7: mbc = 0x22 + + if args.dmg_romsize == "auto": + try: + rom_banks = Util.DMG_Header_ROM_Sizes_Flasher_Map[header["rom_size_raw"]] + except: + print("{:s}Couldn’t determine ROM size, will use 8 MB. It can also be manually set with the “--dmg-romsize” command line switch.{:s}".format(ANSI.YELLOW, ANSI.RESET)) + rom_banks = 512 + else: + sizes = [ "auto", "32kb", "64kb", "128kb", "256kb", "512kb", "1mb", "2mb", "4mb", "8mb" ] + rom_banks = Util.DMG_Header_ROM_Sizes_Flasher_Map[sizes.index(args.dmg_romsize) - 1] + rom_size = rom_banks * 0x4000 + + path = header["game_title"].strip().encode('ascii', 'ignore').decode('ascii') + if path == "": path = "ROM" + path = re.sub(r"[<>:\"/\\|\?\*]", "_", path) + if self.CONN.INFO["cgb"] == 0xC0 or self.CONN.INFO["cgb"] == 0x80: + path = path + ".gbc" + elif self.CONN.INFO["sgb"] == 0x03: + path = path + ".sgb" + else: + path = path + ".gb" + + elif self.CONN.GetMode() == "AGB": + if args.agb_romsize == "auto": + rom_size = header["rom_size"] + else: + sizes = [ "auto", "4mb", "8mb", "16mb", "32mb", "64mb" ] + rom_size = Util.AGB_Header_ROM_Sizes_Map[sizes.index(args.agb_romsize) - 1] + + path = header["game_title"].strip().encode('ascii', 'ignore').decode('ascii') + if path == "": path = header["game_code"].strip().encode('ascii', 'ignore').decode('ascii') + if path == "": path = "ROM" + path = re.sub(r"[<>:\"/\\|\?\*]", "_", path) + path = path + ".gba" + + if args.path != "auto": + if os.path.isdir(args.path): + path = args.path + "/" + path + else: + path = args.path + + if (path == ""): return + if not args.overwrite and os.path.exists(os.path.abspath(path)): + answer = input("The target file “{:s}” already exists.\nDo you want to overwrite it? [y/N]: ".format(os.path.abspath(path))).strip().lower() + print("") + if answer != "y": + print("Canceled.") + return + + + try: + f = open(path, "ab+") + f.close() + except (PermissionError, FileNotFoundError): + print("{:s}Couldn’t access “{:s}”.{:s}".format(ANSI.RED, path, ANSI.RESET)) + return + + if fast_read_mode: print("Fast Read Mode enabled.") + s_mbc = "" + if self.CONN.GetMode() == "DMG": s_mbc = " using Cart Type 0x{:X}".format(mbc) + if self.CONN.GetMode() == "DMG": + print("The ROM will now be read{:s} and saved to “{:s}”.".format(s_mbc, os.path.abspath(path))) + else: + print("The ROM will now be read and saved to “{:s}”.".format(os.path.abspath(path))) + + print("") + + cart_type = 0 + if args.flashcart_handler != "autodetect": + if self.CONN.GetMode() == "DMG": + carts = self.CONN.GetSupportedCartridgesDMG()[1] + elif self.CONN.GetMode() == "AGB": + carts = self.CONN.GetSupportedCartridgesAGB()[1] + cart_type = 0 + for i in range(0, len(carts)): + if not "names" in carts[i]: continue + if carts[i]["type"] != self.CONN.GetMode(): continue + if args.flashcart_handler in carts[i]["names"]: + print("Selected flash cartridge type: {:s}".format(args.flashcart_handler)) + rom_banks = int(carts[i]["flash_size"] / 0x4000) + rom_size = carts[i]["flash_size"] + cart_type = i + break + + self.CONN._TransferData(args={ 'mode':1, 'path':path, 'mbc':mbc, 'rom_banks':rom_banks, 'agb_rom_size':rom_size, 'start_addr':0, 'fast_read_mode':fast_read_mode, 'cart_type':cart_type }, signal=self.PROGRESS.SetProgress) + + def FlashROM(self, args, header): + path = "" + mode = self.CONN.GetMode() + if mode == "DMG": + carts = self.CONN.GetSupportedCartridgesDMG()[1] + elif mode == "AGB": + carts = self.CONN.GetSupportedCartridgesAGB()[1] + else: + return + + cart_type = 0 + for i in range(0, len(carts)): + if not "names" in carts[i]: continue + if carts[i]["type"] != mode: continue + if args.flashcart_handler in carts[i]["names"]: + print("Selected flash cartridge type: {:s}".format(args.flashcart_handler)) + cart_type = i + break + + if cart_type <= 0 and args.flashcart_handler == "autodetect": + if args.force_5v is True: + cart_type = self.CartridgeTypeAutoDetect(limitVoltage=False) + else: + cart_type = self.CartridgeTypeAutoDetect() + if (cart_type == 1): cart_type = 0 + if cart_type == 0: + msg_5v = "" + if mode == "DMG": msg_5v = "If your flash cartridge requires 5V to work, you can use the “--force-5v” command line switch, however please note that 5V can be unsafe for some flash chips." + print("\n{:s}Auto-detection failed. Please use the “--flashcart-handler” command line switch to select the flash cartridge handler manually.\n{:s}{:s}{:s}".format(ANSI.RED, ANSI.YELLOW, msg_5v, ANSI.RESET)) + return + elif cart_type < 0: return + elif cart_type == 0 and args.flashcart_handler != "autodetect": + print("{:s}Couldn’t find the selected flash cartridge type “{:s}”. Please make sure the correct cartridge mode is selected and copy the exact name from the configuration files located in {:s}.{:s}".format(ANSI.RED, args.flashcart_handler, self.CONFIG_PATH, ANSI.RESET)) + return + + if args.path == "auto": + print("{:s}No ROM file for flashing was selected.{:s}".format(ANSI.RED, ANSI.RESET)) + return + else: + path = args.path + + try: + if os.path.getsize(path) > 0x2000000: # reject too large files to avoid exploding RAM + print("{:s}Files bigger than 32 MB are not supported at this time.{:s}".format(ANSI.RED, ANSI.RESET)) + return + with open(path, "rb") as file: buffer = file.read() + except (PermissionError, FileNotFoundError): + print("{:s}Couldn’t access file path “{:s}”.{:s}".format(ANSI.RED, args.path, ANSI.RESET)) + return + + rom_size = len(buffer) + if rom_size > carts[cart_type]['flash_size']: + msg = "The selected flash cartridge type seems to support ROMs that are up to {:.2f} MB in size, but the file you selected is {:.2f} MB.".format(int(carts[cart_type]['flash_size'] / 1024 / 1024), os.path.getsize(path)/1024/1024) + msg += " It’s possible that it’s too large which may cause the flashing to fail." + print("{:s}{:s}{:s}".format(ANSI.YELLOW, msg, ANSI.RESET)) + answer = input("Do you want to continue? [y/N]: ").strip().lower() + print("") + if answer != "y": + print("Canceled.") + return + + override_voltage = False + if args.force_5v is True: + override_voltage = 5 + elif 'voltage_variants' in carts[cart_type] and carts[cart_type]['voltage'] == 3.3: + print("The selected flash cartridge type usually flashes fine with 3.3V, however sometimes it may require 5V. You can use the “--force-5v” command line switch if necessary. Please note that 5V can be unsafe for some flash chips.") + + reverse_sectors = False + if args.reversed_sectors is True: + reverse_sectors = True + print("Will be writing to the cartridge with reversed flash sectors.") + elif 'sector_reversal' in carts[cart_type]: + print("The selected flash cartridge type is reported to sometimes have reversed sectors. You can use the “--reversed-sectors” command line switch if the cartridge is not working after flashing.") + + prefer_chip_erase = args.prefer_chip_erase is True + if not prefer_chip_erase and 'chip_erase' in carts[cart_type]['commands'] and 'sector_erase' in carts[cart_type]['commands']: + print("This flash cartridge supports both Sector Erase and Full Chip Erase methods. You can use the “--prefer-chip-erase” command line switch if necessary.") + + fast_read_mode = args.fast_read_mode is True + verify_flash = args.no_verify_flash is False + + try: + if self.CONN.GetMode() == "DMG": + hdr = RomFileDMG(path).GetHeader() + elif self.CONN.GetMode() == "AGB": + hdr = RomFileAGB(path).GetHeader() + if not hdr["logo_correct"]: + print("{:s}WARNING: The ROM file you selected will not boot on actual hardware due to invalid logo data.{:s}".format(ANSI.YELLOW, ANSI.RESET)) + if not hdr["header_checksum_correct"]: + print("{:s}WARNING: The ROM file you selected will not boot on actual hardware due to an invalid header checksum (expected 0x{:02X} instead of 0x{:02X}).{:s}".format(ANSI.YELLOW, hdr["header_checksum_calc"], hdr["header_checksum"], ANSI.RESET)) + + except: + print("{:s}The selected file could not be read.{:s}".format(ANSI.RED, ANSI.RESET)) + return + + print("") + if fast_read_mode: print("Fast Read Mode enabled for flash verification.") + v = carts[cart_type]["voltage"] + if override_voltage: v = override_voltage + print("The following ROM file will now be written to the flash cartridge at {:s}V:\n{:s}".format(str(v), os.path.abspath(path))) + + print("") + self.CONN._TransferData(args={ 'mode':4, 'path':path, 'cart_type':cart_type, 'override_voltage':override_voltage, 'start_addr':0, 'buffer':buffer, 'prefer_chip_erase':prefer_chip_erase, 'reverse_sectors':reverse_sectors, 'fast_read_mode':fast_read_mode, 'verify_flash':verify_flash }, signal=self.PROGRESS.SetProgress) + buffer = None + + def BackupRestoreRAM(self, args, header): + add_date_time = args.save_filename_add_datetime is True + rtc = args.store_rtc is True + + if self.CONN.GetMode() == "DMG": + if args.dmg_mbc == "auto": + try: + mbc = header["features_raw"] + if mbc == 0: mbc = 5 + except: + print("{:s}Couldn’t determine MBC type, will try to use MBC5. It can also be manually set with the “--dmg-mbc” command line switch.{:s}".format(ANSI.YELLOW, ANSI.RESET)) + mbc = 5 + else: + mbc = int(args.dmg_mbc) + if mbc == 2: mbc = 0x06 + elif mbc == 3: mbc = 0x13 + elif mbc == 5: mbc = 0x19 + elif mbc == 6: mbc = 0x20 + elif mbc == 7: mbc = 0x22 + + if args.dmg_savesize == "auto": + try: + if header['features_raw'] == 0x06: # MBC2 + save_type = Util.DMG_Header_RAM_Sizes_Flasher_Map[1] + elif header['features_raw'] == 0x22 and header["game_title"] in ("KORO2 KIRBYKKKJ", "KIRBY TNT__KTNE"): # MBC7 Kirby + save_type = Util.DMG_Header_RAM_Sizes_Flasher_Map[Util.DMG_Header_RAM_Sizes_Map.index(0x101)] + elif header['features_raw'] == 0x22 and header["game_title"] in ("CMASTER____KCEJ"): # MBC7 Command Master + save_type = Util.DMG_Header_RAM_Sizes_Flasher_Map[Util.DMG_Header_RAM_Sizes_Map.index(0x102)] + elif header['features_raw'] == 0xFD: # TAMA5 + save_type = Util.DMG_Header_RAM_Sizes_Flasher_Map[Util.DMG_Header_RAM_Sizes_Map.index(0x103)] + else: + save_type = Util.DMG_Header_RAM_Sizes_Flasher_Map[Util.DMG_Header_RAM_Sizes_Map.index(header['ram_size_raw'])] + except: + save_type = 0x20000 + else: + sizes = [ "auto", "4k", "16k", "64k", "256k", "512k", "1m", "eeprom2k", "eeprom4k", "tama5" ] + save_type = Util.DMG_Header_RAM_Sizes_Flasher_Map[sizes.index(args.dmg_savesize)] + + path = header["game_title"].strip().encode('ascii', 'ignore').decode('ascii') + if path == "": path = "ROM" + + if save_type == 0: + print("{:s}Unable to auto-detect the save size. Please use the “--dmg-savesize” command line switch to manually select it.{:s}".format(ANSI.RED, ANSI.RESET)) + return + + elif self.CONN.GetMode() == "AGB": + if args.agb_savetype == "auto": + save_type = header["save_type"] + else: + sizes = [ "auto", "eeprom4k", "eeprom64k", "sram256k", "sram512k", "sram1m", "flash512k", "flash1m" ] + save_type = sizes.index(args.agb_savetype) + + path = header["game_title"].strip().encode('ascii', 'ignore').decode('ascii') + if path == "": path = header["game_code"].strip().encode('ascii', 'ignore').decode('ascii') + if path == "": path = "ROM" + + mbc = 0 + if save_type == 0 or save_type == None: + print("{:s}Unable to auto-detect the save type. Please use the “--agb-savetype” command line switch to manually select it.{:s}".format(ANSI.RED, ANSI.RESET)) + return + + else: + return + + if add_date_time: + path = re.sub(r"[<>:\"/\\|\?\*]", "_", path) + "_" + datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + ".sav" + else: + path = re.sub(r"[<>:\"/\\|\?\*]", "_", path) + ".sav" + + if args.path != "auto": + if os.path.isdir(args.path): + path = args.path + "/" + path + else: + path = args.path + + if (path == ""): return + + s_mbc = "" + if self.CONN.GetMode() == "DMG": s_mbc = " using Cart Type 0x{:X}".format(mbc) + if args.action == "backup-save": + if not args.overwrite and os.path.exists(os.path.abspath(path)): + answer = input("The target file “{:s}” already exists.\nDo you want to overwrite it? [y/N]: ".format(os.path.abspath(path))).strip().lower() + print("") + if answer != "y": + print("Canceled.") + return + print("The cartridge save data will now be read{:s} and saved to the following file:\n{:s}".format(s_mbc, os.path.abspath(path))) + elif args.action == "restore-save": + if not args.overwrite: + answer = input("Restoring save data to the cartridge will erase the previous save.\nDo you want to overwrite it? [y/N]: ").strip().lower() + if answer != "y": + print("Canceled.") + return + print("The following save data file will now be written to the cartridge{:s}:\n{:s}".format(s_mbc, os.path.abspath(path))) + elif args.action == "erase-save": + if not args.overwrite: + answer = input("Do you really want to erase the save data from the cartridge? [y/N]: ").strip().lower() + if answer != "y": + print("Canceled.") + return + print("The cartridge save data will now be erased from the cartridge{:s}.".format(s_mbc)) + elif args.action == "debug-probe-save": + if mbc == 0xFD: # TAMA5 + print("Can’t examine save data size for TAMA5 cartridges.") + return + print("The cartridge save data size will now be examined{:s}. Note: This is for debug use only.\n".format(s_mbc)) + + if self.CONN.GetMode() == "AGB": + print("Using Save Type “{:s}”.".format(Util.AGB_Header_Save_Types[save_type])) + elif self.CONN.GetMode() == "DMG": + if rtc and header["features_raw"] in (0x10, 0xFD, 0xFE): # RTC of MBC3, TAMA5, HuC-3 + print("Real Time Clock register values will also be written if applicable/possible.") + + try: + if args.action == "backup-save": + f = open(path, "ab+") + f.close() + elif args.action == "restore-save": + f = open(path, "rb+") + f.close() + except (PermissionError, FileNotFoundError): + print("{:s}Couldn’t access “{:s}”.{:s}".format(ANSI.RED, path, ANSI.RESET)) + return + + print("") + if args.action == "backup-save": + self.CONN._TransferData(args={ 'mode':2, 'path':path, 'mbc':mbc, 'save_type':save_type, 'rtc':rtc }, signal=self.PROGRESS.SetProgress) + elif args.action == "restore-save": + self.CONN._TransferData(args={ 'mode':3, 'path':path, 'mbc':mbc, 'save_type':save_type, 'erase':False, 'rtc':rtc }, signal=self.PROGRESS.SetProgress) + elif args.action == "erase-save": + self.CONN._TransferData(args={ 'mode':3, 'path':path, 'mbc':mbc, 'save_type':save_type, 'erase':True, 'rtc':rtc }, signal=self.PROGRESS.SetProgress) + elif args.action == "debug-probe-save": # debug + self.ARGS["debug"] = True + print("Making a backup of the original save data.") + self.CONN._TransferData(args={ 'mode':2, 'path':self.CONFIG_PATH + "/probe1.bin", 'mbc':mbc, 'save_type':save_type }, signal=self.PROGRESS.SetProgress) + print("Writing random data.") + probe2 = bytearray(os.urandom(os.path.getsize(self.CONFIG_PATH + "/probe1.bin"))) + with open(self.CONFIG_PATH + "/probe2.bin", "wb") as f: f.write(probe2) + self.CONN._TransferData(args={ 'mode':3, 'path':self.CONFIG_PATH + "/probe2.bin", 'mbc':mbc, 'save_type':save_type, 'erase':False }, signal=self.PROGRESS.SetProgress) + print("Reading back and comparing data.") + self.CONN._TransferData(args={ 'mode':2, 'path':self.CONFIG_PATH + "/probe3.bin", 'mbc':mbc, 'save_type':save_type }, signal=self.PROGRESS.SetProgress) + with open(self.CONFIG_PATH + "/probe3.bin", "rb") as f: probe3 = bytearray(f.read()) + print("Restoring original save data.") + self.CONN._TransferData(args={ 'mode':3, 'path':self.CONFIG_PATH + "/probe1.bin", 'mbc':mbc, 'save_type':save_type, 'erase':False }, signal=self.PROGRESS.SetProgress) + + if mbc == 2: + for i in range(0, len(probe2)): + probe2[i] &= 0x0F + probe3[i] &= 0x0F + + found_offset = probe2.find(probe3[0:512]) + if found_offset < 0: + if self.CONN.GetMode() == "AGB": + print("\n{:s}It was not possible to save any data to the cartridge using save type “{:s}”.{:s}".format(ANSI.RED, Util.AGB_Header_Save_Types[save_type], ANSI.RESET)) + else: + print("\n{:s}It was not possible to save any data to the cartridge.{:s}".format(ANSI.RED, ANSI.RESET)) + else: + if found_offset == 0 and probe2 != probe3: # Pokémon Crystal JPN + found_length = 0 + for i in range(0, len(probe2)): + if probe2[i] != probe3[i]: break + found_length += 1 + else: + found_length = len(probe2) - found_offset + + if self.CONN.GetMode() == "DMG": + print("\nDone! The writable save data size is {:s} out of {:s} checked.".format(Util.formatFileSize(found_length, asInt=True), Util.formatFileSize(save_type, asInt=True))) + elif self.CONN.GetMode() == "AGB": + print("\nDone! The writable save data size using save type “{:s}” is {:s}.".format(Util.AGB_Header_Save_Types[save_type], Util.formatFileSize(found_length, asInt=True))) + + try: + (_, _, cfi) = self.CONN.CheckFlashChip(limitVoltage=False) + if len(cfi["raw"]) > 0: + with open(self.CONFIG_PATH + "/cfi.bin", "wb") as f: f.write(cfi["raw"]) + print("CFI data was extracted to “cfi.bin”.") + except: + pass + + input("\nPress ENTER to erase the temporary files.") + os.unlink(self.CONFIG_PATH + "/probe1.bin") + os.unlink(self.CONFIG_PATH + "/probe2.bin") + os.unlink(self.CONFIG_PATH + "/probe3.bin") diff --git a/FlashGBX/FlashGBX_GUI.py b/FlashGBX/FlashGBX_GUI.py new file mode 100644 index 0000000..5215fb2 --- /dev/null +++ b/FlashGBX/FlashGBX_GUI.py @@ -0,0 +1,1683 @@ +# -*- coding: utf-8 -*- +# UTF-8 +import sys, os, time, datetime, re, json, platform, subprocess, argparse, requests, webbrowser, pkg_resources +from PySide2 import QtCore, QtWidgets, QtGui +from .RomFileDMG import RomFileDMG +from .RomFileAGB import RomFileAGB +from .PocketCamera import PocketCamera +from .PocketCameraWindow import PocketCameraWindow +from .Util import APPNAME, VERSION, VERSION_PEP440, ANSI +from . import Util +from . import hw_GBxCartRW +hw_devices = [hw_GBxCartRW] + +class FlashGBX_GUI(QtWidgets.QWidget): + CONN = None + SETTINGS = None + DEVICES = {} + FLASHCARTS = { "DMG":{}, "AGB":{} } + CONFIG_PATH = "" + TBPROG = None # Windows 7+ Taskbar Progress Bar + PROGRESS = None + + def __init__(self, args): + QtWidgets.QWidget.__init__(self) + app_path = args['app_path'] + self.CONFIG_PATH = args['config_path'] + self.SETTINGS = Util.IniSettings(ini_file=args["config_path"] + "/settings.ini") + self.FLASHCARTS = args["flashcarts"] + self.PROGRESS = Util.Progress(self.UpdateProgress) + + self.setStyleSheet("QMessageBox { messagebox-text-interaction-flags: 5; }") + self.setWindowIcon(QtGui.QIcon(app_path + "/res/icon.ico")) + self.setWindowTitle("{:s} {:s}".format(APPNAME, VERSION)) + self.setWindowFlags(self.windowFlags() | QtCore.Qt.MSWindowsFixedSizeDialogHint) + + # Create the QtWidgets.QVBoxLayout that lays out the whole form + self.layout = QtWidgets.QGridLayout() + self.layout.setSizeConstraint(QtWidgets.QLayout.SetFixedSize) + self.layout_left = QtWidgets.QVBoxLayout() + self.layout_right = QtWidgets.QVBoxLayout() + self.layout.setContentsMargins(-1, 8, -1, 8) + + # Cartridge Information GroupBox + self.grpDMGCartridgeInfo = self.GuiCreateGroupBoxDMGCartInfo() + self.grpAGBCartridgeInfo = self.GuiCreateGroupBoxAGBCartInfo() + self.grpAGBCartridgeInfo.setVisible(False) + self.layout_left.addWidget(self.grpDMGCartridgeInfo) + self.layout_left.addWidget(self.grpAGBCartridgeInfo) + + # Actions + self.grpActions = QtWidgets.QGroupBox("Options") + self.grpActionsLayout = QtWidgets.QVBoxLayout() + self.grpActionsLayout.setContentsMargins(-1, 3, -1, -1) + + rowActionsMode = QtWidgets.QHBoxLayout() + self.lblMode = QtWidgets.QLabel("Mode: ") + rowActionsMode.addWidget(self.lblMode) + self.optDMG = QtWidgets.QRadioButton("&Game Boy") + self.connect(self.optDMG, QtCore.SIGNAL("clicked()"), self.SetMode) + self.optAGB = QtWidgets.QRadioButton("Game Boy &Advance") + self.connect(self.optAGB, QtCore.SIGNAL("clicked()"), self.SetMode) + rowActionsMode.addWidget(self.optDMG) + rowActionsMode.addWidget(self.optAGB) + + rowActionsGeneral1 = QtWidgets.QHBoxLayout() + self.btnHeaderRefresh = QtWidgets.QPushButton("Read &Information") + self.btnHeaderRefresh.setStyleSheet("min-height: 17px;") + self.connect(self.btnHeaderRefresh, QtCore.SIGNAL("clicked()"), self.ReadCartridge) + rowActionsGeneral1.addWidget(self.btnHeaderRefresh) + + rowActionsGeneral2 = QtWidgets.QHBoxLayout() + self.btnBackupROM = QtWidgets.QPushButton("Backup &ROM") + self.btnBackupROM.setStyleSheet("min-height: 17px;") + self.connect(self.btnBackupROM, QtCore.SIGNAL("clicked()"), self.BackupROM) + rowActionsGeneral2.addWidget(self.btnBackupROM) + self.btnBackupRAM = QtWidgets.QPushButton("Backup &Save Data") + self.btnBackupRAM.setStyleSheet("min-height: 17px;") + self.connect(self.btnBackupRAM, QtCore.SIGNAL("clicked()"), self.BackupRAM) + rowActionsGeneral2.addWidget(self.btnBackupRAM) + + self.cmbDMGCartridgeTypeResult.currentIndexChanged.connect(self.CartridgeTypeChanged) + + rowActionsGeneral3 = QtWidgets.QHBoxLayout() + self.btnFlashROM = QtWidgets.QPushButton("&Flash ROM") + self.btnFlashROM.setStyleSheet("min-height: 17px;") + self.connect(self.btnFlashROM, QtCore.SIGNAL("clicked()"), self.FlashROM) + rowActionsGeneral3.addWidget(self.btnFlashROM) + self.btnRestoreRAM = QtWidgets.QPushButton("Writ&e Save Data") + self.mnuRestoreRAM = QtWidgets.QMenu() + self.mnuRestoreRAM.addAction("&Restore from save data file", self.WriteRAM) + self.mnuRestoreRAM.addAction("&Erase cartridge save data", lambda: self.WriteRAM(erase=True)) + self.btnRestoreRAM.setMenu(self.mnuRestoreRAM) + self.btnRestoreRAM.setStyleSheet("min-height: 17px;") + rowActionsGeneral3.addWidget(self.btnRestoreRAM) + + self.grpActionsLayout.setSpacing(4) + self.grpActionsLayout.addLayout(rowActionsMode) + self.grpActionsLayout.addLayout(rowActionsGeneral1) + self.grpActionsLayout.addLayout(rowActionsGeneral2) + self.grpActionsLayout.addLayout(rowActionsGeneral3) + self.grpActions.setLayout(self.grpActionsLayout) + + self.layout_right.addWidget(self.grpActions) + + # Transfer Status + self.grpStatus = QtWidgets.QGroupBox("Transfer Status") + grpStatusLayout = QtWidgets.QVBoxLayout() + grpStatusLayout.setContentsMargins(-1, 3, -1, -1) + + rowStatus1a = QtWidgets.QHBoxLayout() + self.lblStatus1a = QtWidgets.QLabel("Data transferred:") + rowStatus1a.addWidget(self.lblStatus1a) + self.lblStatus1aResult = QtWidgets.QLabel("–") + rowStatus1a.addWidget(self.lblStatus1aResult) + grpStatusLayout.addLayout(rowStatus1a) + rowStatus2a = QtWidgets.QHBoxLayout() + self.lblStatus2a = QtWidgets.QLabel("Transfer rate:") + rowStatus2a.addWidget(self.lblStatus2a) + self.lblStatus2aResult = QtWidgets.QLabel("–") + rowStatus2a.addWidget(self.lblStatus2aResult) + grpStatusLayout.addLayout(rowStatus2a) + rowStatus3a = QtWidgets.QHBoxLayout() + self.lblStatus3a = QtWidgets.QLabel("Time elapsed:") + rowStatus3a.addWidget(self.lblStatus3a) + self.lblStatus3aResult = QtWidgets.QLabel("–") + rowStatus3a.addWidget(self.lblStatus3aResult) + grpStatusLayout.addLayout(rowStatus3a) + rowStatus4a = QtWidgets.QHBoxLayout() + self.lblStatus4a = QtWidgets.QLabel("Ready.") + rowStatus4a.addWidget(self.lblStatus4a) + self.lblStatus4aResult = QtWidgets.QLabel("") + rowStatus4a.addWidget(self.lblStatus4aResult) + grpStatusLayout.addLayout(rowStatus4a) + + rowStatus2 = QtWidgets.QHBoxLayout() + self.prgStatus = QtWidgets.QProgressBar() + self.SetProgressBars(min=0, max=1, value=0) + rowStatus2.addWidget(self.prgStatus) + btnText = "Stop" + self.btnCancel = QtWidgets.QPushButton(btnText) + self.btnCancel.setEnabled(False) + btnWidth = self.btnCancel.fontMetrics().boundingRect(btnText).width() + 15 + if platform.system() == "Darwin": btnWidth += 12 + self.btnCancel.setMaximumWidth(btnWidth) + self.connect(self.btnCancel, QtCore.SIGNAL("clicked()"), self.AbortOperation) + rowStatus2.addWidget(self.btnCancel) + + grpStatusLayout.addLayout(rowStatus2) + self.grpStatus.setLayout(grpStatusLayout) + + self.layout_right.addWidget(self.grpStatus) + + self.layout.addLayout(self.layout_left, 0, 0) + self.layout.addLayout(self.layout_right, 0, 1) + + # List devices + self.layout_devices = QtWidgets.QHBoxLayout() + self.lblDevice = QtWidgets.QLabel() + self.cmbDevice = QtWidgets.QComboBox() + self.cmbDevice.setStyleSheet("QComboBox { border: 0; margin: 0; padding: 0; max-width: 0px; }") + self.layout_devices.addWidget(self.lblDevice) + self.layout_devices.addWidget(self.cmbDevice) + self.layout_devices.addStretch() + + self.btnCameraViewer = QtWidgets.QPushButton("&GB Camera") + self.connect(self.btnCameraViewer, QtCore.SIGNAL("clicked()"), self.ShowPocketCameraWindow) + + btnText = "C&onfig" + self.btnConfig = QtWidgets.QPushButton(btnText) + btnWidth = self.btnConfig.fontMetrics().boundingRect(btnText).width() + 24 + if platform.system() == "Darwin": btnWidth += 12 + self.btnConfig.setMaximumWidth(btnWidth) + self.mnuConfig = QtWidgets.QMenu() + self.mnuConfig.addAction("Check for &updates at application startup", lambda: [ self.SETTINGS.setValue("UpdateCheck", str(self.mnuConfig.actions()[0].isChecked()).lower().replace("true", "enabled").replace("false", "disabled")), self.UpdateCheck() ]) + self.mnuConfig.addAction("&Append date && time to filename of save data backups", lambda: self.SETTINGS.setValue("SaveFileNameAddDateTime", str(self.mnuConfig.actions()[1].isChecked()).lower().replace("true", "enabled").replace("false", "disabled"))) + self.mnuConfig.addAction("Prefer full &chip erase over sector erase when both available", lambda: self.SETTINGS.setValue("PreferChipErase", str(self.mnuConfig.actions()[2].isChecked()).lower().replace("true", "enabled").replace("false", "disabled"))) + self.mnuConfig.addAction("&Verify flash after writing", lambda: self.SETTINGS.setValue("VerifyFlash", str(self.mnuConfig.actions()[3].isChecked()).lower().replace("true", "enabled").replace("false", "disabled"))) + self.mnuConfig.addAction("Use &fast read mode (experimental)", lambda: self.SETTINGS.setValue("FastReadMode", str(self.mnuConfig.actions()[4].isChecked()).lower().replace("true", "enabled").replace("false", "disabled"))) # GBxCart RW + self.mnuConfig.addSeparator() + self.mnuConfig.addAction("Show &configuration directory", self.OpenConfigDir) + self.mnuConfig.actions()[0].setCheckable(True) + self.mnuConfig.actions()[1].setCheckable(True) + self.mnuConfig.actions()[2].setCheckable(True) + self.mnuConfig.actions()[3].setCheckable(True) + self.mnuConfig.actions()[4].setCheckable(True) # GBxCart RW + self.mnuConfig.actions()[0].setChecked(self.SETTINGS.value("UpdateCheck") == "enabled") + self.mnuConfig.actions()[1].setChecked(self.SETTINGS.value("SaveFileNameAddDateTime", default="disabled") == "enabled") + self.mnuConfig.actions()[2].setChecked(self.SETTINGS.value("PreferChipErase", default="disabled") == "enabled") + self.mnuConfig.actions()[3].setChecked(self.SETTINGS.value("VerifyFlash", default="enabled") == "enabled") + self.mnuConfig.actions()[4].setChecked(self.SETTINGS.value("FastReadMode", default="disabled") == "enabled") # GBxCart RW + self.btnConfig.setMenu(self.mnuConfig) + + self.btnConnect = QtWidgets.QPushButton("&Connect") + self.connect(self.btnConnect, QtCore.SIGNAL("clicked()"), self.ConnectDevice) + self.layout_devices.addWidget(self.btnCameraViewer) + self.layout_devices.addWidget(self.btnConfig) + self.layout_devices.addWidget(self.btnConnect) + + self.layout.addLayout(self.layout_devices, 1, 0, 1, 0) + + # Disable widgets + self.optAGB.setEnabled(False) + self.optDMG.setEnabled(False) + self.btnHeaderRefresh.setEnabled(False) + self.btnBackupROM.setEnabled(False) + self.btnFlashROM.setEnabled(False) + self.btnBackupRAM.setEnabled(False) + self.btnRestoreRAM.setEnabled(False) + self.btnConnect.setEnabled(False) + self.grpDMGCartridgeInfo.setEnabled(False) + self.grpAGBCartridgeInfo.setEnabled(False) + + # Set the VBox layout as the window's main layout + self.setLayout(self.layout) + + # Show app window first, then do update check + qt_app.processEvents() + + config_ret = args["config_ret"] + for i in range(0, len(config_ret)): + if config_ret[i][0] == 0: + print(config_ret[i][1]) + elif config_ret[i][0] == 1: + QtWidgets.QMessageBox.information(self, "{:s} {:s}".format(APPNAME, VERSION), config_ret[i][1], QtWidgets.QMessageBox.Ok) + elif config_ret[i][0] == 2: + QtWidgets.QMessageBox.warning(self, "{:s} {:s}".format(APPNAME, VERSION), config_ret[i][1], QtWidgets.QMessageBox.Ok) + elif config_ret[i][0] == 3: + QtWidgets.QMessageBox.critical(self, "{:s} {:s}".format(APPNAME, VERSION), config_ret[i][1], QtWidgets.QMessageBox.Ok) + + QtCore.QTimer.singleShot(1, lambda: [ self.UpdateCheck(), self.FindDevices() ]) + + def GuiCreateGroupBoxDMGCartInfo(self): + self.grpDMGCartridgeInfo = QtWidgets.QGroupBox("Game Boy Cartridge Information") + self.grpDMGCartridgeInfo.setMinimumWidth(280) + group_layout = QtWidgets.QVBoxLayout() + group_layout.setContentsMargins(-1, 5, -1, -1) + + rowHeaderTitle = QtWidgets.QHBoxLayout() + lblHeaderTitle = QtWidgets.QLabel("Game Title/Code:") + lblHeaderTitle.setContentsMargins(0, 1, 0, 1) + rowHeaderTitle.addWidget(lblHeaderTitle) + self.lblHeaderTitleResult = QtWidgets.QLabel("") + rowHeaderTitle.addWidget(self.lblHeaderTitleResult) + group_layout.addLayout(rowHeaderTitle) + + rowHeaderSGB = QtWidgets.QHBoxLayout() + lblHeaderSGB = QtWidgets.QLabel("Super Game Boy:") + lblHeaderSGB.setContentsMargins(0, 1, 0, 1) + rowHeaderSGB.addWidget(lblHeaderSGB) + self.lblHeaderSGBResult = QtWidgets.QLabel("") + rowHeaderSGB.addWidget(self.lblHeaderSGBResult) + group_layout.addLayout(rowHeaderSGB) + + rowHeaderCGB = QtWidgets.QHBoxLayout() + lblHeaderCGB = QtWidgets.QLabel("Game Boy Color:") + lblHeaderCGB.setContentsMargins(0, 1, 0, 1) + rowHeaderCGB.addWidget(lblHeaderCGB) + self.lblHeaderCGBResult = QtWidgets.QLabel("") + rowHeaderCGB.addWidget(self.lblHeaderCGBResult) + group_layout.addLayout(rowHeaderCGB) + + rowHeaderLogoValid = QtWidgets.QHBoxLayout() + lblHeaderLogoValid = QtWidgets.QLabel("Nintendo Logo:") + lblHeaderLogoValid.setContentsMargins(0, 1, 0, 1) + rowHeaderLogoValid.addWidget(lblHeaderLogoValid) + self.lblHeaderLogoValidResult = QtWidgets.QLabel("") + rowHeaderLogoValid.addWidget(self.lblHeaderLogoValidResult) + group_layout.addLayout(rowHeaderLogoValid) + + rowHeaderChecksum = QtWidgets.QHBoxLayout() + lblHeaderChecksum = QtWidgets.QLabel("Header Checksum:") + lblHeaderChecksum.setContentsMargins(0, 1, 0, 1) + rowHeaderChecksum.addWidget(lblHeaderChecksum) + self.lblHeaderChecksumResult = QtWidgets.QLabel("") + rowHeaderChecksum.addWidget(self.lblHeaderChecksumResult) + group_layout.addLayout(rowHeaderChecksum) + + rowHeaderROMChecksum = QtWidgets.QHBoxLayout() + lblHeaderROMChecksum = QtWidgets.QLabel("ROM Checksum:") + lblHeaderROMChecksum.setContentsMargins(0, 1, 0, 1) + rowHeaderROMChecksum.addWidget(lblHeaderROMChecksum) + self.lblHeaderROMChecksumResult = QtWidgets.QLabel("") + rowHeaderROMChecksum.addWidget(self.lblHeaderROMChecksumResult) + group_layout.addLayout(rowHeaderROMChecksum) + + rowHeaderROMSize = QtWidgets.QHBoxLayout() + lblHeaderROMSize = QtWidgets.QLabel("ROM Size:") + rowHeaderROMSize.addWidget(lblHeaderROMSize) + self.cmbHeaderROMSizeResult = QtWidgets.QComboBox() + self.cmbHeaderROMSizeResult.setStyleSheet("combobox-popup: 0;") + self.cmbHeaderROMSizeResult.view().setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) + rowHeaderROMSize.addWidget(self.cmbHeaderROMSizeResult) + group_layout.addLayout(rowHeaderROMSize) + + rowHeaderRAMSize = QtWidgets.QHBoxLayout() + lblHeaderRAMSize = QtWidgets.QLabel("Save Type:") + rowHeaderRAMSize.addWidget(lblHeaderRAMSize) + self.cmbHeaderRAMSizeResult = QtWidgets.QComboBox() + self.cmbHeaderRAMSizeResult.setStyleSheet("combobox-popup: 0;") + self.cmbHeaderRAMSizeResult.view().setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) + rowHeaderRAMSize.addWidget(self.cmbHeaderRAMSizeResult) + group_layout.addLayout(rowHeaderRAMSize) + + rowHeaderFeatures = QtWidgets.QHBoxLayout() + lblHeaderFeatures = QtWidgets.QLabel("Mapper Type:") + rowHeaderFeatures.addWidget(lblHeaderFeatures) + self.cmbHeaderFeaturesResult = QtWidgets.QComboBox() + self.cmbHeaderFeaturesResult.setStyleSheet("combobox-popup: 0;") + self.cmbHeaderFeaturesResult.view().setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) + rowHeaderFeatures.addWidget(self.cmbHeaderFeaturesResult) + group_layout.addLayout(rowHeaderFeatures) + + rowCartridgeType = QtWidgets.QHBoxLayout() + lblCartridgeType = QtWidgets.QLabel("Cart:") + rowCartridgeType.addWidget(lblCartridgeType) + self.cmbDMGCartridgeTypeResult = QtWidgets.QComboBox() + self.cmbDMGCartridgeTypeResult.setStyleSheet("max-width: 260px;") + self.cmbDMGCartridgeTypeResult.setStyleSheet("combobox-popup: 0;") + self.cmbDMGCartridgeTypeResult.view().setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) + rowCartridgeType.addWidget(self.cmbDMGCartridgeTypeResult) + group_layout.addLayout(rowCartridgeType) + + self.grpDMGCartridgeInfo.setLayout(group_layout) + + return self.grpDMGCartridgeInfo + + def GuiCreateGroupBoxAGBCartInfo(self): + self.grpAGBCartridgeInfo = QtWidgets.QGroupBox("Game Boy Advance Cartridge Information") + self.grpAGBCartridgeInfo.setMinimumWidth(280) + group_layout = QtWidgets.QVBoxLayout() + group_layout.setContentsMargins(-1, 5, -1, -1) + + rowAGBHeaderTitle = QtWidgets.QHBoxLayout() + lblAGBHeaderTitle = QtWidgets.QLabel("Game Title:") + lblAGBHeaderTitle.setContentsMargins(0, 1, 0, 1) + rowAGBHeaderTitle.addWidget(lblAGBHeaderTitle) + self.lblAGBHeaderTitleResult = QtWidgets.QLabel("") + rowAGBHeaderTitle.addWidget(self.lblAGBHeaderTitleResult) + group_layout.addLayout(rowAGBHeaderTitle) + + rowAGBHeaderCode = QtWidgets.QHBoxLayout() + lblAGBHeaderCode = QtWidgets.QLabel("Game Code:") + lblAGBHeaderCode.setContentsMargins(0, 1, 0, 1) + rowAGBHeaderCode.addWidget(lblAGBHeaderCode) + self.lblAGBHeaderCodeResult = QtWidgets.QLabel("") + rowAGBHeaderCode.addWidget(self.lblAGBHeaderCodeResult) + group_layout.addLayout(rowAGBHeaderCode) + + rowAGBHeaderVersion = QtWidgets.QHBoxLayout() + lblAGBHeaderVersion = QtWidgets.QLabel("Revision:") + lblAGBHeaderVersion.setContentsMargins(0, 1, 0, 1) + rowAGBHeaderVersion.addWidget(lblAGBHeaderVersion) + self.lblAGBHeaderVersionResult = QtWidgets.QLabel("") + rowAGBHeaderVersion.addWidget(self.lblAGBHeaderVersionResult) + group_layout.addLayout(rowAGBHeaderVersion) + + rowAGBHeaderLogoValid = QtWidgets.QHBoxLayout() + lblAGBHeaderLogoValid = QtWidgets.QLabel("Nintendo Logo:") + lblAGBHeaderLogoValid.setContentsMargins(0, 1, 0, 1) + rowAGBHeaderLogoValid.addWidget(lblAGBHeaderLogoValid) + self.lblAGBHeaderLogoValidResult = QtWidgets.QLabel("") + rowAGBHeaderLogoValid.addWidget(self.lblAGBHeaderLogoValidResult) + group_layout.addLayout(rowAGBHeaderLogoValid) + + rowAGBHeader96h = QtWidgets.QHBoxLayout() + lblAGBHeader96h = QtWidgets.QLabel("Cartridge Identifier:") + lblAGBHeader96h.setContentsMargins(0, 1, 0, 1) + rowAGBHeader96h.addWidget(lblAGBHeader96h) + self.lblAGBHeader96hResult = QtWidgets.QLabel("") + rowAGBHeader96h.addWidget(self.lblAGBHeader96hResult) + group_layout.addLayout(rowAGBHeader96h) + + rowAGBHeaderChecksum = QtWidgets.QHBoxLayout() + lblAGBHeaderChecksum = QtWidgets.QLabel("Header Checksum:") + lblAGBHeaderChecksum.setContentsMargins(0, 1, 0, 1) + rowAGBHeaderChecksum.addWidget(lblAGBHeaderChecksum) + self.lblAGBHeaderChecksumResult = QtWidgets.QLabel("") + rowAGBHeaderChecksum.addWidget(self.lblAGBHeaderChecksumResult) + group_layout.addLayout(rowAGBHeaderChecksum) + + rowAGBHeaderROMChecksum = QtWidgets.QHBoxLayout() + lblAGBHeaderROMChecksum = QtWidgets.QLabel("ROM Checksum:") + lblAGBHeaderROMChecksum.setContentsMargins(0, 1, 0, 1) + rowAGBHeaderROMChecksum.addWidget(lblAGBHeaderROMChecksum) + self.lblAGBHeaderROMChecksumResult = QtWidgets.QLabel("") + rowAGBHeaderROMChecksum.addWidget(self.lblAGBHeaderROMChecksumResult) + group_layout.addLayout(rowAGBHeaderROMChecksum) + + rowAGBHeaderROMSize = QtWidgets.QHBoxLayout() + lblAGBHeaderROMSize = QtWidgets.QLabel("ROM Size:") + rowAGBHeaderROMSize.addWidget(lblAGBHeaderROMSize) + self.cmbAGBHeaderROMSizeResult = QtWidgets.QComboBox() + self.cmbAGBHeaderROMSizeResult.setStyleSheet("combobox-popup: 0;") + self.cmbAGBHeaderROMSizeResult.view().setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) + self.cmbAGBHeaderROMSizeResult.addItems(Util.AGB_Header_ROM_Sizes) + self.cmbAGBHeaderROMSizeResult.setCurrentIndex(self.cmbAGBHeaderROMSizeResult.count() - 1) + rowAGBHeaderROMSize.addWidget(self.cmbAGBHeaderROMSizeResult) + group_layout.addLayout(rowAGBHeaderROMSize) + + rowAGBHeaderRAMSize = QtWidgets.QHBoxLayout() + lblAGBHeaderRAMSize = QtWidgets.QLabel("Save Type:") + rowAGBHeaderRAMSize.addWidget(lblAGBHeaderRAMSize) + self.cmbAGBSaveTypeResult = QtWidgets.QComboBox() + self.cmbAGBSaveTypeResult.setStyleSheet("combobox-popup: 0;") + self.cmbAGBSaveTypeResult.view().setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) + self.cmbAGBSaveTypeResult.addItems(Util.AGB_Header_Save_Types) + self.cmbAGBSaveTypeResult.setCurrentIndex(self.cmbAGBSaveTypeResult.count() - 1) + rowAGBHeaderRAMSize.addWidget(self.cmbAGBSaveTypeResult) + group_layout.addLayout(rowAGBHeaderRAMSize) + + rowAGBCartridgeType = QtWidgets.QHBoxLayout() + lblAGBCartridgeType = QtWidgets.QLabel("Cart:") + rowAGBCartridgeType.addWidget(lblAGBCartridgeType) + self.cmbAGBCartridgeTypeResult = QtWidgets.QComboBox() + self.cmbAGBCartridgeTypeResult.setStyleSheet("max-width: 260px;") + self.cmbAGBCartridgeTypeResult.setStyleSheet("combobox-popup: 0;") + self.cmbAGBCartridgeTypeResult.view().setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) + self.cmbAGBCartridgeTypeResult.currentIndexChanged.connect(self.CartridgeTypeChanged) + rowAGBCartridgeType.addWidget(self.cmbAGBCartridgeTypeResult) + group_layout.addLayout(rowAGBCartridgeType) + + self.grpAGBCartridgeInfo.setLayout(group_layout) + return self.grpAGBCartridgeInfo + + def UpdateCheck(self): + update_check = self.SETTINGS.value("UpdateCheck") + if update_check is None: + answer = QtWidgets.QMessageBox.question(self, "{:s} {:s}".format(APPNAME, VERSION), "Welcome to {:s} {:s} by Lesserkuma!\nWould you like to automatically check for new versions at application startup?".format(APPNAME, VERSION), QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.Yes) + if answer == QtWidgets.QMessageBox.Yes: + self.SETTINGS.setValue("UpdateCheck", "enabled") + self.mnuConfig.actions()[0].setChecked(True) + update_check = "enabled" + else: + self.SETTINGS.setValue("UpdateCheck", "disabled") + + if update_check and update_check.lower() == "enabled": + if ".dev" in VERSION_PEP440: + type = "test " + url = "https://test.pypi.org/pypi/FlashGBX/json" + site = "https://test.pypi.org/project/FlashGBX/" + else: + type = "" + url = "https://pypi.org/pypi/FlashGBX/json" + site = "https://github.com/lesserkuma/FlashGBX" + try: + ret = requests.get(url, allow_redirects=True, timeout=1.5) + except requests.exceptions.ConnectTimeout as e: + print("ERROR: Update check failed due to a connection timeout. Please check your internet connection.", e, sep="\n") + ret = False + except requests.exceptions.ConnectionError as e: + print("ERROR: Update check failed due to a connection error. Please check your network connection.", e, sep="\n") + ret = False + except Exception as e: + print("ERROR: An unexpected error occured while querying the latest version information from PyPI.", e, sep="\n") + ret = False + + if ret is not False and ret.status_code == 200: + ret = ret.content + try: + ret = json.loads(ret) + if 'info' in ret and 'version' in ret['info']: + if pkg_resources.parse_version(ret['info']['version']) == pkg_resources.parse_version(VERSION_PEP440): + print("You are using the latest {:s}version of {:s}.".format(type, APPNAME)) + elif pkg_resources.parse_version(ret['info']['version']) > pkg_resources.parse_version(VERSION_PEP440): + msg_text = "A new {:s}version of {:s} has been released!\nVersion {:s} is now available.".format(type, APPNAME, ret['info']['version']) + print(msg_text) + msgbox = QtWidgets.QMessageBox(parent=self, icon=QtWidgets.QMessageBox.Question, windowTitle="{:s} Update Check".format(APPNAME), text=msg_text) + button_open = msgbox.addButton(" Open &website ", QtWidgets.QMessageBox.ActionRole) + button_cancel = msgbox.addButton("&OK", QtWidgets.QMessageBox.RejectRole) + msgbox.setDefaultButton(button_open) + msgbox.setEscapeButton(button_cancel) + answer = msgbox.exec() + if msgbox.clickedButton() == button_open: + webbrowser.open(site) + else: + print("This version of {:s} ({:s}) seems to be newer than the latest {:s}release ({:s}). Please check for updates manually.".format(APPNAME, VERSION_PEP440, type, ret['info']['version'])) + else: + print("ERROR: Update check failed due to missing version information in JSON data from PyPI.") + except json.decoder.JSONDecodeError: + print("ERROR: Update check failed due to malformed JSON data from PyPI.") + except Exception as e: + print("ERROR: An unexpected error occured while querying the latest version information from PyPI.", e, sep="\n") + elif ret is not False: + print("ERROR: Failed to check for updates (HTTP status {:d}).".format(ret.status_code)) + + def DisconnectDevice(self): + try: + devname = self.CONN.GetFullName() + self.CONN.Close() + print("Disconnected from {:s}".format(devname)) + except: + pass + + self.CONN = None + self.optAGB.setEnabled(False) + self.optDMG.setEnabled(False) + self.grpDMGCartridgeInfo.setEnabled(False) + self.grpAGBCartridgeInfo.setEnabled(False) + self.btnCancel.setEnabled(False) + self.btnHeaderRefresh.setEnabled(False) + self.btnBackupROM.setEnabled(False) + self.btnFlashROM.setEnabled(False) + self.btnBackupRAM.setEnabled(False) + self.btnRestoreRAM.setEnabled(False) + self.btnConnect.setText("&Connect") + self.lblDevice.setText("Disconnected.") + + def OpenConfigDir(self): + path = 'file://{0:s}'.format(self.CONFIG_PATH) + try: + if platform.system() == "Windows": + os.startfile(path) + elif platform.system() == "Darwin": + subprocess.Popen(["open", path]) + else: + subprocess.Popen(["xdg-open", path]) + except: + QtWidgets.QMessageBox.information(self, "{:s} {:s}".format(APPNAME, VERSION), "Your configuration files are stored in\n" + path, QtWidgets.QMessageBox.Ok) + + def ConnectDevice(self): + if self.CONN is not None: + self.DisconnectDevice() + return True + else: + if self.cmbDevice.count() > 0: + index = self.cmbDevice.currentText() + else: + index = self.lblDevice.text() + + if index not in self.DEVICES: + self.FindDevices(True) + return + + dev = self.DEVICES[index] + ret = dev.Initialize(self.FLASHCARTS) + msg = "" + + if ret is False: + self.CONN = None + if self.cmbDevice.count() == 0: self.lblDevice.setText("No connection.") + return False + + elif isinstance(ret, list): + for i in range(0, len(ret)): + status = ret[i][0] + text = ret[i][1] + if status == 0: + msg += text + "\n" + elif status == 1: + msgbox = QtWidgets.QMessageBox(parent=self, icon=QtWidgets.QMessageBox.Information, windowTitle="{:s} {:s}".format(APPNAME, VERSION), text=text, standardButtons=QtWidgets.QMessageBox.Ok) + if not '\n' in text: msgbox.setTextFormat(QtCore.Qt.RichText) + msgbox.exec() + elif status == 2: + msgbox = QtWidgets.QMessageBox(parent=self, icon=QtWidgets.QMessageBox.Warning, windowTitle="{:s} {:s}".format(APPNAME, VERSION), text=text, standardButtons=QtWidgets.QMessageBox.Ok) + if not '\n' in text: msgbox.setTextFormat(QtCore.Qt.RichText) + msgbox.exec() + elif status == 3: + msgbox = QtWidgets.QMessageBox(parent=self, icon=QtWidgets.QMessageBox.Critical, windowTitle="{:s} {:s}".format(APPNAME, VERSION), text=text, standardButtons=QtWidgets.QMessageBox.Ok) + if not '\n' in text: msgbox.setTextFormat(QtCore.Qt.RichText) + msgbox.exec() + self.CONN = None + return False + + if dev.IsConnected(): + qt_app.processEvents() + self.CONN = dev + self.optDMG.setAutoExclusive(False) + self.optAGB.setAutoExclusive(False) + if "DMG" in self.CONN.GetSupprtedModes(): + self.optDMG.setEnabled(True) + self.optDMG.setChecked(False) + if "AGB" in self.CONN.GetSupprtedModes(): + self.optAGB.setEnabled(True) + self.optAGB.setChecked(False) + self.optAGB.setAutoExclusive(True) + self.optDMG.setAutoExclusive(True) + self.btnConnect.setText("&Disconnect") + self.cmbDevice.setStyleSheet("QComboBox { border: 0; margin: 0; padding: 0; max-width: 0px; }") + self.lblDevice.setText(dev.GetFullName()) + print("\nConnected to " + dev.GetFullName()) + self.grpDMGCartridgeInfo.setEnabled(True) + self.grpAGBCartridgeInfo.setEnabled(True) + self.grpActions.setEnabled(True) + self.btnCancel.setEnabled(False) + self.SetProgressBars(min=0, max=1, value=0) + + if self.CONN.GetMode() == "DMG": + self.cmbDMGCartridgeTypeResult.clear() + self.cmbDMGCartridgeTypeResult.addItems(self.CONN.GetSupportedCartridgesDMG()[0]) + self.grpAGBCartridgeInfo.setVisible(False) + self.grpDMGCartridgeInfo.setVisible(True) + elif self.CONN.GetMode() == "AGB": + self.cmbAGBCartridgeTypeResult.clear() + self.cmbAGBCartridgeTypeResult.addItems(self.CONN.GetSupportedCartridgesAGB()[0]) + self.grpDMGCartridgeInfo.setVisible(False) + self.grpAGBCartridgeInfo.setVisible(True) + + print(msg, end="") + return True + return False + + def FindDevices(self, connectToFirst=False): + if self.CONN is not None: + self.DisconnectDevice() + self.lblDevice.setText("Searching...") + self.btnConnect.setEnabled(False) + qt_app.processEvents() + time.sleep(0.05) + + global hw_devices + for hw_device in hw_devices: + dev = hw_device.GbxDevice() + ret = dev.Initialize(self.FLASHCARTS) + if ret is False: + self.CONN = None + elif isinstance(ret, list): + for i in range(0, len(ret)): + status = ret[i][0] + msg = ret[i][1] + if status == 3: + QtWidgets.QMessageBox.critical(self, "{:s} {:s}".format(APPNAME, VERSION), msg, QtWidgets.QMessageBox.Ok) + self.CONN = None + + if dev.IsConnected(): + self.DEVICES[dev.GetFullName()] = dev + dev.Close() + + self.cmbDevice.setStyleSheet("QComboBox { border: 0; margin: 0; padding: 0; max-width: 0px; }") + + if len(self.DEVICES) == 0: + self.lblDevice.setText("No devices found.") + self.lblDevice.setStyleSheet("") + self.cmbDevice.clear() + self.btnConnect.setEnabled(False) + elif len(self.DEVICES) == 1 or (connectToFirst and len(self.DEVICES) > 1): + self.lblDevice.setText(list(self.DEVICES.keys())[0]) + self.lblDevice.setStyleSheet("") + self.ConnectDevice() + self.cmbDevice.clear() + self.btnConnect.setEnabled(True) + else: + self.lblDevice.setText("Select device:") + self.cmbDevice.clear() + self.cmbDevice.addItems(self.DEVICES.keys()) + self.cmbDevice.setCurrentIndex(0) + self.cmbDevice.setStyleSheet("") + self.btnConnect.setEnabled(True) + + self.btnConnect.setEnabled(True) + + if len(self.DEVICES) == 0: return False + + # debug + #self.CONN.SetMode("DMG") + #self.CONN.ReadInfo() + #self.CONN.BackupROM(fncSetProgress=self.PROGRESS.SetProgress, path="T:/GBX/TEST/b.gb", mbc=0x19, rom_banks=64, agb_rom_size=1024*1024, fast_read_mode=True, cart_type=21) + return True + + def AbortOperation(self): + self.CONN.CANCEL = True + + def FinishOperation(self): + if self.lblStatus2aResult.text() == "Pending...": self.lblStatus2aResult.setText("–") + self.lblStatus4aResult.setText("") + self.grpDMGCartridgeInfo.setEnabled(True) + self.grpAGBCartridgeInfo.setEnabled(True) + self.grpActions.setEnabled(True) + self.btnCancel.setEnabled(False) + + dontShowAgain = str(self.SETTINGS.value("SkipFinishMessage", default="disabled")).lower() == "enabled" + + msgbox = QtWidgets.QMessageBox(parent=self, icon=QtWidgets.QMessageBox.Information, windowTitle="{:s} {:s}".format(APPNAME, VERSION), text="Operation complete!", standardButtons=QtWidgets.QMessageBox.Ok) + cb = QtWidgets.QCheckBox("Don’t show this message again.", checked=False) + msgbox.setCheckBox(cb) + + if self.CONN.INFO["last_action"] == 4: # Flash ROM + self.CONN.INFO["last_action"] = 0 + self.ReadCartridge(resetStatus=False) + self.lblStatus4a.setText("Done!") + if "verified" in self.PROGRESS.PROGRESS and self.PROGRESS.PROGRESS["verified"] == True: + msgbox.setText("The ROM was flashed and verified successfully!") + else: + msgbox.setText("ROM flashing complete!") + if not dontShowAgain: + msgbox.exec() + dontShowAgain = cb.isChecked() + + elif self.CONN.INFO["last_action"] == 1: # Backup ROM + self.CONN.INFO["last_action"] = 0 + + if self.CONN.GetMode() == "DMG": + if self.CONN.INFO["rom_checksum"] == self.CONN.INFO["rom_checksum_calc"]: + self.lblHeaderROMChecksumResult.setText("Valid (0x{:04X})".format(self.CONN.INFO["rom_checksum"])) + self.lblHeaderROMChecksumResult.setStyleSheet("QLabel { color: green; }") + self.lblStatus4a.setText("Done!") + msgbox.setText("The ROM backup is complete and the checksum was verified successfully!") + if not dontShowAgain: + msgbox.exec() + dontShowAgain = cb.isChecked() + elif "DMG-MMSA-JPN" in self.cmbDMGCartridgeTypeResult.currentText(): + self.lblHeaderROMChecksumResult.setText("0x{:04X}".format(self.CONN.INFO["rom_checksum_calc"])) + self.lblStatus4a.setText("Done!") + msgbox.setText("The ROM backup is complete!") + if not dontShowAgain: + msgbox.exec() + dontShowAgain = cb.isChecked() + else: + self.lblHeaderROMChecksumResult.setText("Invalid (0x{:04X}≠0x{:04X})".format(self.CONN.INFO["rom_checksum_calc"], self.CONN.INFO["rom_checksum"])) + self.lblHeaderROMChecksumResult.setStyleSheet("QLabel { color: red; }") + self.lblStatus4a.setText("Done.") + QtWidgets.QMessageBox.warning(self, "{:s} {:s}".format(APPNAME, VERSION), "The ROM was dumped, but the checksum is not correct. This may indicate a bad dump, however this can be normal for some reproduction cartridges, prototypes, patched games and intentional overdumps.", QtWidgets.QMessageBox.Ok) + elif self.CONN.GetMode() == "AGB": + if Util.AGB_Global_CRC32 == self.CONN.INFO["rom_checksum_calc"]: + self.lblAGBHeaderROMChecksumResult.setText("Valid (0x{:06X})".format(Util.AGB_Global_CRC32)) + self.lblAGBHeaderROMChecksumResult.setStyleSheet("QLabel { color: green; }") + self.lblStatus4a.setText("Done!") + msgbox.setText("The ROM backup is complete and the checksum was verified successfully!") + if not dontShowAgain: + msgbox.exec() + dontShowAgain = cb.isChecked() + elif Util.AGB_Global_CRC32 == 0: + self.lblAGBHeaderROMChecksumResult.setText("0x{:06X}".format(self.CONN.INFO["rom_checksum_calc"])) + self.lblAGBHeaderROMChecksumResult.setStyleSheet(self.lblHeaderCGBResult.styleSheet()) + self.lblStatus4a.setText("Done!") + QtWidgets.QMessageBox.information(self, "{:s} {:s}".format(APPNAME, VERSION), "The ROM backup is complete! As there is no known checksum for this ROM in the database, verification was skipped.", QtWidgets.QMessageBox.Ok) + else: + self.lblAGBHeaderROMChecksumResult.setText("Invalid (0x{:06X}≠0x{:06X})".format(self.CONN.INFO["rom_checksum_calc"], Util.AGB_Global_CRC32)) + self.lblAGBHeaderROMChecksumResult.setStyleSheet("QLabel { color: red; }") + self.lblStatus4a.setText("Done.") + QtWidgets.QMessageBox.warning(self, "{:s} {:s}".format(APPNAME, VERSION), "The ROM backup is complete, but the checksum doesn’t match the known database entry. This may indicate a bad dump, however this can be normal for some reproduction cartridges, prototypes, patched games and intentional overdumps.", QtWidgets.QMessageBox.Ok) + + elif self.CONN.INFO["last_action"] == 2: # Backup RAM + self.lblStatus4a.setText("Done!") + self.CONN.INFO["last_action"] = 0 + if self.CONN.INFO["transferred"] == 131072: # 128 KB + with open(self.CONN.INFO["last_path"], "rb") as file: temp = file.read() + if temp[0x1FFB1:0x1FFB6] == b'Magic': + answer = QtWidgets.QMessageBox.question(self, "{:s} {:s}".format(APPNAME, VERSION), "Game Boy Camera save data was detected.\nWould you like to load it with the GB Camera Viewer now?", QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.Yes) + if answer == QtWidgets.QMessageBox.Yes: + self.CAMWIN = None + self.CAMWIN = PocketCameraWindow(self, icon=self.windowIcon(), file=self.CONN.INFO["last_path"]) + self.CAMWIN.setAttribute(QtCore.Qt.WA_DeleteOnClose, True) + self.CAMWIN.setModal(True) + self.CAMWIN.run() + return + + msgbox.setText("The save data backup is complete!") + if not dontShowAgain: + msgbox.exec() + dontShowAgain = cb.isChecked() + + elif self.CONN.INFO["last_action"] == 3: # Restore RAM + self.lblStatus4a.setText("Done!") + self.CONN.INFO["last_action"] = 0 + if "save_erase" in self.CONN.INFO and self.CONN.INFO["save_erase"]: + msg_text = "The save data was erased." + del(self.CONN.INFO["save_erase"]) + else: + msg_text = "The save data was restored!" + msgbox.setText(msg_text) + if not dontShowAgain: + msgbox.exec() + dontShowAgain = cb.isChecked() + + else: + self.lblStatus4a.setText("Ready.") + self.CONN.INFO["last_action"] = 0 + + if dontShowAgain: self.SETTINGS.setValue("SkipFinishMessage", "enabled") + self.SetProgressBars(min=0, max=1, value=1) + + def CartridgeTypeAutoDetect(self): + cart_type = 0 + cart_text = "" + + if self.CONN.CheckROMStable() is False: + QtWidgets.QMessageBox.critical(self, "{:s} {:s}".format(APPNAME, VERSION), "Unstable ROM reading detected. Please make sure you selected the correct mode and that the cartridge contacts are clean.", QtWidgets.QMessageBox.Ok) + return 0 + + if self.CONN.GetMode() in self.FLASHCARTS and len(self.FLASHCARTS[self.CONN.GetMode()]) == 0: + QtWidgets.QMessageBox.critical(self, "{:s} {:s}".format(APPNAME, VERSION), "No flash cartridge type configuration files found. Try to restart the application with the “--reset” command line switch to reset the configuration.", QtWidgets.QMessageBox.Ok) + return 0 + + msgbox = QtWidgets.QMessageBox(parent=self, icon=QtWidgets.QMessageBox.Question, windowTitle="{:s} {:s}".format(APPNAME, VERSION), text="Would you like " + APPNAME + " to try and auto-detect the flash cartridge type?\n(Genuine game cartridges can not be re-written.)", standardButtons=QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No) + msgbox.setDefaultButton(QtWidgets.QMessageBox.Yes) + cb = QtWidgets.QCheckBox("Limit voltage to 3.3V", checked=True) + if self.CONN.GetMode() == "DMG": + msgbox.setCheckBox(cb) + answer = msgbox.exec() + limitVoltage = cb.isChecked() + if answer == QtWidgets.QMessageBox.No: + return 0 + else: + detected = self.CONN.AutoDetectFlash(limitVoltage) + if len(detected) == 0: + msgbox = QtWidgets.QMessageBox(parent=self, icon=QtWidgets.QMessageBox.Question, windowTitle="{:s} {:s}".format(APPNAME, VERSION), text="No pre-configured flash cartridge type was detected. You can still try and manually select one from the list -- look for similar PCB text and/or flash chip markings. However, chances are this cartridge is currently not supported for flashing with " + APPNAME + ".\n\nWould you like " + APPNAME + " to run a flash chip query? This may help adding support for your flash cartridge in the future.", standardButtons=QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No) + msgbox.setDefaultButton(QtWidgets.QMessageBox.Yes) + if self.CONN.GetMode() == "DMG": + msgbox.setCheckBox(cb) + answer = msgbox.exec() + if self.CONN.GetMode() == "DMG": + limitVoltage = cb.isChecked() + else: + limitVoltage = False + + if answer == QtWidgets.QMessageBox.Yes: + (flash_id, cfi_s, cfi) = self.CONN.CheckFlashChip(limitVoltage) + if cfi_s == "": + QtWidgets.QMessageBox.information(self, "{:s} {:s}".format(APPNAME, VERSION), "Flash chip query result:
" + flash_id + "There was no Common Flash Interface (CFI) response from the cartridge. Please clean the cartridge contacts and make sure that the cartridge is seated correctly. If a flash chip exists on the cartridge PCB, it may be too old or require unique unlocking and handling.", QtWidgets.QMessageBox.Ok) + else: + QtWidgets.QMessageBox.information(self, "{:s} {:s}".format(APPNAME, VERSION), "Flash chip query result:
" + flash_id + "
" + str(cfi_s) + "", QtWidgets.QMessageBox.Ok) + with open(self.CONFIG_PATH + "/cfi.bin", "wb") as f: f.write(cfi['raw']) + return 0 + else: + cart_type = detected[0] + size_undetected = False + sectors_undetected = False + if self.CONN.GetMode() == "DMG": cart_types = self.CONN.GetSupportedCartridgesDMG() + elif self.CONN.GetMode() == "AGB": cart_types = self.CONN.GetSupportedCartridgesAGB() + size = cart_types[1][detected[0]]["flash_size"] + if "sector_size" in cart_types[1][detected[0]]: + sectors = cart_types[1][detected[0]]["sector_size"] + else: + sectors = [] + + for i in range(0, len(detected)): + if size != cart_types[1][detected[i]]["flash_size"]: + size_undetected = True + if "sector_size_from_cfi" not in cart_types[1][detected[i]] and "sector_size" in cart_types[1][detected[i]] and sectors != cart_types[1][detected[i]]["sector_size"]: + sectors_undetected = True + cart_text += "- " + cart_types[0][detected[i]] + "\n" + + if size_undetected: + (_, cfi_s, cfi) = self.CONN.CheckFlashChip(limitVoltage=limitVoltage, cart_type=cart_types[1][cart_type]) + if isinstance(cfi, dict) and 'device_size' in cfi: + for i in range(0, len(detected)): + if cfi['device_size'] == cart_types[1][detected[i]]["flash_size"]: + cart_type = detected[i] + size_undetected = False + break + + if len(detected) == 1: + msg_text = "The following flash cartridge type was detected:\n" + cart_text + "\nThe supported ROM size is up to {:d} MB.".format(int(cart_types[1][cart_type]['flash_size'] / 1024 / 1024)) + else: + if size_undetected is True: + msg_text = "Your cartridge responds to flash commands also used by:\n" + cart_text + "\nA compatible entry from this list will now be auto-selected, but you may need to manually adjust the ROM size selection.\n\nIMPORTANT: While these cartridges share the same electronic signature, their supported ROM size can differ. As the size can not be detected automatically at this time, please select it manually." + else: + msg_text = "Your cartridge responds to flash commands also used by:\n" + cart_text + "\nA compatible entry from this list will now be auto-selected.\nThe supported ROM size is up to {:d} MB.".format(int(cart_types[1][cart_type]['flash_size'] / 1024 / 1024)) + + if sectors_undetected and "sector_size_from_cfi" not in cart_types[1][cart_type]: + msg_text = msg_text + "\n\n{:s}IMPORTANT:{:s} While these share most of their attributes, some of them can not be automatically detected. If you encounter any errors while writing a ROM, please manually select the correct type based on the flash chip markings of your cartridge. Enabling the “Prefer chip erase mode” config option can also help.".format(ANSI.RED, ANSI.RESET) + + msgbox = QtWidgets.QMessageBox(parent=self, icon=QtWidgets.QMessageBox.Question, windowTitle="{:s} {:s}".format(APPNAME, VERSION), text=msg_text) + button_ok = msgbox.addButton("&OK", QtWidgets.QMessageBox.ActionRole) + button_cancel = msgbox.addButton("&Cancel", QtWidgets.QMessageBox.RejectRole) + button_cfi = msgbox.addButton(" Run flash chip &query ", QtWidgets.QMessageBox.ActionRole) + msgbox.setDefaultButton(button_ok) + msgbox.setEscapeButton(button_cancel) + answer = msgbox.exec() + if msgbox.clickedButton() == button_cfi: + (flash_id, cfi_s, cfi) = self.CONN.CheckFlashChip(limitVoltage=limitVoltage, cart_type=cart_types[1][cart_type]) + if cfi_s == "": + QtWidgets.QMessageBox.information(self, "{:s} {:s}".format(APPNAME, VERSION), "Flash chip query result:
" + flash_id + "There was no Common Flash Interface (CFI) response from the cartridge. If a flash chip exists on the cartridge PCB, it may be too old or require unique unlocking and handling.", QtWidgets.QMessageBox.Ok) + else: + QtWidgets.QMessageBox.information(self, "{:s} {:s}".format(APPNAME, VERSION), "Flash chip query result:
" + flash_id + "
" + str(cfi_s) + "", QtWidgets.QMessageBox.Ok) + with open(self.CONFIG_PATH + "/cfi.bin", "wb") as f: f.write(cfi['raw']) + elif msgbox.clickedButton() == button_cancel: return 0 + + return cart_type + + def CartridgeTypeChanged(self, index): + if self.CONN.GetMode() == "DMG": + cart_types = self.CONN.GetSupportedCartridgesDMG() + if cart_types[1][index] == "AUTODETECT": # special keyword + cart_type = self.CartridgeTypeAutoDetect() + if (cart_type == 1): cart_type = 0 + self.cmbDMGCartridgeTypeResult.setCurrentIndex(cart_type) + elif cart_types[1][index] == "RETAIL": # special keyword + pass + else: + for i in range(0, len(Util.DMG_Header_ROM_Sizes_Flasher_Map)): + if cart_types[1][index]["flash_size"] == (Util.DMG_Header_ROM_Sizes_Flasher_Map[i] * 0x4000): + self.cmbHeaderROMSizeResult.setCurrentIndex(i) + + #if "DMG-MMSA-JPN" in cart_types[0][index]: + # self.cmbHeaderFeaturesResult.setCurrentIndex(list(Util.DMG_Header_Features.keys()).index(0x105)) + + elif self.CONN.GetMode() == "AGB": + cart_types = self.CONN.GetSupportedCartridgesAGB() + if cart_types[1][index] == "AUTODETECT": # special keyword + cart_type = self.CartridgeTypeAutoDetect() + if (cart_type == 1): cart_type = 0 + self.cmbAGBCartridgeTypeResult.setCurrentIndex(cart_type) + elif cart_types[1][index] == "RETAIL": # special keyword + pass + else: + self.cmbAGBHeaderROMSizeResult.setCurrentIndex(Util.AGB_Header_ROM_Sizes_Map.index(cart_types[1][index]["flash_size"])) + + def BackupROM(self): + if not self.CheckDeviceAlive(): return + mbc = (list(Util.DMG_Header_Features.items())[self.cmbHeaderFeaturesResult.currentIndex()])[0] + rom_banks = Util.DMG_Header_ROM_Sizes_Flasher_Map[self.cmbHeaderROMSizeResult.currentIndex()] + + fast_read_mode = self.SETTINGS.value("FastReadMode", default="disabled") + if fast_read_mode and fast_read_mode.lower() == "enabled": + fast_read_mode = True + else: + fast_read_mode = False + + rom_size = 0 + cart_type = 0 + if self.CONN.GetMode() == "DMG": + setting_name = "LastDirRomDMG" + last_dir = self.SETTINGS.value(setting_name) + if last_dir is None: last_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DocumentsLocation) + path = self.lblHeaderTitleResult.text().strip().encode('ascii', 'ignore').decode('ascii') + if path == "" or path == "(No ROM data detected)": path = "ROM" + path = re.sub(r"[<>:\"/\\|\?\*]", "_", path) + if self.CONN.INFO["cgb"] == 0xC0 or self.CONN.INFO["cgb"] == 0x80: + path = path + ".gbc" + elif self.CONN.INFO["sgb"] == 0x03: + path = path + ".sgb" + else: + path = path + ".gb" + path = QtWidgets.QFileDialog.getSaveFileName(self, "Backup ROM", last_dir + "/" + path, "Game Boy ROM File (*.gb *.sgb *.gbc);;All Files (*.*)")[0] + cart_type = self.cmbDMGCartridgeTypeResult.currentIndex() + + elif self.CONN.GetMode() == "AGB": + setting_name = "LastDirRomAGB" + last_dir = self.SETTINGS.value(setting_name) + if last_dir is None: last_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DocumentsLocation) + path = self.lblAGBHeaderTitleResult.text().strip().encode('ascii', 'ignore').decode('ascii') + "_" + self.lblAGBHeaderCodeResult.text().strip().encode('ascii', 'ignore').decode('ascii') + if path == "_": path = self.lblAGBHeaderCodeResult.text().strip().encode('ascii', 'ignore').decode('ascii') + if path == "" or path == "(No ROM data detected)": path = "ROM" + path = re.sub(r"[<>:\"/\\|\?\*]", "_", path) + rom_size = Util.AGB_Header_ROM_Sizes_Map[self.cmbAGBHeaderROMSizeResult.currentIndex()] + path = path + ".gba" + path = QtWidgets.QFileDialog.getSaveFileName(self, "Backup ROM", last_dir + "/" + path, "Game Boy Advance ROM File (*.gba *.srl);;All Files (*.*)")[0] + cart_type = self.cmbAGBCartridgeTypeResult.currentIndex() + + if (path == ""): return + + self.SETTINGS.setValue(setting_name, os.path.dirname(path)) + self.lblHeaderROMChecksumResult.setStyleSheet(self.lblHeaderCGBResult.styleSheet()) + self.lblAGBHeaderROMChecksumResult.setStyleSheet(self.lblHeaderCGBResult.styleSheet()) + + self.CONN.BackupROM(fncSetProgress=self.PROGRESS.SetProgress, path=path, mbc=mbc, rom_banks=rom_banks, agb_rom_size=rom_size, fast_read_mode=fast_read_mode, cart_type=cart_type) + + def FlashROM(self, dpath=""): + if not self.CheckDeviceAlive(): return + path = "" + if dpath != "": + text = "The following ROM file will now be written to the flash cartridge:\n" + dpath + answer = QtWidgets.QMessageBox.question(self, "{:s} {:s}".format(APPNAME, VERSION), text, QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel, QtWidgets.QMessageBox.Ok) + if answer == QtWidgets.QMessageBox.Cancel: return + path = dpath + + if self.CONN.GetMode() == "DMG": + setting_name = "LastDirRomDMG" + last_dir = self.SETTINGS.value(setting_name) + if last_dir is None: last_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DocumentsLocation) + carts = self.CONN.GetSupportedCartridgesDMG()[1] + cart_type = self.cmbDMGCartridgeTypeResult.currentIndex() + elif self.CONN.GetMode() == "AGB": + setting_name = "LastDirRomAGB" + last_dir = self.SETTINGS.value(setting_name) + if last_dir is None: last_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DocumentsLocation) + carts = self.CONN.GetSupportedCartridgesAGB()[1] + cart_type = self.cmbAGBCartridgeTypeResult.currentIndex() + else: + return + + if cart_type == 0: + cart_type = self.CartridgeTypeAutoDetect() + if (cart_type == 1): cart_type = 0 + if self.CONN.GetMode() == "DMG": + self.cmbDMGCartridgeTypeResult.setCurrentIndex(cart_type) + elif self.CONN.GetMode() == "AGB": + self.cmbAGBCartridgeTypeResult.setCurrentIndex(cart_type) + if cart_type == 0: return + + while path == "": + if self.CONN.GetMode() == "DMG": + path = QtWidgets.QFileDialog.getOpenFileName(self, "Flash ROM", last_dir, "Game Boy ROM File (*.gb *.gbc *.sgb *.bin);;All Files (*.*)")[0] + elif self.CONN.GetMode() == "AGB": + path = QtWidgets.QFileDialog.getOpenFileName(self, "Flash ROM", last_dir, "Game Boy Advance ROM File (*.gba *.srl);;All Files (*.*)")[0] + + if (path == ""): return + + self.SETTINGS.setValue(setting_name, os.path.dirname(path)) + + if os.path.getsize(path) > 0x2000000: # reject too large files to avoid exploding RAM + QtWidgets.QMessageBox.critical(self, "{:s} {:s}".format(APPNAME, VERSION), "Files bigger than 32 MB are not supported.", QtWidgets.QMessageBox.Ok) + return + + with open(path, "rb") as file: buffer = file.read() + rom_size = len(buffer) + if rom_size > carts[cart_type]['flash_size']: + msg = "The selected flash cartridge type seems to support ROMs that are up to {:.2f} MB in size, but the file you selected is {:.2f} MB.".format(int(carts[cart_type]['flash_size'] / 1024 / 1024), os.path.getsize(path)/1024/1024) + msg += " You can still give it a try, but it’s possible that it’s too large which may cause the flashing to fail." + answer = QtWidgets.QMessageBox.warning(self, "{:s} {:s}".format(APPNAME, VERSION), msg, QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel, QtWidgets.QMessageBox.Cancel) + if answer == QtWidgets.QMessageBox.Cancel: return + + override_voltage = False + if 'voltage_variants' in carts[cart_type] and carts[cart_type]['voltage'] == 3.3: + msgbox = QtWidgets.QMessageBox(parent=self, icon=QtWidgets.QMessageBox.Question, windowTitle="{:s} {:s}".format(APPNAME, VERSION), text="The selected flash cartridge type usually flashes fine with 3.3V, however sometimes it may require 5V. Which mode should be used?") + button_3_3v = msgbox.addButton(" Use &3.3V (safer) ", QtWidgets.QMessageBox.ActionRole) + button_5v = msgbox.addButton("Use &5V", QtWidgets.QMessageBox.ActionRole) + button_cancel = msgbox.addButton("&Cancel", QtWidgets.QMessageBox.RejectRole) + msgbox.setDefaultButton(button_3_3v) + msgbox.setEscapeButton(button_cancel) + answer = msgbox.exec() + if msgbox.clickedButton() == button_5v: + override_voltage = 5 + elif msgbox.clickedButton() == button_cancel: return + + reverse_sectors = False + if 'sector_reversal' in carts[cart_type]: + msgbox = QtWidgets.QMessageBox(parent=self, icon=QtWidgets.QMessageBox.Question, windowTitle="{:s} {:s}".format(APPNAME, VERSION), text="The selected flash cartridge type is reported to sometimes have reversed sectors. If the cartridge is not working after flashing, try reversed sectors.") + button_normal = msgbox.addButton("Normal", QtWidgets.QMessageBox.ActionRole) + button_reversed = msgbox.addButton("Reversed", QtWidgets.QMessageBox.ActionRole) + button_cancel = msgbox.addButton("&Cancel", QtWidgets.QMessageBox.RejectRole) + msgbox.setDefaultButton(button_normal) + msgbox.setEscapeButton(button_cancel) + answer = msgbox.exec() + if msgbox.clickedButton() == button_reversed: + reverse_sectors = True + elif msgbox.clickedButton() == button_cancel: return + + prefer_chip_erase = False + if 'chip_erase' in carts[cart_type]['commands'] and 'sector_erase' in carts[cart_type]['commands']: + prefer_chip_erase = self.SETTINGS.value("PreferChipErase", default="disabled") + if prefer_chip_erase and prefer_chip_erase.lower() == "enabled": + prefer_chip_erase = True + else: + prefer_chip_erase = False + + fast_read_mode = self.SETTINGS.value("FastReadMode", default="disabled") + if fast_read_mode and fast_read_mode.lower() == "enabled": + fast_read_mode = True + else: + fast_read_mode = False + + verify_flash = self.SETTINGS.value("VerifyFlash", default="enabled") + if verify_flash and verify_flash.lower() == "enabled": + verify_flash = True + else: + verify_flash = False + + try: + if self.CONN.GetMode() == "DMG": + hdr = RomFileDMG(path).GetHeader() + elif self.CONN.GetMode() == "AGB": + hdr = RomFileAGB(path).GetHeader() + if not hdr["logo_correct"]: + answer = QtWidgets.QMessageBox.warning(self, "{:s} {:s}".format(APPNAME, VERSION), "Warning: The ROM file you selected will not boot on actual hardware due to invalid logo data.", QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel, QtWidgets.QMessageBox.Cancel) + if answer == QtWidgets.QMessageBox.Cancel: return + if not hdr["header_checksum_correct"]: + answer = QtWidgets.QMessageBox.warning(self, "{:s} {:s}".format(APPNAME, VERSION), "Warning: The ROM file you selected will not boot on actual hardware due to an invalid header checksum (expected 0x{:02X} instead of 0x{:02X}).".format(hdr["header_checksum_calc"], hdr["header_checksum"]), QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel, QtWidgets.QMessageBox.Cancel) + if answer == QtWidgets.QMessageBox.Cancel: return + except: + QtWidgets.QMessageBox.critical(self, "{:s} {:s}".format(APPNAME, VERSION), "The file you selected could not be read.", QtWidgets.QMessageBox.Ok) + return + + self.CONN.FlashROM(fncSetProgress=self.PROGRESS.SetProgress, path=path, cart_type=cart_type, override_voltage=override_voltage, prefer_chip_erase=prefer_chip_erase, reverse_sectors=reverse_sectors, fast_read_mode=fast_read_mode, verify_flash=verify_flash) + buffer = None + + def BackupRAM(self): + if not self.CheckDeviceAlive(): return + rtc = False + features = [] + + if self.CONN.GetMode() == "DMG": + setting_name = "LastDirSaveDataDMG" + last_dir = self.SETTINGS.value(setting_name) + if last_dir is None: last_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DocumentsLocation) + path = self.lblHeaderTitleResult.text().strip().encode('ascii', 'ignore').decode('ascii') + if path == "" or path == "(No ROM data detected)": path = "ROM" + #mbc = Util.DMG_Header_Features_MBC[self.cmbHeaderFeaturesResult.currentIndex()] + mbc = (list(Util.DMG_Header_Features.items())[self.cmbHeaderFeaturesResult.currentIndex()])[0] + try: + features = list(Util.DMG_Header_Features.keys())[self.cmbHeaderFeaturesResult.currentIndex()] + except: + pass + save_type = Util.DMG_Header_RAM_Sizes_Flasher_Map[self.cmbHeaderRAMSizeResult.currentIndex()] + if save_type == 0: + QtWidgets.QMessageBox.warning(self, "{:s} {:s}".format(APPNAME, VERSION), "Please select the correct save data size.", QtWidgets.QMessageBox.Ok) + return + elif self.CONN.GetMode() == "AGB": + setting_name = "LastDirSaveDataAGB" + last_dir = self.SETTINGS.value(setting_name) + if last_dir is None: last_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DocumentsLocation) + path = self.lblAGBHeaderTitleResult.text().strip().encode('ascii', 'ignore').decode('ascii') + "_" + self.lblAGBHeaderCodeResult.text().strip().encode('ascii', 'ignore').decode('ascii') + if path == "_": path = self.lblAGBHeaderCodeResult.text().strip().encode('ascii', 'ignore').decode('ascii') + if path == "" or path == "(No ROM data detected)": path = "ROM" + mbc = 0 + save_type = self.cmbAGBSaveTypeResult.currentIndex() + if save_type == 0: + QtWidgets.QMessageBox.warning(self, "{:s} {:s}".format(APPNAME, VERSION), "The save type was not selected or auto-detection failed.", QtWidgets.QMessageBox.Ok) + return + else: + return + + add_date_time = self.SETTINGS.value("SaveFileNameAddDateTime", default="disabled") + if add_date_time and add_date_time.lower() == "enabled": + path = re.sub(r"[<>:\"/\\|\?\*]", "_", path) + "_" + datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + ".sav" + else: + path = re.sub(r"[<>:\"/\\|\?\*]", "_", path) + ".sav" + + path = QtWidgets.QFileDialog.getSaveFileName(self, "Backup Save Data", last_dir + "/" + path, "Save Data File (*.sav);;All Files (*.*)")[0] + if (path == ""): return + + rtc = False + if features in (0x10, 0xFD, 0xFE): # RTC of MBC3, TAMA5, HuC-3 + msg = "Do you want the cartridge’s Real Time Clock register values also to be saved?" + if features == 0x10 and not self.CONN.IsClkConnected(): + msg += "\n\nPlease note that this feature is not fully supported by the {:s} hardware. Latching and restoring RTC register data will not work.".format(self.CONN.GetName()) + answer = QtWidgets.QMessageBox.question(self, "{:s} {:s}".format(APPNAME, VERSION), msg, QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No | QtWidgets.QMessageBox.Cancel, QtWidgets.QMessageBox.Yes) + if answer == QtWidgets.QMessageBox.Cancel: return + rtc = (answer == QtWidgets.QMessageBox.Yes) + + self.SETTINGS.setValue(setting_name, os.path.dirname(path)) + self.CONN.BackupRAM(fncSetProgress=self.PROGRESS.SetProgress, path=path, mbc=mbc, save_type=save_type, rtc=rtc) + + def WriteRAM(self, dpath="", erase=False): + if not self.CheckDeviceAlive(): return + if self.CONN.GetMode() == "DMG": + setting_name = "LastDirSaveDataDMG" + last_dir = self.SETTINGS.value(setting_name) + if last_dir is None: last_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DocumentsLocation) + if dpath == "": path = self.lblHeaderTitleResult.text().strip().encode('ascii', 'ignore').decode('ascii') + #mbc = Util.DMG_Header_Features_MBC[self.cmbHeaderFeaturesResult.currentIndex()] + mbc = (list(Util.DMG_Header_Features.items())[self.cmbHeaderFeaturesResult.currentIndex()])[0] + try: + features = list(Util.DMG_Header_Features.keys())[self.cmbHeaderFeaturesResult.currentIndex()] + except: + features = [] + save_type = Util.DMG_Header_RAM_Sizes_Flasher_Map[self.cmbHeaderRAMSizeResult.currentIndex()] + if save_type == 0: + QtWidgets.QMessageBox.warning(self, "{:s} {:s}".format(APPNAME, VERSION), "Please select the correct save data size.", QtWidgets.QMessageBox.Ok) + return + + elif self.CONN.GetMode() == "AGB": + setting_name = "LastDirSaveDataAGB" + last_dir = self.SETTINGS.value(setting_name) + if last_dir is None: last_dir = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.DocumentsLocation) + if dpath == "": + path = self.lblAGBHeaderTitleResult.text().strip().encode('ascii', 'ignore').decode('ascii') + "_" + self.lblAGBHeaderCodeResult.text().strip().encode('ascii', 'ignore').decode('ascii') + mbc = 0 + save_type = self.cmbAGBSaveTypeResult.currentIndex() + if save_type == 0: + QtWidgets.QMessageBox.critical(self, "{:s} {:s}".format(APPNAME, VERSION), "The save type was not selected or auto-detection failed.", QtWidgets.QMessageBox.Ok) + return + else: + return + + filesize = 0 + if dpath != "": + text = "The following save data file will now be written to the cartridge:\n" + dpath + answer = QtWidgets.QMessageBox.question(self, "{:s} {:s}".format(APPNAME, VERSION), text, QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel, QtWidgets.QMessageBox.Ok) + if answer == QtWidgets.QMessageBox.Cancel: return + path = dpath + self.SETTINGS.setValue(setting_name, os.path.dirname(path)) + elif erase: + answer = QtWidgets.QMessageBox.warning(self, "{:s} {:s}".format(APPNAME, VERSION), "The save data on your cartridge will now be erased.", QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel, QtWidgets.QMessageBox.Cancel) + if answer == QtWidgets.QMessageBox.Cancel: return + else: + path = path + ".sav" + path = QtWidgets.QFileDialog.getOpenFileName(self, "Restore Save Data", last_dir + "/" + path, "Save Data File (*.sav);;All Files (*.*)")[0] + if not path == "": self.SETTINGS.setValue(setting_name, os.path.dirname(path)) + if (path == ""): return + filesize = os.path.getsize(path) + if filesize > 0x100000: # reject too large files to avoid exploding RAM + QtWidgets.QMessageBox.critical(self, "{:s} {:s}".format(APPNAME, VERSION), "Files bigger than 1 MB are not supported.", QtWidgets.QMessageBox.Ok) + return + + rtc = False + # RTC of TAMA5, HuC-3 + if (features == 0xFD and (filesize == save_type + 0x18 or erase)) or \ + (features == 0xFE and (filesize == save_type + 0xC or erase)): #or (features == 0x10 and filesize == save_type + 0x30 or erase): + answer = QtWidgets.QMessageBox.question(self, "{:s} {:s}".format(APPNAME, VERSION), "Do you want the Real Time Clock register values to be also written?", QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No | QtWidgets.QMessageBox.Cancel, QtWidgets.QMessageBox.Yes) + if answer == QtWidgets.QMessageBox.Cancel: return + rtc = (answer == QtWidgets.QMessageBox.Yes) + + self.CONN.RestoreRAM(fncSetProgress=self.PROGRESS.SetProgress, path=path, mbc=mbc, save_type=save_type, erase=erase, rtc=rtc) + + def CheckDeviceAlive(self, setMode=False): + if self.CONN is not None: + mode = self.CONN.GetMode() + if self.CONN.DEVICE is not None: + if not self.CONN.IsConnected(): + self.DisconnectDevice() + self.DEVICES = {} + dontShowAgain = str(self.SETTINGS.value("AutoReconnect", default="disabled")).lower() == "enabled" + if not dontShowAgain: + cb = QtWidgets.QCheckBox("Always try to reconnect without asking", checked=False) + msgbox = QtWidgets.QMessageBox(parent=self, icon=QtWidgets.QMessageBox.Question, windowTitle="{:s} {:s}".format(APPNAME, VERSION), text="The connection to the device was lost. Do you want to try and reconnect to the first device found? The cartridge information will also be reset and read again.", standardButtons=QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No) + msgbox.setDefaultButton(QtWidgets.QMessageBox.Yes) + msgbox.setCheckBox(cb) + answer = msgbox.exec() + dontShowAgain = cb.isChecked() + if dontShowAgain: self.SETTINGS.setValue("AutoReconnect", "enabled") + if answer == QtWidgets.QMessageBox.No: + return False + if self.FindDevices(True): + if setMode is not False: mode = setMode + if mode == "DMG": self.optDMG.setChecked(True) + elif mode == "AGB": self.optAGB.setChecked(True) + self.SetMode() + return True + else: + return False + else: + return True + return False + + def SetMode(self): + setTo = False + mode = self.CONN.GetMode() + if mode == "DMG": + if self.optDMG.isChecked(): return + setTo = "AGB" + elif mode == "AGB": + if self.optAGB.isChecked(): return + setTo = "DMG" + else: # mode not set yet + if self.optDMG.isChecked(): + setTo = "DMG" + elif self.optAGB.isChecked(): + setTo = "AGB" + + voltageWarning = "" + if self.CONN.CanSetVoltageAutomatically(): # device can switch in software + dontShowAgain = str(self.SETTINGS.value("SkipModeChangeWarning", default="disabled")).lower() == "enabled" + elif self.CONN.CanSetVoltageManually(): # device has a physical switch + voltageWarning = "\n\nImportant: Also make sure your device is set to the correct voltage!" + dontShowAgain = False + else: # no voltage switching supported + dontShowAgain = False + + if not dontShowAgain and mode is not None: + cb = QtWidgets.QCheckBox("Don’t show this message again.", checked=False) + msgbox = QtWidgets.QMessageBox(parent=self, icon=QtWidgets.QMessageBox.Warning, windowTitle="{:s} {:s}".format(APPNAME, VERSION), text="The mode will now be changed to " + {"DMG":"Game Boy", "AGB":"Game Boy Advance"}[setTo] + " mode. To be safe, cartridges should only be exchanged while the device is not powered on." + voltageWarning, standardButtons=QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel) + msgbox.setDefaultButton(QtWidgets.QMessageBox.Ok) + if self.CONN.CanSetVoltageAutomatically(): msgbox.setCheckBox(cb) + answer = msgbox.exec() + dontShowAgain = cb.isChecked() + if answer == QtWidgets.QMessageBox.Cancel: + if mode == "DMG": self.optDMG.setChecked(True) + if mode == "AGB": self.optAGB.setChecked(True) + return False + if dontShowAgain: self.SETTINGS.setValue("SkipModeChangeWarning", "enabled") + + if not self.CheckDeviceAlive(setMode=setTo): return + + if self.optDMG.isChecked() and (mode == "AGB" or mode == None): + self.CONN.SetMode("DMG") + elif self.optAGB.isChecked() and (mode == "DMG" or mode == None): + self.CONN.SetMode("AGB") + + self.ReadCartridge() + qt_app.processEvents() + self.btnHeaderRefresh.setEnabled(True) + self.btnBackupROM.setEnabled(True) + self.btnFlashROM.setEnabled(True) + self.btnBackupRAM.setEnabled(True) + self.btnRestoreRAM.setEnabled(True) + self.grpDMGCartridgeInfo.setEnabled(True) + self.grpAGBCartridgeInfo.setEnabled(True) + + def ReadCartridge(self, resetStatus=True): + if not self.CheckDeviceAlive(): return + data = self.CONN.ReadInfo(setPinsAsInputs=True) + + if data == False or len(data) == 0: + self.DisconnectDevice() + return False + + if self.CONN.GetMode() == "DMG": + self.cmbHeaderFeaturesResult.clear() + self.cmbHeaderFeaturesResult.addItems(list(Util.DMG_Header_Features.values())) + self.cmbHeaderFeaturesResult.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents) + self.cmbDMGCartridgeTypeResult.clear() + self.cmbDMGCartridgeTypeResult.addItems(self.CONN.GetSupportedCartridgesDMG()[0]) + self.cmbDMGCartridgeTypeResult.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents) + self.cmbHeaderROMSizeResult.clear() + self.cmbHeaderROMSizeResult.addItems(Util.DMG_Header_ROM_Sizes) + self.cmbHeaderROMSizeResult.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents) + self.cmbHeaderRAMSizeResult.clear() + self.cmbHeaderRAMSizeResult.addItems(Util.DMG_Header_RAM_Sizes) + self.cmbHeaderRAMSizeResult.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents) + if "flash_type" in data: + self.cmbDMGCartridgeTypeResult.setCurrentIndex(data["flash_type"]) + + self.lblHeaderTitleResult.setText(data['game_title']) + if data['sgb'] in Util.DMG_Header_SGB: + self.lblHeaderSGBResult.setText(Util.DMG_Header_SGB[data['sgb']]) + else: + self.lblHeaderSGBResult.setText("Unknown (0x{:02X})".format(data['sgb'])) + if data['cgb'] in Util.DMG_Header_CGB: + self.lblHeaderCGBResult.setText(Util.DMG_Header_CGB[data['cgb']]) + else: + self.lblHeaderCGBResult.setText("Unknown (0x{:02X})".format(data['cgb'])) + if data['logo_correct']: + self.lblHeaderLogoValidResult.setText("OK") + self.lblHeaderLogoValidResult.setStyleSheet(self.lblHeaderCGBResult.styleSheet()) + else: + self.lblHeaderLogoValidResult.setText("Invalid") + self.lblHeaderLogoValidResult.setStyleSheet("QLabel { color: red; }") + if data['header_checksum_correct']: + self.lblHeaderChecksumResult.setText("Valid (0x{:02X})".format(data['header_checksum'])) + self.lblHeaderChecksumResult.setStyleSheet(self.lblHeaderCGBResult.styleSheet()) + else: + self.lblHeaderChecksumResult.setText("Invalid (0x{:02X})".format(data['header_checksum'])) + self.lblHeaderChecksumResult.setStyleSheet("QLabel { color: red; }") + self.lblHeaderROMChecksumResult.setText("0x{:04X}".format(data['rom_checksum'])) + self.lblHeaderROMChecksumResult.setStyleSheet(self.lblHeaderCGBResult.styleSheet()) + self.cmbHeaderROMSizeResult.setCurrentIndex(data["rom_size_raw"]) + for i in range(0, len(Util.DMG_Header_RAM_Sizes_Map)): + if data["ram_size_raw"] == Util.DMG_Header_RAM_Sizes_Map[i]: + self.cmbHeaderRAMSizeResult.setCurrentIndex(i) + i = 0 + for k in Util.DMG_Header_Features.keys(): + if data["features_raw"] == k: + self.cmbHeaderFeaturesResult.setCurrentIndex(i) + if k == 0x06: # MBC2 + self.cmbHeaderRAMSizeResult.setCurrentIndex(1) + elif k == 0x22 and data["game_title"] in ("KORO2 KIRBYKKKJ", "KIRBY TNT__KTNE"): # MBC7 Kirby + self.cmbHeaderRAMSizeResult.setCurrentIndex(Util.DMG_Header_RAM_Sizes_Map.index(0x101)) + elif k == 0x22 and data["game_title"] in ("CMASTER____KCEJ"): # MBC7 Command Master + self.cmbHeaderRAMSizeResult.setCurrentIndex(Util.DMG_Header_RAM_Sizes_Map.index(0x102)) + elif k == 0xFD: # TAMA5 + self.cmbHeaderRAMSizeResult.setCurrentIndex(Util.DMG_Header_RAM_Sizes_Map.index(0x103)) + + i += 1 + + if data['empty'] == True: # defaults + self.lblHeaderTitleResult.setText("(No ROM data detected)") + self.lblHeaderTitleResult.setStyleSheet("QLabel { color: red; }") + self.cmbHeaderROMSizeResult.setCurrentIndex(11) + self.cmbHeaderRAMSizeResult.setCurrentIndex(0) + self.cmbHeaderFeaturesResult.setCurrentIndex(0) + else: + self.lblHeaderTitleResult.setStyleSheet(self.lblHeaderCGBResult.styleSheet()) + if data['logo_correct'] and not self.CONN.IsSupportedMbc(data["features_raw"]): + QtWidgets.QMessageBox.warning(self, "{:s} {:s}".format(APPNAME, VERSION), "This cartridge uses a Memory Bank Controller that may not be completely supported yet. A future version of {:s} may add support for it.".format(APPNAME), QtWidgets.QMessageBox.Ok) + if data['logo_correct'] and data['game_title'] == "NP M-MENU MENU": + cart_types = self.CONN.GetSupportedCartridgesDMG() + for i in range(0, len(cart_types[0])): + if "DMG-MMSA-JPN" in cart_types[0][i]: + self.cmbDMGCartridgeTypeResult.setCurrentIndex(i) + + self.grpAGBCartridgeInfo.setVisible(False) + self.grpDMGCartridgeInfo.setVisible(True) + + elif self.CONN.GetMode() == "AGB": + self.cmbAGBCartridgeTypeResult.clear() + self.cmbAGBCartridgeTypeResult.addItems(self.CONN.GetSupportedCartridgesAGB()[0]) + self.cmbAGBCartridgeTypeResult.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents) + if "flash_type" in data: + self.cmbAGBCartridgeTypeResult.setCurrentIndex(data["flash_type"]) + + self.lblAGBHeaderTitleResult.setText(data['game_title']) + self.lblAGBHeaderCodeResult.setText(data['game_code']) + self.lblAGBHeaderVersionResult.setText(str(data['version'])) + if data['logo_correct']: + self.lblAGBHeaderLogoValidResult.setText("OK") + self.lblAGBHeaderLogoValidResult.setStyleSheet(self.lblAGBHeaderCodeResult.styleSheet()) + else: + self.lblAGBHeaderLogoValidResult.setText("Invalid") + self.lblAGBHeaderLogoValidResult.setStyleSheet("QLabel { color: red; }") + + if data['96h_correct']: + self.lblAGBHeader96hResult.setText("OK") + self.lblAGBHeader96hResult.setStyleSheet(self.lblAGBHeaderCodeResult.styleSheet()) + else: + self.lblAGBHeader96hResult.setText("Invalid") + self.lblAGBHeader96hResult.setStyleSheet("QLabel { color: red; }") + + if data['header_checksum_correct']: + self.lblAGBHeaderChecksumResult.setText("Valid (0x{:02X})".format(data['header_checksum'])) + self.lblAGBHeaderChecksumResult.setStyleSheet(self.lblAGBHeaderCodeResult.styleSheet()) + else: + self.lblAGBHeaderChecksumResult.setText("Invalid (0x{:02X})".format(data['header_checksum'])) + self.lblAGBHeaderChecksumResult.setStyleSheet("QLabel { color: red; }") + + self.lblAGBHeaderROMChecksumResult.setStyleSheet(self.lblHeaderCGBResult.styleSheet()) + self.lblAGBHeaderROMChecksumResult.setText("Not available") + Util.AGB_Global_CRC32 = 0 + + db_agb_entry = None + if os.path.exists("{0:s}/db_AGB.json".format(self.CONFIG_PATH)): + with open("{0:s}/db_AGB.json".format(self.CONFIG_PATH)) as f: + db_agb = f.read() + db_agb = json.loads(db_agb) + if data["header_sha1"] in db_agb.keys(): + db_agb_entry = db_agb[data["header_sha1"]] + else: + self.lblAGBHeaderROMChecksumResult.setText("Not in database") + else: + print("FAIL: Database for Game Boy Advance titles not found in " + "{0:s}/db_AGB.json".format(self.CONFIG_PATH)) + + if db_agb_entry != None: + self.cmbAGBHeaderROMSizeResult.setCurrentIndex(Util.AGB_Header_ROM_Sizes_Map.index(db_agb_entry['rs'])) + if data["rom_size_calc"] < 0x400000: + self.lblAGBHeaderROMChecksumResult.setText("In database (0x{:06X})".format(db_agb_entry['rc'])) + Util.AGB_Global_CRC32 = db_agb_entry['rc'] + + elif data["rom_size"] != 0: + if not data["rom_size"] in Util.AGB_Header_ROM_Sizes_Map: + data["rom_size"] = 0x2000000 + self.cmbAGBHeaderROMSizeResult.setCurrentIndex(Util.AGB_Header_ROM_Sizes_Map.index(data["rom_size"])) + else: + self.cmbAGBHeaderROMSizeResult.setCurrentIndex(0) + + if data["save_type"] == None: + self.cmbAGBSaveTypeResult.setCurrentIndex(0) + if db_agb_entry != None: + if db_agb_entry['st'] < len(Util.AGB_Header_Save_Types): + self.cmbAGBSaveTypeResult.setCurrentIndex(db_agb_entry['st']) + + if data['empty'] == True: # defaults + self.lblAGBHeaderTitleResult.setText("(No ROM data detected)") + self.lblAGBHeaderTitleResult.setStyleSheet("QLabel { color: red; }") + self.cmbAGBHeaderROMSizeResult.setCurrentIndex(3) + self.cmbAGBSaveTypeResult.setCurrentIndex(0) + else: + self.lblAGBHeaderTitleResult.setStyleSheet(self.lblHeaderCGBResult.styleSheet()) + + self.grpDMGCartridgeInfo.setVisible(False) + self.grpAGBCartridgeInfo.setVisible(True) + + if data['logo_correct'] and isinstance(db_agb_entry, dict) and "rs" in db_agb_entry and db_agb_entry['rs'] == 0x4000000 and not self.CONN.IsSupported3dMemory(): + QtWidgets.QMessageBox.warning(self, "{:s} {:s}".format(APPNAME, VERSION), "This cartridge uses a Memory Bank Controller that may not be completely supported yet. A future version of the {:s} device firmware may add support for it.".format(self.CONN.GetName()), QtWidgets.QMessageBox.Ok) + + if resetStatus: + self.lblStatus1aResult.setText("–") + self.lblStatus2aResult.setText("–") + self.lblStatus3aResult.setText("–") + self.lblStatus4a.setText("Ready.") + self.grpStatus.setTitle("Transfer Status") + self.FinishOperation() + + if self.CONN.CheckROMStable() is False: + QtWidgets.QMessageBox.warning(self, "{:s} {:s}".format(APPNAME, VERSION), "Unstable ROM reading detected. Please make sure you selected the correct mode and that the cartridge contacts are clean.", QtWidgets.QMessageBox.Ok) + return + + if not data['logo_correct'] and data['empty'] == False: + QtWidgets.QMessageBox.warning(self, "{:s} {:s}".format(APPNAME, VERSION), "The Nintendo Logo check failed which usually means that the cartridge couldn’t be read correctly. Please make sure you selected the correct mode and that the cartridge contacts are clean.", QtWidgets.QMessageBox.Ok) + + if data['game_title'][:11] == "YJencrypted": + QtWidgets.QMessageBox.warning(self, "{:s} {:s}".format(APPNAME, VERSION), "This cartridge may be protected against reading or writing a ROM. If you don’t want to risk this cartridge to render itself unusable, please do not try to write a new ROM to it.", QtWidgets.QMessageBox.Ok) + + def UpdateProgress(self, args): + if args is None: return + if "method" in args: + if args["method"] == "ROM_READ": + self.grpStatus.setTitle("Transfer Status (Backup ROM)") + elif args["method"] == "ROM_WRITE": + self.grpStatus.setTitle("Transfer Status (Flash ROM)") + elif args["method"] == "ROM_WRITE_VERIFY": + self.grpStatus.setTitle("Transfer Status (Verify Flash)") + elif args["method"] == "SAVE_READ": + self.grpStatus.setTitle("Transfer Status (Backup Save Data)") + elif args["method"] == "SAVE_WRITE": + self.grpStatus.setTitle("Transfer Status (Write Save Data)") + + if "error" in args: + self.lblStatus4a.setText("Failed!") + self.grpDMGCartridgeInfo.setEnabled(True) + self.grpAGBCartridgeInfo.setEnabled(True) + self.grpActions.setEnabled(True) + self.btnCancel.setEnabled(False) + msgbox = QtWidgets.QMessageBox(parent=self, icon=QtWidgets.QMessageBox.Critical, windowTitle="{:s} {:s}".format(APPNAME, VERSION), text=str(args["error"]), standardButtons=QtWidgets.QMessageBox.Ok) + if not '\n' in str(args["error"]): msgbox.setTextFormat(QtCore.Qt.RichText) + msgbox.exec() + return + + self.grpDMGCartridgeInfo.setEnabled(False) + self.grpAGBCartridgeInfo.setEnabled(False) + self.grpActions.setEnabled(False) + + pos = 0 + size = 0 + speed = 0 + elapsed = 0 + left = 0 + if "pos" in args: pos = args["pos"] + if "size" in args: size = args["size"] + if "speed" in args: speed = args["speed"] + if "time_elapsed" in args: elapsed = args["time_elapsed"] + if "time_left" in args: left = args["time_left"] + + if "action" in args: + if args["action"] == "ERASE": + self.lblStatus1aResult.setText("Pending...") + self.lblStatus2aResult.setText("Pending...") + self.lblStatus3aResult.setText(Util.formatProgressTime(elapsed)) + self.lblStatus4a.setText("Erasing flash... This may take some time.") + self.lblStatus4aResult.setText("") + self.btnCancel.setEnabled(args["abortable"]) + self.SetProgressBars(min=0, max=size, value=pos) + elif args["action"] == "SECTOR_ERASE": + if elapsed >= 1: + self.lblStatus3aResult.setText(Util.formatProgressTime(elapsed)) + self.lblStatus4a.setText("Erasing sector at address 0x{:X}...".format(args["sector_pos"])) + self.lblStatus4aResult.setText("") + self.btnCancel.setEnabled(args["abortable"]) + self.SetProgressBars(min=0, max=size, value=pos) + elif args["action"] == "ABORTING": + self.lblStatus1aResult.setText("–") + self.lblStatus2aResult.setText("–") + self.lblStatus3aResult.setText("–") + self.lblStatus4a.setText("Stopping... Please wait.") + self.lblStatus4aResult.setText("") + self.btnCancel.setEnabled(args["abortable"]) + self.SetProgressBars(min=0, max=size, value=pos) + elif args["action"] == "FINISHED": + self.FinishOperation() + elif args["action"] == "ABORT": + wd = 10 + while self.CONN.WORKER.isRunning(): + time.sleep(0.1) + wd -= 1 + if wd == 0: break + pass + self.CONN.CANCEL = False + self.grpDMGCartridgeInfo.setEnabled(True) + self.grpAGBCartridgeInfo.setEnabled(True) + self.grpActions.setEnabled(True) + self.grpStatus.setTitle("Transfer Status") + self.lblStatus1aResult.setText("–") + self.lblStatus2aResult.setText("–") + self.lblStatus3aResult.setText("–") + self.lblStatus4a.setText("Stopped.") + self.lblStatus4aResult.setText("") + self.btnCancel.setEnabled(False) + self.SetProgressBars(min=0, max=1, value=0) + self.btnCancel.setEnabled(False) + + if "info_type" in args.keys() and "info_msg" in args.keys(): + if args["info_type"] == "msgbox_critical": + msgbox = QtWidgets.QMessageBox(parent=self, icon=QtWidgets.QMessageBox.Critical, windowTitle="{:s} {:s}".format(APPNAME, VERSION), text=args["info_msg"], standardButtons=QtWidgets.QMessageBox.Ok) + if not '\n' in args["info_msg"]: msgbox.setTextFormat(QtCore.Qt.RichText) + msgbox.exec() + elif args["info_type"] == "msgbox_information": + msgbox = QtWidgets.QMessageBox(parent=self, icon=QtWidgets.QMessageBox.Information, windowTitle="{:s} {:s}".format(APPNAME, VERSION), text=args["info_msg"], standardButtons=QtWidgets.QMessageBox.Ok) + if not '\n' in args["info_msg"]: msgbox.setTextFormat(QtCore.Qt.RichText) + msgbox.exec() + elif args["info_type"] == "label": + self.lblStatus4a.setText(args["info_msg"]) + + return + + elif args["action"] == "PROGRESS": + self.SetProgressBars(min=0, max=size, value=pos) + if "abortable" in args: + self.btnCancel.setEnabled(args["abortable"]) + else: + self.btnCancel.setEnabled(True) + self.lblStatus1aResult.setText(Util.formatFileSize(pos)) + if speed > 0: + self.lblStatus2aResult.setText("{:.2f} KB/s".format(speed)) + else: + self.lblStatus2aResult.setText("Pending...") + if left > 0: + self.lblStatus4aResult.setText(Util.formatProgressTime(left)) + else: + self.lblStatus4aResult.setText("Pending...") + if elapsed > 0: + self.lblStatus3aResult.setText(Util.formatProgressTime(elapsed)) + + if speed == 0 and "skipping" in args and args["skipping"] is True: + self.lblStatus4aResult.setText("Pending...") + self.lblStatus4a.setText("Time left:") + + def SetProgressBars(self, min=0, max=100, value=0, setPause=None): + self.prgStatus.setMinimum(min) + self.prgStatus.setMaximum(max) + self.prgStatus.setValue(value) + if self.TBPROG is not None: + if not value > max: + self.TBPROG.setRange(min, max) + self.TBPROG.setValue(value) + if value != min and value != max: + self.TBPROG.setVisible(True) + else: + self.TBPROG.setVisible(False) + if setPause is not None: + self.TBPROG.setPaused(setPause) + else: + self.TBPROG.setPaused(False) + + def ShowPocketCameraWindow(self): + self.CAMWIN = None + self.CAMWIN = PocketCameraWindow(self, icon=self.windowIcon()) + self.CAMWIN.setAttribute(QtCore.Qt.WA_DeleteOnClose, True) + self.CAMWIN.setModal(True) + self.CAMWIN.run() + + def dragEnterEvent(self, e): + if self._dragEventHover(e): + e.accept() + else: + e.ignore() + + def dragMoveEvent(self, e): + if self._dragEventHover(e): + e.accept() + else: + e.ignore() + + def _dragEventHover(self, e): + if self.btnHeaderRefresh.isEnabled() and self.grpActions.isEnabled() and e.mimeData().hasUrls: + for url in e.mimeData().urls(): + if platform.system() == 'Darwin': + # pylint: disable=undefined-variable + fn = str(NSURL.URLWithString_(str(url.toString())).filePathURL().path()) + else: + fn = str(url.toLocalFile()) + + fn_split = os.path.splitext(os.path.abspath(fn)) + if fn_split[1].lower() == ".sav": + return True + elif self.CONN.GetMode() == "DMG" and fn_split[1].lower() in (".gb", ".sgb", ".gbc", ".bin"): + return True + elif self.CONN.GetMode() == "AGB" and fn_split[1].lower() in (".gba", ".srl"): + return True + else: + return False + return False + + def dropEvent(self, e): + if self.btnHeaderRefresh.isEnabled() and self.grpActions.isEnabled() and e.mimeData().hasUrls: + e.setDropAction(QtCore.Qt.CopyAction) + e.accept() + for url in e.mimeData().urls(): + if platform.system() == 'Darwin': + # pylint: disable=undefined-variable + fn = str(NSURL.URLWithString_(str(url.toString())).filePathURL().path()) + else: + fn = str(url.toLocalFile()) + + fn_split = os.path.splitext(os.path.abspath(fn)) + if fn_split[1].lower() in (".gb", ".sgb", ".gbc", ".bin", ".gba", ".srl"): + self.FlashROM(fn) + elif fn_split[1].lower() == ".sav": + self.WriteRAM(fn) + else: + e.ignore() + + def closeEvent(self, event): + self.DisconnectDevice() + event.accept() + + def run(self): + self.layout.update() + self.layout.activate() + screen = QtGui.QGuiApplication.screens()[0] + screenGeometry = screen.geometry() + x = (screenGeometry.width() - self.width()) / 2 + y = (screenGeometry.height() - self.height()) / 2 + self.move(x, y) + self.setAcceptDrops(True) + self.show() + + # Taskbar Progress on Windows only + try: + from PySide2.QtWinExtras import QWinTaskbarButton, QtWin + myappid = 'lesserkuma.flashgbx' + QtWin.setCurrentProcessExplicitAppUserModelID(myappid) + taskbar_button = QWinTaskbarButton() + self.TBPROG = taskbar_button.progress() + self.TBPROG.setRange(0, 100) + taskbar_button.setWindow(self.windowHandle()) + self.TBPROG.setVisible(False) + except ImportError: + pass + + qt_app.exec_() + +qt_app = QtWidgets.QApplication(sys.argv) +qt_app.setApplicationName(APPNAME) diff --git a/FlashGBX/PocketCamera.py b/FlashGBX/PocketCamera.py index f48fbd8..c34ffdc 100644 --- a/FlashGBX/PocketCamera.py +++ b/FlashGBX/PocketCamera.py @@ -1,19 +1,10 @@ -import sys, functools, os, json, platform, hashlib from PIL import Image, ImageDraw -from PIL.ImageQt import ImageQt from PIL.PngImagePlugin import PngInfo -from PySide2 import QtCore, QtWidgets, QtGui +import os, hashlib import email.utils -class PocketCameraWindow(QtWidgets.QDialog): - CUR_PIC = None - CUR_THUMBS = None - CUR_INDEX = 1 - CUR_BICUBIC = True - CUR_FILE = "" - CUR_EXPORT_PATH = "" - CUR_PC = None - APP = None +class PocketCamera: + DATA = None PALETTES = [ [ 255, 255, 255, 176, 176, 176, 104, 104, 104, 0, 0, 0 ], # Grayscale [ 208, 217, 60, 120, 164, 106, 84, 88, 84, 36, 70, 36 ], # Game Boy @@ -23,358 +14,6 @@ class PocketCameraWindow(QtWidgets.QDialog): [ 240, 240, 240, 220, 160, 160, 136, 78, 78, 30, 30, 30 ], # Game Boy Color (USA Gold) [ 240, 240, 240, 134, 200, 100, 58, 96, 132, 30, 30, 30 ], # Game Boy Color (USA/EUR) ] - - def __init__(self, app, file=None, icon=None): - QtWidgets.QDialog.__init__(self) - self.setAcceptDrops(True) - if icon is not None: self.setWindowIcon(QtGui.QIcon(icon)) - self.CUR_FILE = file - self.setWindowTitle("FlashGBX – GB Camera Album Viewer") - if hasattr(QtGui, "Qt"): - self.setWindowFlags((self.windowFlags() | QtGui.Qt.MSWindowsFixedSizeDialogHint) & ~QtGui.Qt.WindowContextHelpButtonHint); - - self.layout = QtWidgets.QGridLayout() - self.layout.setContentsMargins(-1, 8, -1, 8) - self.layout.setSizeConstraint(QtWidgets.QLayout.SetFixedSize) - self.layout_options1 = QtWidgets.QVBoxLayout() - self.layout_options2 = QtWidgets.QVBoxLayout() - self.layout_options3 = QtWidgets.QVBoxLayout() - self.layout_photos = QtWidgets.QHBoxLayout() - - # Options - self.grpColors = QtWidgets.QGroupBox("Color palette") - grpColorsLayout = QtWidgets.QVBoxLayout() - grpColorsLayout.setContentsMargins(-1, 3, -1, -1) - self.rowColors1 = QtWidgets.QHBoxLayout() - self.optColorBW = QtWidgets.QRadioButton("&Grayscale") - self.optColorBW.setToolTip("Generic grayscale palette") - self.connect(self.optColorBW, QtCore.SIGNAL("clicked()"), self.SetColors) - self.optColorDMG = QtWidgets.QRadioButton("&DMG") - self.optColorDMG.setToolTip("Original Game Boy screen palette") - self.connect(self.optColorDMG, QtCore.SIGNAL("clicked()"), self.SetColors) - #self.optColorMGB = QtWidgets.QRadioButton("&MGB") - #self.optColorMGB.setToolTip("Game Boy Pocket palette") - #self.connect(self.optColorMGB, QtCore.SIGNAL("clicked()"), self.SetColors) - self.optColorSGB = QtWidgets.QRadioButton("&SGB") - self.optColorSGB.setToolTip("Super Game Boy palette") - self.connect(self.optColorSGB, QtCore.SIGNAL("clicked()"), self.SetColors) - self.optColorCGB1 = QtWidgets.QRadioButton("CGB &1") - self.optColorCGB1.setToolTip("Japanese Pocket Camera palette") - self.connect(self.optColorCGB1, QtCore.SIGNAL("clicked()"), self.SetColors) - self.optColorCGB2 = QtWidgets.QRadioButton("CGB &2") - self.optColorCGB2.setToolTip("Golden Limited Edition Game Boy Camera palette") - self.connect(self.optColorCGB2, QtCore.SIGNAL("clicked()"), self.SetColors) - self.optColorCGB3 = QtWidgets.QRadioButton("CGB &3") - self.optColorCGB3.setToolTip("Non-Japanese Game Boy Camera palette") - self.connect(self.optColorCGB3, QtCore.SIGNAL("clicked()"), self.SetColors) - self.rowColors1.addWidget(self.optColorBW) - self.rowColors1.addWidget(self.optColorDMG) - #self.rowColors1.addWidget(self.optColorMGB) - self.rowColors1.addWidget(self.optColorSGB) - self.rowColors1.addWidget(self.optColorCGB1) - self.rowColors1.addWidget(self.optColorCGB2) - self.rowColors1.addWidget(self.optColorCGB3) - self.optColorCGB1.setChecked(True) - - grpColorsLayout.addLayout(self.rowColors1) - - self.grpColors.setLayout(grpColorsLayout) - self.layout_options1.addWidget(self.grpColors) - - rowActionsGeneral1 = QtWidgets.QHBoxLayout() - self.btnOpenSRAM = QtWidgets.QPushButton("&Open Save Data File") - self.btnOpenSRAM.setStyleSheet("padding: 5px 10px;") - self.btnOpenSRAM.clicked.connect(self.btnOpenSRAM_Clicked) - self.btnClose = QtWidgets.QPushButton("&Close") - self.btnClose.setStyleSheet("padding: 5px 15px;") - self.btnClose.clicked.connect(self.btnClose_Clicked) - rowActionsGeneral1.addWidget(self.btnOpenSRAM) - rowActionsGeneral1.addStretch() - rowActionsGeneral1.addWidget(self.btnClose) - self.layout_options3.addLayout(rowActionsGeneral1) - - # Photo Viewer - self.grpPhotoView = QtWidgets.QGroupBox("Preview") - self.grpPhotoViewLayout = QtWidgets.QVBoxLayout() - self.grpPhotoViewLayout.setContentsMargins(-1, 3, -1, -1) - self.lblPhotoViewer = QtWidgets.QLabel(self) - self.lblPhotoViewer.setMinimumSize(256, 223) - self.lblPhotoViewer.setMaximumSize(256, 223) - self.lblPhotoViewer.setStyleSheet("border-top: 1px solid #adadad; border-left: 1px solid #adadad; border-bottom: 1px solid #ffffff; border-right: 1px solid #ffffff;") - self.lblPhotoViewer.mousePressEvent = self.lblPhotoViewer_Clicked - self.grpPhotoViewLayout.addWidget(self.lblPhotoViewer) - - # Actions below Viewer - rowActionsGeneral2 = QtWidgets.QHBoxLayout() - self.btnSavePhoto = QtWidgets.QPushButton("&Save This Picture") - self.btnSavePhoto.setStyleSheet("padding: 5px 10px;") - self.btnSavePhoto.clicked.connect(self.btnSavePhoto_Clicked) - rowActionsGeneral2.addWidget(self.btnSavePhoto) - self.btnSaveAll = QtWidgets.QPushButton("Save &All Pictures") - self.btnSaveAll.setStyleSheet("padding: 5px 10px;") - self.btnSaveAll.clicked.connect(self.btnSaveAll_Clicked) - rowActionsGeneral2.addWidget(self.btnSaveAll) - self.grpPhotoViewLayout.addLayout(rowActionsGeneral2) - - self.grpPhotoView.setLayout(self.grpPhotoViewLayout) - - # Photo List - self.grpPhotoThumbs = QtWidgets.QGroupBox("Photo Album") - self.grpPhotoThumbsLayout = QtWidgets.QVBoxLayout() - self.grpPhotoThumbsLayout.setSpacing(2) - self.grpPhotoThumbsLayout.setContentsMargins(-1, 3, -1, -1) - self.lblPhoto = [] - rowsPhotos = [] - for row in range(0, 5): - rowsPhotos.append(QtWidgets.QHBoxLayout()) - rowsPhotos[row].setSpacing(2) - for col in range(0, 6): - self.lblPhoto.append(QtWidgets.QLabel(self)) - self.lblPhoto[len(self.lblPhoto)-1].setMinimumSize(49, 43) - self.lblPhoto[len(self.lblPhoto)-1].setMaximumSize(49, 43) - self.lblPhoto[len(self.lblPhoto)-1].mousePressEvent = functools.partial(self.lblPhoto_Clicked, index=len(self.lblPhoto)-1) - self.lblPhoto[len(self.lblPhoto)-1].setCursor(QtGui.QCursor(QtGui.Qt.PointingHandCursor)) - self.lblPhoto[len(self.lblPhoto)-1].setAlignment(QtGui.Qt.AlignCenter) - self.lblPhoto[len(self.lblPhoto)-1].setStyleSheet("border-top: 1px solid #adadad; border-left: 1px solid #adadad; border-bottom: 1px solid #fefefe; border-right: 1px solid #fefefe;") - rowsPhotos[row].addWidget(self.lblPhoto[len(self.lblPhoto)-1]) - self.grpPhotoThumbsLayout.addLayout(rowsPhotos[row]) - - rowActionsGeneral3 = QtWidgets.QHBoxLayout() - self.btnShowGameFace = QtWidgets.QPushButton("Load &Game Face") - self.btnShowGameFace.setStyleSheet("padding: 5px 10px;") - self.btnShowGameFace.clicked.connect(self.btnShowGameFace_Clicked) - rowActionsGeneral3.addWidget(self.btnShowGameFace) - self.grpPhotoThumbsLayout.addStretch() - self.grpPhotoThumbsLayout.addLayout(rowActionsGeneral3) - - self.grpPhotoThumbsLayout.setAlignment(QtGui.Qt.AlignTop) - self.grpPhotoThumbs.setLayout(self.grpPhotoThumbsLayout) - - self.layout_photos.addWidget(self.grpPhotoThumbs) - self.layout_photos.addWidget(self.grpPhotoView) - - self.layout.addLayout(self.layout_options1, 0, 0) - self.layout.addLayout(self.layout_options2, 1, 0) - self.layout.addLayout(self.layout_photos, 2, 0) - self.layout.addLayout(self.layout_options3, 3, 0) - self.setLayout(self.layout) - - self.APP = app - - palette = self.APP.SETTINGS.value("PocketCameraPalette") - try: - palette = json.loads(palette) - except: - palette = None - if palette is not None: - for i in range(0, len(self.PALETTES)): - if palette == self.PALETTES[i]: - self.rowColors1.itemAt(i).widget().setChecked(True) - - if self.CUR_FILE is not None: - self.OpenFile(self.CUR_FILE) - - export_path = self.APP.SETTINGS.value("LastDirPocketCamera") - if export_path is not None: - self.CUR_EXPORT_PATH = export_path - - self.SetColors() - - self.btnSaveAll.setDefault(True) - self.btnSaveAll.setAutoDefault(True) - self.btnSaveAll.setFocus() - - def run(self): - self.layout.update() - self.layout.activate() - screenGeometry = QtWidgets.QDesktopWidget().screenGeometry() - x = (screenGeometry.width() - self.width()) / 2 - y = (screenGeometry.height() - self.height()) / 2 - self.move(x, y) - self.show() - - def SetColors(self): - if self.CUR_PC is None: return - for i in range(0, self.rowColors1.count()): - if self.rowColors1.itemAt(i).widget().isChecked(): - self.CUR_PC.SetPalette(self.PALETTES[i]) - self.BuildPhotoList() - self.UpdateViewer(self.CUR_INDEX) - - def OpenFile(self, file): - self.CUR_PC = PocketCamera() - if self.CUR_PC.LoadFile(file) == False: - self.CUR_PC = None - QtWidgets.QMessageBox.warning(self, "FlashGBX", "The save data file couldn’t be loaded.", QtWidgets.QMessageBox.Ok) - return False - self.CUR_FILE = file - if self.CUR_EXPORT_PATH == "": - self.CUR_EXPORT_PATH = os.path.dirname(self.CUR_FILE) - self.UpdateViewer(1) - self.SetColors() - - return True - - def lblPhoto_Clicked(self, event, index): - if event.button() == QtGui.Qt.LeftButton: - self.CUR_INDEX = index + 1 - self.UpdateViewer(self.CUR_INDEX) - - def lblPhotoViewer_Clicked(self, event): - if event.button() == QtGui.Qt.LeftButton: - self.CUR_BICUBIC = not self.CUR_BICUBIC - self.UpdateViewer(self.CUR_INDEX) - - def btnOpenSRAM_Clicked(self): - last_dir = self.APP.SETTINGS.value("LastDirSaveDataDMG") - path = QtWidgets.QFileDialog.getOpenFileName(self, "Open GB Camera Save Data File", last_dir, "Save Data File (*.sav);;All Files (*.*)")[0] - if (path == ""): return - if self.OpenFile(path) is True: - self.APP.SETTINGS.setValue("LastDirSaveDataDMG", os.path.dirname(path)) - - def btnShowGameFace_Clicked(self, event): - self.UpdateViewer(0) - self.CUR_INDEX = 0 - - def btnSaveAll_Clicked(self, event): - if self.CUR_PC is None: return - path = self.CUR_EXPORT_PATH + "/IMG_PC.png" - path = QtWidgets.QFileDialog.getSaveFileName(self, "Export all pictures", path, "PNG Files (*.png);;All Files (*.*)")[0] - if path == "": return - self.CUR_EXPORT_PATH = os.path.dirname(path) - - for i in range(0, 31): - file = os.path.splitext(path)[0] + "{:02d}".format(i) + os.path.splitext(path)[1] - if os.path.exists(file): - answer = QtWidgets.QMessageBox.warning(self, "FlashGBX", "There are already pictures that use the same file names. If you continue, these files will be overwritten.", QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel) - if answer == QtWidgets.QMessageBox.Ok: - break - elif answer == QtWidgets.QMessageBox.Cancel: - return - - for i in range(0, 31): - file = os.path.splitext(path)[0] + "{:02d}".format(i) + os.path.splitext(path)[1] - self.SavePicture(i, path=file) - - def btnSavePhoto_Clicked(self, event): - if self.CUR_PC is None: return - self.SavePicture(self.CUR_INDEX) - - def btnClose_Clicked(self, event): - self.reject() - - def hideEvent(self, event): - for i in range(0, self.rowColors1.count()): - if self.rowColors1.itemAt(i).widget().isChecked(): - self.APP.SETTINGS.setValue("PocketCameraPalette", json.dumps(self.PALETTES[i])) - self.APP.SETTINGS.setValue("LastDirPocketCamera", self.CUR_EXPORT_PATH) - self.APP.activateWindow() - - def BuildPhotoList(self): - cam = self.CUR_PC - self.CUR_THUMBS = [None] * 30 - for i in range(0, 30): - pic = cam.GetPicture(i+1).convert("RGBA") - self.lblPhoto[i].setToolTip("") - if cam.IsEmpty(i+1): - pass - #draw = ImageDraw.Draw(pic, "RGBA") - #draw.line([0, 0, 128, 112], fill=(255, 0, 0), width=8) - #draw.line([0, 112, 128, 0], fill=(255, 0, 0), width=8) - elif cam.IsDeleted(i+1): - draw_bg = Image.new("RGBA", pic.size) - draw = ImageDraw.Draw(draw_bg) - draw.line([0, 0, 128, 112], fill=(255, 0, 0, 192), width=8) - draw.line([0, 112, 128, 0], fill=(255, 0, 0, 192), width=8) - pic.paste(draw_bg, mask=draw_bg) - self.lblPhoto[i].setToolTip("This picture was marked as “deleted” and may be overwritten when you take new pictures.") - self.CUR_THUMBS[i] = ImageQt(pic.resize((47, 41), Image.HAMMING)) - qpixmap = QtGui.QPixmap.fromImage(self.CUR_THUMBS[i]) - self.lblPhoto[i].setPixmap(qpixmap) - - def UpdateViewer(self, index): - resampler = Image.NEAREST - if self.CUR_BICUBIC: resampler = Image.BICUBIC - cam = self.CUR_PC - if cam is None: return - - for i in range(0, 30): - self.lblPhoto[i].setStyleSheet("border-top: 1px solid #adadad; border-left: 1px solid #adadad; border-bottom: 1px solid #ffffff; border-right: 1px solid #ffffff;") - - if index == 0: - self.CUR_PIC = ImageQt(cam.GetPicture(0).convert("RGBA").resize((256, 224), resampler)) - else: - self.CUR_PIC = ImageQt(cam.GetPicture(index).convert("RGBA").resize((256, 224), resampler)) - self.lblPhoto[index - 1].setStyleSheet("border: 3px solid green; padding: 1px;") - - qpixmap = QtGui.QPixmap.fromImage(self.CUR_PIC) - self.lblPhotoViewer.setPixmap(qpixmap) - - def SavePicture(self, index, path=""): - if path == "": - path = self.CUR_EXPORT_PATH + "/IMG_PC{:02d}.png".format(index) - path = QtWidgets.QFileDialog.getSaveFileName(self, "Save Photo", path, "PNG Files (*.png);;All Files (*.*)")[0] - if path != "": self.CUR_EXPORT_PATH = os.path.dirname(path) - if path == "": return - - pnginfo = PngInfo() - pnginfo.add_text("Software", "FlashGBX") - pnginfo.add_text("Source", "Pocket Camera") - pnginfo.add_text("Creation Time", email.utils.formatdate()) - - cam = self.CUR_PC - if index == 0: - pic = cam.GetPicture(0) - pnginfo.add_text("Title", "Game Face") - else: - pic = cam.GetPicture(index) - pnginfo.add_text("Title", "Photo {:02d}".format(index)) - - pic.save(path, pnginfo=pnginfo) - - def dragEnterEvent(self, e): - if self._dragEventHover(e): - e.accept() - else: - e.ignore() - - def dragMoveEvent(self, e): - if self._dragEventHover(e): - e.accept() - else: - e.ignore() - - def _dragEventHover(self, e): - if e.mimeData().hasUrls: - for url in e.mimeData().urls(): - if platform.system() == 'Darwin': - fn = str(NSURL.URLWithString_(str(url.toString())).filePathURL().path()) - else: - fn = str(url.toLocalFile()) - - fn_split = os.path.splitext(os.path.abspath(fn)) - if fn_split[1] == ".sav": - return True - return False - - def dropEvent(self, e): - if e.mimeData().hasUrls: - e.setDropAction(QtCore.Qt.CopyAction) - e.accept() - for url in e.mimeData().urls(): - if platform.system() == 'Darwin': - fn = str(NSURL.URLWithString_(str(url.toString())).filePathURL().path()) - else: - fn = str(url.toLocalFile()) - - fn_split = os.path.splitext(os.path.abspath(fn)) - if fn_split[1] == ".sav": - self.OpenFile(fn) - else: - e.ignore() - -class PocketCamera: - DATA = None PALETTE = [ 240, 240, 240, 218, 196, 106, 112, 88, 52, 30, 30, 30 ] # default IMAGES = [None] * 31 IMAGES_DELETED = [] @@ -410,6 +49,8 @@ class PocketCamera: return True def SetPalette(self, palette): + if isinstance(palette, int): + palette = self.PALETTES[palette] for p in range (0, len(self.IMAGES)): self.IMAGES[p].putpalette(palette) self.PALETTE = palette @@ -453,3 +94,30 @@ class PocketCamera: offset = 0x2000 + (index * 0x1000) imgbuffer = self.DATA[offset:offset+0x1000] return self.ConvertPicture(imgbuffer) + + def ExportPicture(self, index, path): + pnginfo = PngInfo() + pnginfo.add_text("Software", "FlashGBX") + pnginfo.add_text("Source", "Pocket Camera") + pnginfo.add_text("Creation Time", email.utils.formatdate()) + + if index == 0: + pic = self.GetPicture(0) + pnginfo.add_text("Title", "Game Face") + else: + pic = self.GetPicture(index) + pnginfo.add_text("Title", "Photo {:02d}".format(index)) + + ext = os.path.splitext(path)[1] + if ext.lower() == ".png": + outpic = pic + outpic.save(path, pnginfo=pnginfo) + elif ext.lower() == ".gif": + outpic = pic + outpic.save(path) + elif ext.lower() in (".jpg", ".jpeg"): + outpic = pic.convert("RGB") + outpic.save(path, quality=100, subsampling=0) + else: + outpic = pic.convert("RGB") + outpic.save(path) diff --git a/FlashGBX/PocketCameraWindow.py b/FlashGBX/PocketCameraWindow.py new file mode 100644 index 0000000..0760391 --- /dev/null +++ b/FlashGBX/PocketCameraWindow.py @@ -0,0 +1,362 @@ +import functools, os, json, platform +from PIL.ImageQt import ImageQt +from PIL import Image, ImageDraw +from PySide2 import QtCore, QtWidgets, QtGui +from .PocketCamera import PocketCamera + +class PocketCameraWindow(QtWidgets.QDialog): + CUR_PIC = None + CUR_THUMBS = None + CUR_INDEX = 1 + CUR_BICUBIC = True + CUR_FILE = "" + CUR_EXPORT_PATH = "" + CUR_PC = None + APP = None + PALETTES = [ + [ 255, 255, 255, 176, 176, 176, 104, 104, 104, 0, 0, 0 ], # Grayscale + [ 208, 217, 60, 120, 164, 106, 84, 88, 84, 36, 70, 36 ], # Game Boy + #[ 196, 207, 161, 139, 149, 109, 77, 83, 60, 31, 31, 31 ], # Game Boy Pocket + [ 255, 255, 255, 181, 179, 189, 84, 83, 103, 9, 7, 19 ], # Super Game Boy + [ 240, 240, 240, 218, 196, 106, 112, 88, 52, 30, 30, 30 ], # Game Boy Color (JPN) + [ 240, 240, 240, 220, 160, 160, 136, 78, 78, 30, 30, 30 ], # Game Boy Color (USA Gold) + [ 240, 240, 240, 134, 200, 100, 58, 96, 132, 30, 30, 30 ], # Game Boy Color (USA/EUR) + ] + + def __init__(self, app, file=None, icon=None): + QtWidgets.QDialog.__init__(self) + self.setAcceptDrops(True) + if icon is not None: self.setWindowIcon(QtGui.QIcon(icon)) + self.CUR_FILE = file + self.setWindowTitle("FlashGBX – GB Camera Album Viewer") + self.setWindowFlags((self.windowFlags() | QtCore.Qt.MSWindowsFixedSizeDialogHint) & ~QtCore.Qt.WindowContextHelpButtonHint) + + self.layout = QtWidgets.QGridLayout() + self.layout.setContentsMargins(-1, 8, -1, 8) + self.layout.setSizeConstraint(QtWidgets.QLayout.SetFixedSize) + self.layout_options1 = QtWidgets.QVBoxLayout() + self.layout_options2 = QtWidgets.QVBoxLayout() + self.layout_options3 = QtWidgets.QVBoxLayout() + self.layout_photos = QtWidgets.QHBoxLayout() + + # Options + self.grpColors = QtWidgets.QGroupBox("Color palette") + grpColorsLayout = QtWidgets.QVBoxLayout() + grpColorsLayout.setContentsMargins(-1, 3, -1, -1) + self.rowColors1 = QtWidgets.QHBoxLayout() + self.optColorBW = QtWidgets.QRadioButton("&Grayscale") + self.optColorBW.setToolTip("Generic grayscale palette") + self.connect(self.optColorBW, QtCore.SIGNAL("clicked()"), self.SetColors) + self.optColorDMG = QtWidgets.QRadioButton("&DMG") + self.optColorDMG.setToolTip("Original Game Boy screen palette") + self.connect(self.optColorDMG, QtCore.SIGNAL("clicked()"), self.SetColors) + #self.optColorMGB = QtWidgets.QRadioButton("&MGB") + #self.optColorMGB.setToolTip("Game Boy Pocket palette") + #self.connect(self.optColorMGB, QtCore.SIGNAL("clicked()"), self.SetColors) + self.optColorSGB = QtWidgets.QRadioButton("&SGB") + self.optColorSGB.setToolTip("Super Game Boy palette") + self.connect(self.optColorSGB, QtCore.SIGNAL("clicked()"), self.SetColors) + self.optColorCGB1 = QtWidgets.QRadioButton("CGB &1") + self.optColorCGB1.setToolTip("Japanese Pocket Camera palette") + self.connect(self.optColorCGB1, QtCore.SIGNAL("clicked()"), self.SetColors) + self.optColorCGB2 = QtWidgets.QRadioButton("CGB &2") + self.optColorCGB2.setToolTip("Golden Limited Edition Game Boy Camera palette") + self.connect(self.optColorCGB2, QtCore.SIGNAL("clicked()"), self.SetColors) + self.optColorCGB3 = QtWidgets.QRadioButton("CGB &3") + self.optColorCGB3.setToolTip("Non-Japanese Game Boy Camera palette") + self.connect(self.optColorCGB3, QtCore.SIGNAL("clicked()"), self.SetColors) + self.rowColors1.addWidget(self.optColorBW) + self.rowColors1.addWidget(self.optColorDMG) + #self.rowColors1.addWidget(self.optColorMGB) + self.rowColors1.addWidget(self.optColorSGB) + self.rowColors1.addWidget(self.optColorCGB1) + self.rowColors1.addWidget(self.optColorCGB2) + self.rowColors1.addWidget(self.optColorCGB3) + self.optColorCGB1.setChecked(True) + + grpColorsLayout.addLayout(self.rowColors1) + + self.grpColors.setLayout(grpColorsLayout) + self.layout_options1.addWidget(self.grpColors) + + rowActionsGeneral1 = QtWidgets.QHBoxLayout() + self.btnOpenSRAM = QtWidgets.QPushButton("&Open Save Data File") + self.btnOpenSRAM.setStyleSheet("padding: 5px 10px;") + self.btnOpenSRAM.clicked.connect(self.btnOpenSRAM_Clicked) + self.btnClose = QtWidgets.QPushButton("&Close") + self.btnClose.setStyleSheet("padding: 5px 15px;") + self.btnClose.clicked.connect(self.btnClose_Clicked) + rowActionsGeneral1.addWidget(self.btnOpenSRAM) + rowActionsGeneral1.addStretch() + rowActionsGeneral1.addWidget(self.btnClose) + self.layout_options3.addLayout(rowActionsGeneral1) + + # Photo Viewer + self.grpPhotoView = QtWidgets.QGroupBox("Preview") + self.grpPhotoViewLayout = QtWidgets.QVBoxLayout() + self.grpPhotoViewLayout.setContentsMargins(-1, 3, -1, -1) + self.lblPhotoViewer = QtWidgets.QLabel(self) + self.lblPhotoViewer.setMinimumSize(256, 223) + self.lblPhotoViewer.setMaximumSize(256, 223) + self.lblPhotoViewer.setStyleSheet("border-top: 1px solid #adadad; border-left: 1px solid #adadad; border-bottom: 1px solid #ffffff; border-right: 1px solid #ffffff;") + self.lblPhotoViewer.mousePressEvent = self.lblPhotoViewer_Clicked + self.grpPhotoViewLayout.addWidget(self.lblPhotoViewer) + + # Actions below Viewer + rowActionsGeneral2 = QtWidgets.QHBoxLayout() + self.btnSavePhoto = QtWidgets.QPushButton("&Save This Picture") + self.btnSavePhoto.setStyleSheet("padding: 5px 10px;") + self.btnSavePhoto.clicked.connect(self.btnSavePhoto_Clicked) + rowActionsGeneral2.addWidget(self.btnSavePhoto) + self.btnSaveAll = QtWidgets.QPushButton("Save &All Pictures") + self.btnSaveAll.setStyleSheet("padding: 5px 10px;") + self.btnSaveAll.clicked.connect(self.btnSaveAll_Clicked) + rowActionsGeneral2.addWidget(self.btnSaveAll) + self.grpPhotoViewLayout.addLayout(rowActionsGeneral2) + + self.grpPhotoView.setLayout(self.grpPhotoViewLayout) + + # Photo List + self.grpPhotoThumbs = QtWidgets.QGroupBox("Photo Album") + self.grpPhotoThumbsLayout = QtWidgets.QVBoxLayout() + self.grpPhotoThumbsLayout.setSpacing(2) + self.grpPhotoThumbsLayout.setContentsMargins(-1, 3, -1, -1) + self.lblPhoto = [] + rowsPhotos = [] + for row in range(0, 5): + rowsPhotos.append(QtWidgets.QHBoxLayout()) + rowsPhotos[row].setSpacing(2) + for _ in range(0, 6): + self.lblPhoto.append(QtWidgets.QLabel(self)) + self.lblPhoto[len(self.lblPhoto)-1].setMinimumSize(49, 43) + self.lblPhoto[len(self.lblPhoto)-1].setMaximumSize(49, 43) + self.lblPhoto[len(self.lblPhoto)-1].mousePressEvent = functools.partial(self.lblPhoto_Clicked, index=len(self.lblPhoto)-1) + self.lblPhoto[len(self.lblPhoto)-1].setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) + self.lblPhoto[len(self.lblPhoto)-1].setAlignment(QtCore.Qt.AlignCenter) + self.lblPhoto[len(self.lblPhoto)-1].setStyleSheet("border-top: 1px solid #adadad; border-left: 1px solid #adadad; border-bottom: 1px solid #fefefe; border-right: 1px solid #fefefe;") + rowsPhotos[row].addWidget(self.lblPhoto[len(self.lblPhoto)-1]) + self.grpPhotoThumbsLayout.addLayout(rowsPhotos[row]) + + rowActionsGeneral3 = QtWidgets.QHBoxLayout() + self.btnShowGameFace = QtWidgets.QPushButton("Load &Game Face") + self.btnShowGameFace.setStyleSheet("padding: 5px 10px;") + self.btnShowGameFace.clicked.connect(self.btnShowGameFace_Clicked) + rowActionsGeneral3.addWidget(self.btnShowGameFace) + self.grpPhotoThumbsLayout.addStretch() + self.grpPhotoThumbsLayout.addLayout(rowActionsGeneral3) + + self.grpPhotoThumbsLayout.setAlignment(QtCore.Qt.AlignTop) + self.grpPhotoThumbs.setLayout(self.grpPhotoThumbsLayout) + + self.layout_photos.addWidget(self.grpPhotoThumbs) + self.layout_photos.addWidget(self.grpPhotoView) + + self.layout.addLayout(self.layout_options1, 0, 0) + self.layout.addLayout(self.layout_options2, 1, 0) + self.layout.addLayout(self.layout_photos, 2, 0) + self.layout.addLayout(self.layout_options3, 3, 0) + self.setLayout(self.layout) + + self.APP = app + + palette = self.APP.SETTINGS.value("PocketCameraPalette") + try: + palette = json.loads(palette) + except: + palette = None + if palette is not None: + for i in range(0, len(self.PALETTES)): + if palette == self.PALETTES[i]: + self.rowColors1.itemAt(i).widget().setChecked(True) + + if self.CUR_FILE is not None: + self.OpenFile(self.CUR_FILE) + + export_path = self.APP.SETTINGS.value("LastDirPocketCamera") + if export_path is not None: + self.CUR_EXPORT_PATH = export_path + + self.SetColors() + + self.btnSaveAll.setDefault(True) + self.btnSaveAll.setAutoDefault(True) + self.btnSaveAll.setFocus() + + def run(self): + self.layout.update() + self.layout.activate() + screenGeometry = QtWidgets.QDesktopWidget().screenGeometry() + x = (screenGeometry.width() - self.width()) / 2 + y = (screenGeometry.height() - self.height()) / 2 + self.move(x, y) + self.show() + + def SetColors(self): + if self.CUR_PC is None: return + for i in range(0, self.rowColors1.count()): + if self.rowColors1.itemAt(i).widget().isChecked(): + self.CUR_PC.SetPalette(self.PALETTES[i]) + self.BuildPhotoList() + self.UpdateViewer(self.CUR_INDEX) + + def OpenFile(self, file): + self.CUR_PC = PocketCamera() + if self.CUR_PC.LoadFile(file) == False: + self.CUR_PC = None + QtWidgets.QMessageBox.warning(self, "FlashGBX", "The save data file couldn’t be loaded.", QtWidgets.QMessageBox.Ok) + return False + self.CUR_FILE = file + if self.CUR_EXPORT_PATH == "": + self.CUR_EXPORT_PATH = os.path.dirname(self.CUR_FILE) + self.UpdateViewer(1) + self.SetColors() + + return True + + def lblPhoto_Clicked(self, event, index): + if event.button() == QtCore.Qt.LeftButton: + self.CUR_INDEX = index + 1 + self.UpdateViewer(self.CUR_INDEX) + + def lblPhotoViewer_Clicked(self, event): + if event.button() == QtCore.Qt.LeftButton: + self.CUR_BICUBIC = not self.CUR_BICUBIC + self.UpdateViewer(self.CUR_INDEX) + + def btnOpenSRAM_Clicked(self): + last_dir = self.APP.SETTINGS.value("LastDirSaveDataDMG") + path = QtWidgets.QFileDialog.getOpenFileName(self, "Open GB Camera Save Data File", last_dir, "Save Data File (*.sav);;All Files (*.*)")[0] + if (path == ""): return + if self.OpenFile(path) is True: + self.APP.SETTINGS.setValue("LastDirSaveDataDMG", os.path.dirname(path)) + + def btnShowGameFace_Clicked(self, event): + self.UpdateViewer(0) + self.CUR_INDEX = 0 + + def btnSaveAll_Clicked(self, event): + if self.CUR_PC is None: return + path = self.CUR_EXPORT_PATH + "/IMG_PC.png" + path = QtWidgets.QFileDialog.getSaveFileName(self, "Export all pictures", path, "PNG files (*.png);;BMP files (*.bmp);;GIF files (*.gif);;JPEG files (*.jpg);;All files (*.*)")[0] + if path == "": return + self.CUR_EXPORT_PATH = os.path.dirname(path) + + for i in range(0, 31): + file = os.path.splitext(path)[0] + "{:02d}".format(i) + os.path.splitext(path)[1] + if os.path.exists(file): + answer = QtWidgets.QMessageBox.warning(self, "FlashGBX", "There are already pictures that use the same file names. If you continue, these files will be overwritten.", QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel) + if answer == QtWidgets.QMessageBox.Ok: + break + elif answer == QtWidgets.QMessageBox.Cancel: + return + + for i in range(0, 31): + file = os.path.splitext(path)[0] + "{:02d}".format(i) + os.path.splitext(path)[1] + self.SavePicture(i, path=file) + + def btnSavePhoto_Clicked(self, event): + if self.CUR_PC is None: return + self.SavePicture(self.CUR_INDEX) + + def btnClose_Clicked(self, event): + self.reject() + + def hideEvent(self, event): + for i in range(0, self.rowColors1.count()): + if self.rowColors1.itemAt(i).widget().isChecked(): + self.APP.SETTINGS.setValue("PocketCameraPalette", json.dumps(self.PALETTES[i])) + self.APP.SETTINGS.setValue("LastDirPocketCamera", self.CUR_EXPORT_PATH) + self.APP.activateWindow() + + def BuildPhotoList(self): + cam = self.CUR_PC + self.CUR_THUMBS = [None] * 30 + for i in range(0, 30): + pic = cam.GetPicture(i+1).convert("RGBA") + self.lblPhoto[i].setToolTip("") + if cam.IsEmpty(i+1): + pass + #draw = ImageDraw.Draw(pic, "RGBA") + #draw.line([0, 0, 128, 112], fill=(255, 0, 0), width=8) + #draw.line([0, 112, 128, 0], fill=(255, 0, 0), width=8) + elif cam.IsDeleted(i+1): + draw_bg = Image.new("RGBA", pic.size) + draw = ImageDraw.Draw(draw_bg) + draw.line([0, 0, 128, 112], fill=(255, 0, 0, 192), width=8) + draw.line([0, 112, 128, 0], fill=(255, 0, 0, 192), width=8) + pic.paste(draw_bg, mask=draw_bg) + self.lblPhoto[i].setToolTip("This picture was marked as “deleted” and may be overwritten when you take new pictures.") + self.CUR_THUMBS[i] = ImageQt(pic.resize((47, 41), Image.HAMMING)) + qpixmap = QtGui.QPixmap.fromImage(self.CUR_THUMBS[i]) + self.lblPhoto[i].setPixmap(qpixmap) + + def UpdateViewer(self, index): + resampler = Image.NEAREST + if self.CUR_BICUBIC: resampler = Image.BICUBIC + cam = self.CUR_PC + if cam is None: return + + for i in range(0, 30): + self.lblPhoto[i].setStyleSheet("border-top: 1px solid #adadad; border-left: 1px solid #adadad; border-bottom: 1px solid #ffffff; border-right: 1px solid #ffffff;") + + if index == 0: + self.CUR_PIC = ImageQt(cam.GetPicture(0).convert("RGBA").resize((256, 224), resampler)) + else: + self.CUR_PIC = ImageQt(cam.GetPicture(index).convert("RGBA").resize((256, 224), resampler)) + self.lblPhoto[index - 1].setStyleSheet("border: 3px solid green; padding: 1px;") + + qpixmap = QtGui.QPixmap.fromImage(self.CUR_PIC) + self.lblPhotoViewer.setPixmap(qpixmap) + + def SavePicture(self, index, path=""): + if path == "": + path = self.CUR_EXPORT_PATH + "/IMG_PC{:02d}.png".format(index) + path = QtWidgets.QFileDialog.getSaveFileName(self, "Save Photo", path, "PNG files (*.png);;BMP files (*.bmp);;GIF files (*.gif);;JPEG files (*.jpg);;All files (*.*)")[0] + if path != "": self.CUR_EXPORT_PATH = os.path.dirname(path) + if path == "": return + + cam = self.CUR_PC + cam.ExportPicture(index, path) + + def dragEnterEvent(self, e): + if self._dragEventHover(e): + e.accept() + else: + e.ignore() + + def dragMoveEvent(self, e): + if self._dragEventHover(e): + e.accept() + else: + e.ignore() + + def _dragEventHover(self, e): + if e.mimeData().hasUrls: + for url in e.mimeData().urls(): + if platform.system() == 'Darwin': + # pylint: disable=undefined-variable + fn = str(NSURL.URLWithString_(str(url.toString())).filePathURL().path()) + else: + fn = str(url.toLocalFile()) + + fn_split = os.path.splitext(os.path.abspath(fn)) + if fn_split[1] == ".sav": + return True + return False + + def dropEvent(self, e): + if e.mimeData().hasUrls: + e.setDropAction(QtCore.Qt.CopyAction) + e.accept() + for url in e.mimeData().urls(): + if platform.system() == 'Darwin': + # pylint: disable=undefined-variable + fn = str(NSURL.URLWithString_(str(url.toString())).filePathURL().path()) + else: + fn = str(url.toLocalFile()) + + fn_split = os.path.splitext(os.path.abspath(fn)) + if fn_split[1] == ".sav": + self.OpenFile(fn) + else: + e.ignore() diff --git a/FlashGBX/RomFileAGB.py b/FlashGBX/RomFileAGB.py index fe32dff..df9d914 100644 --- a/FlashGBX/RomFileAGB.py +++ b/FlashGBX/RomFileAGB.py @@ -44,15 +44,18 @@ class RomFileAGB: data["empty"] = (self.ROMFILE == bytearray([buffer[0]] * len(buffer))) data["logo_correct"] = hashlib.sha1(buffer[0x04:0xA0]).digest() == bytearray([ 0x17, 0xDA, 0xA0, 0xFE, 0xC0, 0x2F, 0xC3, 0x3C, 0x0F, 0x6A, 0xBB, 0x54, 0x9A, 0x8B, 0x80, 0xB6, 0x61, 0x3B, 0x48, 0xEE ]) game_title = bytearray(buffer[0xA0:0xAC]).decode("ascii", "replace") - game_title = re.sub(r"(\x00+)$", "", game_title).replace("\x00", "_") + game_title = re.sub(r"(\x00+)$", "", game_title) + game_title = re.sub(r"((_)_+|(\x00)\x00+|(\s)\s+)", "\\2\\3\\4", game_title).replace("\x00", "_") game_title = ''.join(filter(lambda x: x in set(string.printable), game_title)) data["game_title"] = game_title game_code = bytearray(buffer[0xAC:0xB0]).decode("ascii", "replace") - game_code = re.sub(r"(\x00+)$", "", game_code).replace("\x00", "_") + game_code = re.sub(r"(\x00+)$", "", game_code) + game_title = re.sub(r"((_)_+|(\x00)\x00+|(\s)\s+)", "\\2\\3\\4", game_title).replace("\x00", "_") game_code = ''.join(filter(lambda x: x in set(string.printable), game_code)) data["game_code"] = game_code maker_code = bytearray(buffer[0xB0:0xB2]).decode("ascii", "replace") - maker_code = re.sub(r"(\x00+)$", "", maker_code).replace("\x00", "_") + maker_code = re.sub(r"(\x00+)$", "", maker_code) + game_title = re.sub(r"((_)_+|(\x00)\x00+|(\s)\s+)", "\\2\\3\\4", game_title).replace("\x00", "_") maker_code = ''.join(filter(lambda x: x in set(string.printable), maker_code)) data["maker_code"] = maker_code diff --git a/FlashGBX/RomFileDMG.py b/FlashGBX/RomFileDMG.py index 6d45ef5..4fa6e53 100644 --- a/FlashGBX/RomFileDMG.py +++ b/FlashGBX/RomFileDMG.py @@ -1,14 +1,9 @@ # -*- coding: utf-8 -*- # UTF-8 import hashlib, re, sys, string +from . import Util class RomFileDMG: - DMG_Header_Features = { 0x00:'ROM ONLY', 0x01:'MBC1', 0x02:'MBC1+RAM', 0x03:'MBC1+RAM+BATTERY', 0x05:'MBC2', 0x06:'MBC2+BATTERY', 0x08:'ROM+RAM', 0x09:'ROM+RAM+BATTERY', 0x0B:'MMM01', 0x0C:'MMM01+RAM', 0x0D:'MMM01+RAM+BATTERY', 0x0F:'MBC3+TIMER+BATTERY', 0x10:'MBC3+TIMER+RAM+BATTERY', 0x11:'MBC3', 0x12:'MBC3+RAM', 0x13:'MBC3+RAM+BATTERY', 0x15:'MBC4', 0x16:'MBC4+RAM', 0x17:'MBC4+RAM+BATTERY', 0x19:'MBC5', 0x1A:'MBC5+RAM', 0x1B:'MBC5+RAM+BATTERY', 0x1C:'MBC5+RUMBLE', 0x1D:'MBC5+RUMBLE+RAM', 0x1E:'MBC5+RUMBLE+RAM+BATTERY', 0x20:'MBC6', 0x22:'MBC7+SENSOR+RUMBLE+RAM+BATTERY', 0x55:'Game Genie', 0x56:'Game Genie v3.0', 0xFC:'POCKET CAMERA', 0xFD:'BANDAI TAMA5', 0xFE:'HuC3', 0xFF:'HuC1+RAM+BATTERY' } - DMG_Header_ROM_Sizes = { 0x00:0x8000, 0x01:0x10000, 0x02:0x20000, 0x03:0x40000, 0x04:0x80000, 0x05:0x100000, 0x52:0x120000, 0x53:0x140000, 0x54:0x180000, 0x06:0x200000, 0x07:0x400000, 0x08:0x800000 } - DMG_Header_RAM_Sizes = { 0x00:0, 0x01:0x800, 0x02:0x2000, 0x03:0x8000, 0x05:0x10000, 0x04:0x20000 } - DMG_Header_SGB = { 0x00:'No support', 0x03:'Supported' } - DMG_Header_CGB = { 0x00:'No support', 0x80:'Supported', 0xC0:'Required' } - ROMFILE_PATH = None ROMFILE = bytearray() @@ -59,35 +54,71 @@ class RomFileDMG: data = {} data["empty"] = (self.ROMFILE == bytearray([buffer[0]] * len(buffer))) data["logo_correct"] = hashlib.sha1(buffer[0x104:0x134]).digest() == bytearray([ 0x07, 0x45, 0xFD, 0xEF, 0x34, 0x13, 0x2D, 0x1B, 0x3D, 0x48, 0x8C, 0xFB, 0xDF, 0x03, 0x79, 0xA3, 0x9F, 0xD5, 0x4B, 0x4C ]) - game_title = bytearray(buffer[0x134:0x143]).decode("ascii", "replace") - game_title = re.sub(r"(\x00+)$", "", game_title).replace("\x00", "_") + data["cgb"] = int(buffer[0x143]) + data["sgb"] = int(buffer[0x146]) + + if data["cgb"] in (0x00, 0x80, 0xC0): + game_title = bytearray(buffer[0x134:0x143]).decode("ascii", "replace") + else: + game_title = bytearray(buffer[0x134:0x144]).decode("ascii", "replace") + game_title = re.sub(r"(\x00+)$", "", game_title) + game_title = re.sub(r"((_)_+|(\x00)\x00+|(\s)\s+)", "\\2\\3\\4", game_title).replace("\x00", "_") game_title = ''.join(filter(lambda x: x in set(string.printable), game_title)) data["game_title"] = game_title + data["maker_code"] = format(int(buffer[0x14B]), "02X") if data["maker_code"] == '33': maker_code = bytearray(buffer[0x144:0x146]).decode("ascii", "replace") maker_code = ''.join(filter(lambda x: x in set(string.printable), maker_code)) data["maker_code_new"] = maker_code - data["cgb"] = int(buffer[0x143]) - data["sgb"] = int(buffer[0x146]) data["features_raw"] = int(buffer[0x147]) data["features"] = "?" - if buffer[0x147] in self.DMG_Header_Features: data["features"] = self.DMG_Header_Features[buffer[0x147]] data["rom_size_raw"] = int(buffer[0x148]) data["rom_size"] = "?" - if buffer[0x148] in self.DMG_Header_ROM_Sizes: data["rom_size"] = self.DMG_Header_ROM_Sizes[buffer[0x148]] + if buffer[0x148] in Util.DMG_Header_ROM_Sizes: data["rom_size"] = Util.DMG_Header_ROM_Sizes[buffer[0x148]] data["ram_size_raw"] = int(buffer[0x149]) - if data["features"] == 0x05 or data["features"] == 0x06: + if data["features_raw"] == 0x05 or data["features_raw"] == 0x06: data["ram_size"] = 0x200 else: data["ram_size"] = "?" - if buffer[0x149] in self.DMG_Header_RAM_Sizes: data["ram_size"] = self.DMG_Header_RAM_Sizes[buffer[0x149]] + if buffer[0x149] in Util.DMG_Header_RAM_Sizes: + data["ram_size"] = Util.DMG_Header_RAM_Sizes[buffer[0x149]] data["header_checksum"] = int(buffer[0x14D]) data["header_checksum_calc"] = self.CalcChecksumHeader() data["header_checksum_correct"] = data["header_checksum"] == data["header_checksum_calc"] data["rom_checksum"] = int(256 * buffer[0x14E] + buffer[0x14F]) data["rom_checksum_calc"] = self.CalcChecksumGlobal() data["rom_checksum_correct"] = data["rom_checksum"] == data["rom_checksum_calc"] + + # MBC1M + if data["features_raw"] == 0x03 and data["game_title"] == "MOMOCOL" and data["header_checksum"] == 0x28 or \ + data["features_raw"] == 0x01 and data["game_title"] == "BOMCOL" and data["header_checksum"] == 0x86 or \ + data["features_raw"] == 0x01 and data["game_title"] == "GENCOL" and data["header_checksum"] == 0x8A or \ + data["features_raw"] == 0x01 and data["game_title"] == "SUPERCHINESE 123" and data["header_checksum"] == 0xE4 or \ + data["features_raw"] == 0x01 and data["game_title"] == "MORTALKOMBATI&II" and data["header_checksum"] == 0xB9 or \ + data["features_raw"] == 0x01 and data["game_title"] == "MORTALKOMBAT DUO" and data["header_checksum"] == 0xA7: + data["features_raw"] += 0x100 + + # GB Memory + if data["features_raw"] == 0x19 and data["game_title"] == "NP M-MENU MENU" and data["header_checksum"] == 0xD3: + data["features_raw"] = 0x105 + + # M161 (Mani 4 in 1) + elif data["features_raw"] == 0x10 and data["game_title"] == "TETRIS SET" and data["header_checksum"] == 0x3F: + data["features_raw"] = 0x104 + + # MMM01 (Mani 4 in 1) + elif data["features_raw"] == 0x11 and data["game_title"] == "BOUKENJIMA2 SET" and data["header_checksum"] == 0 or \ + data["features_raw"] == 0x11 and data["game_title"] == "BUBBLEBOBBLE SET" and data["header_checksum"] == 0xC6 or \ + data["features_raw"] == 0x11 and data["game_title"] == "GANBARUGA SET" and data["header_checksum"] == 0x90 or \ + data["features_raw"] == 0x11 and data["game_title"] == "RTYPE 2 SET" and data["header_checksum"] == 0x32: + data["features_raw"] = 0x0B + + if data["features_raw"] in Util.DMG_Header_Features: + data["features"] = Util.DMG_Header_Features[data["features_raw"]] + elif data["logo_correct"]: + print("{:s}WARNING: Unknown memory bank controller type 0x{:02X}{:s}".format(Util.ANSI.YELLOW, data["features_raw"], Util.ANSI.RESET)) + return data def GetData(self): diff --git a/FlashGBX/Util.py b/FlashGBX/Util.py index 12c3049..c7943b5 100644 --- a/FlashGBX/Util.py +++ b/FlashGBX/Util.py @@ -1,8 +1,282 @@ # -*- coding: utf-8 -*- # UTF-8 -import math, time, datetime, copy +import math, time, datetime, copy, configparser, threading, statistics, os, platform +from enum import Enum + +# Common constants +APPNAME = "FlashGBX" +VERSION_PEP440 = "1.4" +VERSION = "v{:s}".format(VERSION_PEP440) +DEBUG = False + +AGB_Header_ROM_Sizes = [ "4 MB", "8 MB", "16 MB", "32 MB", "64 MB (GBA Video)" ] +AGB_Header_ROM_Sizes_Map = [ 0x400000, 0x800000, 0x1000000, 0x2000000, 0x4000000 ] +AGB_Header_Save_Types = [ "None", "4K EEPROM (512 Bytes)", "64K EEPROM (8 KB)", "256K SRAM (32 KB)", "512K SRAM (64 KB)", "1M SRAM (128 KB)", "512K FLASH (64 KB)", "1M FLASH (128 KB)" ] +AGB_Global_CRC32 = 0 + +DMG_Header_Features = { 0x00:'None', 0x01:'MBC1', 0x02:'MBC1+SRAM', 0x03:'MBC1+SRAM+BATTERY', 0x06:'MBC2+BATTERY', 0x10:'MBC3+RTC+SRAM+BATTERY', 0x13:'MBC3+SRAM+BATTERY', 0x19:'MBC5', 0x1B:'MBC5+SRAM+BATTERY', 0x1C:'MBC5+RUMBLE', 0x1E:'MBC5+RUMBLE+SRAM+BATTERY', 0x20:'MBC6+FLASH+SRAM+BATTERY', 0x22:'MBC7+ACCELEROMETER+EEPROM', 0x101:'MBC1M', 0x103:'MBC1M+SRAM+BATTERY', 0x0B:'MMM01', 0x0D:'MMM01+SRAM+BATTERY', 0xFC:'CAMERA+SRAM+BATTERY', 0x105:'G-MMC1', 0x104:'M161', 0xFF:'HuC-1+IR+SRAM+BATTERY', 0xFE:'HuC-3+RTC+SRAM+BATTERY', 0xFD:'TAMA5+RTC+EEPROM' } +DMG_Header_ROM_Sizes = [ "32 KB", "64 KB", "128 KB", "256 KB", "512 KB", "1 MB", "2 MB", "4 MB", "8 MB" ] +DMG_Header_ROM_Sizes_Map = [ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08 ] +DMG_Header_ROM_Sizes_Flasher_Map = [ 2, 4, 8, 16, 32, 64, 128, 256, 512 ] # Number of ROM banks +DMG_Header_RAM_Sizes = [ "None", "4K SRAM (512 Bytes)", "16K SRAM (2 KB)", "64K SRAM (8 KB)", "256K SRAM (32 KB)", "512K SRAM (64 KB)", "1M SRAM (128 KB)", "2K MBC7 EEPROM (256 Bytes)", "4K MBC7 EEPROM (512 Bytes)", "TAMA5 EEPROM (32 Bytes)" ] +DMG_Header_RAM_Sizes_Map = [ 0x00, 0x01, 0x01, 0x02, 0x03, 0x05, 0x04, 0x101, 0x102, 0x103 ] +DMG_Header_RAM_Sizes_Flasher_Map = [ 0, 0x200, 0x800, 0x2000, 0x8000, 0x10000, 0x20000, 0x100, 0x200, 0x20 ] # RAM size in bytes +DMG_Header_SGB = { 0x00:'No support', 0x03:'Supported' } +DMG_Header_CGB = { 0x00:'No support', 0x80:'Supported', 0xC0:'Required' } + +class ANSI: + BOLD = '\033[1m' + RED = '\033[91m' + GREEN = '\033[92m' + YELLOW = '\033[33m' + RESET = '\033[0m' + CLEAR_LINE = '\033[2K' + +class IniSettings(): + FILENAME = "" + SETTINGS = None + def __init__(self, ini_file): + try: + if not os.path.isdir(os.path.dirname(ini_file)): + os.makedirs(os.path.dirname(ini_file)) + if os.path.exists(ini_file): + with open(ini_file, "a+") as f: f.close() + else: + with open(ini_file, "w+") as f: f.close() + except: + print("Error accessing the configuration directory or settings file.") + return + + self.FILENAME = ini_file + self.SETTINGS = configparser.ConfigParser() + self.SETTINGS.optionxform = str + self.Reload() + + def Reload(self): + if self.SETTINGS is None: return + with open(self.FILENAME, "r", encoding="utf-8") as f: + self.SETTINGS.read_file(f) + if len(self.SETTINGS.sections()) == 0: + self.SETTINGS.add_section("General") + + def value(self, key, default=None): return self.GetValue(key, default) + def GetValue(self, key, default=None): + if self.SETTINGS is None: return None + self.Reload() + if key not in self.SETTINGS["General"]: + if default is not None: self.SetValue(key, default) + return default + return (self.SETTINGS["General"][key]) + + def setValue(self, key, value): self.SetValue(key, value) + def SetValue(self, key, value): + if self.SETTINGS is None: return None + self.Reload() + self.SETTINGS["General"][key] = value + dprint("Updating settings:", key, "=", value) + with open(self.FILENAME, "w", encoding="utf-8") as f: + self.SETTINGS.write(f) + + def clear(self): self.Clear() + def Clear(self): + if self.SETTINGS is None: return None + self.SETTINGS.clear() + with open(self.FILENAME, "w", encoding="utf-8") as f: + self.SETTINGS.write(f) + +class Progress(): + MUTEX = threading.Lock() + PROGRESS = {} + UPDATER = None + + def __init__(self, updater): + self.UPDATER = updater + pass + + def SetProgress(self, args): + self.MUTEX.acquire(1) + try: + if not "method" in self.PROGRESS: self.PROGRESS = {} + now = time.time() + if args["action"] == "INITIALIZE": + self.PROGRESS["action"] = args["action"] + self.PROGRESS["method"] = args["method"] + self.PROGRESS["size"] = args["size"] + if "pos" in args: + self.PROGRESS["pos"] = args["pos"] + else: + self.PROGRESS["pos"] = 0 + if "time_start" in args: + self.PROGRESS["time_start"] = args["time_start"] + else: + self.PROGRESS["time_start"] = now + self.PROGRESS["time_last_emit"] = now + self.PROGRESS["time_last_update_speed"] = now + self.PROGRESS["time_left"] = 0 + self.PROGRESS["speed"] = 0 + self.PROGRESS["speeds"] = [] + self.PROGRESS["bytes_last_update_speed"] = 0 + self.UPDATER(self.PROGRESS) + + if args["action"] == "ABORT": + self.UPDATER(args) + self.PROGRESS = {} + + elif args["action"] in ("ERASE", "SECTOR_ERASE"): + if "time_start" in self.PROGRESS: + args["time_elapsed"] = now - self.PROGRESS["time_start"] + elif "time_start" in args: + args["time_elapsed"] = now - args["time_start"] + args["pos"] = 1 + args["size"] = 0 + self.UPDATER(args) + + elif self.PROGRESS == {}: + return + + elif args["action"] == "UPDATE_POS": + self.PROGRESS["pos"] = args["pos"] + self.PROGRESS["action"] = "PROGRESS" + if "time_start" in self.PROGRESS: + self.PROGRESS["time_elapsed"] = now - self.PROGRESS["time_start"] + + try: + total_speed = statistics.mean(self.PROGRESS["speeds"]) + self.PROGRESS["time_left"] = (self.PROGRESS["size"] - self.PROGRESS["pos"]) / 1024 / total_speed + except: + pass + if "abortable" in args: self.PROGRESS["abortable"] = args["abortable"] + self.UPDATER(self.PROGRESS) + + elif args["action"] in ("READ", "WRITE"): + if "method" not in self.PROGRESS: return + elif args["action"] in ("READ") and self.PROGRESS["method"] in ("SAVE_WRITE", "ROM_WRITE"): return + elif args["action"] in ("WRITE") and self.PROGRESS["method"] in ("SAVE_READ", "ROM_READ", "ROM_WRITE_VERIFY"): return + if self.PROGRESS["pos"] >= self.PROGRESS["size"]: return + + self.PROGRESS["action"] = "PROGRESS" + self.PROGRESS["pos"] += args["bytes_added"] + if (now - self.PROGRESS["time_last_emit"]) > 0.05: + self.PROGRESS["time_elapsed"] = now - self.PROGRESS["time_start"] + if (now - self.PROGRESS["time_last_update_speed"]) > 0.25: + time_delta = now - self.PROGRESS["time_last_update_speed"] + pos_delta = self.PROGRESS["pos"] - self.PROGRESS["bytes_last_update_speed"] + if time_delta > 0: + speed = (pos_delta / time_delta) / 1024 + self.PROGRESS["speeds"].append(speed) + if len(self.PROGRESS["speeds"]) > 256: self.PROGRESS["speeds"].pop(0) + self.PROGRESS["speed"] = statistics.median(self.PROGRESS["speeds"]) + self.PROGRESS["time_last_update_speed"] = now + self.PROGRESS["bytes_last_update_speed"] = self.PROGRESS["pos"] + + if "skipping" in args and args["skipping"] is True: + self.PROGRESS["speed"] = 0 + self.PROGRESS["skipping"] = True + else: + self.PROGRESS["skipping"] = False + + if self.PROGRESS["speed"] > 0: + total_speed = statistics.mean(self.PROGRESS["speeds"]) + self.PROGRESS["time_left"] = (self.PROGRESS["size"] - self.PROGRESS["pos"]) / 1024 / total_speed + + self.UPDATER(self.PROGRESS) + self.PROGRESS["time_last_emit"] = now + + elif args["action"] == "FINISHED": + self.PROGRESS["pos"] = self.PROGRESS["size"] + self.UPDATER(self.PROGRESS) + self.PROGRESS["action"] = args["action"] + self.PROGRESS["bytes_last_update_speed"] = self.PROGRESS["size"] + self.PROGRESS["time_elapsed"] = now - self.PROGRESS["time_start"] + self.PROGRESS["time_last_emit"] = now + self.PROGRESS["time_last_update_speed"] = now + self.PROGRESS["time_left"] = 0 + self.PROGRESS["speed"] = (self.PROGRESS["size"] / self.PROGRESS["time_elapsed"]) / 1024 + self.PROGRESS["bytes_last_emit"] = self.PROGRESS["size"] + if "verified" in args: self.PROGRESS["verified"] = (args["verified"] == True) + + if self.PROGRESS["speed"] > self.PROGRESS["size"] / 1024: + self.PROGRESS["speed"] = self.PROGRESS["size"] / 1024 + + self.UPDATER(self.PROGRESS) + del(self.PROGRESS["method"]) + + finally: + self.MUTEX.release() + +class TAMA5_CMD(Enum): + RAM_WRITE = 0x0 + RAM_READ = 0x1 + RTC = 0x4 + +class TAMA5_REG(Enum): + ROM_BANK_L = 0x0 + ROM_BANK_H = 0x1 + MEM_WRITE_L = 0x4 + MEM_WRITE_H = 0x5 + ADDR_H_SET_MODE = 0x6 + ADDR_L = 0x7 + ENABLE = 0xA + MEM_READ_L = 0xC + MEM_READ_H = 0xD + +def formatFileSize(size, asInt=False): + #size = size / 1024 + if size == 1: + return "{:d} Byte".format(size) + elif size < 1024: + return "{:d} Bytes".format(size) + elif size < 1024 * 1024: + if asInt: + return "{:d} KB".format(int(size/1024)) + else: + return "{:.1f} KB".format(size/1024) + else: + if asInt: + return "{:d} MB".format(int(size/1024/1024)) + else: + return "{:.2f} MB".format(size/1024/1024) + +def formatProgressTimeShort(sec): + sec = sec % (24 * 3600) + hr = sec // 3600 + sec %= 3600 + min = sec // 60 + sec %= 60 + return "{:02d}:{:02d}:{:02d}".format(int(hr), int(min), int(sec)) + +def formatProgressTime(sec): + if int(sec) == 1: + return "{:d} second".format(int(sec)) + elif sec < 60: + return "{:d} seconds".format(int(sec)) + elif int(sec) == 60: + return "1 minute" + else: + min = int(sec / 60) + sec = int(sec % 60) + s = str(min) + " " + if min == 1: + s = s + "minute" + else: + s = s + "minutes" + s = s + ", " + str(sec) + " " + if sec == 1: + s = s + "second" + else: + s = s + "seconds" + return s + +def formatPathOS(path, end_sep=False): + if platform.system() == "Windows": + path = path.replace("/", "\\") + if end_sep: + path += "\\" + else: + if end_sep: + path += "/" + return path -# Utility functions def bitswap(n, s): p, q = s if (((n & (1 << p)) >> p) ^ ((n & (1 << q)) >> q)) == 1: @@ -29,6 +303,9 @@ def ParseCFI(buffer): pass return False + pri_address = (buffer[0x2A] | (buffer[0x2C] << 8)) * 2 + if (pri_address + 0x3C) >= 0x400: pri_address = 0x80 + info["vdd_min"] = (buffer[0x36] >> 4) + ((buffer[0x36] & 0x0F) / 10) info["vdd_max"] = (buffer[0x38] >> 4) + ((buffer[0x38] & 0x0F) / 10) @@ -61,9 +338,11 @@ def ParseCFI(buffer): info["chip_erase"] = False info["tb_boot_sector"] = False - if "{:s}{:s}{:s}".format(chr(buffer[0x80]), chr(buffer[0x82]), chr(buffer[0x84])) == "PRI": - if buffer[0x9E] != 0 and buffer[0x9E] != 0xFF: - temp = { 0x02: 'Bottom Boot Device', 0x03: 'Top Boot Device' } + info["tb_boot_sector_raw"] = 0 + if "{:s}{:s}{:s}".format(chr(buffer[pri_address]), chr(buffer[pri_address+2]), chr(buffer[pri_address+4])) == "PRI": + if buffer[pri_address + 0x1E] not in (0, 0xFF): + temp = { 0x02: 'As shown', 0x03: 'Reversed' } + info["tb_boot_sector_raw"] = buffer[0x9E] try: info["tb_boot_sector"] = "{:s} (0x{:02X})".format(temp[buffer[0x9E]], buffer[0x9E]) except: @@ -102,6 +381,5 @@ def ParseCFI(buffer): return info def dprint(*args, **kwargs): - # uncomment for some debug prints - #print(datetime.datetime.now().strftime("[%Y-%m-%d %H:%M:%S]"), " ".join(map(str, args)), **kwargs) - pass + if DEBUG: + print("{:s}{:s} {:s}".format(ANSI.CLEAR_LINE, datetime.datetime.now().strftime("[%Y-%m-%d %H:%M:%S]"), " ".join(map(str, args)), **kwargs)) diff --git a/FlashGBX/config/db_AGB.json b/FlashGBX/config/db_AGB.json index 385be90..c71a336 100644 --- a/FlashGBX/config/db_AGB.json +++ b/FlashGBX/config/db_AGB.json @@ -2400,27 +2400,6 @@ "st": 1, "gc": "BOMJ" }, - "c2f45b5bafe2382326ae33938f1bc5058a1a27cc": { - "rs": 8388608, - "rc": 1701638702, - "ss": 8192, - "st": 2, - "gc": "AMHJ" - }, - "4b068b46f218f23ac7fce7c64e748759ae2a66e5": { - "rs": 8388608, - "rc": 688752656, - "ss": 8192, - "st": 2, - "gc": "AMYJ" - }, - "23ec7be5f5bdde5d7238e14599aa44cb0190e223": { - "rs": 4194304, - "rc": 4159963529, - "ss": 32768, - "st": 3, - "gc": "ABSJ" - }, "5d67242b52ea9e154cbc799b8f7eb369ab572472": { "rs": 16777216, "rc": 167529811, @@ -2435,6 +2414,20 @@ "st": 2, "gc": "AMHE" }, + "c2f45b5bafe2382326ae33938f1bc5058a1a27cc": { + "rs": 8388608, + "rc": 1701638702, + "ss": 8192, + "st": 2, + "gc": "AMHJ" + }, + "4b068b46f218f23ac7fce7c64e748759ae2a66e5": { + "rs": 8388608, + "rc": 688752656, + "ss": 8192, + "st": 2, + "gc": "AMYJ" + }, "9317af9f4c86808a9d2fcfb3bcfaadeb8a4acb79": { "rs": 16777216, "rc": 1741874616, @@ -2449,6 +2442,13 @@ "st": 2, "gc": "AMYE" }, + "23ec7be5f5bdde5d7238e14599aa44cb0190e223": { + "rs": 4194304, + "rc": 4159963529, + "ss": 32768, + "st": 3, + "gc": "ABSJ" + }, "96a75e80641212f9ee3aea2c500bc3b1ebff945e": { "rs": 4194304, "rc": 604144358, @@ -3415,90 +3415,6 @@ "st": 1, "gc": "BPSJ" }, - "4917828a050424930b20c0f760ac716494b9dbdd": { - "rs": 4194304, - "rc": 3446141626, - "ss": 512, - "st": 1, - "gc": "FBME" - }, - "58086ff435641c40d3d72060916b8884e105cf91": { - "rs": 4194304, - "rc": 3485594968, - "ss": 512, - "st": 1, - "gc": "FADE" - }, - "e34dfd5715d058484ab10da49bf0bf31c13af68a": { - "rs": 4194304, - "rc": 3129136150, - "ss": 512, - "st": 1, - "gc": "FDKE" - }, - "8c97edbfb3bc8637d131cc1bd156cd7247a92698": { - "rs": 4194304, - "rc": 3399441711, - "ss": 512, - "st": 1, - "gc": "FDME" - }, - "3364725609fc5ac9e6fab3c3b931630f3393467c": { - "rs": 4194304, - "rc": 2489067823, - "ss": 8192, - "st": 2, - "gc": "FEBE" - }, - "4b637f396eba0daebc25bb3de25610309bf27de3": { - "rs": 4194304, - "rc": 824628754, - "ss": 512, - "st": 1, - "gc": "FICE" - }, - "e42088bc18d29dafb8c874a7d87fb2c1b983f292": { - "rs": 4194304, - "rc": 2996917187, - "ss": 512, - "st": 1, - "gc": "FMRE" - }, - "e3a53b82b787f03ae7d3d75c9f9aa012d30b2c7e": { - "rs": 4194304, - "rc": 589267158, - "ss": 512, - "st": 1, - "gc": "FP7E" - }, - "d42612677ba881015f36f88f56ba6ac415d38a32": { - "rs": 4194304, - "rc": 2128478125, - "ss": 512, - "st": 1, - "gc": "FSME" - }, - "3bb590068613ef71bc99c3192169b8e62043f97b": { - "rs": 4194304, - "rc": 1756477352, - "ss": 8192, - "st": 2, - "gc": "FZLE" - }, - "4cf49737115eabf5d64c64c86f757f010dbec429": { - "rs": 4194304, - "rc": 2660382486, - "ss": 512, - "st": 1, - "gc": "FXVE" - }, - "9721d13f880f9e90b4077ebb81555832be66e360": { - "rs": 4194304, - "rc": 4074519305, - "ss": 8192, - "st": 2, - "gc": "FLBE" - }, "bb959dd1d8a89bbdba7de95729555220e0749a5a": { "rs": 4194304, "rc": 3669476354, @@ -5872,34 +5788,6 @@ "st": 0, "gc": "BF2E" }, - "b429a05c22bfd5d17e979e74a301671245d9ef61": { - "rs": 4194304, - "rc": 1111199685, - "ss": 8192, - "st": 2, - "gc": "FSRJ" - }, - "baedf5b3d763721ba8f2b368bc8bc4ed91283d9b": { - "rs": 4194304, - "rc": 4209311212, - "ss": 512, - "st": 1, - "gc": "FGZJ" - }, - "ad2592a0770975efb800afe7b98e7a3a77db1042": { - "rs": 4194304, - "rc": 1481148492, - "ss": 512, - "st": 1, - "gc": "FSMJ" - }, - "ae557926846dd36c28bebe5bcaa358342ee59bf8": { - "rs": 4194304, - "rc": 2261993135, - "ss": 512, - "st": 1, - "gc": "FDKJ" - }, "4668c771a5a7a2a63906e5b66a60a4bf0c6b67bf": { "rs": 4194304, "rc": 1447492049, @@ -9437,7 +9325,7 @@ }, "14fd9d51892d9f8a2f16a675ddc495d35f490d64": { "rs": 8388608, - "rc": 1085398272, + "rc": 3357251329, "ss": 32768, "st": 3, "gc": "AK5J" @@ -12613,13 +12501,6 @@ "st": 2, "gc": "ARNJ" }, - "5a7b3e826d91514fb9dbc119876db1ad48cf0e87": { - "rs": 4194304, - "rc": 726728774, - "ss": 512, - "st": 1, - "gc": "FADP" - }, "2f1ec7d3d3eedfea092d277bebc57a0659d0b3ca": { "rs": 4194304, "rc": 1385654586, @@ -17471,13 +17352,6 @@ "st": 1, "gc": "BMVJ" }, - "034c3eaa513db1efc98ad1b29e92d28e30c883fa": { - "rs": 4194304, - "rc": 140191515, - "ss": 0, - "st": 0, - "gc": "FSMJ" - }, "5942690b24639606bc87a8397cf673f7d6aa99be": { "rs": 8388608, "rc": 3613237405, diff --git a/FlashGBX/config/fc_AGB_128W30B.txt b/FlashGBX/config/fc_AGB_128W30B.txt index 5257290..240218f 100644 --- a/FlashGBX/config/fc_AGB_128W30B.txt +++ b/FlashGBX/config/fc_AGB_128W30B.txt @@ -1,7 +1,7 @@ { "type":"AGB", "names":[ - "GE28F128W30 with 128W30B0" + "GE28F128W30 with 128W30B" ], "flash_ids":[ [ 0x8A, 0x00, 0x57, 0x88 ] diff --git a/FlashGBX/config/fc_AGB_29LV128DT.txt b/FlashGBX/config/fc_AGB_29LV128DT.txt index 94da201..f9ee264 100644 --- a/FlashGBX/config/fc_AGB_29LV128DT.txt +++ b/FlashGBX/config/fc_AGB_29LV128DT.txt @@ -1,17 +1,17 @@ { "type":"AGB", "names":[ - "AGB-E08-09 with 29LV128DTMC-90Q" + "AGB-E08-09 with 29LV128DTMC-90Q", + "AGB-E05-01 with MX29GL128FHT2I-90G" ], "flash_ids":[ + [ 0xC1, 0x00, 0x7D, 0x22 ], [ 0xC1, 0x00, 0x7D, 0x22 ] ], "voltage":3.3, "flash_size":0x1000000, - "sector_size":[ - [0x10000, 255], - [0x02000, 8] - ], + "sector_size_from_cfi":true, + "chip_erase_timeout":120, "commands":{ "reset":[ [ 0, 0xF0 ] @@ -37,6 +37,22 @@ [ null, null, null ], [ "SA", 0xFFFF, 0xFFFF ] ], + "chip_erase":[ + [ 0xAAA, 0xA9 ], + [ 0x555, 0x56 ], + [ 0xAAA, 0x80 ], + [ 0xAAA, 0xA9 ], + [ 0x555, 0x56 ], + [ 0xAAA, 0x10 ] + ], + "chip_erase_wait_for":[ + [ null, null, null ], + [ null, null, null ], + [ null, null, null ], + [ null, null, null ], + [ null, null, null ], + [ 0, 0xFFFF, 0xFFFF ] + ], "single_write":[ [ 0xAAA, 0xA9 ], [ 0x555, 0x56 ], diff --git a/FlashGBX/config/fc_AGB_M36L0R8060T.txt b/FlashGBX/config/fc_AGB_M36L0R8060T.txt index 5d342f4..31da663 100644 --- a/FlashGBX/config/fc_AGB_M36L0R8060T.txt +++ b/FlashGBX/config/fc_AGB_M36L0R8060T.txt @@ -2,11 +2,13 @@ "type":"AGB", "names":[ "4050_4400_4000_4350_36L0R_V5 with M36L0R8060T", - "36L0R8-39VF512 with M36L0R8060T" + "36L0R8-39VF512 with M36L0R8060T", + "4050_4400_4000_4350_36L0R_V5 with 4050L0YTQ2" ], "flash_ids":[ [ 0x20, 0x00, 0x0E, 0x88 ], - [ 0x20, 0x00, 0x0E, 0x88 ] + [ 0x20, 0x00, 0x0E, 0x88 ], + [ 0x8A, 0x00, 0x0E, 0x88 ] ], "voltage":3.3, "flash_size":0x2000000, diff --git a/FlashGBX/config/fc_AGB_MSP55LV128M.txt b/FlashGBX/config/fc_AGB_MSP55LV128M.txt index d1c8bf4..54dab27 100644 --- a/FlashGBX/config/fc_AGB_MSP55LV128M.txt +++ b/FlashGBX/config/fc_AGB_MSP55LV128M.txt @@ -6,7 +6,8 @@ "BX2006_0106_NEW with S29GL128N10TFI01", "BX2006_TSOP_64BALL with GL128S", "BX2006_TSOPBGA_0106 with M29W640GB6AZA6", - "AGB-E05-02 with M29W128GH" + "AGB-E05-02 with M29W128GH", + "AGB-E05-02 with M29W128FH" ], "flash_ids":[ [ 0x02, 0x00, 0x7D, 0x22 ], @@ -14,11 +15,13 @@ [ 0x02, 0x00, 0x7D, 0x22 ], [ 0x02, 0x00, 0x7D, 0x22 ], [ 0x20, 0x00, 0x7D, 0x22 ], + [ 0x20, 0x00, 0x7D, 0x22 ], [ 0x20, 0x00, 0x7D, 0x22 ] ], "voltage":3.3, "flash_size":0x1000000, - "sector_size":0x20000, + "sector_size_from_cfi":true, + "chip_erase_timeout":120, "commands":{ "reset":[ [ 0, 0xF0 ] @@ -28,6 +31,9 @@ [ 0x555, 0x56 ], [ 0xAAA, 0x90 ] ], + "read_cfi":[ + [ 0xAA, 0x98 ] + ], "sector_erase":[ [ 0xAAA, 0xA9 ], [ 0x555, 0x56 ], @@ -44,6 +50,22 @@ [ null, null, null ], [ "SA", 0xFFFF, 0xFFFF ] ], + "chip_erase":[ + [ 0xAAA, 0xA9 ], + [ 0x555, 0x56 ], + [ 0xAAA, 0x80 ], + [ 0xAAA, 0xA9 ], + [ 0x555, 0x56 ], + [ 0xAAA, 0x10 ] + ], + "chip_erase_wait_for":[ + [ null, null, null ], + [ null, null, null ], + [ null, null, null ], + [ null, null, null ], + [ null, null, null ], + [ 0, 0xFFFF, 0xFFFF ] + ], "buffer_write":[ [ 0xAAA, 0xA9 ], [ 0x555, 0x56 ], diff --git a/FlashGBX/config/fc_DMG_29DL161TD-90.txt b/FlashGBX/config/fc_DMG_29DL161TD-90.txt index 264ea0e..199e8b9 100644 --- a/FlashGBX/config/fc_DMG_29DL161TD-90.txt +++ b/FlashGBX/config/fc_DMG_29DL161TD-90.txt @@ -11,10 +11,7 @@ "start_addr":0, "first_bank":1, "write_pin":"WR", - "sector_size":[ - [0x10000, 31], - [0x2000, 8] - ], + "sector_size_from_cfi":true, "chip_erase_timeout":120, "commands":{ "reset":[ diff --git a/FlashGBX/config/fc_DMG_29DL32TF-70.txt b/FlashGBX/config/fc_DMG_29DL32TF-70.txt index 23f444c..045004f 100644 --- a/FlashGBX/config/fc_DMG_29DL32TF-70.txt +++ b/FlashGBX/config/fc_DMG_29DL32TF-70.txt @@ -11,11 +11,7 @@ "start_addr":0, "first_bank":1, "write_pin":"WR", - "sector_size":[ - [0x2000, 8], - [0x10000, 31] - ], - "sector_reversal":true, + "sector_size_from_cfi":true, "chip_erase_timeout":50, "commands":{ "reset":[ diff --git a/FlashGBX/config/fc_DMG_GB_Memory.txt b/FlashGBX/config/fc_DMG_GB_Memory.txt new file mode 100644 index 0000000..6f1c6c3 --- /dev/null +++ b/FlashGBX/config/fc_DMG_GB_Memory.txt @@ -0,0 +1,97 @@ +{ + "type":"DMG", + "names":[ + "NP GB Memory Cartridge (DMG-MMSA-JPN)" + ], + "flash_ids":[ + [ 0xC2, 0x89, 0xC2, 0xFF ] + ], + "voltage":5, + "flash_size":0x100000, + "start_addr":0, + "first_bank":1, + "write_pin":"WR", + "unlock_before_rom_dump":true, + "hidden_sector_size":128, + "commands":{ + "reset":[ + [ 0x120, 0x02 ], + + [ 0x120, 0x0F ], + [ 0x125, 0x40 ], + [ 0x126, 0x80 ], + [ 0x127, 0xF0 ], + [ 0x13F, 0xA5 ], + + [ 0x120, 0x04 ], + [ 0x13F, 0xA5 ], + [ 0x120, 0x08 ], + [ 0x13F, 0xA5 ] + ], + "unlock":[ + [ 0x120, 0x09, 1 ], + [ 0x121, 0xAA, 1 ], + [ 0x122, 0x55, 1 ], + [ 0x13F, 0xA5, 1 ], + + [ 0x120, 0x11, 1 ], + [ 0x13F, 0xA5, 1 ] + ], + "read_identifier":[ + [ 0x120, 0x0F ], + [ 0x125, 0x55 ], + [ 0x126, 0x55 ], + [ 0x127, 0xAA ], + [ 0x13F, 0xA5 ], + + [ 0x120, 0x0F ], + [ 0x125, 0x2A ], + [ 0x126, 0xAA ], + [ 0x127, 0x55 ], + [ 0x13F, 0xA5 ], + + [ 0x120, 0x0F ], + [ 0x125, 0x55 ], + [ 0x126, 0x55 ], + [ 0x127, 0x90 ], + [ 0x13F, 0xA5 ] + ], + "read_hidden_sector":[ + [ 0x120, 0x0F ], + [ 0x125, 0x55 ], + [ 0x126, 0x55 ], + [ 0x127, 0xAA ], + [ 0x13F, 0xA5 ], + + [ 0x120, 0x0F ], + [ 0x125, 0x2A ], + [ 0x126, 0xAA ], + [ 0x127, 0x55 ], + [ 0x13F, 0xA5 ], + + [ 0x120, 0x0F ], + [ 0x125, 0x55 ], + [ 0x126, 0x55 ], + [ 0x127, 0x77 ], + [ 0x13F, 0xA5 ], + + [ 0x120, 0x0F ], + [ 0x125, 0x55 ], + [ 0x126, 0x55 ], + [ 0x127, 0xAA ], + [ 0x13F, 0xA5 ], + + [ 0x120, 0x0F ], + [ 0x125, 0x2A ], + [ 0x126, 0xAA ], + [ 0x127, 0x55 ], + [ 0x13F, 0xA5 ], + + [ 0x120, 0x0F ], + [ 0x125, 0x55 ], + [ 0x126, 0x55 ], + [ 0x127, 0x77 ], + [ 0x13F, 0xA5 ] + ] + } +} diff --git a/FlashGBX/config/fc_DMG_K8D3216UTC.txt b/FlashGBX/config/fc_DMG_K8D3216UTC.txt new file mode 100644 index 0000000..6054391 --- /dev/null +++ b/FlashGBX/config/fc_DMG_K8D3216UTC.txt @@ -0,0 +1,73 @@ +{ + "type":"DMG", + "names":[ + "SD007_TSOP_48BALL with K8D3216UTC" + ], + "flash_ids":[ + [ 0xEC, 0xEC, 0xA0, 0xA0 ] + ], + "voltage":3.3, + "flash_size":0x400000, + "start_addr":0, + "first_bank":1, + "write_pin":"WR", + "sector_size":[ + [0x10000, 63], + [0x02000, 8] + ], + "chip_erase_timeout":70, + "commands":{ + "reset":[ + [ 0, 0xF0 ] + ], + "read_identifier":[ + [ 0xAAA, 0xA9 ], + [ 0x555, 0x56 ], + [ 0xAAA, 0x90 ] + ], + "chip_erase":[ + [ 0xAAA, 0xA9 ], + [ 0x555, 0x56 ], + [ 0xAAA, 0x80 ], + [ 0xAAA, 0xA9 ], + [ 0x555, 0x56 ], + [ 0xAAA, 0x10 ] + ], + "chip_erase_wait_for":[ + [ null, null, null ], + [ null, null, null ], + [ null, null, null ], + [ null, null, null ], + [ null, null, null ], + [ 0, 0xFF, 0xFF ] + ], + "sector_erase":[ + [ 0xAAA, 0xA9 ], + [ 0x555, 0x56 ], + [ 0xAAA, 0x80 ], + [ 0xAAA, 0xA9 ], + [ 0x555, 0x56 ], + [ "SA", 0x30 ] + ], + "sector_erase_wait_for":[ + [ null, null, null ], + [ null, null, null ], + [ null, null, null ], + [ null, null, null ], + [ null, null, null ], + [ "SA", 0xFF, 0xFF ] + ], + "single_write":[ + [ 0xAAA, 0xA9 ], + [ 0x555, 0x56 ], + [ 0xAAA, 0xA0 ], + [ "PA", "PD" ] + ], + "single_write_wait_for":[ + [ null, null, null ], + [ null, null, null ], + [ null, null, null ], + [ null, null, null ] + ] + } +} diff --git a/FlashGBX/config/fc_DMG_MX29LV160CT.txt b/FlashGBX/config/fc_DMG_MX29LV160CT.txt new file mode 100644 index 0000000..83fc588 --- /dev/null +++ b/FlashGBX/config/fc_DMG_MX29LV160CT.txt @@ -0,0 +1,75 @@ +{ + "type":"DMG", + "names":[ + "SD007_K8D3216_32M with MX29LV160CT" + ], + "flash_ids":[ + [ 0xC1, 0xC1, 0xC4, 0xC4 ] + ], + "voltage":3.3, + "flash_size":0x200000, + "start_addr":0, + "first_bank":1, + "write_pin":"WR", + "sector_size":[ + [0x10000, 31], + [0x8000, 1], + [0x2000, 2], + [0x4000, 1] + ], + "chip_erase_timeout":60, + "commands":{ + "reset":[ + [ 0, 0xF0 ] + ], + "read_identifier":[ + [ 0xAAA, 0xA9 ], + [ 0x555, 0x56 ], + [ 0xAAA, 0x90 ] + ], + "chip_erase":[ + [ 0xAAA, 0xA9 ], + [ 0x555, 0x56 ], + [ 0xAAA, 0x80 ], + [ 0xAAA, 0xA9 ], + [ 0x555, 0x56 ], + [ 0xAAA, 0x10 ] + ], + "chip_erase_wait_for":[ + [ null, null, null ], + [ null, null, null ], + [ null, null, null ], + [ null, null, null ], + [ null, null, null ], + [ 0, 0xFF, 0xFF ] + ], + "sector_erase":[ + [ 0xAAA, 0xA9 ], + [ 0x555, 0x56 ], + [ 0xAAA, 0x80 ], + [ 0xAAA, 0xA9 ], + [ 0x555, 0x56 ], + [ "SA", 0x30 ] + ], + "sector_erase_wait_for":[ + [ null, null, null ], + [ null, null, null ], + [ null, null, null ], + [ null, null, null ], + [ null, null, null ], + [ "SA", 0xFF, 0xFF ] + ], + "single_write":[ + [ 0xAAA, 0xA9 ], + [ 0x555, 0x56 ], + [ 0xAAA, 0xA0 ], + [ "PA", "PD" ] + ], + "single_write_wait_for":[ + [ null, null, null ], + [ null, null, null ], + [ null, null, null ], + [ null, null, null ] + ] + } +} diff --git a/FlashGBX/config/fc_DMG_MX29LV320ABTC.txt b/FlashGBX/config/fc_DMG_MX29LV320ET.txt similarity index 64% rename from FlashGBX/config/fc_DMG_MX29LV320ABTC.txt rename to FlashGBX/config/fc_DMG_MX29LV320ET.txt index cddd06a..0102e80 100644 --- a/FlashGBX/config/fc_DMG_MX29LV320ABTC.txt +++ b/FlashGBX/config/fc_DMG_MX29LV320ET.txt @@ -1,55 +1,73 @@ -{ - "type":"DMG", - "names":[ - "GB-M968 with MX29LV320ABTC", - "SD007_BV5_DRV with M29W320DT" - ], - "flash_ids":[ - [ 0xC2, 0xC2, 0xA8, 0xA8 ], - [ 0x20, 0x20, 0xCA, 0xCA ] - ], - "voltage":3.3, - "flash_size":0x400000, - "start_addr":0, - "first_bank":1, - "write_pin":"WR", - "chip_erase_timeout":60, - "commands":{ - "reset":[ - [ 0, 0xF0 ] - ], - "read_identifier":[ - [ 0xAAA, 0xAA ], - [ 0x555, 0x55 ], - [ 0xAAA, 0x90 ] - ], - "chip_erase":[ - [ 0xAAA, 0xAA ], - [ 0x555, 0x55 ], - [ 0xAAA, 0x80 ], - [ 0xAAA, 0xAA ], - [ 0x555, 0x55 ], - [ 0xAAA, 0x10 ] - ], - "chip_erase_wait_for":[ - [ null, null, null ], - [ null, null, null ], - [ null, null, null ], - [ null, null, null ], - [ null, null, null ], - [ 0, 0xFF, 0xFF ] - ], - "single_write":[ - [ 0xAAA, 0xAA ], - [ 0x555, 0x55 ], - [ 0xAAA, 0xA0 ], - [ "PA", "PD" ] - ], - "single_write_wait_for":[ - [ null, null, null ], - [ null, null, null ], - [ null, null, null ], - [ null, null, null ] - ] - } -} +{ + "type":"DMG", + "names":[ + "DMG-DHCN-20 with MX29LV320ET" + ], + "flash_ids":[ + [ 0xC2, 0xC2, 0xA7, 0xA7 ] + ], + "voltage":3.3, + "flash_size":0x200000, + "start_addr":0, + "first_bank":1, + "write_pin":"WR", + "sector_size":[ + [0x10000, 31], + [0x2000, 8] + ], + "chip_erase_timeout":60, + "commands":{ + "reset":[ + [ 0, 0xF0 ] + ], + "read_identifier":[ + [ 0xAAA, 0xAA ], + [ 0x555, 0x55 ], + [ 0xAAA, 0x90 ] + ], + "chip_erase":[ + [ 0xAAA, 0xAA ], + [ 0x555, 0x55 ], + [ 0xAAA, 0x80 ], + [ 0xAAA, 0xAA ], + [ 0x555, 0x55 ], + [ 0xAAA, 0x10 ] + ], + "chip_erase_wait_for":[ + [ null, null, null ], + [ null, null, null ], + [ null, null, null ], + [ null, null, null ], + [ null, null, null ], + [ 0, 0xFF, 0xFF ] + ], + "sector_erase":[ + [ 0xAAA, 0xAA ], + [ 0x555, 0x55 ], + [ 0xAAA, 0x80 ], + [ 0xAAA, 0xAA ], + [ 0x555, 0x55 ], + [ "SA", 0x30 ] + ], + "sector_erase_wait_for":[ + [ null, null, null ], + [ null, null, null ], + [ null, null, null ], + [ null, null, null ], + [ null, null, null ], + [ "SA", 0xFF, 0xFF ] + ], + "single_write":[ + [ 0xAAA, 0xAA ], + [ 0x555, 0x55 ], + [ 0xAAA, 0xA0 ], + [ "PA", "PD" ] + ], + "single_write_wait_for":[ + [ null, null, null ], + [ null, null, null ], + [ null, null, null ], + [ null, null, null ] + ] + } +} diff --git a/FlashGBX/config/fc_DMG_iG_4MB.txt b/FlashGBX/config/fc_DMG_iG_4MB.txt index 8172745..ffef713 100644 --- a/FlashGBX/config/fc_DMG_iG_4MB.txt +++ b/FlashGBX/config/fc_DMG_iG_4MB.txt @@ -1,16 +1,23 @@ { "type":"DMG", "names":[ - "insideGadgets 4 MB, 128 KB SRAM/FRAM" + "insideGadgets 4 MB (S29GL032M)", + "SD007_BV5_DRV with S29GL032M90TFIR4", + "GB-M968 with MX29LV320ABTC", + "SD007_BV5_DRV with M29W320DT" ], "flash_ids":[ - [ 0x01, 0x01, 0x7E, 0x7E ] + [ 0x01, 0x01, 0x7E, 0x7E ], + [ 0x01, 0x01, 0x7E, 0x7E ], + [ 0xC2, 0xC2, 0xA8, 0xA8 ], + [ 0x20, 0x20, 0xCA, 0xCA ] ], - "voltage":5, + "voltage":3.3, "flash_size":0x400000, "start_addr":0x4000, "first_bank":0, "write_pin":"WR", + "sector_size_from_cfi":true, "chip_erase_timeout":60, "commands":{ "reset":[ @@ -37,6 +44,22 @@ [ null, null, null ], [ 0, 0xFF, 0xFF ] ], + "sector_erase":[ + [ 0xAAA, 0xAA ], + [ 0x555, 0x55 ], + [ 0xAAA, 0x80 ], + [ 0xAAA, 0xAA ], + [ 0x555, 0x55 ], + [ "SA", 0x30 ] + ], + "sector_erase_wait_for":[ + [ null, null, null ], + [ null, null, null ], + [ null, null, null ], + [ null, null, null ], + [ null, null, null ], + [ 0, 0xFF, 0xFF ] + ], "single_write":[ [ 0xAAA, 0xAA ], [ 0x555, 0x55 ], diff --git a/FlashGBX/hw_GBxCartRW.py b/FlashGBX/hw_GBxCartRW.py index 3db482c..445f54c 100644 --- a/FlashGBX/hw_GBxCartRW.py +++ b/FlashGBX/hw_GBxCartRW.py @@ -1,17 +1,17 @@ # -*- coding: utf-8 -*- # UTF-8 -import serial, os, serial.tools.list_ports +import time, math, struct, traceback, zlib, copy, hashlib, os +import serial, serial.tools.list_ports from serial import SerialException -import sys, time, re, glob, math, struct, json, traceback, zlib, copy -from .DataTransfer import * -from .RomFileDMG import * -from .RomFileAGB import * -from .Util import * +from .RomFileDMG import RomFileDMG +from .RomFileAGB import RomFileAGB +from .Util import ANSI, dprint, bitswap, ParseCFI +from . import Util class GbxDevice: DEVICE_NAME = "GBxCart RW" DEVICE_MIN_FW = 19 - DEVICE_MAX_FW = 25 + DEVICE_MAX_FW = 26 DEVICE_CMD = { "CART_MODE":'C', @@ -23,6 +23,8 @@ class GbxDevice: "READ_ROM_4000H":'Q', "WRITE_RAM":'W', "SET_BANK":'B', + "SET_BANK_WITH_CS":'H', + "RESET_MBC":'-', "GB_CART_MODE":'G', # GBA defines/commands "EEPROM_NONE":0, @@ -39,6 +41,8 @@ class GbxDevice: "GBA_READ_ROM":'r', "GBA_READ_ROM_256BYTE":'j', "GBA_READ_ROM_8000H":'Z', + "GBA_READ_3DMEMORY":'}', + "GBA_READ_3DMEMORY_1000H":']', "GBA_READ_SRAM":'m', "GBA_WRITE_SRAM":'w', "GBA_WRITE_ONE_BYTE_SRAM":'o', @@ -112,6 +116,7 @@ class GbxDevice: CANCEL_ARGS = {} SIGNAL = None POS = 0 + NO_PROG_UPDATE = False FAST_READ = False def __init__(self): @@ -134,12 +139,9 @@ class GbxDevice: try: dev = serial.Serial(ports[i], 1000000, timeout=1) self.DEVICE = dev - try: - self.LoadFirmwareVersion() - except: - self.DEVICE = None + self.LoadFirmwareVersion() - if self.DEVICE is None or not self.IsConnected() or self.FW[0] == b'': + if self.DEVICE is None or not self.IsConnected() or self.FW == [] or self.FW[0] == b'': dev.close() self.DEVICE = None conn_msg.append([3, "Couldn’t communicate with the GBxCart RW device on port " + ports[i] + ". Please disconnect and reconnect the device, then try again."]) @@ -197,7 +199,7 @@ class GbxDevice: self.DEVICE.reset_output_buffer() self.LoadFirmwareVersion() return True - except SerialException as e: + except SerialException: return False def Close(self): @@ -225,33 +227,48 @@ class GbxDevice: return self.PORT def LoadFirmwareVersion(self): - self.write(self.DEVICE_CMD["READ_FIRMWARE_VERSION"]) - fw = bytearray(self.read(1))[0] - self.write(self.DEVICE_CMD["READ_PCB_VERSION"]) - pcb = bytearray(self.read(1))[0] - self.FW = [fw, pcb] + try: + self.DEVICE.reset_input_buffer() + self.DEVICE.reset_output_buffer() + self.write("0") + self.write(self.DEVICE_CMD["READ_FIRMWARE_VERSION"]) + fw = bytearray(self.read(1))[0] + self.write(self.DEVICE_CMD["READ_PCB_VERSION"]) + pcb = bytearray(self.read(1))[0] + self.FW = [fw, pcb] + except: + pass def CanSetVoltageManually(self): - fw, pcb = self.FW + _, pcb = self.FW if not pcb in (4, 100): return True else: return False def CanSetVoltageAutomatically(self): - fw, pcb = self.FW + _, pcb = self.FW if pcb in (1, 2, 100): return False else: return True def GetSupprtedModes(self): - fw, pcb = self.FW + _, pcb = self.FW if pcb == 100: return ["DMG"] else: return ["DMG", "AGB"] + def IsSupportedMbc(self, mbc): + return mbc in ( 0x00, 0x01, 0x02, 0x03, 0x06, 0x0B, 0x0D, 0x10, 0x13, 0x19, 0x1B, 0x1C, 0x1E, 0xFC, 0xFD, 0xFE, 0xFF, 0x101, 0x103, 0x105 ) + + def IsSupported3dMemory(self): + return False #int(self.FW[0]) >= 27 + + def IsClkConnected(self): + return False + def GetMode(self): return self.MODE @@ -264,100 +281,71 @@ class GbxDevice: def SetProgress(self, args): if self.CANCEL and args["action"] != "ABORT": return if args["action"] == "UPDATE_POS": self.POS = args["pos"] - if self.SIGNAL: self.SIGNAL.emit(args) + try: + self.SIGNAL.emit(args) + except AttributeError: + if self.SIGNAL is not None: + self.SIGNAL(args) if args["action"] == "FINISHED": self.SIGNAL = None def wait_for_ack(self): buffer = self.read(1) + if buffer == False: + stack = traceback.extract_stack() + stack = stack[len(stack)-2] # caller only + print("{:s}Waiting for confirmation from the device has timed out. (Called from {:s}(), line {:d}){:s}\n".format(ANSI.RED, stack.name, stack.lineno, ANSI.RESET)) + self.CANCEL = True + self.CANCEL_ARGS = {"info_type":"msgbox_critical", "info_msg":"A timeout error occured while waiting for confirmation from the device. Please make sure that the cartridge contacts are clean, re-connect the device and try again from the beginning."} + return False + lives = 2 while buffer != b'1': - print("wait_for_ack(): No valid acknowledgement received (", buffer, "). ", end="") + print("{:s}Waiting for confirmation from the device (buffer={:s})... {:s}".format(ANSI.YELLOW, str(buffer), ANSI.RESET)) lives -= 1 if lives < 1: - print("Failed.") - traceback.print_stack() - #print("\nwait_for_ack(): Skipping...") + stack = traceback.extract_stack() + stack = stack[len(stack)-2] # caller only + print("{:s}Waiting for confirmation from the device has failed. (Called from {:s}(), line {:d}){:s}\n".format(ANSI.RED, stack.name, stack.lineno, ANSI.RESET)) self.CANCEL = True - self.CANCEL_ARGS = {"info_type":"msgbox_critical", "info_msg":"A critical error occured while waiting for confirmation from the device. Please re-connect the device and try again from the beginning."} + self.CANCEL_ARGS = {"info_type":"msgbox_critical", "info_msg":"A critical error occured while waiting for confirmation from the device. Please make sure that the cartridge contacts are clean, re-connect the device and try again from the beginning."} return False else: - print("Retrying...") time.sleep(0.05) buffer = self.read(1) + return True - def read(self, length=64, last=False): + def read(self, length=64, last=False, ask_next_bytes=True, max_bytes=64): readlen = length - if readlen > 64: readlen = 64 - lives = 5 - while True: - if length <= 64: - buffer = self.DEVICE.read(readlen) - if len(buffer) != readlen: - dprint("read(): Received {:d} byte(s) instead of the expected {:d} bytes.".format(len(buffer), readlen)) - self.write('0') + if readlen > 64: readlen = max_bytes + mbuffer = bytearray() + dprint("read(length={:d}, last={:s}, ask_next_bytes={:s}, max_bytes={:d})".format(length, str(last), str(ask_next_bytes), max_bytes)) + for i in range(0, length, readlen): + if self.DEVICE.in_waiting > 1000: dprint("Recv buffer used: {:d} bytes".format(self.DEVICE.in_waiting)) + buffer = self.DEVICE.read(readlen) + if len(buffer) != readlen: + dprint("read(): Received {:d} byte(s) instead of the expected {:d} bytes during iteration {:d}.".format(len(buffer), readlen, i)) + self.write('0') # end + time.sleep(0.5) + while self.DEVICE.in_waiting > 0: + self.DEVICE.reset_input_buffer() time.sleep(0.5) - while self.DEVICE.in_waiting > 0: - self.DEVICE.reset_input_buffer() - time.sleep(0.5) - self.DEVICE.reset_output_buffer() - return False - + self.DEVICE.reset_output_buffer() + return False + + mbuffer += buffer + if not self.NO_PROG_UPDATE: self.SetProgress({"action":"READ", "bytes_added":len(buffer)}) - - if length == 1: - return buffer - else: - if last: - self.write('0') - if not last: - self.write('1') - return buffer[:length] - elif self.FAST_READ and (length == 0x10000 or length == 0x4000): - mbuffer = bytearray() - for i in range(0, length, readlen): - if self.DEVICE.in_waiting > 1000: dprint("Recv buffer used: {:d} bytes".format(self.DEVICE.in_waiting)) - buffer = self.DEVICE.read(readlen) - if len(buffer) != readlen: - dprint("read(): Received {:d} byte(s) instead of the expected {:d} bytes during iteration {:d}.".format(len(buffer), readlen, i)) - self.write('0') - time.sleep(1) - while self.DEVICE.in_waiting > 0: - self.DEVICE.reset_input_buffer() - time.sleep(1) - self.DEVICE.reset_output_buffer() - return False - - mbuffer += buffer - self.SetProgress({"action":"READ", "bytes_added":len(buffer)}) - - self.write('0') - return mbuffer - - else: # length > 64 - mbuffer = bytearray() - for i in range(0, length, 64): - buffer = self.DEVICE.read(readlen) - if len(buffer) != readlen: - dprint("read(): Received {:d} byte(s) instead of the expected {:d} bytes during iteration {:d}.".format(len(buffer), readlen, i)) - self.write('0') - time.sleep(1) - while self.DEVICE.in_waiting > 0: - self.DEVICE.reset_input_buffer() - time.sleep(1) - self.DEVICE.reset_output_buffer() - return False - - mbuffer += buffer - self.SetProgress({"action":"READ", "bytes_added":len(buffer)}) - if not (i + 64 >= length): - self.write('1') - - if last: - self.write('0') - - return mbuffer[:length] + if ask_next_bytes and not (i + readlen >= length): + self.write('1') # ask for next bytes (continue message) + if (i + readlen) > length: + readlen = length - i + + if (ask_next_bytes and last) or not ask_next_bytes: + self.write('0') + + return mbuffer[:length] def write(self, data, wait_for_ack=False): if not isinstance(data, bytearray): @@ -367,13 +355,87 @@ class GbxDevice: if wait_for_ack: return self.wait_for_ack() - def ReadROM(self, offset, length, set_address=True): + def ReadRAM_TAMA5(self, rtc=False): + buffer = bytearray() + self.NO_PROG_UPDATE = True + + # Read save state + for i in range(0, 0x20): + self.cart_write(0xA001, Util.TAMA5_REG.ADDR_H_SET_MODE.value, cs=True) # register select and address (high) + self.cart_write(0xA000, i >> 4 | Util.TAMA5_CMD.RAM_READ.value << 1, cs=True) # bit 0 = higher ram address, rest = command + self.cart_write(0xA001, Util.TAMA5_REG.ADDR_L.value, cs=True) # address (low) + self.cart_write(0xA000, i & 0x0F, cs=True) # bits 0-3 = lower ram address + self.cart_write(0xA001, Util.TAMA5_REG.MEM_READ_H.value, cs=True) # data out (high) + data_h = self.ReadROM(0xA000, 64)[0] + self.cart_write(0xA001, Util.TAMA5_REG.MEM_READ_L.value, cs=True) # data out (low) + data_l = self.ReadROM(0xA000, 64)[0] + data = ((data_h & 0xF) << 4) | (data_l & 0xF) + buffer.append(data) + self.SetProgress({"action":"UPDATE_POS", "abortable":False, "pos":i+1}) + + # Read RTC state + if rtc: + for r in range(0, 0x10): + self.cart_write(0xA001, Util.TAMA5_REG.MEM_WRITE_L.value, cs=True) # set address + self.cart_write(0xA000, r, cs=True) # address + self.cart_write(0xA001, Util.TAMA5_REG.ADDR_H_SET_MODE.value, cs=True) # register select + self.cart_write(0xA000, Util.TAMA5_CMD.RTC.value << 1, cs=True) # rtc mode + self.cart_write(0xA001, Util.TAMA5_REG.ADDR_L.value, cs=True) # set access mode + self.cart_write(0xA000, 1, cs=True) # 1 = read + self.cart_write(0xA001, Util.TAMA5_REG.MEM_READ_L.value, cs=True) # data out + data = self.ReadROM(0xA000, 64)[0] + buffer.append(data) + self.SetProgress({"action":"UPDATE_POS", "abortable":False, "pos":0x21+r}) + + # Add timestamp of backup time (a future version may offer to auto-advance or edit the values) + ts = int(time.time()) + buffer.extend(struct.pack("
> 4, cs=True) + self.cart_write(0xA001, Util.TAMA5_REG.MEM_WRITE_L.value, cs=True) # data in (low) + self.cart_write(0xA000, data[i] & 0xF, cs=True) + self.cart_write(0xA001, Util.TAMA5_REG.ADDR_H_SET_MODE.value, cs=True) # register select and address (high) + self.cart_write(0xA000, i >> 4 | Util.TAMA5_CMD.RAM_WRITE.value << 1, cs=True) # bit 0 = higher ram address, rest = command + self.cart_write(0xA001, Util.TAMA5_REG.ADDR_L.value, cs=True) # address (low) + self.cart_write(0xA000, i & 0x0F, cs=True) # bits 0-3 = lower ram address + self.SetProgress({"action":"UPDATE_POS", "abortable":False, "pos":i+1}) + + if rtc and bytearray(data[0x20:0x30]) != bytearray([0xFF] * 0x10): + for r in range(0, 0x10): + self.cart_write(0xA001, Util.TAMA5_REG.MEM_WRITE_L.value, cs=True) # set address + self.cart_write(0xA000, r, cs=True) # address + self.cart_write(0xA001, Util.TAMA5_REG.MEM_WRITE_H.value, cs=True) # set value to write + self.cart_write(0xA000, data[0x20+r], cs=True) # value + self.cart_write(0xA001, Util.TAMA5_REG.ADDR_H_SET_MODE.value, cs=True) # register select + self.cart_write(0xA000, Util.TAMA5_CMD.RTC.value << 1, cs=True) # rtc mode + self.cart_write(0xA001, Util.TAMA5_REG.ADDR_L.value, cs=True) # set access mode + self.cart_write(0xA000, 0, cs=True) # 0 = write + self.SetProgress({"action":"UPDATE_POS", "abortable":False, "pos":0x21+r}) + + self.SetProgress({"action":"UPDATE_POS", "pos":0x30}) + self.NO_PROG_UPDATE = False + + def ReadROM(self, offset, length, set_address=True, agb_3dmemory=False): + reqlen = length + if length < 64: length = 64 buffer = False lives = 5 dprint("ReadROM(offset=0x{:X}, length=0x{:X}, set_address={:s}) fast_read_mode={:s}".format(offset, length, str(set_address), str(self.FAST_READ))) while buffer == False: + ask_next_bytes = True + max_bytes = 64 if self.MODE == "DMG": if self.FAST_READ and length == 0x4000: + ask_next_bytes = False + max_bytes = 0x80 if set_address: self.set_number(offset, self.DEVICE_CMD["SET_START_ADDRESS"]) self.set_mode(self.DEVICE_CMD["READ_ROM_4000H"]) @@ -381,9 +443,21 @@ class GbxDevice: if set_address: self.set_number(offset, self.DEVICE_CMD["SET_START_ADDRESS"]) self.set_mode(self.DEVICE_CMD["READ_ROM_RAM"]) - buffer = self.read(length, last=True) + buffer = self.read(length, last=True, ask_next_bytes=ask_next_bytes) elif self.MODE == "AGB": - if self.FAST_READ and length == 0x10000: + if agb_3dmemory and self.FAST_READ and length == 0x1000: + ask_next_bytes = False + max_bytes = 0x80 + if set_address: + self.set_number(math.floor(offset / 2), self.DEVICE_CMD["SET_START_ADDRESS"]) + self.set_mode(self.DEVICE_CMD["GBA_READ_3DMEMORY_1000H"]) + elif agb_3dmemory and length == 0x200: + if set_address: + self.set_number(math.floor(offset / 2), self.DEVICE_CMD["SET_START_ADDRESS"]) + self.set_mode(self.DEVICE_CMD["GBA_READ_3DMEMORY"]) + elif self.FAST_READ and length == 0x10000: + ask_next_bytes = False + max_bytes = 0x80 if set_address: self.set_number(math.floor(offset / 2), self.DEVICE_CMD["SET_START_ADDRESS"]) self.set_mode(self.DEVICE_CMD["GBA_READ_ROM_8000H"]) @@ -391,37 +465,36 @@ class GbxDevice: if set_address: self.set_number(math.floor(offset / 2), self.DEVICE_CMD["SET_START_ADDRESS"]) self.set_mode(self.DEVICE_CMD["GBA_READ_ROM"]) - buffer = self.read(length, last=True) + buffer = self.read(length, last=True, ask_next_bytes=ask_next_bytes, max_bytes=max_bytes) - ''' # simulate bad driver - import random - if random.randint(0, 20) == 5: - print("#") - buffer = False - ''' + #import random + #if random.randint(0, 20) == 5: + # print("{:s}😈 Bad driver attack!{:s}".format(ANSI.RED, ANSI.RESET)) + # buffer = False if buffer == False: if lives == 0: self.CANCEL = True - if (self.MODE == "DMG" and length == 0x4000) or (self.MODE == "AGB" and length == 0x10000): + if self.FAST_READ: self.CANCEL_ARGS = {"info_type":"msgbox_critical", "info_msg":"An error occured while receiving data from the device. Please disable Fast Read Mode, re-connect the device and try again."} else: self.CANCEL_ARGS = {"info_type":"msgbox_critical", "info_msg":"An error occured while receiving data from the device. Please re-connect the device and try again."} - print(" Giving up.", flush=True) + print("{:s}{:s}Couldn’t recover from the read error.{:s}".format(ANSI.CLEAR_LINE, ANSI.RED, ANSI.RESET), flush=True) return False elif lives != 5: print("", flush=True) - print("Failed to receive data at 0x{:X}. Retrying...".format(self.POS), end="", flush=True) + print("{:s}{:s}Failed to receive 0x{:X} bytes at 0x{:X}. Retrying...{:s}".format(ANSI.CLEAR_LINE, ANSI.YELLOW, length, self.POS, ANSI.RESET), flush=True) set_address = True lives -= 1 if lives < 5: - print(" OK!", flush=True) + print("{:s}└ The retry was successful!".format(ANSI.CLEAR_LINE), flush=True) - return buffer + return bytearray(buffer[:reqlen]) def gbx_flash_write_address_byte(self, address, data): + dprint("gbx_flash_write_address_byte(address=0x{:X}, data=0x{:X})".format(address, data)) if self.MODE == "DMG": return self.gb_flash_write_address_byte(address, data) elif self.MODE == "AGB": @@ -459,14 +532,20 @@ class GbxDevice: buffer = bytearray(command, "ascii") + bytearray(data) self.write(buffer) - def set_bank(self, address, bank): + def cart_write(self, address, bank, cs=False): + dprint("cart_write(address={:s}, data={:s})".format(format(address, 'x'), format(bank, 'x'))) + # Firmware check R26+ + if cs and self.FW[0] >= 26: + cmd = self.DEVICE_CMD["SET_BANK_WITH_CS"] + else: + cmd = self.DEVICE_CMD["SET_BANK"] address = format(address, 'x') - buffer = self.DEVICE_CMD["SET_BANK"] + address + '\x00' + buffer = cmd + address + '\x00' self.write(buffer) time.sleep(0.005) bank = format(bank, 'd') - buffer = self.DEVICE_CMD["SET_BANK"] + bank + '\x00' + buffer = cmd + bank + '\x00' self.write(buffer) time.sleep(0.005) @@ -474,9 +553,6 @@ class GbxDevice: dprint("set_mode(command={:s})".format(str(command))) buffer = format(command, 's') self.write(buffer) - if command in (self.DEVICE_CMD["VOLTAGE_3_3V"], self.DEVICE_CMD["VOLTAGE_5V"]): - #time.sleep(0.005) - pass def set_number(self, number, command): buffer = format(command, 's') + format(int(number), 'x') + '\x00' @@ -485,40 +561,70 @@ class GbxDevice: def EnableRAM(self, mbc=1, enable=True): if enable: - if mbc <= 4: self.set_bank(0x6000, 1) - self.set_bank(0x0000, 0x0A) + if mbc in (0x01, 0x02, 0x03, 0x101, 0x103, 0x10, 0x13): # MBC1, MBC1M, MBC3 + self.cart_write(0x6000, 1) + self.cart_write(0x0000, 0x0A, cs=(mbc == 0xFD)) else: - self.set_bank(0x0000, 0x00) - if mbc <= 4: self.set_bank(0x6000, 0) + if mbc == 0xFF: # HuC-1 + self.cart_write(0x0000, 0x0E) # enabling IR disables RAM + else: + self.cart_write(0x0000, 0x00, cs=(mbc == 0xFD)) + if mbc <= 4: self.cart_write(0x6000, 0) time.sleep(0.2) def SetBankROM(self, bank, mbc=0, bank_count=0): - dprint("SetBankROM(bank={:d}, mbc={:d}, bank_count={:d})".format(bank, mbc, bank_count)) - if mbc == 0 and bank_count == 0: mbc = 5 - if mbc == 1: # MBC1 - dprint("0x6000=0x00, 0x4000=0x{:X}, 0x2000=0x{:X}".format(bank >> 5, bank & 0x1F)) - self.set_bank(0x6000, 0) - self.set_bank(0x4000, bank >> 5) - self.set_bank(0x2000, bank & 0x1F) - elif mbc == 1.1: # Hudson MBC1 - self.set_bank(0x4000, bank >> 4) + dprint("SetBankROM(bank={:d}, mbc={:X}, bank_count={:d})".format(bank, int(mbc), bank_count)) + if mbc == 0 and bank_count == 0: mbc = 0x19 # MBC5 + if mbc in (0x01, 0x02, 0x03): # MBC1 + dprint("└[MBC1] 0x6000=0x00, 0x4000=0x{:X}, 0x2000=0x{:X}".format(bank >> 5, bank & 0x1F)) + self.cart_write(0x6000, 0) + self.cart_write(0x4000, bank >> 5) + self.cart_write(0x2000, bank & 0x1F) + elif mbc in (0x101, 0x103): # MBC1M + self.cart_write(0x4000, bank >> 4) if (bank < 10): - dprint("0x4000=0x{:X}, 0x2000=0x{:X}".format(bank >> 4, bank & 0x1F)) - self.set_bank(0x2000, bank & 0x1F) + dprint("└[MBC1M] 0x4000=0x{:X}, 0x2000=0x{:X}".format(bank >> 4, bank & 0x1F)) + self.cart_write(0x2000, bank & 0x1F) else: - dprint("0x4000=0x{:X}, 0x2000=0x{:X}".format(bank >> 4, 0x10 | (bank & 0x1F))) - self.set_bank(0x2000, 0x10 | (bank & 0x1F)) - elif (mbc == 0 and bank_count > 256) or mbc == 5: # MBC5 - dprint("0x2100=0x{:X}, 0x3000=0x{:X}".format(bank & 0xFF, ((bank >> 8) & 0xFF))) - self.set_bank(0x2100, (bank & 0xFF)) - self.set_bank(0x3000, ((bank >> 8) & 0xFF)) + dprint("└[MBC1M] 0x4000=0x{:X}, 0x2000=0x{:X}".format(bank >> 4, 0x10 | (bank & 0x1F))) + self.cart_write(0x2000, 0x10 | (bank & 0x1F)) + elif (mbc == 0 and bank_count > 256) or mbc == 0x19: # MBC5 + dprint("└[MBC5] 0x2100=0x{:X}".format(bank & 0xFF)) + self.cart_write(0x2100, (bank & 0xFF)) + if bank == 0 or bank >= 256: + dprint("└[MBC5] 0x3000=0x{:X}".format((bank >> 8) & 0xFF)) + self.cart_write(0x3000, ((bank >> 8) & 0xFF)) + elif mbc in (0x0B, 0x0D): # MMM01 + if bank % 0x20 == 0: + dprint("└[MMM01] RESET_MBC, 0x2000=0x{:X}".format(bank)) + self.set_mode(self.DEVICE_CMD['RESET_MBC']) + self.wait_for_ack() + self.cart_write(0x2000, bank) # start from this ROM bank + self.cart_write(0x6000, 0x00) # 0x00 = 512 KB, 0x04 = 32 KB, 0x08 = 64 KB, 0x10 = 128 KB, 0x20 = 256 KB + self.cart_write(0x4000, 0x40) # RAM bank? + self.cart_write(0x0000, 0x00) + self.cart_write(0x0000, 0x40) # Enable mapping + dprint("└[MMM01] 0x2100=0x{:X}".format(((bank % 0x20) & 0xFF))) + self.cart_write(0x2100, ((bank % 0x20) & 0xFF)) + elif mbc == 0xFD: # TAMA5 + dprint("└[TAMA5] 0xA001=0x00, 0xA000=0x{:X}, 0xA001=0x01, 0xA000=0x{:X}".format(bank & 0x0F, bank >> 4)) + self.cart_write(0xA001, Util.TAMA5_REG.ROM_BANK_L.value, cs=True) # ROM bank (low) + self.cart_write(0xA000, bank & 0x0F, cs=True) + self.cart_write(0xA001, Util.TAMA5_REG.ROM_BANK_H.value, cs=True) # ROM bank (high) + self.cart_write(0xA000, (bank >> 4) & 0x0F, cs=True) + elif mbc == 0xFF: # HuC-1 + dprint("└[HuC-1] 0x2000=0x{:X}".format(bank & 0xFF)) + self.cart_write(0x2000, (bank & 0x3F)) else: # MBC2, MBC3 and others - dprint("0x2100=0x{:X}".format(bank & 0xFF)) - self.set_bank(0x2100, (bank & 0xFF)) - - def SetBankRAM(self, bank): - self.set_bank(0x4000, (bank & 0xFF)) + dprint("└[MBCx] 0x2100=0x{:X}".format(bank & 0xFF)) + self.cart_write(0x2100, (bank & 0xFF)) + def SetBankRAM(self, bank, mbc=0x19): + if mbc in (0x06, 0xFD): return # MBC2 or TAMA5 + dprint("SetBankRAM(bank={:d}, mbc={:d})".format(bank, mbc)) + dprint("└[MBC] 0x4000=0x{:X}".format(bank & 0xFF)) + self.cart_write(0x4000, (bank & 0xFF)) + def ReadFlashSaveMakerID(self): makers = { 0x1F:"ATMEL", 0xBF:"SST/SANYO", 0xC2:"MACRONIX", 0x32:"PANASONIC", 0x62:"SANYO" } self.set_mode(self.DEVICE_CMD["GBA_FLASH_READ_ID"]) @@ -567,20 +673,20 @@ class GbxDevice: self.set_mode(self.DEVICE_CMD["GB_FLASH_WE_PIN"]) self.set_mode(self.DEVICE_CMD["WE_AS_AUDIO_PIN"]) + # Reset Flash + if "reset" in flashcart_meta["commands"]: + for i in range(0, len(flashcart_meta["commands"]["reset"])): + self.gbx_flash_write_address_byte(flashcart_meta["commands"]["reset"][i][0], flashcart_meta["commands"]["reset"][i][1]) + # Unlock Flash if "unlock" in flashcart_meta["commands"]: for i in range(0, len(flashcart_meta["commands"]["unlock"])): addr = flashcart_meta["commands"]["unlock"][i][0] data = flashcart_meta["commands"]["unlock"][i][1] count = flashcart_meta["commands"]["unlock"][i][2] - for j in range(0, count): + for _ in range(0, count): self.gbx_flash_write_address_byte(addr, data) - # Reset Flash - if "reset" in flashcart_meta["commands"]: - for i in range(0, len(flashcart_meta["commands"]["reset"])): - self.gbx_flash_write_address_byte(flashcart_meta["commands"]["reset"][i][0], flashcart_meta["commands"]["reset"][i][1]) - # Read Flash ID / Electronic Signature if "read_identifier" in flashcart_meta["commands"]: for i in range(0, len(flashcart_meta["commands"]["read_identifier"])): @@ -621,7 +727,7 @@ class GbxDevice: addr = flashcart_meta["commands"]["unlock"][i][0] data_ = flashcart_meta["commands"]["unlock"][i][1] count = flashcart_meta["commands"]["unlock"][i][2] - for j in range(0, count): + for _ in range(0, count): self.gbx_flash_write_address_byte(addr, data_) # Reset Flash @@ -687,7 +793,7 @@ class GbxDevice: we_pins = [ "WR", "AUDIO" ] else: rom_string = "[ ROM ] " + rom_string - we_pins = [ False ] + we_pins = [ None ] for we in we_pins: if "method" in cfi: break @@ -715,7 +821,15 @@ class GbxDevice: buffer[i] = bitswap(buffer[i], d_swap) cfi_parsed = ParseCFI(buffer) - dprint(cfi_parsed) + try: + if d_swap is not None: + dprint("CFI @ {:s}/{:X}/{:X}/{:s}".format(str(we), method['read_identifier'][0][0], bitswap(method['read_identifier'][0][1], d_swap), str(d_swap))) + else: + dprint("CFI @ {:s}/{:X}/{:X}/{:s}".format(str(we), method['read_identifier'][0][0], method['read_identifier'][0][1], str(d_swap))) + dprint("└", cfi_parsed) + except: + pass + if cfi_parsed != False: cfi = cfi_parsed cfi["raw"] = buffer @@ -727,7 +841,7 @@ class GbxDevice: cfi["method_id"] = flash_commands.index(method) if d_swap is not None: - for k, v in method.items(): + for k in method.keys(): for c in range(0, len(method[k])): if isinstance(method[k][c][1], int): method[k][c][1] = bitswap(method[k][c][1], d_swap) @@ -767,7 +881,7 @@ class GbxDevice: s += "Buffered write: {:s}\n".format(str(cfi["buffer_write"])) if cfi["chip_erase"]: s += "Chip erase: {:d}–{:d} ms\n".format(cfi["chip_erase_time_avg"], cfi["chip_erase_time_max"]) if cfi["sector_erase"]: s += "Sector erase: {:d}–{:d} ms\n".format(cfi["sector_erase_time_avg"], cfi["sector_erase_time_max"]) - if cfi["tb_boot_sector"] is not False: s += "Top/Bottom flags: {:s}\n".format(str(cfi["tb_boot_sector"])) + if cfi["tb_boot_sector"] is not False: s += "Sector order: {:s}\n".format(str(cfi["tb_boot_sector"])) pos = 0 oversize = False s = s[:-1] @@ -820,6 +934,7 @@ class GbxDevice: return (flash_id, cfi_info, cfi) def CheckROMStable(self): + if not self.IsConnected(): raise Exception("Couldn’t access the the device.") self.ReadROM(0, 64) buffer = self.ReadROM(0, 0x180) time.sleep(0.1) @@ -827,18 +942,21 @@ class GbxDevice: return False return True - def ReadInfo(self): + def ReadInfo(self, setPinsAsInputs=False): if not self.IsConnected(): raise Exception("Couldn’t access the the device.") data = {} self.POS = 0 if self.MODE == "DMG": self.set_mode(self.DEVICE_CMD["VOLTAGE_5V"]) + time.sleep(0.1) + self.set_mode(self.DEVICE_CMD['RESET_MBC']) + self.wait_for_ack() header = self.ReadROM(0, 0x180) if self.MODE == "DMG": data = RomFileDMG(header).GetHeader() - + elif self.MODE == "AGB": data = RomFileAGB(header).GetHeader() size_check = header[0xA0:0xA0+16] @@ -853,78 +971,152 @@ class GbxDevice: self.INFO["flash_type"] = 0 self.INFO["last_action"] = 0 - self.set_mode(self.DEVICE_CMD["SET_PINS_AS_INPUTS"]) + if setPinsAsInputs: self.set_mode(self.DEVICE_CMD["SET_PINS_AS_INPUTS"]) return data - def BackupROM(self, fncSetProgress=None, path="ROM.gb", mbc=0x00, rom_banks=512, agb_rom_size=0, start_addr=0, fast_read_mode=False): - config = { 'mode':1, 'port':self, 'path':path, 'mbc':mbc, 'rom_banks':rom_banks, 'agb_rom_size':agb_rom_size, 'start_addr':start_addr, 'fast_read_mode':fast_read_mode } + def BackupROM(self, fncSetProgress=None, path="ROM.gb", mbc=0x00, rom_banks=512, agb_rom_size=0, start_addr=0, fast_read_mode=False, cart_type=0): + from . import DataTransfer + config = { 'mode':1, 'port':self, 'path':path, 'mbc':mbc, 'rom_banks':rom_banks, 'agb_rom_size':agb_rom_size, 'start_addr':start_addr, 'fast_read_mode':fast_read_mode, 'cart_type':cart_type } if self.WORKER is None: - self.WORKER = DataTransfer(config) + self.WORKER = DataTransfer.DataTransfer(config) self.WORKER.updateProgress.connect(fncSetProgress) else: self.WORKER.setConfig(config) self.WORKER.start() - def BackupRAM(self, fncSetProgress=None, path="ROM.sav", mbc=0x00, save_type=0): - config = { 'mode':2, 'port':self, 'path':path, 'mbc':mbc, 'save_type':save_type } + def BackupRAM(self, fncSetProgress=None, path="ROM.sav", mbc=0x00, save_type=0, rtc=False): + from . import DataTransfer + config = { 'mode':2, 'port':self, 'path':path, 'mbc':mbc, 'save_type':save_type, 'rtc':rtc } if self.WORKER is None: - self.WORKER = DataTransfer(config) + self.WORKER = DataTransfer.DataTransfer(config) self.WORKER.updateProgress.connect(fncSetProgress) else: self.WORKER.setConfig(config) self.WORKER.start() - def RestoreRAM(self, fncSetProgress=None, path="", mbc=0x00, save_type=0, erase=False): - config = { 'mode':3, 'port':self, 'path':path, 'mbc':mbc, 'save_type':save_type, 'erase':erase } + def RestoreRAM(self, fncSetProgress=None, path="", mbc=0x00, save_type=0, erase=False, rtc=False): + from . import DataTransfer + config = { 'mode':3, 'port':self, 'path':path, 'mbc':mbc, 'save_type':save_type, 'erase':erase, 'rtc':rtc } if self.WORKER is None: - self.WORKER = DataTransfer(config) + self.WORKER = DataTransfer.DataTransfer(config) self.WORKER.updateProgress.connect(fncSetProgress) else: self.WORKER.setConfig(config) self.WORKER.start() - def FlashROM(self, fncSetProgress=None, path="", cart_type=0, override_voltage=False, buffer=bytearray(), start_addr=0, prefer_sector_erase=False, reverse_sectors=False, fast_read_mode=False, verify_flash=False): - config = { 'mode':4, 'port':self, 'path':path, 'cart_type':cart_type, 'override_voltage':override_voltage, 'start_addr':start_addr, 'buffer':buffer, 'prefer_sector_erase':prefer_sector_erase, 'reverse_sectors':reverse_sectors, 'fast_read_mode':fast_read_mode, 'verify_flash':verify_flash } + def FlashROM(self, fncSetProgress=None, path="", cart_type=0, override_voltage=False, buffer=bytearray(), start_addr=0, prefer_chip_erase=False, reverse_sectors=False, fast_read_mode=False, verify_flash=False): + from . import DataTransfer + config = { 'mode':4, 'port':self, 'path':path, 'cart_type':cart_type, 'override_voltage':override_voltage, 'start_addr':start_addr, 'buffer':buffer, 'prefer_chip_erase':prefer_chip_erase, 'reverse_sectors':reverse_sectors, 'fast_read_mode':fast_read_mode, 'verify_flash':verify_flash } if self.WORKER is None: - self.WORKER = DataTransfer(config) + self.WORKER = DataTransfer.DataTransfer(config) self.WORKER.updateProgress.connect(fncSetProgress) else: self.WORKER.setConfig(config) self.WORKER.start() - def _TransferData(self, args, signal): # called by thread + def _TransferData(self, args, signal): if not self.IsConnected(): raise Exception("Couldn’t access the the device.") self.SIGNAL = signal mode = args["mode"] - if self.INFO == None: self.ReadInfo() path = args["path"] + if "rtc" not in args: args["rtc"] = False self.INFO["last_path"] = path - self.INFO["last_action"] = mode - time_start = time.time() bank_size = 0x4000 + agb_3dmemory = False self.CANCEL_ARGS = {} self.POS = 0 - - # main work + if self.INFO == None: self.ReadInfo() + + # Firmware check R26+ + if (int(self.FW[0]) < 26) and self.MODE == "DMG" and "mbc" in args and args["mbc"] in (0x0B, 0x0D, 0xFD): + self.SetProgress({"action":"ABORT", "info_type":"msgbox_critical", "info_msg":"A firmware update is required to access this cartridge. Please update the firmware of your GBxCart RW device to version R26 or higher.", "abortable":False}) + return False + # Firmware check R26+ + # Firmware check R27+ + if "agb_rom_size" in args and args["agb_rom_size"] == 64 * 1024 * 1024: # 3D Memory + if (int(self.FW[0]) < 27): + self.SetProgress({"action":"ABORT", "info_type":"msgbox_critical", "info_msg":"A future firmware update is required to access this cartridge. Please look for updates of FlashGBX and GBxCart RW firmware.", "abortable":False}) + return False + # Firmware check R27+ + + # Enable TAMA5 + if self.MODE == "DMG" and "mbc" in args and args["mbc"] == 0xFD: + self.ReadInfo() + tama5_check = int.from_bytes(self.ReadROM(0xA000, 64)[:1], byteorder="little") + dprint("Enabling TAMA5") + lives = 20 + while (tama5_check & 3) != 1: + dprint("└Current value is 0x{:X}, now writing 0xA001=0x{:X}".format(tama5_check, Util.TAMA5_REG.ENABLE.value)) + self.cart_write(0xA001, Util.TAMA5_REG.ENABLE.value, cs=True) + tama5_check = int.from_bytes(self.ReadROM(0xA000, 64)[:1], byteorder="little") + time.sleep(0.1) + lives -= 1 + if lives < 0: + self.SetProgress({"action":"ABORT", "info_type":"msgbox_critical", "info_msg":"The TAMA5 cartridge doesn’t seem to respond. Please try again.", "abortable":False}) + return False + + # main work starts here + self.INFO["last_action"] = mode self.FAST_READ = False if mode == 1: # Backup ROM fast_read_mode = args["fast_read_mode"] buffer_len = 0x1000 if self.MODE == "DMG": + supported_carts = list(self.SUPPORTED_CARTS['DMG'].values()) mbc = args["mbc"] bank_count = args["rom_banks"] if fast_read_mode: buffer_len = 0x4000 self.FAST_READ = True rom_size = bank_count * bank_size + elif self.MODE == "AGB": + supported_carts = list(self.SUPPORTED_CARTS['AGB'].values()) rom_size = args["agb_rom_size"] - if fast_read_mode: + bank_count = 1 + endAddr = rom_size + if rom_size == 64 * 1024 * 1024: # 3D Memory + agb_3dmemory = True + if fast_read_mode: + buffer_len = 0x1000 + self.FAST_READ = True + else: + buffer_len = 0x200 + + elif fast_read_mode: buffer_len = 0x10000 self.FAST_READ = True + if rom_size == 0: rom_size = 32 * 1024 * 1024 + # Read a bit before actually dumping (fixes some carts that don’t like SET_PINS_AS_INPUTS) + self.ReadROM(0, 64) + + # Cart type check (GB Memory) + flashcart_meta = False + if not isinstance(args["cart_type"], dict): + for i in range(0, len(supported_carts)): + if i == args["cart_type"]: flashcart_meta = supported_carts[i] + if flashcart_meta in ("RETAIL", "AUTODETECT"): flashcart_meta = False + else: + flashcart_meta = args["cart_type"] + + if flashcart_meta is not False and "unlock_before_rom_dump" in flashcart_meta and flashcart_meta["unlock_before_rom_dump"] is True: + # Unlock Flash + if "unlock" in flashcart_meta["commands"]: + for i in range(0, len(flashcart_meta["commands"]["unlock"])): + addr = flashcart_meta["commands"]["unlock"][i][0] + data = flashcart_meta["commands"]["unlock"][i][1] + count = flashcart_meta["commands"]["unlock"][i][2] + for _ in range(0, count): + self.gbx_flash_write_address_byte(addr, data) + + # Reset Flash + if "reset" in flashcart_meta["commands"]: + for i in range(0, len(flashcart_meta["commands"]["reset"])): + self.gbx_flash_write_address_byte(flashcart_meta["commands"]["reset"][i][0], flashcart_meta["commands"]["reset"][i][1]) + data_dump = bytearray() startAddr = 0 @@ -933,19 +1125,15 @@ class GbxDevice: try: file = open(path, "wb") - except PermissionError as e: + except PermissionError: self.SetProgress({"action":"ABORT", "info_type":"msgbox_critical", "info_msg":"FlashGBX doesn’t have permission to access this file for writing:\n" + path, "abortable":False}) return False if self.MODE == "DMG": endAddr = bank_size - else: - endAddr = rom_size - bank_count = 1 - - # Read a bit before actually dumping (fixes some bootlegs) - self.ReadROM(0, 64) + dprint("bank_count: {:d}".format(bank_count)) + self.SetProgress({"action":"INITIALIZE", "method":"ROM_READ", "size":rom_size}) for bank in range(0, bank_count): @@ -953,6 +1141,11 @@ class GbxDevice: if bank > 0: startAddr = bank_size endAddr = startAddr + bank_size + + if mbc in (0x0B, 0x0D) and bank % 0x20 == 0: # MMM01 + startAddr = 0 + endAddr = bank_size + self.SetBankROM(bank, mbc) for currAddr in range(startAddr, endAddr, buffer_len): @@ -968,9 +1161,9 @@ class GbxDevice: return if currAddr == startAddr: - buffer = self.ReadROM(currAddr, buffer_len, True) + buffer = self.ReadROM(currAddr, buffer_len, True, agb_3dmemory=agb_3dmemory) else: - buffer = self.ReadROM(currAddr, buffer_len, False) + buffer = self.ReadROM(currAddr, buffer_len, False, agb_3dmemory=agb_3dmemory) if buffer == False: self.CANCEL = True @@ -982,19 +1175,61 @@ class GbxDevice: self.SetProgress({"action":"UPDATE_POS", "pos":recvBytes}) file.close() + + # Read hidden sector (GB Memory) + if flashcart_meta is not False and "read_hidden_sector" in flashcart_meta["commands"] and "hidden_sector_size" in flashcart_meta: + # Unlock Flash + if "unlock" in flashcart_meta["commands"]: + for i in range(0, len(flashcart_meta["commands"]["unlock"])): + addr = flashcart_meta["commands"]["unlock"][i][0] + data = flashcart_meta["commands"]["unlock"][i][1] + count = flashcart_meta["commands"]["unlock"][i][2] + for _ in range(0, count): + self.gbx_flash_write_address_byte(addr, data) + + # Request hidden sector + for i in range(0, len(flashcart_meta["commands"]["read_hidden_sector"])): + self.gbx_flash_write_address_byte(flashcart_meta["commands"]["read_hidden_sector"][i][0], flashcart_meta["commands"]["read_hidden_sector"][i][1]) + + # Read data + buffer = self.ReadROM(0, flashcart_meta["hidden_sector_size"], True) + path2 = os.path.splitext(path)[0] + ".map" + try: + file = open(path2, "wb") + except PermissionError: + self.SetProgress({"action":"ABORT", "info_type":"msgbox_critical", "info_msg":"FlashGBX doesn’t have permission to access this file for writing:\n" + path2, "abortable":False}) + return False + file.write(buffer) + file.close() - # calculate global checksum + # Calculate Global Checksum chk = 0 if self.MODE == "DMG": - for i in range(0, len(data_dump), 2): - if i != 0x14E: - chk = chk + data_dump[i + 1] - chk = chk + data_dump[i] - chk = chk & 0xFFFF + if mbc in (0x0B, 0x0D): # MMM01 + self.set_mode(self.DEVICE_CMD['RESET_MBC']) + self.wait_for_ack() + temp_data = data_dump[0:-0x8000] + temp_menu = data_dump[-0x8000:] + temp_dump = temp_menu + temp_data + for i in range(0, len(temp_dump), 2): + if i != 0x14E: + chk = chk + temp_dump[i + 1] + chk = chk + temp_dump[i] + chk = chk & 0xFFFF + + else: + for i in range(0, len(data_dump), 2): + if i != 0x14E: + chk = chk + data_dump[i + 1] + chk = chk + data_dump[i] + chk = chk & 0xFFFF + elif self.MODE == "AGB": chk = zlib.crc32(data_dump) & 0xffffffff self.INFO["rom_checksum_calc"] = chk + + self.INFO["file_sha1"] = hashlib.sha1(data_dump).hexdigest() self.SetProgress({"action":"FINISHED"}) ######################################### @@ -1014,8 +1249,18 @@ class GbxDevice: startAddr = 0xA000 if mode == 2: # Backup transfer_size = 512 - else: + else: # Restore transfer_size = 64 + + if mbc == 0xFD: # TAMA5 + if args["rtc"]: save_size += 0x10 + elif mbc == 0x22: # MBC7 EEPROM + self.SetProgress({"action":"ABORT", "info_type":"msgbox_critical", "info_msg":"Save data handling is not supported yet for this cartridge type.", "abortable":False}) + return False + + if transfer_size >= save_size: + transfer_size = save_size + bank_size = save_size elif self.MODE == "AGB": bank_size = 0x10000 @@ -1070,21 +1315,24 @@ class GbxDevice: transfer_size = 128 elif maker_id == "SANYO": if int(self.FW[0]) < 24: - self.SetProgress({"action":"ABORT", "info_type":"msgbox_critical", "info_msg":"A firmware update is required to correctly handle the save data chip of this cartridge. Please update the firmware of your GBxCart RW device to version R24 or higher.", "abortable":False}) + self.SetProgress({"action":"ABORT", "info_type":"msgbox_critical", "info_msg":"A firmware update is required to access this cartridge. Please update the firmware of your GBxCart RW device to version R24 or higher.", "abortable":False}) return False # Prepare some stuff if mode == 2: # Backup try: file = open(path, "wb") - except PermissionError as e: + except PermissionError: self.SetProgress({"action":"ABORT", "info_type":"msgbox_critical", "info_msg":"FlashGBX doesn’t have permission to access this file for writing:\n" + path, "abortable":False}) return False self.SetProgress({"action":"INITIALIZE", "method":"SAVE_READ", "size":save_size}) elif mode == 3: # Restore if args["erase"]: # Erase - data_import = save_size * b'\xFF' + if args["mbc"] == 0xFD: # TAMA5 + data_import = save_size * b'\x00' + else: + data_import = save_size * b'\xFF' self.INFO["save_erase"] = True else: with open(path, "rb") as file: data_import = file.read() @@ -1101,7 +1349,7 @@ class GbxDevice: if self.MODE == "DMG": endAddr = startAddr + bank_size if endAddr > (startAddr + save_size): endAddr = startAddr + save_size - self.SetBankRAM(bank) + self.SetBankRAM(bank, mbc) elif self.MODE == "AGB": endAddr = startAddr + min(save_size, bank_size) @@ -1123,7 +1371,6 @@ class GbxDevice: self.EnableRAM(mbc=mbc, enable=False) elif self.MODE == "AGB": if bank > 0: self.set_number(0, self.DEVICE_CMD["GBA_FLASH_SET_BANK"]) - self.set_mode(self.DEVICE_CMD["READ_ROM_RAM"]) self.ReadInfo() cancel_args = {"action":"ABORT", "abortable":False} cancel_args.update(self.CANCEL_ARGS) @@ -1133,31 +1380,34 @@ class GbxDevice: if mode == 2: # Backup if self.MODE == "DMG": - buffer = self.ReadROM(currAddr, buffer_len) - elif self.MODE == "AGB": - if currAddr + buffer_len < save_size: - last = False + if mbc == 0xFD: # TAMA5 + buffer = self.ReadRAM_TAMA5(rtc=args["rtc"]) else: - last = True + buffer = self.ReadROM(currAddr, buffer_len) + elif self.MODE == "AGB": self.set_mode(self.DEVICE_CMD[read_command]) - buffer = self.read(buffer_len, last) + buffer = self.read(length=buffer_len, last=True, ask_next_bytes=False) if buffer == False: self.SetProgress({"action":"ABORT", "info_type":"msgbox_critical", "info_msg":"Backup failed, please try again.", "abortable":False}) return False data_dump.extend(buffer) file.write(buffer) - + elif mode == 3: # Restore data = data_import[pos:pos+buffer_len] if self.MODE == "DMG": - - self.gbx_flash_write_data_bytes(self.DEVICE_CMD["WRITE_RAM"], data) + if mbc == 0xFD: # TAMA5 + self.WriteRAM_TAMA5(data, rtc=args["rtc"]) + else: + self.gbx_flash_write_data_bytes(self.DEVICE_CMD["WRITE_RAM"], data) + self.wait_for_ack() elif self.MODE == "AGB": if save_type == 6 or save_type == 7: # FLASH if maker_id == "ATMEL": self.gbx_flash_write_data_bytes(self.DEVICE_CMD["GBA_FLASH_WRITE_ATMEL"], data) + self.wait_for_ack() else: if (currAddr % 4096 == 0): self.set_number(sector, self.DEVICE_CMD["GBA_FLASH_4K_SECTOR_ERASE"]) @@ -1177,21 +1427,121 @@ class GbxDevice: self.set_number(currAddr, self.DEVICE_CMD["SET_START_ADDRESS"]) self.gbx_flash_write_data_bytes(self.DEVICE_CMD["GBA_FLASH_WRITE_BYTE"], data) + self.wait_for_ack() else: # EEPROM / SRAM self.gbx_flash_write_data_bytes(self.DEVICE_CMD[write_command], data) + self.wait_for_ack() self.SetProgress({"action":"WRITE", "bytes_added":len(data)}) - self.wait_for_ack() pos += buffer_len self.SetProgress({"action":"UPDATE_POS", "pos":pos}) - #if not pos == save_size: signal.emit(None, pos, save_size, speed/1024, time.time()-time_start, 0) self.INFO["transferred"] = pos if self.MODE == "DMG": - self.EnableRAM(mbc=mbc, enable=False) + # RTC for MBC3+RTC+SRAM+BATTERY + if mbc == 0x10 and args["rtc"]: + buffer = bytearray() + self.cart_write(0x6000, 0) + self.cart_write(0x6000, 1) + + if mode == 2: # Backup + for i in range(0x8, 0xD): + self.cart_write(0x4000, i) + buffer.extend(struct.pack("> 4) & 0xF)) + self.cart_write(0x0000, 0x0D) + self.cart_write(0xA000, 0xFE) + self.cart_write(0x0000, 0x00) + self.cart_write(0x0000, 0x0D) + + self.cart_write(0x0000, 0x0B) + self.cart_write(0xA000, 0x31) + self.cart_write(0x0000, 0x0D) + self.cart_write(0xA000, 0xFE) + self.cart_write(0x0000, 0x00) + self.cart_write(0x0000, 0x0D) + + self.cart_write(0x0000, 0x0B) + self.cart_write(0xA000, 0x61) + self.cart_write(0x0000, 0x0D) + self.cart_write(0xA000, 0xFE) + self.cart_write(0x0000, 0x00) + elif self.MODE == "AGB": if bank > 0: if (save_type == 5) and bank > 0: # 1M SRAM @@ -1199,7 +1549,9 @@ class GbxDevice: elif (save_type == 6 or save_type == 7) and bank > 0: # FLASH self.set_number(0, self.DEVICE_CMD["GBA_FLASH_SET_BANK"]) - if mode == 2: file.close() + if mode == 2: + file.close() + self.INFO["file_sha1"] = hashlib.sha1(data_dump).hexdigest() self.INFO["last_action"] = mode self.SetProgress({"action":"FINISHED"}) @@ -1233,14 +1585,14 @@ class GbxDevice: i = i - len(data_import) if i > 0: data_import += bytearray([0xFF] * i) - self._FlashROM(buffer=data_import, cart_type=cart_type, voltage=args["override_voltage"], start_addr=args["start_addr"], signal=signal, prefer_sector_erase=args["prefer_sector_erase"], reverse_sectors=args["reverse_sectors"], fast_read_mode=args["fast_read_mode"], verify_flash=args["verify_flash"]) + self._FlashROM(buffer=data_import, cart_type=cart_type, voltage=args["override_voltage"], start_addr=args["start_addr"], signal=signal, prefer_chip_erase=args["prefer_chip_erase"], reverse_sectors=args["reverse_sectors"], fast_read_mode=args["fast_read_mode"], verify_flash=args["verify_flash"]) # Reset pins to avoid save data loss self.set_mode(self.DEVICE_CMD["SET_PINS_AS_INPUTS"]) ####################################################################################################################################### - def _FlashROM(self, buffer=bytearray(), start_addr=0, cart_type=None, voltage=3.3, signal=None, prefer_sector_erase=False, reverse_sectors=False, fast_read_mode=False, verify_flash=False): + def _FlashROM(self, buffer=bytearray(), start_addr=0, cart_type=None, voltage=3.3, signal=None, prefer_chip_erase=False, reverse_sectors=False, fast_read_mode=False, verify_flash=False): if not self.IsConnected(): raise Exception("Couldn’t access the the device.") if self.INFO == None: self.ReadInfo() self.INFO["last_action"] = 4 @@ -1252,20 +1604,12 @@ class GbxDevice: if start_addr > 0: data_import = (b'\xFF' * start_addr) + data_import - if self.MODE == "DMG": - supported_carts = list(self.SUPPORTED_CARTS['DMG'].values()) - elif self.MODE == "AGB": - supported_carts = list(self.SUPPORTED_CARTS['AGB'].values()) - if cart_type == "RETAIL" or cart_type == "AUTODETECT": return False # Generic ROM Cartridge is not flashable flashcart_meta = copy.deepcopy(cart_type) - # Reverse sectors if requested - if reverse_sectors: flashcart_meta['sector_size'].reverse() - # Firmware check R20+ if (int(self.FW[0]) < 20) and self.MODE == "AGB" and "buffer_write" in flashcart_meta["commands"] and flashcart_meta["commands"]["buffer_write"] == [[0xAAA, 0xAA], [0x555, 0x55], ['SA', 0x25], ['SA', 'BS'], ['PA', 'PD'], ['SA', 0x29]]: - self.SetProgress({"action":"ABORT", "info_type":"msgbox_critical", "info_msg":"A firmware update is required to write to this cartridge. Please update the firmware of your GBxCart RW device to version R20 or higher.", "abortable":False}) + self.SetProgress({"action":"ABORT", "info_type":"msgbox_critical", "info_msg":"A firmware update is required to access this cartridge. Please update the firmware of your GBxCart RW device to version R20 or higher.", "abortable":False}) return False # Firmware check R20+ # Firmware check R23+ @@ -1289,27 +1633,33 @@ class GbxDevice: elif flashcart_meta["voltage"] == 5: self.set_mode(self.DEVICE_CMD["VOLTAGE_5V"]) + # MBC if "mbc" in flashcart_meta: mbc = flashcart_meta['mbc'] - dprint("Set MBC to {:d}".format(mbc)) + if mbc == 2: mbc = 0x06 + elif mbc == 3: mbc = 0x13 + elif mbc == 5: mbc = 0x19 + elif mbc == 6: mbc = 0x20 + elif mbc == 7: mbc = 0x22 + dprint("Using MBC{:d} (ID 0x{:02X}) for flashing".format(flashcart_meta['mbc'], mbc)) if self.MODE == "DMG": self.set_mode(self.DEVICE_CMD["GB_CART_MODE"]) if "flash_commands_on_bank_1" in flashcart_meta and flashcart_meta["flash_commands_on_bank_1"]: + dprint("Setting GB_FLASH_BANK_1_COMMAND_WRITES") self.set_mode(self.DEVICE_CMD["GB_FLASH_BANK_1_COMMAND_WRITES"]) - dprint("Setting GB_FLASH_BANK_1_COMMAND_WRITES...") self.set_mode(self.DEVICE_CMD["GB_FLASH_WE_PIN"]) if flashcart_meta["write_pin"] == "WR": + dprint("Setting WE_AS_WR_PIN") self.set_mode(self.DEVICE_CMD["WE_AS_WR_PIN"]) - dprint("Setting WE_AS_WR_PIN...") elif flashcart_meta["write_pin"] in ("AUDIO", "VIN"): + dprint("Setting WE_AS_AUDIO_PIN") self.set_mode(self.DEVICE_CMD["WE_AS_AUDIO_PIN"]) - dprint("Setting WE_AS_AUDIO_PIN...") if "single_write" in flashcart_meta["commands"] and len(flashcart_meta["commands"]["single_write"]) == 4: # Submit flash program commands to firmware - dprint("Setting GB_FLASH_PROGRAM_METHOD...") + dprint("Setting GB_FLASH_PROGRAM_METHOD") self.set_mode(self.DEVICE_CMD["GB_FLASH_PROGRAM_METHOD"]) for i in range(0, 3): dprint("single_write_command(",i,"):", hex(flashcart_meta["commands"]["single_write"][i][0]), "=", hex(flashcart_meta["commands"]["single_write"][i][1])) @@ -1334,10 +1684,26 @@ class GbxDevice: for i in range(0, len(flashcart_meta["commands"]["reset"])): self.gbx_flash_write_address_byte(flashcart_meta["commands"]["reset"][i][0], flashcart_meta["commands"]["reset"][i][1]) + # Read sector size from CFI if necessary + if "sector_size_from_cfi" in flashcart_meta and flashcart_meta["sector_size_from_cfi"] is True: + (_, cfi_s, cfi) = self.CheckFlashChip(limitVoltage=(voltage == 3.3), cart_type=cart_type) + if cfi_s == "": + self.SetProgress({"action":"ABORT", "info_type":"msgbox_critical", "info_msg":"Couldn’t read the Common Flash Interface (CFI) data from the flash chip in order to determine the correct sector size map. Please make sure that the cartridge contacts are clean, and that the selected cartridge type and settings are correct.", "abortable":False}) + return False + flashcart_meta["sector_size"] = cfi["erase_sector_blocks"] + if cfi["tb_boot_sector_raw"] == 0x03: flashcart_meta['sector_size'].reverse() + dprint("Sector map was read from Common Flash Interface (CFI) data:", cfi["erase_sector_blocks"], cfi["erase_sector_blocks"]) + self.set_number(0, self.DEVICE_CMD["SET_START_ADDRESS"]) + + # Check if write command exists and quit if not + if "single_write" not in flashcart_meta["commands"] and "buffer_write" not in flashcart_meta["commands"]: + self.SetProgress({"action":"ABORT", "info_type":"msgbox_critical", "info_msg":"This cartridge type is currently not supported for ROM flashing.", "abortable":False}) + return False + # Chip Erase chip_erase = False if "chip_erase" in flashcart_meta["commands"]: - if "sector_erase" in flashcart_meta["commands"] and prefer_sector_erase is True: + if "sector_erase" in flashcart_meta["commands"] and prefer_chip_erase is False: chip_erase = False elif "chip_erase_treshold" in flashcart_meta: if len(data_import) > flashcart_meta["chip_erase_treshold"] or "sector_erase" not in flashcart_meta["commands"]: @@ -1363,6 +1729,7 @@ class GbxDevice: self.gbx_flash_write_address_byte(sr_addr, sr_data) wait_for = self.ReadROM(addr, 64) wait_for = ((wait_for[1] << 8 | wait_for[0]) & flashcart_meta["commands"]["chip_erase_wait_for"][i][2]) + dprint("CE_SR {:X}=={:X}?".format(wait_for, data)) if wait_for == data: break time.sleep(0.5) timeout -= 0.5 @@ -1395,7 +1762,7 @@ class GbxDevice: first_bank = 0 bank_count = 1 - self.SetProgress({"action":"INITIALIZE", "time_start":time_start, "method":"ROM_WRITE", "size":len(data_import)}) + self.SetProgress({"action":"INITIALIZE", "time_start":time.time(), "method":"ROM_WRITE", "size":len(data_import)}) currSect = 0 @@ -1419,6 +1786,7 @@ class GbxDevice: dprint(flashcart_meta["sector_size"][currSect][1]) sector_count = None + sector_size = 0 if "sector_erase" in flashcart_meta["commands"]: if isinstance(flashcart_meta["sector_size"], list): sector_size = flashcart_meta["sector_size"][currSect][0] @@ -1434,7 +1802,6 @@ class GbxDevice: ack = True if first_bank == bank_count: first_bank -= 1 # dirty hack so that <32 KB works too for bank in range(first_bank, bank_count): - dprint("BANK {:d}".format(bank)) if self.MODE == "DMG": if bank > first_bank: currAddr = bank_size self.set_number(currAddr, self.DEVICE_CMD["SET_START_ADDRESS"]) @@ -1473,13 +1840,14 @@ class GbxDevice: sector_count = flashcart_meta["sector_size"][currSect][1] if pos % sector_size == 0: - self.SetProgress({"action":"SECTOR_ERASE", "time_start":time.time(), "abortable":True}) + self.SetProgress({"action":"UPDATE_POS", "pos":pos}) + self.SetProgress({"action":"SECTOR_ERASE", "sector_size":sector_size, "sector_pos":pos, "time_start":time.time(), "abortable":True}) # Update sector size if changed if "sector_erase" in flashcart_meta["commands"]: if isinstance(flashcart_meta["sector_size"], list): sector_size = flashcart_meta["sector_size"][currSect][0] - dprint("\n* sector_count:", sector_count, "sector_size:", hex(sector_size), "pos:",hex(pos)) + dprint("* sector_count:", sector_count, "sector_size:", hex(sector_size), "pos:",hex(pos)) for i in range(0, len(flashcart_meta["commands"]["sector_erase"])): addr = flashcart_meta["commands"]["sector_erase"][i][0] @@ -1501,7 +1869,7 @@ class GbxDevice: if addr == "SA+0x4000": addr = currAddr + 0x4000 if addr == "SA+0x7000": addr = currAddr + 0x7000 time.sleep(0.05) - timeout = 50 + timeout = 100 while True: if "wait_read_status_register" in flashcart_meta and flashcart_meta["wait_read_status_register"] == True: for j in range(0, len(flashcart_meta["commands"]["read_status_register"])): @@ -1518,7 +1886,7 @@ class GbxDevice: self.SetProgress({"action":"ABORT", "info_type":"msgbox_critical", "info_msg":"Erasing a flash chip sector timed out. Please make sure that the cartridge contacts are clean, and that the selected cartridge type and settings are correct.", "abortable":False}) return False if wait_for == data: break - self.SetProgress({"action":"SECTOR_ERASE", "time_start":time.time(), "abortable":True}) + self.SetProgress({"action":"SECTOR_ERASE", "sector_size":sector_size, "sector_pos":pos, "time_start":time.time(), "abortable":True}) # Reset Flash if "reset" in flashcart_meta["commands"]: @@ -1532,8 +1900,6 @@ class GbxDevice: if sector_count is not None: sector_count -= 1 - - #self.SetProgress({"action":"UPDATE_SPEED", "abortable":False}) # Write data (with special firmware acceleration if available) if "buffer_write" in flashcart_meta["commands"]: @@ -1587,7 +1953,7 @@ class GbxDevice: if int(currAddr % 0x8000) == 0: self.set_number(currAddr / 2, self.DEVICE_CMD["SET_START_ADDRESS"]) - # R24+ + # Firmware check R24+ elif (int(self.FW[0]) >= 24) and "single_write_7FC0_to_7FFF" not in flashcart_meta: data = data_import[pos:pos+256] if data == bytearray([0xFF] * len(data)): @@ -1665,7 +2031,7 @@ class GbxDevice: elif "single_write" in flashcart_meta["commands"]: if self.MODE == "DMG": - # R24+ + # Firmware check R24+ if (int(self.FW[0]) < 24) or ("pulse_reset_after_write" in flashcart_meta and flashcart_meta["pulse_reset_after_write"]): data = data_import[pos:pos+64] else: @@ -1748,6 +2114,9 @@ class GbxDevice: else: self.SetProgress({"action":"WRITE", "bytes_added":len(data), "skipping":skipping}) + self.SetProgress({"action":"UPDATE_POS", "pos":pos}) + time.sleep(0.5) + # Reset Flash if "reset_every" in flashcart_meta: for j in range(0, pos, flashcart_meta["reset_every"]): @@ -1763,6 +2132,7 @@ class GbxDevice: rom_size = len(data_import) buffer_len = 0x1000 if self.MODE == "DMG": + self.set_mode(self.DEVICE_CMD["VOLTAGE_5V"]) if fast_read_mode: buffer_len = 0x4000 self.FAST_READ = True @@ -1771,8 +2141,6 @@ class GbxDevice: buffer_len = 0x10000 self.FAST_READ = True - data_dump = bytearray() - startAddr = 0 currAddr = 0 pos = 0 @@ -1801,10 +2169,6 @@ class GbxDevice: cancel_args.update(self.CANCEL_ARGS) self.CANCEL_ARGS = {} self.SetProgress(cancel_args) - try: - file.close() - except: - pass return if currAddr == startAddr: @@ -1822,7 +2186,7 @@ class GbxDevice: if buffer[i] != data_import[pos+i]: err_pos = pos+i break - self.SetProgress({"action":"ABORT", "info_type":"msgbox_critical", "info_msg":"The ROM was flashed completely, but verification of flashed data failed at address 0x{:X}.".format(err_pos), "abortable":False}) + self.SetProgress({"action":"ABORT", "info_type":"msgbox_critical", "info_msg":"The ROM was flashed completely, but verification of written data failed at address 0x{:X}.".format(err_pos), "abortable":False}) self.CANCEL = True return False @@ -1833,5 +2197,7 @@ class GbxDevice: self.POS = 0 if self.MODE == "DMG": self.SetBankROM(0, mbc=mbc, bank_count=bank_count) - + self.SetProgress({"action":"FINISHED", "verified":verified}) + +# To whoever tries to make sense of my code (including my future self), I’m very sorry for the bad code. A rewrite is planned. diff --git a/FlashGBX/res/config.zip b/FlashGBX/res/config.zip index 5ec75ea..c09ffeb 100644 Binary files a/FlashGBX/res/config.zip and b/FlashGBX/res/config.zip differ diff --git a/README.md b/README.md index 710e57f..7056b8f 100644 --- a/README.md +++ b/README.md @@ -17,104 +17,134 @@ by Lesserkuma - Write new ROMs to a wide variety of Game Boy and Game Boy Advance flash cartridges - Many reproduction cartridges and flash cartridges can be auto-detected - A flash chip query can be performed for unsupported flash cartridges -- Decode and extract Game Boy Camera (Pocket Camera) photos from save data +- Decode and extract Game Boy Camera photos from save data ### Confirmed working reader/writer hardware -- [insideGadgets GBxCart RW v1.3 and v1.3 Pro](https://www.gbxcart.com/) with firmware versions from R19 up to R25 (other hardware revisions and firmware versions may also work, but are untested) +- [insideGadgets GBxCart RW v1.3 and v1.3 Pro](https://www.gbxcart.com/) with firmware versions from R19 up to R26 (other hardware revisions and firmware versions may also work, but are untested) + +### Currently supported official cartridge memory mappers + +- Game Boy + - All cartridges without memory mapping + - MBC1 + - MBC2 + - MBC3/MBC30 + - MBC5 + - MBC7 (ROM backup only) + - MBC1M + - MMM01 + - Game Boy Camera + - G-MMC1 (ROM and map backup only) + - HuC-1 + - HuC-3 + - TAMA5 + +- Game Boy Advance + - All cartridges without memory mapping + - 3D Memory (GBA Video)¹ + +¹ Preliminary support; will not work until the GBxCart RW device is updated to a future firmware version ### Currently supported flash cartridges - Game Boy - - BUNG Doctor GB Card 64M - - DIY cart with AM29F016/AM29F016B - - DIY cart with AM29F032/AM29F032B - - DIY cart with AT49F040 - - GB Smart 32M - - insideGadgets 32 KB - - insideGadgets 512 KB - - insideGadgets 1 MB, 128 KB SRAM - - insideGadgets 2 MB, 128 KB SRAM/32 KB FRAM - - insideGadgets 4 MB, 128 KB SRAM/FRAM - - insideGadgets 4 MB, 32 KB FRAM, MBC3+RTC - - Mr Flash 64M + - BUNG Doctor GB Card 64M + - DIY cart with AM29F016/AM29F016B + - DIY cart with AM29F032/AM29F032B + - DIY cart with AT49F040 + - GB Smart 32M + - insideGadgets 32 KB + - insideGadgets 512 KB + - insideGadgets 1 MB, 128 KB SRAM + - insideGadgets 2 MB, 128 KB SRAM/32 KB FRAM + - insideGadgets 4 MB, 128 KB SRAM/FRAM + - insideGadgets 4 MB, 32 KB FRAM, MBC3+RTC + - Mr Flash 64M - Game Boy Advance - - Development AGB Cartridge 128M Flash S, E201850 - - Development AGB Cartridge 256M Flash S, E201868 - - Flash2Advance 256M (non-ultra variant, with 2× 28F128J3A150) - - insideGadgets 16 MB, 64K EEPROM with Solar and RTC options - - insideGadgets 32 MB, 1M FLASH with RTC option - - insideGadgets 32 MB, 512K FLASH - - insideGadgets 32 MB, 4K/64K EEPROM - - insideGadgets 32 MB, 256K FRAM with Rumble option + - Development AGB Cartridge 128M Flash S, E201850 + - Development AGB Cartridge 256M Flash S, E201868 + - Flash2Advance 256M (non-ultra variant, with 2× 28F128J3A150) + - insideGadgets 16 MB, 64K EEPROM with Solar Sensor and RTC options + - insideGadgets 32 MB, 1M FLASH with RTC option + - insideGadgets 32 MB, 512K FLASH + - insideGadgets 32 MB, 4K/64K EEPROM + - insideGadgets 32 MB, 256K FRAM with Rumble option ### Currently supported and tested reproduction cartridges - Game Boy - - ES29LV160_DRV with 29DL32TF-70 - - GB-M968 with M29W160EB - - GB-M968 with MX29LV320ABTC - - ALTERA CPLD and S29GL032N90T (no PCB text) - - SD007_48BALL_64M with GL032M11BAIR4 - - SD007_48BALL_64M with M29W640 - - SD007_48BALL_64M_V2 with GL032M11BAIR4 - - SD007_48BALL_64M_V2 with M29W160ET - - SD007_48BALL_64M_V3 with 29DL161TD-90 - - SD007_48BALL_64M_V5 with 36VF3203 - - SD007_48BALL_64M_V5 with 36VF3204 - - SD007_48BALL_64M_V6 with 36VF3204 - - SD007_48BALL_64M_V6 with 29DL163BD-90 - - SD007_BV5_DRV with M29W320DT - - SD007_BV5_V2 with HY29LV160TT - - SD007_BV5_V2 with MX29LV320BTC - - SD007_BV5_V3 with 29LV160BE-90PFTN - - SD007_BV5_V3 with HY29LV160BT-70 - - SD007_BV5_V3 with AM29LV160MB - - SD007_TSOP_48BALL with 36VF3204 - - SD007_TSOP_48BALL with AM29LV160DB - - SD007_TSOP_48BALL with AM29LV160DT - - SD007_TSOP_48BALL with M29W160ET - - SD007_TSOP_48BALL with L160DB12VI + - DMG-DHCN-20 with MX29LV320ET + - ES29LV160_DRV with 29DL32TF-70 + - GB-M968 with M29W160EB + - GB-M968 with MX29LV320ABTC + - ALTERA CPLD and S29GL032N90T (no PCB text) + - SD007_48BALL_64M with GL032M11BAIR4 + - SD007_48BALL_64M with M29W640 + - SD007_48BALL_64M_V2 with GL032M11BAIR4 + - SD007_48BALL_64M_V2 with M29W160ET + - SD007_48BALL_64M_V3 with 29DL161TD-90 + - SD007_48BALL_64M_V5 with 36VF3203 + - SD007_48BALL_64M_V5 with 36VF3204 + - SD007_48BALL_64M_V6 with 36VF3204 + - SD007_48BALL_64M_V6 with 29DL163BD-90 + - SD007_BV5_DRV with M29W320DT + - SD007_BV5_DRV with S29GL032M90TFIR4 + - SD007_BV5_V2 with HY29LV160TT + - SD007_BV5_V2 with MX29LV320BTC + - SD007_BV5_V3 with 29LV160BE-90PFTN + - SD007_BV5_V3 with HY29LV160BT-70 + - SD007_BV5_V3 with AM29LV160MB + - SD007_K8D3216_32M with MX29LV160CT + - SD007_TSOP_48BALL with 36VF3204 + - SD007_TSOP_48BALL with AM29LV160DB + - SD007_TSOP_48BALL with AM29LV160DT + - SD007_TSOP_48BALL with K8D3216UTC + - SD007_TSOP_48BALL with M29W160ET + - SD007_TSOP_48BALL with L160DB12VI - Game Boy Advance - - 28F256L03B-DRV with 256L30B - - 36L0R8-39VF512 with M36L0R8060B - - 36L0R8-39VF512 with M36L0R8060T - - 4050_4400_4000_4350_36L0R_V5 with M36L0R7050T - - 4050_4400_4000_4350_36L0R_V5 with M36L0T8060T - - 4050_4400_4000_4350_36L0R_V5 with M36L0R8060T - - 4400 with 4400L0ZDQ0 - - 4455_4400_4000_4350_36L0R_V3 with M36L0R7050T - - AGB-E05-01 with GL128S - - AGB-E05-01 with MSP55LV128M - - AGB-E05-02 with M29W128GH - - AGB-E08-09 with 29LV128DTMC-90Q - - AGB-SD-E05 with MSP55LV128 - - BX2006_0106_NEW with S29GL128N10TFI01 - - BX2006_TSOP_64BALL with GL128S - - BX2006_TSOP_64BALL with GL256S - - BX2006_TSOPBGA_0106 with M29W640GB6AZA6 - - BX2006_TSOPBGA_0106 with K8D6316UTM-PI07 - - GE28F128W30 with 128W30B0 - - M6MGJ927 (no PCB text) + - 28F256L03B-DRV with 256L30B + - 36L0R8-39VF512 with M36L0R8060B + - 36L0R8-39VF512 with M36L0R8060T + - 4050_4400_4000_4350_36L0R_V5 with 4050L0YTQ2 + - 4050_4400_4000_4350_36L0R_V5 with M36L0R7050T + - 4050_4400_4000_4350_36L0R_V5 with M36L0T8060T + - 4050_4400_4000_4350_36L0R_V5 with M36L0R8060T + - 4400 with 4400L0ZDQ0 + - 4455_4400_4000_4350_36L0R_V3 with M36L0R7050T + - AGB-E05-01 with GL128S + - AGB-E05-01 with MSP55LV128M + - AGB-E05-01 with MX29GL128FHT2I-90G + - AGB-E05-02 with M29W128FH + - AGB-E05-02 with M29W128GH + - AGB-E08-09 with 29LV128DTMC-90Q + - AGB-SD-E05 with MSP55LV128 + - BX2006_0106_NEW with S29GL128N10TFI01 + - BX2006_TSOP_64BALL with GL128S + - BX2006_TSOP_64BALL with GL256S + - BX2006_TSOPBGA_0106 with M29W640GB6AZA6 + - BX2006_TSOPBGA_0106 with K8D6316UTM-PI07 + - GE28F128W30 with 128W30B0 + - M6MGJ927 (no PCB text) Many different reproduction cartridges share their flash chip command set, so even if yours is not on this list, it may still work fine or even be auto-detected as another one. Support for more cartridges can also be added by creating external config files that include the necessary flash chip commands. ## Installing and running -The application should work on pretty much every operating system that supports Qt-GUI applications built using [Python 3](https://www.python.org/downloads/) with [PySide2](https://pypi.org/project/PySide2/), [pyserial](https://pypi.org/project/pyserial/), [Pillow](https://pypi.org/project/Pillow/), [requests](https://pypi.org/project/requests/) and [setuptools](https://pypi.org/project/setuptools/) packages. - -If you have Python and pip installed, you can use `pip install FlashGBX` to download and install the application, or use `pip install --upgrade FlashGBX` to upgrade from an older version. Then use `python -m FlashGBX` to run it. +If you have Python and pip installed, you can use `pip install FlashGBX` to download and install the application, or use `pip install --upgrade FlashGBX` to upgrade from an older version. Then use `python -m FlashGBX` or `python -m FlashGBX --cli` to run it. To run FlashGBX in portable mode, you can also download the source code archive and call `python run.py` after installing the prerequisites yourself. *On some platforms you may have to use `pip3`/`python3` instead of `pip`/`python`.* +The application should work on pretty much every operating system that supports Qt-GUI applications built using [Python 3](https://www.python.org/downloads/) with [PySide2](https://pypi.org/project/PySide2/), [pyserial](https://pypi.org/project/pyserial/), [Pillow](https://pypi.org/project/Pillow/), [requests](https://pypi.org/project/requests/) and [setuptools](https://pypi.org/project/setuptools/) packages. + ### Windows binaries Available in the GitHub [Releases](https://github.com/lesserkuma/FlashGBX/releases) section: @@ -132,9 +162,9 @@ These executables have been created using *PyInstaller* and *Inno Setup*. * On some Linux systems, you may need the *XCB Xinerama package* if you see an error regarding failed Qt platform plugin initialization. You can install it with `sudo apt install libxcb-xinerama0` etc. -* For save data backup/restore on Game Boy Advance reproduction cartridges, depending on how it was built, you may have to manually select the save type for it to work properly. +* On older systems such as MacOS X El Capitan 10.11, you may run into an error that says `TypeError: 'Shiboken.ObjectType' object is not iterable`. Installing [Python 3.7.9](https://www.python.org/downloads/release/python-379/) instead of the latest available version may resolve this issue. If that still doesn’t work, you can try to uninstall PySide2 (`pip uninstall PySide2`) and then run FlashGBX again in command line interface mode. -* The save data backup/restore feature may not work on certain reproduction cartridges with batteryless-patched ROMs. As those cartridges use the same flash chip for both ROM and save data storage, a full ROM backup will usually include the save data. Also, when flashing a new unpatched ROM to a cartridge like this, the game may not be able to save progress without soldering in a battery. +* For save data backup/restore on Game Boy Advance reproduction cartridges, depending on how it was built, you may have to manually select the save type for it to work properly. However, the save data backup/restore feature may not work on certain reproduction cartridges with batteryless-patched ROMs. As those cartridges use the same flash chip for both ROM and save data storage, a full ROM backup will usually include the save data. Also, when flashing a new unpatched ROM to a cartridge like this, the game may not be able to save progress without soldering in a battery. See the [Flash Cart DB website](https://flashcartdb.com/index.php/Clone_and_Repo_Cart_Problems) for more information. ## DISCLAIMER @@ -146,8 +176,9 @@ The author would like to thank the following very kind people for their help and - AlexiG (GBxCart RW hardware, bug reports, flash chip info) - AndehX (app icon, flash chip info) +- antPL (flash chip info) - bbsan (flash chip info) -- ClassicOldSong (fix for Raspberry Pi) +- ClassicOldSong (bug reports) - djedditt (testing) - easthighNerd (feature suggestions) - Frost Clock (flash chip info) @@ -159,9 +190,11 @@ The author would like to thank the following very kind people for their help and - LovelyA72 (flash chip info) - LucentW (flash chip info, testing, bug reports) - marv17 (flash chip info, testing, bug reports, feature suggestions) +- paarongiroux (bug reports) - Paradoxical (flash chip info) - RevZ (Linux help, testing, bug reports, flash chip info) - Super Maker (flash chip info, testing) +- Veund (flash chip info) - Zeii (flash chip info) ## Changes @@ -202,7 +235,7 @@ The author would like to thank the following very kind people for their help and - Fixed support for Windows 7 when using pre-compiled exe file packages - Reduced size and decompression time of the pre-compiled exe file packages by excluding unnecessary DLL files - Fixed a timing issue that could sometimes cause a loss of save data when hot-swapping Game Boy cartridges -- Added some warnings that help with troubleshooting, for example that manually setting the feature box to a MBC5 option may be necessary for a clean dump when dumping ROMs from flash cartridges +- Added some warnings that may help with troubleshooting - Added taskbar progress visualization on Windows systems ### v0.9β (released 2020-12-17) @@ -252,7 +285,7 @@ The author would like to thank the following very kind people for their help and - Added the option to check for updates at application start *(thanks Icesythe7 and JFox for the suggestion and help)* - Added support for BX2006_TSOPBGA_0106 with K8D6316UTM-PI07 *(thanks LucentW)* - Added support for the currently available insideGadgets Game Boy Advance flash cartridges *(thanks AlexiG)* -- Added a Game Boy Camera (Pocket Camera) album viewer and picture extractor +- Added a Game Boy Camera album viewer and picture extractor ### v1.0 (released 2021-01-01) - Added a firmware check when writing to insideGadgets Game Boy Advance flash cartridges (requires GBxCart RW firmware R20 or higher) @@ -275,5 +308,33 @@ The author would like to thank the following very kind people for their help and - Minor bug fixes ### v1.3 (released 2021-01-21) -- Fixed a bug introduced in v1.1 that broke AGB-E08-09 with 29LV128DTMC-90Q support *(thanks LucentW for reporting)* +- Fixed a bug introduced in v1.1 that broke support for AGB-E08-09 with 29LV128DTMC-90Q *(thanks LucentW for reporting)* - Will now show the application’s version number in message boxes + +### v1.4 (released 2021-02-28) +- Added a command line interface (CLI) as an alternative to the GUI interface; see `--help` command line switch for details or run interactive mode with `--cli` +- Fixed some minor compatibility issues for older systems that only have access to slightly outdated versions of the PySide2 package +- Added support for DMG-DHCN-20 with MX29LV320ET *(thanks Veund)* +- Added the option to export Game Boy Camera pictures in more file formats +- Added support for SD007_BV5_DRV with S29GL032M90TFIR4 +- Confirmed support for SD007_BV5_DRV with MX29LV320BTC +- Added support for SD007_K8D3216_32M with MX29LV160CT *(thanks marv17)* +- Added support for AGB-E05-02 with M29W128FH +- Reading the sector map from CFI is experimental and can be enabled by adding `"sector_size_from_cfi":true,` to a flash cartridge config file +- Several flash cartridge types can now be written via full chip erase mode +- For specifying a specific MBC for writing to a DIY flash cartridge, it is now possible to add `"mbc":3,` or the like to its config file +- Removed officially unused Game Boy MBC types and ROM sizes from the drop down lists +- Added support for AGB-E05-01 with MX29GL128FHT2I-90G *(thanks antPL)* +- Added support for official cartridges with the HuC-1 memory bank controller; tested with “Pokémon Card GB” (DMG-ACXJ-JPN) +- Added support for official cartridges with the HuC-3 memory bank controller; tested with “Robot Poncots Sun Version” (DMG-HREJ-JPN) +- Added support for official cartridges with the TAMA5 memory bank controller; tested with “Game de Hakken!! Tamagotchi Osutchi to Mesutchi” (DMG-AOMJ-JPN) (requires GBxCart RW firmware R26 or higher) +- Added preliminary support for official GBA Video cartridges with 3D Memory; tested with “Shrek 2” (AGB-M2SE-USA) *(thanks to endrift’s article [“Dumping the Undumped”](https://mgba.io/2015/10/20/dumping-the-undumped/))* – requires a future firmware update of GBxCart RW +- Added support for optionally saving and restoring RTC registers of official TAMA5 cartridges inside the save file +- Experimental support for optionally saving RTC registers of official MBC3+RTC+SRAM+BATTERY cartridges inside the save file using the 48 bytes save format explained on the [BGB website](https://bgb.bircd.org/rtcsave.html) was added. Latching the RTC register and restoring RTC register values to the cartridge is not supported at this time as it requires a new GBxCart RW hardware device revision. +- Added support for 4050_4400_4000_4350_36L0R_V5 with 4050L0YTQ2 *(thanks Shinichi999)* +- Fixed GUI support on macOS Big Sur *(thanks paarongiroux)* +- Added support for official cartridges with the MBC1M memory bank controller; tested with “Bomberman Collection” (DMG-ABCJ-JPN); save data backup is untested but should work +- Added support for official cartridges with the MMM01 memory bank controller; tested with “Momotarou Collection 2” (DMG-AM3J-JPN) (requires GBxCart RW firmware R26 or higher) +- Support for optionally saving and restoring RTC registers of official HuC-3+RTC+SRAM+BATTERY cartridges inside the save file using the 12 bytes save format used by the [hhugboy emulator](https://github.com/tzlion/hhugboy) was added. +- Added support for SD007_TSOP_48BALL with K8D3216UTC *(thanks marv17)* +- Added ROM and map backup support for official Nintendo Power GB Memory cartridges (DMG-MMSA-JPN); save data handling and ROM writing is not supported yet diff --git a/setup.py b/setup.py index 7378301..1e98b4f 100644 --- a/setup.py +++ b/setup.py @@ -4,9 +4,9 @@ with open("README.md", "r", encoding="utf-8") as fh: long_description = fh.read( setuptools.setup( name="FlashGBX", - version="1.3", + version="1.4", author="Lesserkuma", - description="A GUI application that can read and write Game Boy and Game Boy Advance cartridge data. Currently supports the GBxCart RW hardware device by insideGadgets.", + description="Reads and writes Game Boy and Game Boy Advance cartridge data. Currently supports the GBxCart RW hardware device by insideGadgets.", url="https://github.com/lesserkuma/FlashGBX", packages=setuptools.find_packages(), install_requires=['PySide2', 'pyserial', 'setuptools', 'requests', 'Pillow'],