TommyPROM/TommyPROM/XModem.cpp

371 lines
11 KiB
C++

#include "XModem.h"
#include "CmdStatus.h"
// The original TommyPROM code used XModem CRC protocol but this requires parameters to be
// changed for the host communication program. The default is now to use basic XModem
// with the 8-bit checksum instead of the 16-bit CRC error checking. This shouldn't
// matter because communication errors aren't likely to happen on a 3 foot USB cable like
// they did over the long distance dail-up lines that XModem was designed for. Uncomment
// the XMODEM_CRC_PROTOCOL line below to restore the original XMODEM CRC support.
//#define XMODEM_CRC_PROTOCOL
enum
{
// XMODEM control characters.
XMDM_SOH = 0x01,
XMDM_EOT = 0x04,
XMDM_ACK = 0x06,
XMDM_NAK = 0x15,
XMDM_CAN = 0x18,
XMDM_ESC = 0x1b,
XMDM_CRC = 'C',
#ifdef XMODEM_CRC_PROTOCOL
XMDM_TRANSFER_START = XMDM_CRC
#else
XMDM_TRANSFER_START = XMDM_NAK
#endif
};
enum
{
// Misc constants for XMODEM.
PKTLEN = 128
};
enum { RX_OK, RX_ERROR, RX_IGNORE };
uint32_t XModem::ReceiveFile(uint32_t address)
{
uint8_t buffer[PKTLEN];
int c;
uint8_t seq = 1;
uint32_t numBytes = 0;
bool complete = false;
int status;
#ifdef XMODEM_CRC_PROTOCOL
Serial.println(F("Send the image file using XMODEM CRC"));
#else
Serial.println(F("Send the image file using XMODEM"));
#endif
if (!StartReceive())
{
cmdStatus.error("Timeout waiting for transfer to start.");
return 0;
}
while (!complete)
{
if ((c = GetChar()) < 0)
{
cmdStatus.error("Timeout waiting for start of next packet.");
cmdStatus.setValueDec(0, "seq", seq);
return 0;
}
switch (c)
{
case XMDM_SOH:
// Start of a packet
status = ReceivePacket(buffer, PKTLEN, seq, address);
if (status == RX_OK) {
// Good packet - advance the buffer
numBytes += PKTLEN;
address += PKTLEN;
++seq;
} else if (status == RX_ERROR) {
return 0;
}
// else ignore the duplicate packet
break;
case XMDM_EOT:
// End of transfer
Serial.write(XMDM_ACK);
complete = true;
break;
case XMDM_CAN:
case XMDM_ESC:
// Cancel from sender
cmdStatus.error("Transfer canceled by sender.");
cmdStatus.setValueDec(0, "seq", seq);
return 0;
break;
default:
// Fail the transfer on anything else
cmdStatus.error("Unexpected character received waiting for next packet.");
cmdStatus.setValueDec(0, "char", c);
cmdStatus.setValueDec(1, "seq", seq);
return 0;
break;
}
}
return numBytes;
}
// This method it not very tolerant of communication errors. If the receiver
// does not send a positive ACK for each packet or does not ACK the packet
// within one second then the transfer will fail. Unlike in the dial-up
// days of old, this is designed to be run on a 3 foot cable betwee two fast
// hosts, so communication errors or timeouts are extremely unlikely.
bool XModem::SendFile(uint32_t address, uint32_t fileSize)
{
uint8_t seq = 1;
int rxChar = -1;
uint32_t bytesSent = 0;
#ifdef XMODEM_CRC_PROTOCOL
Serial.println(F("Set the terminal to receive XModem CRC"));
#else
Serial.println(F("Set the terminal to receive XModem"));
#endif
while (rxChar == -1)
{
rxChar = GetChar();
}
if (rxChar != XMDM_TRANSFER_START)
{
#ifdef XMODEM_CRC_PROTOCOL
cmdStatus.error("Expected XModem CRC start char");
#else
cmdStatus.error("Expected XModem NAK char to start");
#endif
cmdStatus.setValueDec(0, "char", rxChar);
return false;
}
while (bytesSent < fileSize)
{
SendPacket(address, seq++);
address += PKTLEN;
rxChar = GetChar(5000);
if (rxChar != XMDM_ACK)
{
cmdStatus.error("Expected XModem ACK.");
cmdStatus.setValueDec(0, "char", rxChar);
return false;
}
bytesSent += PKTLEN;
}
// Signal end of transfer and process the ACK
Serial.write(XMDM_EOT);
rxChar = GetChar(5000);
if (rxChar != XMDM_ACK)
{
cmdStatus.error("Expected XModem ACK to EOT, but received:");
cmdStatus.setValueDec(0, "char", rxChar);
return false;
}
return true;
}
void XModem::Cancel()
{
// Send a cancel and then eat input until the line is quiet for 3 seconds.
Serial.write(XMDM_CAN);
while (GetChar(3000) != -1)
{}
}
// Private functions
int XModem::GetChar(int msWaitTime)
{
msWaitTime *= 4;
do
{
if (Serial.available() > 0)
{
return Serial.read();
}
delayMicroseconds(250);
} while (msWaitTime--);
return -1;
}
uint16_t XModem::UpdateCrc(uint16_t crc, uint8_t data)
{
#ifdef XMODEM_CRC_PROTOCOL
crc = crc ^ ((uint16_t)data << 8);
for (int ix = 0; (ix < 8); ix++)
{
if (crc & 0x8000)
{
crc = (crc << 1) ^ 0x1021;
}
else
{
crc <<= 1;
}
}
#else
crc = (crc + data) & 0xff;
#endif
return crc;
}
bool XModem::StartReceive()
{
delay(2000);
for (int retries = 20; (retries); --retries)
{
// Send the XMDM_TRANSFER_START until the sender of the file responds with
// something. The start character will be sent once a second for a number of
// seconds. If nothing is received in that time then return false to indicate
// that the transfer did not start.
++promDevice.debugRxStarts;
Serial.write(XMDM_TRANSFER_START);
for (unsigned ms = 30000; (ms); --ms)
{
delayMicroseconds(100);
if (Serial.available() > 0)
{
return true;
}
}
}
return false;
}
int XModem::ReceivePacket(uint8_t buffer[], unsigned bufferSize, uint8_t seq, uint32_t destAddr)
{
int c;
uint8_t rxSeq1, rxSeq2;
uint16_t calcCrc = 0;
uint16_t rxCrc;
rxSeq1 = (uint8_t) GetChar();
rxSeq2 = (uint8_t) GetChar();
for (unsigned ix = 0; (ix < bufferSize); ix++)
{
if ((c = GetChar()) < 0)
{
// If the read times out then fail this packet. Note that this check isn't
// done for the sequence and CRC. If they timeout then the values won't match
// so there is not point in the extra code to check for the error. The worst
// that will happen is that the transfer will need to wait 3 timeouts before
// realizing that something is wrong.
cmdStatus.error("Timeout waiting for next packet char.");
cmdStatus.setValueDec(0, "seq", seq);
Serial.write(XMDM_CAN);
return RX_ERROR;
}
buffer[ix] = (uint8_t) c;
calcCrc = UpdateCrc(calcCrc, buffer[ix]);
}
#ifdef XMODEM_CRC_PROTOCOL
rxCrc = ((uint16_t) GetChar()) << 8;
rxCrc |= GetChar();
#else
rxCrc = GetChar();
#endif
// At this point in the code, there should not be anything in the receive buffer
// because the sender has just sent a complete packet and is waiting on a response. If
// the XModem transfer is not started quickly on the host side, TeraTerm seems to
// buffer the NAK character that starts the transfer and use it as an indication that
// the first packet should be sent again. That can cause the Arduino's 64 byte buffer
// to overflow and gets the transfer out of sync. The normal processing will detect
// the first packet and will process it correctly, but the partial packet in the
// buffer will cause the processing of the next packet to fail.
delay(10);
if (Serial.available()) {
++promDevice.debugRxSyncErrors;
if (seq == 1) {
// If any characters are in the buffer after the first packet, quietly eat
// them to get back on a packet boundary and send a NAK to cause a retransmit
// of the packet.
while (Serial.available() > 0) {
delay(1);
Serial.read();
++promDevice.debugExtraChars;
}
delay(1);
Serial.write(XMDM_NAK);
return RX_IGNORE;
} else {
// Sync issues shouldn't happen at any other time so fail the transfer.
cmdStatus.error("RX sync error.");
cmdStatus.setValueDec(0, "seq", seq);
cmdStatus.setValueDec(1, "rxSeq1", rxSeq1);
cmdStatus.setValueDec(2, "rxSeq2", rxSeq2);
cmdStatus.setValueHex(3, "calcCrc", calcCrc);
cmdStatus.setValueHex(4, "rxCrc", rxCrc);
Serial.write(XMDM_CAN);
return RX_ERROR;
}
}
if ((calcCrc == rxCrc) && (rxSeq1 == seq - 1) && ((rxSeq1 ^ rxSeq2) == 0xff))
{
// Resend of previously processed packet.
delay(10);
++promDevice.debugRxDuplicates;
Serial.write(XMDM_ACK);
return RX_IGNORE;
}
else if ((calcCrc != rxCrc) || (rxSeq1 != seq) || ((rxSeq1 ^ rxSeq2) != 0xff))
{
// Fail if the CRC or sequence number is not correct or if the two received
// sequence numbers are not the complement of one another.
cmdStatus.error("Bad CRC or sequence number.");
cmdStatus.setValueDec(0, "seq", seq);
cmdStatus.setValueDec(1, "rxSeq1", rxSeq1);
cmdStatus.setValueDec(2, "rxSeq2", rxSeq2);
cmdStatus.setValueHex(3, "calcCrc", calcCrc);
cmdStatus.setValueHex(4, "rxCrc", rxCrc);
Serial.write(XMDM_CAN);
return RX_ERROR;
}
else
{
// The data is good. Process the packet then ACK it to the sender.
if (!promDevice.writeData(buffer, bufferSize, destAddr))
{
cmdStatus.error("Write failed");
cmdStatus.setValueHex(0, "address", destAddr);
return RX_ERROR;
}
Serial.write(XMDM_ACK);
}
return RX_OK;
}
void XModem::SendPacket(uint32_t address, uint8_t seq)
{
uint16_t crc = 0;
Serial.write(XMDM_SOH);
Serial.write(seq);
Serial.write(~seq);
for (int ix = 0; (ix < PKTLEN); ix++)
{
byte c = promDevice.readData(address++);
Serial.write(c);
crc = UpdateCrc(crc, c);
}
#ifdef XMODEM_CRC_PROTOCOL
Serial.write(crc >> 8);
#endif
Serial.write(crc & 0xff);
}