//      __  ____                 _   __                
//     /  |/  (_)_____________  / | / /___ _   ______ _
//    / /|_/ / / ___/ ___/ __ \/  |/ / __ \ | / / __ `/
//   / /  / / / /__/ /  / /_/ / /|  / /_/ / |/ / /_/ / 
//  /_/  /_/_/\___/_/   \____/_/ |_/\____/|___/\__,_/  
// 
// Copyright (c) 2013. All Rights Reserved.
// MicroNova, LLC
// www.micro-nova.com
//  
// Permission to use, copy, modify and distribute this software and its documentation, 
// is granted for educational, research, and not-for-profit purposes, provided 
// that this notice is included in all copies, modifications, and distributions.
// This software is provided "as-is" without any warranties or guarantees.
//

using System;
using System.ComponentModel;
using System.Text;
using System.Timers;
using System.Threading;
using System.IO;
using System.Diagnostics;

using FTD2XX_NET; // from FTDI's website

    public class MercuryUSB : INotifyPropertyChanged
    {
        #region FTDI PIN CONSTANTS
        ///////////////////////////////////////////////////////////

        // 0 = FLASH CSN
        // 1 = FPGA CSN
        // 2 = SCLK
        // 3 = MISO
        // 4 = MOSI
        // 5 = FPGA program
        private const int miso_bit = 3;
        private const int mosi_bit = 4;
        private const byte miso_mask = 0x08;

        // select FPGA  = 0001 => 0x01
        // select Flash = 0010 => 0x02
        // select none  = 0011 => 0x03
        private const byte select_fpga = (byte)0x01;
        private const byte select_flash = (byte)0x02;
        private const byte select_none = (byte)0x03;

        // clock high = 0100 => 0x04
        private const byte clock_high = (byte)0x04;

        #endregion

        // FTDI object
        private static FTDI myFtdiDevice = new FTDI();
        private static FTDI.FT_STATUS ftStatus = FTDI.FT_STATUS.FT_OK;
        private static uint FTDI_bytes_written = 0;
        //private static uint FTDI_bytes_read = 0;
        private static string tosend;

        #region FLASH_CHIP
        private class FlashChip
        {
            public readonly byte ManufacturerID;
            public readonly byte DeviceID;
            public readonly UInt32 Size;        // In bytes
            public readonly UInt16 PageSize;    // In bytes
            public readonly UInt16 BlockSize;   // In pages
            public readonly UInt16 SectorSize;  // In pages

            public FlashChip(byte manufid, byte devid, UInt32 size, UInt16 pagesize, UInt16 blockSize, UInt16 sectorSize)
            {
                ManufacturerID = manufid;
                DeviceID = devid;
                Size = size;
                PageSize = pagesize;
                BlockSize = blockSize;
                SectorSize = sectorSize;
            }
        }
        FlashChip AT45DB021D = new FlashChip(0x1F, 0x23,  270336, 264, 8, 128);
        FlashChip AT45DB081D = new FlashChip(0x1F, 0x25, 1081344, 264, 8, 256);
        private FlashChip flashChip;
        #endregion FLASH_CHIP

        // STATES
        public event PropertyChangedEventHandler PropertyChanged;
        private static int _dataread = 0;
        private static int _progress = 0;
        private static Boolean _connected = false;
        private static int _busy = 0; // 0 = idle, 1 = heartbeat, 2 = communication
        private static StringBuilder _console = new StringBuilder();
        public System.Timers.Timer heartbeat;

        // CONSTRUCTOR

        public MercuryUSB()
        {
            // Setup heartbeat monitor every 100 ms
            heartbeat = new System.Timers.Timer(100);
            heartbeat.Elapsed += new ElapsedEventHandler(HeartbeatTick);
            heartbeat.AutoReset = true;
        }

        public void init() {

            // get version information from AssemblyInfo.cs
            string version = FileVersionInfo.GetVersionInfo(this.GetType().Assembly.Location).FileVersion;

            // hello there
            this.ConsoleWriteLine("Mercury Programmer (" + version + ") - © 2013 MicroNova LLC");
            this.ConsoleWriteLine("Welcome! Click 'Connect' to begin.");
            // initialize
            this.connected = false;
            this.busy = 0;
        }

        // STATUS METHODS

        public Boolean connected {
            get { return _connected; }
            set { _connected = value;
            SendPropertyChanged("connected");
            }
        }

        public int busy
        {
            get { return _busy; }
            set
            {
                _busy = value;
                SendPropertyChanged("busy");
            }
        }

        public string console
        {
            get { return _console.ToString(); }
        }

        public int progress {
            get { return _progress; }
            set
            {
                _progress = value;
                SendPropertyChanged("progress");
            }
        }

        public int dataread
        {
            get { return _dataread; }
            set
            {
                _dataread = value;
                SendPropertyChanged("dataread");
            }
        }

        public void ConsoleWriteLine(string towrite)
        {
            _console.Append(towrite + System.Environment.NewLine);
            SendPropertyChanged("console");
        }

        public void ConsoleWrite(string towrite)
        {
            _console.Append(towrite);
            SendPropertyChanged("console");
        }
        
        private void SendPropertyChanged(string name)
        {
            // Create the OnPropertyChanged method to raise the event
            if (this.PropertyChanged != null) { 
                this.PropertyChanged(this, new PropertyChangedEventArgs(name));
            }
        }

        private void HeartbeatTick(object sender, ElapsedEventArgs eArgs)
        {
            if (busy == 0)
            {
                busy = 1;
                uint id = 0;
                ftStatus = myFtdiDevice.GetDeviceID(ref id);

                if (ftStatus != FTDI.FT_STATUS.FT_OK)
                {
                    ConsoleWriteLine("Connection lost.");
                    connected = false;
                    progress = 0;
                    heartbeat.Stop();
                }
                busy = 0;
            }

        }

        #region PUBLIC_USB_METHODS
        public void SetPins(byte inByte)
        {
            Thread t = new Thread(() =>
            {
                while (busy != 0) ; // wait until we're not busy
                busy = 2; // we're busy
                myFtdiDevice.SetBitMode(0xFF, 0x01);
                FTDI_write(byte2string(inByte));
                busy = 0; // we're done
            });
            t.IsBackground = true;
            t.Start();
        }

        public void Connect(bool ConnectByIndex, uint Index)
        {
            // threading notes: http://www.albahari.com/threading/

            Thread t = new Thread(() =>
            {
                if (connected == true)
                    ConsoleWriteLine("Already Connected!");
                else
                {
                    busy = 2;
                    connected = this._Connect(ConnectByIndex, Index);
                    if (connected) heartbeat.Start();
                    busy = 0;
                }

            });
            t.IsBackground = true;
            t.Start();
        }

        public void Disconnect()
        {

            Thread t = new Thread(() =>
            {
                if (connected == false)
                    ConsoleWriteLine("Already Disconnected!");
                else
                {
                    busy = 2;
                    connected = this._Disconnect();
                    if (connected == false) heartbeat.Stop();
                    busy = 0;
                }

            });
            t.IsBackground = true;
            t.Start();
        }      

        public void FlashIdentify()
        {
            Thread t = new Thread(() => {
                while (busy != 0) ; // wait until we're not busy
                busy = 2; // we're busy
                this._FlashIdentify();
                busy = 0; // we're done
            });
            t.IsBackground = true;
            t.Start();
        }

        public void FlashChipErase() {
            Stopwatch stopwatch = new Stopwatch();

            Thread t = new Thread(() =>
            {
                while (busy != 0) ; // wait until we're not busy
                busy = 2; // we're busy
                AssertProgPin(true); // assert PROG_B
                if (_FlashIdentify()){
                    stopwatch.Start();
                    if (this._FlashEraseChip())
                    {
                        ConsoleWriteLine("Flash erase successful.");
                        progress = 1000;
                    }
                    else
                    {
                        ConsoleWriteLine("Flash erase failed.");
                        progress = 0;
                    }
                    stopwatch.Stop();
                    ConsoleWriteLine("Time elapsed: " + stopwatch.Elapsed);
                }
                AssertProgPin(false); // deassert PROG_B
                busy = 0; // we're done
            });
            t.IsBackground = true;
            t.Start();
        }

        public void FlashWriteBitstream(string path) 
        {
            Stopwatch stopwatch = new Stopwatch();

            Thread t = new Thread(() =>
            {
                while (busy != 0) ;   // wait until we're not busy
                busy = 2;             // we're busy
                AssertProgPin(true);  // assert PROG_B
                if (_FlashIdentify()) // check that we can talk to the flash chip
                {
                    stopwatch.Start();
                    if (this._FlashWriteBitstream(path))
                    {
                        ConsoleWriteLine("Flash write complete.");
                        progress = 1000;
                    }
                    else
                    {
                        ConsoleWriteLine("Flash write failed.");
                        progress = 0;
                    }
                    stopwatch.Stop();
                ConsoleWriteLine("Time elapsed: " + stopwatch.Elapsed);
                }
                AssertProgPin(false); // deassert PROG_B
                busy = 0; // we're done
            });
            t.IsBackground = true;
            t.Start();        
        }

        public void RegisterWrite(byte address, int datawrite) {
            Thread t = new Thread(() =>
            {
                while (busy != 0) ; // wait until we're not busy
               busy = 2;           // we're busy
               _RegisterWrite(address, datawrite);
               busy = 0;           // we're done

                // write to console
               ConsoleWriteLine(" Wrote 0x" + (datawrite.ToString("X").PadLeft(8, '0')) + " => FPGA address 0x" + address.ToString());
            });
                t.IsBackground = true;
                t.Start();
        }

        public void RegisterRead(byte address) {
            Thread t = new Thread(() =>
            {
                while (busy != 0) ; // wait until we're not busy
                busy = 2;           // we're busy
                dataread = _RegisterRead(address);
                busy = 0; // we're done

                // write to console
                ConsoleWriteLine("  Read 0x" + (dataread.ToString("X").PadLeft(8, '0')) + " <= FPGA address 0x" + address.ToString());
            });
            t.IsBackground = true;
            t.Start();
        }

        public void TestLoopback() { 
        // This tests the functionality of the I/O pins
        // Requires a loopback cable connect pins 0-14 to pins 15-29

            Thread t = new Thread(() =>
            {
                while (busy != 0) ; // wait until we're not busy
                busy = 2;           // we're busy

                _testloopback();

                busy = 0; // we're done
            });
            t.IsBackground = true;
            t.Start();             
        }
        #endregion PUBLIC_USB_METHODS

        #region PRIVATE_USB_METHODS
        private bool _Connect(bool ConnectByIndex, uint Index) {

            string board_serial = "";

            // Returns connection status

            // List devices for debugging.
            // ListDevices();

            // Connect
            if (ConnectByIndex)
                ftStatus = myFtdiDevice.OpenByIndex(Index);
            else
                ftStatus = myFtdiDevice.OpenByDescription("Mercury FPGA");

            if (ftStatus != FTDI.FT_STATUS.FT_OK)
            {
                ConsoleWriteLine("Unable to connect. Make sure the board is connected via USB.");
                return false;
            }

            // Set baud rate to 3MBaud
            ftStatus = myFtdiDevice.SetBaudRate(3000000);
            if (ftStatus != FTDI.FT_STATUS.FT_OK)
            {
                ConsoleWriteLine("Error setting baud rate.");
                return false;
            }

            // Set pin settings
            if (!AssertProgPin(false))
            {
                ConsoleWriteLine("Error initializing bit mode.");
                return false;
            }

            // Read Serial Number
            ftStatus = myFtdiDevice.GetSerialNumber(out board_serial); 
            if (ftStatus != FTDI.FT_STATUS.FT_OK)
            {
                ConsoleWriteLine("Error reading serial number.");
                return false;
            }

            // Initialize pins => select_none
            if (SPI_Deselect())
            {
                ConsoleWriteLine("Connected to board; serial number is "+ board_serial + ".");
                return true;
            }
            else
            {
                ConsoleWriteLine("Error initializing pins.");
                return false;
            }

        }

        private bool _Disconnect(){

            // Returns connection status
            ftStatus = myFtdiDevice.Close();
            if (ftStatus == FTDI.FT_STATUS.FT_OK)
            {
                ConsoleWriteLine("Disconnected.");
                return false;
            }
            return true;        
        
        }

        // return true if proper flash ID
        private bool _FlashIdentify()
        {
            // Send FLASH identification opcode
            SPI_OutByte((byte)(0x9F), select_flash);

            // SHIFT-IN 4 BYTES
            byte[] readwords = new byte[4];
            for (int i = 0; i < 4; i++)
            {
                readwords[i] = SPI_InByte(select_flash);
            }

            // Pull CS high
            SPI_Deselect();

            byte manufid = readwords[0];
            byte devid = readwords[1];

            if (manufid == AT45DB021D.ManufacturerID && devid == AT45DB021D.DeviceID)
            {
                flashChip = AT45DB021D;
                ConsoleWriteLine("2Mbit Flash chip detected (ME1A)");
                return true;
            }
            if (manufid == AT45DB081D.ManufacturerID && devid == AT45DB081D.DeviceID)
            {
                flashChip = AT45DB081D;
                ConsoleWriteLine("8Mbit Flash chip detected (ME1B)");
                return true;
            }
            ConsoleWrite("Flash ID wrong");
            ConsoleWriteLine(" (ManufID=" + manufid + ", DevID=" + devid + ")");
            return false;
        }

        private bool _FlashEraseChip()
        {
            byte[] cmd_erase = { 0xC7, 0x94, 0x80, 0x9A };
            Boolean success = false;

            ConsoleWriteLine("Erasing Flash chip. This may take a moment. Please wait...");

            SPI_OutBytes(cmd_erase, select_flash);  // send erase command
            SPI_Deselect();                         // deselect chip
            success = _FlashPollDone(true);

            return success;
        }

        private bool _FlashEraseSectors(UInt16 numSectors)
        {
            byte[] cmd_erase_0a = { 0x7C, 0x00, 0x00, 0x00 }; // the first 8 pages
            byte[] cmd_erase_0b = { 0x7C, 0x00, 0x10, 0x00 }; // the next 120 pages
            byte[] cmd_erase_x = { 0x7C, 0x00, 0x00, 0x00 };  // subsequent sectors are 128 pages each

            if (numSectors < 1) // no sectors to erase
                return true;

            ConsoleWriteLine("Erasing " + numSectors + " sectors...");
            progress = 0;

            SPI_OutBytes(cmd_erase_0a, select_flash);   // erase sector 0a
            SPI_Deselect();                             // deselect chip
            if (!_FlashPollDone(false))
            {
                ConsoleWriteLine("Error erasing flash sector 0a");
                return false;
            }

            SPI_OutBytes(cmd_erase_0b, select_flash);   // erase sector 0b
            SPI_Deselect();                             // deselect chip
            if (!_FlashPollDone(false))
            {
                ConsoleWriteLine("Error erasing flash sector 0b");
                return false;
            }
            progress = 1000 / numSectors;

            for (byte x = 1; x < numSectors; x++)
            {
                cmd_erase_x[1] = x;
                if (flashChip.DeviceID == AT45DB081D.DeviceID)
                {
                    cmd_erase_x[1] = (byte)(x << 1);
                }
                SPI_OutBytes(cmd_erase_x, select_flash);    // erase sector x
                SPI_Deselect();                             // deselect chip
                if (!_FlashPollDone(false))
                {
                    ConsoleWriteLine("Error erasing flash sector " + x);
                    return false;
                }
                progress = (x + 1) * 1000 / numSectors;
            }
            return true;
        }

        private bool _FlashWriteBitstream(string path)
        {
            byte[] buffer;

            // read in file
            buffer = ReadFile(path);

            if (buffer.Length > flashChip.Size)
            {
                ConsoleWriteLine("Bitfile too big! (> " + flashChip.Size + " Bytes)");
                return false;
            }

            UInt16 pages = (UInt16)Math.Ceiling(buffer.Length / (double)flashChip.PageSize);
            UInt16 sectors = (UInt16)Math.Ceiling(pages / (double)flashChip.SectorSize);
            ConsoleWriteLine("Read " + buffer.Length + " bytes -> " + pages + " pages (" + sectors + " sectors)");
            
            if (!_FlashEraseSectors(sectors))
                return false;

            // counters
            int buffer_index = 0;
            int flash_address = 0;
            int remaining = buffer.Length;

            ConsoleWriteLine("Writing new data... ");

            // sector to write
            byte[] page = new byte[264];

            while (remaining > 0)
            {
                // while data remains in buffer
                for (int j = 0; j < 264; j++)
                {
                    // page <- buffer
                    if (buffer_index < buffer.Length)
                        page[j] = buffer[buffer_index]; // copy data to page
                    else
                        page[j] = (byte)0x00;           // zero-fill page

                    buffer_index++; // increment buffer index
                }

                // write page
                if (!_FlashWritePage(page, flash_address))
                    return false; // in case of time-out

                // data remaining counter
                remaining = remaining - 264;

                // increment address
                flash_address++;

                // update progress bar
                progress = (int)(1000 * ((double)flash_address / (double)pages));
            }

            ConsoleWriteLine("Successfully wrote " + flash_address + " flash pages.");
            return true;
        }
        
        private bool _FlashWritePage(byte[] pagedata, int address)
        {
            // Write a Flash page

            address = address & (int)0x3FF; // 10-bits of address
            byte address_upper = (byte)((address >> 7) & 0x07); // 5 don't care + 3 address (00000 & AAA)
            byte address_lower = (byte)((address << 1) & 0xFE); // 7 address + 1 don't care (AAAAAAA & 0)

            byte[] cmd_bufferwrite = new byte[4];
            cmd_bufferwrite[0] = 0x84; // buffer write opcode
            cmd_bufferwrite[1] = 0x00; // buffer byte start addr 1
            cmd_bufferwrite[2] = 0x00; // buffer byte start addr 2
            cmd_bufferwrite[3] = 0x00; // buffer byte start addr 3

            byte[] cmd_buf2mem = new byte[4];
            cmd_buf2mem[0] = 0x88; // buffer-to-flash (without erase) opcode
            cmd_buf2mem[1] = address_upper; // flash addr 1
            cmd_buf2mem[2] = address_lower; // flash addr 2
            cmd_buf2mem[3] = 0x00;          // flash addr 3

            // FILL BUFFER, then WRITE BUFFER -> FLASH
            tosend = BangBytes(cmd_bufferwrite, select_flash) +    // command: write buffer
                     BangBytes(pagedata, select_flash) +         // write data
                     byte2string(select_none) +                    // deselect
                     BangBytes(cmd_buf2mem, select_flash) +        // command: copy buffer to flash
                     byte2string(select_none);                     // deselect

            // SEND STRING
            if (!FTDI_write(tosend))
                return false;

            // poll status register for completion of page
            return _FlashPollDone(false);

        }

        private bool _FlashPollDone(bool prog)
        { 
        // Poll until the status register is complete
        // returns true on completion, returns false on timeout

            int attempts = 0;

            bool success = false;

             if (prog){ 
                 progress = 1;
             }

            while ((!success) && attempts != 300000)
            {
                attempts++;                                   // timeout counter

                SPI_OutByte(0xD7, select_flash);  // status opcode
                success = SPI_InBit();            // quickly poll status

                SPI_Deselect();                   // deselect chip
            }

            if (!success)
            {
                ConsoleWriteLine("Flash status polling timeout. ");
                ConsoleWriteLine("Failed after " + attempts + " polls.");
            }

            return success;
        }

        private void _RegisterWrite(byte address, int datawrite) 
        {
            byte[] outbytes = new byte[4];
            
            SPI_OutByte(address, select_fpga);  // send address
            SPI_OutByte(0x02, select_fpga);     // send write command

            // send MSB first
            outbytes[0] = (byte)((0xFF000000 & datawrite) >> 24);
            outbytes[1] = (byte)((0x00FF0000 & datawrite) >> 16);
            outbytes[2] = (byte)((0x0000FF00 & datawrite) >> 8);
            outbytes[3] = (byte)(0x000000FF & datawrite);

            SPI_OutBytes(outbytes, select_fpga); // send four data bytes

            SPI_Deselect(); // Release select line           

        }

        private int _RegisterRead(byte address)
        {
            byte[] inbytes = new byte[4];
            int dataread = 0;
            
            SPI_OutByte(address, select_fpga);  // send address
            SPI_OutByte(0x01, select_fpga);     // send read command

            for (int i = 0; i < 4; i++)
                inbytes[i] = (byte)SPI_InByte(select_fpga); // shift 4 bytes in

            SPI_Deselect(); // Release select line

            dataread = (int)
                inbytes[3] |
                inbytes[2] << 8 |
                inbytes[1] << 16 |
                inbytes[0] << 24 ;

            return dataread;
        }
        #endregion PRIVATE_USB_METHODS

        #region PRIVATE_FILE_METHODS
        private byte[] ReadFile(string filePath)
        {
            // derived from: http://www.csharp-examples.net/filestream-read-file/

            byte[] buffer;

            try
            {
                FileStream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read);

                try
                {
                    int length = (int)fileStream.Length;  // get file length
                    buffer = new byte[length];            // create buffer
                    int count;                            // actual number of bytes read
                    int sum = 0;                          // total number of bytes read

                    // read until Read method returns 0 (end of the stream has been reached)
                    while ((count = fileStream.Read(buffer, sum, length - sum)) > 0)
                        sum += count;  // sum is a buffer offset for next reading
                }
                finally
                {
                    fileStream.Close();
                }
            }
            catch {
                // catch exceptions thrown by creating fileStream
                ConsoleWriteLine("File Error");
                return null;
            }

            return buffer;
            
        }
        #endregion PRIVATE_FILE_METHODS

        #region BASIC_COMMUNICATIONS_METHODS
        private bool AssertProgPin(bool assert)
        {
            const byte pins_assert = 0x37;   // [out: 0011 0111] => 0x37 
            const byte pins_deassert = 0x17; // [out: 0001 0111] => 0x17
            const byte mode = 0x01;          // asynchronous bit bang mode

            if (assert)
                // set FPGA_PROG as output (driven low)
                ftStatus = myFtdiDevice.SetBitMode(pins_assert, mode);
            else
                // set FPGA_PROG as high-impedance input (will pull-up high)
                ftStatus = myFtdiDevice.SetBitMode(pins_deassert, mode);

            if (ftStatus != FTDI.FT_STATUS.FT_OK)
            {
                ConsoleWriteLine("Error changing bit mode.");
                return false;
            }

            return true;
        }

        private bool SPI_OutBytes(byte[] data, byte device_select)
        {
            // Sends out bytes via SPI, returns true on success
            // Device: [0 = Flash, 1 = FPGA]
           return FTDI_write(BangBytes(data, device_select));
        }

        private bool SPI_OutByte(byte data, byte device_select)
        {
            // Sends out a single byte via SPI, returns true on success
            // Device: [0 = Flash, 1 = FPGA]
           return FTDI_write(BangByte(data, device_select));
        }

        private byte SPI_InByte(byte device_select)
        {
            // Shift in a byte from device, on FALLING clock edge

            byte rx_byte; // byte to return
            byte[] readarray = new byte[8];    // array of pin states
            char[] clock_pulse = new char[2];  // clock pulse to write

            // First, shift in data

            for (int i = 7; i >= 0; i--)
            {
                // 1. read pin states
                ftStatus = myFtdiDevice.GetPinStates(ref readarray[i]);

                // 2. pulse clock HIGH -> LOW
                clock_pulse[0] = (char)(device_select | clock_high);
                clock_pulse[1] = (char)(device_select);
                tosend = new string(clock_pulse);
                FTDI_write(tosend);
            }

            // Then, convert to byte

            // Clear RX byte
            rx_byte = 0x00;

            for (int i = 7; i >= 0; i--)
            {
                // mask out DIN bit
                readarray[i] = (byte)(readarray[i] & miso_mask);
                // shift DIN bit to LSB
                readarray[i] = (byte)(readarray[i] >> miso_bit);
                // shift DIN bit to appropriate location
                readarray[i] = (byte)(readarray[i] << i);
                // Set bit in RX byte
                rx_byte = (byte)(rx_byte | readarray[i]);
            }

            return rx_byte;
        }

        private bool SPI_InBit()
        {
            // read only the MSB data bit from an SPI device, after writing command
            // useful for quickly reading the Flash status register (MSB = ready)

            byte readpins = 0;    // pin states

            // read pin states
            ftStatus = myFtdiDevice.GetPinStates(ref readpins);

       //     if (ftStatus != FTDI.FT_STATUS.FT_OK)
       //         ConsoleWriteLine("Error reading pin states!");            

            //mask out DIN bit
            return (readpins & miso_mask) == miso_mask;
        }

        private bool SPI_Deselect()
        {
            // deselects the device
            return FTDI_write(byte2string(select_none));
        }

        private bool FTDI_write(string send)
        {
            // Return true on successful FTDI write
            ftStatus = myFtdiDevice.Write(send, send.Length, ref FTDI_bytes_written);

            // Determine if this worked, set debug string
            if (ftStatus != FTDI.FT_STATUS.FT_OK)
            {
                return false;
            }
            else
            {
                return true;
            }
        }
        #endregion BASIC_COMMUNICATIONS_METHODS

        #region SPI_BIT_BANGING_METHODS
        private string BangByte(byte inByte, byte device_select)
        {
            // Prepares a single byte for transmission.
            // Produces a "piano roll" string of bit-banged SPI for the FTDI chip

            // Example:
            //       DOUT  SCLK  CSN
            //  0    D(7)   0    0
            //  1    D(7)   1    0
            //  2    D(6)   0    0  
            //  3    D(6)   1    0 
            //  4    D(5)   0    0
            //  5    D(5)   1    0
            //  6    D(4)   0    0
            //  7    D(4)   1    0
            //  8    D(3)   0    0
            //  9    D(3)   1    0
            // 10    D(2)   0    0
            // 11    D(2)   1    0
            // 12    D(1)   0    0
            // 13    D(1)   1    0
            // 14    D(0)   0    0
            // 15    D(0)   1    0
            // 16    0      0    0

            char[] send = new char[17]; // the "piano roll"
            byte sendtemp = 0x00; // temp byte

            // shift starting with MSB first
            for (int i = 0; i < 8; i++)
            {
                // set data and select
                sendtemp = (byte)(DataOutBit(inByte, 7-i) | device_select);
                // even: clock is low
                send[i * 2] = (char)(sendtemp);
                //DebugChar(send[i * 2]);
                // odd: clock is high
                send[(i * 2) + 1] = (char)(sendtemp | clock_high);
                //DebugChar((send[(i * 2)+1]));
            }

            send[16] = (char)device_select;

            return new string(send);
        }

        private string BangBytes(byte[] inBytes, byte select)
        {
            // Prepares multiple bytes for transmission

            string BangedBytes = "";
            for (int i = 0; i <= (inBytes.Length - 1); i++)
            {
                // step through inBytes
                BangedBytes = BangedBytes + BangByte(inBytes[i], select);
            }

            return BangedBytes;
        }

        private string byte2string(byte inByte)
        {
            // because byte.ToString() will not produce a proper transmission string
            char[] outChar = new char[1];
            outChar[0] = (char)(inByte);
            return new string(outChar);
        }

        private byte DataOutBit(byte inByte, int n)
        {
            // Shift the selected data bit to the MOSI bit

            // Example: 
            //  IN : 0b000000D0, 1
            // OUT : 0b000D0000 

            inByte = (byte)(inByte >> n);        // shift data bit to LSB
            inByte = (byte)(inByte & 0x01);      // mask data bit
            inByte = (byte)(inByte << mosi_bit); // shift to MOSI location
            return inByte;
        }
        #endregion SPI_BIT_BANGING_METHODS

        #region DEBUGGING_METHODS

        public void ListDevices()
        {

            ConsoleWriteLine("Scanning for FTDI devices...");

            UInt32 ftdiDeviceCount = 0;
            ftStatus = myFtdiDevice.GetNumberOfDevices(ref ftdiDeviceCount);

            if (ftStatus == FTDI.FT_STATUS.FT_OK)
            {
                ConsoleWriteLine("Number of FTDI devices: " + ftdiDeviceCount.ToString());
                ConsoleWriteLine("");
            }
            else
            {
                // Wait for a key press
                ConsoleWriteLine("Failed to get number of devices (error " + ftStatus.ToString() + ")");
                // Console.ReadKey();
                return;
            }

            // Allocate storage for device info list
            FTDI.FT_DEVICE_INFO_NODE[] ftdiDeviceList = new FTDI.FT_DEVICE_INFO_NODE[ftdiDeviceCount];

            // Populate our device list
            ftStatus = myFtdiDevice.GetDeviceList(ftdiDeviceList);

            if (ftStatus == FTDI.FT_STATUS.FT_OK)
            {
                for (UInt32 i = 0; i < ftdiDeviceCount; i++)
                {
                    ConsoleWriteLine("Device Index: " + i.ToString());
                    ConsoleWriteLine("Flags: " + String.Format("{0:x}", ftdiDeviceList[i].Flags));
                    ConsoleWriteLine("Type: " + ftdiDeviceList[i].Type.ToString());
                    ConsoleWriteLine("ID: " + String.Format("{0:x}", ftdiDeviceList[i].ID));
                    ConsoleWriteLine("Location ID: " + String.Format("{0:x}", ftdiDeviceList[i].LocId));
                    ConsoleWriteLine("Serial Number: " + ftdiDeviceList[i].SerialNumber.ToString());
                    ConsoleWriteLine("Description: " + ftdiDeviceList[i].Description.ToString());
                    ConsoleWriteLine("");
                }
            }
        }

        private void DebugByte(byte inByte)
        {
            byte[] debug = new byte[1];
            debug[0] = inByte;
            ConsoleWriteLine(BitConverter.ToString(debug));
        }

        private void DebugChar(char inChar)
        {
            byte[] debug = new byte[1];
            debug[0] = (byte)inChar;
            ConsoleWriteLine(BitConverter.ToString(debug));
        }

        public string HexToBinary(byte inHex)
        {
            // Display a byte as binary
            byte[] inHexArray = new byte[1];
            inHexArray[0] = inHex;
            return Convert.ToString(Convert.ToInt32(BitConverter.ToString(inHexArray), 16), 2);
        }

        private bool _testloopback()
        {
            // TEST FUNCTIONALITY OF THE BOARD
            // Returns:
            // True if test succeeds
            // False if test does not succeed

            // BEGIN TEST

            // verify test string
            if (_RegisterRead(0) == Convert.ToInt32("AFFE0001", 16))
                ConsoleWriteLine("PASS - Mercury test binary is loaded.");
            else
                return false;

            // TEST PATTERNS 
            _testloopback_pins(Convert.ToInt32("000000000000000", 2));
            _testloopback_pins(Convert.ToInt32("111111111111111", 2));
            _testloopback_pins(Convert.ToInt32("010101010101010", 2));
            _testloopback_pins(Convert.ToInt32("101010101010101", 2));

            // test succeeded
            return true;    
        }

        private bool _testloopback_pins(int pattern){

            // hold data that is read back
            int read = 0;

            // use lower 15 bits of pattern
            pattern = pattern & 0x00007FFF;

            // output pattern
            _RegisterWrite(4, pattern);
            ConsoleWriteLine("************************");
            ConsoleWriteLine(Convert.ToString(pattern, 2).PadLeft(15,'0') + " TEST PATTERN ");

            // check pins 14:0
            _RegisterWrite(1, 0x00000001);      // set MODE 1: pins 14:0 <= pins 15:29
            System.Threading.Thread.Sleep(200); // wait 100ms

            read = _RegisterRead(2); 
            if (read != pattern)
                ConsoleWriteLine(Convert.ToString(read, 2).PadLeft(15, '0') + " (pins 14:0  <= pins 29:15) FAIL");
            else
                ConsoleWriteLine(Convert.ToString(read, 2).PadLeft(15, '0') + " (pins 14:0  <= pins 29:15) PASS");

            // check pins 29:15
            _RegisterWrite(1, 0x00000002);      // set MODE 2: pins 29:15 <= pins 14:0
            System.Threading.Thread.Sleep(200); // wait 100ms

            read = _RegisterRead(3); 
            if (read != pattern)
                ConsoleWriteLine(Convert.ToString(read, 2).PadLeft(15, '0') + " (pins 29:15 <= pins 14:0)  FAIL");
            else
               ConsoleWriteLine(Convert.ToString(read, 2).PadLeft(15, '0') + " (pins 29:15 <= pins 14:0)  PASS");

            return true;  
        }

        // EEPROM METHODS

       public bool WriteEEPROM(int serial)
        {
           // new default EEPROM structure
           FTDI.FT232R_EEPROM_STRUCTURE MyEEPROM = new FTDI.FT232R_EEPROM_STRUCTURE();

           // changes for Mercury FPGA board
           MyEEPROM.Manufacturer = "MicroNova";
           MyEEPROM.ManufacturerID = "";
           MyEEPROM.Description = "Mercury FPGA";
           MyEEPROM.SerialNumber = serial.ToString();
           MyEEPROM.MaxPower = 500;         // 500mA (maximum allowable)
           MyEEPROM.SelfPowered = false;    // not self powered!
           MyEEPROM.RIsD2XX = true;         // load the D2XX instead of VCP driver

           // try to write to EEPROM
           ftStatus = myFtdiDevice.WriteFT232REEPROM(MyEEPROM);
           if (ftStatus != FTDI.FT_STATUS.FT_OK)
           {
               ConsoleWriteLine("EEPROM write error! :(");
               return false;
           }
           else
           {               
               ConsoleWriteLine("EEPROM written successfully to board " + serial.ToString() + " :)");
               return true;
           }

        }

       #endregion DEBUGGING_METHODS

    }
