From 53dcb072d7dc7aa2cc5c921c710d9ba51e4c6d73 Mon Sep 17 00:00:00 2001 From: Lesserkuma Date: Sun, 28 Feb 2021 20:07:48 +0100 Subject: [PATCH] 1.4 --- .github/FlashGBX_Ubuntu.png | Bin 67433 -> 67733 bytes .github/FlashGBX_Windows.png | Bin 14574 -> 14395 bytes .github/FlashGBX_macOS.png | Bin 68383 -> 68369 bytes FlashGBX/DataTransfer.py | 8 +- FlashGBX/FlashGBX.py | 2011 ++--------------- FlashGBX/FlashGBX_CLI.py | 955 ++++++++ FlashGBX/FlashGBX_GUI.py | 1683 ++++++++++++++ FlashGBX/PocketCamera.py | 396 +--- FlashGBX/PocketCameraWindow.py | 362 +++ FlashGBX/RomFileAGB.py | 9 +- FlashGBX/RomFileDMG.py | 59 +- FlashGBX/Util.py | 294 ++- FlashGBX/config/db_AGB.json | 170 +- FlashGBX/config/fc_AGB_128W30B.txt | 2 +- FlashGBX/config/fc_AGB_29LV128DT.txt | 26 +- FlashGBX/config/fc_AGB_M36L0R8060T.txt | 6 +- FlashGBX/config/fc_AGB_MSP55LV128M.txt | 26 +- FlashGBX/config/fc_DMG_29DL161TD-90.txt | 5 +- FlashGBX/config/fc_DMG_29DL32TF-70.txt | 6 +- FlashGBX/config/fc_DMG_GB_Memory.txt | 97 + FlashGBX/config/fc_DMG_K8D3216UTC.txt | 73 + FlashGBX/config/fc_DMG_MX29LV160CT.txt | 75 + ...29LV320ABTC.txt => fc_DMG_MX29LV320ET.txt} | 128 +- FlashGBX/config/fc_DMG_iG_4MB.txt | 29 +- FlashGBX/hw_GBxCartRW.py | 856 +++++-- FlashGBX/res/config.zip | Bin 115852 -> 117044 bytes README.md | 211 +- setup.py | 4 +- 28 files changed, 4708 insertions(+), 2783 deletions(-) create mode 100644 FlashGBX/FlashGBX_CLI.py create mode 100644 FlashGBX/FlashGBX_GUI.py create mode 100644 FlashGBX/PocketCameraWindow.py create mode 100644 FlashGBX/config/fc_DMG_GB_Memory.txt create mode 100644 FlashGBX/config/fc_DMG_K8D3216UTC.txt create mode 100644 FlashGBX/config/fc_DMG_MX29LV160CT.txt rename FlashGBX/config/{fc_DMG_MX29LV320ABTC.txt => fc_DMG_MX29LV320ET.txt} (64%) diff --git a/.github/FlashGBX_Ubuntu.png b/.github/FlashGBX_Ubuntu.png index da1c6b14ea31515a2ec7522f65dad8cb89e6202b..8f8d7e07a42f69a300e22bb3630801202a8fd6e8 100644 GIT binary patch literal 67733 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�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

" + 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 5ec75ea5ee38439a76176d498909e46d7b39740d..c09ffeb89c98bae55c3d11d3faf26160963554ed 100644 GIT binary patch delta 95361 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

JvHgqt|5aYTXjTSg-TV)h&pg9o=JonN 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 SdE`fdXbQ3_GNn=<1O5-ILynXH literal 14574 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%0AxM^=_Y06fj3LnA6R8p=&%6H? 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 literal 68383 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 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: