mirror of
https://github.com/lesserkuma/FlashGBX.git
synced 2026-04-24 23:37:34 -05:00
0.10
This commit is contained in:
parent
3ba60c38de
commit
9404d7e096
BIN
.github/FlashGBX_Ubuntu.png
vendored
BIN
.github/FlashGBX_Ubuntu.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 68 KiB |
BIN
.github/FlashGBX_Windows.png
vendored
BIN
.github/FlashGBX_Windows.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 16 KiB |
BIN
.github/FlashGBX_macOS.png
vendored
BIN
.github/FlashGBX_macOS.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 74 KiB |
|
|
@ -1,19 +1,21 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# 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 §or 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 §or 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("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
|
||||
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
448
FlashGBX/PocketCamera.py
Normal 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 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 = []
|
||||
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)
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
53
FlashGBX/config/fc_AGB_K8D6316UTM-PI07.txt
Normal file
53
FlashGBX/config/fc_AGB_K8D6316UTM-PI07.txt
Normal 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 ]
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -19,7 +19,6 @@
|
|||
"voltage":3.3,
|
||||
"flash_size":0x1000000,
|
||||
"sector_size":0x20000,
|
||||
"chip_erase_timeout":75,
|
||||
"commands":{
|
||||
"reset":[
|
||||
[ 0, 0xF0 ]
|
||||
|
|
|
|||
84
FlashGBX/config/fc_AGB_iG_16MB_28EW256A.txt
Normal file
84
FlashGBX/config/fc_AGB_iG_16MB_28EW256A.txt
Normal 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 ]
|
||||
]
|
||||
}
|
||||
}
|
||||
84
FlashGBX/config/fc_AGB_iG_32MB_28EW256A.txt
Normal file
84
FlashGBX/config/fc_AGB_iG_32MB_28EW256A.txt
Normal 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 ]
|
||||
]
|
||||
}
|
||||
}
|
||||
68
FlashGBX/config/fc_AGB_iG_32MB_S29GL512N.txt
Normal file
68
FlashGBX/config/fc_AGB_iG_32MB_S29GL512N.txt
Normal 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 ]
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -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 ]
|
||||
|
|
|
|||
|
|
@ -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.
53
README.md
53
README.md
|
|
@ -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 doesn’t 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 doesn’t 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 don’t 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
|
||||
|
|
|
|||
6
setup.py
6
setup.py
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user