This commit is contained in:
Lesserkuma 2020-12-27 01:12:48 +01:00
parent 3ba60c38de
commit 9404d7e096
16 changed files with 1158 additions and 159 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 74 KiB

View File

@ -1,19 +1,21 @@
# -*- coding: utf-8 -*-
#
import sys, threading, os, glob, time, re, json, platform, subprocess, zlib, argparse, math, struct, statistics
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]
APPNAME = "FlashGBX"
VERSION = "0.9β"
VERSION_PEP440 = "0.10"
VERSION = "v{:s}".format(VERSION_PEP440)
class FlashGBX(QtWidgets.QWidget):
global APPNAME, VERSION
global APPNAME, VERSION, VERSION_PEP440
AGB_Header_ROM_Sizes = [ "4 MB", "8 MB", "16 MB", "32 MB" ]
AGB_Header_ROM_Sizes_Map = [ 0x400000, 0x800000, 0x1000000, 0x2000000 ]
@ -38,68 +40,15 @@ class FlashGBX(QtWidgets.QWidget):
CONFIG_PATH = ""
TBPROG = None # Windows 7+ Taskbar Progress Bar
PROGRESS = {}
#DEBUG = []
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)
def __init__(self, args):
app_path = args['app_path']
self.CONFIG_PATH = args['config_path']
QtWidgets.QWidget.__init__(self)
self.setStyleSheet("QMessageBox { messagebox-text-interaction-flags: 5; }")
self.setWindowIcon(QtGui.QIcon(app_path + "/res/icon.ico"))
self.setWindowTitle(APPNAME + " v" + VERSION)
self.setWindowFlags(self.windowFlags() | QtGui.Qt.MSWindowsFixedSizeDialogHint);
# Settings and Config
(config_version, fc_files) = self.ReadConfig(reset=args['argparsed'].reset)
if config_version != VERSION:
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, APPNAME, "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("ERROR: {:s} not found. This is required to read the flash cartridge type configuration.\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.setWindowTitle("{:s} {:s}".format(APPNAME, VERSION))
if hasattr(QtGui, "Qt"):
self.setWindowFlags(self.windowFlags() | QtGui.Qt.MSWindowsFixedSizeDialogHint)
# Create the QtWidgets.QVBoxLayout that lays out the whole form
self.layout = QtWidgets.QGridLayout()
@ -171,7 +120,7 @@ class FlashGBX(QtWidgets.QWidget):
self.layout_right.addWidget(self.grpActions)
# Transfer Status
grpStatus = QtWidgets.QGroupBox("Transfer Status")
self.grpStatus = QtWidgets.QGroupBox("Transfer Status")
grpStatusLayout = QtWidgets.QVBoxLayout()
grpStatusLayout.setContentsMargins(-1, 3, -1, -1)
@ -214,9 +163,9 @@ class FlashGBX(QtWidgets.QWidget):
rowStatus2.addWidget(self.btnCancel)
grpStatusLayout.addLayout(rowStatus2)
grpStatus.setLayout(grpStatusLayout)
self.grpStatus.setLayout(grpStatusLayout)
self.layout_right.addWidget(grpStatus)
self.layout_right.addWidget(self.grpStatus)
self.layout.addLayout(self.layout_left, 0, 0)
self.layout.addLayout(self.layout_right, 0, 1)
@ -230,31 +179,35 @@ class FlashGBX(QtWidgets.QWidget):
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("&Append date && time to filename of save data backups", lambda: self.SETTINGS.setValue("SaveFileNameAddDateTime", str(self.mnuConfig.actions()[0].isChecked()).lower().replace("true", "enabled").replace("false", "disabled")))
self.mnuConfig.addAction("Prefer &sector erase over full chip erase when available", lambda: self.SETTINGS.setValue("PreferSectorErase", str(self.mnuConfig.actions()[1].isChecked()).lower().replace("true", "enabled").replace("false", "disabled")))
self.mnuConfig.addAction("Enable &fast read mode (experimental)", lambda: self.SETTINGS.setValue("FastReadMode", str(self.mnuConfig.actions()[2].isChecked()).lower().replace("true", "enabled").replace("false", "disabled"))) # GBxCart RW
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 &sector erase over full chip erase when available", lambda: self.SETTINGS.setValue("PreferSectorErase", str(self.mnuConfig.actions()[2].isChecked()).lower().replace("true", "enabled").replace("false", "disabled")))
self.mnuConfig.addAction("Enable &fast read mode (experimental)", lambda: self.SETTINGS.setValue("FastReadMode", str(self.mnuConfig.actions()[3].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()[0].setChecked(self.SETTINGS.value("SaveFileNameAddDateTime") == "enabled")
self.mnuConfig.actions()[1].setCheckable(True)
self.mnuConfig.actions()[1].setChecked(self.SETTINGS.value("PreferSectorErase") == "enabled")
self.mnuConfig.actions()[2].setCheckable(True) # GBxCart RW
self.mnuConfig.actions()[2].setChecked(self.SETTINGS.value("FastReadMode") == "enabled") # GBxCart RW
self.mnuConfig.actions()[2].setCheckable(True)
self.mnuConfig.actions()[3].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.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.btnScan)
self.layout_devices.addWidget(self.btnConnect)
self.layout.addLayout(self.layout_devices, 1, 0, 1, 0)
@ -267,14 +220,88 @@ class FlashGBX(QtWidgets.QWidget):
self.btnFlashROM.setEnabled(False)
self.btnBackupRAM.setEnabled(False)
self.btnRestoreRAM.setEnabled(False)
self.btnConnect.setEnabled(False)
self.grpDMGCartridgeInfo.setEnabled(False)
self.grpAGBCartridgeInfo.setEnabled(False)
# Scan for devices
self.FindDevices()
# Set the VBox layout as the window's main layout
self.setLayout(self.layout)
# Read config, find devices and connect
self.InitConfig(args)
self.FindDevices()
# Show app window first, then do update check
qt_app.processEvents()
QtCore.QTimer.singleShot(1, lambda: [ self.UpdateCheck() ])
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", "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(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, APPNAME, "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("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)
def GuiCreateGroupBoxDMGCartInfo(self):
self.grpDMGCartridgeInfo = QtWidgets.QGroupBox("Game Boy Cartridge Information")
@ -378,7 +405,7 @@ class FlashGBX(QtWidgets.QWidget):
self.cmbHeaderFeaturesResult = QtWidgets.QComboBox()
self.cmbHeaderFeaturesResult.setStyleSheet("combobox-popup: 0;");
self.cmbHeaderFeaturesResult.view().setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
self.cmbHeaderFeaturesResult.addItems(self.DMG_Header_Features.values())
self.cmbHeaderFeaturesResult.addItems(list(self.DMG_Header_Features.values()))
rowHeaderFeatures.addWidget(self.cmbHeaderFeaturesResult)
group_layout.addLayout(rowHeaderFeatures)
@ -419,7 +446,7 @@ class FlashGBX(QtWidgets.QWidget):
group_layout.addLayout(rowAGBHeaderCode)
rowAGBHeaderVersion = QtWidgets.QHBoxLayout()
lblAGBHeaderVersion = QtWidgets.QLabel("Version:")
lblAGBHeaderVersion = QtWidgets.QLabel("Revision:")
lblAGBHeaderVersion.setContentsMargins(0, 1, 0, 1)
rowAGBHeaderVersion.addWidget(lblAGBHeaderVersion)
self.lblAGBHeaderVersionResult = QtWidgets.QLabel("")
@ -493,17 +520,78 @@ class FlashGBX(QtWidgets.QWidget):
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, APPNAME, "Would you like to automatically check for new versions at application startup?", QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
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}\n".format(devname))
print("Disconnected from {:s}".format(devname))
except:
pass
self.CONN = None
self.btnScan.show()
#self.btnScan.show()
self.optAGB.setEnabled(False)
self.optDMG.setEnabled(False)
self.grpDMGCartridgeInfo.setEnabled(False)
@ -539,7 +627,8 @@ class FlashGBX(QtWidgets.QWidget):
index = self.lblDevice.text()
if index not in self.DEVICES:
self.FindDevices()
self.FindDevices(True)
return
dev = self.DEVICES[index]
ret = dev.Initialize(self.FLASHCARTS)
@ -567,7 +656,7 @@ class FlashGBX(QtWidgets.QWidget):
if dev.IsConnected():
qt_app.processEvents()
self.CONN = dev
self.btnScan.hide()
#self.btnScan.hide()
self.optDMG.setAutoExclusive(False)
self.optAGB.setAutoExclusive(False)
if "DMG" in self.CONN.GetSupprtedModes():
@ -581,7 +670,7 @@ class FlashGBX(QtWidgets.QWidget):
self.btnConnect.setText("&Disconnect")
self.cmbDevice.setStyleSheet("QComboBox { border: 0; margin: 0; padding: 0; max-width: 0px; }");
self.lblDevice.setText(dev.GetFullName())
print("Connected to " + dev.GetFullName())
print("\nConnected to " + dev.GetFullName())
self.grpDMGCartridgeInfo.setEnabled(True)
self.grpAGBCartridgeInfo.setEnabled(True)
self.grpActions.setEnabled(True)
@ -604,9 +693,12 @@ class FlashGBX(QtWidgets.QWidget):
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.1)
global hw_devices
for hw_device in hw_devices:
@ -647,6 +739,9 @@ class FlashGBX(QtWidgets.QWidget):
self.cmbDevice.setStyleSheet("");
self.btnConnect.setEnabled(True)
#self.btnScan.setEnabled(True)
self.btnConnect.setEnabled(True)
if len(self.DEVICES) == 0: return False
return True
@ -661,23 +756,20 @@ class FlashGBX(QtWidgets.QWidget):
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=APPNAME, text="Operation complete!", standardButtons=QtWidgets.QMessageBox.Ok)
cb = QtWidgets.QCheckBox("Dont show this message again.", checked=False)
msgbox.setCheckBox(cb)
if self.CONN.INFO["last_action"] == 4: # Flash ROM
self.CONN.INFO["last_action"] = 0
t1 = self.lblStatus1aResult.text()
t2 = self.lblStatus2aResult.text()
t3 = self.lblStatus3aResult.text()
t4 = self.cmbDMGCartridgeTypeResult.currentIndex()
t5 = self.cmbAGBCartridgeTypeResult.currentIndex()
self.ReadCartridge()
self.lblStatus1aResult.setText(t1)
self.lblStatus2aResult.setText(t2)
self.lblStatus3aResult.setText(t3)
self.ReadCartridge(resetStatus=False)
self.lblStatus4a.setText("Done!")
self.cmbDMGCartridgeTypeResult.setCurrentIndex(t4)
self.cmbAGBCartridgeTypeResult.setCurrentIndex(t5)
msgbox = QtWidgets.QMessageBox(parent=self, icon=QtWidgets.QMessageBox.Information, windowTitle=APPNAME, text="ROM flashing complete!", standardButtons=QtWidgets.QMessageBox.Ok)
msgbox.exec()
#self.CONN.FlashROM(fncSetProgress=self.DEBUG[0], path=self.DEBUG[1], cart_type=self.DEBUG[2], override_voltage=self.DEBUG[3])
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
@ -687,8 +779,11 @@ class FlashGBX(QtWidgets.QWidget):
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=APPNAME, text="The ROM was dumped successfully!", standardButtons=QtWidgets.QMessageBox.Ok)
msgbox.exec()
#msgbox = QtWidgets.QMessageBox(parent=self, icon=QtWidgets.QMessageBox.Information, windowTitle=APPNAME, text="The ROM was dumped successfully!", standardButtons=QtWidgets.QMessageBox.Ok)
msgbox.setText("The ROM was dumped 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; }");
@ -699,8 +794,10 @@ class FlashGBX(QtWidgets.QWidget):
self.lblAGBHeaderROMChecksumResult.setText("Valid (0x{:06X})".format(self.AGB_Global_CRC32))
self.lblAGBHeaderROMChecksumResult.setStyleSheet("QLabel { color: green; }");
self.lblStatus4a.setText("Done!")
msgbox = QtWidgets.QMessageBox(parent=self, icon=QtWidgets.QMessageBox.Information, windowTitle=APPNAME, text="The ROM was dumped successfully!", standardButtons=QtWidgets.QMessageBox.Ok)
msgbox.exec()
msgbox.setText("The ROM was dumped 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"]))
@ -716,15 +813,41 @@ class FlashGBX(QtWidgets.QWidget):
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, APPNAME, "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)
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):
@ -769,19 +892,29 @@ class FlashGBX(QtWidgets.QWidget):
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 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][detected[0]]['flash_size'] / 1024 / 1024))
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][detected[0]]['flash_size'] / 1024 / 1024))
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][detected[0]]['flash_size'] / 1024 / 1024))
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\nNOTE: 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][detected[0]]['flash_size'] / 1024 / 1024))
msgbox = QtWidgets.QMessageBox(parent=self, icon=QtWidgets.QMessageBox.Question, windowTitle=APPNAME, text=msg_text)
button_ok = msgbox.addButton("&OK", QtWidgets.QMessageBox.ActionRole)
@ -873,7 +1006,6 @@ class FlashGBX(QtWidgets.QWidget):
self.lblHeaderROMChecksumResult.setStyleSheet(self.lblHeaderCGBResult.styleSheet())
self.lblAGBHeaderROMChecksumResult.setStyleSheet(self.lblHeaderCGBResult.styleSheet())
#self.DEBUG = [ self.SetProgress, path, mbc, rom_banks, rom_size, fast_read_mode ]
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=""):
@ -919,6 +1051,10 @@ class FlashGBX(QtWidgets.QWidget):
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, APPNAME, "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']:
@ -968,7 +1104,6 @@ class FlashGBX(QtWidgets.QWidget):
answer = QtWidgets.QMessageBox.warning(self, APPNAME, "Warning: The ROM file you selected may 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)
if answer == QtWidgets.QMessageBox.Cancel: return
#self.DEBUG = [ self.SetProgress, path, cart_type, override_voltage ]
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)
buffer = None
@ -1052,6 +1187,9 @@ class FlashGBX(QtWidgets.QWidget):
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, APPNAME, "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)
@ -1062,14 +1200,14 @@ class FlashGBX(QtWidgets.QWidget):
if not self.CONN.IsConnected():
self.DisconnectDevice()
self.DEVICES = {}
dontShowAgain = self.SETTINGS.value("AutoReconnect")
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=APPNAME, 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.setCheckBox(cb)
answer = msgbox.exec()
dontShowAgain = cb.isChecked()
if dontShowAgain: self.SETTINGS.setValue("AutoReconnect", dontShowAgain)
if dontShowAgain: self.SETTINGS.setValue("AutoReconnect", "enabled")
if answer == QtWidgets.QMessageBox.No:
return False
if self.FindDevices(True):
@ -1101,7 +1239,7 @@ class FlashGBX(QtWidgets.QWidget):
voltageWarning = ""
if self.CONN.CanSetVoltageAutomatically(): # device can switch in software
dontShowAgain = self.SETTINGS.value("SkipModeChangeWarning")
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
@ -1118,7 +1256,7 @@ class FlashGBX(QtWidgets.QWidget):
if mode == "DMG": self.optDMG.setChecked(True)
if mode == "AGB": self.optAGB.setChecked(True)
return False
if dontShowAgain: self.SETTINGS.setValue("SkipModeChangeWarning", dontShowAgain)
if dontShowAgain: self.SETTINGS.setValue("SkipModeChangeWarning", "enabled")
if not self.CheckDeviceAlive(setMode=setTo): return
@ -1137,7 +1275,7 @@ class FlashGBX(QtWidgets.QWidget):
self.grpDMGCartridgeInfo.setEnabled(True)
self.grpAGBCartridgeInfo.setEnabled(True)
def ReadCartridge(self):
def ReadCartridge(self, resetStatus=True):
if not self.CheckDeviceAlive(): return
data = self.CONN.ReadInfo()
@ -1262,12 +1400,15 @@ class FlashGBX(QtWidgets.QWidget):
self.grpDMGCartridgeInfo.setVisible(False)
self.grpAGBCartridgeInfo.setVisible(True)
self.lblStatus1aResult.setText("")
self.lblStatus2aResult.setText("")
self.lblStatus3aResult.setText("")
self.lblStatus4a.setText("Ready.")
self.FinishOperation()
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, APPNAME, "Unstable ROM reading detected. Please reconnect the device, make sure you selected the correct mode and that the cartridge contacts are clean.", QtWidgets.QMessageBox.Ok)
@ -1319,10 +1460,19 @@ class FlashGBX(QtWidgets.QWidget):
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"] == "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"):
@ -1340,11 +1490,6 @@ class FlashGBX(QtWidgets.QWidget):
elif args["action"] == "UPDATE_POS":
self.PROGRESS["pos"] = args["pos"]
#elif args["action"] in ("UPDATE_SPEED"): # so that speed displayed won't drop right after sector erase
# self.PROGRESS["speed_updated"] = True
# self.PROGRESS["time_last_update_speed"] = now
# self.PROGRESS["bytes_last_update_speed"] = self.PROGRESS["pos"]
elif args["action"] in ("READ", "WRITE"):
if "method" not in self.PROGRESS: return
elif args["action"] == "READ" and self.PROGRESS["method"] in ("SAVE_WRITE", "ROM_WRITE"): return
@ -1389,6 +1534,7 @@ class FlashGBX(QtWidgets.QWidget):
if self.PROGRESS["speed"] > self.PROGRESS["size"] / 1024:
self.PROGRESS["speed"] = self.PROGRESS["size"] / 1024
#self.grpStatus.setTitle("Transfer Status")
self.UpdateProgress(self.PROGRESS)
self.PROGRESS = {}
@ -1518,6 +1664,13 @@ class FlashGBX(QtWidgets.QWidget):
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()
@ -1616,9 +1769,10 @@ def main(portableMode=False):
args = parser.parse_args()
config_path = cp[args.cfgdir]
print(APPNAME + " v" + VERSION + " by Lesserkuma")
print("\n{: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 updates and troubleshooting visit https://github.com/lesserkuma/FlashGBX\n")
print("\nFor troubleshooting please visit https://github.com/lesserkuma/FlashGBX")
app = FlashGBX({"app_path":app_path, "config_path":config_path, "argparsed":args})
app.run()

448
FlashGBX/PocketCamera.py Normal file
View File

@ -0,0 +1,448 @@
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 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
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")
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.connect(self.optColorBW, QtCore.SIGNAL("clicked()"), self.SetColors)
self.optColorDMG = QtWidgets.QRadioButton("&DMG")
self.connect(self.optColorDMG, QtCore.SIGNAL("clicked()"), self.SetColors)
#self.optColorMGB = QtWidgets.QRadioButton("&MGB")
#self.connect(self.optColorMGB, QtCore.SIGNAL("clicked()"), self.SetColors)
self.optColorSGB = QtWidgets.QRadioButton("&SGB")
self.connect(self.optColorSGB, QtCore.SIGNAL("clicked()"), self.SetColors)
self.optColorCGB1 = QtWidgets.QRadioButton("CGB &1")
self.connect(self.optColorCGB1, QtCore.SIGNAL("clicked()"), self.SetColors)
self.optColorCGB2 = QtWidgets.QRadioButton("CGB &2")
self.connect(self.optColorCGB2, QtCore.SIGNAL("clicked()"), self.SetColors)
self.optColorCGB3 = QtWidgets.QRadioButton("CGB &3")
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 couldnt 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 = []
ORDER = None
def __init__(self):
pass
def LoadFile(self, savefile):
if os.path.getsize(savefile) != 128*1024: return False
with open(savefile, "rb") as file: self.DATA = file.read()
if self.DATA[0x1FFB1:0x1FFB6] != b'Magic':
self.DATA = None
return False
order_raw = self.DATA[0x11D7:0x11F5]
order = [None] * 30
deleted = []
for i in range(0, 30):
if order_raw[i] == 0xFF:
deleted.append(i)
else:
order[order_raw[i]] = i
while None in order: order.remove(None)
order.extend(deleted)
self.ORDER = order
self.IMAGES_DELETED = deleted
self.IMAGES[0] = self.ExtractGameFace()
for i in range(0, 30):
self.IMAGES[i+1] = self.ExtractPicture(i)
return True
def SetPalette(self, palette):
for p in range (0, len(self.IMAGES)):
self.IMAGES[p].putpalette(palette)
self.PALETTE = palette
def GetPicture(self, index):
return self.IMAGES[index]
def IsEmpty(self, index):
return (hashlib.sha1(self.IMAGES[index].tobytes()).digest() == b'\xefX\xa8\x12\xa8\x1a\xb1EI\xd8\xf4\xfb\x86\xe9\xec\xb5J_\xb7#')
def IsDeleted(self, index):
index = self.ORDER[index-1]
return index in self.IMAGES_DELETED
def ConvertPicture(self, buffer):
tile_width = 16
tile_height = 14
img = Image.new(mode='P', size=(128, 112))
img.putpalette(self.PALETTE)
pixels = img.load()
for h in range(tile_height):
for w in range(tile_width):
tile_pos = 16 * ((h * tile_width) + w)
tile = buffer[tile_pos:tile_pos+16]
for i in range(8):
for j in range(8):
hi = (tile[i * 2] >> (7 - j)) & 1
lo = (tile[i * 2 + 1] >> (7 - j)) & 1
pixels[(w * 8) + j, (h * 8) + i] = (lo << 1 | hi)
return img
def ExtractGameFace(self):
offset = 0x11FC
imgbuffer = self.DATA[offset:offset+0x1000]
return self.ConvertPicture(imgbuffer)
def ExtractPicture(self, index):
index = self.ORDER[index]
offset = 0x2000 + (index * 0x1000)
imgbuffer = self.DATA[offset:offset+0x1000]
return self.ConvertPicture(imgbuffer)

View File

@ -1,9 +1,11 @@
{
"type":"AGB",
"names":[
"28F256L03B-DRV with 256L30B"
"28F256L03B-DRV with 256L30B",
"4400 with 4400L0ZDQ0"
],
"flash_ids":[
[ 0x8A, 0x00, 0x15, 0x88 ],
[ 0x8A, 0x00, 0x15, 0x88 ]
],
"voltage":3.3,

View File

@ -0,0 +1,53 @@
{
"type":"AGB",
"names":[
"BX2006_TSOPBGA_0106 with K8D6316UTM-PI07"
],
"flash_ids":[
[ 0xEC, 0x00, 0xE0, 0x22 ]
],
"voltage":3.3,
"flash_size":0x800000,
"sector_size":[
[0x10000, 127],
[0x2000, 8]
],
"commands":{
"reset":[
[ 0, 0xF0 ]
],
"read_identifier":[
[ 0xAAA, 0xA9 ],
[ 0x555, 0x56 ],
[ 0xAAA, 0x90 ]
],
"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", 0xFFFF, 0xFFFF ]
],
"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 ]
]
}
}

View File

@ -19,7 +19,6 @@
"voltage":3.3,
"flash_size":0x1000000,
"sector_size":0x20000,
"chip_erase_timeout":75,
"commands":{
"reset":[
[ 0, 0xF0 ]

View File

@ -0,0 +1,84 @@
{
"type":"AGB",
"names":[
"insideGadgets 16 MB (28EW256A) + RTC/Solar"
],
"flash_ids":[
[ 0x89, 0x00, 0x7E, 0x22 ]
],
"voltage":3.3,
"flash_size":0x1000000,
"sector_size":0x20000,
"chip_erase_timeout":300,
"single_write_first_256_bytes":true,
"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, 0xFFFF, 0xFFFF ]
],
"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", 0xFFFF, 0xFFFF ]
],
"buffer_write":[
[ 0xAAA, 0xAA ],
[ 0x555, 0x55 ],
[ "SA", 0x25 ],
[ "SA", "BS" ],
[ "PA", "PD" ],
[ "SA", 0x29 ]
],
"buffer_write_wait_for":[
[ null, null, null ],
[ null, null, null ],
[ null, null, null ],
[ null, null, null ],
[ null, null, null ],
[ "SA", "PD", 0xFFFF ]
],
"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 ]
]
}
}

View File

@ -0,0 +1,84 @@
{
"type":"AGB",
"names":[
"insideGadgets 32 MB (28EW256A) + RTC/Rumble"
],
"flash_ids":[
[ 0x89, 0x00, 0x7E, 0x22 ]
],
"voltage":3.3,
"flash_size":0x2000000,
"sector_size":0x20000,
"chip_erase_timeout":300,
"single_write_first_256_bytes":true,
"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, 0xFFFF, 0xFFFF ]
],
"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", 0xFFFF, 0xFFFF ]
],
"buffer_write":[
[ 0xAAA, 0xAA ],
[ 0x555, 0x55 ],
[ "SA", 0x25 ],
[ "SA", "BS" ],
[ "PA", "PD" ],
[ "SA", 0x29 ]
],
"buffer_write_wait_for":[
[ null, null, null ],
[ null, null, null ],
[ null, null, null ],
[ null, null, null ],
[ null, null, null ],
[ "SA", "PD", 0xFFFF ]
],
"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 ]
]
}
}

View File

@ -0,0 +1,68 @@
{
"type":"AGB",
"names":[
"insideGadgets 32 MB (S29GL512N) + RTC"
],
"flash_ids":[
[ 0x01, 0x00, 0x7E, 0x22 ]
],
"voltage":3.3,
"flash_size":0x2000000,
"sector_size":0x20000,
"chip_erase_timeout":300,
"single_write_first_256_bytes":true,
"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, 0xFFFF, 0xFFFF ]
],
"buffer_write":[
[ 0xAAA, 0xAA ],
[ 0x555, 0x55 ],
[ "SA", 0x25 ],
[ "SA", "BS" ],
[ "PA", "PD" ],
[ "SA", 0x29 ]
],
"buffer_write_wait_for":[
[ null, null, null ],
[ null, null, null ],
[ null, null, null ],
[ null, null, null ],
[ null, null, null ],
[ "SA", "PD", 0xFFFF ]
],
"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 ]
]
}
}

View File

@ -3,9 +3,11 @@
"names":[
"SD007_BV5_V3 with AM29LV160MB",
"SD007_TSOP_48BALL with L160DB12VI",
"SD007_TSOP_48BALL with AM29LV160DB",
"SD007_TSOP_48BALL with AM29LV160DT"
],
"flash_ids":[
[ 0x02, 0x02, 0x4A, 0x4A ],
[ 0x02, 0x02, 0x4A, 0x4A ],
[ 0x02, 0x02, 0x4A, 0x4A ],
[ 0x02, 0x02, 0xC4, 0xC4 ]

View File

@ -11,7 +11,7 @@ from .Util import *
class GbxDevice:
DEVICE_NAME = "GBxCart RW"
DEVICE_MIN_FW = 19
DEVICE_MAX_FW = 23
DEVICE_MAX_FW = 24
DEVICE_CMD = {
"CART_MODE":'C',
@ -65,6 +65,7 @@ class GbxDevice:
"GB_FLASH_WRITE_BYTE":'F',
"GB_FLASH_WRITE_64BYTE":'T',
"GB_FLASH_WRITE_256BYTE":'X',
"GB_FLASH_WRITE_UNBUFFERED_256BYTE":'{',
"GB_FLASH_WRITE_BUFFERED_32BYTE":'Y',
"GB_FLASH_BANK_1_COMMAND_WRITES":'N',
"GB_FLASH_WRITE_64BYTE_PULSE_RESET":'J',
@ -76,6 +77,7 @@ class GbxDevice:
"GBA_FLASH_WRITE_BUFFERED_256BYTE":'c',
"GBA_FLASH_WRITE_BUFFERED_256BYTE_SWAPPED_D0D1":'d',
"GBA_FLASH_WRITE_INTEL_64BYTE":'l',
"GBA_FLASH_WRITE_INTEL_256BYTE":';',
"GBA_FLASH_WRITE_INTEL_64BYTE_WORD":'u',
"GBA_FLASH_WRITE_INTEL_INTERLEAVED_256BYTE":'v',
"GBA_FLASH_WRITE_SHARP_64BYTE":'x',
@ -269,7 +271,9 @@ class GbxDevice:
if timeout < 1:
print("Failed.")
traceback.print_stack()
print("\nwait_for_ack(): Skipping...")
#print("\nwait_for_ack(): Skipping...")
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."}
return False
else:
print("Retrying...")
@ -306,7 +310,8 @@ class GbxDevice:
elif length == 0x10000 or length == 0x4000:
mbuffer = bytearray()
for i in range(0, length, 64):
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))
@ -385,7 +390,10 @@ class GbxDevice:
if buffer == False:
if lives == 0:
self.CANCEL = True
self.CANCEL_ARGS = {"info_type":"msgbox_critical", "info_msg":"A critical error occured while trying to read the cartridge ROM. Please try again from the beginning."}
if (self.MODE == "DMG" and length == 0x4000) or (self.MODE == "AGB" and length == 0x10000):
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)
return False
elif lives != 5:
@ -418,7 +426,7 @@ class GbxDevice:
ack = self.wait_for_ack()
if ack == False:
self.CANCEL = True
self.CANCEL_ARGS = {"info_type":"msgbox_critical", "info_msg":"A critical error occured while trying to write the ROM. Please try again from the beginning."}
self.CANCEL_ARGS = {"info_type":"msgbox_critical", "info_msg":"A critical error occured while trying to write the ROM. Please re-connect the device and try again from the beginning."}
return False
def gb_flash_write_address_byte(self, address, data):
@ -430,7 +438,7 @@ class GbxDevice:
ack = self.wait_for_ack()
if ack == False:
self.CANCEL = True
self.CANCEL_ARGS = {"info_type":"msgbox_critical", "info_msg":"A critical error occured while trying to write a byte. Please try again from the beginning."}
self.CANCEL_ARGS = {"info_type":"msgbox_critical", "info_msg":"A critical error occured while trying to write a byte. Please re-connect the device and try again from the beginning."}
return False
def gbx_flash_write_data_bytes(self, command, data):
@ -839,6 +847,7 @@ class GbxDevice:
self.INFO["last_action"] = mode
time_start = time.time()
bank_size = 0x4000
self.CANCEL_ARGS = {}
# main work
if mode == 1: # Backup ROM
@ -944,15 +953,18 @@ class GbxDevice:
save_size = args["save_type"]
bank_count = int(max(save_size, bank_size) / bank_size)
self.EnableRAM(mbc=mbc, enable=True)
transfer_size = 64
startAddr = 0xA000
if mode == 2: # Backup
transfer_size = 512
else:
transfer_size = 64
elif self.MODE == "AGB":
bank_size = 0x10000
save_type = args["save_type"]
bank_count = 1
transfer_size = 64
eeprom_size = 0
transfer_size = 64
if save_type == 0:
return
@ -998,6 +1010,10 @@ class GbxDevice:
if maker_id == "ATMEL":
print("NOTE: For save data, this cartridge uses an ATMEL chip which is untested.")
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})
return False
# Prepare some stuff
if mode == 2: # Backup
@ -1011,11 +1027,13 @@ class GbxDevice:
elif mode == 3: # Restore
if args["erase"]: # Erase
data_import = save_size * b'\xFF'
self.INFO["save_erase"] = True
else:
with open(path, "rb") as file: data_import = file.read()
if save_size > len(data_import):
data_import += b'\xFF' * (save_size - len(data_import))
self.SetProgress({"action":"INITIALIZE", "method":"SAVE_WRITE", "size":save_size})
currAddr = 0
@ -1111,7 +1129,7 @@ class GbxDevice:
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["transfered"] = pos
self.INFO["transferred"] = pos
if self.MODE == "DMG":
self.EnableRAM(mbc=mbc, enable=False)
@ -1132,8 +1150,10 @@ class GbxDevice:
elif mode == 4: # Flash ROM
if self.MODE == "DMG":
supported_carts = list(self.SUPPORTED_CARTS['DMG'].values())
i_size = 0x4000
elif self.MODE == "AGB":
supported_carts = list(self.SUPPORTED_CARTS['AGB'].values())
i_size = 0x10000
if not isinstance(args["cart_type"], dict):
cart_type = "RETAIL"
@ -1147,6 +1167,13 @@ class GbxDevice:
with open(path, "rb") as file: data_import = file.read()
else:
data_import = args["buffer"]
# pad to next possible size
i = i_size
while len(data_import) > i: i += i_size
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"])
# Reset pins to avoid save data loss
@ -1417,6 +1444,7 @@ class GbxDevice:
self.SetProgress({"action":"ABORT", "info_type":"msgbox_critical", "info_msg":"Erasing a flash chip sector timed out. Please make sure the correct flash cartridge type is selected.", "abortable":False})
return False
if wait_for == data: break
self.SetProgress({"action":"SECTOR_ERASE", "time_start":time.time(), "abortable":True})
# Reset Flash
if "reset" in flashcart_meta["commands"]:
@ -1486,6 +1514,21 @@ class GbxDevice:
if int(currAddr % 0x8000) == 0:
self.set_number(currAddr / 2, self.DEVICE_CMD['SET_START_ADDRESS'])
# 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)):
skipping = True
else:
if skipping:
self.set_number(currAddr / 2, self.DEVICE_CMD['SET_START_ADDRESS'])
skipping = False
self.gbx_flash_write_data_bytes("GBA_FLASH_WRITE_INTEL_256BYTE", data)
self.wait_for_ack()
currAddr += 256
pos += 256
else:
data = data_import[pos:pos+64]
if data == bytearray([0xFF] * len(data)):
@ -1514,15 +1557,47 @@ class GbxDevice:
currAddr += 256
pos += 256
else:
dprint(flashcart_meta["commands"]["buffer_write"])
self.SetProgress({"action":"ABORT", "info_type":"msgbox_critical", "info_msg":"Buffer writing for this flash chip is not implemented yet.", "abortable":False})
# insideGadgets flash carts etc.
elif flashcart_meta["commands"]["buffer_write"] == [[0xAAA, 0xAA], [0x555, 0x55], ['SA', 0x25], ['SA', 'BS'], ['PA', 'PD'], ['SA', 0x29]]:
if "single_write_first_256_bytes" in flashcart_meta and flashcart_meta["single_write_first_256_bytes"] and currAddr < 256:
for i in range(0, len(flashcart_meta["commands"]["single_write"])):
addr = flashcart_meta["commands"]["single_write"][i][0]
data = flashcart_meta["commands"]["single_write"][i][1]
if addr == "PA": addr = int(currAddr)
if data == "PD": data = struct.unpack('H', data_import[pos:pos+2])[0]
self.gbx_flash_write_address_byte(addr, data)
currAddr += 2
pos += 2
data = bytearray([0] * 2)
if currAddr == 256:
self.set_number(currAddr / 2, self.DEVICE_CMD['SET_START_ADDRESS'])
else:
data = data_import[pos:pos+256]
if data == bytearray([0xFF] * len(data)):
skipping = True
else:
if skipping:
self.set_number(currAddr / 2, self.DEVICE_CMD['SET_START_ADDRESS'])
skipping = False
self.gbx_flash_write_data_bytes("GBA_FLASH_WRITE_BUFFERED_256BYTE", data)
self.wait_for_ack()
currAddr += 256
pos += 256
else: # TODO
self.SetProgress({"action":"ABORT", "info_type":"msgbox_critical", "info_msg":"Buffer writing for this flash chip is not implemented yet.\n\n{:s}".format(str(flashcart_meta["commands"]["buffer_write"])), "abortable":False})
return False
# TODO
elif "single_write" in flashcart_meta["commands"]:
if self.MODE == "DMG":
data = data_import[pos:pos+64]
# 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:
data = data_import[pos:pos+256]
if data == bytearray([0xFF] * len(data)):
skipping = True
else:
@ -1531,13 +1606,16 @@ class GbxDevice:
skipping = False
if "pulse_reset_after_write" in flashcart_meta and flashcart_meta["pulse_reset_after_write"]:
self.gbx_flash_write_data_bytes("GB_FLASH_WRITE_64BYTE_PULSE_RESET", data)
else:
elif len(data) == 64:
self.gbx_flash_write_data_bytes("GB_FLASH_WRITE_64BYTE", data)
elif len(data) == 256:
self.gbx_flash_write_data_bytes("GB_FLASH_WRITE_UNBUFFERED_256BYTE", data)
if not self.wait_for_ack():
self.SetProgress({"action":"ABORT", "info_type":"msgbox_critical", "info_msg":"Timeout error.", "abortable":False})
return False
currAddr += 64
pos += 64
currAddr += len(data)
pos += len(data)
elif self.MODE == "AGB":
# MSP55LV128 etc.

Binary file not shown.

View File

@ -17,10 +17,11 @@ by Lesserkuma
- Write new ROMs to a wide variety of Game Boy and Game Boy Advance flash cartridges
- Many reproduction cartridges and flash cartridges can be auto-detected
- A flash chip query can be performed for unsupported flash cartridges
- Decode and extract Game Boy Camera (Pocket Camera) photos from save data
### Confirmed working reader/writer hardware
- [insideGadgets GBxCart RW v1.3 and v1.3 Pro](https://www.gbxcart.com/) with firmware versions from R19 up to R23 (other hardware revisions and firmware versions may also work, but are untested)
- [insideGadgets GBxCart RW v1.3 and v1.3 Pro](https://www.gbxcart.com/) with firmware versions from R19 up to R24 (other hardware revisions and firmware versions may also work, but are untested)
### Currently supported flash cartridges
@ -39,6 +40,11 @@ by Lesserkuma
- Game Boy Advance
- Flash2Advance 256M (non-ultra variant, with 2× 28F128J3A150)
- insideGadgets 16 MB, 64K EEPROM with Solar and RTC options *(thanks AlexiG)*
- insideGadgets 32 MB, 1M FLASH with RTC option *(thanks AlexiG)*
- insideGadgets 32 MB, 512K FLASH *(thanks AlexiG)*
- insideGadgets 32 MB, 4K/64K EEPROM *(thanks AlexiG)*
- insideGadgets 32 MB, 256K FRAM with Rumble option *(thanks AlexiG)*
- Nintendo AGB Cartridge 128M Flash S, E201850
- Nintendo AGB Cartridge 256M Flash S, E201868
@ -59,13 +65,15 @@ by Lesserkuma
- SD007_48BALL_64M_V6 with 36VF3204
- SD007_48BALL_64M_V6 with 29DL163BD-90 *(thanks LovelyA72)*
- SD007_BV5_DRV with M29W320DT *(thanks Frost Clock)*
- SD007_BV5_V3 with 29LV160BE-90PFTN *(thanks LucentW)*
- SD007_BV5_V3 with HY29LV160BT-70 *(thanks LucentW)*
- SD007_BV5_V2 with HY29LV160TT *(thanks RevZ)*
- SD007_BV5_V2 with MX29LV320BTC *(thanks RevZ)*
- SD007_BV5_V3 with 29LV160BE-90PFTN *(thanks LucentW)*
- SD007_BV5_V3 with HY29LV160BT-70 *(thanks LucentW)*
- SD007_BV5_V3 with AM29LV160MB *(thanks RevZ)*
- SD007_TSOP_48BALL with 36VF3204
- SD007_TSOP_48BALL with AM29LV160DB *(thanks marv17)*
- SD007_TSOP_48BALL with AM29LV160DT *(thanks marv17)*
- SD007_TSOP_48BALL with M29W160ET *(thanks LucentW)*
- SD007_TSOP_48BALL with L160DB12VI *(thanks marv17)*
- Game Boy Advance
@ -76,6 +84,7 @@ by Lesserkuma
- 4050_4400_4000_4350_36L0R_V5 with M36L0R7050T
- 4050_4400_4000_4350_36L0R_V5 with M36L0T8060T
- 4050_4400_4000_4350_36L0R_V5 with M36L0R8060T
- 4400 with 4400L0ZDQ0 *(thanks Zeii)*
- 4455_4400_4000_4350_36L0R_V3 with M36L0R7050T
- AGB-E05-01 with GL128S
- AGB-E05-01 with MSP55LV128M
@ -85,17 +94,20 @@ by Lesserkuma
- BX2006_0106_NEW with S29GL128N10TFI01 *(thanks litlemoran)*
- BX2006_TSOP_64BALL with GL128S
- BX2006_TSOPBGA_0106 with M29W640GB6AZA6
- BX2006_TSOPBGA_0106 with K8D6316UTM-PI07 *(thanks LucentW)*
Many different reproduction cartridges share their flash chip command set, so even if yours is not on this list, it may still work fine or even be auto-detected as another one. Support for more cartridges can also be added by creating external config files that include the necessary flash chip commands.
## Installing and running
The application should work on pretty much every operating system that supports Qt-GUI applications built using [Python 3](https://www.python.org/downloads/) with [PySide2](https://pypi.org/project/PySide2/) and [pyserial](https://pypi.org/project/pyserial/) packages.
The application should work on pretty much every operating system that supports Qt-GUI applications built using [Python 3](https://www.python.org/downloads/) with [PySide2](https://pypi.org/project/PySide2/), [pyserial](https://pypi.org/project/pyserial/), [Pillow](https://pypi.org/project/Pillow/), [requests](https://pypi.org/project/requests/) and [setuptools](https://pypi.org/project/setuptools/) packages.
If you have Python and pip installed, you can use `pip install FlashGBX` to download and install the application. Then use `python -m FlashGBX` to run it.
If you have Python and pip installed, you can use `pip install FlashGBX` to download and install the application, or use `pip install --upgrade FlashGBX` to upgrade from an older version. Then use `python -m FlashGBX` to run it.
To run FlashGBX in portable mode, you can also download the source code archive and call `python run.py` after installing the prerequisites yourself.
*On some platforms you may have to use `pip3`/`python3` instead of `pip`/`python`.*
### Windows binaries
Available in the GitHub [Releases](https://github.com/lesserkuma/FlashGBX/releases) section:
@ -107,7 +119,7 @@ These executables have been created using *PyInstaller* and *Inno Setup*.
### Troubleshooting
* If something doesnt work as expected, first try to clean the game cartridge contacts (best with IPA 99%+ on a Q-tip) and reconnect the device.
* If something doesnt work as expected, first try to clean the game cartridge contacts (best with IPA 99.9%+ on a Q-tip) and reconnect the device.
* On Linux systems, you may run into a *Permission Error* problem when trying to connect to USB devices without *sudo* privileges. To grant yourself the necessary permissions temporarily, you can run `sudo chmod 0666 /dev/ttyUSB0` (replace with actual device path) before running the app. For a permanent solution, add yourself to the usergroup that has access to serial devices by default (e.g. *dialout* on Debian-based distros; `sudo adduser $USER dialout`) and then reboot the system.
@ -127,8 +139,11 @@ The author would like to thank the following very kind people for their help and
- AlexiG (GBxCart RW hardware, bug reports, flash chip info)
- AndehX (app icon, flash chip info)
- ClassicOldSong (fix for Raspberry Pi)
- djedditt (testing)
- easthighNerd (feature suggestions)
- Frost Clock (flash chip info)
- Icesythe7 (feature suggestions)
- JFox (help with properly packaging the app for pip)
- julgr (macOS help, testing)
- litlemoran (flash chip info)
@ -136,6 +151,7 @@ The author would like to thank the following very kind people for their help and
- LucentW (flash chip info, testing)
- marv17 (flash chip info, testing)
- RevZ (Linux help, testing, bug reports, flash chip info)
- Zeii (flash chip info)
## Changes
@ -164,8 +180,6 @@ The author would like to thank the following very kind people for their help and
- Added experimental support for GBxCart RW revisions other than v1.3 and fixed a crash when connecting to unknown revisions of the GBxCart RW
- The app is now available as a package and can be installed directly through *pip* *(thanks JFox)*
- Changed the way configuration files are stored (for details call with `--help` command line switch)
- ~~Added the option to write an automatically trimmed ROM file which can reduce flashing time, especially in Game Boy Advance mode (note that not all ROMs can be trimmed)~~
- ~~When dumping a flash cartridge that has been flashed with a trimmed ROM, the ROM will be fixed so checksums will still match up (can be disabled for debugging by adding `_notrimfix` to the file name)~~
- Added a button that opens a file browser to the currently used config directory for easy access
- Added the option to erase/wipe the save data on a cartridge
- Rearranged some buttons on the main window so that the newly added button popup menus dont block anything
@ -186,7 +200,7 @@ The author would like to thank the following very kind people for their help and
- Confirmed support for SD007_48BALL_64M_V2 with GL032M11BAIR4
- Added support for 4050_4400_4000_4350_36L0R_V5 with M36L0R8060T/M36L0T8060T
- Rewrote parts of the GBxCart RW interface code
- Removed the option to trim ROM files and will now instead just skip writing empty chunks of data
- When flashing ROM files, empty chunks of data will now be skipped
- Fixed config files for MSP55LV128 and MSP55LV128M flash chips
- Confirmed support for SD007_48BALL_64M_V6 with 36VF3204
- Confirmed support for SD007_TSOP_48BALL with 36VF3204
@ -204,14 +218,27 @@ The author would like to thank the following very kind people for their help and
- Added support for SD007_BV5_V3 with 29LV160BE-90PFTN *(thanks LucentW)*
- Added support for SD007_BV5_V3 with HY29LV160BT *(thanks LucentW)*
- Added support for SD007_48BALL_64M_V5 with 36VF3203 *(thanks LucentW)*
- Added support for SD007_TSOP_48BALL with M29W160ET70ZA6 *(thanks LucentW)*
- Added support for SD007_TSOP_48BALL with M29W160ET *(thanks LucentW)*
- Added support for AGB-E08-09 with 29LV128DTMC-90Q *(thanks LucentW)*
- Confirmed support for SD007_TSOP_48BALL with L160DB12VI *(thanks marv17)*
- Added support for SD007_TSOP_48BALL with AM29LV160DT *(thanks marv17)*
- Added support for SD007_BV5_DRV with M29W320DT *(thanks Frost Clock)*
- Added experimental *fast read mode* support for GBxCart RW v1.3 with firmware R19+ (about 20% faster)
- Bumped the required minimum firmware version of GBxCart RW v1.3 to R19
- Added experimental *Fast Read Mode* support for GBxCart RW with firmware R19+; can be up to 20% faster, but the functioning currently heavily relies on system performance and drivers
- Bumped the required minimum firmware version of GBxCart RW to R19
- Confirmed support for 4050_4400_4000_4350_36L0R_V5 with M36L0R7050T
- Added the option to enable the preference of sector erase over chip erase when flashing a ROM (this can improve flashing speed for ROMs smaller than the flash chip capacity)
- Added the option to enable the preference of sector erase over chip erase when flashing a ROM; this can improve flashing speed for ROMs smaller than the flash chip capacity
- Some flash chips may have reversed sectors despite shared flash ID; if you think your cartridge is affected, you can add `"sector_reversal":true,` to its config file for a prompt upon flashing
- Renamed config.ini to settings.ini to avoid confusion with the term “config file”
### v0.10 (released 2020-12-27)
- Fixed an issue with Raspberry Pi compatibility *(thanks ClassicOldSong)*
- Confirmed support for SD007_TSOP_48BALL with AM29LV160DB *(thanks marv17)*
- Fixed timeout errors with ROMs that have non-standard file sizes (e.g. trimmed files)
- Improved writing speed for most Game Boy reproduction cartridges by up to 40% (requires GBxCart RW firmware R24 or higher)
- Improved writing speed for M36L0R and similar flash chips by up to 80% (requires GBxCart RW firmware R24 or higher)
- Confirmed support for 4400 with 4400L0ZDQ0 *(thanks Zeii)*
- Backup and restore save data of flash chips manufactured by SANYO requires GBxCart RW firmware R24 or higher; a warning message for this will now be displayed in necessary cases
- Added the option to check for updates at application start *(thanks Icesythe7 and JFox)*
- Added support for BX2006_TSOPBGA_0106 with K8D6316UTM-PI07 *(thanks LucentW)*
- Added support for the currently available insideGadgets Game Boy Advance flash cartridges *(thanks AlexiG)*
- Added a Game Boy Camera (Pocket Camera) album viewer and picture extractor

View File

@ -4,15 +4,15 @@ with open("README.md", "r", encoding="utf-8") as fh: long_description = fh.read(
setuptools.setup(
name="FlashGBX",
version="0.9b0",
version="0.10",
author="Lesserkuma",
description="A GUI application that can read and write Game Boy and Game Boy Advance cartridge data. Currently supports the GBxCart RW hardware device by insideGadgets.",
url="https://github.com/lesserkuma/FlashGBX",
packages=setuptools.find_packages(),
install_requires=['PySide2', 'pyserial'],
install_requires=['PySide2', 'pyserial', 'setuptools', 'requests', 'Pillow'],
include_package_data=True,
classifiers=[
"Development Status :: 4 - Beta",
"Development Status :: 5 - Production/Stable",
"Environment :: X11 Applications :: Qt",
"Topic :: Terminals :: Serial",
"Operating System :: OS Independent",