From 47a334a143cf4f08a6f18e71c44d23f8950e72ce Mon Sep 17 00:00:00 2001 From: Lesserkuma Date: Tue, 18 Apr 2023 13:24:36 +0200 Subject: [PATCH] 3.26 --- CHANGES.md | 5 ++ FlashGBX/FlashGBX_GUI.py | 4 +- FlashGBX/PocketCameraWindow.py | 6 ++- FlashGBX/Util.py | 6 +-- FlashGBX/hw_GBxCartRW.py | 70 ++++++++++++++-------------- FlashGBX/res/config.zip | Bin 365590 -> 365590 bytes FlashGBX/res/fw_GBxCart_RW_v1_3.zip | Bin 15704 -> 15743 bytes README.md | 2 +- setup.py | 2 +- 9 files changed, 50 insertions(+), 45 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index e31d105..22ad66d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,4 +1,9 @@ # Release notes +### v3.26 (released 2023-04-18) +- Fixed a bug that made exporting Game Boy Camera pictures with a frame not work *(thanks Ell)* +- Fixed a bug with Game Boy ROM write verification on sectors smaller than 0x4000 bytes *(thanks KOOORAY)* +- Improved auto-detect for the BennVenn MBC3000 RTC cart + ### v3.25 (released 2023-04-14) - Bundles GBxCart RW v1.4/v1.4a firmware version R41+L9 (minor improvements) - Fixed an issue with the Game Boy Camera Album Viewer *(thanks CodyWick13)* diff --git a/FlashGBX/FlashGBX_GUI.py b/FlashGBX/FlashGBX_GUI.py index 6026f5b..8061828 100644 --- a/FlashGBX/FlashGBX_GUI.py +++ b/FlashGBX/FlashGBX_GUI.py @@ -1017,7 +1017,7 @@ class FlashGBX_GUI(QtWidgets.QWidget): if dontShowAgainCameraSavePopup: self.SETTINGS.setValue("SkipCameraSavePopup", "enabled") if answer == QtWidgets.QMessageBox.Yes: self.CAMWIN = None - self.CAMWIN = PocketCameraWindow(self, icon=self.windowIcon(), file=self.CONN.INFO["last_path"], config_path=Util.CONFIG_PATH) + self.CAMWIN = PocketCameraWindow(self, icon=self.windowIcon(), file=self.CONN.INFO["last_path"], config_path=Util.CONFIG_PATH, app_path=Util.APP_PATH) self.CAMWIN.setAttribute(QtCore.Qt.WA_DeleteOnClose, True) self.CAMWIN.setModal(True) self.CAMWIN.run() @@ -2690,7 +2690,7 @@ class FlashGBX_GUI(QtWidgets.QWidget): data = self.CONN.INFO["data"] self.CAMWIN = None - self.CAMWIN = PocketCameraWindow(self, icon=self.windowIcon(), file=data, config_path=Util.CONFIG_PATH) + self.CAMWIN = PocketCameraWindow(self, icon=self.windowIcon(), file=data, config_path=Util.CONFIG_PATH, app_path=Util.APP_PATH) self.CAMWIN.setAttribute(QtCore.Qt.WA_DeleteOnClose, True) self.CAMWIN.setModal(True) self.CAMWIN.run() diff --git a/FlashGBX/PocketCameraWindow.py b/FlashGBX/PocketCameraWindow.py index 0177534..f52aef9 100644 --- a/FlashGBX/PocketCameraWindow.py +++ b/FlashGBX/PocketCameraWindow.py @@ -17,6 +17,7 @@ class PocketCameraWindow(QtWidgets.QDialog): CUR_EXPORT_PATH = "." CUR_PC = None CUR_PALETTE = 3 + APP_PATH = "." CONFIG_PATH = "." APP = None PALETTES = [ @@ -28,12 +29,13 @@ class PocketCameraWindow(QtWidgets.QDialog): [ 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, config_path="."): + def __init__(self, app, file=None, icon=None, config_path=".", app_path="."): QtWidgets.QDialog.__init__(self) self.setAcceptDrops(True) if icon is not None: self.setWindowIcon(QtGui.QIcon(icon)) self.CUR_FILE = file self.CONFIG_PATH = config_path + self.APP_PATH = app_path self.setWindowTitle("FlashGBX – GB Camera Album Viewer") self.setWindowFlags((self.windowFlags() | QtCore.Qt.MSWindowsFixedSizeDialogHint) & ~QtCore.Qt.WindowContextHelpButtonHint) @@ -343,7 +345,7 @@ class PocketCameraWindow(QtWidgets.QDialog): frame = True own_frame = self.CONFIG_PATH + "/pc_frame.png" if not os.path.exists(own_frame): - shutil.copy(self.APP.APP_PATH + "/res/pc_frame.png", own_frame) + shutil.copy(self.APP_PATH + "/res/pc_frame.png", own_frame) with open(own_frame, "rb") as f: frame = f.read() if index == 31: frame = False # last seen image diff --git a/FlashGBX/Util.py b/FlashGBX/Util.py index f6fdd31..52b4b7a 100644 --- a/FlashGBX/Util.py +++ b/FlashGBX/Util.py @@ -7,9 +7,9 @@ from enum import Enum # Common constants APPNAME = "FlashGBX" -VERSION_PEP440 = "3.25" +VERSION_PEP440 = "3.26" VERSION = "v{:s}".format(VERSION_PEP440) -VERSION_TIMESTAMP = 1681495829 +VERSION_TIMESTAMP = 1681817120 DEBUG = False DEBUG_LOG = [] APP_PATH = "" @@ -809,4 +809,4 @@ def dprint(*args, **kwargs): DEBUG_LOG = DEBUG_LOG[-32768:] if DEBUG: msg = "{:s}{:s}".format(ANSI.CLEAR_LINE, msg) - #print(msg) + print(msg) diff --git a/FlashGBX/hw_GBxCartRW.py b/FlashGBX/hw_GBxCartRW.py index a1c9f51..8ade987 100644 --- a/FlashGBX/hw_GBxCartRW.py +++ b/FlashGBX/hw_GBxCartRW.py @@ -17,7 +17,7 @@ class GbxDevice: DEVICE_NAME = "GBxCart RW" DEVICE_MIN_FW = 1 DEVICE_MAX_FW = 9 - DEVICE_LATEST_FW_TS = { 4:1619427330, 5:1681395695, 6:1681395696 } + DEVICE_LATEST_FW_TS = { 4:1681739002, 5:1681395695, 6:1681395696 } DEVICE_CMD = { "NULL":0x30, @@ -114,7 +114,7 @@ class GbxDevice: FAST_READ = False SKIPPING = False BAUDRATE = 1000000 - MAX_BUFFER_LEN = 0x2000 + MAX_BUFFER_LEN = 0x800 DEVICE_TIMEOUT = 0.75 WRITE_DELAY = False READ_ERRORS = 0 @@ -144,14 +144,12 @@ class GbxDevice: if not self.LoadFirmwareVersion() and max_baud >= 1700000: dev.close() self.BAUDRATE = 1700000 - self.MAX_BUFFER_LEN = 0x2000 dev = serial.Serial(ports[i], self.BAUDRATE, timeout=0.1) self.DEVICE = dev - if not self.LoadFirmwareVersion(): + if not self.LoadFirmwareVersion() or self.FW["pcb_ver"] not in (5, 6, 101): dev.close() self.DEVICE = None self.BAUDRATE = 1000000 - self.MAX_BUFFER_LEN = 0x100 continue elif max_baud >= 1700000 and self.FW["pcb_ver"] in (5, 6, 101) and self.BAUDRATE < 1700000: self.ChangeBaudRate(baudrate=1700000) @@ -176,15 +174,17 @@ class GbxDevice: self.DEVICE = None conn_msg.append([3, "The GBxCart RW device on port " + ports[i] + " requires a firmware update to work with this software. Please try again after updating it to version L" + str(self.DEVICE_MIN_FW) + " or higher.

Firmware updates are available at https://www.gbxcart.com/."]) continue - #elif self.FW["fw_ver"] < self.DEVICE_MAX_FW: - # conn_msg.append([1, "The GBxCart RW device on port " + ports[i] + " is running an older firmware version. Please consider updating to version L" + str(self.DEVICE_MAX_FW) + " to make use of the latest features.

Firmware updates are available at https://www.gbxcart.com/."]) - elif self.FW["fw_ver"] > self.DEVICE_MAX_FW: + elif self.FW["fw_ts"] > self.DEVICE_LATEST_FW_TS[self.FW["pcb_ver"]]: conn_msg.append([0, "Note: The GBxCart RW device on port " + ports[i] + " is running a firmware version that is newer than what this version of FlashGBX was developed to work with, so errors may occur."]) - if (self.FW["pcb_ver"] not in (4, 5, 6, 101)): # only the v1.3, v1.4, v1.4a, Mini v1.1 PCB revisions are supported + if self.FW["pcb_ver"] not in (4, 5, 6, 101): # only the v1.3, v1.4, v1.4a, Mini v1.1 PCB revisions are supported dev.close() self.DEVICE = None continue + elif self.FW["pcb_ver"] in (5, 6, 101) and self.BAUDRATE > 1000000: + self.MAX_BUFFER_LEN = 0x2000 + else: + self.MAX_BUFFER_LEN = 0x800 conn_msg.append([0, "For help please visit the insideGadgets Discord: https://gbxcart.com/discord"]) @@ -213,7 +213,6 @@ class GbxDevice: self.DEVICE.reset_output_buffer() self._write(self.DEVICE_CMD["OFW_PCB_VER"]) temp = self.DEVICE.read(1) - #if len(temp) == 0: return False pcb = temp[0] if pcb == b'': return False self._write(self.DEVICE_CMD["OFW_FW_VER"]) @@ -255,12 +254,10 @@ class GbxDevice: self._write(self.DEVICE_CMD["OFW_USART_1_7M_SPEED"]) self.Close() self.BAUDRATE = baudrate - self.MAX_BUFFER_LEN = 0x2000 elif baudrate == 1000000: self._write(self.DEVICE_CMD["OFW_USART_1_0M_SPEED"]) self.Close() self.BAUDRATE = baudrate - self.MAX_BUFFER_LEN = 0x100 def CanSetVoltageManually(self): return False @@ -377,8 +374,10 @@ class GbxDevice: return self.FW["pcb_ver"] in (4, 5, 6) def FirmwareUpdateAvailable(self): - if self.FW["pcb_ver"] not in (5, 6): return False - return (self.FW["pcb_ver"] in (4, 5, 6) and self.FW["fw_ts"] < self.DEVICE_LATEST_FW_TS[self.FW["pcb_ver"]]) + if self.FW["pcb_ver"] not in (4, 5, 6): return False + if (self.FW["pcb_ver"] in (4, 5, 6) and self.FW["fw_ts"] < self.DEVICE_LATEST_FW_TS[self.FW["pcb_ver"]]): + if self.FW["pcb_ver"] == 4: self.FW_UPDATE_REQ = True + return True def GetFirmwareUpdaterClass(self): if self.FW["pcb_ver"] == 4: # v1.3 @@ -1115,7 +1114,10 @@ class GbxDevice: buffer += temp if not self.NO_PROG_UPDATE: self.SetProgress({"action":"READ", "bytes_added":len(temp)}) - + + if self.MODE == "DMG": + self._set_fw_variable("DMG_READ_CS_PULSE", 0) + return buffer def ReadRAM_MBC7(self, address, length): @@ -1168,6 +1170,8 @@ class GbxDevice: buffer.append(data) self.SetProgress({"action":"UPDATE_POS", "abortable":False, "pos":i+1}) + self._set_fw_variable("DMG_READ_CS_PULSE", 0) + self.NO_PROG_UPDATE = npu return buffer @@ -2128,11 +2132,8 @@ class GbxDevice: end_bank = math.ceil((buffer_pos + args["bl_size"]) / rom_bank_size) rom_banks = end_bank - dprint("start_address=0x{:X}, end_address=0x{:X}, start_bank=0x{:X}, rom_banks=0x{:X}".format(start_address, end_address, start_bank, rom_banks)) - + dprint("start_address=0x{:X}, end_address=0x{:X}, start_bank=0x{:X}, rom_banks=0x{:X}, buffer_len=0x{:X}, max_length=0x{:X}".format(start_address, end_address, start_bank, rom_banks, buffer_len, max_length)) bank = start_bank - - #for bank in range(0, rom_banks): while bank < rom_banks: # ↓↓↓ Switch ROM bank if self.MODE == "DMG": @@ -2141,17 +2142,16 @@ class GbxDevice: self._write(self.DEVICE_CMD["DMG_MBC_RESET"], wait=True) (start_address, bank_size) = _mbc.SelectBankROM(bank) end_address = start_address + bank_size - buffer_len = _mbc.GetROMBankSize() + buffer_len = min(buffer_len, _mbc.GetROMBankSize()) if "verify_write" in args: - buffer_len = min(buffer_len, bank_size) - #if "start_addr" in flashcart.CONFIG and bank == 0: start_address = flashcart.CONFIG["start_addr"] + buffer_len = min(buffer_len, bank_size, len(args["verify_write"])) end_address = start_address + bank_size start_address += (buffer_pos % rom_bank_size) if end_address > start_address + args["verify_len"]: end_address = start_address + args["verify_len"] - - # dprint("{:X}/{:X}/{:X}".format(start_address, bank_size, buffer_len)) elif self.MODE == "AGB": + if "verify_write" in args: + buffer_len = min(buffer_len, len(args["verify_write"])) if "flash_bank_select_type" in cart_type and cart_type["flash_bank_select_type"] > 0: flashcart.SelectBankROM(bank) temp = end_address - start_address @@ -2163,8 +2163,6 @@ class GbxDevice: pos = start_address lives = 20 - # dprint("pos_total=0x{:X}, start_address=0x{:X}, end_address=0x{:X}".format(pos_total, start_address, end_address)) - while pos < end_address: temp = bytearray() if self.CANCEL: @@ -2184,26 +2182,26 @@ class GbxDevice: temp = self.ReadROM_3DMemory(address=pos, length=buffer_len, max_length=max_length) else: if self.FW["fw_ver"] >= 9 and "verify_write" in args and (self.MODE != "AGB" or args["verify_base_pos"] > 0xC9): - # Verify mode (by checksum) - dprint("Checksum verification (verify_base_pos=0x{:X}, pos=0x{:X}, pos_total=0x{:X}, buffer_len=0x{:X})".format(args["verify_base_pos"], pos, pos_total, buffer_len)) + # Verify mode (by CRC32) + dprint("CRC32 verification (verify_base_pos=0x{:X}, pos=0x{:X}, pos_total=0x{:X}, buffer_len=0x{:X})".format(args["verify_base_pos"], pos, pos_total, buffer_len)) if self.MODE == "DMG": self._set_fw_variable("ADDRESS", pos) elif self.MODE == "AGB": self._set_fw_variable("ADDRESS", pos >> 1) self._write(self.DEVICE_CMD["CALC_CRC32"]) self._write(bytearray(struct.pack(">I", buffer_len))) - checksum_expected = zlib.crc32(args["verify_write"][pos_total:pos_total+buffer_len]) - checksum_calculated = struct.unpack(">I", self._read(4))[0] - dprint("Expected Checksum: 0x{:X}".format(checksum_expected)) - dprint("Calculated Checksum: 0x{:X}".format(checksum_calculated)) - if checksum_expected == checksum_calculated: + crc32_expected = zlib.crc32(args["verify_write"][pos_total:pos_total+buffer_len]) + crc32_calculated = struct.unpack(">I", self._read(4))[0] + dprint("Expected CRC32: 0x{:X}".format(crc32_expected)) + dprint("Calculated CRC32: 0x{:X}".format(crc32_calculated)) + if crc32_expected == crc32_calculated: pos += buffer_len pos_total += buffer_len - dprint("Verification successful between 0x{:X} and 0x{:X}".format(pos_total-buffer_len, pos_total)) + dprint("CRC32 verification successful between 0x{:X} and 0x{:X}".format(pos_total-buffer_len, pos_total)) self.SetProgress({"action":"UPDATE_POS", "pos":args["verify_from"]+pos_total}) continue else: - dprint("Mismatch during verification between 0x{:X} and 0x{:X}".format(pos_total, pos_total+buffer_len)) + dprint("Mismatch during CRC32 verification between 0x{:X} and 0x{:X}".format(pos_total, pos_total+buffer_len)) return pos_total else: # Normal read @@ -3468,7 +3466,7 @@ class GbxDevice: if self.MODE == "DMG" and _mbc.HasFlashBanks(): _mbc.SelectBankFlash(bank) time.sleep(delay) if self.DEVICE is None: - raise ConnectionAbortedError("An critical connection error occured while writing 0x{:X} bytes at position 0x{:X} ({:s}). Please re-connect the device and try again from the beginning.".format(buffer_len, buffer_pos, Util.formatFileSize(size=buffer_pos, asInt=False))) + raise ConnectionAbortedError("A critical connection error occured while writing 0x{:X} bytes at position 0x{:X} ({:s}). Please re-connect the device and try again from the beginning.".format(buffer_len, buffer_pos, Util.formatFileSize(size=buffer_pos, asInt=False))) self.CANCEL_ARGS = {} self.CANCEL = False diff --git a/FlashGBX/res/config.zip b/FlashGBX/res/config.zip index 06652020d9f6e1c81036f325eea56dc8d732036e..43f18ab7131fe0703d27d263b06904a2df3fa4e9 100644 GIT binary patch delta 2045 zcmZ9NTS$~a6vweViOJ{0yONJ?cY(wgZitEPq7=Bm4{mvz_s-A&i~bzN`@ z!m^?i%MgVi0wW5dCoL!x#0ZK43j!^A5IW!Z&Cbleo%x;r`Oi5sv)@k6D3dda%N^j} z=icuQT!O1X`y-}3&rCrucSRS`za9PENkYw#uRHFZQsXK!Tx0R794jQf7z{#EAxp(@ zQ3By595^hX5LXGLF~uIJgFy151X6Xeb96^aGfE*{0*j@P#Xyh+%q&K=7%SwL1u|GN z$qF_WQ`?UVfnns66_O?TXc4KtjJMz|gDRc=J#9y413QbU$G8oQ48&C7NV_)BOTbi) z$D)-}#oKahUIe+zxZ`>`1QTNg#7L>B3hYDFo_dAa!kK2|3aZNg*#b;BmflEkN_Jwu6OverWgkQ%_?D zN-~2R-v$AgbbOByD?%3@l%GJ*d9yiy?vUdc)! O550I;n&OWaBL4$*Sr@AS delta 2045 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

g$|ZAi1#&I~b`uR#`sX&mnEK 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 KgAIoL*ZV(H!8erv delta 7698 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