/// /* * ZmodemReceiver - receive files using zmodem. * * A c# class developed as a library for conducting serial interface * file transfers, developed by Chuck Forsberg. This code is strongly * based on the lrzsz linux library, and is an expanded port of that * code, released under * GNU GPL. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA * 02111-1307, USA. * * Developed by Eric Martin MBARI 2016 * */ using System; using System.Text; using System.Threading; using System.IO; using System.IO.Ports; using System.Diagnostics; using Microsoft.SPOT; using Microsoft.SPOT.IO; namespace CPF.FileTransfer { public class ZmodemReceiver : Zmodem { //Constants private const int ZRQ_MAX = 10; private const UInt16 MAX_BLOCK = 8192; private UInt16 MAX_TXFRAME = 1024; //Variables private SerialPort sPort; private byte Lzmanag; /*Local File Management Request*/ private bool rxClob; private bool rxBinary; private bool rxASCII; private bool ThisBinary; private bool renameExistingFiles; private bool zctlEsc; private bool skipifnotfound; private STATES state; private int zrTimeout = 2; private byte tryzhdrtype = ZRINIT; private FileMode openmode; private byte zconv; //ZMODEM file conversion request; private byte zmanag; //ZMODEM file management request; private byte ztrans; //ZMODEM file transport request; private byte[] rxbuff; public string filePrefix = ""; //STATES private enum STATES { INIT, // Waiting for any message from the transmitter RECVFILE, DONE, TIMED_OUT }; public enum ManageOpts { NEWORLONGER = 1, // Transfer if source is newer or longer CRC, // Transfer if different file CRC or length APPEND, // Append Contents to Existing FIle CLOBBER, // Replace Existing File NEWER // Transfer if source is newer }; //Properties /// Use 16BIT Crc in reply messages public bool CrcEnable; /// Disable timeouts during public bool DisableTimeouts; /// Enable the resume file functionality. public bool ResumeEnable; /// /// Enable Clobbering of the Received File, disables ResumeEnable if true. /// public bool RxClob { get { return rxClob; } set { if (value) ResumeEnable = true; rxClob = value; } } /// Sets unit to receive data in ascii /// format, disables binary transfer. public bool RxASCII { get { return rxASCII; } set { rxBinary = !value; rxASCII = value; } } /// Sets unit to receive data in binary /// format, disables ascii transfer. public bool RxBinary { get { return rxBinary; } set { rxASCII = (!value); rxBinary = value; } } /// Enable/Disable Escape sequences /// for control characters. public bool ZctlEsc { get { return zctlEsc; } set { zctlEsc = value; Init(zctlEsc); } } /// /// Rename any files already existing, this disables /// file resume functionality. /// public bool RenameExistingFiles { get { return renameExistingFiles; } set { renameExistingFiles = value; if (value) ResumeEnable = false; } } /// Set timeout in tenths of seconds. public int Timeout { get { return zrTimeout; } set { if (value == 0) DisableTimeouts = true; zrTimeout = value; } } public ManageOpts LocalManagement { get { return (ManageOpts) Lzmanag; } set { Lzmanag = (byte)value; } } /// /// Constructor for single or multiple file transfers through serialports using /// the ZMODEM protocol. /// /// SerialPort object. Must be open for r/w. /// Enable the ability to resume a file partially downloaded in a /// previous session. CRC on all packets will be checked. /// /// This is how you conduct a receive operation. Default property settings are: /// - No local management options /// - No CRC enabled (reduces bandwidth. /// - Standard timeouts enabled /// - Binary transmission /// - Skip if not found is disabled. /// /// SerialPort sp = new SerialPort("COM1", 115200, Parity.None, 8, StopBits.One); /// ZmodemReceiver zr = new ZmodemReceiver(sp, true); //allow resume functionality /// while (zr.doTasks()) { /// // this does not return regularly, just between phases of the transfer. /// } /// //Transfer Complete /// public ZmodemReceiver(SerialPort sp, bool tryResume) { this.Lzmanag = 0; // no local management options this.CrcEnable = false; this.DisableTimeouts = false; this.RxBinary = false; this.RxASCII = false; this.ThisBinary = false; this.skipifnotfound = false; this.rxbuff = new byte[MAX_BLOCK]; // this sets Init(false); this.ZctlEsc = false; this.sPort = sp; this.state = STATES.INIT; this.ResumeEnable = true; } private int zrqinits_received = 0; private int init_loops = 0; private UInt32 rxpos = 0; private ushort nbytes = 0; private bool pollOnNext = true; private int errors = 0; public bool doTasks() { int c; byte cmdzack1flg; switch (state) { case STATES.INIT: // Test if we're under the max zrq's and that we haven't timed out too many times. if ((init_loops - zrqinits_received) < ZRQ_MAX && (zrqinits_received < ZRQ_MAX)) { if (pollOnNext) { stohdr(1024); Txhdr[ZF0] |= CANFC32 | CANFDX| CANOVIO; //Txhdr[ZF0] = 0; //Txhdr[ZF1] = 0; //todo timesync flag if (zctlEsc) Txhdr[ZF0] |= TESCCTL; zshhdr(sPort, tryzhdrtype, ref Txhdr); init_loops++; if (tryzhdrtype == ZSKIP) tryzhdrtype = ZRINIT; pollOnNext = false; } switch (c = zgethdr(sPort, ref Rxhdr, 0, ref rxpos)) { case ZRQINIT: zrqinits_received++; pollOnNext = true; return true; case ZEOF: Debug.Print("GOT ZEOF"); pollOnNext = true; return true; case TIMEOUT: pollOnNext = true; return true; case ZFILE: //This is what we're looking for. zconv = Rxhdr[ZF0]; if ((Rxhdr[ZF1] & ZF1_ZMSKNOLOC) > 0) { Rxhdr[ZF1] = (byte)(Rxhdr[ZF1] & ~ZF1_ZMSKNOLOC); skipifnotfound = true; } zmanag = Rxhdr[ZF1]; ztrans = Rxhdr[ZF2]; tryzhdrtype = ZRINIT; // clear the rx array Array.Clear(rxbuff, 0, rxbuff.Length); nbytes = 0; c = zrdata(sPort, ref rxbuff, ref nbytes); if (c == GOTCRCW) { state = STATES.RECVFILE; return true; } zshhdr(sPort, ZNAK, ref Txhdr); return true; case ZSINIT: zctlEsc |= (TESCCTL & Rxhdr[ZF0]) != 0; nbytes = 0; if (zrdata(sPort, ref Attn, ref nbytes) == GOTCRCW) { stohdr(1); zshhdr(sPort, ZACK, ref Txhdr); return true; } zshhdr(sPort, ZNAK, ref Txhdr); return true; case ZFREECNT:// right now this doesn't work... stohdr(getfree()); zshhdr(sPort, ZACK, ref Txhdr); return true; case ZCOMMAND: //Not enabled in C# protocol cmdzack1flg = Rxhdr[ZF0]; zshhdr(sPort, ZACK, ref Txhdr); state = STATES.DONE; return true; case ZCOMPL: return true; case ZFIN: ackbibi(); state = STATES.DONE; return true; case ZRINIT: return false; case ZCAN: return false; default: return true; } } break; case STATES.RECVFILE: if (rzfiles(ref zi) == ERROR) return false; break; case STATES.TIMED_OUT: break; case STATES.DONE: Debug.Print("Transfer Session Complete"); return false; } return true; } private int rzfiles(ref zfileinfo zm) { int c; for (; ; ) { ///Debug.Print("rzfiles(): opening new file."); c = rzfile(ref zm); switch (c) { case ZEOF: //Debug.Print("rzfiles(): ZEOF received.\n"); state = STATES.INIT; pollOnNext = true; return c; case ZSKIP: //Some debug printing ... TODO state = STATES.INIT; pollOnNext = true; return c; default: return c; case ERROR: return c; } //if (state != STATES.RECVFILE) // break; } //TODO return OK; } private int rzfile(ref zfileinfo zm) { int c, n; //long last_rxbytes = 0; //ulong last_bps; //long not_printed = 0; //uint bytes_in_block = 0; zm.eof_seen = false; n = 10; if (procheader(ref rxbuff, nbytes, ref zm) == ERROR) { tryzhdrtype = ZSKIP; return ZSKIP; } if (processZFileInfo(ref zm) == ERROR) { tryzhdrtype = ZSKIP; return ZSKIP; } for (; ; ) { repos: stohdr(zm.bytes_received); zshhdr(sPort, ZRPOS, ref Txhdr); nxthdr: //wait for reply c = zgethdr(sPort, ref Rxhdr, 0, ref rxpos); switch (c) { default: Debug.Print("rzfile(): zgethdr error"); return ERROR; case ZNAK: case TIMEOUT: if (--n < 0) { Debug.Print("rzfile(): error: ZNAK or TIMEOUT"); return ERROR; } continue; case ZFILE: nbytes = 0; if (zrdata(sPort, ref rxbuff, ref nbytes) == ERROR) Debug.Print("rzfile(): error on zrdata from ZFILE header."); continue; case ZEOF: if (rclhdr(ref Rxhdr) != zm.bytes_received) { errors = 0; goto nxthdr; } //close file if () if (!closeit(ref zm)) { return ERROR; } return c; case ERROR: if (--n < 0) { return ERROR; } zmputs(ref Attn); continue; case ZSKIP: if (!closeit(ref zm)) return ERROR; Debug.Print("rzfile(): sender skipped file\r\n"); return c; case ZDATA: if (rclhdr(ref Rxhdr) != zm.bytes_received) { if (--n < 0) { Debug.Print("rzfile(): out of sync\r\n"); return ERROR; } zmputs(ref Attn); continue; } moredata: Debug.Print("BYTES SO FAR: " + zm.bytes_received + " of "+zm.bytes_total); // skipped a bunch of bps checks nbytes = 0; switch (c = zrdata(sPort, ref rxbuff, ref nbytes)) { case ZCAN: Debug.Print("rzfile(): zrdata returned ZCAN\r\n"); return ERROR; case ERROR: if (--n < 0) { Debug.Print("rzfile(): zrdata returned ERROR\r\n"); return ERROR; } else { //Dump the buffer Debug.Print("rzfile(): dumping buffer and sending new ZRPOS."); sPort.DiscardInBuffer(); zmputs(ref Attn); goto repos; } case TIMEOUT: if (--n < 0) { Debug.Print("rzfile(): zrdata returned TIMEOUT\r\n"); return ERROR; } else continue; case GOTCRCW: n = 20; if (putsec(ref zm, ref rxbuff, nbytes) == OK) { zm.bytes_received += nbytes; stohdr(zm.bytes_received); zshhdr(sPort, (ZACK | 0x80), ref Txhdr); goto nxthdr; } else { return ERROR; } case GOTCRCQ: n = 20; if (putsec(ref zm, ref rxbuff, nbytes) == OK) { zm.bytes_received += nbytes; stohdr(zm.bytes_received); zshhdr(sPort, (ZACK), ref Txhdr); goto moredata; } else { return ERROR; } case GOTCRCG: n = 20; if (putsec(ref zm, ref rxbuff, nbytes) == OK) { zm.bytes_received += nbytes; goto moredata; } else { return ERROR; } case GOTCRCE: n = 20; if (putsec(ref zm, ref rxbuff, nbytes) == OK) { zm.bytes_received += nbytes; stohdr(zm.bytes_received); zshhdr(sPort, (ZACK), ref Txhdr); goto nxthdr; } else { return ERROR; } default: Debug.Print("rzfile(): unhandled Reply"); break; } break; } } } /// /// Assumes the file is open, and writes a sector to it. /// /// /// /// /// private int putsec(ref zfileinfo zm, ref byte[] buff, ushort nbytes) { if (nbytes == 0) return OK; if (zm.fs == null) { Debug.Print("putsec(): File not open.\r\n"); return ERROR; } if (ThisBinary) { try { zm.fs.Write(buff, 0, nbytes); } catch (Exception e) { Debug.Print("Putsec(): writing binary data: " + e.ToString()); return ERROR; } } else { if (zm.eof_seen) return OK; for (int n = 0; n < nbytes; n++) { if (buff[n] == 0x0D) // \r continue; if (buff[n] == CPMEOF) { zm.eof_seen = true; return OK; } try { zm.fs.WriteByte(buff[n]); } catch (Exception e) { Debug.Print("Putsec(): writing ascii byte. " + e.ToString()); } } } return OK; } private void zmputs(ref byte[] s) { for (int n = 0; n < s.Length; n++ ) { if (s[n] == 0xDD | s[n] == 0xDE) sPort.Write(s, n, 1); else if (s[n] == 0xDE) Thread.Sleep(1000); else if (s[n] == 0xDD) { //sPort.BreakState = true; //Thread.Sleep(10); //sPort.BreakState = false; } } } private bool closeit(ref zfileinfo zm) { try { zm.fs.Close(); zm.fs = null; } catch { return false; } Debug.Print("Closing File:" + zm.fname); return true; } private ushort Readnum = MAX_BLOCK; private void ackbibi() { //Debug.Print("Ackbibi:"); Readnum = 1; stohdr(0); for (int n = 0; n < 3; n++) { //purgeline(0); zshhdr(sPort, ZFIN, ref Txhdr); switch (readline(sPort, 100)) { case 'O': readline(sPort, 1); /*Discard second 'O'*/ //Debug.Print("Transfer complete"); return; case RCDO: return; case TIMEOUT: default: break; } } } internal struct zfileinfo { public string fname; public DateTime modtime; public uint mode; public uint bytes_total; public uint bytes_sent; public uint bytes_received; public uint bytes_skipped; public uint bytes_remaining; public uint files_remaining; public uint serial_number; public bool eof_seen; public FileStream fs; } private zfileinfo zi; /// /// Process incoming file information header /// /// /// /// private int procheader(ref byte[] buff, uint nbytes, ref zfileinfo zm) { //string flags; int nullidx = Array.IndexOf(buff, (byte)0); if (nullidx <= 0) return ERROR; StringBuilder sb = new StringBuilder(); zm.fname = sb.Clear().Append(this.filePrefix).Append(Encoding.UTF8.GetChars(buff, 0, nullidx)).ToString() ; Debug.Print("Receiving File: " + zm.fname + "\r\n"); int nullidx2 = Array.IndexOf(buff, (byte)0, nullidx + 1); StringBuilder flags = new StringBuilder(); flags.Append(Encoding.UTF8.GetChars(buff, nullidx + 1, nullidx2 - nullidx - 1)); Debug.Print("GOT FLAGS: [" + flags.ToString() + "]\r\n"); //Parse the flags if (nullidx2 <= 0) { return OK; } int tokstart = nullidx + 1; int toklen = bytesToNextSpace(buff, tokstart); try { //Parse Length zm.bytes_total = uint.Parse(sb.Clear().Append(Encoding.UTF8.GetChars(buff, tokstart, toklen)).ToString()); sb.Clear(); tokstart += toklen + 1; if (tokstart >= nullidx2) return OK; //Parse Modification Date toklen = bytesToNextSpace(buff, tokstart); int modtime = convertOctalStringToDec(ref buff, tokstart, toklen); // convert from octal zm.modtime = UnixEpoch.AddSeconds(modtime); tokstart += toklen + 1; if (tokstart >= nullidx2) return OK; //Parse File Mode toklen = bytesToNextSpace(buff, tokstart); zm.mode = (UInt32)convertOctalStringToDec(ref buff, tokstart,toklen); tokstart += toklen + 1; if (tokstart >= nullidx2) return OK; //Parse Serial Number toklen = bytesToNextSpace(buff, tokstart); zm.serial_number = uint.Parse(sb.Clear().Append(Encoding.UTF8.GetChars(buff, tokstart, toklen)).ToString()); tokstart += toklen + 1; if (tokstart >= nullidx2) return OK; // Parse Files Remaining toklen = bytesToNextSpace(buff, tokstart); zm.files_remaining = uint.Parse(sb.Clear().Append(Encoding.UTF8.GetChars(buff, tokstart, toklen)).ToString()); tokstart += toklen + 1; if (tokstart >= nullidx2) return OK; // Parse Bytes Remaining toklen = bytesToNextSpace(buff, tokstart); zm.bytes_remaining = uint.Parse(sb.Clear().Append(Encoding.UTF8.GetChars(buff, tokstart, toklen)).ToString()); //There is one more field (file type, but that is not really used. } catch { return ERROR; } return OK; } private int convertOctalStringToDec(ref byte [] buff, int offset, int len) { int result=0; int pow = 0; int n; StringBuilder sb = new StringBuilder(); //Debug.Print("CONVERTING :" + sb.Clear().Append(SafeEncoding.UTF8.GetChars(buff, offset, len)).ToString()); for (n = len-1; n >= 0 ; n--) { result += (int)System.Math.Pow(8, pow++) * int.Parse(sb.Clear().Append(Encoding.UTF8.GetChars(buff, n + offset, 1)).ToString()); } //Debug.Print("RESULT: " + result.ToString()); return result; } private int buffersize = 32768; private int processZFileInfo(ref zfileinfo zm) { //Set default parameters openmode = FileMode.OpenOrCreate; ThisBinary = (!rxASCII) || rxBinary; if (Lzmanag > 0) zmanag = Lzmanag; if (!rxBinary && (zconv == ZCNL)) ThisBinary = false; if (zconv == ZCBIN) ThisBinary = true; if (ThisBinary && (zconv == ZCBIN) && ResumeEnable) zconv = ZCRESUM; if ((zmanag == ZF1_ZMAPND && zconv != ZCRESUM)) openmode = FileMode.Append; if (skipifnotfound) openmode = FileMode.Open; if ((zm.mode & UNIXFILE) != 0) ThisBinary = true; // Check for existing file if (zconv != ZCRESUM && !RxClob && (zmanag & ZF1_ZMMASK) != ZF1_ZMCLOB && (zmanag & ZF1_ZMMASK) != ZF1_ZMAPND && File.Exists(zm.fname) ) { //Look for newer or not if (zmanag == ZF1_ZMNEW || zmanag == ZF1_ZMNEWL) { bool isLonger = new System.IO.FileInfo(zm.fname).LastWriteTimeUtc > zm.modtime; if (zmanag == ZF1_ZMNEW) { if (isLonger) { Debug.Print("Skipping: " + zm.fname + ", newer file exists."); return ERROR; } } else { // newer or longer long fLength = new System.IO.FileInfo(zm.fname).Length; if (isLonger && fLength > zm.bytes_total) { Debug.Print("Skipping: " + zm.fname + ", newer and longer file exists."); return ERROR; } } } else if (zmanag == ZF1_ZMCRC) { int r = do_crc_check(ref zm, 0); if (r == ERROR) return ERROR; if (r != ZCRC_DIFFERS) { Debug.Print("Skipping, CRC match on local and remote files."); return ERROR; // skips } else { openmode = FileMode.Create; Debug.Print("CRC Mismatch, redownloading entire file."); } } else { if ((zmanag & ZF1_ZMMASK) != ZF1_ZMCHNG) { Debug.Print("Skipping: " + zm.fname + ", file exists"); return ERROR; } // try to rename int i = 1; do { if (!File.Exists(zm.fname + i.ToString())) { zm.fname += i.ToString(); break; } } while (i < 1000); if (File.Exists(zm.fname)) { Debug.Print("Skipping: " + zm.fname + ", file could not be renamed."); return ERROR; } } } // NOT IMPLEMENTING TIMESYNC // NOT IMPLEMENTING LOWERCASING // NOT IMPLEMENTING PIPE try { if (zm.fs == null) zm.fs = File.Open(zm.fname, openmode, FileAccess.ReadWrite); } catch { Debug.Print("processZFileInfo(): File open failed.\r\n"); } if (ThisBinary && zconv == ZCRESUM) { if (zm.fs != null) { bool can_resume = true; if (zmanag == ZF1_ZMCRC) { int r = do_crc_check(ref zm, zm.fs.Length); if (r == ERROR) { zm.fs.Close(); return ZFERR; } if (r == ZCRC_DIFFERS) { can_resume = false; } } if (zm.fs.Length > zm.bytes_total) { can_resume = false; } /* Retransfer Whole Blocks */ zm.bytes_skipped = (uint)((zm.fs.Length) & ~(1023)); if (can_resume) { try { zm.fs.Seek(zm.bytes_skipped, SeekOrigin.Begin); } catch { zm.fs.Close(); return ZFERR; } } else zm.bytes_skipped = 0; /*resume impossible, file has changed */ goto buffer_it; } zm.bytes_skipped = 0; } try { //Deal with directory structure if required string directoryName = Path.GetDirectoryName(zm.fname); if ((directoryName.Length) > 0 && (!Directory.Exists(directoryName))) Directory.CreateDirectory(directoryName); } catch { Debug.Print("processZfileInfo(): Cannot Create Directory for file, " + zm.fname + " \r \n"); } if (zm.fs == null) { Debug.Print("processZfileInfo(): Cannot open file, " + zm.fname + " \r \n"); return ERROR; } /* uint last_length = 0; if (buffersize == -1) { if (zm.bytes_total > last_length) { last_length = 0; } } if (buffersize != 0) { last_length = 32768; if (buffersize == -1) { if (zm.bytes_total > 0) last_length = zm.bytes_total; } else last_length = (uint)buffersize; //buffer '4096' byte space last_length = (last_length + 4095) & 0xFFFFF000; } */ // zm.fs = File.Open(zm.fname, openmode, FileAccess.ReadWrite); //} buffer_it: zm.bytes_received = zm.bytes_skipped; return OK; } private static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, 0); private const int ZCRC_DIFFERS = ERROR + 1; private const int ZCRC_EQUAL = ERROR + 2; private int do_crc_check(ref zfileinfo zm, long check_bytes) { UInt32 crc, rcrc; long n; int c; int t1 = 0, t2 = 0; bool iOpened; if (check_bytes > UInt32.MaxValue) { Debug.Print("do_crc_check(): filesize too big"); return ERROR; } //Check File size long fLength = new FileInfo(zm.fname).Length; if (check_bytes == 0 && fLength != zm.bytes_total) return ZCRC_DIFFERS; //open if it's not already try { if (zm.fs == null) { zm.fs = File.Open(zm.fname, FileMode.Open, FileAccess.ReadWrite); iOpened = true; } else iOpened = false; } catch { return ERROR; } crc = 0xFFFFFFFF; n = check_bytes; if (n == 0) n = fLength; while ((n-- > 0) && ((c = zm.fs.ReadByte()) != -1)) { crc = CRC.UPDC32((byte)c, crc); } crc = ~crc; zm.fs.Seek(0, 0); if (iOpened) { zm.fs.Close(); zm.fs = null; } while (t1 < 3) { stohdr((uint)check_bytes); zshhdr(sPort, ZCRC, ref Txhdr); while (t2 < 3) { uint tmp = 0; c = zgethdr(sPort, ref Rxhdr, 0, ref tmp); rcrc = tmp; switch (c) { default: //ignore break; case ZFIN: return ERROR; case ZRINIT: return ERROR; case ZCAN: Debug.Print("do_crc_check(): got ZCAN\r\n"); return ERROR; case ZCRC: if (crc != rcrc) return ZCRC_DIFFERS; return ZCRC_EQUAL; } } } return ERROR; } private static int bytesToNextSpace(byte[] buff, int tokstart) { int toklen = Array.IndexOf(buff, (byte)32, tokstart) - tokstart; // can't find another space... check for null, maybe its the last field. if (toklen < 0) { toklen = Array.IndexOf(buff, (byte)0, tokstart) - tokstart; } return toklen; } /*private int rzfile() { zi.eof_seen = false; } */ private ushort getfree() { return (UInt16)(0xFFFF); } /* private int sys2(byte c) { return 0;// currently not implemented } */ private int wcreceive() { return 0; } } }