//------------------------------------------------------------------------------
//--                                                                          --
//--  Downloads S19 formatted files to the MegaSquirt EFI controller via      --
//--  the embedded bootloader routines.  All documentation is contained       --
//--  within this file.                                                       --
//--                                                                          --
//--  Copyright (c) 2002,2003,2004 by Eric Fahlgren.  All rights reserved.    --
//--                                                                          --
//--  This program is free software; you can redistribute it and/or           --
//--  modify it under the terms of the GNU General Public License             --
//--  as published by the Free Software Foundation; either version            --
//--  2 of the License, or (at your option) any later version.                --
//--  See http://www.gnu.org/licenses/gpl.txt                                 --
//--                                                                          --
//--  For support contact <eric@wryday.com>.                                  --
//--                                                                          --
//------------------------------------------------------------------------------
//--                                                                          --
//--  History                                                                 --
//--  1.00 - Eric Fahlgren's original code.                                   --
//--  1.01 - Added -b, -c switches.                                           --
//--  1.02 - Added voltage read after download, -o for non-B&G voltage loc.   --
//--  1.03 - Allow multiple files (probably doesn't work).                    --
//--  1.04 - Add -d switch to allow users to delay between s19 lines.         --
//--  1.05 - Modify -d switch to allow different types of delays;             --
//--        add wipe only.                                                    --
//--  1.06 - Ignore empty lines, spit out and then ignore S0 comment recs.    --
//--  1.07 - Add faster communication rates.                                  --
//--  1.08 - Added "recover" mode that pauses for reboot between wipe and dl. --
//--  1.09 - Cosmetic change: better message when voltage == 0.0.             --
//--  1.10 - Allow user to select either Program or Update.                   --
//--  1.11 - Verify all s-recs via checksum.                                  --
//--                                                                          --
//------------------------------------------------------------------------------

#include <windows.h>
#include <stdio.h>

static char *rcsId = "$Id$";

#ifdef _DEBUG
#  define new DEBUG_NEW
#  undef THIS_FILE
   static char THIS_FILE[] = __FILE__;
#endif

//------------------------------------------------------------------------------

#define MAJOR 1
#define MINOR 11

struct download {
   int     commPortNumber;
   int     commRate;
   HANDLE  hPortHandle;
   char    s19Name[10][128];
   int     nFiles;
   FILE   *s19;
   char   *progName;
   bool    forceBootloader;
   bool    wipeOnly;
   bool    recover;
   bool    program;
   int     BatOfs;
   int     delay[3];

public:
   enum { afterLine = 0, afterChar = 1, afterWipe = 2 };

   download()
    : commPortNumber   ( 1           )
    , commRate         ( 9600        )
    , hPortHandle      ( NULL        )
    , nFiles           ( 0           )
    , s19              ( NULL        )
    , progName         ( NULL        )
    , forceBootloader  ( false       )
    , wipeOnly         ( false       )
    , recover          ( false       )
    , program          ( false       )
    , BatOfs           ( 8           )
   {
      delay[0] =   0;
      delay[1] =   0;
      delay[2] = 500;
   }

   void parseArgs(int argc, char *argv[]);
   void sendFiles();

private:
   void   usage        ();
   bool   openCommPort ();
   void   closeCommPort();
   bool   readCommPort (BYTE &pChar);
   bool   writeCommPort(const char *pBuf, DWORD dwCount);
   void   send         (const char *msg);
   double getVoltage   ();
   void   recv         (const char *response);
   void   verifyRecord (const char *lineBuffer, int lineNumber);
   void   sendFile     ();
};

//------------------------------------------------------------------------------

void download::usage()
{
   // Make sure usage fits on an 80-column screen.
   //  123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789x
   fprintf(stderr,
      "usage: %s [options] [-c<port>] [-r<rate>] [s19-file]...\n"
      "   as in\n"
      "\n"
      "      %s -c2 mysquirt.s19\n"
      "\n", progName, progName); 
   fprintf(stderr,
      "Download requires that the bootloader already be resident on your MCU, it\n"
      "cannot burn a blank chip.\n"
      "\n"
      "   -b       - send \"force bootloader\" command to MS, only works with Guy\n"
      "              Hill embedded code.\n"
      "   -p       - use Program instead of Update.\n"
      "   -c port  - specify comm port to which MegaSquirt is connected, default is\n"
      "              port 1.\n"
      "   -d MS    - delay MS milliseconds between lines, increase if you cannot\n"
      "              reboot after download, default is zero.\n"
      "   -dc MS   - add an inter-character delay, completely useless.\n"
      "   -dw MS   - set after-wipe delay to MS, default is 500 ms.\n"
      "   -o n     - change the byte offset in the runtime variables to get battery\n"
      "              voltage, default is 8 (B&G standard).\n"
      "   -r rate  - specifiy baud rate of connection to MS box, 9600 (default),\n"
      "              19200 or 38400 are the only valid speeds (use negative rate\n"
      "              values, like '-r -2343', to get non-standard speeds).\n"
      "   -v       - use 'recoVer' mode, do a wipe, prompt for power cycle, then download.\n"
      "   -w       - wipe only, don't try to download.\n"
      "   s19-file - the list of names of the s19 files to download, default file is\n"
      "              is megasquirt.s19.\n"
      "\n");
   exit(1);
} 

//------------------------------------------------------------------------------

#define AdjustI(n) {argv[iarg] += (n); if (argv[iarg][0] == '\0') ++iarg;}

void download::parseArgs(int argc, char *argv[])
{
   int which;
   progName = argv[0];

   for (int iarg = 1; iarg < argc; iarg++) {
      if (argv[iarg][0] == '-') {
         switch (argv[iarg][1]) {
            case 'b':
               forceBootloader = true;
               break;

            case 'c':
               AdjustI(2);
               commPortNumber = strtol(argv[iarg], NULL, 10);
               break;

            case 'p':
               program = true;
               break;

            case 'd':
                    if (argv[iarg][2] == 'c') { which = afterChar; AdjustI(3); }
               else if (argv[iarg][2] == 'w') { which = afterWipe; AdjustI(3); }
               else                           { which = afterLine; AdjustI(2); }
               delay[which] = strtol(argv[iarg], NULL, 10);
               break;

            case 'o':
               AdjustI(2);
               BatOfs = strtol(argv[iarg], NULL, 10);
               break;

            case 'r':
            {
               bool checkRate = true;
               AdjustI(2);
               if (argv[iarg][0] == '-') {
                  checkRate = false;
                  argv[iarg]++;
               }
               commRate = strtol(argv[iarg], NULL, 10);
               if (checkRate
               &&  commRate !=  9600
               &&  commRate != 19200
               &&  commRate != 38400) {
                  fprintf(stderr, "ERROR: Specified comm rate %d invalid.\n\n", commRate);
                  usage();
               }
               break;
            }

            case 'v':
               recover  = true;
               break;

            case 'w':
               wipeOnly = true;
               break;

            default:
               usage();
               break;
         }
      }
      else {
         if (nFiles > 9) usage();
         strcpy(s19Name[nFiles], argv[iarg]);
         nFiles++;
      }
   }

   if (nFiles == 0) {
      strcpy(s19Name[0], "megasquirt.s19");
      nFiles++;
   }
}

//------------------------------------------------------------------------------

#include <stdarg.h>

static void error(int exitCode, char *fmt, ...)
{
   va_list args;
   va_start(args, fmt);
      vfprintf(stderr, fmt, args);
   va_end(args);

   exit(exitCode);
}

//------------------------------------------------------------------------------

bool download::openCommPort()
{
   char pPortName[6];
   sprintf(pPortName, "com%d", commPortNumber);
   hPortHandle = CreateFile(pPortName,
                    GENERIC_READ | GENERIC_WRITE,
                    0,
                    NULL,
                    OPEN_EXISTING,
                    FILE_ATTRIBUTE_NORMAL,
                    NULL);
   if (hPortHandle == INVALID_HANDLE_VALUE) return false;

   DCB dcb;
   if (!GetCommState(hPortHandle, &dcb)) return false;
   dcb.BaudRate = commRate;
   dcb.ByteSize = 8;
   dcb.Parity   = NOPARITY;
   dcb.StopBits = ONESTOPBIT;
   if (!SetCommState(hPortHandle, &dcb)) return false;

   COMMTIMEOUTS timeout;
   if (!GetCommTimeouts(hPortHandle, &timeout)) return false;
   timeout.ReadIntervalTimeout         =   0; // ms between chars on read.
   timeout.ReadTotalTimeoutMultiplier  =   0;
   timeout.ReadTotalTimeoutConstant    = 250;
   timeout.WriteTotalTimeoutMultiplier =   0;
   timeout.WriteTotalTimeoutConstant   =   0;
   if (!SetCommTimeouts(hPortHandle, &timeout)) return false;

   return true;
}

//------------------------------------------------------------------------------

void download::closeCommPort()
{
   if (hPortHandle != NULL) {
      CloseHandle(hPortHandle);
      hPortHandle = NULL;
   }
}

//------------------------------------------------------------------------------
// readCommPort - Read char from from the COM port.  Non-blocking, timeout set
// in openCommPort.

bool download::readCommPort(BYTE &pChar)
{
   DWORD dwActualRead;
   bool bOK = 0 != ReadFile(hPortHandle, &pChar, 1, &dwActualRead, NULL);
   if (dwActualRead != 1) bOK = false;
   return bOK;
}

//------------------------------------------------------------------------------

bool download::writeCommPort(const char *pBuf, DWORD dwCount)
{
   DWORD dwBytesWritten;
   bool  bOK = 0 != WriteFile(hPortHandle, pBuf, dwCount, &dwBytesWritten, NULL);
   if (dwBytesWritten != dwCount) bOK = false;
   return bOK;
}

//------------------------------------------------------------------------------
//--  Possible boot loader messages, culled from boot_r12.asm:                --
//--                                                                          --
//--     CR,LF,'Boot>'                                                        --
//--     '  (P)rogram (W)ipe (U)pgrade e(X)it'                                --
//--     '  Complete'                                                         --
//--     ' - waiting ...'                                                     --
//--     ' - error'                                                           --
//--     ' - what?'                                                           --
//--     ' - Reset Vector Invalid'                                            --
//--                                                                          --
//------------------------------------------------------------------------------

#define LF "\012"
#define CR "\015"

//------------------------------------------------------------------------------

void download::send(const char *msg)
{
   writeCommPort(msg, strlen(msg));
}

//------------------------------------------------------------------------------

double download::getVoltage()
{
   send("A");
   BYTE rtv[100];
   for (int i = 0; i < BatOfs+1; i++ ) {
      if (!readCommPort(rtv[i])) return -1.0;
   }
   return rtv[BatOfs]*30.0 / 255.0;
}

//------------------------------------------------------------------------------

void download::recv(const char *response)
{
   char inputBuffer[128];
   BYTE p;
   int len = strlen(response);
   int j = 0;
   for (int i = 0; i <= len; i++ ) {
      if (readCommPort(p)) {
         inputBuffer[j++] = p;
         putchar(p);
      }
   }
   if (j > len) j = len;
   inputBuffer[j] = '\0';

   if (strcmp(response, inputBuffer)) {
      error(4, "Download failed:\n"
               "   Expected response \"%s\",\n"
               "   but received this \"%s\".\n", response, inputBuffer);
   }
}

//------------------------------------------------------------------------------

void download::verifyRecord(const char *lineBuffer, int lineNumber)
{
   char cs = 0;
   for (int i = 2; i < strlen(lineBuffer); i += 2) {
      if (lineBuffer[i] == 13 || lineBuffer[i] == 10 || lineBuffer[i] == 0) break;
      char hex[3] = { lineBuffer[i], lineBuffer[i+1], '\0'};
      cs += char(strtol(hex, NULL, 16));
   }
   cs = ~cs;
   if (cs != 0) {
      error(5, "Download failed:\n"
               "  Checksum invalid on line %d, file is corrupted.\n",
               lineNumber);
   }
}

//------------------------------------------------------------------------------

void download::sendFile()
{
   char lineBuffer[128];
   int  lineNumber = 0;
   int  totalBytes = 0;
   printf("\n");
   while (fgets(lineBuffer, sizeof(lineBuffer), s19)) {
      if (lineBuffer[0] != '\0') { // Skip blank lines.
         verifyRecord(lineBuffer, lineNumber+1);
         if (strncmp(lineBuffer, "S0", 2) == 0) {
            // Comment S-rec, just echo it out to the screen.
            // Like S0550000443A5C67726970...
            printf("S19 comment: \"");
            for (int i = 8; i < strlen(lineBuffer); i += 2) {
               char hex[3] = { lineBuffer[i], lineBuffer[i+1], '\0'};
               printf("%c", strtol(hex, NULL, 16));
            }
            printf("\"\n");
         }
         else { // Data record, so send it.
            ++lineNumber;
            totalBytes += (strlen(lineBuffer) - 12) / 2; // 12 characters overhead per line.
            if (lineNumber % 10 == 0) printf("Line %4d\r", lineNumber);
            if (delay[afterChar] == 0)
               writeCommPort(lineBuffer, strlen(lineBuffer));
            else {
               for (int i = 0; i < strlen(lineBuffer); i++) {
                  writeCommPort(lineBuffer+i, 1);
                  Sleep(delay[afterChar]);
               }
            }
            Sleep(delay[afterLine]);
         }
      }
   }
   printf("File sent, %d lines, %d bytes.\n", lineNumber, totalBytes);
}

//------------------------------------------------------------------------------

void download::sendFiles()
{
   if (!wipeOnly) {
      for (int iFile = 0; iFile < nFiles; iFile++) {
         // Make sure we can read all the files, first.
         s19 = fopen(s19Name[iFile], "rb");
         if (s19 == NULL)
            error(3,  "Download failed:\n"
                      "   Could not open file '%s'.\n", s19Name[iFile]);
         // Read them all and make sure they don't have overlapping
         // addresses, or the wipe/program will fail later.
         fclose(s19);
      }
   }

   if (!openCommPort())
      error(2, "Download failed:\n"
               "   Could not open comm port %d.", commPortNumber);

   if (forceBootloader) send("!!");
   else                 send(CR);
   recv(CR LF "Boot>");
   send("w"); // Wipe.
   Sleep(delay[afterWipe]);
   recv("w  Complete" CR LF "Boot>");

   if (recover) {
      printf("\nPower cycle your MegaSquirt now.  Press <enter> when your MS has rebooted.");
      char s[2];
      fgets(s, 2, stdin);
      recv(CR LF "Boot>");
   }

   if (wipeOnly) {
      printf("\nWiped.\n");
   }
   else {
      for (int iFile = 0; iFile < nFiles; iFile++) {
         s19 = fopen(s19Name[iFile], "rb");
         if (s19 == NULL) // We did this before, but belt & suspenders.
            error(3,  "Download failed:\n"
                      "   Could not open file '%s'.\n", s19Name[iFile]);

         if (program) {
            send("p"); // Program.
            recv("p - waiting ...");
         }
         else {
            send("u"); // Update.
            recv("u - waiting ...");
         }

         printf("\nSending file %d of %d - %s", iFile+1, nFiles, s19Name[iFile]);
         sendFile();
         recv("  Complete" CR LF "Boot>");
         fclose(s19);
      }

      Sleep(100);
      send("x"); // Reboot MS.
      Sleep(100);

      send("S");
      Sleep(100);
      printf("\n");
      BYTE p;
      while (readCommPort(p)) {
         putchar(p);
         if (p == 'x') printf("\nSignature: ");
      }
      printf("\n");

      double v = getVoltage();
      if (v < 0.0)
         printf("Could not fetch voltage from MS, download may have failed.\n");
      else {
         printf("Controller battery voltage: %5.2f\n", v);
         if (v == 0.0) {
            printf(
               "\bIt appears that you still have the bootload jumper installed.\n"
               "If this is not the case, and you have removed the jumper, then\n"
               "something is wrong and your download probably failed.\n"
               "\n"
               "If MS doesn't reboot, type \"download -help\" and check out the -v switch.\n"
               );
         }
         else if (v < 7.0 || v > 15.0) {
            printf(
               "\bController voltage too low for reliable FLASH burning.\n"
               "Don't be surprised if you can't reboot MS.\n"
               "\n"
               "If MS doesn't reboot, type \"download -help\" and check out the -v switch.\n"
            );
         }
      }
   }
      
   closeCommPort();
}

//------------------------------------------------------------------------------

int main(int argc, char *argv[])
{
   printf("MS Download %d.%02d\n", MAJOR, MINOR);

   download d;
   d.parseArgs(argc, argv);
   d.sendFiles();
   exit(0);
   return 0; // So lint doesn't complain.
}


//------------------------------------------------------------------------------

