Geeks With Blogs
The Life and Times of a Dev Yes, we're really that weird

Today, I had to write an XModem implementation to upload fonts to an ImTech Mark 605 print controller.  The binary upload wasn’t working as expected, which left the XModem implementation, which does seem to work as expected.

These print controllers are really nice and are enabling us to do some pretty cool stuff, which I can’t describe here.

Before creating my own implementation, I spent quite a bit of time searching.  The stuff you find on XModem tends to not be terribly helpful.  Eventually, I stumbled upon the Poderosa project and their implementation of XModem, which works well and I was able to adapt to what I needed.

If you find this helpful, please let me know.  If you’re Microsoft letting me know that my computer is infected with a virus and that if I’ll just let you install some software on my computer you’ll make everything o.k., go jump in a lake. . .

Here’s what I came up with (once Open Live Writer plug-ins are back, I’ll try and do better formatted code):

// -----------------------------------------------------------------------
//    <copyright file="XModemSender.cs" company="VendRx, Inc.">
//        Copyright (c) VendRx, Inc. 2015, also released under the Apache License, version 2.0
//    </copyright>
//    <summary>
//      This is adapted from the Poderosa project:  https://raw.githubusercontent.com/poderosaproject/poderosa/master/XZModem/xmodem.cs, which is under the Apache 2.0 license.
/*
* Copyright 2004,2006 The Poderosa Project.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* $Id: xmodem.cs,v 1.2 2011/10/27 23:21:59 kzmi Exp $
*/
//    </summary>
// ---------------------------------------------------------------------

using System;

using System.IO.Ports;
using System.Threading.Tasks;

public class XModemSender
{
    private const byte ACK = 6;
    private const byte CAN = 24;
    private const int ChecksumLength = 128;
    private const byte CRC = (byte)'C';
    private const int CrcLength = 1024;
    private const byte EOT = 4;
    private const byte NAK = 21;
    private const byte SOH = 1;
    private const byte STX = 2;
    private const byte SUB = 26;

    private bool CrcEnabled { get; set; }

    private int CurrentOffset { get; set; }

    private byte[] Data { get; set; }

    private bool Negotiating { get; set; }

    private int NextOffset { get; set; }

    private int PacketLength { get; set; }

    private int RetryCount { get; set; }

    private bool Sending { get; set; }

    private int SequenceNumber { get; set; }

    private SerialPort SerialPort { get; set; }

    private bool Success { get; set; }

    /// <summary>
    ///     Sends the specified byte[] using the xmodem protocol.
    /// </summary>
    /// <param name="serialPort">The serial port that will receive the data.</param>
    /// <param name="data">The data to be sent.</param>
    /// <returns>True if data is sent successfully, false otherwise.</returns>
    public bool SendData(SerialPort serialPort, byte[] data)
    {
        this.SerialPort = serialPort;
        this.Data = data;

        int receivedBytes = this.SerialPort.ReceivedBytesThreshold;
        this.SerialPort.ReceivedBytesThreshold = 1;
        this.Sending = true;
        this.Success = false;
        this.CrcEnabled = false;
        this.CurrentOffset = 0;
        this.SequenceNumber = 0;

        try
        {
            Task task = Task.Run(() => this.Send());

            this.WaitForState(() => !this.Sending, 60000, 10);

            if (task.IsFaulted)
            {
                throw task.Exception;
            }
        }
        finally
        {
            this.SerialPort.DataReceived -= this.SerialPortDataReceived;
            this.SerialPort.ReceivedBytesThreshold = receivedBytes;
        }

        return this.Success;
    }

    /// <summary>
    /// Waits for the expression to be true or for a timeout.
    /// </summary>
    /// <param name="value">The value.</param>
    /// <param name="expression">The expression to evaluate.</param>
    /// <param name="timeout">How long to wait.</param>
    /// <param name="sleepTime">The amount of time to sleep between checks of the expression.</param>
    /// <returns>True if timed out, false otherwise.</returns>
    private static bool WaitForState(this object value, Func<bool> expression, int timeout, int sleepTime)
    {
        DateTime startTime = DateTime.Now;
        while (startTime.AddMilliseconds(timeout) > DateTime.Now && expression.Invoke() == false)
        {
            Thread.Sleep(sleepTime);
        }

        return !expression.Invoke();
    }

    private static ushort CalcCRC(byte[] data, int offset, int length)
    {
        ushort crc = 0;
        for (int i = 0; i < length; i++)
        {
            byte d = data[offset + i];
            crc ^= (ushort)(d << 8);
            for (int j = 1; j <= 8; j++)
            {
                if ((crc & 0x8000) != 0)
                {
                    crc = (ushort)((crc << 1) ^ 0x1021);
                }
                else
                {
                    crc <<= 1;
                }
            }
        }
        return crc;
    }

    private void Send()
    {
        this.SerialPort.DataReceived += this.SerialPortDataReceived;
        this.Negotiating = true;
    }

    private void SendPacket()
    {
        if (this.CurrentOffset >= this.Data.Length)
        {
            byte[] end = new byte[1];
            end[0] = EOT;
            this.SerialPort.Write(end, 0, 1);
            this.CurrentOffset = this.Data.Length;
        }
        else
        {
            int packetLength = ChecksumLength;

            if (this.CrcEnabled && this.CurrentOffset + 1024 <= this.Data.Length)
            {
                packetLength = CrcLength;
            }

            byte[] buffer = new byte[3 + packetLength + (this.CrcEnabled ? 2 : 1)];
            buffer[0] = packetLength == ChecksumLength ? SOH : STX;
            buffer[1] = (byte)this.SequenceNumber;
            buffer[2] = (byte)(255 - buffer[1]);
            int dataLength = Math.Min(packetLength, this.Data.Length - this.CurrentOffset);
            Array.Copy(this.Data, this.CurrentOffset, buffer, 3, dataLength);
            for (int i = dataLength; i < packetLength; i++)
            {
                buffer[3 + i] = 26;
            }

            if (this.CrcEnabled)
            {
                ushort sum = CalcCRC(buffer, 3, packetLength);
                buffer[3 + packetLength] = (byte)(sum >> 8);
                buffer[3 + packetLength + 1] = (byte)(sum & 0xFF);
            }
            else
            {
                byte sum = 0;
                for (int i = 0; i < packetLength; i++)
                {
                    sum += buffer[3 + i];
                }

                buffer[3 + packetLength] = sum;
            }

            this.NextOffset = this.CurrentOffset + packetLength;

            this.SerialPort.DiscardInBuffer();
            this.SerialPort.Write(buffer, 0, buffer.Length);
        }
    }

    private void SerialPortDataReceived(object sender, SerialDataReceivedEventArgs e)
    {
        byte[] data = new byte[this.SerialPort.BytesToRead];
        this.SerialPort.Read(data, 0, data.Length);

        if (this.Negotiating)
        {
            if (data[0] == NAK || data[0] == CRC)
            {
                this.CrcEnabled = data[0] == CRC;
                this.PacketLength = this.CrcEnabled ? CrcLength : ChecksumLength;
                this.Negotiating = false;
                this.SequenceNumber = 1;
                this.CurrentOffset = 0;
                this.NextOffset = 0;

                this.SendPacket();
            }
        }
        else if (data[0] == ACK && this.CurrentOffset == this.Data.Length)
        {
            this.Success = true;
            this.Sending = false;
        }
        else if (data[0] == ACK)
        {
            this.RetryCount = 0;
            this.SequenceNumber++;
            this.CurrentOffset = this.NextOffset;
            this.SendPacket();
        }
        else if (data[0] == NAK)
        {
            this.RetryCount++;
            if (this.RetryCount > 5)
            {
                throw new Exception("Failed to upload data 5 times in a row.");
            }

            this.SendPacket();
        }
    }
}

Posted on Thursday, December 10, 2015 4:48 PM Serial Port | Back to top

Related Posts on Geeks With Blogs Matching Categories

Comments on this post: XModem Implementation

No comments posted yet.
Your comment:
 (will show your gravatar)


Copyright © Robert May | Powered by: GeeksWithBlogs.net