You have reached the legacy GHI Electronics, LLC website, for the new website please visit here. For the new forum please visit here.

This legacy website will be taken offline at the end of this year. If there is anything that you would like to archive and save for future reference please do so.

XMODEM for PC Full .NET Framework by Iggmoe

Nov. 14, 2013   |   Snippet   |   Licensed as Apache 2.0   |   5130 views

Note: This version of XMODEM is designed to be run on a PC. The .NET Micro Framework version can also be found on Codeshare.

This is a fairly complete implementation of the XModem protocol that supports XModem-Checksum, XModem-CRC, and XModem-1K. Send and Receive have been tested on HyperTerminal and TeraTerm, and the code has been written to be compatible with as many terminal emulator programs as possible. Various public properties have been exposed to allow the user to specify custom timeouts, retry limits, packet sizes, etc. Extensive comments and usage examples throughout.

This was written to adhere as closely as possible to Chuck Forsberg's documentation, but also incorporates some later de-facto additions like file transfer cancellation and other common features.

Primary references:
http://www.textfiles.com/programming/ymodem.txt
http://wiki.synchro.net/ref:xmodem

Comments or questions?   Discuss on the forum.



Author Version Date
Iggmoe 4 06/05 '18 at 03:34pm
Iggmoe 3 02/20 '17 at 10:33pm
Iggmoe 2 11/15 '13 at 03:06pm
Iggmoe 1 11/14 '13 at 07:17pm
Version 4 — Source
using System;
using System.Threading;
using System.IO;
using System.IO.Ports;

// SUMMARY:
// Desktop version of XMODEM based on the full .NET Framework.
//
// Author:
// Adrian Abordo
// adrian.abordo@iggmoe.com
//
// FEATURES:
// Sender and Receive cancel a file transfer upon receipt of a single <CAN> byte, for compatibility with terminals that only send 1 <CAN>
// byte per cancellation, like TeraTerm.
//
// When Sender or Receiver wishes to cancel a file transfer, they send multiple <CAN> bytes, for compatibility with those terminals that
// require consecutive <CAN> bytes in order to cancel.
//
// An XModem-CRC or XModem-1K Receiver automatically reverts to the older XModem-Checksum if the Sender does not support the newer formats,
// as outlined in Chuck Forsberg's official documentation.
//
// An XModem-1K Receiver can accept data packets that are a mixture of 128-bytes and/or 1024-bytes long. Chuck Forsberg's official documentation
// allows XModem-1K Senders to transmit data packets of either length, so for maximum compatibility with various implementations, this
// XModem-1K Receiver is flexible as well.
//
// However, to accomodate XModem-1K Receiver implementations that are not capable of receiving packets with different lengths, this XModem-1K
// Sender always sends 1024 data bytes per packet.
//
// Receiver accepts both <EOT> and <EOF> as file termination bytes. The official documentation specifies <EOT> to indicate end of file,
// but some MS-DOS and Microsoft implementations of XModem send <EOF> instead. Both values are accepted for maximum compatibility.
//
// This Sender uses <EOT> to indicate end of file. However, a custom byte value can be specified via EndOfFileByteToSend.
//
// This Sender automatically updates its variant (XModem-1K, XModem-CRC or XModem-Checksum) according to the file initiation byte received. This
// allows it to upgrade to a newer variant or downgrade to an older one according to the variant requested by the Receiver.
//
// TrimPaddingBytesFromEnd() can be used to remove trailing padding bytes that are normally added to the end of the file to create a uniform packet
// length. Removing trailing padding bytes is recommended to ensure that the received file is identical byte-for-byte to its original version.

namespace YOUR_NAMESPACE_HERE
{
    public class XMODEM_FullDotNET
    {
        // TERMINAL PROGRAM OBSERVATIONS:
        //
        // TeraTerm UTF-8 Pro:
        // XModem-1K ALWAYS sends 1024 data bytes/packet.
        // When Receiver cancels, only 1 <CAN> byte is sent. The <CAN> byte is sent as a standalone byte outside a packet. The Sender simply stops transmitting
        // at the nearest whole packet, and no acknowledgement is returned.
        // When Sender cancels, no control bytes are sent. Transmission simply stops at the nearest whole packet. Only the Receiver interpacket timeout will abort
        // the transfer on the Receiver side.
        //
        // HyperTerminal 7.0:
        // XModem-1K USUALLY sends 1024 data bytes/packet. However, it can also switch to 128-byte packets if the portion to send is short.
        // When Receiver cancels, 5 <CAN> bytes are sent. The Sender simply stops transmitting at the nearest whole packet, and no acknowledgement is returned.
        // When Sender cancels, 5 <CAN> bytes are sent. The Receiver does not acknowledge. (An <ACK> is sent, but it's actually in response to the final
        // whole packet sent, not the cancellation notification.)
        //
        // This Implementation:
        // For both Sender and Receiver, requesting a cancellation is done by sending <CAN> 5 times. If the Sender or Receiver receives at least 1 <CAN> byte,
        // transfer will abort. If the Sender receives the cancellation, it will stop at the nearest whole packet. No acknowledgement is sent under any
        // circumstances by any party, in response to a cancellation.  When transmitting over a a long distance noisy channel, such as a telephone line,
        // aborting on a single <CAN> byte can be problemmatic due to other standalone control bytes possibly being corrupted into <CAN> and triggering an
        // unintended abort. However, this implementation is intended to be operated by devices that are directly connected via a serial cable, and so the risk
        // of byte corruption is much less. Aborting on a single <CAN> allows this implementation to respond to terminal software that only sends 1 <CAN>
        // like TeraTerm. However, if this is problematic, NumCancellationBytesRequired can be used to set the minimum number of cancellation bytes that
        // will result in cancellation.
        //
        // On G120 largest byte array size permissible = 786364 bytes


        // *********************************** BEGIN: RECEIVER CUSTOMIZABLE PARAMETERS ***********************************

        /// <summary>
        /// The Receiver is responsible for initiating a file transfer.
        /// It does this by sending a FileInitiationByte, which is NAK for XModem-Checksum and C for XModem-CRC and XModem-1K.
        /// If the receiver has sent a FileInitiationByte, yet it has not received the first packet within the timeout specified
        /// below, it should resend the FileInitiationByte.
        /// </summary>
        public int ReceiverFileInitiationRetryMillisec = 250;

        /// <summary>
        /// This is the maximum number of times the Receiver will send the FileInitiationByte if the Sender has not sent the first packet.
        /// If this limit is reached, what happens next will depend on the Receiver's variant.
        ///
        /// SITUATION 1A: The Receiver is requesting XModem-CRC or XModem-1K --AND-- FallBackAllowed == True
        /// A Receiver wishing to use the newer CRC or 1K protocol requests a file transfer by sending <C>.
        /// The Sender is supposed to respond by sending the first packet. However, if the Sender does not support either of these new variants,
        /// it will not recognize the <C> and will not respond. If the maximum number of file initiation attempts is reached, the Receiver
        /// will fall back to the older XModem-Checksum protocol and send <NAK> as its FileInitiationByte instead.
        /// The number of attempts is reset. If the limit is reached again and the Sender still has not sent the first packet, then the file
        /// transfer is aborted.
        ///
        /// SITUATION 1B: The Receiver is requesting XModem-CRC or XModem-1K --AND-- FallBackAllowed == False
        /// If this limit is reached and the first packet has not been received, the Receiver will abort the file transfer.
        ///
        /// SITUATION 2: The Receiver is requesting XModem-Checksum
        /// If this limit is reached and the first packet has not been received, the Receiver will abort the file transfer.
        /// </summary>
        public int ReceiverFileInitiationMaxAttempts = 240;

        /// <summary>
        /// XModem-CRC and XModem-1K evolved from XModem-Checksum.
        /// The official specification states that a Receiver which is requesting XModem-CRC or XModem-1K must have the
        /// ability to "fall back" to XModem-Checksum in case the Sender does not support newer formats.
        ///
        /// When True, this flag allows fallback to occur.
        /// When False, fallback will not occur, and the Receiver will simply abort the file transfer if the newer protocols aren't supported.
        /// This flag only has an effect if the Receiver is configured for XModem-CRC or XModem-1K. It has no impact if the Receiver is
        /// already configured for XModem-Checksum.
        /// </summary>
        public bool ReceiverFallBackAllowed = true;

        /// <summary>
        /// If the Receiver is expecting data from the Sender, this is the maximum amount of time it will wait before sending NAK to prompt
        /// the Sender to either resend the packet or finish the file.
        /// </summary>
        public int ReceiverTimeoutMillisec = 10000;

        /// <summary>
        /// If the Sender has not responded to repeated NAK nagging after this number of attempts, then the Receiver should abort the transfer.
        /// </summary>
        public int ReceiverMaxConsecutiveRetries = 10;

        private int _NumCancellationBytesRequired = 1;
        /// This is the minimum number of cancellation bytes that a Sender or Receiver must receive in order to cancel the file transfer.
        /// If 0 or less is specified, CAN bytes are ignored and will not result in cancellation, in which case cancellation will depend
        /// entirely on the timeout mechanism.
        public int NumCancellationBytesRequired
        {
            get { return _NumCancellationBytesRequired; }
            set
            {
                _NumCancellationBytesRequired = value;
            }
        }

        private int _NumCancellationBytesToSend = 5;
        /// <summary>
        /// This is the number of CAN bytes that a Sender or Receiver will transmit to the connected party if the user requests a cancellation.
        /// </summary>
        public int NumCancellationBytesToSend
        {
            get { return _NumCancellationBytesToSend; }
            set
            {
                if (value >= 0)
                    _NumCancellationBytesToSend = value;
                else
                    NumCancellationBytesToSend = 5;
            }
        }

        // *********************************** END: RECEIVER CUSTOMIZABLE PARAMETERS ***********************************

        // *********************************** BEGIN: SENDER CUSTOMIZABLE PARAMETERS ***********************************

        /// <summary>
        /// The Receiver checks each packet for errors. If a packet contains errors, the Receiver is supposed to send NAK, telling the Sender to resend the packet.
        /// This is the total number of times the Sender will resend the same packet whenever consecutive NAKs are received.
        /// If this limit is reached and the Receiver is still sending NAK, the communication channel is assumed to be terminally unreliable
        /// and file transfer should abort.
        /// </summary>
        public int MaxSenderRetries = 10;

        /// <summary>
        /// The Receiver is supposed to validate each packet with ACK or NAK.
        /// If validation has not been received from the Receiver for this amount of time, the packet is resent.
        /// </summary>
        public int SenderPacketRetryTimeoutMillisec = 15000;

        /// <summary>
        /// If the Sender has not heard ANYTHING from the Receiver after this amount of time has elapsed, the file transfer is aborted.
        /// </summary>
        public int SendInactivityTimeoutMillisec = 60000;

        // *********************************** END: SENDER CUSTOMIZABLE PARAMETERS ***********************************

        // *********************************** BEGIN: COMMON CUSTOMIZABLE PARAMETERS ***********************************

        // ASCII codes for common character constants.
        // These are exposed as public fields in case the user needs to customize them with nonstandard values.
        public byte SOH = 1;    // Sender begins each 128-byte packet with this header
        public byte STX = 2;    // Sender begins each 1024-byte packet with this header
        public byte ACK = 6;    // Receiver sends this to indicate packet was received successfully with no errors
        public byte NAK = 21;   // Receiver sends this to initiate XModem-Checksum file transfer -- OR -- indicate packet errors
        public byte C = 67;     // Receiver sends this to initiate XModem-CRC or XModem-1K file transfer
        public byte EOT = 4;    // Sender sends this to mark the end of file. Receiver must acknowledge receipt of this byte with <ACK>, otherwise Sender resends <EOT>
        public byte SUB = 26;   // This is used as a padding byte in the original specification
        public byte CAN = 24;   // [Commonly used but unofficial] Sender or Receiver sends this byte to abort file transfer
        public byte EOF = 26;   // [Commonly used but unofficial] MS-DOS version of <EOT>

        /// <summary>
        /// Defines the number of data bytes in a nominal 1024-byte packet.
        /// This allows the user to redefine a custom packet size for non-standard XModem implementations.
        /// </summary>
        private int _Packet1024NominalSize = 1024;
        public int Packet1024NominalSize
        {
            get { return _Packet1024NominalSize; }
            set
            {
                if (value > 0)
                {
                    _Packet1024NominalSize = value;
                    DefineDataPacketTemplate();
                }
            }
        }

        /// <summary>
        /// Defines the number of data bytes in a nominal 128-byte packet.
        /// This allows the user to redefine a custom packet size for non-standard XModem implementations.
        /// </summary>
        private int _Packet128NominalSize = 128;
        public int Packet128NominalSize
        {
            get { return _Packet128NominalSize; }
            set
            {
                if (value > 0)
                {
                    _Packet128NominalSize = value;
                    DefineDataPacketTemplate();
                }
            }
        }

        // *********************************** END: COMMON CUSTOMIZABLE PARAMETERS ***********************************

        /// <summary>
        /// CONSTRUCTOR.
        /// </summary>
        /// <param name="port">SerialPort to use when sending or receiving.</param>
        /// <param name="variant">
        /// The particular flavor of XModem to use.
        /// See Variants enumeration for a description of each XModem variant.
        /// </param>
        /// <param name="paddingByte">
        /// If sending, this is the byte value that will be used to pad a packet if the data being sent is shorter than
        /// the required packet length. If omitted, defaults to SUB (byte decimal 26).
        /// </param>
        /// <param name="endOfFileByteToSend">
        /// If sending, this is the byte value that will be sent to indicate that the end-of-file has been reached.
        /// Of omitted, defaults to EOT (byte decimal 4). Some XModem receiver implementations may require that EOF
        /// is used instead to indicate end-of-file.
        /// </param>
        public XMODEM_FullDotNET(SerialPort port, Variants variant, byte paddingByte = 26, byte endOfFileByteToSend = 4)
        {
            // Field initialization
            PaddingByte = paddingByte;
            EndOfFileByteToSend = endOfFileByteToSend;

            // Property Initialization
            Port = port;
            Variant = variant;
        }

        /// <summary>
        /// Cancels the file transfer operation (send or receive) and notifies the other party of the
        /// cancellation by transmitting CAN bytes.
        /// </summary>
        public void CancelFileTransfer()
        {
            // Abort what we're currently doing
            Abort();

            // Send cancellation bytes
            for (int k = 1; k <= _NumCancellationBytesToSend; k++)
            {
                Port.Write(new byte[] { CAN }, 0, 1);
            }

            _TerminationReason = TerminationReasonEnum.UserCancelled;
        }

        /// <summary>
        /// Describes the various flavors of XModem.
        ///
        /// XModemChecksum: This is the original, classic version of XModem. It is also the slowest and most error-prone.
        /// 128 data bytes per packet with checksum error-detection
        /// 132 bytes/packet total = 3 header bytes + 128 data bytes + 1 error-detection byte
        ///
        /// XModemCRC: This similar to the original, except with CRC-16 error detection instead of a simple checksum for more reliability.
        /// 128 data bytes per packet with CRC-16 error-detection
        /// 133 bytes/packet total = 3 header bytes + 128 data bytes + 2 error-detection bytes
        ///
        /// XModem1K: This is an updated version of XModem-CRC, expanded to 1024 data bytes (though 128 data bytes are still accepted).
        /// 128 and/or 1024 data bytes per packet and CRC-16 error-detection
        /// 133 bytes/packet total = 3 header bytes + 128 data bytes + 2 error-detection bytes
        /// 1029 bytes/packet total = 3 header bytes + 1024 data bytes + 2 error-detection bytes
        /// </summary>
        public enum Variants
        {
            XModemChecksum,
            XModemCRC,
            XModem1K
        };

        private Variants _Variant;
        /// <summary>
        /// Gets/sets the XModem flavor that this implementation adheres to.
        /// </summary>
        public Variants Variant
        {
            get { return _Variant; }
            set
            {
                _Variant = value;
                DefineDataPacketTemplate(); // In case user wants to send data
            }
        }

        /// <summary>
        /// XModem's possible operational states.
        /// </summary>
        private enum States
        {
            Inactive,                           // The object is neither Sending nor Receiving
            ReceiverFileInitiation,             // Receiver is sending the file initiation byte at regular intervals
            ReceiverHeaderSearch,               // Receiver is expecting SOH or STX packet header
            ReceiverBlockNumSearch,             // Receiver is expecting the block number
            ReceiverBlockNumComplementSearch,   // Receiver is expecting the block number complement
            ReceiverDataBytesSearch,            // Receiver is populating data bytes
            ReceiverErrorCheckSearch,           // Receiver is expecting 1-byte or 2-byte check value(s)
            SenderAwaitingFileInitiation,       // Sender is expecting file transmission request from Receiver
            SenderPacketSent,                   // Sender has sent a packet and is waiting for Receiver to validate it
            SenderAlertForPossibleCancellation, // Sender is not expecting anything in particular but can respond to a cancellation request if needed
            SenderAwaitingEndOfFileConfirmation // Sender has transmitted the end-of-file byte and is waiting for Receiver to acknowledge receipt of that byte
        }

        private States CurrentState = States.Inactive;

        /// <summary>
        /// Describes how the current XModem session has ended.
        /// </summary>
        public enum TerminationReasonEnum
        {
            TransferStillActiveNotTerminated,   // File transfer is still active and has not terminated yet
            UserCancelled,                      // User has cancelled the file transfer
            EndOfFile,                          // End-of-file was reached. This is the ideal outcome when sending or receiving.
            FileInitiationTimeout,              // Receiver has repeatedly requested file transfer to begin, but Sender has not responded
            CancelNotificationReceived,         // Transfer aborted because cancellation bytes were detected from the Sender or Receiver
            TooManyRetries,                     // Too many erroneous packets have been sent or received. Indicates corruption or total communication loss.
            NoResponseFromReceiver              // When Sending a file, the Receiver has become completely silent for an extended period.
        }

        public TerminationReasonEnum _TerminationReason = TerminationReasonEnum.TransferStillActiveNotTerminated;
        /// <summary>
        /// Describes under what conditions the file transfer has terminated.
        /// </summary>
        public TerminationReasonEnum TerminationReason
        {
            get { return _TerminationReason; }
        }

        /// <summary>
        /// PacketReceived event handler.
        /// </summary>
        /// <param name="sender">The XMODEM object that raised the event.</param>
        /// <param name="packet">The complete, validated data packet received.</param>
        /// <param name="endOfFileDetected">
        /// Boolean flag that indicates if this packet is the final packet expected.
        /// True if the end-of-file byte was received, and the current packet is therefore the last one.
        /// False if the end-of-file byte has not been received, and more packets are still expected.
        /// </param>
        public delegate void PacketReceivedEventHandler(XMODEM_FullDotNET sender, byte[] packet, bool endOfFileDetected);

        /// <summary>
        /// Raised whenever a complete packet has been received.
        /// </summary>
        public event PacketReceivedEventHandler PacketReceived;

        /// <summary>
        /// Communication port that will be used to send and receive.
        /// </summary>
        public SerialPort Port;

        /// <summary>
        /// Performs blocking when the Receive() method is called by the user.
        /// </summary>
        private ManualResetEvent ReceiverUserBlock = new ManualResetEvent(false);

        private int _NumCancellationBytesReceived = 0;
        /// <summary>
        /// Tracks the number of CAN bytes that have currently been received by this Sender or Receiver.
        /// </summary>
        public int NumCancellationBytesReceived
        {
            get { return _NumCancellationBytesRequired; }
        }

        /// <summary>
        /// MemoryStream used to store all data received if user wants to receive data in one big lump.
        /// This remains null if user wants to receive data packet-by-packet instead.
        /// </summary>
        private MemoryStream AllDataReceivedBuffer;

        /// <summary>
        /// Initiates the file receive process. This method blocks until the file transfer has terminated.
        /// </summary>
        /// <param name="allDataReceivedBuffer">
        /// Optional MemoryStream object, which may be omitted.
        /// By instantiating an empty MemoryStream object and supplying it as an argument, the user can collect all data
        /// received into a single data structure and process it in one big lump once the transfer has finished.
        /// This is NOT recommended if the expected file size can exceed the contiguous memory capacity of the device.
        /// If the expected file size is large, subscribing to the PacketReceived event is recommended so that processing
        /// can be done one packet at a time within the available memory.
        /// </param>
        /// <returns>
        /// A TerminationReasonEnum that describes under what conditions the receive process has terminated. This may
        /// be due to a successful EndOfFile being reached, or due to timeouts, errors, etc.
        /// </returns>
        public TerminationReasonEnum Receive(MemoryStream allDataReceivedBuffer = null)
        {
            // Initialize control variables
            _TerminationReason = TerminationReasonEnum.TransferStillActiveNotTerminated;
            Aborted = false;
            BlockNumExpected = 1;
            _NumFileInitiationBytesSent = 0;
            _NumCancellationBytesReceived = 0;
            Remainder = new byte[0];
            DataPacketNumBytesStored = 0;
            ExpectingFirstPacket = true;
            ValidPacketReceived = false;

            // Define file initiation byte according to variant
            if (_Variant == Variants.XModemChecksum)
                FileInitiationByteToSend = NAK;
            else
                FileInitiationByteToSend = C;

            // Initialize state
            CurrentState = States.ReceiverFileInitiation;

            // Open port if it isn't open already
            if (Port.IsOpen == false)
                Port.Open();

            // Clear out serial port buffers
            Port.DiscardInBuffer();
            Port.DiscardOutBuffer();

            // If user wants to get all data in one big lump, initialize the buffer with whatever MemoryStream object
            // is passed. If a memorystream is omitted, this defaults to null, which means received data
            // will not be stored.
            AllDataReceivedBuffer = allDataReceivedBuffer;

            // Attach event handler
            Port.DataReceived += new SerialDataReceivedEventHandler(Port_DataReceived);

            // Begin file initiation
            if (ReceiverFileInitiationTimer == null)
                ReceiverFileInitiationTimer = new Timer(ReceiverFileInitiationRoutine, null, 0, ReceiverFileInitiationRetryMillisec);
            else
                ReceiverFileInitiationTimer.Change(0, ReceiverFileInitiationRetryMillisec);

            // Block here
            ReceiverUserBlock.Reset();
            ReceiverUserBlock.WaitOne();
            ReceiverUserBlock.Reset();

            return TerminationReason;
        }

        // The byte value that a Receiver sends when requesting file transfer:
        // XModem Checksum = <NAK>
        // XModem CRC = <C>
        // XModem 1K = <C>
        private byte FileInitiationByteToSend;

        /// <summary>
        /// Sends the file initiation byte at regular intervals to request start of transfer from Sender.
        /// </summary>
        private Timer ReceiverFileInitiationTimer;

        /// <summary>
        /// Timer callback for file initiation timer.
        /// </summary>
        /// <param name="notUsed"></param>
        private void ReceiverFileInitiationRoutine(object notUsed)
        {
            Port.Write(new byte[] { FileInitiationByteToSend }, 0, 1);
            NumFileInitiationBytesSent += 1;
        }

        // Tracks how many file initiation bytes have already been sent to the Sender
        private int _NumFileInitiationBytesSent = 0;
        public int NumFileInitiationBytesSent
        {
            get { return _NumFileInitiationBytesSent; }
            private set
            {
                _NumFileInitiationBytesSent = value;

                // Determine if Receiver should fall back to an older variant
                if (_NumFileInitiationBytesSent > ReceiverFileInitiationMaxAttempts)
                {
                    if (FileInitiationByteToSend == C && ReceiverFallBackAllowed == true)
                    {
                        // Fall back to older variant
                        FileInitiationByteToSend = NAK;
                        _NumFileInitiationBytesSent = 0;
                    }
                    else
                    {
                        Abort();
                        _TerminationReason = TerminationReasonEnum.FileInitiationTimeout;
                        ReceiverUserBlock.Set();
                    }
                }
            }
        }

        // Indicates whether a valid packet has been received and is fit to be forwarded to user
        private bool ValidPacketReceived = false;

        /// <summary>
        /// State machine dispatcher.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void Port_DataReceived(object sender, SerialDataReceivedEventArgs e)
        {
            SerialPort sp = sender as SerialPort;
            int numBytes = sp.BytesToRead;
            byte[] recv = new byte[numBytes];
            sp.Read(recv, 0, numBytes);

            if (numBytes > 0)
            {
                switch (CurrentState)
                {
                    // RECEIVER STATES:
                    case States.ReceiverFileInitiation:
                        // A response was received from the Sender after 1 or more file initiation bytes have been sent.

                        // Halt the file initiation sender
                        if (ReceiverFileInitiationTimer != null)
                            ReceiverFileInitiationTimer.Change(Timeout.Infinite, Timeout.Infinite);

                        // Start the timeout/NAK watchdog
                        if (ReceiverNAKWatchdog == null)
                            ReceiverNAKWatchdog = new Timer(NAKNag, null, ReceiverTimeoutMillisec, ReceiverTimeoutMillisec);
                        else
                            ReceiverNAKWatchdog.Change(ReceiverTimeoutMillisec, ReceiverTimeoutMillisec);

                        // Advance to the next stage and process the current data received
                        CurrentState = States.ReceiverHeaderSearch;
                        ReceiverPacketBuilder(recv);
                        break;

                    case States.ReceiverHeaderSearch:
                    case States.ReceiverBlockNumSearch:
                    case States.ReceiverBlockNumComplementSearch:
                    case States.ReceiverDataBytesSearch:
                    case States.ReceiverErrorCheckSearch:
                        ResetNAKWatchdog();
                        ReceiverPacketBuilder(recv);
                        break;


                    // SENDER STATES:
                    case States.SenderAwaitingFileInitiation:
                        ResetReceiverStillAliveWatchdog();
                        if (DetectCancellation(recv) == false)
                        {
                            // Determine if file initiation byte for CRC or Checksum error-checking received.
                            if (Array.IndexOf(recv, C) > -1 || Array.IndexOf(recv, NAK) > -1)
                            {
                                // If user originally specified older XModem-Checksum but <C> was received instead,
                                // upgrade variant to new XModem-1K (which can handle both 128 and 1024 data bytes)
                                // for maximum flexibility.
                                if (_Variant == Variants.XModemChecksum && Array.IndexOf(recv, C) > -1)
                                    Variant = Variants.XModem1K;

                                if (Array.IndexOf(recv, NAK) > -1)
                                    Variant = Variants.XModemChecksum;

                                CurrentState = States.SenderAlertForPossibleCancellation;
                                WaitForResponseFromReceiver.Set();
                            }
                        }
                        break;

                    case States.SenderPacketSent:
                        ResetReceiverStillAliveWatchdog();
                        if (DetectCancellation(recv) == false)
                        {
                            if (Array.IndexOf(recv, ACK) > -1)
                            {
                                // ACK received
                                ResetSenderPacketResponseWatchdog();
                                _SenderConsecutiveRetryAttempts = 0;
                                PacketSuccessfullySent = true;
                                _BlockNumToSend += 1;
                                WaitForResponseFromReceiver.Set();
                            }
                            else if (Array.IndexOf(recv, NAK) > -1)
                            {
                                // NAK received
                                ResetSenderPacketResponseWatchdog();
                                _SenderConsecutiveRetryAttempts += 1;
                                WaitForResponseFromReceiver.Set();
                            }
                        }
                        break;

                    case States.SenderAlertForPossibleCancellation:
                        ResetReceiverStillAliveWatchdog();
                        DetectCancellation(recv);
                        break;

                    case States.SenderAwaitingEndOfFileConfirmation:
                        ResetReceiverStillAliveWatchdog();
                        if (DetectCancellation(recv) == false)
                        {
                            if (Array.IndexOf(recv, ACK) > -1)
                            {
                                // ACK received
                                EndOfFileAcknowledgementReceived = true;
                                Abort();
                            }
                        }
                        break;

                }
            }
        }

        /// <summary>
        /// Enforces timeouts if no data is received or the packet repeatedly fails the checksum.
        /// This timer must be reset within a set amount of time or NAK is transmitted.
        /// </summary>
        private Timer ReceiverNAKWatchdog;

        /// <summary>
        /// Resets the NAK watchdog.
        /// </summary>
        private void ResetNAKWatchdog()
        {
            if (ReceiverNAKWatchdog != null)
                ReceiverNAKWatchdog.Change(ReceiverTimeoutMillisec, ReceiverTimeoutMillisec);

            ReceiverNumConsecutiveNAKSent = 0;
        }

        /// <summary>
        /// Timer callback for the NAK watchdog.
        /// </summary>
        /// <param name="notUsed"></param>
        private void NAKNag(object notUsed)
        {
            SendNAK();
        }

        private void SendNAK()
        {
            Port.Write(new byte[] { NAK }, 0, 1);
            ReceiverNumConsecutiveNAKSent += 1;
        }

        private void SendACK()
        {
            Port.Write(new byte[] { ACK }, 0, 1);
            ReceiverNumConsecutiveNAKSent = 0;
        }

        // Tracks how many consecutive <NAK> bytes have been sent
        public int _ReceiverNumConsecutiveNAKSent = 0;
        public int ReceiverNumConsecutiveNAKSent
        {
            get { return _ReceiverNumConsecutiveNAKSent; }
            set
            {
                _ReceiverNumConsecutiveNAKSent = value;

                if (_ReceiverNumConsecutiveNAKSent >= ReceiverMaxConsecutiveRetries)
                {
                    Abort();
                    _TerminationReason = TerminationReasonEnum.TooManyRetries;
                    ReceiverUserBlock.Set();
                }
            }
        }

        /// <summary>
        /// The expected data packet size as specified by the incoming packet header.
        /// </summary>
        private int ExpectedDataPacketSize;

        // Stores the value of a candidate block number
        private byte BlockNumReceived;

        // Stores the value of a candidate block number complement (255 minus block number)
        private byte BlockNumComplementCandidateReceived;

        private bool ExpectingFirstPacket = true;
        private byte BlockNumExpected = 1;

        private byte[] BytesToParse;
        private byte[] Remainder = new byte[0];

        // Stores data bytes received. It is instantiated to be the same length as the expected data packet size.
        private byte[] DataPacketReceived;

        // Keeps track of the number of bytes actually placed inside the current data packet
        private int DataPacketNumBytesStored = 0;

        // Stores the check value. This will contain 1 byte for XModem-Checksum, and 2 bytes for XModem-CRC or XModem-1K.
        private byte[] ErrorCheck;

        // Calculates CRC-16 CCITT checksum using polynomial (X^16 + X^12 + X^5  + 1)
        private CRC16CCITT CRC = new CRC16CCITT(CRC16CCITT.InitialCrcValue.Zeros);

        private void ReceiverPacketBuilder(byte[] freshBytes)
        {
            // The DataReceived event is occasionally triggered even when there are no bytes to read, so guard against this
            if (freshBytes.Length == 0)
                return;

            // We only want this to run once per DataReceived event and only if we are searching for a valid header byte
            if (Remainder.Length > 0 && CurrentState == States.ReceiverHeaderSearch)
                BytesToParse = CombineArrays(Remainder, freshBytes);
            else
                BytesToParse = freshBytes;

            // This keeps track of the index positions that searches begin at
            int headerByteSearchStartIndex = 0;
            int searchStartIndex = 0;

            // Loop while we are within bounds of the bytes to parse
            while (searchStartIndex < BytesToParse.Length && headerByteSearchStartIndex < BytesToParse.Length)
            {

                if (CurrentState == States.ReceiverHeaderSearch)
                {
                    // Empty the remainder if populated, since it should have fulfilled its purpose prior to us
                    // reaching this point. We don't want the remainder to carry over into the next DataReceived event
                    // if we are in the header byte search state.
                    if (Remainder.Length > 0)
                        Remainder = new byte[0];

                    // Check for file termination tokens
                    if (BytesToParse[headerByteSearchStartIndex] == EOT ||
                        BytesToParse[headerByteSearchStartIndex] == EOF)
                    {
                        SendACK();
                        Abort();
                        _TerminationReason = TerminationReasonEnum.EndOfFile;

                        // If subscribed to, raise packet received event and indicate end of file reached
                        if (PacketReceived != null && ValidPacketReceived == true && DataPacketReceived != null && DataPacketReceived.Length > 0)
                            PacketReceived(this, DataPacketReceived, true);

                        ReceiverUserBlock.Set();
                        return;
                    }
                    else
                    {
                        // If subscribed to, raise packet received event and indicate end of file NOT yet reached
                        if (PacketReceived != null && ValidPacketReceived == true && DataPacketReceived != null && DataPacketReceived.Length > 0)
                            PacketReceived(this, DataPacketReceived, false);
                    }

                    // Reset packet received validation flag
                    ValidPacketReceived = false;

                    // Check for cancellation bytes
                    if (_NumCancellationBytesRequired > 0)
                    {
                        // Check if current byte is a cancellation request
                        if (BytesToParse[headerByteSearchStartIndex] == CAN)
                        {
                            _NumCancellationBytesReceived += 1;

                            if (_NumCancellationBytesReceived >= _NumCancellationBytesRequired)
                            {
                                // CANCEL THE FILE TRANSFER
                                Abort();
                                _TerminationReason = TerminationReasonEnum.CancelNotificationReceived;
                                ReceiverUserBlock.Set();
                                return;
                            }
                            else
                            {
                                // Move on to the next byte
                                headerByteSearchStartIndex += 1;
                                continue;
                            }
                        }
                        else
                        {
                            // If not a cancellation byte, reset the cancellation byte counter
                            _NumCancellationBytesReceived = 0;
                        }
                    }

                    // Determine if we have a candidate header byte based on our XModem variant
                    if (_Variant == Variants.XModemChecksum || _Variant == Variants.XModemCRC)
                    {
                        // XModem-Checksum and XModem-CRC should always have 128 data byte packets headed by <SOH>.
                        // Determine the location of this header byte, if it's present.                        
                        int foundIndex = Array.IndexOf(BytesToParse, SOH, headerByteSearchStartIndex);

                        if (foundIndex == -1) // No header byte found
                        {
                            // Quit this search and ignore the remaining bytes.
                            // Look for a header byte in the next transmission.
                            return;
                        }
                        else if (foundIndex > -1)
                        {
                            // Save the index position where we THINK a valid header byte resides at.
                            // The candidate header byte may be discovered to be invalid later on, and we
                            // need an index to return to in case we need to repeat the header search
                            // starting from the next index.
                            headerByteSearchStartIndex = foundIndex + 1;

                            // Specify the packet size corresponding to this header
                            ExpectedDataPacketSize = _Packet128NominalSize;

                            // Move on to the next stage
                            searchStartIndex = foundIndex + 1;
                            CurrentState = States.ReceiverBlockNumSearch;
                            continue;
                        }
                    }
                    else if (_Variant == Variants.XModem1K)
                    {
                        // XModem-1K can receive 1024 data byte packets headed by <STX> --OR-- 128 data byte packets headed by <SOH>.
                        // The official standard allows a Sender to send a mixture of packet sizes, so a Receiver has to look for both.
                        int packetStartIndexSTX = Array.IndexOf(BytesToParse, STX, headerByteSearchStartIndex);
                        int packetStartIndexSOH = Array.IndexOf(BytesToParse, SOH, headerByteSearchStartIndex);

                        // Look for the packet header byte
                        int foundIndex = 0;
                        if (packetStartIndexSTX > -1 && packetStartIndexSOH > -1)
                        {
                            // There is a (slim) possibility that both <SOH> and <STX> bytes may be found.                      
                            // If both are found, our candidate packet start index should be the earlier of the two.
                            // If we're wrong, we can always get to the later one during the next iteration.
                            if (packetStartIndexSTX <= packetStartIndexSOH)
                            {
                                // Header for 1024 data bytes/packet
                                ExpectedDataPacketSize = _Packet1024NominalSize;
                                foundIndex = packetStartIndexSTX;
                            }
                            else
                            {
                                // Header for 128 data bytes/packet
                                ExpectedDataPacketSize = _Packet128NominalSize;
                                foundIndex = packetStartIndexSOH;
                            }
                        }
                        else if (packetStartIndexSTX > -1)  // Only 1 found
                        {
                            // Header for 1024 data bytes/packet
                            ExpectedDataPacketSize = _Packet1024NominalSize;
                            foundIndex = packetStartIndexSTX;
                        }
                        else if (packetStartIndexSOH > -1)  // Only 1 found
                        {
                            // Header for 128 data bytes/packet
                            ExpectedDataPacketSize = _Packet128NominalSize;
                            foundIndex = packetStartIndexSOH;
                        }
                        else
                        {
                            // If neither candidate headers were found, quit this search and ignore the remaining bytes.
                            // Look for a header byte in the next transmission.
                            return;
                        }

                        // Save the index position where we THINK a valid header byte resides at.
                        // The candidate header byte may be discovered to be invalid later on, and we
                        // need an index to return to in case we need to repeat the header search
                        // starting from the next index.
                        headerByteSearchStartIndex = foundIndex + 1;

                        // Move on to the next stage
                        searchStartIndex = foundIndex + 1;
                        CurrentState = States.ReceiverBlockNumSearch;
                        continue;
                    }
                }

                if (CurrentState == States.ReceiverBlockNumSearch)
                {
                    // The block number should be immediately after the packet header byte
                    BlockNumReceived = BytesToParse[searchStartIndex];

                    // If the candidate block number is equal to a header byte, there is a slim possibility that what we currently believe
                    // is the block number may actually be the true header byte, especially if the current candidate header byte is discovered
                    // to be invalid later. Therefore, we should save the "block number" as a remainder so we can re-examine it later if
                    // the current header byte canididate does not pan out and another DataReceived event is raised.
                    if ((_Variant == Variants.XModemChecksum || _Variant == Variants.XModemCRC)
                        && BlockNumReceived == SOH)
                    {
                        Remainder = new byte[] { BlockNumReceived };   // Initialize remainder
                    }
                    else if (_Variant == Variants.XModem1K && (BlockNumReceived == SOH || BlockNumReceived == STX))
                    {
                        Remainder = new byte[] { BlockNumReceived };   // Initialize remainder
                    }

                    // Move on to the next stage
                    searchStartIndex += 1;
                    CurrentState = States.ReceiverBlockNumComplementSearch;
                    continue;
                }

                if (CurrentState == States.ReceiverBlockNumComplementSearch)
                {
                    // The complement of the block number should be right after the block number
                    BlockNumComplementCandidateReceived = BytesToParse[searchStartIndex];

                    // Determine if we have received a valid block number and complement.
                    // This will determine if we have a valid packet header and can proceed to the next stage.
                    if (BlockNumComplementCandidateReceived == 255 - BlockNumReceived)
                    {
                        // Valid packet header....

                        // The packet header is valid, so we don't need to re-examine the remainder and can therefore
                        // discard it
                        if (Remainder.Length > 0)
                            Remainder = new byte[0];

                        // Instantiate the data packet array which will hold incoming data
                        DataPacketReceived = new byte[ExpectedDataPacketSize];
                        DataPacketNumBytesStored = 0;

                        // Move on to the next stage
                        searchStartIndex += 1;
                        CurrentState = States.ReceiverDataBytesSearch;
                        continue;
                    }
                    else
                    {
                        // Not a valid packet header....

                        // Add the candidate block number complement to the remainder if:
                        // 1.) the remainder already contains an alternate candidate header byte --OR--
                        // 2.) the candidate block number complement has the same value as a header byte, and could therefore be a valid header byte itself
                        if (Remainder.Length > 0 || ((_Variant == Variants.XModemChecksum || _Variant == Variants.XModemCRC)
                            && BlockNumReceived == SOH))
                        {
                            Remainder = CombineArrays(Remainder, new byte[] { BlockNumComplementCandidateReceived });   // Add to remainder
                        }
                        else if (Remainder.Length > 0 || (_Variant == Variants.XModem1K && (BlockNumReceived == SOH || BlockNumReceived == STX)))
                        {
                            Remainder = CombineArrays(Remainder, new byte[] { BlockNumComplementCandidateReceived });   // Add to remainder
                        }

                        // Search for another header byte
                        CurrentState = States.ReceiverHeaderSearch;
                        continue;
                    }
                }

                if (CurrentState == States.ReceiverDataBytesSearch)
                {
                    // Begin filling the data packet....

                    // Determine if there are enough unparsed bytes to fill the data packet
                    int numUnparsedBytesRemaining = BytesToParse.Length - searchStartIndex;
                    int numDataPacketBytesStillMissing = DataPacketReceived.Length - DataPacketNumBytesStored;

                    int numDataBytesToPull;
                    if (numUnparsedBytesRemaining >= numDataPacketBytesStillMissing)
                        numDataBytesToPull = numDataPacketBytesStillMissing;
                    else
                        numDataBytesToPull = numUnparsedBytesRemaining;

                    Array.Copy(BytesToParse, searchStartIndex, DataPacketReceived, DataPacketNumBytesStored, numDataBytesToPull);
                    DataPacketNumBytesStored += numDataBytesToPull;
                    searchStartIndex += numDataBytesToPull;

                    if (DataPacketNumBytesStored >= ExpectedDataPacketSize)
                    {
                        // If all expected data bytes have been gathered, move on to the next stage
                        CurrentState = States.ReceiverErrorCheckSearch;
                        ErrorCheck = new byte[0];
                    }

                    continue;
                }

                if (CurrentState == States.ReceiverErrorCheckSearch)
                {
                    if (_Variant == Variants.XModemChecksum)
                    {
                        // 1 error-check byte expected.
                        ErrorCheck = new byte[] { BytesToParse[searchStartIndex] };

                        // Validate packet
                        ValidatePacket();

                        // Start over              
                        headerByteSearchStartIndex = searchStartIndex + 1;
                        CurrentState = States.ReceiverHeaderSearch;
                    }
                    else  // XModem-CRC or XModem-1K
                    {
                        // 2 error-check bytes expected.

                        // If we have not yet gathered the required number of error check bytes, append to the error check
                        // array.
                        if (ErrorCheck.Length < 2)
                        {
                            ErrorCheck = CombineArrays(ErrorCheck, new byte[] { BytesToParse[searchStartIndex] });
                        }

                        if (ErrorCheck.Length >= 2)
                        {
                            // We have enough error-check bytes, so validate packet
                            ValidatePacket();

                            // Return to the initial state and start over              
                            headerByteSearchStartIndex = searchStartIndex + 1;
                            CurrentState = States.ReceiverHeaderSearch;
                        }
                        else
                        {
                            // Advance to the next byte
                            searchStartIndex += 1;
                        }
                    }
                } // End if
            } // End while
        } // End method

        private void ValidatePacket()
        {
            // In order for a packet to be accepted, it must be an expected block number, and the transmitted
            // check value must match the calculated check value.

            // Dealing with a block number that is out of sequence:
            //
            // Normally, successive block numbers must be monotonically increasing (accounting for binary wraparound) in order
            // to be valid. <NAK> is normally sent if a block number is out of sequence. However, there's an exception.
            //
            // Exception:
            // If the current block number has been duplicated (it has the same value as a block number that was
            // received previously), <ACK> is sent anyway on the theory that the Sender may not have received
            // the previous <ACK> and decided to send the same packet again. <ACK> will prompt the Sender to move on to the
            // next packet, which is what we want. (<NAK> will make it resend the packet yet again, which is unwanted.)
            //
            // Exception to the exception:
            // If this is the very first packet, the block number MUST be 1. Otherwise, <NAK> will be sent.

            if (BlockNumReceived == BlockNumExpected)
            {
                if (ValidateChecksum() == true)
                {
                    // Update control variables
                    BlockNumExpected += 1;
                    ExpectingFirstPacket = false;

                    // If user wants all received data to be outputed in one lump, add this packet to the buffer
                    if (AllDataReceivedBuffer != null)
                        AllDataReceivedBuffer.Write(DataPacketReceived, 0, DataPacketReceived.Length);

                    ValidPacketReceived = true;

                    // Notify Sender to send the next packet
                    SendACK();
                }
                else
                {
                    // Inform sender that checksum invalid
                    SendNAK();
                    ValidPacketReceived = false;
                }
            }
            else if (ExpectingFirstPacket == false && BlockNumReceived == (byte)(BlockNumExpected - 1))
            {
                // Receiver got a duplicate packet.
                // Send <ACK> to prompt the Sender to advance to the next packet. Ignore the current (redundant) packet.
                SendACK();
                ValidPacketReceived = false;
            }
            else
            {
                // The block number is completely out of sequence, so send NAK
                SendNAK();
                ValidPacketReceived = false;
            }
        }

        /// <summary>
        /// Calculates the check value (simple checksum or CRC-16) appended to the end of a packet.
        /// </summary>
        /// <returns>
        /// True if the calculated check value and received check value match.
        /// False if there is a mismatch between the calculated and received check values.
        /// </returns>
        private bool ValidateChecksum()
        {
            switch (_Variant)
            {
                // Arithmetic checksum:
                case Variants.XModemChecksum:
                    byte checksum = CheckSum(DataPacketReceived);
                    if (checksum == ErrorCheck[0])
                        return true;
                    else
                        return false;

                // CRC-16:
                case Variants.XModemCRC:
                case Variants.XModem1K:
                    ushort crcChecksumCalculated = CRC.ComputeChecksum(DataPacketReceived);
                    ushort crcChecksumReceived = BytesToUShort(ErrorCheck[0], ErrorCheck[1]);
                    if (crcChecksumCalculated == crcChecksumReceived)
                        return true;
                    else
                        return false;

                // Just so VS2010 doesn't complain:
                default:
                    return false;
            }
        }

        private ushort BytesToUShort(byte highByte, byte lowByte)
        {
            return (ushort)((highByte << 8) + lowByte);
        }

        private byte[] UShortToBytes(ushort val)
        {
            byte highByte = (byte)(val / 256);
            byte lowByte = (byte)(val % 256);

            return new byte[] { highByte, lowByte };
        }

        /// <summary>
        /// Calculates the simple checksum by summing all values in a byte array and returning the remainder.
        /// </summary>
        /// <param name="seq">
        /// Byte array whose checksum to calculate.
        /// </param>
        /// <returns>
        /// Modulo-256 checksum.
        /// </returns>
        private byte CheckSum(byte[] seq)
        {
            byte sum = 0;
            for (int k = 0; k < seq.Length; k++)
            {
                sum += seq[k];
            }
            return sum;
        }

        private bool Aborted = false;

        /// <summary>
        /// Internal method used to cancel the file transfer.
        /// </summary>
        private void Abort()
        {
            CurrentState = States.Inactive;
            TerminateSend = true;
            SenderInitialized = false;
            Aborted = true;

            // Detach event handler
            Port.DataReceived -= Port_DataReceived;

            // If we are sending data, tell Sender not to expect any more responses from Receiver.
            // This has no ill effect if we are receiving instead.
            WaitForResponseFromReceiver.Set();

            // Deactivate Send and Receive watchdogs
            if (ReceiverNAKWatchdog != null)
                ReceiverNAKWatchdog.Change(Timeout.Infinite, Timeout.Infinite);

            if (ReceiverFileInitiationTimer != null)
                ReceiverFileInitiationTimer.Change(Timeout.Infinite, Timeout.Infinite);

            if (SenderPacketResponseWatchdog != null)
                SenderPacketResponseWatchdog.Change(Timeout.Infinite, Timeout.Infinite);

            if (ReceiverStillAliveWatchdog != null)
                ReceiverStillAliveWatchdog.Change(Timeout.Infinite, Timeout.Infinite);

            // Flush serial data so they don't contaminate a future session
            Port.DiscardInBuffer();
            Port.DiscardOutBuffer();
        }

        // ************************************* SENDER SENDER SENDER SENDER SENDER SENDER SENDER ***********************************

        private ManualResetEvent WaitForResponseFromReceiver = new ManualResetEvent(false);

        private Timer SenderPacketResponseWatchdog;

        private Timer ReceiverStillAliveWatchdog;

        /// <summary>
        /// This is an array that will be instantiated to the specified data packet size and filled with padding bytes.
        /// The intent is for this array to be copied for each outgoing data packet and populated with data bytes.
        /// This avoids the overhead of having to populate new arrays with padding bytes each time.
        /// </summary>
        private byte[] SenderDataPacketMasterTemplate;

        private byte[] DataPacketToSend;

        private bool TerminateSend = false;

        /// <summary>
        /// Tracks the number of data bytes that have currently been added to the outbound packet.
        /// </summary>
        private int NumUserDataBytesAddedToCurrentPacket = 0;

        /// <summary>
        /// The byte value used to pad a packet in order to meet its 128-byte or 1024-byte required length.
        /// </summary>
        public byte PaddingByte;

        /// <summary>
        /// When using this XModem to send data, the official specification requires that EOT is transmitted to signal the end of file.
        /// However, some programs may require a different byte value, such as EOF instead. This allows the user to specify
        /// a custom byte value to transmit to the Receiver when the file is complete.
        /// </summary>
        public byte EndOfFileByteToSend;

        /// <summary>
        /// Defines a master outbound data packet that will be filled with padding bytes.
        /// The purpose of this template is to be copied for each new outbound packet and subsequently
        /// populated with actual data.
        /// </summary>
        private void DefineDataPacketTemplate()
        {
            int dataPacketSize;
            if (_Variant == Variants.XModem1K)
                dataPacketSize = _Packet1024NominalSize;
            else
                dataPacketSize = _Packet128NominalSize;

            SenderDataPacketMasterTemplate = new byte[dataPacketSize];

            // Fill the template with padding bytes
            for (int k = 0; k < SenderDataPacketMasterTemplate.Length; k++)
                SenderDataPacketMasterTemplate[k] = PaddingByte;
        }

        private byte _BlockNumToSend = 1;
        public byte BlockNumToSend
        {
            get { return _BlockNumToSend; }
        }

        /// <summary>
        /// Initializes the modem send process.
        /// </summary>
        /// <param name="dataToSend">
        /// Optional argument.
        /// If provided, this is the file that should be transmitted in its entirety.
        /// The EndOfFile byte is automatically transmitted once this array is finished.
        /// If omitted, the file may be sent piece-by-piece using the AddToOutboundPacket() method.
        /// </param>
        /// <returns>
        /// The number of data bytes successfully transmitted.
        /// </returns>
        public int Send(byte[] dataToSend = null)
        {
            // Initialize control variables
            _TerminationReason = TerminationReasonEnum.TransferStillActiveNotTerminated;
            Aborted = false;
            _BlockNumToSend = 1;    // Current outbound block number
            _SenderConsecutiveRetryAttempts = 0;
            PacketSuccessfullySent = false;
            DataPacketToSend = null;
            NumUserDataBytesAddedToCurrentPacket = 0;
            _TotalUserDataBytesPacketized = 0;
            _TotalUserDataBytesSent = 0;
            _NumCancellationBytesReceived = 0;
            TerminateSend = false;
            EndOfFileAcknowledgementReceived = false;
            SenderInitialized = true;   // Ensures that this method is called first before AddToOutboundPacket()

            WaitForResponseFromReceiver.Reset();
            CurrentState = States.SenderAwaitingFileInitiation;

            // Open port if it isn't open already
            if (Port.IsOpen == false)
                Port.Open();

            Port.DiscardInBuffer();
            Port.DiscardOutBuffer();

            if (ReceiverStillAliveWatchdog == null)
                ReceiverStillAliveWatchdog = new Timer(ReceiverStillAliveWatchdogRoutine, null, SendInactivityTimeoutMillisec, SendInactivityTimeoutMillisec);
            else
                ReceiverStillAliveWatchdog.Change(SendInactivityTimeoutMillisec, SendInactivityTimeoutMillisec);

            if (SenderPacketResponseWatchdog == null)
                SenderPacketResponseWatchdog = new Timer(SenderPacketResponseWatchdogRoutine, null, SenderPacketRetryTimeoutMillisec, SenderPacketRetryTimeoutMillisec);
            else
                SenderPacketResponseWatchdog.Change(SenderPacketRetryTimeoutMillisec, SenderPacketRetryTimeoutMillisec);

            // Attach event handler
            Port.DataReceived += new SerialDataReceivedEventHandler(Port_DataReceived);

            // Wait here for file initiation byte to be received from Receiver
            WaitForResponseFromReceiver.WaitOne();
            WaitForResponseFromReceiver.Reset();

            if (dataToSend != null && Aborted == false)
            {
                AddToOutboundPacket(dataToSend);
                if (TerminateSend == false)
                    EndFile();

                return _TotalUserDataBytesSent;
            }
            else
                return 0;
        }

        private int _TotalUserDataBytesPacketized = 0;
        /// <summary>
        /// Returns the total number of user data bytes that have been incorporated into outbound packets since the current
        /// send session was started. This counts data bytes that have already been successfully transmitted as well as
        /// those data bytes that have been added to a packet that is still waiting for completion. Header bytes, padding bytes,
        /// checksum bytes (any overhead incurred by the XModem infrastructure) are NOT included.
        ///
        /// This allows the user, for example, to calculate the proper file offset if reading an extremely large file
        /// from persistent storage.
        /// </summary>
        public int TotalUserDataBytesPacketized
        {
            get { return _TotalUserDataBytesPacketized; }
        }

        private int _TotalUserDataBytesSent = 0;
        /// <summary>
        /// Returns the total number of user data bytes that have been successfully transmitted since the current send session
        /// was started. Only data bytes are included in this count. Header bytes, padding bytes, checksum bytes (any overhead
        /// incurred by the XModem infrastructure) are NOT included.
        ///
        /// When the transfer has terminated, this can also tell the user if the file was successfully sent in its entirety.
        /// </summary>
        public int TotalUserDataBytesSent
        {
            get { return _TotalUserDataBytesSent; }
        }

        /// <summary>
        /// Sentinel variable that verifies that Send() method is called before AddToOutboundPacket().
        /// </summary>
        private bool SenderInitialized = false;

        /// <summary>
        /// Assembles one or more packets from the bytes passed to this method and automatically transmits them when the
        /// packet size has been satisfied.
        ///
        /// The number of bytes supplied by the user during each call may be completely arbitrary. Supplied arguments may be
        /// extremely short or extremely long. This method automatically parses the supplied byte arrays and builds packets
        /// on the fly. Once enough data has been received for a packet, that packet is automatically transmitted.
        ///
        /// This method is useful when reading a very large file piece-by-piece from a source, and for packetizing those
        /// pieces automatically.
        /// </summary>
        /// <param name="dataToSend">
        /// The data bytes that should be added to the current pending packet. This array may be any length.
        /// </param>
        /// <returns>
        /// The number of data bytes successfully sent during this particular method call.
        /// This is 0 if the packet is still too short to send using the data bytes collected thus far. By knowing how many data
        /// bytes were successfully transmitted during this method invocation, the user can calculate the correct file offset
        /// if the source file is extremely lage and is being read from persistent storage, for example.
        ///
        /// Note that only the data bytes successfully transmitted during THIS method call are counted. Use the
        /// TotalUserDataBytesSent property to keep track of the accumulated total of all user data bytes sent during the
        /// current send session.
        /// </returns>
        public int AddToOutboundPacket(byte[] dataToSend)
        {
            // Tracks the number of user data bytes that have been successfully sent during this method invocation
            int numUnpaddedDataBytesSentThisCall = 0;

            // Ensure that the Send() method is first called before this method
            if (SenderInitialized == false)
            {
                throw new ArgumentException("The XMODEM.Send() method must first be called before XMODEM.AddToOutboundPacket() is used.");
            }

            int dataOffset = 0;
            while (dataOffset < dataToSend.Length && TerminateSend == false)
            {

                // Instantiate outbound data packet if empty
                if (DataPacketToSend == null)
                {
                    if (_Variant == Variants.XModem1K)
                        DataPacketToSend = new byte[_Packet1024NominalSize];
                    else
                        DataPacketToSend = new byte[_Packet128NominalSize];

                    Array.Copy(SenderDataPacketMasterTemplate, DataPacketToSend, DataPacketToSend.Length);
                }

                int numUnparsedDataBytes = dataToSend.Length - dataOffset;
                int numPacketDataBytesNeeded = DataPacketToSend.Length - NumUserDataBytesAddedToCurrentPacket;

                int numBytesToAdd;
                if (numPacketDataBytesNeeded >= numUnparsedDataBytes)
                    numBytesToAdd = numUnparsedDataBytes;
                else
                    numBytesToAdd = numPacketDataBytesNeeded;

                Array.Copy(dataToSend, dataOffset, DataPacketToSend, NumUserDataBytesAddedToCurrentPacket, numBytesToAdd);

                NumUserDataBytesAddedToCurrentPacket += numBytesToAdd;
                dataOffset += numBytesToAdd;
                _TotalUserDataBytesPacketized += numBytesToAdd;

                if (NumUserDataBytesAddedToCurrentPacket >= DataPacketToSend.Length)
                {
                    TransmitPacket();

                    // Determine if packet transmission was successful, or the maximum number of retries has been exhausted:
                    if (PacketSuccessfullySent == true)
                    {
                        // If packet successfully transmitted, keep a running tally of data bytes sent out.
                        // Only count actual user-provided data. Padding bytes are not counted.
                        _TotalUserDataBytesSent += NumUserDataBytesAddedToCurrentPacket;
                        numUnpaddedDataBytesSentThisCall += NumUserDataBytesAddedToCurrentPacket;
                       
                        // Reset control variables
                        NumUserDataBytesAddedToCurrentPacket = 0;

                        // Re-initialize a new packet
                        DataPacketToSend = null;
                    }
                    else if (TerminateSend == false)
                    {
                        // Terminal condition if ACK not received even after multiple attempts
                        Abort();
                        _TerminationReason = TerminationReasonEnum.TooManyRetries;
                        break;
                    }
                }
            }

            return numUnpaddedDataBytesSentThisCall;
        }

        private bool PacketSuccessfullySent = false;

        private int _SenderConsecutiveRetryAttempts = 0;
        public int SenderConsecutiveRetryAttempts
        {
            get { return _SenderConsecutiveRetryAttempts; }
        }

        private void TransmitPacket()
        {
            // Calculate check-value
            byte[] checkValueBytes;
            if (_Variant == Variants.XModemChecksum)
                checkValueBytes = new byte[] { CheckSum(DataPacketToSend) };
            else
            {
                ushort checkValueShort = CRC.ComputeChecksum(DataPacketToSend);
                checkValueBytes = UShortToBytes(checkValueShort);
            }

            // Determine packet size header
            byte packetSizeHeader;
            if (_Variant == Variants.XModem1K)
                packetSizeHeader = STX;
            else
                packetSizeHeader = SOH;

            PacketSuccessfullySent = false;
            while (PacketSuccessfullySent == false && _SenderConsecutiveRetryAttempts < MaxSenderRetries && TerminateSend == false)
            {
                // Send packet size header
                Port.Write(new byte[] { packetSizeHeader }, 0, 1);

                // Send block number
                Port.Write(new byte[] { _BlockNumToSend }, 0, 1);

                // Send block number complement
                Port.Write(new byte[] { (byte)(255 - _BlockNumToSend) }, 0, 1);

                // Send data packet
                Port.Write(DataPacketToSend, 0, DataPacketToSend.Length);

                // Update state. Do this just before the Receiver is expected to respond so its response
                // doesn't fall through the cracks if it replies extremely quickly.
                WaitForResponseFromReceiver.Reset();
                CurrentState = States.SenderPacketSent;

                // Send check-value. This completes the packet. A response from the Receiver is expected after this.
                Port.Write(checkValueBytes, 0, checkValueBytes.Length);

                // Wait for ACK or NAK or CAN
                WaitForResponseFromReceiver.WaitOne();
                WaitForResponseFromReceiver.Reset();

                // Once we've gotten a response, the only message expected from the Receiver at this point is a possible cancellation
                CurrentState = States.SenderAlertForPossibleCancellation;
            }
        }

        /// <summary>
        /// Determines if the minimum number of consecutive cancellation bytes are present in a byte array.
        /// </summary>
        /// <param name="recv">
        /// The byte array to search for consecutive cancellation bytes.
        /// </param>
        /// <returns>
        /// True if cancellation condition has been met.
        /// False if cancellation request is absent.
        /// </returns>
        private bool DetectCancellation(byte[] recv)
        {
            if (NumCancellationBytesRequired > 0)
            {
                int foundIndex = Array.IndexOf(recv, CAN);
                if (foundIndex > -1)
                {
                    for (int indexToCheck = foundIndex; indexToCheck < recv.Length; indexToCheck++)
                    {
                        // If more than 1 CAN byte is required for cancellation, they must be consecutive
                        // for cancellation to occur.
                        if (recv[indexToCheck] == CAN)
                            _NumCancellationBytesReceived += 1;
                        else
                            _NumCancellationBytesReceived = 0;

                        if (_NumCancellationBytesReceived >= NumCancellationBytesRequired)
                        {
                            Abort();
                            _TerminationReason = TerminationReasonEnum.CancelNotificationReceived;
                            return true;    // Cancellation detected
                        }
                    }
                }
                else
                {
                    _NumCancellationBytesReceived = 0;
                }
            }
            return false;   // No cancellation detected
        }

        private bool EndOfFileAcknowledgementReceived = false;

        /// <summary>
        /// Informs the Receiver that the transmitted file is complete.
        /// Any pending packets are sent, followed by the end-of-file byte.
        /// </summary>
        /// <returns>
        /// The number of user data bytes successfully transmitted during this method call.
        /// </returns>
        public int EndFile()
        {
            // Check if there are unsent data bytes remaining in a pending packet, and if so, send the packet
            // containing the unsent bytes
            if (NumUserDataBytesAddedToCurrentPacket > 0)
            {
                TransmitPacket();
                _TotalUserDataBytesSent += NumUserDataBytesAddedToCurrentPacket;
            }

            CurrentState = States.SenderAwaitingEndOfFileConfirmation;

            int numEndOfFileBytesSent = 0;
            while (EndOfFileAcknowledgementReceived == false && numEndOfFileBytesSent <= MaxSenderRetries)
            {
                WaitForResponseFromReceiver.Reset();
                Port.Write(new byte[] { EndOfFileByteToSend }, 0, 1);
                numEndOfFileBytesSent += 1;
                WaitForResponseFromReceiver.WaitOne();
            }

            if (EndOfFileAcknowledgementReceived == true)
            {
                Abort();
                _TerminationReason = TerminationReasonEnum.EndOfFile;
                return NumUserDataBytesAddedToCurrentPacket;
            }
            else
            {
                Abort();
                _TerminationReason = TerminationReasonEnum.TooManyRetries;
                return 0;
            }
        }

        private void SenderPacketResponseWatchdogRoutine(object notUsed)
        {
            // <ACK>, <NAK> or <CAN> is expected from the Receiver after each packet sent.
            // If an appropriate response is not received within the expected timeout, release the WaitHandle which is enforcing
            // the wait-for-response. This will make the Sender resend the packet once more (until the retry limit is reached).
            if (CurrentState == States.SenderPacketSent)
                WaitForResponseFromReceiver.Set();
        }

        private void ResetSenderPacketResponseWatchdog()
        {
            if (SenderPacketResponseWatchdog != null)
                SenderPacketResponseWatchdog.Change(SenderPacketRetryTimeoutMillisec, SenderPacketRetryTimeoutMillisec);
        }

        private void ReceiverStillAliveWatchdogRoutine(object notUsed)
        {
            Abort();
            _TerminationReason = TerminationReasonEnum.NoResponseFromReceiver;
        }

        private void ResetReceiverStillAliveWatchdog()
        {
            if (ReceiverStillAliveWatchdog != null)
                ReceiverStillAliveWatchdog.Change(SendInactivityTimeoutMillisec, SendInactivityTimeoutMillisec);
        }

        /// <summary>
        /// Removes one or more padding bytes that may exist at the end of a byte array.
        /// </summary>
        /// <param name="input">
        /// The byte array to trim.
        /// </param>
        /// <param name="paddingByteToRemove">
        /// The byte value which defines a padding byte.
        /// If omitted, this defaults to SUB (byte decimal 26).
        /// </param>
        /// <returns>
        /// A byte array without trailing padding bytes (if any are found).
        /// </returns>
        public byte[] TrimPaddingBytesFromEnd(byte[] input, byte paddingByteToRemove = 26)
        {
            int numBytesToDiscard = 0;

            for (int k = input.Length - 1; k >= 0; k--)
            {
                if (input[k] == paddingByteToRemove)
                    numBytesToDiscard += 1;
                else
                    break;
            }

            int numBytesToKeep = input.Length - numBytesToDiscard;

            byte[] output = new byte[numBytesToKeep];
            Array.Copy(input, output, numBytesToKeep);

            return output;
        }

        /// <summary>
        /// Concatenates two byte arrays and returns the combined array.
        /// </summary>
        /// <param name="Array1">The 1st byte array to concatenate.</param>
        /// <param name="Array2">The 2nd byte array to concatenate.</param>
        /// <returns>
        /// The combined byte array which consists of { Array1, Array2 }.
        /// </returns>
        private byte[] CombineArrays(byte[] Array1, byte[] Array2)
        {
            int length1 = Array1.Length;
            int length2 = Array2.Length;

            byte[] combinedArray = new byte[length1 + length2];
            Array1.CopyTo(combinedArray, 0);
            Array2.CopyTo(combinedArray, length1);

            return combinedArray;
        }

    } // End class

    /// <summary>
    /// Calculates CRC-16 CCITT checksum using polynomial (X^16 + X^12 + X^5 + 1).
    /// </summary>
    public class CRC16CCITT
    {
        const ushort poly = 4129;
        ushort[] table = new ushort[256];
        ushort initialValue = 0;

        public ushort ComputeChecksum(byte[] bytes)
        {
            ushort crc = this.initialValue;
            for (int i = 0; i < bytes.Length; ++i)
            {
                crc = (ushort)((crc << 8) ^ table[((crc >> 8) ^ (0xff & bytes[i]))]);
            }
            return crc;
        }

        public byte[] ComputeChecksumBytes(byte[] bytes)
        {
            ushort crc = ComputeChecksum(bytes);
            return BitConverter.GetBytes(crc);
        }

        public enum InitialCrcValue { Zeros, NonZero1 = 0xffff, NonZero2 = 0x1D0F }

        public CRC16CCITT(InitialCrcValue initialValue)
        {
            this.initialValue = (ushort)initialValue;
            ushort temp, a;
            for (int i = 0; i < table.Length; ++i)
            {
                temp = 0;
                a = (ushort)(i << 8);
                for (int j = 0; j < 8; ++j)
                {
                    if (((temp ^ a) & 0x8000) != 0)
                    {
                        temp = (ushort)((temp << 1) ^ poly);
                    }
                    else
                    {
                        temp <<= 1;
                    }
                    a <<= 1;
                }
                table[i] = temp;
            }
        }
    }

} // End namespace