This commit is contained in:
Lesserkuma 2024-11-07 13:23:21 +01:00
parent 8bebc6cf69
commit c7f1688551
45 changed files with 1539 additions and 274 deletions

BIN
.github/01.png vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 44 KiB

BIN
.github/build/Ubuntu/builddata.tar.gz vendored Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

82
.github/build/Windows/PySide2/setup.iss vendored Normal file
View File

@ -0,0 +1,82 @@
; Script generated by the Inno Setup Script Wizard.
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
#define MyAppName "FlashGBX"
#define MyAppVersion "<APP_VERSION>"
#define MyAppPublisher "Lesserkuma"
#define MyAppURL "https://github.com/lesserkuma/FlashGBX"
#define MyAppExeName "FlashGBX-app.exe"
#define MyFilesDir "<FILES_DIR>"
#define MyCH341Dir "<CH341_DIR>"
#define MyOutputDir "<OUTPUT_DIR>"
[Setup]
; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
AppId={{060FD4CF-E72F-497D-812E-EB5599D59A0A}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
AppVerName={#MyAppName} v{#MyAppVersion}
AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppURL}
AppSupportURL={#MyAppURL}
AppUpdatesURL={#MyAppURL}
DefaultDirName={autopf}\{#MyAppName}
DisableProgramGroupPage=yes
;PrivilegesRequiredOverridesAllowed=dialog
OutputDir={#MyOutputDir}
OutputBaseFilename=Setup_FlashGBX
Compression=lzma2/ultra64
SolidCompression=yes
WizardStyle=modern
UninstallDisplayIcon={app}\FlashGBX-app.exe
UninstallDisplayName={#MyAppName} v{#MyAppVersion}
PrivilegesRequired=lowest
ArchitecturesAllowed=x64compatible
ArchitecturesInstallIn64BitMode=x64compatible
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
[Types]
Name: "full"; Description: "Install Application and Drivers"
Name: "custom"; Description: "Custom Installation"; Flags: iscustom
[Components]
Name: "program"; Description: "FlashGBX application"; Types: full custom; Flags: fixed
Name: "driver_ch341"; Description: "CH340/CH341 driver v3.8.2023.02 for GBxCart RW and GBFlash (install/re-install)"; Types: full
[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"
[Files]
Source: "{#MyFilesDir}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: program
Source: "{#MyCH341Dir}\*.*"; DestDir: "{app}\Drivers\CH341"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: driver_ch341
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
[Icons]
Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}";
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
[Run]
Filename: "{app}\Drivers\CH341\CH341SER.EXE"; Description: "Install CH340/CH341 driver"; Flags: waituntilterminated; Components: driver_ch341
Filename: "{app}\Python_3.8.10\python.exe"; Parameters: "-m pip install PySide2==5.15.2.1 --no-warn-script-location --isolated --cache-dir {app}\Python_3.8.10\cache"; StatusMsg: "Setting up prerequisites in embedded Python runtime (only used for FlashGBX)..."; Flags: waituntilterminated runminimized; Components: program
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
[InstallDelete]
Type: filesandordirs; Name: "{app}\Python";
Type: filesandordirs; Name: "{app}\Python_3.8.10";
Type: filesandordirs; Name: "{app}\*.pyd";
[Code]
procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
begin
if CurUninstallStep = usPostUninstall then
begin
DelTree(ExpandConstant('{app}'), True, True, True);
if MsgBox('Delete all config files of FlashGBX as well?', mbConfirmation, MB_YESNO or MB_DEFBUTTON2) = IDYES then
begin
DelTree(ExpandConstant('{localappdata}\{#MyAppName}'), True, True, True);
end;
end;
end;

Binary file not shown.

82
.github/build/Windows/PySide6/setup.iss vendored Normal file
View File

@ -0,0 +1,82 @@
; Script generated by the Inno Setup Script Wizard.
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
#define MyAppName "FlashGBX"
#define MyAppVersion "<APP_VERSION>"
#define MyAppPublisher "Lesserkuma"
#define MyAppURL "https://github.com/lesserkuma/FlashGBX"
#define MyAppExeName "FlashGBX-app.exe"
#define MyFilesDir "<FILES_DIR>"
#define MyCH341Dir "<CH341_DIR>"
#define MyOutputDir "<OUTPUT_DIR>"
[Setup]
; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
AppId={{060FD4CF-E72F-497D-812E-EB5599D59A0A}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
AppVerName={#MyAppName} v{#MyAppVersion}
AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppURL}
AppSupportURL={#MyAppURL}
AppUpdatesURL={#MyAppURL}
DefaultDirName={autopf}\{#MyAppName}
DisableProgramGroupPage=yes
;PrivilegesRequiredOverridesAllowed=dialog
OutputDir={#MyOutputDir}
OutputBaseFilename=Setup_FlashGBX
Compression=lzma2/ultra64
SolidCompression=yes
WizardStyle=modern
UninstallDisplayIcon={app}\FlashGBX-app.exe
UninstallDisplayName={#MyAppName} v{#MyAppVersion}
PrivilegesRequired=lowest
ArchitecturesAllowed=x64compatible
ArchitecturesInstallIn64BitMode=x64compatible
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
[Types]
Name: "full"; Description: "Install Application and Drivers"
Name: "custom"; Description: "Custom Installation"; Flags: iscustom
[Components]
Name: "program"; Description: "FlashGBX application"; Types: full custom; Flags: fixed
Name: "driver_ch341"; Description: "CH340/CH341 driver v3.8.2023.02 for GBxCart RW and GBFlash (install/re-install)"; Types: full
[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"
[Files]
Source: "{#MyFilesDir}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: program
Source: "{#MyCH341Dir}\*.*"; DestDir: "{app}\Drivers\CH341"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: driver_ch341
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
[Icons]
Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}";
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
[Run]
Filename: "{app}\Drivers\CH341\CH341SER.EXE"; Description: "Install CH340/CH341 driver"; Flags: waituntilterminated; Components: driver_ch341
Filename: "{app}\Python_3.10.11\python.exe"; Parameters: "-m pip install PySide6==6.5.0 --no-warn-script-location --isolated --cache-dir {app}\Python_3.10.11\cache"; StatusMsg: "Setting up prerequisites in embedded Python runtime (only used for FlashGBX)..."; Flags: waituntilterminated runminimized; Components: program
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
[InstallDelete]
Type: filesandordirs; Name: "{app}\Python";
Type: filesandordirs; Name: "{app}\Python_3.10.11";
Type: filesandordirs; Name: "{app}\*.pyd";
[Code]
procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
begin
if CurUninstallStep = usPostUninstall then
begin
DelTree(ExpandConstant('{app}'), True, True, True);
if MsgBox('Delete all config files of FlashGBX as well?', mbConfirmation, MB_YESNO or MB_DEFBUTTON2) = IDYES then
begin
DelTree(ExpandConstant('{localappdata}\{#MyAppName}'), True, True, True);
end;
end;
end;

51
.github/build/macOS/FlashGBX.spec vendored Normal file
View File

@ -0,0 +1,51 @@
# -*- mode: python ; coding: utf-8 -*-
a = Analysis(
['run.py'],
pathex=[],
binaries=[('FlashGBX/res/icon.ico', 'res')],
datas=[],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
[],
exclude_binaries=True,
name='FlashGBX',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=True,
icon=['FlashGBX/res/icon.ico'],
)
coll = COLLECT(
exe,
a.binaries,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name='FlashGBX',
)
info_plist = {
'CFBundleName': 'FlashGBX',
'CFBundleDisplayName': 'FlashGBX',
'CFBundleGetInfoString': 'Interface software for GB/GBC/GBA cart readers',
'CFBundleShortVersionString': '<APP_VERSION>',
'CFBundleIdentifier': 'com.lesserkuma.FlashGBX',
}
app = BUNDLE(
coll,
name='FlashGBX.app',
icon='FlashGBX/res/icon.ico',
bundle_identifier='com.lesserkuma.FlashGBX',
info_plist=info_plist,
)

118
.github/workflows/build-macos.yaml vendored Normal file
View File

@ -0,0 +1,118 @@
name: macOS
on:
release:
types: [published]
workflow_dispatch:
jobs:
build:
runs-on: ${{ matrix.os }}
permissions:
contents: write
env:
VERSION: ${{ github.event.release.tag_name || '0.0' }}
strategy:
fail-fast: false
matrix:
include:
- os: macos-latest
arch: arm64
- os: macos-13
arch: x86_64
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Create directory for Python packages
run: |
mkdir -p ${{ github.workspace }}/venv
- name: Cache Python packages
id: cache-pip
uses: actions/cache@v4
with:
path: |
${{ github.workspace }}/venv
key: flashgbx-macos-${{ matrix.arch }}-python-dependencies-2024110401
restore-keys: |
flashgbx-macos-${{ matrix.arch }}-python-dependencies-
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.9'
- name: Install Python packages
if: steps.cache-pip.outputs.cache-hit != 'true'
run: |
python -m venv ${{ github.workspace }}/venv
source ${{ github.workspace }}/venv/bin/activate
python -m pip install pyinstaller==6.11.0 Pillow==10.3.0 PySide6==6.7.2 pyserial==3.5 python-dateutil==2.9.0.post0 requests==2.32.3
- name: Build FlashGBX
run: |
cd "${{ github.workspace }}"
cp ".github/build/macOS/FlashGBX.spec" .
sed -i '' 's/<APP_VERSION>/${{ env.VERSION }}/g' "./FlashGBX.spec"
rm -r FlashGBX/config
source ${{ github.workspace }}/venv/bin/activate
pyinstaller FlashGBX.spec
mkdir dist/FlashGBX.app/Contents/MacOS/config
mkdir dist/FlashGBX.app/Contents/MacOS/res
cp -r FlashGBX/res/* dist/FlashGBX.app/Contents/MacOS/res
- name: Create DMG
run: |
brew install create-dmg
mkdir -p "${{ github.workspace }}/dist/dmg"
cp -r "${{ github.workspace }}/dist/FlashGBX.app" "${{ github.workspace }}/dist/dmg"
dmg_path="${{ github.workspace }}/dist/FlashGBX_${{ env.VERSION }}_macOS-${{ matrix.arch }}.dmg"
max_retries=5
retry_delay=10
for attempt in $(seq 1 $max_retries); do
if create-dmg \
--volname "FlashGBX" \
--volicon "${{ github.workspace }}/FlashGBX/res/icon.ico" \
--window-pos 200 120 \
--window-size 600 300 \
--icon-size 100 \
--icon "FlashGBX.app" 175 120 \
--hide-extension "FlashGBX.app" \
--app-drop-link 425 120 \
"$dmg_path" \
"${{ github.workspace }}/dist/dmg/"; then
echo "Successfully created image of FlashGBX v${{ env.VERSION }}."
break
else
echo "Failed to create DMG (attempt $attempt/$max_retries). Retrying in $retry_delay seconds..."
sleep $retry_delay
fi
if [[ $attempt -eq $max_retries ]]; then
echo "Error: Failed to create DMG after $max_retries attempts."
exit 1
fi
done
- name: Create artifact
if: env.VERSION == '0.0'
uses: actions/upload-artifact@v4
with:
name: FlashGBX_macOS-${{ matrix.arch }}
path: ${{ github.workspace }}/dist/FlashGBX_${{ env.VERSION }}_macOS-${{ matrix.arch }}.dmg
- name: Upload release asset
if: env.VERSION != '0.0'
uses: softprops/action-gh-release@v2.0.8
with:
tag_name: ${{ github.event.release.tag_name }}
files: ${{ github.workspace }}/dist/FlashGBX_${{ env.VERSION }}_macOS-${{ matrix.arch }}.dmg
draft: false
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

56
.github/workflows/build-ubuntu.yaml vendored Normal file
View File

@ -0,0 +1,56 @@
name: Ubuntu
on:
release:
types: [published]
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: write
env:
VERSION: ${{ github.event.release.tag_name || '0.0' }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Extract build data
run: |
mkdir -p "${{ github.workspace }}/dist/deb"
tar xzf ".github/build/Ubuntu/builddata.tar.gz" -C "${{ github.workspace }}/dist/deb"
sed -i 's/<APP_VERSION>/${{ env.VERSION }}/g' "${{ github.workspace }}/dist/deb/DEBIAN/control"
- name: Move FlashGBX files into deb build folder
run: |
rm -r "${{ github.workspace }}/FlashGBX/config"
mv "${{ github.workspace }}/FlashGBX" "${{ github.workspace }}/dist/deb"
- name: Install tools
run: |
sudo apt install devscripts dh-make debhelper
- name: Create DEB file
run: |
cd "${{ github.workspace }}/dist/deb"
dpkg-deb --build . "../FlashGBX_${{ env.VERSION }}_Ubuntu-all.deb"
- name: Create artifact
if: env.VERSION == '0.0'
uses: actions/upload-artifact@v4
with:
name: FlashGBX_Ubuntu-all
path: ${{ github.workspace }}/dist/FlashGBX_${{ env.VERSION }}_Ubuntu-all.deb
- name: Upload release asset
if: env.VERSION != '0.0'
uses: softprops/action-gh-release@v2.0.8
with:
tag_name: ${{ github.event.release.tag_name }}
files: ${{ github.workspace }}/dist/FlashGBX_${{ env.VERSION }}_Ubuntu-all.deb
draft: false
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -0,0 +1,59 @@
name: Windows Portable (Qt5)
on:
release:
types: [published]
workflow_dispatch:
jobs:
build:
runs-on: windows-latest
permissions:
contents: write
env:
VERSION: ${{ github.event.release.tag_name || '0.0' }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Copy files to temporary directory
run: |
$tempPath = "$env:RUNNER_TEMP\Temp"
mkdir "$tempPath" | Out-Null
& "C:\Program Files\7-Zip\7z.exe" x .\.github\build\Windows\PySide2\Python_3.8.10.7z -o"$env:RUNNER_TEMP\Temp"
xcopy ".\FlashGBX" "$tempPath\FlashGBX\" /s /i /y
$filesToCopy = @("CHANGES.md", "README.md", "LICENSE", "Third Party Notices.md")
foreach ($file in $filesToCopy) {
Copy-Item "$env:GITHUB_WORKSPACE\$file" "$env:RUNNER_TEMP\Temp\" -Force
}
if (Test-Path "$tempPath\FlashGBX\config") {
Remove-Item "$tempPath\FlashGBX\config\*" -Recurse -Force
}
- name: Create zip archive
run: |
$zipFileName = "FlashGBX_${{ env.VERSION }}_Windows_Portable_Qt5.zip"
& "C:\Program Files\7-Zip\7z.exe" a -tzip -mx=9 "$env:RUNNER_TEMP\$zipFileName" "$env:RUNNER_TEMP\Temp\*"
echo "Created zip file: $zipFileName"
- name: Create artifact
if: env.VERSION == '0.0'
uses: actions/upload-artifact@v4
with:
name: FlashGBX_Windows_Portable_Qt5
path: "${{ runner.temp }}\\FlashGBX_${{ env.VERSION }}_Windows_Portable_Qt5.zip"
- name: Upload release asset
if: env.VERSION != '0.0'
uses: softprops/action-gh-release@v2.0.8
with:
tag_name: ${{ github.event.release.tag_name }}
files: "${{ runner.temp }}\\FlashGBX_${{ env.VERSION }}_Windows_Portable_Qt5.zip"
draft: false
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -0,0 +1,59 @@
name: Windows Portable
on:
release:
types: [published]
workflow_dispatch:
jobs:
build:
runs-on: windows-latest
permissions:
contents: write
env:
VERSION: ${{ github.event.release.tag_name || '0.0' }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Copy files to temporary directory
run: |
$tempPath = "$env:RUNNER_TEMP\Temp"
mkdir "$tempPath" | Out-Null
& "C:\Program Files\7-Zip\7z.exe" x .\.github\build\Windows\PySide6\Python_3.10.11.7z -o"$env:RUNNER_TEMP\Temp"
xcopy ".\FlashGBX" "$tempPath\Python_3.10.11\Lib\site-packages\FlashGBX\" /s /i /y
$filesToCopy = @("CHANGES.md", "README.md", "LICENSE", "Third Party Notices.md")
foreach ($file in $filesToCopy) {
Copy-Item "$env:GITHUB_WORKSPACE\$file" "$env:RUNNER_TEMP\Temp\" -Force
}
if (Test-Path "$tempPath\Python_3.10.11\Lib\site-packages\FlashGBX\config") {
Remove-Item "$tempPath\Python_3.10.11\Lib\site-packages\FlashGBX\config\*" -Recurse -Force
}
- name: Create zip archive
run: |
$zipFileName = "FlashGBX_${{ env.VERSION }}_Windows_Portable.zip"
& "C:\Program Files\7-Zip\7z.exe" a -tzip -mx=9 "$env:RUNNER_TEMP\$zipFileName" "$env:RUNNER_TEMP\Temp\*"
echo "Created zip file: $zipFileName"
- name: Create artifact
if: env.VERSION == '0.0'
uses: actions/upload-artifact@v4
with:
name: FlashGBX_Windows_Portable
path: "${{ runner.temp }}\\FlashGBX_${{ env.VERSION }}_Windows_Portable.zip"
- name: Upload release asset
if: env.VERSION != '0.0'
uses: softprops/action-gh-release@v2.0.8
with:
tag_name: ${{ github.event.release.tag_name }}
files: "${{ runner.temp }}\\FlashGBX_${{ env.VERSION }}_Windows_Portable.zip"
draft: false
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -0,0 +1,91 @@
name: Windows Setup (Qt5)
on:
release:
types: [published]
workflow_dispatch:
jobs:
build:
runs-on: windows-latest
permissions:
contents: write
env:
VERSION: ${{ github.event.release.tag_name || '0.0' }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Copy files to temporary directory
run: |
$tempPath = "$env:RUNNER_TEMP\Temp"
mkdir "$tempPath" | Out-Null
& "C:\Program Files\7-Zip\7z.exe" x .\.github\build\Windows\PySide2\Python_3.8.10.7z -o"$env:RUNNER_TEMP\Temp"
xcopy ".\FlashGBX" "$tempPath\FlashGBX\" /s /i /y
$filesToCopy = @("CHANGES.md", "README.md", "LICENSE", "Third Party Notices.md")
foreach ($file in $filesToCopy) {
Copy-Item "$env:GITHUB_WORKSPACE\$file" "$env:RUNNER_TEMP\Temp\" -Force
}
if (Test-Path "$tempPath\FlashGBX\config") {
Remove-Item "$tempPath\FlashGBX\config\*" -Recurse -Force
}
- name: Configure Setup
run: |
$myAppVersion = "${{ env.VERSION }}"
$myFilesDir = "$env:RUNNER_TEMP\Temp"
$myCH341Dir = "$env:GITHUB_WORKSPACE\.github\build\Windows\Drivers\CH341"
$myOutputDir = "$env:RUNNER_TEMP\Setup"
mkdir $myOutputDir | Out-Null
$issFilePath = "$env:GITHUB_WORKSPACE\.github\build\Windows\PySide2\setup.iss"
(Get-Content $issFilePath) `
-replace "<APP_VERSION>", "$myAppVersion" `
-replace "<FILES_DIR>", "$myFilesDir" `
-replace "<CH341_DIR>", "$myCH341Dir" `
-replace "<OUTPUT_DIR>", "$myOutputDir" | Set-Content $issFilePath
$filesToCopy = @("CHANGES.md", "README.md", "LICENSE", "Third Party Notices.md")
foreach ($file in $filesToCopy) {
Copy-Item "$env:GITHUB_WORKSPACE\$file" "$myOutputDir" -Force
}
Rename-Item -Path "$myFilesDir\FlashGBX.exe" -NewName "FlashGBX-app.exe" -Force
- name: Install Inno Setup
run: |
$innoSetupUrl = "https://www.jrsoftware.org/download.php/is.exe"
$installerPath = "$env:RUNNER_TEMP\InnoSetupInstaller.exe"
Invoke-WebRequest -Uri $innoSetupUrl -OutFile $installerPath
Start-Process -FilePath $installerPath -ArgumentList "/SILENT" -Wait
- name: Run Inno Setup
run: |
$issFilePath = "$env:GITHUB_WORKSPACE\.github\build\Windows\PySide2\setup.iss"
& "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" $issFilePath
- name: Create zip archive
run: |
$zipFileName = "FlashGBX_${{ env.VERSION }}_Windows_Setup_Qt5.zip"
& "C:\Program Files\7-Zip\7z.exe" a -tzip -mx=9 "$env:RUNNER_TEMP\$zipFileName" "$env:RUNNER_TEMP\Setup\*"
echo "Created zip file: $zipFileName"
- name: Create artifact
if: env.VERSION == '0.0'
uses: actions/upload-artifact@v4
with:
name: FlashGBX_Windows_Setup_Qt5
path: "${{ runner.temp }}\\FlashGBX_${{ env.VERSION }}_Windows_Setup_Qt5.zip"
- name: Upload release asset
if: env.VERSION != '0.0'
uses: softprops/action-gh-release@v2.0.8
with:
tag_name: ${{ github.event.release.tag_name }}
files: "${{ runner.temp }}\\FlashGBX_${{ env.VERSION }}_Windows_Setup_Qt5.zip"
draft: false
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -0,0 +1,91 @@
name: Windows Setup
on:
release:
types: [published]
workflow_dispatch:
jobs:
build:
runs-on: windows-latest
permissions:
contents: write
env:
VERSION: ${{ github.event.release.tag_name || '0.0' }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Copy files to temporary directory
run: |
$tempPath = "$env:RUNNER_TEMP\Temp"
mkdir "$tempPath" | Out-Null
& "C:\Program Files\7-Zip\7z.exe" x .\.github\build\Windows\PySide6\Python_3.10.11.7z -o"$env:RUNNER_TEMP\Temp"
xcopy ".\FlashGBX" "$tempPath\Python_3.10.11\Lib\site-packages\FlashGBX\" /s /i /y
$filesToCopy = @("CHANGES.md", "README.md", "LICENSE", "Third Party Notices.md")
foreach ($file in $filesToCopy) {
Copy-Item "$env:GITHUB_WORKSPACE\$file" "$env:RUNNER_TEMP\Temp\" -Force
}
if (Test-Path "$tempPath\Python_3.10.11\Lib\site-packages\FlashGBX\config") {
Remove-Item "$tempPath\Python_3.10.11\Lib\site-packages\FlashGBX\config\*" -Recurse -Force
}
- name: Configure Setup
run: |
$myAppVersion = "${{ env.VERSION }}"
$myFilesDir = "$env:RUNNER_TEMP\Temp"
$myCH341Dir = "$env:GITHUB_WORKSPACE\.github\build\Windows\Drivers\CH341"
$myOutputDir = "$env:RUNNER_TEMP\Setup"
mkdir $myOutputDir | Out-Null
$issFilePath = "$env:GITHUB_WORKSPACE\.github\build\Windows\PySide6\setup.iss"
(Get-Content $issFilePath) `
-replace "<APP_VERSION>", "$myAppVersion" `
-replace "<FILES_DIR>", "$myFilesDir" `
-replace "<CH341_DIR>", "$myCH341Dir" `
-replace "<OUTPUT_DIR>", "$myOutputDir" | Set-Content $issFilePath
$filesToCopy = @("CHANGES.md", "README.md", "LICENSE", "Third Party Notices.md")
foreach ($file in $filesToCopy) {
Copy-Item "$env:GITHUB_WORKSPACE\$file" "$myOutputDir" -Force
}
Rename-Item -Path "$myFilesDir\FlashGBX.exe" -NewName "FlashGBX-app.exe" -Force
- name: Install Inno Setup
run: |
$innoSetupUrl = "https://www.jrsoftware.org/download.php/is.exe"
$installerPath = "$env:RUNNER_TEMP\InnoSetupInstaller.exe"
Invoke-WebRequest -Uri $innoSetupUrl -OutFile $installerPath
Start-Process -FilePath $installerPath -ArgumentList "/SILENT" -Wait
- name: Run Inno Setup
run: |
$issFilePath = "$env:GITHUB_WORKSPACE\.github\build\Windows\PySide6\setup.iss"
& "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" $issFilePath
- name: Create zip archive
run: |
$zipFileName = "FlashGBX_${{ env.VERSION }}_Windows_Setup.zip"
& "C:\Program Files\7-Zip\7z.exe" a -tzip -mx=9 "$env:RUNNER_TEMP\$zipFileName" "$env:RUNNER_TEMP\Setup\*"
echo "Created zip file: $zipFileName"
- name: Create artifact
if: env.VERSION == '0.0'
uses: actions/upload-artifact@v4
with:
name: FlashGBX_Windows_Setup
path: "${{ runner.temp }}\\FlashGBX_${{ env.VERSION }}_Windows_Setup.zip"
- name: Upload release asset
if: env.VERSION != '0.0'
uses: softprops/action-gh-release@v2.0.8
with:
tag_name: ${{ github.event.release.tag_name }}
files: "${{ runner.temp }}\\FlashGBX_${{ env.VERSION }}_Windows_Setup.zip"
draft: false
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -1,37 +0,0 @@
name: Trigger Remote Build macOS on Release
on:
release:
types: [created]
workflow_dispatch:
inputs:
release_tag:
description: 'Release tag'
required: true
type: string
jobs:
trigger-release:
if: github.event_name == 'release'
runs-on: ubuntu-latest
steps:
- name: Trigger FlashGBX Remote Build
uses: benc-uk/workflow-dispatch@v1
with:
workflow: FlashGBX Remote Build
token: ${{ secrets.FLASHGBX_MACOS_TOKEN }}
repo: Cliffback/FlashGBX-macOS
ref: main
inputs: '{"create_release": "true", "latest_version": "${{ github.event.release.tag_name }}"}'
trigger-manual:
if: github.event_name == 'workflow_dispatch' && github.event.inputs.release_tag != ''
runs-on: ubuntu-latest
steps:
- name: Trigger FlashGBX Remote Build
uses: benc-uk/workflow-dispatch@v1
with:
workflow: FlashGBX Remote Build
token: ${{ secrets.FLASHGBX_MACOS_TOKEN }}
repo: Cliffback/FlashGBX-macOS
ref: main
inputs: '{"create_release": "true", "latest_version": "${{ github.event.inputs.release_tag }}"}'

View File

@ -1,4 +1,14 @@
# Release notes
### v4.3 (released 2024-11-07)
- Added support for DVP DRV with MX29LV320CT *(thanks Mufsta)*
- Added support for insideGadgets MegaDuck 32K flash cart
- Added support for GBFlash 1M FLASH RTC (AGB-R1M-02V4)
- Added support for the unlicensed MBCX Mapper by Geeksimon
- Added support for GBFlash MBCX (8 MiB)
- Added support for GBFlash MBCX (32 MiB)
- Updated the Game Boy and Game Boy Advance lookup databases for save types, ROM sizes and checksums
- Minor bug fixes and improvements
### v4.2 (released 2024-08-04)
- Fixed an issue with MBC2 save data on Nintendo Power GB-Memory Cartridges (DMG-MMSA-JPN) *(thanks iyatemu)*
- Updated LK firmware to version L13 (fixes an issue with some FRAM-modded Game Boy cartridges) *(thanks jrharbort)*

View File

@ -5,7 +5,6 @@
import traceback
from serial import SerialException
from . import pyside as PySide2
from . import Util
from .Util import dprint
class DataTransfer(PySide2.QtCore.QThread):
@ -54,5 +53,5 @@ class DataTransfer(PySide2.QtCore.QThread):
if error is not None:
print(tb)
dprint(tb)
self.updateProgress.emit({"action":"ABORT", "info_type":"msgbox_critical", "fatal":True, "info_msg":"An unresolvable error has occured. See console output for more information. Reconnect the device, restart the software and try again.\n\n{:s}: {:s}".format(type(error).__name__, str(error)), "abortable":False})
self.updateProgress.emit({"action":"ABORT", "info_type":"msgbox_critical", "fatal":True, "info_msg":"An unresolvable error has occured. See the debug log file for more information. Reconnect the device, restart the software and try again.\n\n{:s}: {:s}".format(type(error).__name__, str(error)), "abortable":False})
self.FINISHED = True

View File

@ -133,7 +133,7 @@ def main(portableMode=False):
ap_cli1.add_argument("path", nargs="?", default="auto", help="target or source file path (optional when reading, required when writing)")
ap_cli2 = parser.add_argument_group('optional command line interface arguments')
ap_cli2.add_argument("--dmg-romsize", choices=["auto", "32kb", "64kb", "128kb", "256kb", "512kb", "1mb", "2mb", "4mb", "8mb", "16mb", "32mb"], type=str.lower, default="auto", help="set size of Game Boy cartridge ROM data")
ap_cli2.add_argument("--dmg-romsize", choices=["auto", "32kb", "64kb", "128kb", "256kb", "512kb", "1mb", "2mb", "4mb", "8mb", "16mb", "32mb", "64mb", "128mb"], type=str.lower, default="auto", help="set size of Game Boy cartridge ROM data")
ap_cli2.add_argument("--dmg-mbc", type=str.lower, default="auto", help="set memory bank controller type of Game Boy cartridge")
ap_cli2.add_argument("--dmg-savesize", choices=["auto", "4k", "16k", "64k", "256k", "512k", "1m", "eeprom2k", "eeprom4k", "tama5", "4m"], type=str.lower, default="auto", help="set size of Game Boy cartridge save data")
ap_cli2.add_argument("--agb-romsize", choices=["auto", "32kb", "64kb", "128kb", "256kb", "512kb", "1mb", "2mb", "4mb", "8mb", "16mb", "32mb", "64mb", "128mb", "256mb", "512mb"], type=str.lower, default="auto", help="set size of Game Boy Advance cartridge ROM data")

View File

@ -624,7 +624,8 @@ class FlashGBX_CLI():
header = self.CONN.ReadInfo()
self.ReadCartridge(header)
ret = self.CONN.DetectCartridge(limitVoltage=limitVoltage, checkSaveType=True)
self.CONN._DetectCartridge(args={"limitVoltage":limitVoltage, "checkSaveType":True})
ret = self.CONN.INFO["detect_cart"]
(header, _, save_type, save_chip, sram_unstable, cart_types, cart_type_id, cfi_s, _, flash_id, detected_size) = ret
# Save Type
@ -638,6 +639,8 @@ class FlashGBX_CLI():
supp_cart_types = self.CONN.GetSupportedCartridgesDMG()
elif self.CONN.GetMode() == "AGB":
supp_cart_types = self.CONN.GetSupportedCartridgesAGB()
else:
raise NotImplementedError
if len(cart_types) > 0:
cart_type = cart_type_id
@ -654,6 +657,7 @@ class FlashGBX_CLI():
# Save Type
msg_save_type_s = ""
temp = ""
if save_chip is not None:
temp = "{:s} ({:s})".format(Util.AGB_Header_Save_Types[save_type], save_chip)
else:
@ -695,6 +699,7 @@ class FlashGBX_CLI():
else:
if (len(flash_id.split("\n")) > 2) and ((self.CONN.GetMode() == "DMG") or ("dacs_8m" in header and header["dacs_8m"] is not True)):
msg_cart_type_s = "Cartridge Type: Unknown flash cartridge."
try_this = ""
if ("[ 0/90]" in flash_id):
try_this = "Generic Flash Cartridge (0/90)"
elif ("[ AAA/AA]" in flash_id):
@ -767,7 +772,7 @@ class FlashGBX_CLI():
print("{:s}Couldnt determine ROM size, will use 8 MiB. It can also be manually set with the “--dmg-romsize” command line switch.{:s}".format(ANSI.YELLOW, ANSI.RESET))
rom_size = 8 * 1024 * 1024
else:
sizes = [ "auto", "32kb", "64kb", "128kb", "256kb", "512kb", "1mb", "2mb", "4mb", "8mb", "16mb", "32mb" ]
sizes = [ "auto", "32kb", "64kb", "128kb", "256kb", "512kb", "1mb", "2mb", "4mb", "8mb", "16mb", "32mb", "64mb", "128mb" ]
rom_size = Util.DMG_Header_ROM_Sizes_Flasher_Map[sizes.index(args.dmg_romsize) - 1]
elif self.CONN.GetMode() == "AGB":
@ -820,6 +825,9 @@ class FlashGBX_CLI():
carts = self.CONN.GetSupportedCartridgesDMG()[1]
elif self.CONN.GetMode() == "AGB":
carts = self.CONN.GetSupportedCartridgesAGB()[1]
else:
raise NotImplementedError
cart_type = 0
for i in range(0, len(carts)):
if not "names" in carts[i]: continue
@ -978,6 +986,9 @@ class FlashGBX_CLI():
s_mbc = " using Mapper Type 0x{:X}".format(mbc)
elif self.CONN.GetMode() == "AGB":
hdr = RomFileAGB(buffer).GetHeader()
else:
raise NotImplementedError
if not hdr["logo_correct"] and (self.CONN.GetMode() == "AGB" or (self.CONN.GetMode() == "DMG" and mbc not in (0x203, 0x205))):
print("{:s}Warning: The ROM file you selected will not boot on actual hardware due to invalid boot logo data.{:s}".format(ANSI.YELLOW, ANSI.RESET))
bootlogo = None
@ -1249,14 +1260,6 @@ class FlashGBX_CLI():
print("\n{:s}Done! The writable save data size is {:s} out of {:s} checked.{:s}".format(ANSI.GREEN, Util.formatFileSize(size=found_length), Util.formatFileSize(size=Util.DMG_Header_RAM_Sizes_Flasher_Map[Util.DMG_Header_RAM_Sizes_Map.index(save_type)]), ANSI.RESET))
elif self.CONN.GetMode() == "AGB":
print("\n{:s}Done! The writable save data size using save type “{:s}” is {:s}.{:s}".format(ANSI.GREEN, Util.AGB_Header_Save_Types[save_type], Util.formatFileSize(size=found_length), ANSI.RESET))
try:
(_, _, cfi) = self.CONN.CheckFlashChip(limitVoltage=False)
if len(cfi["raw"]) > 0:
with open(Util.CONFIG_PATH + "/cfi.bin", "wb") as f: f.write(cfi["raw"])
print("CFI data was extracted to “cfi.bin”.")
except:
pass
def UpdateFirmware_PrintText(self, text, enableUI=False, setProgress=None):
if setProgress is not None:
@ -1415,8 +1418,8 @@ class FlashGBX_CLI():
with zf.open("fw.ini") as f: ini_file = f.read()
ini_file = ini_file.decode(encoding="utf-8")
self.INI = Util.IniSettings(ini=ini_file, main_section="Firmware")
fw_ver = self.INI.GetValue("fw_ver")
fw_buildts = self.INI.GetValue("fw_buildts")
#fw_ver = self.INI.GetValue("fw_ver")
#fw_buildts = self.INI.GetValue("fw_buildts")
print("Select the firmware to install:\n1) Lesserkumas FlashGBX firmware\n2) BennVenns DragnDrop firmware\n3) BennVenns JoeyGUI firmware\n")
answer = input("Enter number 1-3: ").lower().strip()
@ -1454,6 +1457,7 @@ class FlashGBX_CLI():
FWUPD = FirmwareUpdater(port=port)
file_name = Util.APP_PATH + "/res/fw_JoeyJr.zip"
with zipfile.ZipFile(file_name) as archive:
fw_data = None
if fw_choice == 1:
with archive.open("FIRMWARE_LK.JR") as f: fw_data = bytearray(f.read())
elif fw_choice == 2:

View File

@ -30,6 +30,8 @@ class FlashGBX_GUI(QtWidgets.QWidget):
TEXT_COLOR = (0, 0, 0, 255)
def __init__(self, args):
sys.excepthook = Util.exception_hook
QtWidgets.QWidget.__init__(self)
Util.CONFIG_PATH = args['config_path']
Util.APP_PATH = args['app_path']
@ -314,7 +316,7 @@ class FlashGBX_GUI(QtWidgets.QWidget):
self.lblDMGGameNameResult = QtWidgets.QLabel("")
rowDMGGameName.addWidget(self.lblDMGGameNameResult)
rowDMGGameName.setStretch(0, 9)
rowDMGGameName.setStretch(1, 11)
rowDMGGameName.setStretch(1, 12)
group_layout.addLayout(rowDMGGameName)
rowDMGRomTitle = QtWidgets.QHBoxLayout()
@ -324,7 +326,7 @@ class FlashGBX_GUI(QtWidgets.QWidget):
self.lblDMGRomTitleResult = QtWidgets.QLabel("")
rowDMGRomTitle.addWidget(self.lblDMGRomTitleResult)
rowDMGRomTitle.setStretch(0, 9)
rowDMGRomTitle.setStretch(1, 11)
rowDMGRomTitle.setStretch(1, 12)
group_layout.addLayout(rowDMGRomTitle)
rowDMGGameCodeRevision = QtWidgets.QHBoxLayout()
@ -334,7 +336,7 @@ class FlashGBX_GUI(QtWidgets.QWidget):
self.lblDMGGameCodeRevisionResult = QtWidgets.QLabel("")
rowDMGGameCodeRevision.addWidget(self.lblDMGGameCodeRevisionResult)
rowDMGGameCodeRevision.setStretch(0, 9)
rowDMGGameCodeRevision.setStretch(1, 11)
rowDMGGameCodeRevision.setStretch(1, 12)
group_layout.addLayout(rowDMGGameCodeRevision)
rowDMGHeaderRtc = QtWidgets.QHBoxLayout()
@ -345,7 +347,7 @@ class FlashGBX_GUI(QtWidgets.QWidget):
self.lblDMGHeaderRtcResult.mousePressEvent = lambda event: [ self.EditRTC(event) ]
rowDMGHeaderRtc.addWidget(self.lblDMGHeaderRtcResult)
rowDMGHeaderRtc.setStretch(0, 9)
rowDMGHeaderRtc.setStretch(1, 11)
rowDMGHeaderRtc.setStretch(1, 12)
group_layout.addLayout(rowDMGHeaderRtc)
rowDMGHeaderBootlogo = QtWidgets.QHBoxLayout()
@ -355,7 +357,7 @@ class FlashGBX_GUI(QtWidgets.QWidget):
self.lblDMGHeaderBootlogoResult = QtWidgets.QLabel("")
rowDMGHeaderBootlogo.addWidget(self.lblDMGHeaderBootlogoResult)
rowDMGHeaderBootlogo.setStretch(0, 9)
rowDMGHeaderBootlogo.setStretch(1, 11)
rowDMGHeaderBootlogo.setStretch(1, 12)
group_layout.addLayout(rowDMGHeaderBootlogo)
rowDMGHeaderROMChecksum = QtWidgets.QHBoxLayout()
@ -365,7 +367,7 @@ class FlashGBX_GUI(QtWidgets.QWidget):
self.lblDMGHeaderROMChecksumResult = QtWidgets.QLabel("")
rowDMGHeaderROMChecksum.addWidget(self.lblDMGHeaderROMChecksumResult)
rowDMGHeaderROMChecksum.setStretch(0, 9)
rowDMGHeaderROMChecksum.setStretch(1, 11)
rowDMGHeaderROMChecksum.setStretch(1, 12)
group_layout.addLayout(rowDMGHeaderROMChecksum)
rowDMGHeaderROMSize = QtWidgets.QHBoxLayout()
@ -376,7 +378,7 @@ class FlashGBX_GUI(QtWidgets.QWidget):
self.cmbDMGHeaderROMSizeResult.view().setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
rowDMGHeaderROMSize.addWidget(self.cmbDMGHeaderROMSizeResult)
rowDMGHeaderROMSize.setStretch(0, 9)
rowDMGHeaderROMSize.setStretch(1, 11)
rowDMGHeaderROMSize.setStretch(1, 12)
group_layout.addLayout(rowDMGHeaderROMSize)
rowDMGHeaderSaveType = QtWidgets.QHBoxLayout()
@ -387,7 +389,7 @@ class FlashGBX_GUI(QtWidgets.QWidget):
self.cmbDMGHeaderSaveTypeResult.view().setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
rowDMGHeaderSaveType.addWidget(self.cmbDMGHeaderSaveTypeResult)
rowDMGHeaderSaveType.setStretch(0, 9)
rowDMGHeaderSaveType.setStretch(1, 11)
rowDMGHeaderSaveType.setStretch(1, 12)
group_layout.addLayout(rowDMGHeaderSaveType)
rowDMGHeaderMapper = QtWidgets.QHBoxLayout()
@ -398,7 +400,7 @@ class FlashGBX_GUI(QtWidgets.QWidget):
self.cmbDMGHeaderMapperResult.view().setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
rowDMGHeaderMapper.addWidget(self.cmbDMGHeaderMapperResult)
rowDMGHeaderMapper.setStretch(0, 9)
rowDMGHeaderMapper.setStretch(1, 11)
rowDMGHeaderMapper.setStretch(1, 12)
group_layout.addLayout(rowDMGHeaderMapper)
rowDMGCartridgeType = QtWidgets.QHBoxLayout()
@ -428,7 +430,7 @@ class FlashGBX_GUI(QtWidgets.QWidget):
self.lblAGBGameNameResult = QtWidgets.QLabel("")
rowAGBGameName.addWidget(self.lblAGBGameNameResult)
rowAGBGameName.setStretch(0, 9)
rowAGBGameName.setStretch(1, 11)
rowAGBGameName.setStretch(1, 12)
group_layout.addLayout(rowAGBGameName)
rowAGBRomTitle = QtWidgets.QHBoxLayout()
@ -438,7 +440,7 @@ class FlashGBX_GUI(QtWidgets.QWidget):
self.lblAGBRomTitleResult = QtWidgets.QLabel("")
rowAGBRomTitle.addWidget(self.lblAGBRomTitleResult)
rowAGBRomTitle.setStretch(0, 9)
rowAGBRomTitle.setStretch(1, 11)
rowAGBRomTitle.setStretch(1, 12)
group_layout.addLayout(rowAGBRomTitle)
rowAGBHeaderGameCodeRevision = QtWidgets.QHBoxLayout()
@ -448,7 +450,7 @@ class FlashGBX_GUI(QtWidgets.QWidget):
self.lblAGBHeaderGameCodeRevisionResult = QtWidgets.QLabel("")
rowAGBHeaderGameCodeRevision.addWidget(self.lblAGBHeaderGameCodeRevisionResult)
rowAGBHeaderGameCodeRevision.setStretch(0, 9)
rowAGBHeaderGameCodeRevision.setStretch(1, 11)
rowAGBHeaderGameCodeRevision.setStretch(1, 12)
group_layout.addLayout(rowAGBHeaderGameCodeRevision)
rowAGBGpioRtc = QtWidgets.QHBoxLayout()
@ -459,7 +461,7 @@ class FlashGBX_GUI(QtWidgets.QWidget):
self.lblAGBGpioRtcResult.mousePressEvent = lambda event: [ self.EditRTC(event) ]
rowAGBGpioRtc.addWidget(self.lblAGBGpioRtcResult)
rowAGBGpioRtc.setStretch(0, 9)
rowAGBGpioRtc.setStretch(1, 11)
rowAGBGpioRtc.setStretch(1, 12)
group_layout.addLayout(rowAGBGpioRtc)
rowAGBHeaderBootlogo = QtWidgets.QHBoxLayout()
@ -469,7 +471,7 @@ class FlashGBX_GUI(QtWidgets.QWidget):
self.lblAGBHeaderBootlogoResult = QtWidgets.QLabel("")
rowAGBHeaderBootlogo.addWidget(self.lblAGBHeaderBootlogoResult)
rowAGBHeaderBootlogo.setStretch(0, 9)
rowAGBHeaderBootlogo.setStretch(1, 11)
rowAGBHeaderBootlogo.setStretch(1, 12)
group_layout.addLayout(rowAGBHeaderBootlogo)
rowAGBHeaderChecksum = QtWidgets.QHBoxLayout()
@ -479,7 +481,7 @@ class FlashGBX_GUI(QtWidgets.QWidget):
self.lblAGBHeaderChecksumResult = QtWidgets.QLabel("")
rowAGBHeaderChecksum.addWidget(self.lblAGBHeaderChecksumResult)
rowAGBHeaderChecksum.setStretch(0, 9)
rowAGBHeaderChecksum.setStretch(1, 11)
rowAGBHeaderChecksum.setStretch(1, 12)
group_layout.addLayout(rowAGBHeaderChecksum)
rowAGBHeaderROMChecksum = QtWidgets.QHBoxLayout()
@ -489,7 +491,7 @@ class FlashGBX_GUI(QtWidgets.QWidget):
self.lblAGBHeaderROMChecksumResult = QtWidgets.QLabel("")
rowAGBHeaderROMChecksum.addWidget(self.lblAGBHeaderROMChecksumResult)
rowAGBHeaderROMChecksum.setStretch(0, 9)
rowAGBHeaderROMChecksum.setStretch(1, 11)
rowAGBHeaderROMChecksum.setStretch(1, 12)
group_layout.addLayout(rowAGBHeaderROMChecksum)
rowAGBHeaderROMSize = QtWidgets.QHBoxLayout()
@ -502,7 +504,7 @@ class FlashGBX_GUI(QtWidgets.QWidget):
self.cmbAGBHeaderROMSizeResult.setCurrentIndex(self.cmbAGBHeaderROMSizeResult.count() - 1)
rowAGBHeaderROMSize.addWidget(self.cmbAGBHeaderROMSizeResult)
rowAGBHeaderROMSize.setStretch(0, 9)
rowAGBHeaderROMSize.setStretch(1, 11)
rowAGBHeaderROMSize.setStretch(1, 12)
group_layout.addLayout(rowAGBHeaderROMSize)
rowAGBHeaderSaveType = QtWidgets.QHBoxLayout()
@ -515,7 +517,7 @@ class FlashGBX_GUI(QtWidgets.QWidget):
self.cmbAGBSaveTypeResult.setCurrentIndex(self.cmbAGBSaveTypeResult.count() - 1)
rowAGBHeaderSaveType.addWidget(self.cmbAGBSaveTypeResult)
rowAGBHeaderSaveType.setStretch(0, 9)
rowAGBHeaderSaveType.setStretch(1, 11)
rowAGBHeaderSaveType.setStretch(1, 12)
group_layout.addLayout(rowAGBHeaderSaveType)
rowAGBCartridgeType = QtWidgets.QHBoxLayout()
@ -716,7 +718,7 @@ class FlashGBX_GUI(QtWidgets.QWidget):
def AboutGameDB(self):
msg = f"{APPNAME} uses a game database that is based on the ongoing efforts of the No-Intro project. Visit <a href=\"https://no-intro.org/\">https://no-intro.org/</a> for more information.<br><br>"
msg += f"No-Intro databases referenced for this version of {APPNAME}:<br>"
msg += "• Nintendo - Game Boy (20240713-090345)<br>• Nintendo - Game Boy Advance (20240803-104002)<br>• Nintendo - Game Boy Advance (Video) (20240727-194101)<br>• Nintendo - Game Boy Color (20240801-100010)" # No-Intro DBs
msg += "• Nintendo - Game Boy (20241003-140822)<br>• Nintendo - Game Boy Advance (20241102-092521)<br>• Nintendo - Game Boy Advance (Video) (20241021-195439)<br>• Nintendo - Game Boy Color (20241105-004534)" # No-Intro DBs
QtWidgets.QMessageBox.information(self, "{:s} {:s}".format(APPNAME, VERSION), msg, QtWidgets.QMessageBox.Ok)
def OpenPath(self, path=None):
@ -740,26 +742,19 @@ class FlashGBX_GUI(QtWidgets.QWidget):
if isinstance(event, QtGui.QMouseEvent):
if event.button() in (QtCore.Qt.MouseButton.MiddleButton, QtCore.Qt.MouseButton.RightButton): return
device = False
try:
Util.dprint("{:s} version: {:s} ({:d})".format(Util.APPNAME, Util.VERSION_PEP440, Util.VERSION_TIMESTAMP))
Util.dprint("Platform: {:s}".format(platform.platform()))
if self.CONN is not None:
Util.dprint("Connected device: {:s}".format(self.CONN.GetFullNameExtended(more=True)))
else:
Util.dprint("No device connected")
Util.dprint("Now writing debug log file")
device = self.CONN.GetFullNameExtended(more=True)
except:
pass
Util.write_debug_log(device)
try:
fn = Util.CONFIG_PATH + "/debug.log"
with open(fn, "wb") as f:
f.write("\n".join(Util.DEBUG_LOG).encode("UTF-8-SIG"))
print("debug.log written")
if open_log is True:
fn = Util.CONFIG_PATH + "/debug.log"
self.OpenPath(fn)
return True
except:
return False
pass
def ConnectDevice(self):
if self.CONN is not None:
@ -872,6 +867,7 @@ class FlashGBX_GUI(QtWidgets.QWidget):
if dev.FirmwareUpdateAvailable():
dontShowAgain = str(self.SETTINGS.value("SkipFirmwareUpdate", default="disabled")).lower() == "enabled"
if not dontShowAgain or dev.FW_UPDATE_REQ:
cb = None
if dev.FW_UPDATE_REQ is True:
text = "A firmware update for your {:s} device is required to use this software. Do you want to update now?".format(dev.GetFullName())
msgbox = QtWidgets.QMessageBox(parent=self, icon=QtWidgets.QMessageBox.Warning, windowTitle="{:s} {:s}".format(APPNAME, VERSION), text=text, standardButtons=QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, defaultButton=QtWidgets.QMessageBox.Yes)
@ -889,8 +885,9 @@ class FlashGBX_GUI(QtWidgets.QWidget):
if not Util.DEBUG:
self.DisconnectDevice()
else:
dontShowAgain = cb.isChecked()
if dontShowAgain: self.SETTINGS.setValue("SkipFirmwareUpdate", "enabled")
if cb is not None:
dontShowAgain = cb.isChecked()
if dontShowAgain: self.SETTINGS.setValue("SkipFirmwareUpdate", "enabled")
if answer == QtWidgets.QMessageBox.Yes:
self.ShowFirmwareUpdateWindow()
@ -899,8 +896,16 @@ class FlashGBX_GUI(QtWidgets.QWidget):
if not Util.DEBUG:
self.DisconnectDevice()
QtWidgets.QMessageBox.warning(self, "{:s} {:s}".format(APPNAME, VERSION), text, QtWidgets.QMessageBox.Ok)
if dev.IsUnregistered():
try:
text = dev.GetRegisterInformation()
QtWidgets.QMessageBox.critical(self, "{:s} {:s}".format(APPNAME, VERSION), text, QtWidgets.QMessageBox.Ok)
except:
pass
return True
return False
def FindDevices(self, connectToFirst=False, port=None, mode=None, firstRun=False):
@ -959,7 +964,7 @@ class FlashGBX_GUI(QtWidgets.QWidget):
msg += message + "\n\n"
QtWidgets.QMessageBox.critical(self, "{:s} {:s}".format(APPNAME, VERSION), msg[:-2], QtWidgets.QMessageBox.Ok)
elif not firstRun:
QtWidgets.QMessageBox(parent=self, icon=QtWidgets.QMessageBox.Warning, windowTitle="{:s} {:s}".format(APPNAME, VERSION), text="No compatible devices found. Please ensure the device is connected properly.\n\nTroubleshooting advice:\n- Reconnect the device, try different USB ports/cables, avoid passive USB hubs\n- Use a USB data cable (battery charging cables may not work)\n- Check if the operating system detects the device (if not, reboot your machine)\n- Ensure your user account has permissions to use the device\n- Refer to the device compatibility list on the <a href=\"https://github.com/lesserkuma/FlashGBX/#compatible-cartridge-readerwriter-hardware\">GitHub page</a>".replace("\n", "<br>"), standardButtons=QtWidgets.QMessageBox.Ok).exec()
QtWidgets.QMessageBox(parent=self, icon=QtWidgets.QMessageBox.Warning, windowTitle="{:s} {:s}".format(APPNAME, VERSION), text="No compatible devices found. Please ensure the device is connected properly.\n\nTroubleshooting advice:\n- Reconnect the device, try different USB ports/cables, avoid passive USB hubs\n- Use a USB data cable (battery charging cables may not work)\n- Check if the operating system detects the device (if not, reboot your machine)\n- Ensure your user account has permissions to use the device\n- Refer to the device compatibility list on the <a href=\"https://github.com/lesserkuma/FlashGBX/#compatible-cartridge-readerwriter-hardware\">GitHub page</a>\n- Update the firmware manually through Options → Tools → Firmware Updater".replace("\n", "<br>"), standardButtons=QtWidgets.QMessageBox.Ok).exec()
self.lblDevice.setText("No devices found.")
self.lblDevice.setStyleSheet("")
@ -1080,7 +1085,7 @@ class FlashGBX_GUI(QtWidgets.QWidget):
else:
msg += " This may indicate a bad dump, however this can be normal for some reproduction cartridges, unlicensed games, prototypes, patched games and intentional overdumps. You can also try to change the read mode in the options."
if self.CONN.GetMode() == "DMG" and self.cmbDMGHeaderMapperResult.currentText() == "MBC1":
msg += "\n\nIf this is a NP GB-Memory Cartridge, please use the “Retry as G-MMC1” button."
msg += "\n\nIf this is a NP GB-Memory Cartridge, try the “Retry as G-MMC1” option."
button_gmmc1 = msgbox.addButton(" Retry as G-MMC1 ", QtWidgets.QMessageBox.ActionRole)
msgbox.setText(msg + msg_te)
msgbox.setIcon(QtWidgets.QMessageBox.Warning)
@ -1244,6 +1249,11 @@ class FlashGBX_GUI(QtWidgets.QWidget):
self.CONN.INFO["dump_info"]["batteryless_sram"] = temp3
else:
self.ReadCartridge(resetStatus=False)
elif self.CONN.INFO["last_action"] == 6: # Detect Cartridge
self.lblStatus4a.setText("Ready.")
self.CONN.INFO["last_action"] = 0
self.FinishDetectCartridge(self.CONN.INFO["detect_cart"])
else:
self.lblStatus4a.setText("Ready.")
@ -1375,11 +1385,20 @@ class FlashGBX_GUI(QtWidgets.QWidget):
return
if cart_type == 0:
cart_type = self.DetectCartridge(canSkipMessage=True)
if "detected_cart_type" not in self.STATUS: self.STATUS["detected_cart_type"] = ""
if self.STATUS["detected_cart_type"] == "":
self.STATUS["detected_cart_type"] = "WAITING_FLASH"
self.STATUS["detect_cartridge_args"] = { "dpath":path }
self.STATUS["can_skip_message"] = True
self.DetectCartridge(checkSaveType=False)
return
cart_type = self.STATUS["detected_cart_type"]
if "detected_cart_type" in self.STATUS: del(self.STATUS["detected_cart_type"])
if cart_type is False: # clicked Cancel button
return
elif cart_type is None or cart_type == 0:
QtWidgets.QMessageBox.critical(self, "{:s} {:s}".format(APPNAME, VERSION), "A compatible flash cartridge type could not be auto-detected.", QtWidgets.QMessageBox.Ok)
QtWidgets.QMessageBox.critical(self, "{:s} {:s}".format(APPNAME, VERSION), "A compatible flash cartridge profile could not be auto-detected.", QtWidgets.QMessageBox.Ok)
return
if self.CONN.GetMode() == "DMG":
@ -1387,6 +1406,8 @@ class FlashGBX_GUI(QtWidgets.QWidget):
elif self.CONN.GetMode() == "AGB":
self.cmbAGBCartridgeTypeResult.setCurrentIndex(cart_type)
if "detected_cart_type" in self.STATUS: del(self.STATUS["detected_cart_type"])
if self.CONN.GetMode() == "DMG":
self.SetDMGMapperResult(carts[cart_type])
mbc = Util.ConvertMapperTypeToMapper(self.cmbDMGHeaderMapperResult.currentIndex())
@ -1467,7 +1488,7 @@ class FlashGBX_GUI(QtWidgets.QWidget):
if not Util.compare_mbc(hdr["mapper_raw"], mbc):
mbc1 = Util.get_mbc_name(mbc)
mbc2 = Util.get_mbc_name(hdr["mapper_raw"])
compatible_mbc = [ "None", "MBC2", "MBC3", "MBC30", "MBC5", "MBC7", "MAC-GBD", "G-MMC1", "HuC-1", "HuC-3" ]
compatible_mbc = [ "None", "MBC2", "MBC3", "MBC30", "MBC5", "MBC7", "MAC-GBD", "G-MMC1", "HuC-1", "HuC-3", "Unlicensed MBCX Mapper" ]
if (mbc2 == "None") or (mbc1 == "G-MMC1" and mbc2 == "MBC1") or (mbc2 == "G-MMC1" and mbc1 == "MBC1"):
pass
elif mbc2 != "None" and not (mbc1 in compatible_mbc and mbc2 in compatible_mbc):
@ -1640,14 +1661,25 @@ class FlashGBX_GUI(QtWidgets.QWidget):
return
cart_type = self.cmbAGBCartridgeTypeResult.currentIndex()
if cart_type == 0 or ("dump_info" not in self.CONN.INFO or "batteryless_sram" not in self.CONN.INFO["dump_info"]):
cart_type = self.DetectCartridge()
if "detected_cart_type" not in self.STATUS: self.STATUS["detected_cart_type"] = ""
if self.STATUS["detected_cart_type"] == "":
self.STATUS["detected_cart_type"] = "WAITING_SAVE_READ"
self.STATUS["detect_cartridge_args"] = { "dpath":path }
self.STATUS["can_skip_message"] = True
self.DetectCartridge(checkSaveType=True)
return
cart_type = self.STATUS["detected_cart_type"]
if "detected_cart_type" in self.STATUS: del(self.STATUS["detected_cart_type"])
if cart_type is False: # clicked Cancel button
return
elif cart_type is None or cart_type == 0:
QtWidgets.QMessageBox.critical(self, "{:s} {:s}".format(APPNAME, VERSION), "A compatible flash cartridge type could not be auto-detected.", QtWidgets.QMessageBox.Ok)
QtWidgets.QMessageBox.critical(self, "{:s} {:s}".format(APPNAME, VERSION), "A compatible flash cartridge profile could not be auto-detected.", QtWidgets.QMessageBox.Ok)
return
self.cmbAGBCartridgeTypeResult.setCurrentIndex(cart_type)
if "detected_cart_type" in self.STATUS: del(self.STATUS["detected_cart_type"])
if "dump_info" in self.CONN.INFO and "batteryless_sram" in self.CONN.INFO["dump_info"]:
detected = self.CONN.INFO["dump_info"]["batteryless_sram"]
else:
@ -1678,10 +1710,12 @@ class FlashGBX_GUI(QtWidgets.QWidget):
self.STATUS["last_path"] = path
self.STATUS["args"] = args
def WriteRAM(self, dpath="", erase=False, test=False):
def WriteRAM(self, dpath="", erase=False, test=False, skip_warning=False):
if not self.CheckDeviceAlive(): return
mode = self.CONN.GetMode()
if erase is True: dpath = ""
if dpath == "":
path = Util.GenerateFileName(mode=mode, header=self.CONN.INFO, settings=self.SETTINGS)
path = os.path.splitext(path)[0]
@ -1716,14 +1750,16 @@ class FlashGBX_GUI(QtWidgets.QWidget):
filesize = 0
if dpath != "":
text = "The following save data file will now be written to the cartridge:\n" + dpath
answer = QtWidgets.QMessageBox.question(self, "{:s} {:s}".format(APPNAME, VERSION), text, QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel, QtWidgets.QMessageBox.Ok)
if answer == QtWidgets.QMessageBox.Cancel: return
if not skip_warning:
text = "The following save data file will now be written to the cartridge:\n" + dpath
answer = QtWidgets.QMessageBox.question(self, "{:s} {:s}".format(APPNAME, VERSION), text, QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel, QtWidgets.QMessageBox.Ok)
if answer == QtWidgets.QMessageBox.Cancel: return
path = dpath
self.SETTINGS.setValue(setting_name, os.path.dirname(path))
elif erase:
answer = QtWidgets.QMessageBox.warning(self, "{:s} {:s}".format(APPNAME, VERSION), "The save data on your cartridge will now be erased.", QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel, QtWidgets.QMessageBox.Cancel)
if answer == QtWidgets.QMessageBox.Cancel: return
if not skip_warning:
answer = QtWidgets.QMessageBox.warning(self, "{:s} {:s}".format(APPNAME, VERSION), "The save data on your cartridge will now be erased.", QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel, QtWidgets.QMessageBox.Cancel)
if answer == QtWidgets.QMessageBox.Cancel: return
elif test:
path = None
if self.CONN.GetFWBuildDate() == "": # Legacy Mode
@ -2020,14 +2056,25 @@ class FlashGBX_GUI(QtWidgets.QWidget):
return
cart_type = self.cmbAGBCartridgeTypeResult.currentIndex()
if cart_type == 0 or ("dump_info" not in self.CONN.INFO or "batteryless_sram" not in self.CONN.INFO["dump_info"]):
cart_type = self.DetectCartridge()
if "detected_cart_type" not in self.STATUS: self.STATUS["detected_cart_type"] = ""
if self.STATUS["detected_cart_type"] == "":
self.STATUS["detected_cart_type"] = "WAITING_SAVE_WRITE"
self.STATUS["detect_cartridge_args"] = { "dpath":path, "erase":erase }
self.STATUS["can_skip_message"] = True
self.DetectCartridge(checkSaveType=True)
return
cart_type = self.STATUS["detected_cart_type"]
if "detected_cart_type" in self.STATUS: del(self.STATUS["detected_cart_type"])
if cart_type is False: # clicked Cancel button
return
elif cart_type is None or cart_type == 0:
QtWidgets.QMessageBox.critical(self, "{:s} {:s}".format(APPNAME, VERSION), "A compatible flash cartridge type could not be auto-detected.", QtWidgets.QMessageBox.Ok)
QtWidgets.QMessageBox.critical(self, "{:s} {:s}".format(APPNAME, VERSION), "A compatible flash cartridge profile could not be auto-detected.", QtWidgets.QMessageBox.Ok)
return
self.cmbAGBCartridgeTypeResult.setCurrentIndex(cart_type)
if "detected_cart_type" in self.STATUS: del(self.STATUS["detected_cart_type"])
if "dump_info" in self.CONN.INFO and "batteryless_sram" in self.CONN.INFO["dump_info"]:
detected = self.CONN.INFO["dump_info"]["batteryless_sram"]
else:
@ -2087,6 +2134,7 @@ class FlashGBX_GUI(QtWidgets.QWidget):
except:
pass
intro_msg = ""
if detected is not False:
try:
loc_index = locs.index(detected["bl_offset"])
@ -2154,7 +2202,7 @@ class FlashGBX_GUI(QtWidgets.QWidget):
if self.CONN.GetMode() == "DMG":
mbc = Util.get_mbc_name(Util.ConvertMapperTypeToMapper(self.cmbDMGHeaderMapperResult.currentIndex()))
if mbc in ("MBC3", "MBC30"):
if mbc in ("MBC3", "MBC30", "Unlicensed MBCX Mapper"):
dlg_args = {
"title":"MBC3/MBC30 Real Time Clock Editor",
"intro":"Enter the number of days, hours, minutes and seconds that passed since the RTC initially started.\n\nPlease note that all values are internal values. The game may use these only as a relative reference.",
@ -2379,10 +2427,16 @@ class FlashGBX_GUI(QtWidgets.QWidget):
if not self.CheckDeviceAlive(setMode=setTo): return
if self.optDMG.isChecked() and (mode == "AGB" or mode == None):
self.CONN.SetMode("DMG")
elif self.optAGB.isChecked() and (mode == "DMG" or mode == None):
self.CONN.SetMode("AGB")
try:
if self.optDMG.isChecked() and (mode == "AGB" or mode == None):
self.CONN.SetMode("DMG")
elif self.optAGB.isChecked() and (mode == "DMG" or mode == None):
self.CONN.SetMode("AGB")
except BrokenPipeError:
msg = "Failed to turn on the cartridge power.\n\nAs a workaround, try to disable the \"Automatic cartridge power off\" setting and then hotplug the cartridge after clicking \"Refresh\"."
QtWidgets.QMessageBox.critical(self, "{:s} {:s}".format(APPNAME, VERSION), msg, QtWidgets.QMessageBox.Ok)
self.DisconnectDevice()
return False
ok = self.ReadCartridge()
qt_app.processEvents()
@ -2422,7 +2476,11 @@ class FlashGBX_GUI(QtWidgets.QWidget):
return False
if self.CONN.CheckROMStable() is False and resetStatus:
QtWidgets.QMessageBox.critical(self, "{:s} {:s}".format(APPNAME, VERSION), "The cartridge connection is unstable!\nPlease clean the cartridge pins, carefully realign the cartridge and then try again.", QtWidgets.QMessageBox.Ok)
try:
if data != bytearray(data[0] * len(data)):
QtWidgets.QMessageBox.critical(self, "{:s} {:s}".format(APPNAME, VERSION), "The cartridge connection is unstable!\nPlease clean the cartridge pins, carefully realign the cartridge and then try again.", QtWidgets.QMessageBox.Ok)
except:
pass
if self.CONN.GetMode() == "DMG":
self.cmbDMGHeaderMapperResult.clear()
@ -2447,7 +2505,7 @@ class FlashGBX_GUI(QtWidgets.QWidget):
self.lblDMGGameCodeRevisionResult.setText("{:s}-{:s}".format(data["db"]["gc"], str(data["version"])))
temp = data["db"]["gn"]
self.lblDMGGameNameResult.setText(temp)
while self.lblDMGGameNameResult.fontMetrics().boundingRect(self.lblDMGGameNameResult.text()).width() > 200:
while self.lblDMGGameNameResult.fontMetrics().boundingRect(self.lblDMGGameNameResult.text()).width() > 240:
temp = temp[:-1]
self.lblDMGGameNameResult.setText(temp + "")
if temp != data["db"]["gn"]:
@ -2569,7 +2627,7 @@ class FlashGBX_GUI(QtWidgets.QWidget):
self.lblAGBHeaderGameCodeRevisionResult.setText("{:s}-{:s}".format(data["db"]["gc"], str(data["version"])))
temp = data["db"]["gn"]
self.lblAGBGameNameResult.setText(temp)
while self.lblAGBGameNameResult.fontMetrics().boundingRect(self.lblAGBGameNameResult.text()).width() > 200:
while self.lblAGBGameNameResult.fontMetrics().boundingRect(self.lblAGBGameNameResult.text()).width() > 240:
temp = temp[:-1]
self.lblAGBGameNameResult.setText(temp + "")
if temp != data["db"]["gn"]:
@ -2680,13 +2738,12 @@ class FlashGBX_GUI(QtWidgets.QWidget):
self.btnRestoreRAM.setEnabled(True)
self.btnHeaderRefresh.setFocus()
self.SetProgressBars(min=0, max=100, value=0)
self.lblStatus4a.setText("Ready.")
qt_app.processEvents()
if data['game_title'][:11] == "YJencrypted" and resetStatus:
QtWidgets.QMessageBox.warning(self, "{:s} {:s}".format(APPNAME, VERSION), "This cartridge may be protected against reading or writing a ROM. If you dont want to risk this cartridge to render itself unusable, please do not try to write a new ROM to it.", QtWidgets.QMessageBox.Ok)
def DetectCartridge(self, canSkipMessage=False):
def DetectCartridge(self, checkSaveType=True):
if not self.CheckDeviceAlive(): return
if not self.CONN.CheckROMStable():
answer = QtWidgets.QMessageBox.warning(self, "{:s} {:s}".format(APPNAME, VERSION), "The cartridge connection is unstable!\nPlease clean the cartridge pins, carefully realign the cartridge for best results.\n\nContinue anyway?", QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No)
@ -2702,12 +2759,20 @@ class FlashGBX_GUI(QtWidgets.QWidget):
self.lblStatus2aResult.setText("")
self.lblStatus3aResult.setText("")
self.lblStatus4aResult.setText("")
self.lblStatus4a.setText("Analyzing Cartridge...")
# self.lblStatus4a.setText("Analyzing Cartridge...")
self.SetProgressBars(min=0, max=0, value=1)
qt_app.processEvents()
if "can_skip_message" not in self.STATUS: self.STATUS["can_skip_message"] = False
limitVoltage = str(self.SETTINGS.value("AutoDetectLimitVoltage", default="disabled")).lower() == "enabled"
self.CONN.DetectCartridge(fncSetProgress=self.PROGRESS.SetProgress, args={"limitVoltage":limitVoltage, "checkSaveType":checkSaveType})
def FinishDetectCartridge(self, ret):
self.lblStatus1aResult.setText("")
self.lblStatus2aResult.setText("")
self.lblStatus3aResult.setText("")
limitVoltage = str(self.SETTINGS.value("AutoDetectLimitVoltage", default="disabled")).lower() == "enabled"
ret = self.CONN.DetectCartridge(limitVoltage=limitVoltage, checkSaveType=not canSkipMessage)
if ret is False:
QtWidgets.QMessageBox.critical(self, "{:s} {:s}".format(APPNAME, VERSION), "An error occured while trying to analyze the cartridge and you may need to physically reconnect the device.\n\nThis cartridge may not be auto-detectable, please select the cartridge type manually.", QtWidgets.QMessageBox.Ok)
self.DisconnectDevice()
@ -2716,7 +2781,7 @@ class FlashGBX_GUI(QtWidgets.QWidget):
(header, save_size, save_type, save_chip, sram_unstable, cart_types, cart_type_id, cfi_s, _, flash_id, detected_size) = ret
# Save Type
if not canSkipMessage:
if not self.STATUS["can_skip_message"]:
try:
if save_type is not None and save_type is not False:
if self.CONN.GetMode() == "DMG":
@ -2735,6 +2800,8 @@ class FlashGBX_GUI(QtWidgets.QWidget):
supp_cart_types = self.CONN.GetSupportedCartridgesDMG()
elif self.CONN.GetMode() == "AGB":
supp_cart_types = self.CONN.GetSupportedCartridgesAGB()
else:
raise NotImplementedError
except Exception as e:
msgbox = QtWidgets.QMessageBox(parent=self, icon=QtWidgets.QMessageBox.Critical, windowTitle="{:s} {:s}".format(APPNAME, VERSION), text="An unknown error occured. Please try again.\n\n" + str(e), standardButtons=QtWidgets.QMessageBox.Ok)
msgbox.exec()
@ -2768,7 +2835,7 @@ class FlashGBX_GUI(QtWidgets.QWidget):
# Save Type
msg_save_type_s = ""
temp = ""
if not canSkipMessage and save_type is not False and save_type is not None:
if not self.STATUS["can_skip_message"] and save_type is not False and save_type is not None:
if save_chip is not None:
temp = "{:s} ({:s})".format(Util.AGB_Header_Save_Types[save_type], save_chip)
else:
@ -2916,7 +2983,7 @@ class FlashGBX_GUI(QtWidgets.QWidget):
msg = "The following cartridge configuration was detected:<br><br>"
if found_supported:
dontShowAgain = str(self.SETTINGS.value("SkipAutodetectMessage", default="disabled")).lower() == "enabled"
if not dontShowAgain or not canSkipMessage:
if not dontShowAgain or not self.STATUS["can_skip_message"]:
temp = "{:s}{:s}{:s}{:s}{:s}{:s}".format(msg, msg_flash_size_s, msg_save_type_s, msg_flash_mapper_s, msg_cart_type_s, msg_gbmem)
temp = temp[:-4]
msgbox = QtWidgets.QMessageBox(parent=self, icon=QtWidgets.QMessageBox.Information, windowTitle="{:s} {:s}".format(APPNAME, VERSION), text=temp)
@ -2926,7 +2993,7 @@ class FlashGBX_GUI(QtWidgets.QWidget):
button_cancel = None
msgbox.setDefaultButton(button_ok)
cb = QtWidgets.QCheckBox("Always skip this message", checked=False)
if canSkipMessage:
if self.STATUS["can_skip_message"]:
button_cancel = msgbox.addButton("&Cancel", QtWidgets.QMessageBox.RejectRole)
msgbox.setEscapeButton(button_cancel)
msgbox.setCheckBox(cb)
@ -2935,7 +3002,7 @@ class FlashGBX_GUI(QtWidgets.QWidget):
msgbox.exec()
dontShowAgain = cb.isChecked()
if dontShowAgain and canSkipMessage: self.SETTINGS.setValue("SkipAutodetectMessage", "enabled")
if dontShowAgain and self.STATUS["can_skip_message"]: self.SETTINGS.setValue("SkipAutodetectMessage", "enabled")
if msgbox.clickedButton() == button_details:
show_details = True
@ -2950,7 +3017,9 @@ class FlashGBX_GUI(QtWidgets.QWidget):
self.btnHeaderRefresh.setFocus()
self.SetProgressBars(min=0, max=100, value=0)
self.lblStatus4a.setText("Ready.")
return False
self.STATUS["can_skip_message"] = False
if "detected_cart_type" in self.STATUS: del(self.STATUS["detected_cart_type"])
return
if not found_supported or show_details is True:
msgbox = QtWidgets.QMessageBox(parent=self, icon=QtWidgets.QMessageBox.Information, windowTitle="{:s} {:s}".format(APPNAME, VERSION))
@ -2976,7 +3045,9 @@ class FlashGBX_GUI(QtWidgets.QWidget):
if answer == QtWidgets.QMessageBox.Yes:
self.SETTINGS.setValue("AutoDetectLimitVoltage", "disabled")
self.mnuConfig.actions()[4].setChecked(False)
return self.DetectCartridge()
self.STATUS["can_skip_message"] = False
self.DetectCartridge()
return
temp = "{:s}{:s}{:s}{:s}{:s}{:s}{:s}{:s}{:s}{:s}".format(msg, msg_header_s, msg_flash_size_s, msg_save_type_s, msg_flash_mapper_s, msg_flash_id_s, msg_cfi_s, msg_cart_type_s_detail, msg_gbmem, msg_fw)
temp = temp[:-4]
@ -3006,7 +3077,27 @@ class FlashGBX_GUI(QtWidgets.QWidget):
self.btnHeaderRefresh.setFocus()
self.SetProgressBars(min=0, max=100, value=0)
self.lblStatus4a.setText("Ready.")
return cart_type
waiting = None
if "detected_cart_type" in self.STATUS and self.STATUS["detected_cart_type"] in ("WAITING_FLASH", "WAITING_SAVE_READ", "WAITING_SAVE_WRITE"):
waiting = self.STATUS["detected_cart_type"]
self.STATUS["detected_cart_type"] = cart_type
self.STATUS["can_skip_message"] = False
if waiting == "WAITING_FLASH":
if "detect_cartridge_args" in self.STATUS:
self.FlashROM(dpath=self.STATUS["detect_cartridge_args"]["dpath"])
del(self.STATUS["detect_cartridge_args"])
else:
self.FlashROM()
elif waiting == "WAITING_SAVE_READ":
self.BackupRAM()
elif waiting == "WAITING_SAVE_WRITE":
if "detect_cartridge_args" in self.STATUS:
self.WriteRAM(dpath=self.STATUS["detect_cartridge_args"]["dpath"], erase=self.STATUS["detect_cartridge_args"]["erase"], skip_warning=True)
del(self.STATUS["detect_cartridge_args"])
else:
self.WriteRAM()
def WaitProgress(self, args):
if args["user_action"] == "REINSERT_CART":
@ -3037,6 +3128,8 @@ class FlashGBX_GUI(QtWidgets.QWidget):
self.grpStatus.setTitle("Transfer Status (Write Save Data)")
elif args["method"] == "SAVE_WRITE_VERIFY":
self.grpStatus.setTitle("Transfer Status (Verify Save Data)")
elif args["method"] == "DETECT_CART":
self.grpStatus.setTitle("Transfer Status (Analyze Cartridge)")
if "error" in args:
self.lblStatus4a.setText("Failed!")
@ -3120,6 +3213,11 @@ class FlashGBX_GUI(QtWidgets.QWidget):
self.lblStatus4aResult.setText("")
self.btnCancel.setEnabled(args["abortable"])
self.SetProgressBars(min=0, max=size, value=pos)
elif args["action"] == "UPDATE_INFO":
self.lblStatus4a.setText(args["text"])
self.lblStatus4aResult.setText("")
self.btnCancel.setEnabled(args["abortable"])
self.SetProgressBars(min=0, max=size, value=pos)
elif args["action"] == "FINISHED":
if pos > 0:
self.lblStatus1aResult.setText(Util.formatFileSize(size=pos))
@ -3338,18 +3436,6 @@ class FlashGBX_GUI(QtWidgets.QWidget):
except:
pass
qt_app.exec()
# # Taskbar Progress on Windows only
# try:
# from PySide6.QtWin import QtWinTaskbarButton, QtWin
# myappid = 'lesserkuma.flashgbx'
# QtWin.setAppUserModelId(myappid)
# taskbar_button = QtWinTaskbarButton()
# self.TBPROG = taskbar_button.progress()
# self.TBPROG.setRange(0, 100)
# taskbar_button.setWindow(self.windowHandle())
# self.TBPROG.setVisible(False)
# except ImportError:
# pass
else: # PySide2
qt_app.exec_()

View File

@ -708,6 +708,7 @@ class Flashcart_DMG_BUNG_16M(Flashcart):
self.CartWrite([[0x2000, 0x02]], fast_write=False)
self.CartWrite([[0x6AAA, 0x90]], fast_write=True)
cart_flash_id = list(self.CartRead(0, 4))
verified = False
if rom != cart_flash_id and cart_flash_id == self.CONFIG["flash_ids"][0]:
self.Reset()
verified = True

View File

@ -116,7 +116,7 @@ class LK_Device(ABC):
"AGB_IRQ_ENABLED":[8, 0x10],
}
ACTIONS = {"ROM_READ":1, "SAVE_READ":2, "SAVE_WRITE":3, "ROM_WRITE":4, "ROM_WRITE_VERIFY":4, "SAVE_WRITE_VERIFY":3, "RTC_WRITE":5}
ACTIONS = {"ROM_READ":1, "SAVE_READ":2, "SAVE_WRITE":3, "ROM_WRITE":4, "ROM_WRITE_VERIFY":4, "SAVE_WRITE_VERIFY":3, "RTC_WRITE":5, "DETECT_CART":6}
SUPPORTED_CARTS = {}
FW = {}
@ -229,8 +229,14 @@ class LK_Device(ABC):
#################################################################
def IsSupportedMbc(self, mbc):
return mbc in ( 0x00, 0x01, 0x02, 0x03, 0x05, 0x06, 0x08, 0x09, 0x0B, 0x0D, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x20, 0x22, 0xFC, 0xFD, 0xFE, 0xFF, 0x101, 0x103, 0x104, 0x105, 0x110, 0x201, 0x202, 0x203, 0x204, 0x205 )
return mbc in ( 0x00, 0x01, 0x02, 0x03, 0x05, 0x06, 0x08, 0x09, 0x0B, 0x0D, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x20, 0x22, 0xFC, 0xFD, 0xFE, 0xFF, 0x101, 0x103, 0x104, 0x105, 0x110, 0x201, 0x202, 0x203, 0x204, 0x205, 0x206 )
def IsUnregistered(self):
if "unregistered" in self.FW:
return self.FW["unregistered"]
else:
return False
def TryConnect(self, port, baudrate):
dprint("Trying to connect to {:s} at baud rate {:d} ({:s})".format(port, baudrate, type(self).__module__))
try:
@ -635,11 +641,11 @@ class LK_Device(ABC):
ret = self._read(1)
if ret != 0x01:
if ret is False:
msg = "Error: No response while trying to write to the device."
msg = "Error: No response while trying to communicate with the device."
time.sleep(0.5)
self.DEVICE.reset_input_buffer()
else:
msg = "Error: Bad response “{:s}” while trying to write to the device.".format(str(ret))
msg = "Error: Bad response “{:s}” while trying to communicate with the device.".format(str(ret))
print(f"{ANSI.RED}{msg}{ANSI.RESET}")
dprint(msg)
#self.CANCEL_ARGS.update({"info_type":"msgbox_critical", "info_msg":"A critical communication error occured during a write. Please avoid passive USB hubs, try different USB ports/cables and re-connect the device."})
@ -741,7 +747,7 @@ class LK_Device(ABC):
if self.DEVICE.in_waiting == 0:
dprint("Waiting for ACK...")
hp -= 1
time.sleep(0.05)
time.sleep(0.1)
continue
temp = self.DEVICE.read(self.DEVICE.in_waiting)
if len(temp) >= 1: temp = temp[len(temp) - 1]
@ -751,6 +757,13 @@ class LK_Device(ABC):
self._write(self.DEVICE_CMD["QUERY_CART_PWR"])
time.sleep(0.05)
hp -= 1
if hp == 0:
self.DEVICE.close()
self.DEVICE = None
self.ERROR = True
raise BrokenPipeError("Couldnt power on the cartridge.")
self.DEVICE.timeout = self.DEVICE_TIMEOUT
self._write(self.DEVICE_CMD["QUERY_CART_PWR"])
@ -854,21 +867,25 @@ class LK_Device(ABC):
def GetSupportedCartridgesAGB(self):
return (list(self.SUPPORTED_CARTS['AGB'].keys()), list(self.SUPPORTED_CARTS['AGB'].values()))
def SetProgress(self, args):
def SetProgress(self, args, signal=None):
if self.CANCEL and args["action"] not in ("ABORT", "FINISHED", "ERROR"): return
if "pos" in args: self.POS = args["pos"]
if args["action"] == "UPDATE_POS": self.INFO["transferred"] = args["pos"]
if signal is None:
signal = self.SIGNAL
try:
self.SIGNAL.emit(args)
signal.emit(args)
except AttributeError:
if self.SIGNAL is not None:
self.SIGNAL(args)
if signal is not None:
signal(args)
if args["action"] == "INITIALIZE":
if self.CanPowerCycleCart(): self.CartPowerOn() # Ensure cart is powered
self.POS = 0
elif args["action"] == "FINISHED":
self.POS = 0
signal = None
self.SIGNAL = None
def Debug(self):
@ -905,7 +922,7 @@ class LK_Device(ABC):
header = self.ReadROM(0, 0x180)
if ".dev" in Util.VERSION_PEP440 or Util.DEBUG:
with open("debug_header.bin", "wb") as f: f.write(header)
with open(Util.CONFIG_PATH + "/debug_header.bin", "wb") as f: f.write(header)
# Parse ROM header
if self.MODE == "DMG":
@ -1069,7 +1086,19 @@ class LK_Device(ABC):
return data
def DetectCartridge(self, mbc=None, limitVoltage=False, checkSaveType=True):
def _DetectCartridge(self, args): # Wrapper for thread call
self.SetProgress({"action":"INITIALIZE", "abortable":False, "method":"DETECT_CART"})
signal = self.SIGNAL
self.SIGNAL = None
ret = self.DoDetectCartridge(mbc=None, limitVoltage=args["limitVoltage"], checkSaveType=args["checkSaveType"], signal=signal)
self.INFO["detect_cart"] = ret
self.INFO["last_action"] = self.ACTIONS["DETECT_CART"]
self.INFO["action"] = None
self.SIGNAL = signal
self.SetProgress({"action":"FINISHED"})
return True
def DoDetectCartridge(self, mbc=None, limitVoltage=False, checkSaveType=True, signal=None):
self.SIGNAL = None
self.CANCEL = False
self.ERROR = False
@ -1079,8 +1108,10 @@ class LK_Device(ABC):
sram_unstable = None
save_size = None
checkBatterylessSRAM = False
_apot = 0
# Header
if signal is not None: self.SetProgress({"action":"UPDATE_INFO", "text":"Detecting ROM..."}, signal=signal)
info = self.ReadInfo(checkRtc=True)
if self.MODE == "DMG" and mbc is None:
mbc = info["mapper_raw"]
@ -1097,6 +1128,7 @@ class LK_Device(ABC):
self._set_fw_variable("AUTO_POWEROFF_TIME", 5000)
# Detect Flash Cart
if signal is not None: self.SetProgress({"action":"UPDATE_INFO", "text":"Detecting Flash..."}, signal=signal)
ret = self.DetectFlash(limitVoltage=limitVoltage)
if ret is False: return False
(cart_types, cart_type_id, flash_id, cfi_s, cfi, detected_size) = ret
@ -1113,6 +1145,7 @@ class LK_Device(ABC):
elif self.MODE == "AGB" and "flash_bank_select_type" in cart_type and cart_type["flash_bank_select_type"] > 1:
checkSaveType = False
elif self.MODE == "DMG" and "mbc" in cart_type and cart_type["mbc"] == 0x105: # G-MMC1
if signal is not None: self.SetProgress({"action":"UPDATE_INFO", "text":"Detecting GB-Memory..."}, signal=signal)
header = self.ReadROM(0, 0x180)
data = RomFileDMG(header).GetHeader()
_mbc = DMG_MBC().GetInstance(args={"mbc":cart_type["mbc"]}, cart_write_fncptr=self._cart_write, cart_read_fncptr=self._cart_read, cart_powercycle_fncptr=self.CartPowerCycleOrAskReconnect, clk_toggle_fncptr=self._clk_toggle)
@ -1131,6 +1164,7 @@ class LK_Device(ABC):
# Save Type and Size
if checkSaveType:
if signal is not None: self.SetProgress({"action":"UPDATE_INFO", "text":"Detecting save type..."}, signal=signal)
if self.MODE == "DMG":
save_size = 131072
save_type = 0x04
@ -1148,6 +1182,8 @@ class LK_Device(ABC):
args = { 'mode':2, 'path':None, 'mbc':mbc, 'save_type':save_type, 'rtc':False, 'detect':True }
elif self.MODE == "AGB":
args = { 'mode':2, 'path':None, 'mbc':mbc, 'save_type':8, 'rtc':False, 'detect':True }
else:
return
ret = self._BackupRestoreRAM(args=args)
@ -1175,6 +1211,10 @@ class LK_Device(ABC):
if self.INFO["data"][i:i+3] != bytearray([self.INFO["data"][i]] * 3):
check = False
break
if self.INFO["data"][0:0x8000] == self.INFO["data"][0x8000:0x10000]: # MBCX
check = True
if check:
save_size = 32768
save_type = 0x03
@ -1201,7 +1241,13 @@ class LK_Device(ABC):
if flash_save_id != 0 and flash_save_id in Util.AGB_Flash_Save_Chips:
save_size = Util.AGB_Flash_Save_Chips_Sizes[list(Util.AGB_Flash_Save_Chips).index(flash_save_id)]
save_chip = Util.AGB_Flash_Save_Chips[flash_save_id]
if save_size == 131072:
if flash_save_id in (0xBF5B, 0xFFFF): # Bootlegs
if self.INFO["data"][0:0x10000] == self.INFO["data"][0x10000:0x20000]:
save_type = 4
else:
save_type = 5
elif save_size == 131072:
save_type = 5
elif save_size == 65536:
save_type = 4
@ -1395,6 +1441,8 @@ class LK_Device(ABC):
command = "DMG_CART_READ"
elif self.MODE == "AGB":
command = "AGB_CART_READ"
else:
raise NotImplementedError
for n in range(0, num):
self._write(self.DEVICE_CMD[command])
@ -1944,7 +1992,7 @@ class LK_Device(ABC):
chunk_len = max_length if left > max_length else left
chunk_from = offset + chunk_pos
chunk_to = offset + chunk_pos + chunk_len
dprint(f"Running CRC32 verification, comparing between source 0x{chunk_from:x}~0x{chunk_to:x} and target 0x{address+chunk_pos:x}~0x{address+chunk_pos+chunk_len:x}")
dprint(f"Running CRC32 verification, comparing between source 0x{chunk_from:X}~0x{chunk_to:X} and target 0x{address+chunk_pos:X}~0x{address+chunk_pos+chunk_len:X}")
crc32_expected = zlib.crc32(buffer[chunk_from:chunk_to])
for i in range(0, 2 if (reset is True and flashcart is not None) else 1): # for retrying with reset
@ -2008,6 +2056,9 @@ class LK_Device(ABC):
self.SetAGBReadMethod(0)
self._write(self.DEVICE_CMD["SET_MODE_AGB"], wait=self.FW["fw_ver"] >= 12)
else:
raise NotImplementedError
rom = self._cart_read(0, 8)
dprint("Resetting and unlocking all cart types")
@ -2182,7 +2233,7 @@ class LK_Device(ABC):
flash_id_methods.append([we - 1, i, list(cmp), cfi_buffer, flash_id_cmds[i]["read_identifier"]])
if ".dev" in Util.VERSION_PEP440 or Util.DEBUG:
with open("debug_cfi.bin", "wb") as f: f.write(cfi_buffer)
with open(Util.CONFIG_PATH + "/debug_cfi.bin", "wb") as f: f.write(cfi_buffer)
try:
magic = "{:s}{:s}{:s}".format(chr(cfi_buffer[0x20]), chr(cfi_buffer[0x22]), chr(cfi_buffer[0x24]))
d_swap = (0, 0)
@ -2199,7 +2250,7 @@ class LK_Device(ABC):
for j in range(0, len(cfi_buffer)):
cfi_buffer[j] = bitswap(cfi_buffer[j], d_swap[j2])
if ".dev" in Util.VERSION_PEP440 or Util.DEBUG:
with open("debug_cfi_d0d1+d6d7.bin", "wb") as f: f.write(cfi_buffer)
with open(Util.CONFIG_PATH + "/debug_cfi_d0d1+d6d7.bin", "wb") as f: f.write(cfi_buffer)
else:
cfi_buffer = None
except:
@ -2281,6 +2332,8 @@ class LK_Device(ABC):
supp_flash_types = self.GetSupportedCartridgesDMG()
elif self.MODE == "AGB":
supp_flash_types = self.GetSupportedCartridgesAGB()
else:
raise NotImplementedError
if "flash_size" in supp_flash_types[1][flash_types[0]]:
size = supp_flash_types[1][flash_types[0]]["flash_size"]
@ -2381,6 +2434,9 @@ class LK_Device(ABC):
def FlashROM(self, fncSetProgress=None, args=None):
self.DoTransfer(4, fncSetProgress, args)
def DetectCartridge(self, fncSetProgress=None, args=None):
self.DoTransfer(5, fncSetProgress, args)
#################################################################
def _BackupROM(self, args):
@ -2416,8 +2472,10 @@ class LK_Device(ABC):
# Firmware check L8
if self.FW["fw_ver"] < 8 and flashcart and "enable_pullups" in cart_type:
self.SetProgress({"action":"ABORT", "info_type":"msgbox_critical", "info_msg":"This cartridge type requires at least firmware version L8.", "abortable":False})
return False
#self.SetProgress({"action":"ABORT", "info_type":"msgbox_critical", "info_msg":"This cartridge type requires at least firmware version L8.", "abortable":False})
#return False
print("{:s}Note: This cartridge may not be fully compatible with your {:s} device running an old or legacy firmware version.{:s}".format(ANSI.YELLOW, self.FW["pcb_name"], ANSI.RESET))
del(cart_type["enable_pullups"])
# Firmware check L8
buffer_len = 0x4000
@ -2712,12 +2770,11 @@ class LK_Device(ABC):
pos_total += len(temp)
if "verify_write" in args:
#if pos_total >= len(args["verify_write"]): break
check = args["verify_write"][pos_total-len(temp):pos_total]
if temp[:len(check)] != check:
if ".dev" in Util.VERSION_PEP440 or Util.DEBUG:
dprint("Writing 0x{:X} bytes to debug_verify_0x{:X}.bin".format(len(temp), pos_total-len(temp)))
with open("debug_verify_0x{:X}.bin".format(pos_total-len(temp)), "ab") as f: f.write(temp)
with open(Util.CONFIG_PATH + "/debug_verify_0x{:X}.bin".format(pos_total-len(temp)), "ab") as f: f.write(temp)
for i in range(0, pos_total):
if (i < len(args["verify_write"]) - 1) and (i < pos_total - 1) and args["verify_write"][i] != buffer[i]:
@ -2818,7 +2875,7 @@ class LK_Device(ABC):
# Check for ROM loops
self.INFO["loop_detected"] = False
temp = min(0x2000000, len(buffer))
temp = len(buffer)
while temp > 0x4000:
temp = temp >> 1
if (buffer[0:0x4000] == buffer[temp:temp+0x4000]):
@ -2861,6 +2918,8 @@ class LK_Device(ABC):
_agb_gpio = AGB_GPIO(args={"rtc":True}, cart_write_fncptr=self._cart_write, cart_read_fncptr=self._cart_read, cart_powercycle_fncptr=self.CartPowerCycleOrAskReconnect, clk_toggle_fncptr=self._clk_toggle)
if _agb_gpio.HasRTC() is not True: return False
ret = _agb_gpio.WriteRTCDict(args["rtc_dict"])
else:
raise NotImplementedError
return ret
def _BackupRestoreRAM(self, args):
@ -2872,6 +2931,12 @@ class LK_Device(ABC):
empty_data_byte = 0x00
extra_size = 0
audio_low = False
# Initialization
ram_banks = 0
buffer_len = 0
sram_5 = 0
temp = None
cart_type = None
if "cart_type" in args and args["cart_type"] >= 0:
@ -3052,6 +3117,7 @@ class LK_Device(ABC):
if args["rtc"] is True:
extra_size = 0x10
action = None
if args["mode"] == 2: # Backup
action = "SAVE_READ"
buffer = bytearray()
@ -3467,7 +3533,14 @@ class LK_Device(ABC):
return True
def _FlashROM(self, args):
# Initialization
self.FAST_READ = True
temp = None
rom_bank_size = 0
end_bank = 0
pos_from = 0
verify_len = 0
if "buffer" in args:
data_import = args["buffer"]
else:
@ -3549,13 +3622,17 @@ class LK_Device(ABC):
# Firmware check L5
# Firmware check L8
if (self.FW["fw_ver"] < 8 and "enable_pullups" in cart_type and cart_type["enable_pullups"] is True):
self.SetProgress({"action":"ABORT", "info_type":"msgbox_critical", "info_msg":"This cartridge type requires at least firmware version L8.", "abortable":False})
return False
#self.SetProgress({"action":"ABORT", "info_type":"msgbox_critical", "info_msg":"This cartridge type requires at least firmware version L8.", "abortable":False})
#return False
print("{:s}Note: This cartridge may not be fully compatible with your {:s} device running an old or legacy firmware version.{:s}".format(ANSI.YELLOW, self.FW["pcb_name"], ANSI.RESET))
del(cart_type["enable_pullups"])
# Firmware check L8
# Firmware check L12
if (self.FW["fw_ver"] < 12 and "set_irq_high" in cart_type and cart_type["set_irq_high"] is True):
self.SetProgress({"action":"ABORT", "info_type":"msgbox_critical", "info_msg":"This cartridge type requires at least firmware version L12.", "abortable":False})
return False
#self.SetProgress({"action":"ABORT", "info_type":"msgbox_critical", "info_msg":"This cartridge type requires at least firmware version L12.", "abortable":False})
#return False
print("{:s}Note: This cartridge may not be fully compatible with your {:s} device running an old or legacy firmware version.{:s}".format(ANSI.YELLOW, self.FW["pcb_name"], ANSI.RESET))
del(cart_type["set_irq_high"])
if (self.FW["fw_ver"] < 12 and "status_register_mask" in cart_type):
self.SetProgress({"action":"ABORT", "info_type":"msgbox_critical", "info_msg":"This cartridge type requires at least firmware version L12.", "abortable":False})
return False
@ -3704,7 +3781,7 @@ class LK_Device(ABC):
else:
dprint("Hidden sector data:", args["buffer_map"])
if ".dev" in Util.VERSION_PEP440 or Util.DEBUG:
with open("debug_mmsa_map.bin", "wb") as f: f.write(args["buffer_map"])
with open(Util.CONFIG_PATH + "/debug_mmsa_map.bin", "wb") as f: f.write(args["buffer_map"])
data_map_import = copy.copy(args["buffer_map"])
data_map_import = bytearray(data_map_import)
dprint("Hidden sector data loaded")
@ -4030,9 +4107,6 @@ class LK_Device(ABC):
sector_pos = sector_offsets.index(sector[:2])
start_bank = math.floor(buffer_pos / rom_bank_size)
end_bank = math.ceil((buffer_pos + sector[1]) / rom_bank_size)
# print(hex(start_address), hex(end_address), start_bank, end_bank)
# print(hex(sector_offsets[sector_pos][0]), sector_pos)
# print("")
elif self.MODE == "DMG":
dprint("Writing sector:", hex(sector[0]), hex(sector[1]))
buffer_pos = sector[0]
@ -4043,12 +4117,88 @@ class LK_Device(ABC):
sector_pos = sector_offsets.index(sector[:2])
start_bank = math.floor(buffer_pos / rom_bank_size)
end_bank = math.ceil((buffer_pos + sector[1]) / rom_bank_size)
# print(hex(start_address), hex(end_address), start_bank, end_bank)
# print(hex(sector_offsets[sector_pos][0]), sector_pos)
# print("")
#for bank in range(start_bank, end_bank):
bank = start_bank
# ↓↓↓ Check if data matches already
if self.FW["fw_ver"] >= 10 and not (flashcart and cart_type["command_set"] == "GBAMP"):
if "verify_write" in args and args["verify_write"] is True:
buffer_pos_matchcheck = buffer_pos
verified = False
ts_se_start = time.time()
while bank < end_bank:
status = None
# ↓↓↓ Switch ROM bank
if self.MODE == "DMG":
if _mbc.ResetBeforeBankChange(bank) is True:
dprint("Resetting the MBC")
self._write(self.DEVICE_CMD["DMG_MBC_RESET"], wait=True)
(start_address, bank_size) = _mbc.SelectBankROM(bank)
if flashcart.PulseResetAfterWrite():
if bank == 0:
if self.FW["fw_ver"] < 2:
self._write(self.DEVICE_CMD["OFW_GB_CART_MODE"])
else:
self._write(self.DEVICE_CMD["SET_MODE_DMG"], wait=self.FW["fw_ver"] >= 12)
self._write(self.DEVICE_CMD["DMG_MBC_RESET"], wait=True)
# else:
# self._write(self.DEVICE_CMD["OFW_GB_FLASH_BANK_1_COMMAND_WRITES"])
self._set_fw_variable("DMG_ROM_BANK", bank)
buffer_len = min(buffer_len, bank_size)
if "start_addr" in flashcart.CONFIG and bank == 0: start_address = flashcart.CONFIG["start_addr"]
end_address = start_address + bank_size
start_address += (buffer_pos % rom_bank_size)
if end_address > start_address + sector[1]:
end_address = start_address + sector[1]
elif self.MODE == "AGB":
if "flash_bank_select_type" in cart_type and cart_type["flash_bank_select_type"] > 0:
if bank != current_bank:
flashcart.Reset(full_reset=True)
flashcart.SelectBankROM(bank)
temp = end_address - start_address
start_address %= cart_type["flash_bank_size"]
end_address = min(cart_type["flash_bank_size"], start_address + temp)
current_bank = bank
# ↑↑↑ Switch ROM bank
pos = start_address
#dprint("pos=0x{:X}, buffer_pos=0x{:X}, start_address=0x{:X}, end_address=0x{:X}".format(pos, buffer_pos_matchcheck, start_address, end_address))
verified = self.CompareCRC32(buffer=data_import, offset=buffer_pos_matchcheck, length=end_address - pos, address=pos, flashcart=flashcart, reset=True)
self.SetProgress({"action":"UPDATE_POS", "pos":buffer_pos_matchcheck})
if verified is not True:
break
buffer_pos_matchcheck += (end_address - pos)
bank += 1
if verified is True:
dprint("Skipping sector #{:d}, because the CRC32 matched".format(sector_pos))
if flashcart.FlashCommandsOnBank1(): _mbc.SelectBankROM(bank)
self.NO_PROG_UPDATE = True
se_ret = flashcart.SectorErase(pos=pos, buffer_pos=buffer_pos, skip=verified)
self.NO_PROG_UPDATE = False
if self.CANCEL:
cancel_args = {"action":"ABORT", "abortable":False}
cancel_args.update(self.CANCEL_ARGS)
self.CANCEL_ARGS = {}
self.ERROR_ARGS = {}
self.SetProgress(cancel_args)
if self.CanPowerCycleCart(): self.CartPowerCycle()
return
ts_se_elapsed = time.time() - ts_se_start
if se_ret:
sector_size = se_ret
dprint("Next sector size: 0x{:X}".format(sector_size))
buffer_pos += sector_size
continue
else:
verified = False
bank = start_bank
# ↑↑↑ Check if data matches already
while bank < end_bank:
if self.CANCEL:
cancel_args = {"action":"ABORT", "abortable":False}
@ -4116,44 +4266,20 @@ class LK_Device(ABC):
se_ret = None
if chip_erase is False:
if sector_pos < len(sector_offsets) and buffer_pos == sector_offsets[sector_pos][0]:
len_rest = end_address - pos
skip_se = False
ts_se_start = time.time()
if self.FW["fw_ver"] >= 12 and "compare_sectors" in args and args["compare_sectors"] is True and sector[1] <= len_rest and not (flashcart and cart_type["command_set"] == "GBAMP"):
verified = self.CompareCRC32(buffer=data_import, offset=sector[0], length=sector[1], address=start_address, flashcart=flashcart, reset=True)
if verified is True:
skip_se = True
elif verified is not True and len(verified) == 2:
dprint("Writing sector #{:d}, because the CRC32 didnt match: 0x{:X} ≠ 0x{:X}".format(sector_pos, verified[0], verified[1]))
verified = False
skip_se = False
if self.CANCEL:
cancel_args = {"action":"ABORT", "abortable":False}
cancel_args.update(self.CANCEL_ARGS)
self.CANCEL_ARGS = {}
self.ERROR_ARGS = {}
self.SetProgress(cancel_args)
if self.CanPowerCycleCart(): self.CartPowerCycle()
return
if skip_se is True:
dprint("Skipping sector #{:d}, because the CRC32 matched".format(sector_pos))
self.SetProgress({"action":"UPDATE_POS", "pos":buffer_pos, "sector_pos":sector_pos, "force_update":True})
else:
if sector not in verify_sectors:
verify_sectors.append(sector)
dprint("Erasing sector #{:d} at position 0x{:X} (0x{:X})".format(sector_pos, buffer_pos, pos))
self.SetProgress({"action":"UPDATE_POS", "pos":buffer_pos, "force_update":True})
if self.CanPowerCycleCart(): self.CartPowerOn()
dprint("Erasing sector #{:d} at position 0x{:X} (0x{:X})".format(sector_pos, buffer_pos, pos))
self.SetProgress({"action":"UPDATE_POS", "pos":buffer_pos, "force_update":True})
if self.CanPowerCycleCart(): self.CartPowerOn()
sector_pos += 1
if flashcart.FlashCommandsOnBank1(): _mbc.SelectBankROM(bank)
self.NO_PROG_UPDATE = True
se_ret = flashcart.SectorErase(pos=pos, buffer_pos=buffer_pos, skip=skip_se)
se_ret = flashcart.SectorErase(pos=pos, buffer_pos=buffer_pos, skip=False)
self.NO_PROG_UPDATE = False
if "from_user" in self.CANCEL_ARGS and self.CANCEL_ARGS["from_user"]:
continue
if sector not in verify_sectors:
verify_sectors.append(sector)
ts_se_elapsed = time.time() - ts_se_start
if se_ret:
@ -4161,11 +4287,6 @@ class LK_Device(ABC):
sector_size = se_ret
dprint("Next sector size: 0x{:X}".format(sector_size))
skip_init = False
if skip_se:
buffer_pos += sector_size
pos += sector_size
continue
# ↑↑↑ Sector erase
if se_ret is not False:
@ -4313,7 +4434,7 @@ class LK_Device(ABC):
if "verify_write" in args and args["verify_write"] is True:
self.SetProgress({"action":"INITIALIZE", "method":"ROM_WRITE_VERIFY", "size":len(data_import), "flash_offset":flash_offset})
if ".dev" in Util.VERSION_PEP440 or Util.DEBUG:
with open("debug_verify.bin", "wb") as f: pass
with open(Util.CONFIG_PATH + "/debug_verify.bin", "wb") as f: pass
current_bank = None
broken_sectors = []
@ -4502,6 +4623,8 @@ class LK_Device(ABC):
self.CANCEL_ARGS = {}
self.READ_ERRORS = 0
self.WRITE_ERRORS = 0
_apot = 0
if self.IsConnected():
_apoe = False
if self.CanPowerCycleCart():
@ -4524,6 +4647,7 @@ class LK_Device(ABC):
elif args['mode'] == 2: ret = self._BackupRestoreRAM(args)
elif args['mode'] == 3: ret = self._BackupRestoreRAM(args)
elif args['mode'] == 4: ret = self._FlashROM(args)
elif args['mode'] == 5: ret = self._DetectCartridge(args)
elif args['mode'] == 0xFF: self.Debug()
if self.FW is None: return False
if self.FW["fw_ver"] >= 2 and self.FW["pcb_name"] == "GBxCart RW":

View File

@ -75,8 +75,8 @@ class DMG_MBC:
return DMG_Unlicensed_Sachen(args=args, cart_write_fncptr=cart_write_fncptr, cart_read_fncptr=cart_read_fncptr, cart_powercycle_fncptr=cart_powercycle_fncptr, clk_toggle_fncptr=clk_toggle_fncptr)
elif mbc_id == 0x205: # 0x205:'Datel Orbit V2',
return DMG_Unlicensed_DatelOrbitV2(args=args, cart_write_fncptr=cart_write_fncptr, cart_read_fncptr=cart_read_fncptr, cart_powercycle_fncptr=cart_powercycle_fncptr, clk_toggle_fncptr=clk_toggle_fncptr)
# elif mbc_id == 0x206: # 0x206:'Datel MegaMem',
# return DMG_Unlicensed_DatelMegaMem(args=args, cart_write_fncptr=cart_write_fncptr, cart_read_fncptr=cart_read_fncptr, cart_powercycle_fncptr=cart_powercycle_fncptr, clk_toggle_fncptr=clk_toggle_fncptr)
elif mbc_id == 0x206: # 0x206:'MBCX',
return DMG_Unlicensed_MBCX(args=args, cart_write_fncptr=cart_write_fncptr, cart_read_fncptr=cart_read_fncptr, cart_powercycle_fncptr=cart_powercycle_fncptr, clk_toggle_fncptr=clk_toggle_fncptr)
else:
self.__init__(args=args, cart_write_fncptr=cart_write_fncptr, cart_read_fncptr=cart_read_fncptr, cart_powercycle_fncptr=cart_powercycle_fncptr, clk_toggle_fncptr=clk_toggle_fncptr)
return self
@ -269,7 +269,7 @@ class DMG_MBC3(DMG_MBC):
def HasRTC(self):
dprint("Checking for RTC")
if self.MBC_ID not in (0x0F, 0x10, 0x110):
if self.MBC_ID not in (0x0F, 0x10, 0x110, 0x206):
dprint("No RTC because mapper value is not used for RTC:", self.MBC_ID)
return False
self.EnableRAM(enable=False)
@ -831,6 +831,7 @@ class DMG_GMMC1(DMG_MBC5):
def CalcChecksum(self, buffer):
header = RomFileDMG(buffer[:0x180]).GetHeader()
target_chk_value = 0
target_sha1_value = 0
if header["game_title"] == "NP M-MENU MENU":
target_sha1_value = "15f5d445c0b2fdf4221cf2a986a4a5cb8dfda131"
target_chk_value = 0x19E8
@ -1531,6 +1532,44 @@ class DMG_Unlicensed_DatelOrbitV2(DMG_MBC):
def GetMaxROMSize(self):
return 128*1024
class DMG_Unlicensed_MBCX(DMG_MBC3):
def GetName(self):
return "MBCX"
def HasFlashBanks(self):
return True
def SelectBankFlash(self, index):
dprint(self.GetName(), "|SelectBankFlash()|", index)
commands = [
[ 0x0000, 0x05 ],
[ 0x4000, 0x82 ],
[ 0xA000, index ],
[ 0x0000, 0x00 ]
]
self.CURRENT_FLASH_BANK = index
self.CartWrite(commands, delay=0.1)
def SelectBankROM(self, index):
dprint(self.GetName(), index)
if (index % 512 == 0) or (self.CURRENT_FLASH_BANK != math.floor(index / 512)):
self.SelectBankFlash(math.floor(index / 512))
self.CURRENT_ROM_BANK = index
index = index % 512
commands = [
[ 0x3000, ((index >> 8) & 0xFF) ],
[ 0x2100, index & 0xFF ],
]
self.CartWrite(commands)
return (0x4000, self.ROM_BANK_SIZE)
def GetMaxROMSize(self):
return 32*1024*1024
class AGB_GPIO:
CART_WRITE_FNCPTR = None

View File

@ -110,7 +110,7 @@ class PocketCamera:
offset = 0x2000 + (index * 0x1000)
elif index == 30:
offset = 0x11FC
elif index == 31:
else:
offset = 0
imgbuffer = self.DATA[offset:offset+0x1000]
return self.ConvertPicture(imgbuffer)
@ -138,7 +138,7 @@ class PocketCamera:
frame.paste(pic, (left, top))
pic = frame
pic = pic.resize((pic.width * scale, pic.height * scale), Image.NEAREST)
pic = pic.resize((pic.width * scale, pic.height * scale), Image.Resampling.NEAREST)
ext = os.path.splitext(path)[1]
if ext == "" or ext.lower() == ".png":

View File

@ -309,13 +309,13 @@ class PocketCameraWindow(QtWidgets.QDialog):
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))
self.CUR_THUMBS[i] = ImageQt(pic.resize((47, 41), Image.Resampling.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
resampler = Image.Resampling.NEAREST
if self.CUR_BICUBIC: resampler = Image.Resampling.BICUBIC
cam = self.CUR_PC
if cam is None: return
@ -323,7 +323,7 @@ class PocketCameraWindow(QtWidgets.QDialog):
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 >= 30:
self.CUR_PIC = ImageQt(cam.GetPicture(index).convert("RGBA").resize((256, 224), Image.BICUBIC if index == 31 else resampler))
self.CUR_PIC = ImageQt(cam.GetPicture(index).convert("RGBA").resize((256, 224), Image.Resampling.BICUBIC if index == 31 else resampler))
else:
self.CUR_PIC = ImageQt(cam.GetPicture(index).convert("RGBA").resize((256, 224), resampler))
self.lblPhoto[index].setStyleSheet("border: 3px solid green; padding: 1px;")

View File

@ -431,6 +431,12 @@ class RomFileDMG:
temp = self.LogoToImage(buffer[0x104:0x104+0x30])
if temp is not False: data["logo_sachen"] = temp
# GBFlash MBCX
if data["game_title"] == "MBCX_MENU":
data["rom_size_raw"] = 0x0A
data["ram_size_raw"] = 0x03
data["mapper_raw"] = 0x206
if data["mapper_raw"] in Util.DMG_Header_Mapper:
data["mapper"] = Util.DMG_Header_Mapper[data["mapper_raw"]]
elif data["logo_correct"]:

View File

@ -2,15 +2,15 @@
# FlashGBX
# Author: Lesserkuma (github.com/lesserkuma)
import math, time, datetime, copy, configparser, threading, os, platform, traceback, io, struct, re, statistics, random
import math, time, datetime, copy, configparser, threading, os, platform, traceback, io, struct, re, statistics, random, sys
from io import StringIO
from enum import Enum
# Common constants
APPNAME = "FlashGBX"
VERSION_PEP440 = "4.2"
VERSION_PEP440 = "4.3"
VERSION = "v{:s}".format(VERSION_PEP440)
VERSION_TIMESTAMP = 1722797669
VERSION_TIMESTAMP = 1731015769
DEBUG = False
DEBUG_LOG = []
APP_PATH = ""
@ -23,11 +23,11 @@ AGB_Header_Save_Sizes = [ 0, 512, 8192, 32768, 65536, 131072, 1048576, 65536, 13
AGB_Flash_Save_Chips = { 0xBFD4:"SST 39VF512", 0x1F3D:"Atmel AT29LV512", 0xC21C:"Macronix MX29L512", 0x321B:"Panasonic MN63F805MNP", 0xC209:"Macronix MX29L010", 0x6213:"SANYO LE26FV10N1TS", 0xBF5B:"Unlicensed SST49LF080A", 0xFFFF:"Unlicensed 0xFFFF" }
AGB_Flash_Save_Chips_Sizes = [ 0x10000, 0x10000, 0x10000, 0x10000, 0x20000, 0x20000, 0x20000, 0x20000 ]
DMG_Header_Mapper = { 0x00:'None', 0x01:'MBC1', 0x02:'MBC1+SRAM', 0x03:'MBC1+SRAM+BATTERY', 0x06:'MBC2+SRAM+BATTERY', 0x0F:'MBC3+RTC+BATTERY', 0x10:'MBC3+RTC+SRAM+BATTERY', 0x110:'MBC30+RTC+SRAM+BATTERY', 0x12:'MBC3+SRAM', 0x13:'MBC3+SRAM+BATTERY', 0x19:'MBC5', 0x1A:'MBC5+SRAM', 0x1B:'MBC5+SRAM+BATTERY', 0x1C:'MBC5+RUMBLE', 0x1E:'MBC5+RUMBLE+SRAM+BATTERY', 0x20:'MBC6+SRAM+FLASH+BATTERY', 0x22:'MBC7+ACCELEROMETER+EEPROM', 0x101:'MBC1M', 0x103:'MBC1M+SRAM+BATTERY', 0x0B:'MMM01', 0x0D:'MMM01+SRAM+BATTERY', 0xFC:'MAC-GBD+SRAM+BATTERY', 0x105:'G-MMC1+SRAM+BATTERY', 0x104:'M161', 0xFF:'HuC-1+IR+SRAM+BATTERY', 0xFE:'HuC-3+RTC+SRAM+BATTERY', 0xFD:'TAMA5+RTC+EEPROM', 0x201:'Unlicensed 256M Mapper', 0x202:'Unlicensed Wisdom Tree Mapper', 0x203:'Unlicensed Xploder GB Mapper', 0x204:'Unlicensed Sachen Mapper', 0x205:'Unlicensed Datel Orbit V2 Mapper' }
DMG_Mapper_Types = { "None":[ 0x00, 0x08, 0x09 ], "MBC1":[ 0x01, 0x02, 0x03 ], "MBC2":[ 0x05, 0x06 ], "MBC3":[ 0x0F, 0x10, 0x11, 0x12, 0x13 ], "MBC30":[ 0x110 ], "MBC5":[ 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E ], "MBC6":[ 0x20 ], "MBC7":[ 0x22 ], "MBC1M":[ 0x101, 0x103 ], "MMM01":[ 0x0B, 0x0D ], "MAC-GBD":[ 0xFC ], "G-MMC1":[ 0x105 ], "M161":[ 0x104 ], "HuC-1":[ 0xFF ], "HuC-3":[ 0xFE ], "TAMA5":[ 0xFD ], "Unlicensed 256M Multi Cart Mapper":[ 0x201 ], "Unlicensed Wisdom Tree Mapper":[ 0x202 ], "Unlicensed Xploder GB Mapper":[ 0x203 ], "Unlicensed Sachen Mapper":[ 0x204 ], "Unlicensed Datel Orbit V2 Mapper":[ 0x205 ] }
DMG_Header_ROM_Sizes = [ "32 KiB", "64 KiB", "128 KiB", "256 KiB", "512 KiB", "1 MiB", "2 MiB", "4 MiB", "8 MiB", "16 MiB", "32 MiB" ]
DMG_Header_ROM_Sizes_Map = [ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A ]
DMG_Header_ROM_Sizes_Flasher_Map = [ 0x8000, 0x10000, 0x20000, 0x40000, 0x80000, 0x100000, 0x200000, 0x400000, 0x800000, 0x1000000, 0x2000000 ]
DMG_Header_Mapper = { 0x00:'None', 0x01:'MBC1', 0x02:'MBC1+SRAM', 0x03:'MBC1+SRAM+BATTERY', 0x06:'MBC2+SRAM+BATTERY', 0x0F:'MBC3+RTC+BATTERY', 0x10:'MBC3+RTC+SRAM+BATTERY', 0x110:'MBC30+RTC+SRAM+BATTERY', 0x12:'MBC3+SRAM', 0x13:'MBC3+SRAM+BATTERY', 0x19:'MBC5', 0x1A:'MBC5+SRAM', 0x1B:'MBC5+SRAM+BATTERY', 0x1C:'MBC5+RUMBLE', 0x1E:'MBC5+RUMBLE+SRAM+BATTERY', 0x20:'MBC6+SRAM+FLASH+BATTERY', 0x22:'MBC7+ACCELEROMETER+EEPROM', 0x101:'MBC1M', 0x103:'MBC1M+SRAM+BATTERY', 0x0B:'MMM01', 0x0D:'MMM01+SRAM+BATTERY', 0xFC:'MAC-GBD+SRAM+BATTERY', 0x105:'G-MMC1+SRAM+BATTERY', 0x104:'M161', 0xFF:'HuC-1+IR+SRAM+BATTERY', 0xFE:'HuC-3+RTC+SRAM+BATTERY', 0xFD:'TAMA5+RTC+EEPROM', 0x201:'Unlicensed 256M Mapper', 0x202:'Unlicensed Wisdom Tree Mapper', 0x203:'Unlicensed Xploder GB Mapper', 0x204:'Unlicensed Sachen Mapper', 0x205:'Unlicensed Datel Orbit V2 Mapper', 0x206:'Unlicensed MBCX Mapper' }
DMG_Mapper_Types = { "None":[ 0x00, 0x08, 0x09 ], "MBC1":[ 0x01, 0x02, 0x03 ], "MBC2":[ 0x05, 0x06 ], "MBC3":[ 0x0F, 0x10, 0x11, 0x12, 0x13 ], "MBC30":[ 0x110 ], "MBC5":[ 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E ], "MBC6":[ 0x20 ], "MBC7":[ 0x22 ], "MBC1M":[ 0x101, 0x103 ], "MMM01":[ 0x0B, 0x0D ], "MAC-GBD":[ 0xFC ], "G-MMC1":[ 0x105 ], "M161":[ 0x104 ], "HuC-1":[ 0xFF ], "HuC-3":[ 0xFE ], "TAMA5":[ 0xFD ], "Unlicensed 256M Multi Cart Mapper":[ 0x201 ], "Unlicensed Wisdom Tree Mapper":[ 0x202 ], "Unlicensed Xploder GB Mapper":[ 0x203 ], "Unlicensed Sachen Mapper":[ 0x204 ], "Unlicensed Datel Orbit V2 Mapper":[ 0x205 ], "Unlicensed MBCX Mapper":[ 0x206 ] }
DMG_Header_ROM_Sizes = [ "32 KiB", "64 KiB", "128 KiB", "256 KiB", "512 KiB", "1 MiB", "2 MiB", "4 MiB", "8 MiB", "16 MiB", "32 MiB", "64 MiB", "128 MiB" ]
DMG_Header_ROM_Sizes_Map = [ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C ]
DMG_Header_ROM_Sizes_Flasher_Map = [ 0x8000, 0x10000, 0x20000, 0x40000, 0x80000, 0x100000, 0x200000, 0x400000, 0x800000, 0x1000000, 0x2000000, 0x4000000, 0x8000000 ]
DMG_Header_RAM_Sizes = [ "None", "4K SRAM (512 Bytes)", "16K SRAM (2 KiB)", "64K SRAM (8 KiB)", "256K SRAM (32 KiB)", "512K SRAM (64 KiB)", "1M SRAM (128 KiB)", "MBC6 SRAM+FLASH (1.03 MiB)", "MBC7 2K EEPROM (256 Bytes)", "MBC7 4K EEPROM (512 Bytes)", "TAMA5 EEPROM (32 Bytes)", "Unlicensed 4M SRAM (512 KiB)", "Unlicensed 1M EEPROM (128 KiB)" ]
DMG_Header_RAM_Sizes_Map = [ 0x00, 0x100, 0x01, 0x02, 0x03, 0x05, 0x04, 0x104, 0x101, 0x102, 0x103, 0x201, 0x203, 0x204 ]
DMG_Header_RAM_Sizes_Flasher_Map = [ 0, 0x200, 0x800, 0x2000, 0x8000, 0x10000, 0x20000, 0x108000, 0x100, 0x200, 0x20, 0x80000, 0x20000, 0x80000 ] # RAM size in bytes
@ -169,6 +169,10 @@ class Progress():
self.PROGRESS["time_start"] = args["time_start"]
else:
self.PROGRESS["time_start"] = now
if "abortable" in args:
self.PROGRESS["abortable"] = args["abortable"]
else:
self.PROGRESS["abortable"] = True
self.PROGRESS["time_last_emit"] = now
self.PROGRESS["time_last_update_speed"] = now
self.PROGRESS["time_left"] = 0
@ -218,7 +222,7 @@ class Progress():
self.PROGRESS["sector_pos"] = args["sector_pos"]
if "abortable" in args:
self.PROGRESS["abortable"] = args["abortable"]
if ((now - self.PROGRESS["time_last_emit"]) > 0.06) or "force_update" in args and args["force_update"] is True:
self.PROGRESS["time_elapsed"] = now - self.PROGRESS["time_start"]
time_delta = now - self.PROGRESS["time_last_update_speed"]
@ -254,6 +258,11 @@ class Progress():
self.UPDATER(self.PROGRESS)
self.PROGRESS["time_last_emit"] = now
elif args["action"] == "UPDATE_INFO":
self.PROGRESS["text"] = args["text"]
self.PROGRESS["action"] = args["action"]
self.UPDATER(self.PROGRESS)
elif args["action"] == "FINISHED":
self.PROGRESS["pos"] = self.PROGRESS["size"]
self.UPDATER(self.PROGRESS)
@ -535,6 +544,8 @@ def GetDumpReport(di, device):
di["rom_size"] = "{:s}".format(AGB_Header_ROM_Sizes[AGB_Header_ROM_Sizes_Map.index(di["rom_size"])])
else:
di["rom_size"] = "{:,} bytes".format(di["rom_size"])
else:
raise NotImplementedError
di["cart_type"] = list(device.SUPPORTED_CARTS[mode].keys())[di["cart_type"]]
if di["file_name"] is None:
@ -795,7 +806,10 @@ def GetDumpReport(di, device):
if "st" in db: s += "* Save Type: {:s}\n".format(AGB_Header_Save_Types[db["st"]])
#if "ss" in db: s += "* Save Size: {:s}\n".format(formatFileSize(size=db["ss"], asInt=True))
return s
if platform.system() == "Windows":
return s.replace("\n", "\r\n")
else:
return s
def GenerateFileName(mode, header, settings=None):
fe_ni = True
@ -921,3 +935,37 @@ def dprint(*args, **kwargs):
if DEBUG:
msg = "{:s}{:s}".format(ANSI.CLEAR_LINE, msg)
print(msg)
def write_debug_log(device=False):
dprint("{:s} version: {:s} ({:d})".format(APPNAME, VERSION_PEP440, VERSION_TIMESTAMP))
dprint("Platform: {:s}".format(platform.platform()))
if device is not False:
if device is not None:
dprint("Connected device: {:s}".format(device))
else:
dprint("No device connected")
dprint("Now writing debug log file")
try:
fn = CONFIG_PATH + "/debug.log"
with open(fn, "wb") as f:
if platform.system() == "Windows":
f.write("\r\n".join(DEBUG_LOG).encode("UTF-8-SIG"))
else:
f.write("\n".join(DEBUG_LOG).encode("UTF-8-SIG"))
print("debug.log written")
return True
except:
return False
def exception_hook(exc_type, exc_value, exc_traceback):
if issubclass(exc_type, KeyboardInterrupt):
sys.__excepthook__(exc_type, exc_value, exc_traceback)
return
s = "⚠️ EXCEPTION OCCURED ⚠️\n"
lines = traceback.format_exception(exc_type, exc_value, exc_traceback)
for line in lines:
s += f"{line:s}"
print(s)
dprint(s)
write_debug_log()

View File

@ -13,6 +13,7 @@
"buffer_size":64,
"sector_size":0x20000,
"reset_every":0x400000,
"chip_erase_timeout":2000,
"command_set":"AMD",
"commands":{
"reset":[

View File

@ -3,7 +3,8 @@
"names":[
"insideGadgets 32 MiB (S29GL512N) + RTC",
"AGB-E20-30 with S29GL256N10TFI01",
"GBFlash 1M FLASH RTC (AGB-R1M-02V3)"
"GBFlash 1M FLASH RTC (AGB-R1M-02V3)",
"GBFlash 1M FLASH RTC (AGB-R1M-02V4)"
],
"flash_ids":[
[ 0x01, 0x00, 0x7E, 0x22 ],

View File

@ -0,0 +1,53 @@
{
"type":"DMG",
"names":[
"insideGadgets MegaDuck 32K"
],
"flash_ids":[
[ 0xBF, 0xB6 ]
],
"voltage":5,
"start_addr":0,
"first_bank":1,
"write_pin":"WR",
"chip_erase_timeout":30,
"command_set":"AMD",
"commands":{
"reset":[
[ 0, 0xF0 ]
],
"read_identifier":[
[ 0x5555, 0xAA ],
[ 0x2AAA, 0x55 ],
[ 0x5555, 0x90 ]
],
"chip_erase":[
[ 0x5555, 0xAA ],
[ 0x2AAA, 0x55 ],
[ 0x5555, 0x80 ],
[ 0x5555, 0xAA ],
[ 0x2AAA, 0x55 ],
[ 0x5555, 0x10 ]
],
"chip_erase_wait_for":[
[ null, null, null ],
[ null, null, null ],
[ null, null, null ],
[ null, null, null ],
[ null, null, null ],
[ 0, 0xFF, 0xFF ]
],
"single_write":[
[ 0x5555, 0xAA ],
[ 0x2AAA, 0x55 ],
[ 0x5555, 0xA0 ],
[ "PA", "PD" ]
],
"single_write_wait_for":[
[ null, null, null ],
[ null, null, null ],
[ null, null, null ],
[ null, null, null ]
]
}
}

View File

@ -0,0 +1,88 @@
{
"type":"DMG",
"names":[
"GBFlash MBCX (32 MiB)"
],
"flash_ids":[
[ 0x01, 0x01, 0x7E, 0x7E ]
],
"voltage":3.3,
"flash_size":0x2000000,
"start_addr":0x4000,
"first_bank":0,
"write_pin":"WR",
"sector_size_from_cfi":true,
"chip_erase_timeout":120,
"mbc":0x206,
"command_set":"AMD",
"commands":{
"reset":[
[ 0x0, 0xF0 ]
],
"read_identifier":[
[ 0xAAA, 0xAA ],
[ 0x555, 0x55 ],
[ 0xAAA, 0x90 ]
],
"chip_erase":[
[ 0xAAA, 0xAA ],
[ 0x555, 0x55 ],
[ 0xAAA, 0x80 ],
[ 0xAAA, 0xAA ],
[ 0x555, 0x55 ],
[ 0xAAA, 0x10 ]
],
"chip_erase_wait_for":[
[ null, null, null ],
[ null, null, null ],
[ null, null, null ],
[ null, null, null ],
[ null, null, null ],
[ 0, 0xFF, 0xFF ]
],
"sector_erase":[
[ 0xAAA, 0xAA ],
[ 0x555, 0x55 ],
[ 0xAAA, 0x80 ],
[ 0xAAA, 0xAA ],
[ 0x555, 0x55 ],
[ "SA", 0x30 ]
],
"sector_erase_wait_for":[
[ null, null, null ],
[ null, null, null ],
[ null, null, null ],
[ null, null, null ],
[ null, null, null ],
[ "SA", 0xFF, 0xFF ]
],
"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,88 @@
{
"type":"DMG",
"names":[
"GBFlash MBCX (8 MiB)"
],
"flash_ids":[
[ 0x01, 0x01, 0x7E, 0x7E ]
],
"voltage":3.3,
"flash_size":0x800000,
"start_addr":0x4000,
"first_bank":0,
"write_pin":"WR",
"sector_size_from_cfi":true,
"chip_erase_timeout":60,
"mbc":0x206,
"command_set":"AMD",
"commands":{
"reset":[
[ 0x0, 0xF0 ]
],
"read_identifier":[
[ 0xAAA, 0xAA ],
[ 0x555, 0x55 ],
[ 0xAAA, 0x90 ]
],
"chip_erase":[
[ 0xAAA, 0xAA ],
[ 0x555, 0x55 ],
[ 0xAAA, 0x80 ],
[ 0xAAA, 0xAA ],
[ 0x555, 0x55 ],
[ 0xAAA, 0x10 ]
],
"chip_erase_wait_for":[
[ null, null, null ],
[ null, null, null ],
[ null, null, null ],
[ null, null, null ],
[ null, null, null ],
[ 0, 0xFF, 0xFF ]
],
"sector_erase":[
[ 0xAAA, 0xAA ],
[ 0x555, 0x55 ],
[ 0xAAA, 0x80 ],
[ 0xAAA, 0xAA ],
[ 0x555, 0x55 ],
[ "SA", 0x30 ]
],
"sector_erase_wait_for":[
[ null, null, null ],
[ null, null, null ],
[ null, null, null ],
[ null, null, null ],
[ null, null, null ],
[ "SA", 0xFF, 0xFF ]
],
"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

@ -1,10 +1,12 @@
{
"type":"DMG",
"names":[
"DVP DRV with MX29LV320CB"
"DVP DRV with MX29LV320CB",
"DVP DRV with MX29LV320CT"
],
"flash_ids":[
[ 0xC2, 0xC2, 0xA8, 0xA8 ]
[ 0xC2, 0xC2, 0xA8, 0xA8 ],
[ 0xC2, 0xC2, 0xA7, 0xA7 ]
],
"voltage":3.3,
"flash_size":0x400000,

View File

@ -2,11 +2,13 @@
"type":"DMG",
"names":[
"SD008-6810-512S with MSP55LV512",
"SD008_512ND_4M with epoxy flash chip"
"SD008_512ND_4M with epoxy flash chip",
"Nameless PCB with epoxy flash chip"
],
"flash_ids":[
[ 0x02, 0x7D, 0x00, 0x08 ],
[ 0x02, 0x7D, 0x00, 0x98 ]
[ 0x02, 0x7D, 0x00, 0x98 ],
[ 0x04, 0x7D, 0x00, 0x98 ]
],
"voltage":3.3,
"voltage_variants":true,

View File

@ -230,11 +230,6 @@ try:
self.DEVICE = device
else:
self.APP.QT_APP.processEvents()
text = "This Firmware Updater is for GBFlash devices only. Please only proceed if your device is a GBFlash."
msgbox = QtWidgets.QMessageBox(parent=self, icon=QtWidgets.QMessageBox.Warning, windowTitle="FlashGBX", text=text, standardButtons=QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel)
msgbox.setDefaultButton(QtWidgets.QMessageBox.Ok)
answer = msgbox.exec()
if answer == QtWidgets.QMessageBox.Cancel: return
self.FWUPD = FirmwareUpdater(app_path, None)
self.layout = QtWidgets.QGridLayout()

View File

@ -110,11 +110,6 @@ try:
self.DEVICE = device
else:
self.APP.QT_APP.processEvents()
text = "This Firmware Updater is for insideGadgets GBxCart RW v1.4 devices only. Please only proceed if your device matches this hardware revision.\n\nOlder GBxCart RW revisions can be updated only after connecting to them first."
msgbox = QtWidgets.QMessageBox(parent=self, icon=QtWidgets.QMessageBox.Warning, windowTitle="FlashGBX", text=text, standardButtons=QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel)
msgbox.setDefaultButton(QtWidgets.QMessageBox.Ok)
answer = msgbox.exec()
if answer == QtWidgets.QMessageBox.Cancel: return
self.FWUPD = FirmwareUpdater(app_path, None)
self.layout = QtWidgets.QGridLayout()
@ -217,8 +212,10 @@ try:
self.lblDeviceFWVerResult.setText(self.FW_VER)
if self.PCB_VER == "v1.4":
self.optDevicePCBVer14.setChecked(True)
self.optDevicePCBVer14a.setEnabled(False)
elif self.PCB_VER == "v1.4a/b/c":
self.optDevicePCBVer14a.setChecked(True)
self.optDevicePCBVer14.setEnabled(False)
self.SetPCBVersion()
def SetPCBVersion(self):

View File

@ -30,7 +30,9 @@ class FirmwareUpdater():
path = os.path.dirname(path) + "/"
fncSetStatus(text="Connecting... This may take a moment.")
with open(file, "rb") as f: temp = f.read().decode("UTF-8", "ignore")
filename = os.path.split(file)[1]
filepath = os.path.split(file)[0]
with open(filepath + "/" + filename, "rb") as f: temp = f.read().decode("UTF-8", "ignore")
if not temp.startswith("UPDATE"):
with open(file, "wb") as f:
temp = bytearray(b"UPDATE")
@ -46,10 +48,16 @@ class FirmwareUpdater():
return 2
try:
with open(file, "rb") as f: temp = f.read().decode("UTF-8", "ignore")
except FileNotFoundError:
fncSetStatus(text="Couldnt access MODE.TXT. Remove cartridge and try again.")
return 2
with open(filepath + "/" + filename, "rb") as f: temp = f.read().decode("UTF-8", "ignore")
except FileNotFoundError as e:
try:
if filename == "MODE.TXT":
with open(filepath + "/" + "MODE!.TXT", "rb") as f: temp = f.read().decode("UTF-8", "ignore")
else:
raise FileNotFoundError from e
except FileNotFoundError:
fncSetStatus(text="Couldnt access MODE.TXT. Remove cartridge and try again.")
return 2
if not temp.startswith("UPDATE"):
fncSetStatus(text="Couldnt enter UPDATE mode, please try again.")
@ -272,7 +280,7 @@ try:
self.rowUpdate.addStretch()
self.rowUpdate2 = QtWidgets.QHBoxLayout()
self.lblUpdateDisclaimer = QtWidgets.QLabel("Please note that FlashGBX is not officially supported by BennVenn, so please use this firmware updater at your own risk.")
self.lblUpdateDisclaimer = QtWidgets.QLabel("Please note that FlashGBX is not officially supported by BennVenn.")
self.lblUpdateDisclaimer.setWordWrap(True)
self.lblUpdateDisclaimer.setAlignment(QtGui.Qt.AlignmentFlag.AlignCenter)
self.rowUpdate2.addWidget(self.lblUpdateDisclaimer)
@ -467,7 +475,7 @@ try:
msgbox = QtWidgets.QMessageBox(parent=self, icon=QtWidgets.QMessageBox.Critical, windowTitle="FlashGBX", text=text, standardButtons=QtWidgets.QMessageBox.Ok)
answer = msgbox.exec()
return False
answer = QtWidgets.QMessageBox.information(self, "FlashGBX", "If your Joey Jr device is currently running the Drag'n'Drop firmware, please continue and choose its <b>MODE.TXT</b> file.", QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel, QtWidgets.QMessageBox.Ok)
answer = QtWidgets.QMessageBox.information(self, "FlashGBX", "If your Joey Jr device is currently running the Drag'n'Drop firmware, please continue and choose its <b>MODE.TXT</b> (or <b>MODE!.TXT</b>) file.", QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel, QtWidgets.QMessageBox.Ok)
if answer == QtWidgets.QMessageBox.Cancel:
self.SetStatus("No device found.", enableUI=True)
return False

View File

@ -9,7 +9,7 @@ class GbxDevice(LK_Device):
DEVICE_NAME = "GBFlash"
DEVICE_MIN_FW = 1
DEVICE_MAX_FW = 12
DEVICE_LATEST_FW_TS = { 5:1722774120, 10:1722774120, 11:1722774120, 12:1722774120, 13:1722774120 }
DEVICE_LATEST_FW_TS = { 5:1730731680, 10:1730731680, 11:1730731680, 12:1730731680, 13:1730731680 }
PCB_VERSIONS = { 5:'', 12:'v1.2', 13:'v1.3' }
def __init__(self):
@ -111,8 +111,10 @@ class GbxDevice(LK_Device):
self.FW["cart_power_ctrl"] = True if self._read(1) == 1 else False
# Reset to bootloader support
self.FW["bootloader_reset"] = True if self._read(1) == 1 else False
temp = self._read(1)
self.FW["bootloader_reset"] = True if temp & 1 == 1 else False
self.FW["unregistered"] = True if temp >> 7 == 1 else False
return True
except Exception as e:
@ -192,7 +194,7 @@ class GbxDevice(LK_Device):
return True
def FirmwareUpdateAvailable(self):
if self.FW["pcb_ver"] == 5: # unofficial firmware
if self.FW["pcb_ver"] == 5 or self.FW["fw_ts"] < 1730592000: # unofficial firmware
self.FW_UPDATE_REQ = True
return True
if self.FW["fw_ts"] < self.DEVICE_LATEST_FW_TS[self.FW["pcb_ver"]]:
@ -239,6 +241,13 @@ class GbxDevice(LK_Device):
def GetFullName(self):
if self.FW["pcb_ver"] < 13 and self.CanPowerCycleCart():
return "{:s} {:s} + PLUGIN 01".format(self.GetName(), self.GetPCBVersion())
s = "{:s} {:s} + PLUGIN 01".format(self.GetName(), self.GetPCBVersion())
else:
return "{:s} {:s}".format(self.GetName(), self.GetPCBVersion())
s = "{:s} {:s}".format(self.GetName(), self.GetPCBVersion())
if self.IsUnregistered():
s += " (unregistered)"
return s
def GetRegisterInformation(self):
text = f"Your GBFlash device reported a registration error, which means it may be an illegitimate clone.\n\nThe devices integrated piracy detection may limit the device in performance and functionality until proper registration. The {Util.APPNAME:s} software has no control over this.\n\nPlease visit <a href=\"https://gbflash.geeksimon.com/\">https://gbflash.geeksimon.com/</a> for more information.".replace("\n", "<br>")
return text

View File

@ -15,7 +15,7 @@ class GbxDevice(LK_Device):
MAX_BUFFER_READ = 0x1000
MAX_BUFFER_WRITE = 0x400
def Initialize(self, flashcarts, port=None, max_baud=2000000):
def Initialize(self, flashcarts=None, port=None, max_baud=2000000):
if self.IsConnected(): self.DEVICE.close()
if platform.system() == "Darwin": max_baud = 1000000
@ -81,7 +81,8 @@ class GbxDevice(LK_Device):
self.DEVICE.timeout = self.DEVICE_TIMEOUT
# Load Flash Cartridge Handlers
self.UpdateFlashCarts(flashcarts)
if flashcarts is not None:
self.UpdateFlashCarts(flashcarts)
# Stop after first found device
break
@ -177,6 +178,7 @@ class GbxDevice(LK_Device):
elif baudrate == 1000000:
self._write(self.DEVICE_CMD["OFW_USART_1_0M_SPEED"])
self.BAUDRATE = baudrate
self.DEVICE.close()
def CheckActive(self):
if time.time() < self.LAST_CHECK_ACTIVE + 1: return True

View File

@ -11,6 +11,27 @@
<p>Qt and the Qt logo are trademarks of The Qt Company Ltd.</p>
<p>Qt is The Qt Company Ltd product developed as an open source project. See <a href="https://qt.io/">qt.io</a> for more information.</p>
# Python
PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
--------------------------------------------
1. This LICENSE AGREEMENT is between the Python Software Foundation ("PSF"), and the Individual or Organization ("Licensee") accessing and otherwise using Python software in source or binary form and its associated documentation.
2. Subject to the terms and conditions of this License Agreement, PSF hereby grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, analyze, test, perform and/or display publicly, prepare derivative works, distribute, and otherwise use Python alone or in any derivative version, provided, however, that PSF's License Agreement and PSF's notice of copyright, i.e., "Copyright © 2001-2021 Python Software Foundation; All Rights Reserved" are retained in Python alone or in any derivative version prepared by Licensee.
3. In the event Licensee prepares a derivative work that is based on or incorporates Python or any part thereof, and wants to make the derivative work available to others as provided herein, then Licensee hereby agrees to include in any such work a brief summary of the changes made to Python.
4. PSF is making Python available to Licensee on an "AS IS" basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT INFRINGE ANY THIRD PARTY RIGHTS.
5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
6. This License Agreement will automatically terminate upon a material breach of its terms and conditions.
7. Nothing in this License Agreement shall be deemed to create any relationship of agency, partnership, or joint venture between PSF and Licensee. This License Agreement does not grant permission to use PSF trademarks or trade name in a trademark sense to endorse or promote products or services of Licensee, or any third party.
8. By copying, installing or otherwise using Python, Licensee agrees to be bound by the terms and conditions of this License Agreement.
# Pillow
The Python Imaging Library (PIL) is
@ -108,7 +129,6 @@ IN THE SOFTWARE.
# requests
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/

Binary file not shown.

Binary file not shown.

View File

@ -1,6 +1,6 @@
# FlashGBX (by Lesserkuma)
for [Windows](https://github.com/lesserkuma/FlashGBX/releases), [Linux](https://github.com/lesserkuma/FlashGBX#run-using-python-linux-macos-windows), [macOS](https://github.com/lesserkuma/FlashGBX#run-using-python-linux-macos-windows)
for Windows, Linux, macOS (→ [Download](https://github.com/lesserkuma/FlashGBX/releases))
<img src="https://raw.githubusercontent.com/lesserkuma/FlashGBX/master/.github/01.png" alt="FlashGBX on Windows 11" width="500"><br><img src="https://raw.githubusercontent.com/lesserkuma/FlashGBX/master/.github/02.png" alt="GB Camera Album Viewer" width="500">
@ -15,37 +15,44 @@ for [Windows](https://github.com/lesserkuma/FlashGBX/releases), [Linux](https://
- A flash chip query (including Common Flash Interface information) can be performed for flash cartridges
- Decode and extract Game Boy Camera photos from save data
- Generate ROM dump reports for game preservation purposes
- Delta flashing support, useful for development by writing only differences between two ROMs (if named `rom.gba` and `rom.delta.gba`)
### Compatible cartridge reader/writer hardware
- [GBxCart RW](https://www.gbxcart.com/) (tested with v1.3, v1.4, v1.4a and v1.4c)
- [GBxCart RW](https://www.gbxcart.com/) (tested with v1.4, v1.4a and v1.4c)
- [GBFlash](https://github.com/simonkwng/GBFlash) (tested with v1.2 and v1.3)
- [Joey Jr](https://bennvenn.myshopify.com/collections/game-cart-to-pc-interface/products/usb-gb-c-cart-dumper-the-joey-jr) (tested with V2++)
## Installing and running
### Windows Packages
### Pre-compiled binaries and packages
Available in the GitHub [Releases](https://github.com/lesserkuma/FlashGBX/releases) section:
Available in the GitHub [Releases](https://github.com/lesserkuma/FlashGBX/releases) section are pre-compiled downloads available for:
* Windows Setup: An installer that will add the application to the start menu and optionally create a desktop icon
* Windows Portable: Have everything in one place including the config files
* **Windows (64-bit)**
* Setup: An installer that will add the application to the start menu and optionally create a desktop icon
* Portable: Have everything in one place including the config files
*(For users of Windows 7, legacy “Qt5” versions are provided as well.)*
These work for installing fresh and upgrading from an older version.
* **Linux**
* Ubuntu (.deb file): Install using `dpkg -i /path/to/FlashGBX_x.x_Ubuntu-all.deb`.<br>*(Based on a contribution by [JJ-Fox](https://github.com/JJ-Fox))*
* Other distributions: Pre-made packages are contributed by JJ-Fox [here](https://github.com/JJ-Fox/FlashGBX-Linux-builds/releases/latest).
### Run using Python (Linux, macOS, Windows)
* **macOS**
* x86-64/arm64 (.dmg file): Install by opening the .dmg file and copying over the “FlashGBX” application to the desktop.<br>If it doesnt run, it probably got quarantined during download. Run the following command in a Terminal window to unquarantine it: `xattr -d com.apple.quarantine /path/to/FlashGBX.app`.<br>*(Based on a contribution by [Cliffback](https://github.com/Cliffback))*
#### Installing
*(If you have a Joey Jr and use macOS, please run the [Joey Jr Firmware Updater](https://github.com/lesserkuma/JoeyJr_FWUpdater) before using FlashGBX.)*
### Run via Python
FlashGBX can also be run in a Python environment like so:
1. Download and install [Python](https://www.python.org/downloads/) (version 3.10.11 is recommended)
2. Open a Terminal or Command Prompt window
3. Install FlashGBX with this command:<br>`pip3 install "FlashGBX[qt6]"`
* If installation fails, try this command instead:<br>`pip3 install "FlashGBX[qt5]"`
* If installation still fails, you can install the minimal version (command line interface) with this command:<br>`pip3 install FlashGBX`
* Pre-made Linux packages and instructions for select distributions are available [here](https://github.com/JJ-Fox/FlashGBX-Linux-builds/releases/latest), contributed by JJ-Fox.
* Pre-made macOS packages and instructions are available [here](https://github.com/Cliffback/FlashGBX-macOS) (in the “Releases” section), contributed by Cliffback.
* Update to the latest version by replacing `install` with `install -U`.
#### Running
Use this command in a Terminal or Command Prompt window to launch the installed FlashGBX application:
@ -56,11 +63,6 @@ Use this command in a Terminal or Command Prompt window to launch the installed
*Note: On Linux systems, the `brltty` module may render serial communication devices non-accessible. See the troubleshooting section for details.*
#### Upgrading from an older version
1. Open a Terminal or Command Prompt window
2. Enter this command:<br>`pip3 install -U FlashGBX`
## Cartridge Compatibility
### Supported cartridge memory mappers
- Game Boy
@ -85,6 +87,7 @@ Use this command in a Terminal or Command Prompt window to launch the installed
- Unlicensed Xploder GB Mapper
- Unlicensed Sachen Mapper
- Unlicensed Datel Orbit V2 Mapper
- Unlicensed MBCX Mapper
- Game Boy Advance
- All cartridges without memory mapping
@ -127,6 +130,8 @@ Use this command in a Terminal or Command Prompt window to launch the installed
- GameShark Pro
- GB-CART32K-A with SST39SF020A
- GB Smart 32M
- GBFlash MBCX (8 MiB)
- GBFlash MBCX (32 MiB)
- GBFlash RTC with MX29LV320EB
- HDR Game Boy Camera Flashcart
- insideGadgets 32 KiB
@ -139,6 +144,7 @@ Use this command in a Terminal or Command Prompt window to launch the installed
- insideGadgets 4 MiB, 128 KiB SRAM/FRAM
- insideGadgets 4 MiB, 32 KiB FRAM, MBC3+RTC
- insideGadgets 4 MiB (2× 2 MiB), 32 KiB FRAM, MBC5
- insideGadgets MegaDuck 32K
- Mr Flash 64M
- Sillyhatday MBC5-DUAL-FLASH-4/8MB
- Squareboi 4 MiB (2× 2 MiB)
@ -163,6 +169,7 @@ Use this command in a Terminal or Command Prompt window to launch the installed
- FunnyPlaying MidnightTrace 32 MiB Flash Cart
- GBA Movie Player v2 CF (with SST39VF400A)¹
- GBFlash 1M FLASH RTC (AGB-R1M-02V3)
- GBFlash 1M FLASH RTC (AGB-R1M-02V4)
- insideGadgets 16 MiB, 64K EEPROM with Solar Sensor and RTC options
- insideGadgets 32 MiB, 1M FLASH with RTC option
- insideGadgets 32 MiB, 512K FLASH
@ -185,6 +192,7 @@ Use this command in a Terminal or Command Prompt window to launch the installed
- DRV with AM29LV160DB and ALTERA CPLD
- DRV with AM29LV160DT and ALTERA CPLD
- DVP DRV with MX29LV320CB
- DVP DRV with MX29LV320CT
- ES29LV160_DRV with 29DL32TF-70
- GB-M968 with 29LV160DB
- GB-M968 with M29W160EB
@ -328,21 +336,23 @@ Many different reproduction cartridges share their flash chip command set, so ev
* On some Linux systems like Fedora, you may need to install the `python3-pillow-qt` package manually in order for the GUI mode to work.
* On some Linux systems you may see the message “No devices found.” with the GBxCart RW hardware device, even though youre using a USB cable capable of data transfers. This may be caused by a module called `brltty` (a driver for refreshable braille displays) that is erroneously interfering and taking over control of any connected USB device that uses the CH340/341 chipset. The solution would be to uninstall or blacklist the `brltty` driver and then reboot the system.
* On some Linux systems you may see the message “No devices found.” with the GBxCart RW or GBFlash hardware device, even though youre using a USB cable capable of data transfers. This may be caused by a module called `brltty` (a driver for refreshable braille displays) that is erroneously interfering and taking over control of any connected USB device that uses the CH340/341 chipset. The solution would be to uninstall or blacklist the `brltty` driver and then reboot the system. This is not an issue with Joey Jr devices.
* If youre using macOS version 10.13 or older, there may be no driver for serial communication devices installed on your system. You can either upgrade your macOS version to 10.14+ or manually install a driver which is available [here](https://github.com/adrianmihalko/ch340g-ch34g-ch34x-mac-os-x-driver).
* If youre using macOS and get a “Segmentation Fault: 11.” error, try uninstalling Python, reinstalling Python version 3.10.11 and then try again.
* If youre using macOS and get a “Segmentation Fault: 11.” error, try the “Run using Python” method with Python version 3.10.11.
## Miscellaneous
* To use your own frame around extracted Game Boy Camera pictures, place a file called `pc_frame.png` (must be at least 160×144 pixels) into the configuration directory. (GUI mode only)
* To write only the differences between two ROMs, name the original one `<name>.gba` and the edited one `<name>.delta.gba`.
## Contributors
The author would like to thank the following very kind people for their help, contributions or documentation (in alphabetical order):
2358, 90sFlav, AcoVanConis, AdmirtheSableye, AlexiG, ALXCO-Hardware, AndehX, antPL, aronson, Ausar, bbsan, BennVenn, ccs21, chobby, ClassicOldSong, Cliffback, CodyWick13, Corborg, Cristóbal, crizzlycruz, Crystal, Därk, Davidish, DevDavisNunez, Diddy_Kong, djedditt, Dr-InSide, dyf2007, easthighNerd, EchelonPrime, edo999, Eldram, Ell, EmperorOfTigers, endrift, Erba Verde, ethanstrax, eveningmoose, Falknör, FerrantePescara, frarees, Frost Clock, gandalf1980, gboh, gekkio, Godan, Grender, HDR, Herax, Hiccup, hiks, howie0210, iamevn, Icesythe7, ide, inYourBackline, iyatemu, Jayro, Jenetrix, JFox, joyrider3774, jrharbort, JS7457, julgr, Kaede, kane159, KOOORAY, kscheel, kyokohunter, Leitplanke, litlemoran, LovelyA72, Lu, Luca DS, LucentW, manuelcm1, marv17, Merkin, metroid-maniac, Mr_V, olDirdey, orangeglo, paarongiroux, Paradoxical, Rairch, Raphaël BOICHOT, redalchemy, RetroGorek, RevZ, RibShark, s1cp, Satumox, Sgt.DoudouMiel, SH, Shinichi999, Sillyhatday, simonK, Sithdown, skite2001, Smelly-Ghost, Sonikks, Squiddy, Stitch, Super Maker, t5b6_de, Tauwasser, TheNFCookie, Timville, twitnic, velipso, Veund, voltagex, Voultar, Warez Waldo, wickawack, Winter1760, Wkr, x7l7j8cc, xactoes, xukkorz, yosoo, Zeii, Zelante, zipplet, Zoo, zvxr
2358, 90sFlav, AcoVanConis, AdmirtheSableye, AlexiG, ALXCO-Hardware, AndehX, antPL, aronson, Ausar, bbsan, BennVenn, ccs21, chobby, ClassicOldSong, Cliffback, CodyWick13, Corborg, Cristóbal, crizzlycruz, Crystal, Därk, Davidish, DevDavisNunez, Diddy_Kong, djedditt, Dr-InSide, dyf2007, easthighNerd, EchelonPrime, edo999, Eldram, Ell, EmperorOfTigers, endrift, Erba Verde, ethanstrax, eveningmoose, Falknör, FerrantePescara, frarees, Frost Clock, gandalf1980, gboh, gekkio, Godan, Grender, HDR, Herax, Hiccup, hiks, howie0210, iamevn, Icesythe7, ide, inYourBackline, iyatemu, Jayro, Jenetrix, JFox, joyrider3774, jrharbort, JS7457, julgr, Kaede, kane159, KOOORAY, kscheel, kyokohunter, Leitplanke, litlemoran, LovelyA72, Lu, Luca DS, LucentW, manuelcm1, marv17, Merkin, metroid-maniac, Mr_V, Mufsta, olDirdey, orangeglo, paarongiroux, Paradoxical, Rairch, Raphaël BOICHOT, redalchemy, RetroGorek, RevZ, RibShark, s1cp, Satumox, Sgt.DoudouMiel, SH, Shinichi999, Sillyhatday, simonK, Sithdown, skite2001, Smelly-Ghost, Sonikks, Squiddy, Stitch, Super Maker, t5b6_de, Tauwasser, TheNFCookie, Timville, twitnic, velipso, Veund, voltagex, Voultar, Warez Waldo, wickawack, Winter1760, Wkr, x7l7j8cc, xactoes, xukkorz, yosoo, Zeii, Zelante, zipplet, Zoo, zvxr
## Third Party Notices and Licenses

View File

@ -11,6 +11,27 @@
<p>Qt and the Qt logo are trademarks of The Qt Company Ltd.</p>
<p>Qt is The Qt Company Ltd product developed as an open source project. See <a href="https://qt.io/">qt.io</a> for more information.</p>
# Python
PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
--------------------------------------------
1. This LICENSE AGREEMENT is between the Python Software Foundation ("PSF"), and the Individual or Organization ("Licensee") accessing and otherwise using Python software in source or binary form and its associated documentation.
2. Subject to the terms and conditions of this License Agreement, PSF hereby grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, analyze, test, perform and/or display publicly, prepare derivative works, distribute, and otherwise use Python alone or in any derivative version, provided, however, that PSF's License Agreement and PSF's notice of copyright, i.e., "Copyright © 2001-2021 Python Software Foundation; All Rights Reserved" are retained in Python alone or in any derivative version prepared by Licensee.
3. In the event Licensee prepares a derivative work that is based on or incorporates Python or any part thereof, and wants to make the derivative work available to others as provided herein, then Licensee hereby agrees to include in any such work a brief summary of the changes made to Python.
4. PSF is making Python available to Licensee on an "AS IS" basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT INFRINGE ANY THIRD PARTY RIGHTS.
5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
6. This License Agreement will automatically terminate upon a material breach of its terms and conditions.
7. Nothing in this License Agreement shall be deemed to create any relationship of agency, partnership, or joint venture between PSF and Licensee. This License Agreement does not grant permission to use PSF trademarks or trade name in a trademark sense to endorse or promote products or services of Licensee, or any third party.
8. By copying, installing or otherwise using Python, Licensee agrees to be bound by the terms and conditions of this License Agreement.
# Pillow
The Python Imaging Library (PIL) is
@ -108,7 +129,6 @@ IN THE SOFTWARE.
# requests
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/

View File

@ -8,7 +8,7 @@ with open("README.md", "r", encoding="utf-8") as fh: long_description = fh.read(
setuptools.setup(
name="FlashGBX",
version="4.2",
version="4.3",
author="Lesserkuma",
description="Reads and writes Game Boy and Game Boy Advance cartridge data",
url="https://github.com/lesserkuma/FlashGBX",