class Application(Frame): def __init__(self, master=None): super().__init__(master) self.pack() # First row f1 = LabelFrame(self, text='NAND file with No$GBA footer', padx=10, pady=10) # NAND Button self.nand_mode = False nand_icon = PhotoImage(data=('R0lGODlhEAAQAIMAAAAAADMzM2ZmZpmZmczMzP///wAAAAAAAAA' 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAMAAAYALAAAAAAQAB' 'AAAARG0MhJaxU4Y2sECAEgikE1CAFRhGMwSMJwBsU6frIgnR/bv' 'hTPrWUSDnGw3JGU2xmHrsvyU5xGO8ql6+S0AifPW8kCKpcpEQA7')) self.nand_button = Button(f1, image=nand_icon, command=self.change_mode, state=DISABLED) self.nand_button.image = nand_icon self.nand_button.pack(side='left') self.nand_file = StringVar() Entry(f1, textvariable=self.nand_file, state='readonly', width=40).pack(side='left') Button(f1, text='...', command=self.choose_nand).pack(side='left') f1.pack(padx=10, pady=10, fill=X) # Second row f2 = Frame(self) # Check box self.twilight = IntVar() self.twilight.set(1) self.chk = Checkbutton(f2, text='Install latest TWiLight Menu++ on custom firmware', variable=self.twilight) self.chk.pack(padx=10, anchor=W) # NAND operation frame self.nand_frame = LabelFrame(f2, text='NAND operation', padx=10, pady=10) self.nand_operation = IntVar() self.nand_operation.set(0) Radiobutton(self.nand_frame, text='Remove No$GBA footer', variable=self.nand_operation, value=0, command=lambda: self.enable_entries(False)).pack(anchor=W) Radiobutton(self.nand_frame, text='Add No$GBA footer', variable=self.nand_operation, value=1, command=lambda: self.enable_entries(True)).pack(anchor=W) fl = Frame(self.nand_frame) self.cid_label = Label(fl, text='eMMC CID', state=DISABLED) self.cid_label.pack(anchor=W, padx=(24, 0)) self.cid = StringVar() self.cid_entry = Entry(fl, textvariable=self.cid, width=20, state=DISABLED) self.cid_entry.pack(anchor=W, padx=(24, 0)) fl.pack(side='left') fr = Frame(self.nand_frame) self.console_id_label = Label(fr, text='Console ID', state=DISABLED) self.console_id_label.pack(anchor=W) self.console_id = StringVar() self.console_id_entry = Entry(fr, textvariable=self.console_id, width=20, state=DISABLED) self.console_id_entry.pack(anchor=W) fr.pack(side='right') f2.pack(fill=X) # Third row f3 = Frame(self) self.start_button = Button(f3, text='Start', width=16, command=self.hiya, state=DISABLED) self.start_button.pack(side='left', padx=(0, 5)) Button(f3, text='Quit', command=root.destroy, width=16).pack(side='left', padx=(5, 0)) f3.pack(pady=(10, 20)) self.folders = [] self.files = [] ################################################################################################ def change_mode(self): if (self.nand_mode): self.nand_frame.pack_forget() self.chk.pack(padx=10, anchor=W) self.nand_mode = False else: if askokcancel('Warning', ('You are about to enter NAND mode. Do it only if you know ' 'what you are doing. Proceed?'), icon=WARNING): self.chk.pack_forget() self.nand_frame.pack(padx=10, pady=(0, 10), fill=X) self.nand_mode = True ################################################################################################ def enable_entries(self, status): self.cid_label['state'] = (NORMAL if status else DISABLED) self.cid_entry['state'] = (NORMAL if status else DISABLED) self.console_id_label['state'] = (NORMAL if status else DISABLED) self.console_id_entry['state'] = (NORMAL if status else DISABLED) ################################################################################################ def choose_nand(self): name = askopenfilename(filetypes=( ( 'nand.bin', '*.bin' ), ( 'DSi-1.mmc', '*.mmc' ) )) self.nand_file.set(name) self.nand_button['state'] = (NORMAL if name != '' else DISABLED) self.start_button['state'] = (NORMAL if name != '' else DISABLED) ################################################################################################ def hiya(self): if not self.nand_mode: showinfo('Info', 'Now you will be asked to choose the SD card path that will be used ' 'for installing the custom firmware (or an output folder).\n\nIn order to avoid ' 'boot errors please assure it is empty before continuing.') self.sd_path = askdirectory() # Exit if no path was selected if self.sd_path == '': return # If adding a No$GBA footer, check if CID and ConsoleID values are OK elif self.nand_operation.get() == 1: cid = self.cid.get() console_id = self.console_id.get() # Check lengths if len(cid) != 32: showerror('Error', 'Bad eMMC CID') return elif len(console_id) != 16: showerror('Error', 'Bad Console ID') return # Parse strings to hex try: cid = bytearray.fromhex(cid) except ValueError: showerror('Error', 'Bad eMMC CID') return try: console_id = bytearray(reversed(bytearray.fromhex(console_id))) except ValueError: showerror('Error', 'Bad Console ID') return dialog = Toplevel(self) # Open as dialog (parent disabled) dialog.grab_set() dialog.title('Status') # Disable maximizing dialog.resizable(0, 0) frame = Frame(dialog, bd=2, relief=SUNKEN) scrollbar = Scrollbar(frame) scrollbar.pack(side=RIGHT, fill=Y) self.log = ThreadSafeText(frame, bd=0, width=52, height=20, yscrollcommand=scrollbar.set) self.log.pack() scrollbar.config(command=self.log.yview) frame.pack() Button(dialog, text='Close', command=dialog.destroy, width=16).pack(pady=10) # Center in window dialog.update_idletasks() width = dialog.winfo_width() height = dialog.winfo_height() dialog.geometry('%dx%d+%d+%d' % (width, height, root.winfo_x() + (root.winfo_width() / 2) - (width / 2), root.winfo_y() + (root.winfo_height() / 2) - (height / 2))) # Check if we'll be adding a No$GBA footer if self.nand_mode and self.nand_operation.get() == 1: Thread(target=self.add_footer, args=(cid, console_id)).start() else: Thread(target=self.check_nand).start() ################################################################################################ def check_nand(self): self.log.write('Checking NAND file...') # Read the NAND file try: with open(self.nand_file.get(), 'rb') as f: # Go to the No$GBA footer offset f.seek(-64, 2) # Read the footer's header :-) bstr = f.read(0x10) if bstr == b'DSi eMMC CID/CPU': # Read the CID bstr = f.read(0x10) self.cid.set(bstr.hex().upper()) self.log.write('- eMMC CID: ' + self.cid.get()) # Read the console ID bstr = f.read(8) self.console_id.set(bytearray(reversed(bstr)).hex().upper()) self.log.write('- Console ID: ' + self.console_id.get()) # Check we are removing the No$GBA footer if self.nand_mode: Thread(target=self.remove_footer).start() else: Thread(target=self.get_latest_hiyacfw).start() else: self.log.write('ERROR: No$GBA footer not found') except IOError as e: print(e) self.log.write('ERROR: Could not open the file ' + path.basename(self.nand_file.get())) ################################################################################################ def get_latest_hiyacfw(self): # Try to use already downloaded HiyaCFW archive filename = 'HiyaCFW.7z' try: if path.isfile(filename): self.log.write('\nPreparing HiyaCFW...') else: self.log.write('\nDownloading latest HiyaCFW release...') with urlopen('https://github.com/RocketRobz/hiyaCFW/releases/latest/download/' + filename) as src, open(filename, 'wb') as dst: copyfileobj(src, dst) self.log.write('- Extracting HiyaCFW archive...') proc = Popen([ _7z, 'x', '-bso0', '-y', filename, 'for PC', 'for SDNAND SD card' ]) ret_val = proc.wait() if ret_val == 0: self.files.append(filename) self.folders.append('for PC') self.folders.append('for SDNAND SD card') # Got to decrypt NAND if bootloader.nds is present Thread(target=self.decrypt_nand if path.isfile('bootloader.nds') else self.extract_bios).start() else: self.log.write('ERROR: Extractor failed') except (URLError, IOError) as e: print(e) self.log.write('ERROR: Could not get HiyaCFW') except OSError as e: print(e) self.log.write('ERROR: Could not execute ' + exe) ################################################################################################ def extract_bios(self): self.log.write('\nExtracting ARM7/ARM9 BIOS from NAND...') try: proc = Popen([ twltool, 'boot2', '--in', self.nand_file.get() ]) ret_val = proc.wait() if ret_val == 0: # Hash arm7.bin sha1_hash = sha1() with open('arm7.bin', 'rb') as f: sha1_hash.update(f.read()) self.log.write('- arm7.bin SHA1:\n ' + sha1_hash.digest().hex().upper()) # Hash arm9.bin sha1_hash = sha1() with open('arm9.bin', 'rb') as f: sha1_hash.update(f.read()) self.log.write('- arm9.bin SHA1:\n ' + sha1_hash.digest().hex().upper()) self.files.append('arm7.bin') self.files.append('arm9.bin') Thread(target=self.patch_bios).start() else: self.log.write('ERROR: Extractor failed') Thread(target=self.clean, args=(True,)).start() except OSError as e: print(e) self.log.write('ERROR: Could not execute ' + exe) Thread(target=self.clean, args=(True,)).start() ################################################################################################ def patch_bios(self): self.log.write('\nPatching ARM7/ARM9 BIOS...') try: self.patcher(path.join('for PC', 'bootloader files', 'bootloader arm7 patch.ips'), 'arm7.bin') self.patcher(path.join('for PC', 'bootloader files', 'bootloader arm9 patch.ips'), 'arm9.bin') # Hash arm7.bin sha1_hash = sha1() with open('arm7.bin', 'rb') as f: sha1_hash.update(f.read()) self.log.write('- Patched arm7.bin SHA1:\n ' + sha1_hash.digest().hex().upper()) # Hash arm9.bin sha1_hash = sha1() with open('arm9.bin', 'rb') as f: sha1_hash.update(f.read()) self.log.write('- Patched arm9.bin SHA1:\n ' + sha1_hash.digest().hex().upper()) Thread(target=self.arm9_prepend).start() except IOError as e: print(e) self.log.write('ERROR: Could not patch BIOS') Thread(target=self.clean, args=(True,)).start() except Exception as e: print(e) self.log.write('ERROR: Invalid patch header') Thread(target=self.clean, args=(True,)).start() ################################################################################################ def arm9_prepend(self): self.log.write('\nPrepending data to ARM9 BIOS...') try: with open('arm9.bin', 'rb') as f: data = f.read() with open('arm9.bin', 'wb') as f: with open(path.join('for PC', 'bootloader files', 'bootloader arm9 append to start.bin'), 'rb') as pre: f.write(pre.read()) f.write(data) # Hash arm9.bin sha1_hash = sha1() with open('arm9.bin', 'rb') as f: sha1_hash.update(f.read()) self.log.write('- Prepended arm9.bin SHA1:\n ' + sha1_hash.digest().hex().upper()) Thread(target=self.make_bootloader).start() except IOError as e: print(e) self.log.write('ERROR: Could not prepend data to ARM9 BIOS') Thread(target=self.clean, args=(True,)).start() ################################################################################################ def make_bootloader(self): self.log.write('\nGenerating new bootloader...') exe = (path.join('for PC', 'bootloader files', 'ndstool.exe') if sysname == 'Windows' else path.join(sysname, 'ndsblc')) try: proc = Popen([ exe, '-c', 'bootloader.nds', '-9', 'arm9.bin', '-7', 'arm7.bin', '-t', path.join('for PC', 'bootloader files', 'banner.bin'), '-h', path.join('for PC', 'bootloader files', 'header.bin') ]) ret_val = proc.wait() if ret_val == 0: self.files.append('bootloader.nds') # Hash bootloader.nds sha1_hash = sha1() with open('bootloader.nds', 'rb') as f: sha1_hash.update(f.read()) self.log.write('- bootloader.nds SHA1:\n ' + sha1_hash.digest().hex().upper()) Thread(target=self.decrypt_nand).start() else: self.log.write('ERROR: Generator failed') Thread(target=self.clean, args=(True,)).start() except OSError as e: print(e) self.log.write('ERROR: Could not execute ' + exe) Thread(target=self.clean, args=(True,)).start() ################################################################################################ def decrypt_nand(self): self.log.write('\nDecrypting NAND...') try: proc = Popen([ twltool, 'nandcrypt', '--in', self.nand_file.get(), '--out', self.console_id.get() + '.img' ]) ret_val = proc.wait() print("\n") if ret_val == 0: self.files.append(self.console_id.get() + '.img') Thread(target=self.win_extract_nand if sysname == 'Windows' else self.extract_nand).start() else: self.log.write('ERROR: Decryptor failed') Thread(target=self.clean, args=(True,)).start() except OSError as e: print(e) self.log.write('ERROR: Could not execute ' + exe) Thread(target=self.clean, args=(True,)).start() ################################################################################################ def win_extract_nand(self): self.log.write('\nExtracting files from NAND...') try: proc = Popen([ _7z, 'x', '-bso0', '-y', self.console_id.get() + '.img', '0.fat' ]) ret_val = proc.wait() if ret_val == 0: self.files.append('0.fat') proc = Popen([ _7z, 'x', '-bso0', '-y', '-o' + self.sd_path, '0.fat' ]) ret_val = proc.wait() if ret_val == 0: Thread(target=self.get_launcher).start() else: self.log.write('ERROR: Extractor failed, please update 7-Zip') Thread(target=self.clean, args=(True,)).start() else: self.log.write('ERROR: Extractor failed') Thread(target=self.clean, args=(True,)).start() except OSError as e: print(e) self.log.write('ERROR: Could not execute ' + exe) Thread(target=self.clean, args=(True,)).start() ################################################################################################ def extract_nand(self): self.log.write('\nExtracting files from NAND...') try: # DSi first partition offset: 0010EE00h proc = Popen([ fatcat, '-O', '1109504', '-x', self.sd_path, self.console_id.get() + '.img' ]) ret_val = proc.wait() if ret_val == 0: Thread(target=self.get_launcher).start() else: self.log.write('ERROR: Extractor failed') Thread(target=self.clean, args=(True,)).start() except OSError as e: print(e) self.log.write('ERROR: Could not execute ' + exe) Thread(target=self.clean, args=(True,)).start() ################################################################################################ def get_launcher(self): app = self.detect_region() # Stop if no supported region was found if not app: Thread(target=self.clean, args=(True,)).start() return # Delete contents of the launcher folder as it will be replaced by the one from HiyaCFW launcher_folder = path.join(self.sd_path, 'title', '00030017', app, 'content') # Walk through all files in the launcher content folder for file in listdir(launcher_folder): file = path.join(launcher_folder, file) # Set current file as read/write in case we are in Windows and unlaunch was installed # in the NAND. For Linux and MacOS fatcat doesn't keep file attributes if sysname == 'Windows': chmod(file, 438) # Delete current file remove(file) # Try to use already downloaded launcher try: if path.isfile(self.launcher_region): self.log.write('\nPreparing ' + self.launcher_region + ' launcher...') else: self.log.write('\nDownloading ' + self.launcher_region + ' launcher...') with urlopen('https://raw.githubusercontent.com' '/mondul/HiyaCFW-Helper/master/launchers/' + self.launcher_region) as src, open(self.launcher_region, 'wb') as dst: copyfileobj(src, dst) self.log.write('- Decrypting launcher...') proc = Popen([ _7z, 'x', '-bso0', '-y', '-p' + app, self.launcher_region, '00000002.app' ]) ret_val = proc.wait() if ret_val == 0: self.files.append(self.launcher_region) self.files.append('00000002.app') # Hash 00000002.app sha1_hash = sha1() with open('00000002.app', 'rb') as f: sha1_hash.update(f.read()) self.log.write('- Patched launcher SHA1:\n ' + sha1_hash.digest().hex().upper()) Thread(target=self.install_hiyacfw, args=(launcher_folder,)).start() else: self.log.write('ERROR: Extractor failed') Thread(target=self.clean, args=(True,)).start() except IOError as e: print(e) self.log.write('ERROR: Could not download ' + self.launcher_region + ' launcher') Thread(target=self.clean, args=(True,)).start() except OSError as e: print(e) self.log.write('ERROR: Could not execute ' + exe) Thread(target=self.clean, args=(True,)).start() ################################################################################################ def install_hiyacfw(self, launcher_folder): self.log.write('\nCopying HiyaCFW files...') # Reset copied files cache _path_created.clear() copy_tree('for SDNAND SD card', self.sd_path, update=1) copyfile('bootloader.nds', path.join(self.sd_path, 'hiya', 'bootloader.nds')) copyfile('00000002.app', path.join(launcher_folder, '00000002.app')) Thread(target=self.get_latest_twilight if self.twilight.get() == 1 else self.clean).start() ################################################################################################ def get_latest_twilight(self): # Try to use already downloaded TWiLight Menu++ archive filename = 'TWiLightMenu.7z' try: if path.isfile(filename): self.log.write('\nPreparing TWiLight Menu++...') else: self.log.write('\nDownloading latest TWiLight Menu++ release...') with urlopen('https://github.com/DS-Homebrew/TWiLightMenu/releases/latest/download/' + filename) as src, open(filename, 'wb') as dst: copyfileobj(src, dst) self.log.write('- Extracting ' + filename[:-3] + ' archive...') proc = Popen([ _7z, 'x', '-bso0', '-y', filename, '_nds', 'DSi - CFW users', 'DSi&3DS - SD card users', 'roms' ]) ret_val = proc.wait() if ret_val == 0: self.files.append(filename) self.folders.append('DSi - CFW users') self.folders.append('_nds') self.folders.append('DSi&3DS - SD card users') self.folders.append('roms') Thread(target=self.install_twilight, args=(filename[:-3],)).start() else: self.log.write('ERROR: Extractor failed') Thread(target=self.clean, args=(True,)).start() except (URLError, IOError) as e: print(e) self.log.write('ERROR: Could not get TWiLight Menu++') Thread(target=self.clean, args=(True,)).start() except OSError as e: print(e) self.log.write('ERROR: Could not execute ' + exe) Thread(target=self.clean, args=(True,)).start() ################################################################################################ def install_twilight(self, name): self.log.write('\nCopying ' + name + ' files...') copy_tree(path.join('DSi - CFW users', 'SDNAND root'), self.sd_path, update=1) copy_tree('_nds', path.join(self.sd_path, '_nds')) copy_tree('DSi&3DS - SD card users', self.sd_path, update=1) copy_tree('roms', path.join(self.sd_path, 'roms')) Thread(target=self.clean).start() ################################################################################################ def clean(self, err=False): self.log.write('\nCleaning...') while len(self.folders) > 0: rmtree(self.folders.pop(), ignore_errors=True) while len(self.files) > 0: try: remove(self.files.pop()) except: pass if err: self.log.write('Done') return self.log.write('Done!\nEject your SD card and insert it into your DSi') ################################################################################################ def patcher(self, patchpath, filepath): patch_size = path.getsize(patchpath) patchfile = open(patchpath, 'rb') if patchfile.read(5) != b'PATCH': patchfile.close() raise Exception() target = open(filepath, 'r+b') # Read First Record r = patchfile.read(3) while patchfile.tell() not in [ patch_size, patch_size - 3 ]: # Unpack 3-byte pointers. offset = self.unpack_int(r) # Read size of data chunk r = patchfile.read(2) size = self.unpack_int(r) if size == 0: # RLE Record r = patchfile.read(2) rle_size = self.unpack_int(r) data = patchfile.read(1) * rle_size else: data = patchfile.read(size) # Write to file target.seek(offset) target.write(data) # Read Next Record r = patchfile.read(3) if patch_size - 3 == patchfile.tell(): trim_size = self.unpack_int(patchfile.read(3)) target.truncate(trim_size) # Cleanup target.close() patchfile.close() ################################################################################################ def unpack_int(self, bstr): # Read an n-byte big-endian integer from a byte string ( ret_val, ) = unpack_from('>I', b'\x00' * (4 - len(bstr)) + bstr) return ret_val ################################################################################################ def detect_region(self): REGION_CODES = { '484e4145': 'USA', '484e414a': 'JAP', '484e414b': 'KOR', '484e4150': 'EUR', '484e4155': 'AUS' } # Autodetect console region try: for app in listdir(path.join(self.sd_path, 'title', '00030017')): for file in listdir(path.join(self.sd_path, 'title', '00030017', app, 'content')): if file.endswith('.app'): try: self.log.write('- Detected ' + REGION_CODES[app] + ' console NAND dump') self.launcher_region = REGION_CODES[app] return app except KeyError: self.log.write('ERROR: Unsupported console region') return False self.log.write('ERROR: Could not detect console region') except OSError as e: self.log.write('ERROR: ' + e.strerror + ': ' + e.filename) return False ################################################################################################ def remove_footer(self): self.log.write('\nRemoving No$GBA footer...') file = self.console_id.get() + '-no-footer.bin' try: copyfile(self.nand_file.get(), file) # Back-up footer info with open(self.console_id.get() + '-info.txt', 'w') as f: f.write('eMMC CID: ' + self.cid.get() + '\r\n') f.write('Console ID: ' + self.console_id.get() + '\r\n') with open(file, 'r+b') as f: # Go to the No$GBA footer offset f.seek(-64, 2) # Remove footer f.truncate() self.log.write('\nDone!\nModified NAND stored as\n' + file + '\nStored footer info in ' + self.console_id.get() + '-info.txt') except IOError as e: print(e) self.log.write('ERROR: Could not open the file ' + path.basename(self.nand_file.get())) ################################################################################################ def add_footer(self, cid, console_id): self.log.write('Adding No$GBA footer...') file = self.console_id.get() + '-footer.bin' try: copyfile(self.nand_file.get(), file) with open(file, 'r+b') as f: # Go to the No$GBA footer offset f.seek(-64, 2) # Read the footer's header :-) bstr = f.read(0x10) # Check if it already has a footer if bstr == b'DSi eMMC CID/CPU': self.log.write('ERROR: File already has a No$GBA footer') f.close() remove(file) return # Go to the end of file f.seek(0, 2) # Write footer f.write(b'DSi eMMC CID/CPU') f.write(cid) f.write(console_id) f.write(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') self.log.write('\nDone!\nModified NAND stored as\n' + file) except IOError as e: print(e) self.log.write('ERROR: Could not open the file ' + path.basename(self.nand_file.get()))
class Application(Frame): def __init__(self, master=None): super().__init__(master) self.pack() # First row f1 = LabelFrame(self, text='NAND file with No$GBA footer', padx=10, pady=10) # NAND Button self.nand_mode = False nand_icon = PhotoImage(data=('R0lGODlhEAAQAIMAAAAAADMzM2ZmZpmZmczMzP///wAAAAAAAAA' 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAMAAAYALAAAAAAQAB' 'AAAARG0MhJaxU4Y2sECAEgikE1CAFRhGMwSMJwBsU6frIgnR/bv' 'hTPrWUSDnGw3JGU2xmHrsvyU5xGO8ql6+S0AifPW8kCKpcpEQA7')) self.nand_button = Button(f1, image=nand_icon, command=self.change_mode, state=DISABLED) self.nand_button.image = nand_icon self.nand_button.pack(side='left') self.nand_file = StringVar() Entry(f1, textvariable=self.nand_file, state='readonly', width=40).pack(side='left') Button(f1, text='...', command=self.choose_nand).pack(side='left') f1.pack(padx=10, pady=10, fill=X) # Second row f2 = Frame(self) # Check box self.twilight = IntVar() self.twilight.set(1) self.chk = Checkbutton(f2, text='Install latest TWiLight Menu++ on custom firmware', variable=self.twilight) self.chk.pack(padx=10, anchor=W) # NAND operation frame self.nand_frame = LabelFrame(f2, text='NAND operation', padx=10, pady=10) self.nand_operation = IntVar() self.nand_operation.set(0) Radiobutton(self.nand_frame, text='Uninstall unlaunch or install v1.4 stable', variable=self.nand_operation, value=0, command=lambda: self.enable_entries(False)).pack(anchor=W) Radiobutton(self.nand_frame, text='Remove No$GBA footer', variable=self.nand_operation, value=1, command=lambda: self.enable_entries(False)).pack(anchor=W) Radiobutton(self.nand_frame, text='Add No$GBA footer', variable=self.nand_operation, value=2, command=lambda: self.enable_entries(True)).pack(anchor=W) fl = Frame(self.nand_frame) self.cid_label = Label(fl, text='eMMC CID', state=DISABLED) self.cid_label.pack(anchor=W, padx=(24, 0)) self.cid = StringVar() self.cid_entry = Entry(fl, textvariable=self.cid, width=20, state=DISABLED) self.cid_entry.pack(anchor=W, padx=(24, 0)) fl.pack(side='left') fr = Frame(self.nand_frame) self.console_id_label = Label(fr, text='Console ID', state=DISABLED) self.console_id_label.pack(anchor=W) self.console_id = StringVar() self.console_id_entry = Entry(fr, textvariable=self.console_id, width=20, state=DISABLED) self.console_id_entry.pack(anchor=W) fr.pack(side='right') f2.pack(fill=X) # Third row f3 = Frame(self) self.start_button = Button(f3, text='Start', width=16, command=self.hiya, state=DISABLED) self.start_button.pack(side='left', padx=(0, 5)) Button(f3, text='Quit', command=root.destroy, width=16).pack(side='left', padx=(5, 0)) f3.pack(pady=(10, 20)) self.folders = [] self.files = [] ################################################################################################ def change_mode(self): if (self.nand_mode): self.nand_frame.pack_forget() self.chk.pack(padx=10, anchor=W) self.nand_mode = False else: if askokcancel('Warning', ('You are about to enter NAND mode. Do it only if you know ' 'what you are doing. Proceed?'), icon=WARNING): self.chk.pack_forget() self.nand_frame.pack(padx=10, pady=(0, 10), fill=X) self.nand_mode = True ################################################################################################ def enable_entries(self, status): self.cid_label['state'] = (NORMAL if status else DISABLED) self.cid_entry['state'] = (NORMAL if status else DISABLED) self.console_id_label['state'] = (NORMAL if status else DISABLED) self.console_id_entry['state'] = (NORMAL if status else DISABLED) ################################################################################################ def choose_nand(self): name = askopenfilename(filetypes=( ( 'nand.bin', '*.bin' ), ( 'DSi-1.mmc', '*.mmc' ) )) self.nand_file.set(name) self.nand_button['state'] = (NORMAL if name != '' else DISABLED) self.start_button['state'] = (NORMAL if name != '' else DISABLED) ################################################################################################ def hiya(self): if not self.nand_mode: showinfo('Info', 'Now you will be asked to choose the SD card path that will be used ' 'for installing the custom firmware (or an output folder).\n\nIn order to avoid ' 'boot errors please assure it is empty before continuing.') self.sd_path = askdirectory() # Exit if no path was selected if self.sd_path == '': return # If adding a No$GBA footer, check if CID and ConsoleID values are OK elif self.nand_operation.get() == 2: cid = self.cid.get() console_id = self.console_id.get() # Check lengths if len(cid) != 32: showerror('Error', 'Bad eMMC CID') return elif len(console_id) != 16: showerror('Error', 'Bad Console ID') return # Parse strings to hex try: cid = cid.decode('hex') except TypeError: showerror('Error', 'Bad eMMC CID') return try: console_id = bytearray(reversed(console_id.decode('hex'))) except TypeError: showerror('Error', 'Bad Console ID') return dialog = Toplevel(self) # Open as dialog (parent disabled) dialog.grab_set() dialog.title('Status') # Disable maximizing dialog.resizable(0, 0) frame = Frame(dialog, bd=2, relief=SUNKEN) scrollbar = Scrollbar(frame) scrollbar.pack(side=RIGHT, fill=Y) self.log = ThreadSafeText(frame, bd=0, width=52, height=20, yscrollcommand=scrollbar.set) self.log.pack() scrollbar.config(command=self.log.yview) frame.pack() Button(dialog, text='Close', command=dialog.destroy, width=16).pack(pady=10) # Center in window dialog.update_idletasks() width = dialog.winfo_width() height = dialog.winfo_height() dialog.geometry('%dx%d+%d+%d' % (width, height, root.winfo_x() + (root.winfo_width() / 2) - (width / 2), root.winfo_y() + (root.winfo_height() / 2) - (height / 2))) # Check if we'll be adding a No$GBA footer if self.nand_mode and self.nand_operation.get() == 2: Thread(target=self.add_footer, args=(cid, console_id)).start() else: Thread(target=self.check_nand).start() ################################################################################################ def check_nand(self): self.log.write('Checking NAND file...') # Read the NAND file try: with open(self.nand_file.get(), 'rb') as f: # Go to the No$GBA footer offset f.seek(-64, 2) # Read the footer's header :-) bstr = f.read(0x10) if bstr == b'DSi eMMC CID/CPU': # Read the CID bstr = f.read(0x10) self.cid.set(bstr.hex().upper()) self.log.write('- eMMC CID: ' + self.cid.get()) # Read the console ID bstr = f.read(8) self.console_id.set(bytearray(reversed(bstr)).hex().upper()) self.log.write('- Console ID: ' + self.console_id.get()) # Check we are making an unlaunch operation or removing the No$GBA footer if self.nand_mode: if self.nand_operation.get() == 0: Thread(target=self.decrypt_nand).start() else: Thread(target=self.remove_footer).start() pass else: Thread(target=self.get_latest_hiyacfw).start() else: self.log.write('ERROR: No$GBA footer not found') except IOError: self.log.write('ERROR: Could not open the file ' + path.basename(self.nand_file.get())) ################################################################################################ def get_latest_hiyacfw(self): filename = 'HiyaCFW.7z' try: self.log.write('\nDownloading latest HiyaCFW release...') conn = urlopen('https://api.github.com/repos/RocketRobz/hiyaCFW/releases/latest') latest = jsonify(conn) conn.close() urlretrieve(latest['assets'][0]['browser_download_url'], filename) self.log.write('- Extracting HiyaCFW archive...') exe = path.join(sysname, '7za') proc = Popen([ exe, 'x', '-bso0', '-y', filename, 'for PC', 'for SDNAND SD card' ]) ret_val = proc.wait() if ret_val == 0: self.files.append(filename) self.folders.append('for PC') self.folders.append('for SDNAND SD card') # Got to decrypt NAND if bootloader.nds is present Thread(target=self.decrypt_nand if path.isfile('bootloader.nds') else self.extract_bios).start() else: self.log.write('ERROR: Extractor failed') except (URLError, IOError) as e: self.log.write('ERROR: Could not get HiyaCFW') except OSError: self.log.write('ERROR: Could not execute ' + exe) ################################################################################################ def extract_bios(self): self.log.write('\nExtracting ARM7/ARM9 BIOS from NAND...') exe = path.join(sysname, 'twltool') try: proc = Popen([ exe, 'boot2', '--in', self.nand_file.get() ]) ret_val = proc.wait() if ret_val == 0: # Hash arm7.bin sha1_hash = sha1() with open('arm7.bin', 'rb') as f: sha1_hash.update(f.read()) self.log.write('- arm7.bin SHA1:\n ' + sha1_hash.digest().hex().upper()) # Hash arm9.bin sha1_hash = sha1() with open('arm9.bin', 'rb') as f: sha1_hash.update(f.read()) self.log.write('- arm9.bin SHA1:\n ' + sha1_hash.digest().hex().upper()) self.files.append('arm7.bin') self.files.append('arm9.bin') Thread(target=self.patch_bios).start() else: self.log.write('ERROR: Extractor failed') Thread(target=self.clean, args=(True,)).start() except OSError: self.log.write('ERROR: Could not execute ' + exe) Thread(target=self.clean, args=(True,)).start() ################################################################################################ def patch_bios(self): self.log.write('\nPatching ARM7/ARM9 BIOS...') try: self.patcher(path.join('for PC', 'bootloader files', 'bootloader arm7 patch.ips'), 'arm7.bin') self.patcher(path.join('for PC', 'bootloader files', 'bootloader arm9 patch.ips'), 'arm9.bin') # Hash arm7.bin sha1_hash = sha1() with open('arm7.bin', 'rb') as f: sha1_hash.update(f.read()) self.log.write('- Patched arm7.bin SHA1:\n ' + sha1_hash.digest().hex().upper()) # Hash arm9.bin sha1_hash = sha1() with open('arm9.bin', 'rb') as f: sha1_hash.update(f.read()) self.log.write('- Patched arm9.bin SHA1:\n ' + sha1_hash.digest().hex().upper()) Thread(target=self.arm9_prepend).start() except IOError: self.log.write('ERROR: Could not patch BIOS') Thread(target=self.clean, args=(True,)).start() except Exception: self.log.write('ERROR: Invalid patch header') Thread(target=self.clean, args=(True,)).start() ################################################################################################ def arm9_prepend(self): self.log.write('\nPrepending data to ARM9 BIOS...') try: with open('arm9.bin', 'rb') as f: data = f.read() with open('arm9.bin', 'wb') as f: with open(path.join('for PC', 'bootloader files', 'bootloader arm9 append to start.bin'), 'rb') as pre: f.write(pre.read()) f.write(data) # Hash arm9.bin sha1_hash = sha1() with open('arm9.bin', 'rb') as f: sha1_hash.update(f.read()) self.log.write('- Prepended arm9.bin SHA1:\n ' + sha1_hash.digest().hex().upper()) Thread(target=self.make_bootloader).start() except IOError: self.log.write('ERROR: Could not prepend data to ARM9 BIOS') Thread(target=self.clean, args=(True,)).start() ################################################################################################ def make_bootloader(self): self.log.write('\nGenerating new bootloader...') exe = (path.join('for PC', 'bootloader files', 'ndstool') if sysname == 'Windows' else path.join(sysname, 'ndsblc')) try: proc = Popen([ exe, '-c', 'bootloader.nds', '-9', 'arm9.bin', '-7', 'arm7.bin', '-t', path.join('for PC', 'bootloader files', 'banner.bin'), '-h', path.join('for PC', 'bootloader files', 'header.bin') ]) ret_val = proc.wait() if ret_val == 0: # Hash bootloader.nds sha1_hash = sha1() with open('bootloader.nds', 'rb') as f: sha1_hash.update(f.read()) self.log.write('- bootloader.nds SHA1:\n ' + sha1_hash.digest().hex().upper()) Thread(target=self.decrypt_nand).start() else: self.log.write('ERROR: Generator failed') Thread(target=self.clean, args=(True,)).start() except OSError: self.log.write('ERROR: Could not execute ' + exe) Thread(target=self.clean, args=(True,)).start() ################################################################################################ def decrypt_nand(self): self.log.write('\nDecrypting NAND...') exe = path.join(sysname, 'twltool') try: proc = Popen([ exe, 'nandcrypt', '--in', self.nand_file.get(), '--out', self.console_id.get() + '.img' ]) ret_val = proc.wait() if ret_val == 0: if not self.nand_mode: self.files.append(self.console_id.get() + '.img') Thread(target=self.mount_nand).start() else: self.log.write('ERROR: Decryptor failed') Thread(target=self.clean, args=(True,)).start() except OSError: self.log.write('ERROR: Could not execute ' + exe) Thread(target=self.clean, args=(True,)).start() ################################################################################################ def mount_nand(self): self.log.write('\nMounting decrypted NAND...') try: if sysname == 'Windows': exe = osfmount cmd = [ osfmount, '-a', '-t', 'file', '-f', self.console_id.get() + '.img', '-m', '#:', '-o', 'ro,rem' ] if self.nand_mode: cmd[-1] = 'rw,rem' proc = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE) outs, errs = proc.communicate() if proc.returncode == 0: self.mounted = search(r'[a-zA-Z]:\s', outs.decode('utf-8')).group(0).strip() self.log.write('- Mounted on drive ' + self.mounted) else: self.log.write('ERROR: Mounter failed') Thread(target=self.clean, args=(True,)).start() return elif sysname == 'Darwin': exe = 'hdiutil' cmd = [ exe, 'attach', '-imagekey', 'diskimage-class=CRawDiskImage', '-nomount', self.console_id.get() + '.img' ] if not self.nand_mode: cmd.insert(2, '-readonly') proc = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE) outs, errs = proc.communicate() if proc.returncode == 0: self.raw_disk = search(r'^\/dev\/disk\d+', outs.decode('utf-8')).group(0) self.log.write('- Mounted raw disk on ' + self.raw_disk) cmd = [ exe, 'mount', self.raw_disk + 's1' ] if not self.nand_mode: cmd.insert(2, '-readonly') proc = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE) outs, errs = proc.communicate() if proc.returncode == 0: self.mounted = search(r'\/Volumes\/.+', outs.decode('utf-8')).group(0) self.log.write('- Mounted volume on ' + self.mounted) else: self.log.write('ERROR: Mounter failed') Thread(target=self.clean, args=(True,)).start() return else: self.log.write('ERROR: Mounter failed') Thread(target=self.clean, args=(True,)).start() return else: # Linux exe = 'losetup' cmd = [ exe, '-P', '-f', '--show', self.console_id.get() + '.img' ] if not self.nand_mode: cmd.insert(2, '-r') proc = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE) outs, errs = proc.communicate() if proc.returncode == 0: self.loop_dev = search(r'\/dev\/loop\d+', outs.decode('utf-8')).group(0) self.log.write('- Mounted loop device on ' + self.loop_dev) exe = 'mount' self.mounted = '/mnt' cmd = [ exe, '-t', 'vfat', self.loop_dev + 'p1', self.mounted ] if not self.nand_mode: cmd.insert(1, '-r') proc = Popen(cmd) ret_val = proc.wait() if ret_val == 0: self.log.write('- Mounted partition on ' + self.mounted) else: self.log.write('ERROR: Mounter failed') Thread(target=self.clean, args=(True,)).start() return else: self.log.write('ERROR: Mounter failed') Thread(target=self.clean, args=(True,)).start() return # Check we are making an unlaunch operation Thread(target=self.unlaunch_proc if self.nand_mode else self.extract_nand).start() except OSError: self.log.write('ERROR: Could not execute ' + exe) Thread(target=self.clean, args=(True,)).start() ################################################################################################ def extract_nand(self): self.log.write('\nExtracting files from NAND...') err = False # Reset copied files cache _path_created.clear() try: copy_tree(self.mounted, self.sd_path, preserve_mode=0, update=1) except: self.log.write('ERROR: Extractor failed') err = True Thread(target=self.unmount_nand, args=(err,)).start() ################################################################################################ def unmount_nand(self, err=False): self.log.write('\nUnmounting NAND...') try: if sysname == 'Windows': exe = osfmount proc = Popen([ osfmount, '-D', '-m', self.mounted ]) elif sysname == 'Darwin': exe = 'hdiutil' proc = Popen([ exe, 'detach', self.raw_disk ]) else: # Linux exe = 'umount' proc = Popen([ exe, self.mounted ]) ret_val = proc.wait() if ret_val == 0: exe = 'losetup' proc = Popen([ exe, '-d', self.loop_dev ]) else: self.log.write('ERROR: Unmounter failed') Thread(target=self.clean, args=(True,)).start() return ret_val = proc.wait() if ret_val == 0: if err: Thread(target=self.clean, args=(True,)).start() else: Thread(target=self.encrypt_nand if self.nand_mode else self.get_launcher).start() else: self.log.write('ERROR: Unmounter failed') Thread(target=self.clean, args=(True,)).start() except OSError: self.log.write('ERROR: Could not execute ' + exe) Thread(target=self.clean, args=(True,)).start() ################################################################################################ def get_launcher(self): app = self.detect_region() # Stop if no supported region was found if not app: self.files.append('bootloader.nds') Thread(target=self.clean, args=(True,)).start() return # Check if unlaunch was installed on the NAND dump tmd = path.join(self.sd_path, 'title', '00030017', app, 'content', 'title.tmd') if path.getsize(tmd) > 520: self.log.write('- WARNING: Unlaunch installed on the NAND dump') # Delete title.tmd in case it does not get overwritten remove(tmd) try: self.log.write('\nDownloading ' + self.launcher_region + ' launcher...') urlretrieve('https://raw.githubusercontent.com/mondul/HiyaCFW-Helper/master/' 'launchers/' + self.launcher_region, self.launcher_region) self.log.write('- Decrypting launcher...') exe = path.join(sysname, '7za') proc = Popen([ exe, 'x', '-bso0', '-y', '-p' + app, self.launcher_region, '00000002.app' ]) ret_val = proc.wait() if ret_val == 0: self.files.append(self.launcher_region) # Hash 00000002.app sha1_hash = sha1() with open('00000002.app', 'rb') as f: sha1_hash.update(f.read()) self.log.write('- Patched launcher SHA1:\n ' + sha1_hash.digest().hex().upper()) Thread(target=self.install_hiyacfw, args=(path.join(self.sd_path, 'title', '00030017', app, 'content', '00000002.app'),)).start() else: self.log.write('ERROR: Extractor failed') Thread(target=self.clean, args=(True,)).start() except IOError: self.log.write('ERROR: Could not download ' + self.launcher_region + ' launcher') Thread(target=self.clean, args=(True,)).start() except OSError: self.log.write('ERROR: Could not execute ' + exe) Thread(target=self.clean, args=(True,)).start() ################################################################################################ def install_hiyacfw(self, launcher_path): self.log.write('\nCopying HiyaCFW files...') copy_tree('for SDNAND SD card', self.sd_path, update=1) move('bootloader.nds', path.join(self.sd_path, 'hiya', 'bootloader.nds')) move('00000002.app', launcher_path) Thread(target=self.get_latest_twilight if self.twilight.get() == 1 else self.clean).start() ################################################################################################ def get_latest_twilight(self): filename = 'TWiLightMenu.7z' try: self.log.write('\nDownloading latest TWiLight Menu++ release...') conn = urlopen('https://api.github.com/repos/RocketRobz/TWiLightMenu/releases/' 'latest') latest = jsonify(conn) conn.close() urlretrieve(latest['assets'][0]['browser_download_url'], filename) self.log.write('- Extracting ' + filename[:-3] + ' archive...') exe = path.join(sysname, '7za') proc = Popen([ exe, 'x', '-bso0', '-y', filename, '_nds', 'DSi - CFW users', 'DSi&3DS - SD card users', 'roms' ]) ret_val = proc.wait() if ret_val == 0: self.files.append(filename) self.folders.append('DSi - CFW users') self.folders.append('DSi&3DS - SD card users') Thread(target=self.install_twilight, args=(filename[:-3],)).start() else: self.log.write('ERROR: Extractor failed') Thread(target=self.clean, args=(True,)).start() except (URLError, IOError) as e: self.log.write('ERROR: Could not get TWiLight Menu++') Thread(target=self.clean, args=(True,)).start() except OSError: self.log.write('ERROR: Could not execute ' + exe) Thread(target=self.clean, args=(True,)).start() ################################################################################################ def install_twilight(self, name): self.log.write('\nCopying ' + name + ' files...') copy_tree(path.join('DSi - CFW users', 'SDNAND root'), self.sd_path, update=1) move('_nds', path.join(self.sd_path, '_nds')) copy_tree('DSi&3DS - SD card users', self.sd_path, update=1) move('roms', path.join(self.sd_path, 'roms')) # Set files as read-only twlcfg0 = path.join(self.sd_path, 'shared1', 'TWLCFG0.dat') twlcfg1 = path.join(self.sd_path, 'shared1', 'TWLCFG1.dat') if sysname == 'Darwin': Popen([ 'chflags', 'uchg', twlcfg0, twlcfg1 ]).wait() elif sysname == 'Linux': Popen([ path.join('Linux', 'fatattr'), '+R', twlcfg0, twlcfg1 ]).wait() else: chmod(twlcfg0, 292) chmod(twlcfg1, 292) Thread(target=self.clean).start() ################################################################################################ def clean(self, err=False): self.log.write('\nCleaning...') while len(self.folders) > 0: rmtree(self.folders.pop(), ignore_errors=True) while len(self.files) > 0: try: remove(self.files.pop()) except: pass if err: self.log.write('Done') return # Get logged user in Linux if sysname == 'Linux': from os import getlogin # Workaround for some Linux systems where this function does not work try: ug = getlogin() except OSError: ug = 'root' if (self.nand_mode): file = self.console_id.get() + self.suffix + '.bin' rename(self.console_id.get() + '.img', file) # Change owner of the file in Linux if sysname == 'Linux': Popen([ 'chown', '-R', ug + ':' + ug, file ]).wait() self.log.write('\nDone!\nModified NAND stored as\n' + file) return # Change owner of the out folder in Linux if sysname == 'Linux': Popen([ 'chown', '-R', ug + ':' + ug, self.sd_path ]).wait() self.log.write('Done!\nExtract your SD card and insert it into your DSi') ################################################################################################ def patcher(self, patchpath, filepath): patch_size = path.getsize(patchpath) patchfile = open(patchpath, 'rb') if patchfile.read(5) != b'PATCH': patchfile.close() raise Exception() target = open(filepath, 'r+b') # Read First Record r = patchfile.read(3) while patchfile.tell() not in [ patch_size, patch_size - 3 ]: # Unpack 3-byte pointers. offset = self.unpack_int(r) # Read size of data chunk r = patchfile.read(2) size = self.unpack_int(r) if size == 0: # RLE Record r = patchfile.read(2) rle_size = self.unpack_int(r) data = patchfile.read(1) * rle_size else: data = patchfile.read(size) # Write to file target.seek(offset) target.write(data) # Read Next Record r = patchfile.read(3) if patch_size - 3 == patchfile.tell(): trim_size = self.unpack_int(patchfile.read(3)) target.truncate(trim_size) # Cleanup target.close() patchfile.close() ################################################################################################ def unpack_int(self, bstr): # Read an n-byte big-endian integer from a byte string ( ret_val, ) = unpack_from('>I', b'\x00' * (4 - len(bstr)) + bstr) return ret_val ################################################################################################ def detect_region(self): REGION_CODES = { '484e4145': 'USA', '484e414a': 'JAP', '484e4150': 'EUR', '484e4155': 'AUS' } # Autodetect console region base = self.mounted if self.nand_mode else self.sd_path try: for app in listdir(path.join(base, 'title', '00030017')): for file in listdir(path.join(base, 'title', '00030017', app, 'content')): if file.endswith('.app'): try: self.log.write('- Detected ' + REGION_CODES[app] + ' console NAND dump') self.launcher_region = REGION_CODES[app] return app except KeyError: self.log.write('ERROR: Unsupported console region') return False self.log.write('ERROR: Could not detect console region') except OSError as e: self.log.write('ERROR: ' + e.strerror + ': ' + e.filename) return False ################################################################################################ def unlaunch_proc(self): self.log.write('\nChecking unlaunch status...') app = self.detect_region() # Stop if no supported region was found if not app: # TODO: Unmount NAND return tmd = path.join(self.mounted, 'title', '00030017', app, 'content', 'title.tmd') tmd_size = path.getsize(tmd) if tmd_size == 520: self.log.write('- Not installed. Downloading v1.4...') try: filename = urlretrieve('http://problemkaputt.de/unlau14.zip')[0] exe = path.join(sysname, '7za') proc = Popen([ exe, 'x', '-bso0', '-y', filename, 'UNLAUNCH.DSI' ]) ret_val = proc.wait() if ret_val == 0: self.files.append(filename) self.files.append('UNLAUNCH.DSI') self.log.write('- Installing unlaunch...') self.suffix = '-unlaunch' with open(tmd, 'ab') as f: with open('UNLAUNCH.DSI', 'rb') as unl: f.write(unl.read()) # Set files as read-only for file in listdir(path.join(self.mounted, 'title', '00030017', app, 'content')): file = path.join(self.mounted, 'title', '00030017', app, 'content', file) if sysname == 'Darwin': Popen([ 'chflags', 'uchg', file ]).wait() elif sysname == 'Linux': Popen([ path.join('Linux', 'fatattr'), '+R', file ]).wait() else: chmod(file, 292) else: self.log.write('ERROR: Extractor failed') # TODO: Unmount NAND except IOError: self.log.write('ERROR: Could not get unlaunch') # TODO: Unmount NAND except OSError: self.log.write('ERROR: Could not execute ' + exe) # TODO: Unmount NAND else: self.log.write('- Installed. Uninstalling...') self.suffix = '-no-unlaunch' # Set files as read-write for file in listdir(path.join(self.mounted, 'title', '00030017', app, 'content')): file = path.join(self.mounted, 'title', '00030017', app, 'content', file) if sysname == 'Darwin': Popen([ 'chflags', 'nouchg', file ]).wait() elif sysname == 'Linux': Popen([ path.join('Linux', 'fatattr'), '-R', file ]).wait() else: chmod(file, 438) with open(tmd, 'r+b') as f: f.truncate(520) Thread(target=self.unmount_nand).start() ################################################################################################ def encrypt_nand(self): self.log.write('\nEncrypting back NAND...') exe = path.join(sysname, 'twltool') try: proc = Popen([ exe, 'nandcrypt', '--in', self.console_id.get() + '.img' ]) ret_val = proc.wait() if ret_val == 0: Thread(target=self.clean).start() else: self.log.write('ERROR: Encryptor failed') except OSError: self.log.write('ERROR: Could not execute ' + exe) ################################################################################################ def remove_footer(self): self.log.write('\nRemoving No$GBA footer...') file = self.console_id.get() + '-no-footer.bin' try: copyfile(self.nand_file.get(), file) # Back-up footer info with open(self.console_id.get() + '-info.txt', 'wb') as f: f.write('eMMC CID: ' + self.cid.get() + '\r\n') f.write('Console ID: ' + self.console_id.get() + '\r\n') with open(file, 'r+b') as f: # Go to the No$GBA footer offset f.seek(-64, 2) # Remove footer f.truncate() # Change owner of the file in Linux if sysname == 'Linux': from os import getlogin ug = getlogin() Popen([ 'chown', '-R', ug + ':' + ug, file ]).wait() self.log.write('\nDone!\nModified NAND stored as\n' + file + '\nStored footer info in ' + self.console_id.get() + '-info.txt') except IOError: self.log.write('ERROR: Could not open the file ' + path.basename(self.nand_file.get())) ################################################################################################ def add_footer(self, cid, console_id): self.log.write('Adding No$GBA footer...') file = self.console_id.get() + '-footer.bin' try: copyfile(self.nand_file.get(), file) with open(file, 'r+b') as f: # Go to the No$GBA footer offset f.seek(-64, 2) # Read the footer's header :-) bstr = f.read(0x10) # Check if it already has a footer if bstr == b'DSi eMMC CID/CPU': self.log.write('ERROR: File already has a No$GBA footer') f.close() remove(file) return; # Go to the end of file f.seek(0, 2) # Write footer f.write(b'DSi eMMC CID/CPU') f.write(cid) f.write(console_id) f.write('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') self.log.write('\nDone!\nModified NAND stored as\n' + file) except IOError: self.log.write('ERROR: Could not open the file ' + path.basename(self.nand_file.get()))
class Application(Frame): def __init__(self, master=None): super().__init__(master) self.pack() # First row f1 = LabelFrame(self, text='NAND file with No$GBA footer', padx=10, pady=10) # NAND Button self.nand_mode = False nand_icon = PhotoImage(data=('R0lGODlhEAAQAIMAAAAAADMzM2ZmZpmZmczMzP///wAAAAAAAAA' 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAMAAAYALAAAAAAQAB' 'AAAARG0MhJaxU4Y2sECAEgikE1CAFRhGMwSMJwBsU6frIgnR/bv' 'hTPrWUSDnGw3JGU2xmHrsvyU5xGO8ql6+S0AifPW8kCKpcpEQA7')) self.nand_button = Button(f1, image=nand_icon, command=self.change_mode, state=DISABLED) self.nand_button.image = nand_icon self.nand_button.pack(side='left') self.nand_file = StringVar() Entry(f1, textvariable=self.nand_file, state='readonly', width=40).pack(side='left') Button(f1, text='...', command=self.choose_nand).pack(side='left') f1.pack(padx=10, pady=10, fill=X) # Second row f2 = Frame(self) # Check box self.twilight = IntVar() self.twilight.set(1) self.chk = Checkbutton(f2, text='Install latest TWiLight Menu++ on custom firmware', variable=self.twilight) self.chk.pack(padx=10, anchor=W) # NAND operation frame self.nand_frame = LabelFrame(f2, text='NAND operation', padx=10, pady=10) self.nand_operation = IntVar() self.nand_operation.set(0) Radiobutton(self.nand_frame, text='Uninstall unlaunch or install v1.4 stable', variable=self.nand_operation, value=0, command=lambda: self.enable_entries(False)).pack(anchor=W) Radiobutton(self.nand_frame, text='Remove No$GBA footer', variable=self.nand_operation, value=1, command=lambda: self.enable_entries(False)).pack(anchor=W) Radiobutton(self.nand_frame, text='Add No$GBA footer', variable=self.nand_operation, value=2, command=lambda: self.enable_entries(True)).pack(anchor=W) fl = Frame(self.nand_frame) self.cid_label = Label(fl, text='eMMC CID', state=DISABLED) self.cid_label.pack(anchor=W, padx=(24, 0)) self.cid = StringVar() self.cid_entry = Entry(fl, textvariable=self.cid, width=20, state=DISABLED) self.cid_entry.pack(anchor=W, padx=(24, 0)) fl.pack(side='left') fr = Frame(self.nand_frame) self.console_id_label = Label(fr, text='Console ID', state=DISABLED) self.console_id_label.pack(anchor=W) self.console_id = StringVar() self.console_id_entry = Entry(fr, textvariable=self.console_id, width=20, state=DISABLED) self.console_id_entry.pack(anchor=W) fr.pack(side='right') f2.pack(fill=X) # Third row f3 = Frame(self) self.start_button = Button(f3, text='Start', width=16, command=self.hiya, state=DISABLED) self.start_button.pack(side='left', padx=(0, 5)) Button(f3, text='Quit', command=root.destroy, width=16).pack(side='left', padx=(5, 0)) f3.pack(pady=(10, 20)) self.folders = [] self.files = [] ################################################################################################ def change_mode(self): if (self.nand_mode): self.nand_frame.pack_forget() self.chk.pack(padx=10, anchor=W) self.nand_mode = False else: if askokcancel('Warning', ('You are about to enter NAND mode. Do it only if you know ' 'what you are doing. Proceed?'), icon=WARNING): self.chk.pack_forget() self.nand_frame.pack(padx=10, pady=(0, 10), fill=X) self.nand_mode = True ################################################################################################ def enable_entries(self, status): self.cid_label['state'] = (NORMAL if status else DISABLED) self.cid_entry['state'] = (NORMAL if status else DISABLED) self.console_id_label['state'] = (NORMAL if status else DISABLED) self.console_id_entry['state'] = (NORMAL if status else DISABLED) ################################################################################################ def choose_nand(self): name = askopenfilename(filetypes=( ( 'nand.bin', '*.bin' ), ( 'DSi-1.mmc', '*.mmc' ) )) self.nand_file.set(name) self.nand_button['state'] = (NORMAL if name != '' else DISABLED) self.start_button['state'] = (NORMAL if name != '' else DISABLED) ################################################################################################ def hiya(self): if not self.nand_mode: showinfo('Info', 'Now you will be asked to choose the SD card path that will be used ' 'for installing the custom firmware (or an output folder).\n\nIn order to avoid ' 'boot errors please assure it is empty before continuing.') self.sd_path = askdirectory() # Exit if no path was selected if self.sd_path == '': return # If adding a No$GBA footer, check if CID and ConsoleID values are OK elif self.nand_operation.get() == 2: cid = self.cid.get() console_id = self.console_id.get() # Check lengths if len(cid) != 32: showerror('Error', 'Bad eMMC CID') return elif len(console_id) != 16: showerror('Error', 'Bad Console ID') return # Parse strings to hex try: cid = bytearray.fromhex(cid) except ValueError: showerror('Error', 'Bad eMMC CID') return try: console_id = bytearray(reversed(bytearray.fromhex(console_id))) except ValueError: showerror('Error', 'Bad Console ID') return dialog = Toplevel(self) # Open as dialog (parent disabled) dialog.grab_set() dialog.title('Status') # Disable maximizing dialog.resizable(0, 0) frame = Frame(dialog, bd=2, relief=SUNKEN) scrollbar = Scrollbar(frame) scrollbar.pack(side=RIGHT, fill=Y) self.log = ThreadSafeText(frame, bd=0, width=52, height=20, yscrollcommand=scrollbar.set) self.log.pack() scrollbar.config(command=self.log.yview) frame.pack() Button(dialog, text='Close', command=dialog.destroy, width=16).pack(pady=10) # Center in window dialog.update_idletasks() width = dialog.winfo_width() height = dialog.winfo_height() dialog.geometry('%dx%d+%d+%d' % (width, height, root.winfo_x() + (root.winfo_width() / 2) - (width / 2), root.winfo_y() + (root.winfo_height() / 2) - (height / 2))) # Check if we'll be adding a No$GBA footer if self.nand_mode and self.nand_operation.get() == 2: Thread(target=self.add_footer, args=(cid, console_id)).start() else: Thread(target=self.check_nand).start() ################################################################################################ def check_nand(self): self.log.write('Checking NAND file...') # Read the NAND file try: with open(self.nand_file.get(), 'rb') as f: # Go to the No$GBA footer offset f.seek(-64, 2) # Read the footer's header :-) bstr = f.read(0x10) if bstr == b'DSi eMMC CID/CPU': # Read the CID bstr = f.read(0x10) self.cid.set(bstr.hex().upper()) self.log.write('- eMMC CID: ' + self.cid.get()) # Read the console ID bstr = f.read(8) self.console_id.set(bytearray(reversed(bstr)).hex().upper()) self.log.write('- Console ID: ' + self.console_id.get()) # Check we are making an unlaunch operation or removing the No$GBA footer if self.nand_mode: if self.nand_operation.get() == 0: Thread(target=self.decrypt_nand).start() else: Thread(target=self.remove_footer).start() pass else: Thread(target=self.get_latest_hiyacfw).start() else: self.log.write('ERROR: No$GBA footer not found') except IOError as e: print(e) self.log.write('ERROR: Could not open the file ' + path.basename(self.nand_file.get())) ################################################################################################ def get_latest_hiyacfw(self): # Try to use already downloaded HiyaCFW archive filename = 'HiyaCFW.7z' try: if path.isfile(filename): self.log.write('\nPreparing HiyaCFW...') else: self.log.write('\nDownloading latest HiyaCFW release...') conn = urlopen('https://api.github.com/repos/RocketRobz/hiyaCFW/releases/latest') latest = jsonify(conn) conn.close() with urlopen(latest['assets'][0] ['browser_download_url']) as src, open(filename, 'wb') as dst: copyfileobj(src, dst) self.log.write('- Extracting HiyaCFW archive...') exe = path.join(sysname, '7za') proc = Popen([ exe, 'x', '-bso0', '-y', filename, 'for PC', 'for SDNAND SD card' ]) ret_val = proc.wait() if ret_val == 0: self.files.append(filename) self.folders.append('for PC') self.folders.append('for SDNAND SD card') # Got to decrypt NAND if bootloader.nds is present Thread(target=self.decrypt_nand if path.isfile('bootloader.nds') else self.extract_bios).start() else: self.log.write('ERROR: Extractor failed') except (URLError, IOError) as e: print(e) self.log.write('ERROR: Could not get HiyaCFW') except OSError as e: print(e) self.log.write('ERROR: Could not execute ' + exe) ################################################################################################ def extract_bios(self): self.log.write('\nExtracting ARM7/ARM9 BIOS from NAND...') exe = path.join(sysname, 'twltool') try: proc = Popen([ exe, 'boot2', '--in', self.nand_file.get() ]) ret_val = proc.wait() if ret_val == 0: # Hash arm7.bin sha1_hash = sha1() with open('arm7.bin', 'rb') as f: sha1_hash.update(f.read()) self.log.write('- arm7.bin SHA1:\n ' + sha1_hash.digest().hex().upper()) # Hash arm9.bin sha1_hash = sha1() with open('arm9.bin', 'rb') as f: sha1_hash.update(f.read()) self.log.write('- arm9.bin SHA1:\n ' + sha1_hash.digest().hex().upper()) self.files.append('arm7.bin') self.files.append('arm9.bin') Thread(target=self.patch_bios).start() else: self.log.write('ERROR: Extractor failed') Thread(target=self.clean, args=(True,)).start() except OSError as e: print(e) self.log.write('ERROR: Could not execute ' + exe) Thread(target=self.clean, args=(True,)).start() ################################################################################################ def patch_bios(self): self.log.write('\nPatching ARM7/ARM9 BIOS...') try: self.patcher(path.join('for PC', 'bootloader files', 'bootloader arm7 patch.ips'), 'arm7.bin') self.patcher(path.join('for PC', 'bootloader files', 'bootloader arm9 patch.ips'), 'arm9.bin') # Hash arm7.bin sha1_hash = sha1() with open('arm7.bin', 'rb') as f: sha1_hash.update(f.read()) self.log.write('- Patched arm7.bin SHA1:\n ' + sha1_hash.digest().hex().upper()) # Hash arm9.bin sha1_hash = sha1() with open('arm9.bin', 'rb') as f: sha1_hash.update(f.read()) self.log.write('- Patched arm9.bin SHA1:\n ' + sha1_hash.digest().hex().upper()) Thread(target=self.arm9_prepend).start() except IOError as e: print(e) self.log.write('ERROR: Could not patch BIOS') Thread(target=self.clean, args=(True,)).start() except Exception as e: print(e) self.log.write('ERROR: Invalid patch header') Thread(target=self.clean, args=(True,)).start() ################################################################################################ def arm9_prepend(self): self.log.write('\nPrepending data to ARM9 BIOS...') try: with open('arm9.bin', 'rb') as f: data = f.read() with open('arm9.bin', 'wb') as f: with open(path.join('for PC', 'bootloader files', 'bootloader arm9 append to start.bin'), 'rb') as pre: f.write(pre.read()) f.write(data) # Hash arm9.bin sha1_hash = sha1() with open('arm9.bin', 'rb') as f: sha1_hash.update(f.read()) self.log.write('- Prepended arm9.bin SHA1:\n ' + sha1_hash.digest().hex().upper()) Thread(target=self.make_bootloader).start() except IOError as e: print(e) self.log.write('ERROR: Could not prepend data to ARM9 BIOS') Thread(target=self.clean, args=(True,)).start() ################################################################################################ def make_bootloader(self): self.log.write('\nGenerating new bootloader...') exe = (path.join('for PC', 'bootloader files', 'ndstool') if sysname == 'Windows' else path.join(sysname, 'ndsblc')) try: proc = Popen([ exe, '-c', 'bootloader.nds', '-9', 'arm9.bin', '-7', 'arm7.bin', '-t', path.join('for PC', 'bootloader files', 'banner.bin'), '-h', path.join('for PC', 'bootloader files', 'header.bin') ]) ret_val = proc.wait() if ret_val == 0: # Hash bootloader.nds sha1_hash = sha1() with open('bootloader.nds', 'rb') as f: sha1_hash.update(f.read()) self.log.write('- bootloader.nds SHA1:\n ' + sha1_hash.digest().hex().upper()) Thread(target=self.decrypt_nand).start() else: self.log.write('ERROR: Generator failed') Thread(target=self.clean, args=(True,)).start() except OSError as e: print(e) self.log.write('ERROR: Could not execute ' + exe) Thread(target=self.clean, args=(True,)).start() ################################################################################################ def decrypt_nand(self): self.log.write('\nDecrypting NAND...') exe = path.join(sysname, 'twltool') try: proc = Popen([ exe, 'nandcrypt', '--in', self.nand_file.get(), '--out', self.console_id.get() + '.img' ]) ret_val = proc.wait() print("\n") if ret_val == 0: if not self.nand_mode: self.files.append(self.console_id.get() + '.img') Thread(target=self.mount_nand).start() else: self.log.write('ERROR: Decryptor failed') Thread(target=self.clean, args=(True,)).start() except OSError as e: print(e) self.log.write('ERROR: Could not execute ' + exe) Thread(target=self.clean, args=(True,)).start() ################################################################################################ def mount_nand(self): self.log.write('\nMounting decrypted NAND...') try: if sysname == 'Windows': exe = osfmount cmd = [ osfmount, '-a', '-t', 'file', '-f', self.console_id.get() + '.img', '-m', '#:', '-o', 'ro,rem' ] if self.nand_mode: cmd[-1] = 'rw,rem' proc = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE) outs, errs = proc.communicate() if proc.returncode == 0: self.mounted = search(r'[a-zA-Z]:\s', outs.decode('utf-8')).group(0).strip() self.log.write('- Mounted on drive ' + self.mounted) else: self.log.write('ERROR: Mounter failed') Thread(target=self.clean, args=(True,)).start() return elif sysname == 'Darwin': exe = 'hdiutil' cmd = [ exe, 'attach', '-imagekey', 'diskimage-class=CRawDiskImage', '-nomount', self.console_id.get() + '.img' ] if not self.nand_mode: cmd.insert(2, '-readonly') proc = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE) outs, errs = proc.communicate() if proc.returncode == 0: self.raw_disk = search(r'^\/dev\/disk\d+', outs.decode('utf-8')).group(0) self.log.write('- Mounted raw disk on ' + self.raw_disk) cmd = [ exe, 'mount', self.raw_disk + 's1' ] if not self.nand_mode: cmd.insert(2, '-readonly') proc = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE) outs, errs = proc.communicate() if proc.returncode == 0: self.mounted = search(r'\/Volumes\/.+', outs.decode('utf-8')).group(0) self.log.write('- Mounted volume on ' + self.mounted) else: self.log.write('ERROR: Mounter failed') Thread(target=self.clean, args=(True,)).start() return else: self.log.write('ERROR: Mounter failed') Thread(target=self.clean, args=(True,)).start() return else: # Linux exe = 'losetup' cmd = [ exe, '-P', '-f', '--show', self.console_id.get() + '.img' ] if not self.nand_mode: cmd.insert(2, '-r') proc = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE) outs, errs = proc.communicate() if proc.returncode == 0: self.loop_dev = search(r'\/dev\/loop\d+', outs.decode('utf-8')).group(0) self.log.write('- Mounted loop device on ' + self.loop_dev) exe = 'mount' self.mounted = '/mnt' cmd = [ exe, '-t', 'vfat', self.loop_dev + 'p1', self.mounted ] if not self.nand_mode: cmd.insert(1, '-r') proc = Popen(cmd) ret_val = proc.wait() if ret_val == 0: self.log.write('- Mounted partition on ' + self.mounted) else: self.log.write('ERROR: Mounter failed') Thread(target=self.clean, args=(True,)).start() return else: self.log.write('ERROR: Mounter failed') Thread(target=self.clean, args=(True,)).start() return # Check we are making an unlaunch operation Thread(target=self.unlaunch_proc if self.nand_mode else self.extract_nand).start() except OSError as e: print(e) self.log.write('ERROR: Could not execute ' + exe) Thread(target=self.clean, args=(True,)).start() ################################################################################################ def extract_nand(self): self.log.write('\nExtracting files from NAND...') err = False # Reset copied files cache _path_created.clear() try: copy_tree(self.mounted, self.sd_path, preserve_mode=0, update=1) except Exception as e: print(e) self.log.write('ERROR: Extractor failed') err = True Thread(target=self.unmount_nand, args=(err,)).start() ################################################################################################ def unmount_nand(self, err=False): self.log.write('\nUnmounting NAND...') try: if sysname == 'Windows': exe = osfmount proc = Popen([ osfmount, '-D', '-m', self.mounted ]) elif sysname == 'Darwin': exe = 'hdiutil' proc = Popen([ exe, 'detach', self.raw_disk ]) else: # Linux exe = 'umount' proc = Popen([ exe, self.mounted ]) ret_val = proc.wait() if ret_val == 0: exe = 'losetup' proc = Popen([ exe, '-d', self.loop_dev ]) else: self.log.write('ERROR: Unmounter failed') Thread(target=self.clean, args=(True,)).start() return ret_val = proc.wait() if ret_val == 0: if err: Thread(target=self.clean, args=(True,)).start() else: Thread(target=self.encrypt_nand if self.nand_mode else self.get_launcher).start() else: self.log.write('ERROR: Unmounter failed') Thread(target=self.clean, args=(True,)).start() except OSError as e: print(e) self.log.write('ERROR: Could not execute ' + exe) Thread(target=self.clean, args=(True,)).start() ################################################################################################ def get_launcher(self): app = self.detect_region() # Stop if no supported region was found if not app: self.files.append('bootloader.nds') Thread(target=self.clean, args=(True,)).start() return # Check if unlaunch was installed on the NAND dump tmd = path.join(self.sd_path, 'title', '00030017', app, 'content', 'title.tmd') if path.getsize(tmd) > 520: self.log.write('- WARNING: Unlaunch installed on the NAND dump') # Delete title.tmd in case it does not get overwritten remove(tmd) # Try to use already downloaded launcher try: if path.isfile(self.launcher_region): self.log.write('\nPreparing ' + self.launcher_region + ' launcher...') else: self.log.write('\nDownloading ' + self.launcher_region + ' launcher...') with urlopen('https://raw.githubusercontent.com' '/mondul/HiyaCFW-Helper/master/launchers/' + self.launcher_region) as src, open(self.launcher_region, 'wb') as dst: copyfileobj(src, dst) self.log.write('- Decrypting launcher...') exe = path.join(sysname, '7za') proc = Popen([ exe, 'x', '-bso0', '-y', '-p' + app, self.launcher_region, '00000002.app' ]) ret_val = proc.wait() if ret_val == 0: self.files.append(self.launcher_region) # Hash 00000002.app sha1_hash = sha1() with open('00000002.app', 'rb') as f: sha1_hash.update(f.read()) self.log.write('- Patched launcher SHA1:\n ' + sha1_hash.digest().hex().upper()) Thread(target=self.install_hiyacfw, args=(path.join(self.sd_path, 'title', '00030017', app, 'content', '00000002.app'),)).start() else: self.log.write('ERROR: Extractor failed') Thread(target=self.clean, args=(True,)).start() except IOError as e: print(e) self.log.write('ERROR: Could not download ' + self.launcher_region + ' launcher') Thread(target=self.clean, args=(True,)).start() except OSError as e: print(e) self.log.write('ERROR: Could not execute ' + exe) Thread(target=self.clean, args=(True,)).start() ################################################################################################ def install_hiyacfw(self, launcher_path): self.log.write('\nCopying HiyaCFW files...') copy_tree('for SDNAND SD card', self.sd_path, update=1) move('bootloader.nds', path.join(self.sd_path, 'hiya', 'bootloader.nds')) move('00000002.app', launcher_path) Thread(target=self.get_latest_twilight if self.twilight.get() == 1 else self.clean).start() ################################################################################################ def get_latest_twilight(self): filename = 'TWiLightMenu.7z' try: self.log.write('\nDownloading latest TWiLight Menu++ release...') conn = urlopen('https://api.github.com/repos/DS-Homebrew/TWiLightMenu/releases/' 'latest') latest = jsonify(conn) conn.close() with urlopen(latest['assets'][0] ['browser_download_url']) as src, open(filename, 'wb') as dst: copyfileobj(src, dst) self.log.write('- Extracting ' + filename[:-3] + ' archive...') exe = path.join(sysname, '7za') proc = Popen([ exe, 'x', '-bso0', '-y', filename, '_nds', 'DSi - CFW users', 'DSi&3DS - SD card users', 'roms' ]) ret_val = proc.wait() if ret_val == 0: self.files.append(filename) self.folders.append('DSi - CFW users') self.folders.append('DSi&3DS - SD card users') Thread(target=self.install_twilight, args=(filename[:-3],)).start() else: self.log.write('ERROR: Extractor failed') Thread(target=self.clean, args=(True,)).start() except (URLError, IOError) as e: print(e) self.log.write('ERROR: Could not get TWiLight Menu++') Thread(target=self.clean, args=(True,)).start()
class AnalysisToolsManager: def __init__(self, controller): self.tab = None self.controller = controller self.tk_format = utils.TkFormat(self.controller.config_info) self.analysis_dialog = None self.exclude_artifacts = IntVar() self.abs_val = IntVar() self.use_max_for_centers = IntVar() self.use_delta = IntVar() self.neg_depth = IntVar() self.normalize_entry = None self.right_zoom_entry = None self.right_zoom_entry2 = None self.left_zoom_entry = None self.left_zoom_entry2 = None self.left_slope_entry = None self.right_slope_entry = None self.slopes_listbox = None self.abs_val_check = None self.use_max_for_centers_check = None self.use_delta_check = None self.neg_depth_check = None self.exclude_artifacts_check = None self.extra_analysis_check_frame = None self.plot_slope_var = None self.offset_entry = None self.offset_sample_var = None self.plot_slope_menu = None self.plot_slope_button = None self.analyze_var = None self.outer_slope_frame = None self.slope_results_frame = None def show(self, tab): self.tab = tab self.tab.freeze( ) # You have to finish dealing with this before, say, opening another analysis box. buttons = { "reset": { self.select_tab: [], self.tab.reset: [], self.uncheck_exclude_artifacts: [], self.disable_plot: [], # utils.thread_lift_widget: [], }, "close": {}, } self.analysis_dialog = VerticalScrolledDialog(self.controller, "Analyze Data", "", buttons=buttons, button_width=13) self.analysis_dialog.top.attributes("-topmost", True) outer_normalize_frame = Frame(self.analysis_dialog.interior, bg=self.tk_format.bg, padx=self.tk_format.padx, pady=15, highlightthickness=1) outer_normalize_frame.pack(expand=True, fill=BOTH) slope_title_label = Label(outer_normalize_frame, text="Normalize:", bg=self.tk_format.bg, fg=self.tk_format.textcolor) slope_title_label.pack() normalize_frame = Frame(outer_normalize_frame, bg=self.tk_format.bg, padx=self.tk_format.padx, pady=15) normalize_frame.pack() normalize_label = Label(normalize_frame, text="Wavelength (nm):", bg=self.tk_format.bg, fg=self.tk_format.textcolor) self.normalize_entry = Entry( normalize_frame, width=7, bd=self.tk_format.bd, bg=self.tk_format.entry_background, selectbackground=self.tk_format.selectbackground, selectforeground=self.tk_format.selectforeground, ) normalize_button = Button( normalize_frame, text="Apply", command=self.normalize, width=6, fg=self.tk_format.buttontextcolor, bg=self.tk_format.buttonbackgroundcolor, bd=self.tk_format.bd, ) normalize_button.config( fg=self.tk_format.buttontextcolor, highlightbackground=self.tk_format.highlightbackgroundcolor, bg=self.tk_format.buttonbackgroundcolor, ) normalize_button.pack(side=RIGHT, padx=(10, 10)) self.normalize_entry.pack(side=RIGHT, padx=self.tk_format.padx) normalize_label.pack(side=RIGHT, padx=self.tk_format.padx) outer_offset_frame = Frame(self.analysis_dialog.interior, bg=self.tk_format.bg, padx=self.tk_format.padx, pady=15, highlightthickness=1) outer_offset_frame.pack(expand=True, fill=BOTH) slope_title_label = Label(outer_offset_frame, text="Add offset to sample:", bg=self.tk_format.bg, fg=self.tk_format.textcolor) slope_title_label.pack(pady=(0, 15)) offset_sample_frame = Frame(outer_offset_frame, bg=self.tk_format.bg, padx=self.tk_format.padx, pady=self.tk_format.pady) offset_sample_frame.pack() offset_sample_label = Label(offset_sample_frame, text="Sample: ", bg=self.tk_format.bg, fg=self.tk_format.textcolor) offset_sample_label.pack(side=LEFT) self.offset_sample_var = StringVar() sample_names = [] repeats = False max_len = 0 for sample in self.tab.samples: if sample.name in sample_names: repeats = True else: sample_names.append(sample.name) max_len = np.max([max_len, len(sample.name)]) if repeats: sample_names = [] for sample in self.tab.samples: sample_names.append(sample.title + ": " + sample.name) max_len = np.max([max_len, len(sample_names[-1])]) self.offset_sample_var.set(sample_names[0]) # pylint: disable = no-value-for-parameter offset_menu = OptionMenu(offset_sample_frame, self.offset_sample_var, *sample_names) offset_menu.configure( width=max_len, highlightbackground=self.tk_format.highlightbackgroundcolor) offset_menu.pack(side=LEFT) offset_frame = Frame(outer_offset_frame, bg=self.tk_format.bg, padx=self.tk_format.padx, pady=15) offset_frame.pack() offset_label = Label(offset_frame, text="Offset:", bg=self.tk_format.bg, fg=self.tk_format.textcolor) self.offset_entry = Entry( offset_frame, width=7, bd=self.tk_format.bd, bg=self.tk_format.entry_background, selectbackground=self.tk_format.selectbackground, selectforeground=self.tk_format.selectforeground, ) offset_button = Button( offset_frame, text="Apply", command=self.offset, width=6, fg=self.tk_format.buttontextcolor, bg=self.tk_format.buttonbackgroundcolor, bd=self.tk_format.bd, ) offset_button.config( fg=self.tk_format.buttontextcolor, highlightbackground=self.tk_format.highlightbackgroundcolor, bg=self.tk_format.buttonbackgroundcolor, ) offset_button.pack(side=RIGHT, padx=(10, 10)) self.offset_entry.pack(side=RIGHT, padx=self.tk_format.padx) offset_label.pack(side=RIGHT, padx=self.tk_format.padx) outer_outer_zoom_frame = Frame(self.analysis_dialog.interior, bg=self.tk_format.bg, padx=self.tk_format.padx, pady=15, highlightthickness=1) outer_outer_zoom_frame.pack(expand=True, fill=BOTH) zoom_title_frame = Frame(outer_outer_zoom_frame, bg=self.tk_format.bg) zoom_title_frame.pack(pady=(5, 10)) zoom_title_label = Label(zoom_title_frame, text="Adjust plot x and y limits:", bg=self.tk_format.bg, fg=self.tk_format.textcolor) zoom_title_label.pack(side=LEFT, pady=(0, 4)) outer_zoom_frame = Frame(outer_outer_zoom_frame, bg=self.tk_format.bg, padx=self.tk_format.padx) outer_zoom_frame.pack(expand=True, fill=BOTH, pady=(0, 10)) zoom_frame = Frame(outer_zoom_frame, bg=self.tk_format.bg, padx=self.tk_format.padx) zoom_frame.pack() zoom_label = Label(zoom_frame, text="x1:", bg=self.tk_format.bg, fg=self.tk_format.textcolor) self.left_zoom_entry = Entry( zoom_frame, width=7, bd=self.tk_format.bd, bg=self.tk_format.entry_background, selectbackground=self.tk_format.selectbackground, selectforeground=self.tk_format.selectforeground, ) zoom_label2 = Label(zoom_frame, text="x2:", bg=self.tk_format.bg, fg=self.tk_format.textcolor) self.right_zoom_entry = Entry( zoom_frame, width=7, bd=self.tk_format.bd, bg=self.tk_format.entry_background, selectbackground=self.tk_format.selectbackground, selectforeground=self.tk_format.selectforeground, ) zoom_button = Button( zoom_frame, text="Apply", command=self.apply_x, width=7, fg=self.tk_format.buttontextcolor, bg=self.tk_format.buttonbackgroundcolor, bd=self.tk_format.bd, ) zoom_button.config( fg=self.tk_format.buttontextcolor, highlightbackground=self.tk_format.highlightbackgroundcolor, bg=self.tk_format.buttonbackgroundcolor, ) zoom_button.pack(side=RIGHT, padx=(10, 10)) self.right_zoom_entry.pack(side=RIGHT, padx=self.tk_format.padx) zoom_label2.pack(side=RIGHT, padx=self.tk_format.padx) self.left_zoom_entry.pack(side=RIGHT, padx=self.tk_format.padx) zoom_label.pack(side=RIGHT, padx=self.tk_format.padx) outer_zoom_frame2 = Frame(outer_outer_zoom_frame, bg=self.tk_format.bg, padx=self.tk_format.padx) outer_zoom_frame2.pack(expand=True, fill=BOTH, pady=(0, 10)) zoom_frame2 = Frame(outer_zoom_frame2, bg=self.tk_format.bg, padx=self.tk_format.padx) zoom_frame2.pack() zoom_label3 = Label(zoom_frame2, text="y1:", bg=self.tk_format.bg, fg=self.tk_format.textcolor) self.left_zoom_entry2 = Entry( zoom_frame2, width=7, bd=self.tk_format.bd, bg=self.tk_format.entry_background, selectbackground=self.tk_format.selectbackground, selectforeground=self.tk_format.selectforeground, ) zoom_label4 = Label(zoom_frame2, text="y2:", bg=self.tk_format.bg, fg=self.tk_format.textcolor) self.right_zoom_entry2 = Entry( zoom_frame2, width=7, bd=self.tk_format.bd, bg=self.tk_format.entry_background, selectbackground=self.tk_format.selectbackground, selectforeground=self.tk_format.selectforeground, ) zoom_button2 = Button( zoom_frame2, text="Apply", command=self.apply_y, width=7, fg=self.tk_format.buttontextcolor, bg=self.tk_format.buttonbackgroundcolor, bd=self.tk_format.bd, ) zoom_button2.config( fg=self.tk_format.buttontextcolor, highlightbackground=self.tk_format.highlightbackgroundcolor, bg=self.tk_format.buttonbackgroundcolor, ) zoom_button2.pack(side=RIGHT, padx=(10, 10)) self.right_zoom_entry2.pack(side=RIGHT, padx=self.tk_format.padx) zoom_label4.pack(side=RIGHT, padx=self.tk_format.padx) self.left_zoom_entry2.pack(side=RIGHT, padx=self.tk_format.padx) zoom_label3.pack(side=RIGHT, padx=self.tk_format.padx) outer_outer_slope_frame = Frame(self.analysis_dialog.interior, bg=self.tk_format.bg, padx=self.tk_format.padx, pady=15, highlightthickness=1) outer_outer_slope_frame.pack(expand=True, fill=BOTH) self.outer_slope_frame = Frame(outer_outer_slope_frame, bg=self.tk_format.bg, padx=self.tk_format.padx) self.outer_slope_frame.pack(expand=True, fill=BOTH, pady=(0, 10)) slope_title_frame = Frame(self.outer_slope_frame, bg=self.tk_format.bg) slope_title_frame.pack(pady=(5, 5)) slope_title_label = Label(slope_title_frame, text="Analyze ", bg=self.tk_format.bg, fg=self.tk_format.textcolor) slope_title_label.pack(side=LEFT, pady=(0, 4)) self.analyze_var = StringVar() self.analyze_var.set("slope") analyze_menu = OptionMenu( slope_title_frame, self.analyze_var, "slope", "band depth", "band center", "reflectance", # "reciprocity", "difference", command=self.disable_plot, ) analyze_menu.configure( width=10, highlightbackground=self.tk_format.highlightbackgroundcolor) analyze_menu.pack(side=LEFT) # We'll put checkboxes for additional options into this frame at the time the user selects a given option (e.g. # select 'difference' from menu, add option to calculate differences based on absolute value self.extra_analysis_check_frame = Frame(self.outer_slope_frame, bg=self.tk_format.bg, padx=self.tk_format.padx) self.extra_analysis_check_frame.pack() # Note that we are not packing this checkbutton yet. self.abs_val_check = Checkbutton( self.extra_analysis_check_frame, selectcolor=self.tk_format.check_bg, fg=self.tk_format.textcolor, text=" Use absolute values for average differences", bg=self.tk_format.bg, pady=self.tk_format.pady, highlightthickness=0, variable=self.abs_val, ) self.use_max_for_centers_check = Checkbutton( self.extra_analysis_check_frame, selectcolor=self.tk_format.check_bg, fg=self.tk_format.textcolor, text= " If band max is more prominent than\nband min, use to find center.", bg=self.tk_format.bg, pady=self.tk_format.pady, highlightthickness=0, variable=self.use_max_for_centers, ) self.use_max_for_centers_check.select() self.use_delta_check = Checkbutton( self.extra_analysis_check_frame, selectcolor=self.tk_format.check_bg, fg=self.tk_format.textcolor, text=" Center at max \u0394" + "R from continuum \nrather than spectral min/max. ", bg=self.tk_format.bg, pady=self.tk_format.pady, highlightthickness=0, variable=self.use_delta, ) self.use_delta_check.select() self.neg_depth_check = Checkbutton( self.extra_analysis_check_frame, selectcolor=self.tk_format.check_bg, fg=self.tk_format.textcolor, text= " If band max is more prominent than \nband min, report negative depth.", bg=self.tk_format.bg, pady=self.tk_format.pady, highlightthickness=0, variable=self.neg_depth, ) self.neg_depth_check.select() slope_frame = Frame(self.outer_slope_frame, bg=self.tk_format.bg, padx=self.tk_format.padx, highlightthickness=0) slope_frame.pack(pady=(15, 0)) slope_label = Label(slope_frame, text="x1:", bg=self.tk_format.bg, fg=self.tk_format.textcolor) self.left_slope_entry = Entry( slope_frame, width=7, bd=self.tk_format.bd, bg=self.tk_format.entry_background, selectbackground=self.tk_format.selectbackground, selectforeground=self.tk_format.selectforeground, ) slope_label_2 = Label(slope_frame, text="x2:", bg=self.tk_format.bg, fg=self.tk_format.textcolor) self.right_slope_entry = Entry( slope_frame, width=7, bd=self.tk_format.bd, bg=self.tk_format.entry_background, selectbackground=self.tk_format.selectbackground, selectforeground=self.tk_format.selectforeground, ) slope_button = Button( slope_frame, text="Calculate", command=self.calculate, width=7, fg=self.tk_format.buttontextcolor, bg=self.tk_format.buttonbackgroundcolor, bd=self.tk_format.bd, ) slope_button.config( fg=self.tk_format.buttontextcolor, highlightbackground=self.tk_format.highlightbackgroundcolor, bg=self.tk_format.buttonbackgroundcolor, ) slope_button.pack(side=RIGHT, padx=(10, 10)) self.right_slope_entry.pack(side=RIGHT, padx=self.tk_format.padx) slope_label_2.pack(side=RIGHT, padx=self.tk_format.padx) self.left_slope_entry.pack(side=RIGHT, padx=self.tk_format.padx) slope_label.pack(side=RIGHT, padx=self.tk_format.padx) self.slope_results_frame = Frame(self.outer_slope_frame, bg=self.tk_format.bg) self.slope_results_frame.pack( fill=BOTH, expand=True ) # We'll put a listbox with slope info in here later after calculating. outer_plot_slope_frame = Frame(outer_outer_slope_frame, bg=self.tk_format.bg, padx=self.tk_format.padx, pady=10) outer_plot_slope_frame.pack(expand=True, fill=BOTH) plot_slope_frame = Frame(outer_plot_slope_frame, bg=self.tk_format.bg, padx=self.tk_format.padx) plot_slope_frame.pack(side=RIGHT) plot_slope_label = Label(plot_slope_frame, text="Plot as a function of", bg=self.tk_format.bg, fg=self.tk_format.textcolor) self.plot_slope_var = StringVar() self.plot_slope_var.set("e") self.plot_slope_menu = OptionMenu(plot_slope_frame, self.plot_slope_var, "e", "i", "g", "e,i", "theta", "az, e") self.plot_slope_menu.configure( width=2, highlightbackground=self.tk_format.highlightbackgroundcolor) self.plot_slope_button = Button( plot_slope_frame, text="Plot", command=self.plot, width=7, fg=self.tk_format.buttontextcolor, bg=self.tk_format.buttonbackgroundcolor, bd=self.tk_format.bd, ) self.plot_slope_button.config( fg=self.tk_format.buttontextcolor, highlightbackground=self.tk_format.highlightbackgroundcolor, bg=self.tk_format.buttonbackgroundcolor, state=DISABLED, ) self.plot_slope_button.pack(side=RIGHT, padx=(10, 10)) self.plot_slope_menu.pack(side=RIGHT, padx=self.tk_format.padx) plot_slope_label.pack(side=RIGHT, padx=self.tk_format.padx) exclude_artifacts_frame = Frame(self.analysis_dialog.interior, bg=self.tk_format.bg, padx=self.tk_format.padx, pady=15, highlightthickness=1) exclude_artifacts_frame.pack(fill=BOTH, expand=True) self.exclude_artifacts_check = Checkbutton( exclude_artifacts_frame, selectcolor=self.tk_format.check_bg, fg=self.tk_format.textcolor, text= " Exclude data susceptible to artifacts\n (high g, 1000-1400 nm) ", bg=self.tk_format.bg, pady=self.tk_format.pady, highlightthickness=0, variable=self.exclude_artifacts, # TODO: check if the comma is meant to be there in the lambda definition command=lambda x="foo", : self.tab.set_exclude_artifacts( self.exclude_artifacts.get()), ) self.exclude_artifacts_check.pack() if self.tab.exclude_artifacts: self.exclude_artifacts_check.select() self.analysis_dialog.interior.configure(highlightthickness=1, highlightcolor="white") def calculate(self): try: self.controller.view_notebook.select(self.tab.top) except TclError: print("Error selecting tab in analysis_tools_manager.calculate().") print(self.tab) pass artifact_warning = False if self.analyze_var.get() == "slope": left, right, slopes, artifact_warning = self.tab.calculate_slopes( self.left_slope_entry.get(), self.right_slope_entry.get()) self.update_entries(left, right) self.populate_listbox(slopes) self.update_plot_menu(["e", "i", "g", "e,i", "theta", "az, e"]) elif self.analyze_var.get() == "band depth": left, right, depths, artifact_warning = self.tab.calculate_band_depths( self.left_slope_entry.get(), self.right_slope_entry.get(), self.neg_depth.get(), self.use_delta.get(), ) self.update_entries(left, right) self.populate_listbox(depths) self.update_plot_menu(["e", "i", "g", "e,i", "theta", "az, e"]) elif self.analyze_var.get() == "band center": left, right, centers, artifact_warning = self.tab.calculate_band_centers( self.left_slope_entry.get(), self.right_slope_entry.get(), self.use_max_for_centers.get(), self.use_delta.get(), ) self.update_entries(left, right) self.populate_listbox(centers) self.update_plot_menu(["e", "i", "g", "e,i", "theta", "az, e"]) elif self.analyze_var.get() == "reflectance": left, right, reflectance, artifact_warning = self.tab.calculate_avg_reflectance( self.left_slope_entry.get(), self.right_slope_entry.get()) self.update_entries(left, right) self.populate_listbox(reflectance) self.update_plot_menu(["e", "i", "g", "e,i", "theta", "az, e"]) elif self.analyze_var.get() == "reciprocity": left, right, reciprocity, artifact_warning = self.tab.calculate_reciprocity( self.left_slope_entry.get(), self.right_slope_entry.get()) self.update_entries(left, right) self.populate_listbox(reciprocity) self.update_plot_menu(["e", "i", "g", "e,i"]) elif self.analyze_var.get() == "difference": left, right, error, artifact_warning = self.tab.calculate_error( self.left_slope_entry.get(), self.right_slope_entry.get(), self.abs_val.get()) # Tab validates left and right values. If they are no good, put in min and max wavelengths available. self.update_entries(left, right) self.populate_listbox(error) self.update_plot_menu(["\u03bb", "e,i"]) if artifact_warning: ErrorDialog( self, "Warning", "Warning: Excluding data potentially\ninfluenced by artifacts from 1000-1400 nm." ) self.analysis_dialog.min_height = 1000 self.analysis_dialog.update() def update_plot_menu(self, plot_options): self.plot_slope_var.set(plot_options[0]) self.plot_slope_menu["menu"].delete(0, "end") # Insert list of new options (tk._setit hooks them up to var) max_len = len(plot_options[0]) for option in plot_options: max_len = np.max([max_len, len(option)]) # pylint: disable = protected-access self.plot_slope_menu["menu"].add_command(label=option, command=tkinter._setit( self.plot_slope_var, option)) self.plot_slope_menu.configure(width=max_len) def update_entries(self, left, right): self.left_slope_entry.delete(0, "end") self.left_slope_entry.insert(0, str(left)) self.right_slope_entry.delete(0, "end") self.right_slope_entry.insert(0, str(right)) def populate_listbox(self, results): if len(results) > 0: self.slope_results_frame.pack(fill=BOTH, expand=True, pady=(10, 10)) try: self.slopes_listbox.delete(0, "end") except (AttributeError, TclError): self.slopes_listbox = utils.ScrollableListbox( self.slope_results_frame, self.tk_format.bg, self.tk_format.entry_background, self.tk_format.listboxhighlightcolor, selectmode=EXTENDED, ) self.slopes_listbox.configure(height=8) for result in results: self.slopes_listbox.insert("end", result) self.slopes_listbox.pack(fill=BOTH, expand=True) self.plot_slope_button.configure(state=NORMAL) def plot(self): if self.analyze_var.get() == "slope": self.tab.plot_slopes(self.plot_slope_var.get()) elif self.analyze_var.get() == "band depth": self.tab.plot_band_depths(self.plot_slope_var.get()) elif self.analyze_var.get() == "band center": self.tab.plot_band_centers(self.plot_slope_var.get()) elif self.analyze_var.get() == "reflectance": self.tab.plot_avg_reflectance(self.plot_slope_var.get()) elif self.analyze_var.get() == "reciprocity": self.tab.plot_reciprocity(self.plot_slope_var.get()) elif self.analyze_var.get() == "difference": new = self.tab.plot_error(self.plot_slope_var.get()) if self.plot_slope_var.get() == "\u03bb": x1 = float(self.left_slope_entry.get()) x2 = float(self.right_slope_entry.get()) new.adjust_x(x1, x2) # TODO: plots not always fully updating # (e.g. contour plot labels not showing up until you do a screen wiggle. # utils.thread_lift_widget(self.analysis_dialog.top) def normalize(self): self.select_tab() try: self.slopes_listbox.delete(0, "end") self.plot_slope_button.configure(state="disabled") except (AttributeError, TclError): pass self.tab.normalize(self.normalize_entry.get()) # thread = Thread(target=utils.lift_widget, args=(self.analysis_dialog.top,)) # thread.start() def offset(self): self.tab.offset(self.offset_sample_var.get(), self.offset_entry.get()) # This doesn't work - it hangs between thread.start() and thread.join(). Likely because of calls to canvas.draw() # thread = Thread(target=self.tab.offset, args=(self.offset_sample_var.get(), self.offset_entry.get())) # thread.start() # thread.join() # utils.lift_widget(self.analysis_dialog.top) def remove_topmost(self): print("removing!!") self.analysis_dialog.top.attributes("-topmost", False) def apply_x(self): self.controller.view_notebook.select(self.tab.top) try: x1 = float(self.left_zoom_entry.get()) x2 = float(self.right_zoom_entry.get()) self.tab.adjust_x(x1, x2) # utils.lift_widget(self.analysis_dialog.top) except ValueError: # utils.lift_widget(self.analysis_dialog.top) ErrorDialog( self, title="Invalid Zoom Range", label="Error! Invalid x limits: " + self.left_zoom_entry.get() + ", " + self.right_zoom_entry.get(), ) def apply_y(self): self.controller.view_notebook.select(self.tab.top) try: y1 = float(self.left_zoom_entry2.get()) y2 = float(self.right_zoom_entry2.get()) self.tab.adjust_y(y1, y2) # utils.lift_widget(self.analysis_dialog.top) except ValueError: # utils.lift_widget(self.analysis_dialog.top) ErrorDialog( self, title="Invalid Zoom Range", label="Error! Invalid y limits: " + self.left_zoom_entry2.get() + ", " + self.right_zoom_entry2.get(), ) def uncheck_exclude_artifacts(self): self.exclude_artifacts.set(0) self.exclude_artifacts_check.deselect() # utils.lift_widget(self.analysis_dialog.top) def disable_plot(self, analyze_var="None"): try: self.slopes_listbox.delete(0, "end") except (AttributeError, TclError): pass self.plot_slope_button.configure(state="disabled") if analyze_var == "difference": self.analysis_dialog.frame.min_height = 850 self.neg_depth_check.pack_forget() self.use_max_for_centers_check.pack_forget() self.use_delta_check.pack_forget() self.abs_val_check.pack() self.extra_analysis_check_frame.pack() elif analyze_var == "band center": self.analysis_dialog.frame.min_height = 1000 self.neg_depth_check.pack_forget() self.abs_val_check.pack_forget() self.use_delta_check.pack_forget() self.use_max_for_centers_check.pack() self.use_delta_check.pack() self.extra_analysis_check_frame.pack() elif analyze_var == "band depth": self.analysis_dialog.frame.min_height = 1000 self.abs_val_check.pack_forget() self.use_max_for_centers_check.pack_forget() self.use_delta_check.pack_forget() self.neg_depth_check.pack() self.use_delta_check.pack() self.extra_analysis_check_frame.pack() else: self.analysis_dialog.frame.min_height = 850 self.abs_val_check.pack_forget() self.neg_depth_check.pack_forget() self.use_max_for_centers_check.pack_forget() self.use_delta_check.pack_forget() self.extra_analysis_check_frame.grid_propagate(0) self.extra_analysis_check_frame.configure( height=1) # for some reason 0 doesn't work. self.extra_analysis_check_frame.pack() self.outer_slope_frame.pack() # utils.lift_widget(self.analysis_dialog.top) # def calculate_photometric_variability(self): # photo_var = self.tab.calculate_photometric_variability( # self.right_photo_var_entry.get(), self.left_photo_var_entry.get() # ) # try: # self.photo_var_listbox.delete(0, "end") # except: # self.photo_var_listbox = utils.ScrollableListbox( # self.photo_var_results_frame, # self.tk_format.bg, # self.tk_format.entry_background, # self.tk_format.listboxhighlightcolor, # selectmode=EXTENDED, # ) # for var in photo_var: # self.photo_var_listbox.insert("end", var) # self.photo_var_listbox.pack(fill=BOTH, expand=True) def select_tab(self): self.controller.view_notebook.select(self.tab.top)