#!/usr/bin/python
# *************************************************************************
# Python 3.x rewrite by nicl83
# *************************************************************************
# Teensy++ 2.0 modifications by Effleurage
#  NANDway.py
#
# Teensy++ 2.0 modifications by judges@eEcho.com
# *************************************************************************
#  noralizer.py - NOR flasher for PS3
#
# Copyright (C) 2010-2011  Hector Martin "marcan" <hector@marcansoft.com>
#
# This code is licensed to you under the terms of the GNU GPL, version 2;
# see file COPYING or http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt
# *************************************************************************

import time
import datetime
import sys
import serial


class TeensySerialError(Exception):
    "Exception class for errors when communicating with the Teensy."


class TeensySerial:
    "Class for communicating with a Teensy running NANDWay firmware."
    BUFSIZE = 32768

    def __init__(self, port: str):
        try:
            self.ser = serial.Serial(
                port,
                baudrate=9600,
                timeout=300,
                rtscts=False,
                dsrdtr=False,
                xonxoff=False,
                write_timeout=120)
        except serial.SerialException as exc:
            raise TeensySerialError(f"could not open serial {port}") from exc
        self.ser.reset_input_buffer()
        self.ser.reset_output_buffer()
        self.obuf: bytearray = bytearray()

    def write(self, write_data: int | bytes):
        """
        Add data to the output buffer.
        If data overflows buffer, it will be flushed to the device. (FIFO)
        """
        if isinstance(write_data, int):
            self.obuf.append(write_data)
        else:
            self.obuf.extend(write_data)
        while len(self.obuf) > self.BUFSIZE:
            self.ser.write(self.obuf[:self.BUFSIZE])
            self.obuf = self.obuf[self.BUFSIZE:]

    def flush(self):
        "Flush the output buffer to the device."
        if len(self.obuf):
            self.ser.write(self.obuf)
            self.ser.flush()
            self.obuf.clear()

    def read(self, size: int):
        "Read data from the serial device."
        self.flush()
        read_data = self.ser.read(size)
        return read_data

    def readbyte(self):
        "Read one byte from the serial device."
        return self.read(1)[0]

    def close(self):
        "Close the serial device."
        print()
        print("Closing serial device...")
        self.ser.close()
        print("Done.")


class NANDError(Exception):
    "Exception class for errors when interacting with the NAND."


class NANDFlasher(TeensySerial):
    "Class for a NAND flasher using a Teensy running NANDWay firmware."
    # pylint: disable=too-many-instance-attributes
    mf_id: int = 0
    device_id: int = 0
    nand_page_size: int = 0
    nand_ras: int = 0  # Redundent Area Size
    nand_page_size_plus_ras: int = 0
    nand_page_count: int = 0
    nand_block_count: int = 0
    nand_pages_per_block: int = 0
    nand_block_size: int = 0
    nand_block_size_plus_ras: int = 0
    nand_bus_width: int = 0
    nand_plane_count: int = 0
    nand_plane_size: int = 0

    # Teensy commands
    CMD_PING1 = 0
    CMD_PING2 = 1
    CMD_BOOTLOADER = 2
    CMD_IO_LOCK = 3
    CMD_IO_RELEASE = 4
    CMD_PULLUPS_DISABLE = 5
    CMD_PULLUPS_ENABLE = 6
    CMD_NAND0_ID = 7
    CMD_NAND0_READPAGE = 8
    CMD_NAND0_WRITEPAGE = 9
    CMD_NAND0_ERASEBLOCK = 10
    CMD_NAND1_ID = 11
    CMD_NAND1_READPAGE = 12
    CMD_NAND1_WRITEPAGE = 13
    CMD_NAND1_ERASEBLOCK = 14

    # NAND names
    NAND_NAMES = {
        0xEC: {  # Samsung
            "vendor_name": "Samsung",
            0xA1: "K9F1G08R0A",
            0XD5: "K9GAG08U0M",
            0xF1: "K9F1G08U0A",
            0x79: "K9T1G08U0M",
            0xDA: "K9F2G08U0M"
        },
        0xAD: {  # Hynix (oh no)
            "vendor_name": "Hynix",
            0x73: "HY27US08281A",
            0xD7: "H27UBG8T2A",
            0xDA: "HY27UF082G2B",
            0xDC: "H27U4G8F2D"
        },
        0x98: {  # Toshiba
            "vendor_name": "Toshiba",
            0xDC: "TC58NVG2S3E"
        }
    }

    def __init__(self, port: str, nand_id: int, ver_major: int, ver_minor: int):
        if port:
            super().__init__(port)
        self.nand_id = nand_id & 1
        self.nand_disable_pullups = nand_id & 10
        self.version_major = ver_major
        self.version_minor = ver_minor

    def ping(self):
        "Ping the Teensy and check the firmware version."
        self.write(self.CMD_PING1)
        self.write(self.CMD_PING2)
        ver_major = self.readbyte()
        ver_minor = self.readbyte()
        free_ram = (self.readbyte() << 8) | self.readbyte()
        if (ver_major != self.version_major) or (ver_minor != self.version_minor):
            print(
                "Ping failed",
                f"(expected v{self.version_major}.{self.version_minor:02},",
                f"got {ver_major}.{ver_minor:02})"
            )
            self.close()
            sys.exit(1)

        return free_ram

    def readid(self):
        "Read the manufacturer and device IDs from the device."
        if self.nand_disable_pullups == 0:
            self.write(self.CMD_PULLUPS_ENABLE)
        else:
            self.write(self.CMD_PULLUPS_DISABLE)

        if self.nand_id == 1:
            self.write(self.CMD_NAND1_ID)
        else:
            self.write(self.CMD_NAND0_ID)

        is_command_supported = self.readbyte()
        if is_command_supported != 89:  # 'Y'
            print()
            print("NAND_ID 1 not supported for Signal Booster Edition! Exiting...")
            self.close()
            sys.exit(1)

        nand_info = self.read(25)

        print("Raw ID info:",
              ' '.join(f"0x{byte:02x}" for byte in nand_info[0:5])
              )

        self.mf_id = nand_info[0]
        self.device_id = nand_info[1]

        self.nand_page_size = (nand_info[5] << 24) | (
            nand_info[6] << 16) | (nand_info[7] << 8) | nand_info[8]

        self.nand_ras = (nand_info[9] << 8) | nand_info[10]

        self.nand_bus_width = nand_info[11]

        self.nand_block_size = (nand_info[12] << 24) | (
            nand_info[13] << 16) | (nand_info[14] << 8) | nand_info[15]
        self.nand_block_count = (nand_info[16] << 24) | (
            nand_info[17] << 16) | (nand_info[18] << 8) | nand_info[19]

        self.nand_plane_count = nand_info[20]
        self.nand_plane_size = (nand_info[21] << 24) | (
            nand_info[22] << 16) | (nand_info[23] << 8) | nand_info[24]

        if (self.nand_page_size <= 0):
            print()
            print("Error reading size of NAND! Exiting...")
            self.close()
            sys.exit(1)
        if (self.nand_bus_width != 8):
            print()
            print("Only 8-bit NANDs are supported! Exiting...")
            self.close()
            sys.exit(1)
        if (self.mf_id == 0):
            print()
            print("Unknown chip manufacturer! Exiting...")
            self.close()
            sys.exit(1)
        if (self.device_id == 0):
            print()
            print("Unknown device id! Exiting...")
            self.close()
            sys.exit(1)

        if self.mf_id == 0x98 and self.device_id == 0xdc:
            # TC58NVG2S3E
            self.nand_block_count = self.nand_block_count // 4
            self.nand_plane_size = self.nand_plane_size // 4

        self.nand_pages_per_block = int(
            self.nand_block_size / self.nand_page_size)
        self.nand_page_size_plus_ras = self.nand_page_size + self.nand_ras
        self.nand_page_count = self.nand_pages_per_block * self.nand_block_count
        self.nand_block_size_plus_ras = self.nand_pages_per_block * \
            self.nand_page_size_plus_ras

    def printstate(self):
        "Print information on the current NAND state."
        # print "NAND%d information:"%self.NAND_ID
        print(f"NAND{self.nand_id} information:")
        self.readid()

        print()
        if self.mf_id in self.NAND_NAMES:
            mfg_name = self.NAND_NAMES[self.mf_id]['vendor_name']
            nand_name = self.NAND_NAMES[self.mf_id].get(
                self.device_id, "Unknown")
            print(f"NAND chip manufacturer: {mfg_name} (0x{self.mf_id:02x})")
            print(
                f"NAND chip type:        {nand_name} (0x{self.device_id:02x})")
        else:
            print(f"NAND chip manufacturer: unknown (0x{self.mf_id:02x})")
            print(f"NAND chip type:        unknown (0x{self.device_id:02x})")

        print(f"""
        NAND size:              {self.nand_block_size * self.nand_block_count / 1024 / 1024} MB
        NAND plus RAS size:     {self.nand_block_size_plus_ras * self.nand_block_count / 1024 / 1024} MB
        Page size:              {self.nand_page_size} bytes
        Page plus RAS size:     {self.nand_page_size_plus_ras} bytes
        Block size:             {self.nand_block_size} bytes
        Block plus RAS size:    {self.nand_block_size_plus_ras} bytes
        RAS size:               {self.nand_ras} bytes
        Plane size:             {self.nand_plane_size}
        Pages per block:        {self.nand_pages_per_block}
        Number of blocks:       {self.nand_block_count}
        Number of pages:        {self.nand_page_count}
        Number of planes:       {self.nand_plane_count}
        Bus width:              {self.nand_bus_width}-bit
        """)

    def bootloader(self):
        self.write(self.CMD_BOOTLOADER)
        self.flush()

    def read_result(self):
        # read status byte
        res = self.readbyte()

        # 'K' = okay, 'T' = timeout error when writing, 'R' = Teensy receive buffer timeout, 'V' = Verification error
        error_msg = ""

        if (res != 75):  # 'K'
            if (res == 84):  # 'T'
                error_msg = "RY/BY timeout error while writing!"
            elif (res == 82):  # 'R'
                self.close()
                raise NANDError(
                    "Teensy receive buffer timeout! Disconnect and reconnect Teensy!")
            elif (res == 86):  # 'V'
                error_msg = "Verification error!"
            elif (res == 80):  # 'P'
                error_msg = "Device is write-protected!"
            else:
                self.close()
                raise NANDError("Received unknown error! (Got 0x%02x)" % res)

            print(error_msg)
            return 0

        return 1

    def erase_block(self, page: int):
        "Erase a NAND block."
        if self.nand_id == 1:
            self.write(self.CMD_NAND1_ERASEBLOCK)
        else:
            self.write(self.CMD_NAND0_ERASEBLOCK)

        page_block = page / self.nand_pages_per_block

        # row (page number) address
        self.write(page & 0xFF)
        self.write((page >> 8) & 0xFF)
        self.write((page >> 16) & 0xFF)

        if self.read_result() == 0:
            print(f"Block {page_block} - error erasing block")
            return 0

        return 1

    def readpage(self, page: int):
        "Read data from a NAND page."
        if (self.nand_id == 1):
            self.write(self.CMD_NAND1_READPAGE)
        else:
            self.write(self.CMD_NAND0_READPAGE)

        # address
        # self.write(0x0)
        # self.write(0x0)
        self.write(page & 0xFF)
        self.write((page >> 8) & 0xFF)
        self.write((page >> 16) & 0xFF)

        read_error_code = self.read_result()
        if read_error_code == 0:
            raise NANDError(f"Error while reading page {page}")
        else:
            data = self.read(self.nand_page_size_plus_ras)
            return data

    def writepage(self, page_data: bytes, page_number: int):
        "Write data to a NAND page."
        if len(page_data) != self.nand_page_size_plus_ras:
            print(f"Incorrent data size {len(page_data)}")
            return -1

        if (self.nand_id == 1):
            self.write(self.CMD_NAND1_WRITEPAGE)
        else:
            self.write(self.CMD_NAND0_WRITEPAGE)

        # address
        # self.write(0x0)
        # self.write(0x0)
        self.write(page_number & 0xFF)
        self.write((page_number >> 8) & 0xFF)
        self.write((page_number >> 16) & 0xFF)

        self.write(page_data)

        if self.read_result() == 0:
            return 0

        return 1

    def dump(self, filename: str, block_offset: int, nblocks: int):
        "Dump data from the NAND to a file."

        if nblocks == 0:
            nblocks = self.nand_block_count

        if nblocks > self.nand_block_count:
            nblocks = self.nand_block_count

        with open(filename, "wb") as dumpfile:
            for page in range(block_offset*self.nand_pages_per_block, (block_offset+nblocks)*self.nand_pages_per_block, 1):
                data = self.readpage(page)
                dumpfile.write(data)
                # print "\r%d KB / %d KB"%((page-(block_offset*self.NAND_PAGES_PER_BLOCK)+1)*self.NAND_PAGE_SZ_PLUS_RAS/1024, nblocks*self.NAND_BLOCK_SZ_PLUS_RAS/1024),
                dump_size_progress = (
                    page-(block_offset*self.nand_pages_per_block)+1)*self.nand_page_size_plus_ras/1024
                dump_size_total = nblocks*self.nand_block_size_plus_ras/1024
                print(f"{dump_size_progress} KB / {dump_size_total} KB", end="\r")
                sys.stdout.flush()

    def program_block(self, data: bytes, pgblock: int, verify: bool):
        pagenr = 0

        datasize = len(data)
        if datasize != self.nand_block_size_plus_ras:
            print(
                f"Incorrect length {datasize} != {self.nand_block_size_plus_ras}")
            return -1

        while pagenr < self.nand_pages_per_block:
            real_pagenr = (pgblock * self.nand_pages_per_block) + pagenr
            if pagenr == 0:
                self.erase_block(real_pagenr)

            self.writepage(data[pagenr*self.nand_page_size_plus_ras:(pagenr+1)
                           * self.nand_page_size_plus_ras], real_pagenr)

            pagenr += 1

        # verification
        if verify:
            pagenr = 0
            while pagenr < self.nand_pages_per_block:
                real_pagenr = (pgblock * self.nand_pages_per_block) + pagenr
                if data[pagenr*self.nand_page_size_plus_ras:(pagenr+1)*self.nand_page_size_plus_ras] != self.readpage(real_pagenr):
                    print()
                    # print "Error! Block verification failed. block=0x%x page=%d"%(pgblock, real_pagenr)
                    print(
                        "Error! Block verification failed.",
                        f"block=0x{pgblock:x} page=0x{real_pagenr:x}"
                        )
                    return -1

                pagenr += 1

        return 0

    def program(self, data: bytes, verify: bool, block_offset: int, nblocks: int):
        "Program a NAND chip."
        datasize = len(data)

        if nblocks == 0:
            nblocks = self.nand_block_count - block_offset

        # validate that the data is a multiplication of self.NAND_BLOCK_SZ_PLUS_RAS
        if datasize % self.nand_block_size_plus_ras:
            # print "Error: expecting file size to be a multiplication of block+ras size: %d"%(self.NAND_BLOCK_SZ_PLUS_RAS)
            print(
                f"Error: expecting file size to be a multiplication of block+ras size: {self.nand_block_size_plus_ras}")
            return -1

        # validate that the the user didn't want to read from incorrect place in the file
        if block_offset + nblocks > datasize/self.nand_block_size_plus_ras:
            # print "Error: file is %x bytes long and last block is at %x"%(datasize, (block_offset + nblocks + 1) * self.NAND_BLOCK_SZ_PLUS_RAS)
            print(
                f"Error: file is {datasize:x}  bytes long and last block is at {(block_offset + nblocks + 1) * self.nand_block_size_plus_ras}")
            return -1

        # validate that the the user didn't want to write to incorrect place in the NAND
        if block_offset + nblocks > self.nand_block_count:
            # print "Error: nand has %x blocks. writing outside the nand's capacity"%(self.NAND_NBLOCKS, block_offset + nblocks + 1)
            print(
                f"Error: nand has {self.nand_block_count:x}, writing outside the nand's capacity")
            return -1

        block = 0

        # print "Writing %x blocks to device (starting at offset %x)..."%(nblocks, block_offset)
        print(
            f"Writing {nblocks:x} blocks to device (starting at offset {block_offset:x})...")

        while block < nblocks:
            pgblock = block+block_offset
            self.program_block(data[pgblock*self.nand_block_size_plus_ras:(
                pgblock+1)*self.nand_block_size_plus_ras], pgblock, verify)

            write_progress = ((block+1)*self.nand_block_size_plus_ras)/1024
            write_total = (nblocks*self.nand_block_size_plus_ras)/1024
            print(f"{write_progress} KB / {write_total} KB", end="\r")
            # print "\r%d KB / %d KB"%(((block+1)*self.NAND_BLOCK_SZ_PLUS_RAS)/1024, (nblocks*self.NAND_BLOCK_SZ_PLUS_RAS)/1024),
            sys.stdout.flush()

            block += 1

        print()


def ps3_validate_block(block_data: bytes, page_plus_ras_sz: int, page_sz: int, blocknr: int):
    "Validate a block from a PS3 NAND."
    spare1 = block_data[page_sz:page_plus_ras_sz]
    spare2 = block_data[page_plus_ras_sz+page_sz:page_plus_ras_sz*2]

    if blocknr == 0x1FF:
        return 1

    if spare1[0] != 0xFF or spare2[0] != 0xFF:
        return 0

    return 1


if __name__ == "__main__":
    VERSION_MAJOR = 0
    VERSION_MINOR = 65

    # print "NANDway v%d.%02d - Teensy++ 2.0 NAND Flasher for PS3/Xbox/Wii"%(VERSION_MAJOR, VERSION_MINOR)
    print(
        f"NANDWay v{VERSION_MAJOR}.{VERSION_MINOR:02} - Teensy++ 2.0 NAND Flasher for PS3/Xbox/Wii")
    print("(Original NORway.py by judges <judges@eEcho.com>)")
    print("(Original noralizer.py by Hector Martin \"marcan\" <hector@marcansoft.com>)")
    print()

    if len(sys.argv) == 1:
        print("""
        Usage:
        NANDway.py Serial-Port 0/1 Command

          Serial-Port  Name of serial port to open (eg. COM1, COM2, /dev/ttyACM0, etc)
          0/1  NAND id number: 0-NAND0, 1-NAND1
          Commands:
          *  info
             Displays information about NAND
          *  dump Filename [Offset] [Length]
             Dumps to Filename at [Offset] and [Length]
          *  vwrite/write Filename [Offset] [Length]
             Flashes (v=verify) Filename at [Offset] and [Length]
          *  vdiffwrite/diffwrite Filename Diff-file
             Flashes (v=verify) Filename using a Diff-file
          *  ps3badblocks Filename
             Identifies bad blocks in Filename (raw dump)
          *  bootloader
             Enters Teensy's bootloader mode (for Teensy reprogramming)

             Notes: 1) All offsets and lengths are in hex (number of blocks)
                    2) The Diff-file is a file which lists all the changed
                       offsets of a dump file. This will increase flashing
                       time dramatically.

        Examples:
          NANDway.py COM1 0 info
          NANDway.py COM1 0 dump d:\\myflash.bin
          NANDway.py COM1 1 dump d:\\myflash.bin 3d a0
          NANDway.py COM1 0 write d:\\myflash.bin
          NANDway.py COM3 1 write d:\\myflash.bin 20 1c
          NANDway.py COM3 0 vwrite d:\\myflash.bin
          NANDway.py COM3 1 vwrite d:\\myflash.bin 8d 20
          NANDway.py COM4 0 diffwrite d:\\myflash.bin d:\\myflash_diff.txt
          NANDway.py COM3 1 vdiffwrite d:\\myflash.bin d:\\myflash_diff.txt
          NANDway.py COM1 0 bootloader
          NANDway.py ps3badblocks d:\\myflash.bin
        """)
        sys.exit(0)

    if (len(sys.argv) == 3) and (sys.argv[1] == "ps3badblocks"):
        tStart = time.time()

        with open(sys.argv[2], "rb") as datafile:
            data = datafile.read()

        datasize = len(data)
        page_sz = 2048
        page_plus_ras_sz = 2112
        nblocks = 1024
        pages_per_block = 64
        block = 0
        block_plus_ras_sz = page_plus_ras_sz*pages_per_block
        block_offset = 0

        tStart = time.time()

        while block < nblocks:
            pgblock = block+block_offset

            block_data = data[pgblock*(block_plus_ras_sz):(pgblock+1)*(block_plus_ras_sz)]
            block_valid = ps3_validate_block(
                block_data, page_plus_ras_sz, page_sz, block)
            if block_valid == 0:
                print()
                # print "Invalid block: %d (0x%X)"%(pgblock, pgblock)
                print(f"Invalid block: {pgblock} (0x{pgblock:X})")
                print()

            # print "\r%d KB / %d KB"%(((block+1)*(block_plus_ras_sz))/1024, (nblocks*(block_plus_ras_sz))/1024),
            bblock_progress = ((block+1)*(block_plus_ras_sz))/1024
            bblock_total = (nblocks*(block_plus_ras_sz))/1024
            print(f"{bblock_progress} KB / {bblock_total} KB", end="\r")
            sys.stdout.flush()

            block += 1

        print()
        # print "Done. [%s]"%(datetime.timedelta(seconds=time.time() - tStart))
        print(f"Done. [{datetime.timedelta(seconds=time.time() - tStart)}]")
        sys.exit(0)

    n = NANDFlasher(sys.argv[1], int(sys.argv[2], 10),
                    VERSION_MAJOR, VERSION_MINOR)
    print("Pinging Teensy...")
    freeram = n.ping()
    # print "Available memory: %d bytes"%(freeram)
    print(f"Available memory: {freeram} bytes")
    print()

    tStart = time.time()
    if len(sys.argv) in (5, 6, 7) and sys.argv[3] == "dump":
        n.printstate()
        print()
        print("Dumping...")
        sys.stdout.flush()
        print()

        block_offset = 0
        nblocks = 0

        if len(sys.argv) == 6:
            block_offset = int(sys.argv[5], 16)
        elif len(sys.argv) == 7:
            block_offset = int(sys.argv[5], 16)
            nblocks = int(sys.argv[6], 16)

        n.dump(sys.argv[4], block_offset, nblocks)

        print()
        # print "Done. [%s]"%(datetime.timedelta(seconds=time.time() - tStart))
        print(f"Done. [{datetime.timedelta(seconds=time.time() - tStart)}]")

    if len(sys.argv) == 4 and sys.argv[3] == "info":
        n.printstate()
        print()

    elif len(sys.argv) in (5, 6, 7) and (sys.argv[3] == "write" or sys.argv[3] == "vwrite"):
        n.printstate()
        print()

        print("Writing...")
        sys.stdout.flush()

        print()

        with open(sys.argv[4], "rb") as datafile:
            data = datafile.read()

        block_offset = 0
        nblocks = 0

        if (sys.argv[3] == "vwrite"):
            verify = True
        else:
            verify = False

        if len(sys.argv) == 6:
            block_offset = int(sys.argv[5], 16)
        elif len(sys.argv) == 7:
            block_offset = int(sys.argv[5], 16)
            nblocks = int(sys.argv[6], 16)

        n.program(data, verify, block_offset, nblocks)

        print()
        # print "Done. [%s]"%(datetime.timedelta(seconds=time.time() - tStart))
        print(f"Done. [{datetime.timedelta(seconds=time.time() - tStart)}]")

    elif len(sys.argv) == 6 and (sys.argv[3] == "diffwrite" or sys.argv[3] == "vdiffwrite"):
        n.printstate()
        print()
        print("Writing using diff file ...")
        sys.stdout.flush()
        print()

        with open(sys.argv[4], "rb") as datafile:
            data = datafile.read()
        with open(sys.argv[5], "rb") as difffile:
            diff_data = difffile.readlines()

        block_offset = 0
        nblocks = 0
        nlines = len(diff_data)
        cur_line = 0

        if (sys.argv[3] == "vdiffwrite"):
            verify = True
        else:
            verify = False

        for line in diff_data:
            addr = int(line[2:], 16)
            if addr % n.nand_block_size_plus_ras:
                # print "Error: incorrect address for block addr=%x. addresses must be on a per-block boundary"%(addr)
                print(
                    f"Error: incorrect address for block addr={addr:x}. addresses must be on a per-block boundary")
                sys.exit(0)

            block_offset = int(addr/n.nand_block_size_plus_ras)
            # print "Programming offset %x block %x (%d/%d)"%(addr, block_offset, cur_line+1, nlines)
            print(
                f"Programming offset {addr:x} block {block_offset:x} ({cur_line+1}/{nlines})")
            n.program(data, verify, block_offset, True)
            cur_line += 1

        print()
        # print "Done. [%s]"%(datetime.timedelta(seconds=time.time() - tStart))
        print(f"Done. [{datetime.timedelta(seconds=time.time() - tStart)}]")

    elif len(sys.argv) == 4 and sys.argv[3] == "bootloader":
        print()
        print("Entering Teensy's bootloader mode... Goodbye!")
        n.bootloader()
        sys.exit(0)

    n.ping()