/*
 * xmodem.exe - terminal session with XMODEM (xsum+CRC) up/download capabilities
 *            - edit xmodem.ini to change desired COM port
 *            - Keystrokes:
 *                              Esc to exit terminal
 *                              PgUp - upload a file (prompts for name)
 *                              PgDn - download a file (prompts for name)
 *
 *            - download function could be shorter but more cryptic
 */
#include <rtos.h>
#include <sio.h>
#include <xmodem.h>
#include <stdio.h>
#include <mem.h>
#include <bios.h>
#include <inifile.h>
#include <kconio.h>


#define COMBASE1 0x3f8
#define COMBASE2 0x2f8
#define COMIRQ1  4
#define COMIRQ2  3

#define NUM_RETRIES 10      /* # of consecutive failures before abort xfer */

int combuf = 256;
/*********************************************************************/
void get_userfilename( char *msg, char *buffer )
{
    cprintf("\r\n%s : ", msg );
    *buffer = 255;
    kgets( buffer );
}

/*********************************************************************/
/* debug_getbyte - read serial byte if available
 *               - break program if user hits esc
 */
int debug_getbyte( WORD port, BYTE *data )
{
    BYTE b;

    if ( kbhit() ) {
        b = getch();
        if ( b == 27 ) {
            sio_writebyte( port, XM_CAN );
            sio_writebyte( port, 3 );       /* ctrl c */
            rt_halt("user exit from XMODEM mode");
        } else
            sio_writebyte( port, b );
    }
    if ( sio_recv_waiting( port ) ) {
        b = sio_readbyte( port );
        *data = b;
        return( 1 );
    }
    return( 0 );
}
/*********************************************************************/
/* debug_readbyte - busy wait for data from a port
 *                - inherits keyboard ESC feature from debug_getbyte
 */
BYTE debug_readbyte( int port )
{
    BYTE b;

    while ( ! debug_getbyte( port, &b ) )
        /* nothing to do */
        rt_sleep( 0 );
    return( b );
}

/*********************************************************************/
temp_outbyte( int port, BYTE b )
{
//    while ( sio_tran_waiting( port )) rt_yield();
    sio_writebyte( port, b );
}
/*********************************************************************/
BYTE calc_xsum( BYTE *p, int count )
{
    BYTE xsum;
    int i;

    xsum = 0;
    while ( count--  )
        xsum += *p++;
    return( xsum );
}

/*********************************************************************/
WORD calc_crc( BYTE *p, int count )
{
    WORD crc;
    int i;

    crc = 0;
    while ( count--  ) {
        crc = crc ^ ((*p++) <<8);
        for ( i = 0 ; i < 8 ; ++i )
            if ( crc & 0x8000 )
                crc = crc << 1 ^ 0x1021;
            else
                crc = crc << 1;
    }
    return( crc );
}
/*********************************************************************/
void upload(WORD port, char *fname)
{
    int  i, count;
    BYTE b;
    char *buffer = NULL;
    FILE *f = NULL;
    BYTE seq;   /* XMODEM sequence number, */
    BYTE xsum;  /* 8 bit checksum */
    WORD xcrc;  /* 16 bit CRC */
    int use_crc = 0;
    DWORD bytes = 0;
    DWORD timer;

    timer = ktime;          /* note start time */

    b = debug_readbyte( port );

    if ( b == XM_CRC ) {
        use_crc = 1;
        b = XM_NAK;     /* start the syphon */
    }
    do {    /* structured exception handler-like */
        /* need a NAK from receiver to start this going */
        if ( b != XM_NAK) {
            cprintf("ERROR: receiver is not listenning\r\n");
            break;
        }
        if ( ( f = fopen( fname, "rb" ))== NULL ) {
            cprintf("ERROR: cannot open file: %s\r\n", fname );
            /* inform our peer */
            break;
        }

        if ( ( buffer = kcalloc( 1, XM_BUFSIZE)) == NULL ) {
            cprintf("ERROR: out of memory\r\n" );
            break;
        }

        seq = 1;
        do {
            /* read some of the file from disk */
            count = fread( buffer, 1, XM_BUFSIZE, f );

            /* reached end of file */
            if ( count == 0 ) break;

            /* set any unread bytes to zero */
            if ( count < XM_BUFSIZE )
                memset( buffer + count, 26, XM_BUFSIZE - count );   /* Ctrl Z */

            /* look for errors while disk reading */
            while ( debug_getbyte( port, &b ));

            /* next part may have to be retried - communications are not perfect */
            do {
                cprintf("data packet # %i ", (WORD) seq );
                temp_outbyte( port, XM_SOH );       /* start */
                temp_outbyte( port, seq );          /* sequence number */
                temp_outbyte( port, seq ^ 0xff );   /* and its complement */

                /* write bytes and compute checksum */
                if ( use_crc )
                    xcrc = calc_crc( buffer, XM_BUFSIZE);
                else
                    xsum = calc_xsum( buffer, XM_BUFSIZE);

                for ( i = 0 ; i < XM_BUFSIZE ; ++i ) {
                    b = buffer[i];
                    temp_outbyte( port, b );
                }
                /* write the CRC or checksum */
                if ( use_crc ) {
                    temp_outbyte( port, xcrc >> 8 );
                    temp_outbyte( port, xcrc & 255 );
                } else
                    temp_outbyte( port, xsum );

                /* and now we get the news */
                b = debug_readbyte( port );

                switch ( b ) {
                    case XM_NAK : cprintf("NAK\r\n"); break;
                    case XM_ACK : cprintf("ACK\r\n"); break;
                    default     : cprintf("%02x\r\n", b ); break;
                }
            } while ( b == XM_NAK );
            bytes += XM_BUFSIZE;
            /* now advance to the next packet */
            ++seq;
        } while ( 1 );
    } while ( 0 );

    do {
        temp_outbyte( port, XM_EOT );
        b = debug_readbyte( port );

        switch ( b ) {
            case XM_NAK : cprintf("NAK\r\n"); break;
            case XM_ACK : cprintf("ACK\r\n"); break;
            default     : cprintf("%02x\r\n", b );
        }
    } while ( b == XM_NAK );

    timer = ktime - timer;
    if ( timer > 0 ) {
        cprintf(" %lu bytes in %lu.%lu seconds, %lu bps\n",
            bytes, timer / 1000, timer % 1000,
            bytes*8*1000/timer );
    }
    if ( buffer != NULL ) kfree( buffer );
    if ( f != NULL ) fclose( f );

}

/*********************************************************************/
/* xm_waitbyte - wait a spell = 0
 *             - return early with 1 if a BYTE found for read
 */
xm_waitbyte( WORD port, BYTE *p, DWORD delay_seconds )
{
    DWORD when;

    /* calculate when loop would timeout */
    when = ktime + delay_seconds * 1000;

    if ( when < (delay_seconds * 1000))
        /* timer wrapping - once every 57 days, we'll ignore it in simple example*/
        return( 1 );

    while ( ktime < when ) {
        if ( kbhit() )
            rt_halt("user requested exit while waiting for serial bytes");

        if ( sio_recv_waiting( port ) ) {
            *p = sio_readbyte( port );
            return( 1 );
        }
        rt_sleep( 0 );      /* give up slice */
    }
    /* timed out */
    return( 0 );
}


/*********************************************************************/
void download(WORD port, char *fname)
{
    int  i, count;
    BYTE b;
    int retries;
    char buffer[ XM_BUFSIZE ];
    FILE *f = NULL;
    BYTE seq;       /* XMODEM sequence number */
    BYTE localseq;  /* our expected sequence number */
    BYTE xsum;  /* 8 bit checksum */
    WORD xcrc;  /* 16 bit CRC */
    int use_crc = 0;
    DWORD bytes = 0;
    DWORD timer;

    timer = ktime;          /* note start time */

    /* write out C's waiting for input */
    retries = 10;
    use_crc = 1;
    do {
        do {
            /* write out individual C */
            sio_writebyte( port, XM_CRC );      /* C */
            if ( xm_waitbyte( port, &b, 10 ) ) break;
            if ( retries-- == 0 ) {
                cprintf("Download gave up after no response\r\n");
                return;
            }
        } while ( 1 );

        /* look at sent data */
        if ( b == 3 ) return;   /* control break */
    } while ( b != XM_SOH );

    /* we have a SOH */
    localseq = 1;
    timer = ktime;

    /* try to create output file */
    if ( (f = fopen( fname,"wb")) == NULL ) {
        sio_writebyte( port, XM_CAN );
        rt_halt("unable to open local file for download");
    }

    retries = NUM_RETRIES;
    goto skip_soh;      /* we already have the first byte handy */

    /* we expect packets full of bytes */
    do {
        if ( --retries == 0 ) {
            cprintf("too many retries, giving up\r\n");
            sio_writebyte( port, XM_CAN );
            break;
        }

        if ( !xm_waitbyte( port, &b, 10 ) ) {
            /* handle timeout */
            cprintf("timeout waiting for start of packet\r\n");
            sio_writebyte( port, XM_NAK );
            continue;
        }
skip_soh:
        if ( b == XM_CAN )  /* sender requests a cancel */
            break;
        if ( b == XM_EOT ) {/* end of transmission !!! */
            sio_writebyte( port, XM_ACK );  // acknowledge it
            break;
        }

        if ( b != XM_SOH ) {
            cprintf("garbage received at start of packet\r\n");
            sio_writebyte( port, XM_NAK );
            continue;
        }

        /* looking for sequence number */
        if ( !xm_waitbyte( port, &b, 10 ) ) {
            /* handle timeout */
            cprintf("timeout waiting for sequence number\r\n");
            sio_writebyte( port, XM_NAK );
            continue;
        }
        if ( b == (localseq-1 )) {
            /* we already acknowledged it */
            cprintf("received duplicate packet\r\n");
            sio_writebyte( port, XM_ACK );
            continue;
        }
        if ( b != localseq ) {
            /* either garbage or peer is on a different packet */
            cprintf("sequence number out of order : %u expected : %u\r\n",
                b, localseq );
            sio_writebyte( port, XM_NAK );
            continue;
        }
        /* look for complement byte */
        if ( !xm_waitbyte( port, &b, 10 ) ) {
            /* handle timeout */
            cprintf("timeout waiting for sequence complement\r\n");
            sio_writebyte( port, XM_NAK );
            continue;
        }
        if ( b != (~localseq &255 )) {
            /* complement failed */
            cprintf("sequence number corrupt, complement <> seq\r\n");
            sio_writebyte( port, XM_NAK );
            continue;
        }

        /* get data */
        for ( count = 0 ; count < XM_BUFSIZE ; ++count ) {
             if ( !xm_waitbyte( port, &b, 10 ) ) {
                /* handle timeout */
                cprintf("timeout waiting for data item %u\r\n", count+1);
                sio_writebyte( port, XM_NAK );
                continue;
            }
            buffer[ count ] = b;
        }

        /* get CRC or checksum */
        if ( !xm_waitbyte( port, &b, 10 ) ) {
            /* handle timeout */
            cprintf("timeout waiting for xsum\r\n");
            sio_writebyte( port, XM_NAK );
            continue;
        }

        if ( use_crc ) {
            xcrc = b << 8;
            if ( !xm_waitbyte( port, &b, 10 ) ) {
                /* handle timeout */
                cprintf("timeout waiting for xsum\r\n");
                sio_writebyte( port, XM_NAK );
                continue;
            }
            xcrc |= (b & 255);

            if ( xcrc != calc_crc( buffer, XM_BUFSIZE)) {
                cprintf("CRC failed\r\n");
                sio_writebyte( port, XM_NAK );
                continue;
            }
        } else {
            if ( b != calc_xsum( buffer, XM_BUFSIZE)) {
                cprintf("checksum failed\r\n");
                sio_writebyte( port, XM_NAK );
                continue;
            }
        }
        /* packet was successful */
        if ( fwrite( buffer, sizeof( buffer ), 1, f) != 1 ) {
            cprintf("disk file write failed\r\n");
            break;
        }
        cprintf("downloaded %u bytes\r", bytes );

        sio_writebyte( port, XM_ACK );
        /* now advance to the next packet */
        bytes += XM_BUFSIZE;
        localseq++;

        retries = NUM_RETRIES;

    } while ( 1 );

    timer = ktime - timer;
    if ( timer > 0 ) {
        cprintf(" %lu bytes in %lu.%lu seconds, %lu bps\n",
            bytes, timer / 1000, timer % 1000,
            bytes*8*1000/timer );
    }
    if ( f != NULL ) fclose( f );

}
/***********************************************************************/
/* term_thread - includes all the code to make this work as a terminal */
/*               session while user negotiates upload/download         */
/*             - PgUp initiates upload                                 */
/*             - PgDn initiates download                               */
/***********************************************************************/
void term_thread(DWORD port)
{
    int  i, count;
    BYTE b;
    WORD key;
    static char buffer[ 256 ];

    /*
     * Initialize the COM port
     */
    if ( port == 1 )
        sio_init( 1, COMBASE1, COMIRQ1, combuf, combuf, NULL, 0 );
    else
        sio_init( 2, COMBASE2, COMIRQ2, combuf, combuf, NULL, 0 );

    sio_setup (port, (DWORD)9600, 8, SIO_PARITY_NONE, 2, 0);

    kwindow( 1,2, 80,25 );

    do {    /* structured exception handler-like */
        if ( kbhit() ) {
            key = bioskey( 0 );
            b = key & 255;

            if ( key == 0x4900 ) { /* PgUp */
                cprintf("uploading ...\n\r");
                get_userfilename( "file to upload", buffer );
                upload( port, buffer );
                cprintf("...done\r\n");
            } else if ( key == 0x5100 ) {
                cprintf("downloading ...\n\r");
                get_userfilename( "file to write to", buffer );
                download( port, buffer );
                cprintf("...done\r\n");
            } else if ( b == 27 ) break;
            else sio_writebyte( port, b );
        }
        if ( sio_recv_waiting( port ) ) {
            b = sio_readbyte( port );
            putch( b );
        }
    } while ( 1 );
    sio_close (port);
    rt_halt("user terminated");
}
/*********************************************************************/

main ()
{
    int comport;
    int msg;
    DWORD data;

    rt_init (200);
    rt_timerfreq( 100 );
    kpreemptive = 1;

    kctrlbreak = 0;     /* ignore ctrl breaks */
    clrscr();
    cprintf("Esc to escape..., PgUp to invoke upload, PgDn for download");

    /* determine the desired com port */
    if ((comport = (int)GetIniDWORD("xmodem.ini","default","comport",-1))==-1){
        comport = 1;
        SetIniDWORD("xmodem.ini","default","comport", comport );
    }


    rt_newthread(term_thread, comport , 8192, 0, "term session" );
    kreadmessage( &msg, &data );
}

