//*****************************************
//******** PC Configurator V1.00 **********
//*** (C) - 2001 B.Bowling/A. Grippo ******
//** All derivatives from this software ***
//*** are required to keep this header ****
//*****************************************

#include "stdafx.h"
#include "megatune.h"
#include "msDatabase.h"
#include "veconst.h"
#include "repository.h"

static char *rcsId = "$Id$";

#ifdef _DEBUG
#  define new DEBUG_NEW
#  undef THIS_FILE
   static char THIS_FILE[] = __FILE__;
#endif

extern repository rep;

//------------------------------------------------------------------------------

CString msDatabase::settingsFile = "";

int msDatabase::maprange[8] = {  30,   40,   50,   60,   70,   80,   90,  100 };
int msDatabase::rpmrange[8] = { 500, 1000, 1500, 2000, 2500, 3000, 3500, 5500 };

WORD msDatabase::adf[256] = { // Default table for GM sensor.
   100,  59,  63,  66,  68,  70,  71,  72,
    73,  74,  74,  75,  76,  76,  77,  77,
    78,  78,  79,  79,  80,  80,  80,  81,
    81,  81,  82,  82,  82,  82,  83,  83,
    83,  83,  84,  84,  84,  84,  85,  85,
    85,  85,  85,  86,  86,  86,  86,  86,
    87,  87,  87,  87,  87,  87,  88,  88,
    88,  88,  88,  88,  89,  89,  89,  89,
    89,  89,  89,  90,  90,  90,  90,  90,
    90,  90,  91,  91,  91,  91,  91,  91,
    91,  92,  92,  92,  92,  92,  92,  92,
    92,  93,  93,  93,  93,  93,  93,  93,
    93,  94,  94,  94,  94,  94,  94,  94,
    94,  94,  95,  95,  95,  95,  95,  95,
    95,  95,  96,  96,  96,  96,  96,  96,
    96,  96,  96,  97,  97,  97,  97,  97,
    97,  97,  97,  97,  98,  98,  98,  98,
    98,  98,  98,  98,  98,  99,  99,  99,
    99,  99,  99,  99,  99, 100, 100, 100,
   100, 100, 100, 100, 100, 101, 101, 101,
   101, 101, 101, 101, 101, 102, 102, 102,
   102, 102, 102, 102, 102, 103, 103, 103,
   103, 103, 103, 103, 104, 104, 104, 104,
   104, 104, 104, 105, 105, 105, 105, 105,
   105, 105, 106, 106, 106, 106, 106, 106,
   107, 107, 107, 107, 107, 108, 108, 108,
   108, 108, 109, 109, 109, 109, 109, 110,
   110, 110, 110, 111, 111, 111, 111, 112,
   112, 112, 112, 113, 113, 113, 114, 114,
   114, 115, 115, 116, 116, 116, 117, 117,
   118, 119, 119, 120, 121, 121, 122, 123,
   124, 126, 127, 129, 131, 135, 141, 100
};

WORD msDatabase::thf[256] = { // Default table for GM sensor.
   210, 255, 255, 255, 255, 255, 255, 255,
   255, 255, 255, 255, 255, 255, 255, 255,
   254, 250, 247, 243, 240, 237, 234, 231,
   229, 226, 224, 222, 219, 217, 215, 213,
   211, 209, 207, 206, 204, 202, 201, 199,
   198, 196, 195, 193, 192, 191, 189, 188,
   187, 185, 184, 183, 182, 181, 179, 178,
   177, 176, 175, 174, 173, 172, 171, 170,
   169, 168, 167, 166, 165, 164, 163, 162,
   161, 161, 160, 159, 158, 157, 156, 155,
   155, 154, 153, 152, 151, 151, 150, 149,
   148, 148, 147, 146, 145, 145, 144, 143,
   142, 142, 141, 140, 139, 139, 138, 137,
   137, 136, 135, 135, 134, 133, 133, 132,
   131, 131, 130, 129, 129, 128, 127, 127,
   126, 125, 125, 124, 123, 123, 122, 122,
   121, 120, 120, 119, 118, 118, 117, 116,
   116, 115, 115, 114, 113, 113, 112, 111,
   111, 110, 110, 109, 108, 108, 107, 106,
   106, 105, 105, 104, 103, 103, 102, 101,
   101, 100, 100,  99,  98,  98,  97,  96,
    96,  95,  94,  94,  93,  92,  92,  91,
    90,  90,  89,  88,  88,  87,  86,  86,
    85,  84,  83,  83,  82,  81,  81,  80,
    79,  78,  78,  77,  76,  75,  74,  74,
    73,  72,  71,  70,  70,  69,  68,  67,
    66,  65,  64,  63,  63,  62,  61,  60,
    59,  58,  57,  56,  55,  53,  52,  51,
    50,  49,  48,  46,  45,  44,  42,  41,
    40,  38,  36,  35,  33,  31,  30,  28,
    26,  23,  21,  19,  16,  13,  10,   7,
     3,   0,   0,   0,   0,   0,   0, 210
};

WORD msDatabase::kpaf[256] = { // Default table for MPX4115.
   100, 100,  11,  11,  12,  12,  13,  13,
    13,  14,  14,  15,  15,  16,  16,  16,
    17,  17,  18,  18,  19,  19,  19,  20,
    20,  21,  21,  22,  22,  22,  23,  23,
    24,  24,  25,  25,  25,  26,  26,  27,
    27,  28,  28,  28,  29,  29,  30,  30,
    31,  31,  31,  32,  32,  33,  33,  34,
    34,  34,  35,  35,  36,  36,  37,  37,
    37,  38,  38,  39,  39,  40,  40,  40,
    41,  41,  42,  42,  43,  43,  43,  44,
    44,  45,  45,  46,  46,  46,  47,  47,
    48,  48,  49,  49,  49,  50,  50,  51,
    51,  51,  52,  52,  53,  53,  54,  54,
    54,  55,  55,  56,  56,  57,  57,  57,
    58,  58,  59,  59,  60,  60,  60,  61,
    61,  62,  62,  63,  63,  63,  64,  64,
    65,  65,  66,  66,  66,  67,  67,  68,
    68,  69,  69,  69,  70,  70,  71,  71,
    72,  72,  72,  73,  73,  74,  74,  75,
    75,  75,  76,  76,  77,  77,  78,  78,
    78,  79,  79,  80,  80,  81,  81,  81,
    82,  82,  83,  83,  84,  84,  84,  85,
    85,  86,  86,  87,  87,  87,  88,  88,
    89,  89,  90,  90,  90,  91,  91,  92,
    92,  93,  93,  93,  94,  94,  95,  95,
    95,  96,  96,  97,  97,  98,  98,  98,
    99,  99, 100, 100, 101, 101, 101, 102,
   102, 103, 103, 104, 104, 104, 105, 105,
   106, 106, 107, 107, 107, 108, 108, 109,
   109, 110, 110, 110, 111, 111, 112, 112,
   113, 113, 113, 114, 114, 115, 115, 116,
   116, 116, 117, 117, 118, 118, 100, 100
};

WORD msDatabase::barf[256] = { // Default table for MPX4115.
   100, 100, 141, 141, 141, 141, 140, 140,
   140, 140, 140, 139, 139, 139, 139, 139,
   139, 139, 138, 138, 138, 138, 138, 137,
   137, 137, 137, 136, 136, 136, 136, 136,
   135, 135, 135, 135, 135, 134, 134, 134,
   134, 133, 133, 133, 133, 133, 132, 132,
   132, 132, 132, 131, 131, 131, 131, 131,
   131, 131, 130, 130, 130, 130, 129, 129,
   129, 129, 129, 128, 128, 128, 128, 128,
   127, 127, 127, 127, 126, 126, 126, 126,
   126, 125, 125, 125, 125, 125, 124, 124,
   124, 124, 123, 123, 123, 123, 123, 123,
   123, 123, 122, 122, 122, 122, 121, 121,
   121, 121, 121, 120, 120, 120, 120, 120,
   119, 119, 119, 119, 118, 118, 118, 118,
   118, 117, 117, 117, 117, 117, 116, 116,
   116, 116, 115, 115, 115, 115, 115, 115,
   115, 114, 114, 114, 114, 114, 113, 113,
   113, 113, 113, 112, 112, 112, 112, 111,
   111, 111, 111, 111, 110, 110, 110, 110,
   110, 109, 109, 109, 109, 108, 108, 108,
   108, 108, 107, 107, 107, 107, 107, 107,
   107, 106, 106, 106, 106, 106, 105, 105,
   105, 105, 104, 104, 104, 104, 104, 103,
   103, 103, 103, 103, 102, 102, 102, 102,
   102, 101, 101, 101, 101, 100, 100, 100,
   100, 100, 100, 100,  99,  99,  99,  99,
    99,  98,  98,  98,  98,  98,  97,  97,
    97,  97,  96,  96,  96,  96,  96,  95,
    95,  95,  95,  95,  94,  94,  94,  94,
    93,  93,  93,  93,  93,  92,  92,  92,
    92,  92,  92,  92,  91,  91, 100, 100
};

msDatabase::thermType msDatabase::therm = msDatabase::Fahrenheit;

double msDatabase::tempFromDb(double t)
{
   if (therm == Celsius) {
      t = (t-32.0) * 5.0/9.0;
   }
   return t;
}

double msDatabase::dbFromTemp(double t)
{
   if (therm == Celsius) {
      t = t * 9.0/5.0 + 32.0;
   }
   return t;
}

void msDatabase::fixThermoLabel(CStatic &l, char *fmt, double value)
{
   char t = therm == Fahrenheit ? 'F' : 'C';
   char b[128];
   if (fmt)
      sprintf(b, fmt, tempFromDb(value));
   else
      l.GetWindowText(b,128);
   int n = strlen(b)-1;
   if (b[n] == ')') n--;
   b[n] = t;
   l.SetWindowText(b);
}

//------------------------------------------------------------------------------

BYTE msDatabase::tpsFromPct(BYTE pct)
{
   return BYTE(double(pct)/100.0 * (tpsAdcHi-tpsAdcLo) + tpsAdcLo);
}

BYTE msDatabase::pctFromTps(BYTE tps)
{
   return BYTE(tpsf[tps]);
}

void msDatabase::tpsSet(BYTE adcLo, BYTE adcHi)
{
   tpsAdcLo = adcLo;
   tpsAdcHi = adcHi;
   for (int adc = 0; adc < 256; adc++) {
      BYTE pct = BYTE((adc-adcLo) * 100 / (adcHi-adcLo));
      if (adc <= adcLo) pct =   0;
      if (adc >= adcHi) pct = 100;
      tpsf[adc] = pct;
   }
}

//------------------------------------------------------------------------------
// msDatabase dialog

#include <direct.h>

static BYTE iDwwu[]   = { 120, 120, 115, 115, 115, 110, 105, 100, 100, 100 };
static BYTE iDtpsaq[] = { 5, 20, 40, 77 };

msDatabase::msDatabase(CWnd* pParent /*=NULL*/)
 : CDialog        (msDatabase::IDD, pParent),
   controllerVersion(rep.mtDV),
   _dualTable     (false),
   controllerResetCount(0),
   _recording     (false),
   commPortNumber (  1),
   _logging       (false),
   reqFuel        (0.0),
   rpmk           (  0),
   timerInterval  (200),
   nCylinders     (  8),
   mapType        (  0),
   portType       (  0),
   engType        (  0),
   afr            (14.7),
   cid            (0.0),
   injectorFlow   (0.0),
   _pageNo        (0),
   _changed       (false),
   _loaded        (false),
   _burned        (true) // Until we send something out.
{
   _getcwd(cwd, sizeof(cwd));
   for (int i = 0; i < 64; i++) _const[Dve+i]    = 1;
   for (    i = 0; i < 10; i++) _const[Dwwu+i]   = iDwwu[i];
   for (    i = 0; i <  4; i++) _const[Dtpsaq+i] = iDtpsaq[i];

   _const[Drpmk]         = 0x05;
   _const[Drpmk+1]       = 0xDC;

   _const[Dprimep]       =  15;
   _const[Dcwu]          = 100;
   _const[Dcwh]          =  25;
   _const[Dawev]         =  20;
   _const[Dawc]          = 250;
   _const[Dtpsacold]     =   2;
   _const[Dtpsthresh]    =   6;
   _const[Dtpsaclkcmp]   =  10;
   _const[Dtpsdq]        =  20;
   _const[Degotemp]      = 200;
   _const[Degocountcmp]  =  32;
   _const[Degodelta]     =   1;
   _const[Degolimit]     =  10;
   _const[Drpmoxlimit]   =  12;
   _const[Degoswitchv]   =  26; // Raw ADC for 0.50v

   _const[Dreq_fuel]     = 100;
   _const[Ddivider]      =   8;
   _const[Dalternate]    =   1;
   _const[Dinjopen]      =  10;
   _const[Dinjocfuel]    =   0;
   _const[Dinjpwm]       =  30;
   _const[Dinjpwmt]      = 255;
   _const[Dbattfac]      =   6;
   _const[Dconfig11]     = 113;
   _const[Dconfig12]     = 112;
   _const[Dconfig13]     =   0;
   _const[Dfastidle]     = 185;

   for (i = 0; i < 8; i++) {
      _const[Drpmrangeve+i] = static_cast<BYTE>(rpmrange[i] / 100);
      _const[Dkparangeve+i] = static_cast<BYTE>(maprange[i]);
      if (alphaN()) _const[Dkparangeve+i] = tpsFromPct(_const[Dkparangeve+i]);
   }

   memcpy(_const+128, _const, 128);

   //{{AFX_DATA_INIT(msDatabase)
	//}}AFX_DATA_INIT

   readConfig();
   openCommPort();
   getConst(); // Attempt to grab data from controller.
}

msDatabase::~msDatabase()
{
   if (_recording) {
      _recording = false; // Do this before closing the file in case the timer fires.
      fclose(_recordfile);
   }
}

//------------------------------------------------------------------------------

void msDatabase::DoDataExchange(CDataExchange* pDX)
{
   CDialog::DoDataExchange(pDX);
   //{{AFX_DATA_MAP(msDatabase)
	//}}AFX_DATA_MAP
}

//------------------------------------------------------------------------------

BEGIN_MESSAGE_MAP(msDatabase, CDialog)
   //{{AFX_MSG_MAP(msDatabase)
	//}}AFX_MSG_MAP
END_MESSAGE_MAP()

//------------------------------------------------------------------------------
// msDatabase message handlers

BEGIN_EVENTSINK_MAP(msDatabase, CDialog)
   //{{AFX_EVENTSINK_MAP(msDatabase)
	//}}AFX_EVENTSINK_MAP
END_EVENTSINK_MAP()

//------------------------------------------------------------------------------

FILE *cfgOpen(const char *fileName, const char *mode)
{
    // First, look mtCfg dir, then look "here."
   char configDir[128];
   sprintf(configDir, "mtCfg/%s", fileName);
   FILE *file = fopen(configDir, mode);
   if (file == NULL) file = fopen(fileName, mode);
   return file;
}

//------------------------------------------------------------------------------
   // There are three input messages handled here.
   //
   //    Signature (response to the "S" request);
   //    VE/constants tables (response to the "V" request);
   //    Run-time variables (response to the "A" request).
   //
   // The signature is arbitrary length, you must check it
   // on input.  The VE/constants table is 128 bytes long and
   // the run-time variables are 22 bytes.

//------------------------------------------------------------------------------

bool msDatabase::number(char *s, long &n)
{
   n = 0;

   // Prefixes: % = binary, $ = hexadecimal, ! = decimal.
   // Suffixes: Q = binary, H = hexadecimal, T = decimal.

   int  base = 10;
   char ch   = s[0];

   switch (ch) {
      case '%': base =  2; s[0] = ' '; break;
      case '$': base = 16; s[0] = ' '; break;
      case '!': base = 10; s[0] = ' '; break;

      case '0': case '1': case '2': case '3': case '4':
      case '5': case '6': case '7': case '8': case '9':
      case 'A': case 'B': case 'C': case 'D': case 'E': case 'F':
      case 'a': case 'b': case 'c': case 'd': case 'e': case 'f':
         ch = s[strlen(s)-1];
         switch (ch) {
            case 'Q': case 'q': base =  2; break;
            case 'H': case 'h': base = 16; break;
            case 'T': case 't': base = 10; break;

            case '0': case '1': case '2': case '3': case '4':
            case '5': case '6': case '7': case '8': case '9':
            case 'A': case 'B': case 'C': case 'D': case 'E': case 'F':
            case 'a': case 'b': case 'c': case 'd': case 'e': case 'f':
               // Everthing is ok so far.
               break;

            default:
               return false;
               break;
         }
         break;
      default:
         return false;
         break;
   }

   n = strtol(s, NULL, base);
   return true;
}

bool msDatabase::number(char *s, WORD &n)
{
   long nn;
   if (!number(s, nn))
      return false;
   else {
      if (nn > 65536 || nn < 0) return false;
      n = static_cast<WORD>(nn);
   }
   return true;
}

bool msDatabase::number(char *s, BYTE &n)
{
   long nn;
   if (!number(s, nn))
      return false;
   else {
      if (nn > 255 || nn < 0) return false;
      n = static_cast<BYTE>(nn);
   }
   return true;
}

bool msDatabase::charIn(char c, char *set)
{
   return strchr(set, c) != NULL;
}

//------------------------------------------------------------------------------

#define WHITESPACE " \t"

bool msDatabase::readTable(char *fileName, WORD *values, bool msgDisp)
{
   int    nValues = 0;
   FILE   *tableFile = cfgOpen(fileName, "r");
   bool   ok = true;

   if (tableFile == NULL) {
      char msg[128];
      sprintf(msg,
         "Configuration file %s not found.\n"
         "Using default values for internal table.", fileName);
      if (msgDisp) MessageBox(msg, "MegaTune");
      ok = false;
   }
   else {
      char lineBuffer[512];

      //------------------------------------------------------------------------
      //--  Parses lines of the form:                                         --
      //--                                                                    --
      //--    ^<whitespace>DB<whitespace><number><any old junk>$              --
      //--                                                                    --
      //--  where character case is insignificant, and the form of numbers    --
      //--  is derived from P&E 6808 assembly language.                       --

      while (fgets(lineBuffer, sizeof(lineBuffer)-1, tableFile)) {
         char *s = lineBuffer;

         if (!charIn(*s, WHITESPACE)) continue;
         while (charIn(*s, WHITESPACE)) s++;        // Skip leading white space.
         if (!charIn(*s, "dD")) continue; else s++;
         if (!charIn(*s, "bB")) continue; else s++;
         if (!charIn(*s, WHITESPACE)) continue;
         while (charIn(*s, WHITESPACE)) s++;

         char *last = s;               // Null terminate the string of interest.
         while (charIn(*last, "0123456789$%!QHTqht")) last++;
         *last = '\0';

         if (nValues >= 256) {
            char msg[128];
            sprintf(msg,
               "Too many values (more than 256) found in %s.\n"
               "Extra values will be ignored.", fileName);
            MessageBox(msg);
            ok = false;
            break;
         }
         if (!number(s, values[nValues++])) {
            char msg[128];
            sprintf(msg,
               "\"%s\" is not a number in %s.\n"
               "Internal table will be wrong.", s, fileName);
            MessageBox(msg);
            ok = false;
            break;
         }
      }

      fclose(tableFile);

      if (nValues < 256) {
         char msg[128];
         sprintf(msg,
            "Too few values (fewer than 256) found in %s.\n"
            "Internal table will be wrong.", fileName);
         MessageBox(msg);
         ok = false;
      }
   }

   return ok;
}

void msDatabase::readConfig()
{
   FILE *msConfig = cfgOpen("megatune.cfg", "r");
   if (msConfig != NULL) {
      char cbuf[100];
      if (fgets(cbuf, 100, msConfig) != NULL) {
         if (cbuf[3] == '1') commPortNumber = 1;
         if (cbuf[3] == '2') commPortNumber = 2;
         if (cbuf[3] == '3') commPortNumber = 3;
         if (cbuf[3] == '4') commPortNumber = 4;
      }

      if (fgets(cbuf, 100, msConfig) != NULL) {
         int igtemp;
         sscanf(cbuf, "%d", &igtemp);
         timerInterval = igtemp;
      }
      fclose(msConfig);
   }

   readTable("airdenfactor.inc", adf,  true);
   readTable("thermfactor.inc",  thf,  true);
   if (!readTable("matfactor.inc", mtf, false)) {
      // If no MAT factor table, just use the CTS table.
      for (int i = 0; i < 256; i++) mtf[i] = thf[i];
   }
   readTable("kpafactor.inc",    kpaf, true);
   maxmap = kpaf[0];
   for (int i = 1; i < 256; i++) if (kpaf[i] > maxmap) maxmap = kpaf[i];
   if (!readTable("throttlefactor.inc", tpsf, false)) {
      for (int i = 0; i < 256; i++) tpsf[i] = static_cast<BYTE>(i); // static_cast<BYTE>(i * 100.0/255.0);
   }
   tpsAdcLo =   0;
   tpsAdcHi = 255;
   for (i =   0; tpsf[i] ==   0 && i < 256; i++) tpsAdcLo = BYTE(i);
   for (i = 255; tpsf[i] == 100 && i >=  0; i--) tpsAdcHi = BYTE(i);
}

void msDatabase::writeConfig()
{
   closeCommPort(); // gack, put this in setCommPort
   openCommPort();

   FILE *msConfig = cfgOpen("megatune.cfg", "w");
   if (msConfig == NULL)
      MessageBox("Configuration file megatune.cfg can't be written.\nDirectory protection error?");
   else {
      fprintf(msConfig, "COM%d\n", commPortNumber);
      fprintf(msConfig, "%d\n", timerInterval);
      fclose(msConfig);
   }
}

//------------------------------------------------------------------------------

static DWORD tickStart = GetTickCount();

static double timeNow()
{
   return double(GetTickCount()-tickStart) / 1000.0;
}

static char BASED_CODE szXlsFilter[] = "Excel Spreadsheet (*.xls)|*.xls|All Files (*.*)|*.*";

void msDatabase::setRecording(bool rec, datalogType LogType)
{
   if (!rec) {
      _recording = false; // Make sure to turn it off first.
      if (_recordfile) {
         fclose(_recordfile);
         _recordfile = NULL;
      }
   }
   else if (rec) {
      if (_recording) {
         fclose(_recordfile);
         _recordfile = NULL;
         _recording  = false;
      }

      CFileDialog dlgr(FALSE, "xls", "datalog.xls", OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT, szXlsFilter);
      char cwd[_MAX_PATH];
      _getcwd(cwd, sizeof(cwd));
      dlgr.m_ofn.lpstrInitialDir = cwd;

      if (dlgr.DoModal() == IDOK) {
         TRY {
            _recordfile = fopen(dlgr.GetPathName(), "w");
            if (_recordfile == NULL) THROW(new CFileException(CFileException::generic));

            _recording = true;
            _logType   = LogType;
            tickStart  = GetTickCount();
            switch (_logType) {
               case classicLog:
                  fprintf(_recordfile, "Seconds\tRPM\tMAP\tO2\tEngineBit\tGego\tGair\tGwarm\tGbaro\tGve\tGammae\n");
                  break;
               case fullLog:
                  fprintf(_recordfile, "Time\tSecL\tRPM\tMAP\tTP\tO2\tMAT\tCLT\tEngine\tGego\tGair\tGwarm\tGbaro\tGammae\tTPSacc\tGve\tPW\tGve2\tPW2\n");
                  break;
               case rawLog:
                  fprintf(_recordfile, "Rsecl\tRsquirt\tRengine\tRbaro\tRmap\tRmat\tRclt\tRtps\tRbatt\tRego\tRegocorr\tRaircorr\tRwarmcorr\tRrpm\tRpw\tRtpsaccel\tRbarocorr\tRgammae\tRvecurr\tRpw2\tRvecurr2\tRpgOfs\n");
                  break;
            }
         }
         CATCH(CFileException, e) {
            char message[200];
            sprintf(message, "File \"%s\" can't be opened", dlgr.GetPathName());
            MessageBox(message);
            _recording = false;
         }
         END_CATCH
      }
   }
}

//------------------------------------------------------------------------------

static char BASED_CODE szMsqFilter[] = "MegaSquirt Settings Files (*.msq)|*.msq|All Files (*.*)|*.*";

// 1.0.1 is original release through 2.00
// 2.0.0 is dual table files.

#define Major 2 // File version, not MegaTune.
#define Minor 0
#define Rev   0

struct versionInfo {
   char       desc[10];          // 16 bytes, tag plus numbers.
   short int  major;
   short int  minor;
   short int  rev;

   versionInfo() : major(Major), minor(Minor), rev(Rev) {
      assert(sizeof(versionInfo) == 16);
      strcpy(desc, "#MegaTune"); // That's 10 including the null terminator.
   }
   versionInfo(short int M, short int m, short int r)
    : major(M),
      minor(m),
      rev  (r)
   {
      assert(sizeof(versionInfo) == 16);
      strcpy(desc, "#MegaTune"); // That's 10 including the null terminator.
   }

   operator ==(versionInfo &rhs)
   {
      return
         major == rhs.major &&
         minor == rhs.minor &&
         rev   == rhs.rev   &&
         strcmp(desc, "#MegaTune") == 0;
   }
};

static versionInfo v101(1, 0, 1);
static versionInfo v200(2, 0, 0);

//------------------------------------------------------------------------------

void msDatabase::writeSettings()
{
      TRY {
         CFile msqFile;
         msqFile.Open(settingsFile, CFile::modeCreate | CFile::modeWrite);
            versionInfo v;
            msqFile.Write(&v,            sizeof(versionInfo));
            msqFile.Write(_const,        sizeof(_const));
            msqFile.Write(&cid,          sizeof(cid));
            msqFile.Write(&injectorFlow, sizeof(injectorFlow));
            msqFile.Write(&afr,          sizeof(afr));
         msqFile.Close();
         _changed = false;
      }
      CATCH(CFileException, e) {
         char message[128];
         sprintf(message, "File \"%s\" can't be opened", settingsFile);
         MessageBox(message);
      }
      END_CATCH
}

void msDatabase::saveAs()
{
   CFileDialog dlgr(FALSE, "msq", "megasquirt.msq", OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT, szMsqFilter);
   dlgr.m_ofn.lpstrInitialDir = cwd;
   if (dlgr.DoModal() == IDOK) {
      settingsFile = dlgr.GetPathName();
      writeSettings();
   }
}

void msDatabase::save()
{
   if (settingsFile == "")
      saveAs();
   else {
      writeSettings();
   }
}

//------------------------------------------------------------------------------

void msDatabase::readSettings()
{
   TRY {
      CFile msqFile;
      msqFile.Open(settingsFile, CFile::modeRead);
      versionInfo v, current;
      msqFile.Read(&v, sizeof(versionInfo));
      int size = 0;
      if (v == v101) size = 129;
      if (v == v200) size = 257;
      if (size > 0) {
         msqFile.Read(_const,        size);
         msqFile.Read(&cid,          sizeof(cid));
         msqFile.Read(&injectorFlow, sizeof(injectorFlow));
         msqFile.Read(&afr,          sizeof(afr));
         nCylinders = ncyl_mask_bits(Const(Dconfig11))+1;
         nSquirts   = int(0.00001 + double(nCylinders) / double(Const(Ddivider)));
         _loaded  = true;
         _changed = false;
         if (MessageBox("Burn values to controller?", "Burn Values", MB_YESNO | MB_DEFBUTTON1 | MB_ICONQUESTION) == IDYES) {
             putConst();
         }
      }
      else {
         char message[128];
         sprintf(message, "File has wrong version, expected %d.%d.%d and found %d.%d.%d", current.major, current.minor, current.rev, v.major, v.minor, v.rev);
         MessageBox(message);
         _loaded  = false;
         _changed = true;
      }
      msqFile.Close();
   }

   CATCH(CFileException, e) {
      char message[128];
      sprintf(message, "File \"%s\" can't be opened", settingsFile);
      MessageBox(message);
   }
   END_CATCH
}

//------------------------------------------------------------------------------

void msDatabase::open()
{
   CFileDialog dlgr(TRUE, "msq", NULL, OFN_HIDEREADONLY | OFN_FILEMUSTEXIST, szMsqFilter);
   dlgr.m_ofn.lpstrInitialDir = cwd;
   if (dlgr.DoModal() == IDOK) {
      settingsFile = dlgr.GetPathName();
      readSettings();
   }
}

//------------------------------------------------------------------------------

int msDatabase::alphaN()
{
   return alphaN_from_bits(Const(Dconfig13));
}

void msDatabase::alphaN(int setting)
{
   Const(Dconfig13, static_cast<BYTE>((Const(Dconfig13) & ~ALPHAN_MASK_BITS) | setting << 2));
}

int msDatabase::egoType()
{
   return wbego_from_bits(Const(Dconfig13));
}

void msDatabase::egoType(int value)
{
   Const(Dconfig13, static_cast<BYTE>((Const(Dconfig13) & ~O2SENSOR_MASK_BITS) | value << 1));
}

int msDatabase::baroCorr()
{
   return baroc_from_bits(Const(Dconfig13));
}

void msDatabase::baroCorr(int value)
{
   Const(Dconfig13, static_cast<BYTE>((Const(Dconfig13) & ~BAROC_MASK_BITS) | value << 3));
}

//------------------------------------------------------------------------------

static double interp(double x, double x0, double y0, double x1, double y1)
{
   return y0 + (x-x0) / (x1-x0) * (y1-y0);
}

int msDatabase::interpolateVE(int rpm, int map)
{
   if (map < maprange[0]) map = maprange[0];
   if (map > maprange[7]) map = maprange[7];
   if (rpm < rpmrange[0]) rpm = rpmrange[0];
   if (rpm > rpmrange[7]) rpm = rpmrange[7];

   int irpm, imap, rpm1, rpm2, map1, map2;
   for (irpm = 0; irpm < 7; irpm++) {
      if (rpm <= rpmrange[irpm+1]) {
         rpm1 = rpmrange[irpm];
         rpm2 = rpmrange[irpm+1];
         break;
      }
   }
   for (imap = 0; imap < 7 ; imap++) {
      if (map <= maprange[imap+1]) {
         map1 = maprange[imap];
         map2 = maprange[imap+1];
         break;
      }
   }

   int ve11 = Const(Dve+(irpm+0)+(imap+0)*8);
   int ve21 = Const(Dve+(irpm+1)+(imap+0)*8);
   int ve12 = Const(Dve+(irpm+0)+(imap+1)*8);
   int ve22 = Const(Dve+(irpm+1)+(imap+1)*8);

   double loMapVE = interp(rpm, rpm1, ve11, rpm2, ve21);
   double hiMapVE = interp(rpm, rpm1, ve12, rpm2, ve22);
	return int(interp(map, map1, loMapVE, map2, hiMapVE));
}

//------------------------------------------------------------------------------

int  msDatabase::pageNo()
{
   return _pageNo;
}

int msDatabase::setPageNo(int pageNo)
{
   if (pageNo > 1)  pageNo = 1;
   if (pageNo < 0)  pageNo = 0;
   if (!_dualTable) pageNo = 0;
   return _pageNo = pageNo;
}

bool msDatabase::dualTable()
{
   return _dualTable;
}

//------------------------------------------------------------------------------

BYTE msDatabase::Const   (int offset)         { return  _const[offset+_pageNo*128]; }
BYTE msDatabase::Const   (int offset, BYTE v) { return (_const[offset+_pageNo*128] = v); }
BYTE msDatabase::ConstRaw(int offset)         { return  _const[offset]; }
BYTE msDatabase::ConstRaw(int offset, BYTE v) { return (_const[offset] = v); }

void msDatabase::putConst()
{
   int nBytes   = dualTable() ? 128 : 125;
   int nPages   = dualTable() ?   2 :   1;
   int savePage = pageNo();
   for (_pageNo = 0; _pageNo < nPages; _pageNo++) {
      for (int i = 0; i < nBytes; i++) {
         putConstByte(i);
      }
   }
   setPageNo(savePage);
   burnConst();
}

void msDatabase::putConstByte(int offset)
{
   sendPage();
   BYTE outbuf[3] = { 'W', BYTE(offset), Const(offset) };
   writeCommPort(outbuf, 3);
   _burned = false;
}

void msDatabase::putConstByte(int offset, int value, bool sendit)
{
   bool changed = Const(offset) != value;
   Const(offset, static_cast<BYTE>(value));
   if (sendit && changed) putConstByte(offset);
   _changed = _changed || changed;
}

void msDatabase::burnConst()
{
   BYTE rBuf[Rget];
   if (!getRuntime(rBuf))
      MessageBox("Could not connect to controller.", "No Connection");
   else {
      if (vBatt < 7.0) {
         if (MessageBox("Controller voltage too low for reliable FLASH burning.\nTry anyhow?", "Burn Flash", MB_YESNO | MB_DEFBUTTON2 | MB_ICONQUESTION | MB_SYSTEMMODAL) == IDNO) return;
      }
      writeCommPort("B");
      _burned = true;
   }
}

//------------------------------------------------------------------------------

void msDatabase::getVersion()
{
   if (1||controllerVersion == -1.0) {
      writeCommPort("Q");
      BYTE q;
      if (!readCommPort(q))
         controllerVersion = rep.mtDV;
      else
         controllerVersion = double(q) / 10.0;

      _dualTable = controllerVersion == 0.1;

      if (_dualTable) {
         controllerVersion = 2.0; // In all other regards, this is 2.0.
      }
#if 0
      writeCommPort("S");
      BYTE nBytes = 0;
      if (readCommPort(nBytes)) {
         for (int i = 0; i < nBytes; i++) {
            if (!readCommPort(q)) break;
         }
      }
#endif
   }
}

//------------------------------------------------------------------------------

void msDatabase::sendPage(int pageNo)
{
   getVersion();
   if (!_dualTable) return;

   BYTE pagbuf[2] = { 'P', static_cast<BYTE>(pageNo >= 0 ? pageNo : _pageNo) };
   writeCommPort(pagbuf, 2);
}

//------------------------------------------------------------------------------

bool msDatabase::getMemory(BYTE pageNumber, BYTE buffer[256])
{
   getVersion();
   if (!_dualTable) return false;

   BYTE cmd[2] = { 'F', pageNumber };
   writeCommPort(cmd, 2);

   for (int i = 0; i < 256; i++) {
      if (!readCommPort(buffer[i])) return false;
   }

   return true;
}

//------------------------------------------------------------------------------

bool msDatabase::getConst(int pageNo)
{
   sendPage(pageNo);

   writeCommPort("V");
   int nBytes = dualTable() ? 128 : 125;
   int ofs = (pageNo >= 0 ? pageNo : _pageNo)*128;
   for (int i = 0; i < nBytes; i++) {
      if (!readCommPort(_const[i+ofs])) {
         return false;
      }
   }

   nCylinders = ncyl_mask_bits(Const(Dconfig11))+1;
   nSquirts   = int(0.00001 + double(nCylinders) / double(Const(Ddivider)));

   return true;
}

//------------------------------------------------------------------------------

bool msDatabase::getRuntime(BYTE rBuf[Rget])
{
   controllerReset = false;
   static int previousSecl = 255;

   writeCommPort("A");
   for (int i = 0; i < Rget; i++) {
      if (!readCommPort(rBuf[i])) {
         while (i < Rget) rBuf[i++] = 0;
         return false;
      }
   }

   BYTE secl = rBuf[Rsecl];
   if (secl != previousSecl) {
      if (secl == 0 && previousSecl != 255) {
         // An unexpected reset of controller has occurred.
         //MessageBeep(MB_ICONEXCLAMATION);
         Beep(2000,100);
         Beep(1000,100);
         Beep(2000,100);
         controllerReset = true;
         controllerResetCount++;
      }
      previousSecl = secl;
   }

   double alternating1 = _dualTable ? 1 : Const(Dalternate) ? 2 : 1;
   double alternating2 = _dualTable ? 1 : alternating1;
   cycleTime = 600.0 / rBuf[Rrpm]; // ms per cycle, assume 2 stroke.
   if (stroke_from_bits(Const(Dconfig11)) == 0) cycleTime *= 2.0; // For 4 stroke.
   pulseWidth1 = double(rBuf[Rpw] / 10.0);
   dutyCycle1  = 100.0 * nSquirts/alternating1 * pulseWidth1 / cycleTime;
   pulseWidth2 = _dualTable ? double(rBuf[Rpw2] / 10.0) : pulseWidth1;
   // nSquirts might be different for bank2...
   dutyCycle2  = 100.0 * nSquirts/alternating2 * pulseWidth2 / cycleTime;

   vBatt = rBuf[Rbatt]*30.0 / 255.0;

   throttle = pctFromTps(rBuf[Rtps]);
   map      = kpaf[rBuf[Rmap]];
   mat      = tempFromDb(mtf[rBuf[Rmat]]-40);
   coolant  = tempFromDb(thf[rBuf[Rclt]]-40);

   // Datalog record when enabled.
   if (_recording) {
      switch (_logType) {
         case classicLog:
            fprintf(_recordfile, "%d\t%d\t%d\t%.3f\t%d\t%d\t%d\t%d\t%d\t%d\t%d\n",
                    rBuf[Rsecl],
                    rBuf[Rrpm] * 100,
                    map,
                    rBuf[Rego] * 0.0195312,
                    rBuf[Rengine],
                    rBuf[Regocorr],
                    rBuf[Raircorr],
                    rBuf[Rwarmcorr],
                    rBuf[Rbarocorr],
                    rBuf[Rvecurr],
                    rBuf[Rgammae]);
            break;
         case fullLog:
            fprintf(_recordfile, "%0.3f\t%d\t%d\t%d\t%d\t%.3f\t%.1f\t%.1f\t%d\t%d\t%d\t%d\t%d\t%d\t%d\t%d\t%.1f\t%d\t%.1f\n",
                    timeNow(),
                    rBuf[Rsecl],
                    rBuf[Rrpm],
                    map,
                    throttle,
                    rBuf[Rego] * 0.0195312,
                    mat,
                    coolant,
                    rBuf[Rengine],
                    rBuf[Regocorr],
                    rBuf[Raircorr],
                    rBuf[Rwarmcorr],
                    rBuf[Rbarocorr],
                    rBuf[Rgammae],
                    rBuf[Rtpsaccel],
                    rBuf[Rvecurr],
                    rBuf[Rpw]/10.0,
                    rBuf[Rvecurr2],
                    rBuf[Rpw2]/10.0);
            break;
         case rawLog:
            for (int iB = 0; iB < Rget; iB++) {
               if (iB > 0) fprintf(_recordfile, "\t");
               fprintf(_recordfile, "%d", rBuf[iB]);
            }
            fprintf(_recordfile, "\n");
            break;
      }
      fflush(_recordfile);
   }

   return true;
}

//------------------------------------------------------------------------------

bool msDatabase::toggleCommLogging()
{
   return _logging = !_logging;
}

//------------------------------------------------------------------------------

void msDatabase::log(logType op, bool ok, BYTE *buf, int len)
{
   if (!_logging) return;

   static logType  lastOp = rd;
   static char    *type[2] = { "RCV", "SND" };
   static int      pos = 0;

   static FILE *logFile = NULL;
   if (logFile == NULL) logFile = fopen("comm.log", "w");

   if (logFile != NULL) {
      if (op == wr || op != lastOp) {
         pos = 0;
         fprintf(logFile, "\n%c %s", ok?' ':'X', type[op]);
      }
      lastOp = op;

      for (int i = 0; i < len; i++) {
         if (pos > 0 && pos % 16 == 0) fprintf(logFile, "\n     ");
         if (pos == 0 && op == wr)
            fprintf(logFile, " %c ", buf[0]);
         else
            fprintf(logFile, " %02x", buf[i]);
         pos++;
      }
      fflush(logFile);
   }
}

//------------------------------------------------------------------------------
//--  Lifted substantially from Tom Dolsky's hw2sys routines.                 --

bool msDatabase::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 = 9600;
   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 msDatabase::closeCommPort()
{
   if (hPortHandle != NULL) {
      CloseHandle(hPortHandle);
      hPortHandle = NULL;
   }
}

//------------------------------------------------------------------------------
// readCommPort - Read char from from the COM port. Blocks if no char available.

bool msDatabase::readCommPort(BYTE &pChar)
{
   DWORD dwActualRead;
   bool bOK = 0 != ReadFile(hPortHandle, &pChar, 1, &dwActualRead, NULL);
   if (dwActualRead != 1) bOK = false;
   log(rd, bOK, &pChar, 1);
   return bOK;
}

//------------------------------------------------------------------------------

bool msDatabase::writeCommPort(BYTE *pBuf, DWORD dwCount)
{
   DWORD dwBytesWritten;
   bool  bOK = 0 != WriteFile(hPortHandle, pBuf, dwCount, &dwBytesWritten, NULL);
   if (dwBytesWritten != dwCount) bOK = false;
   log(wr, bOK, pBuf, dwCount);
   return bOK;
}

bool msDatabase::writeCommPort(char *pBuf)
{
   return writeCommPort(reinterpret_cast<BYTE *>(pBuf), strlen(pBuf));
}

//------------------------------------------------------------------------------

typedef struct {
   char *id;
   int   ofs;
   int   sz;
   char dbox;
   char *name;
} dmpDescriptor;

dmpDescriptor dmpTable[] = {
   { "Dprimep",      119,   1,  'E',     "Priming Pulse Duration" },
   { "Dcwu",          64,   1,  'E',     "Cold Cranking Pulsewidth" },
   { "Dcwh",          65,   1,  'E',     "Hot Cranking Pulsewidth" },
   { "Dawev",         66,   1,  'E',     "Afterstart Enrichment Percent" },
   { "Dawc",          67,   1,  'E',     "Afterstart Enrichment Cycles" },
   { "Dwwu",          68,  10,  'E',     "Warmup Enrichment Values" },
   { "Dtpsaq",        78,   4,  'E',     "Acceleration Enrichment Values" },
   { "Dtpsacold",     82,   1,  'E',     "Cold Acceleration Enrichment" },
   { "Dtpsthresh",    83,   1,  'E',     "TPSdot Threshold" },
   { "Dtpsaclkcmp",   84,   1,  'E',     "Acceleration Pulse Duration" },
   { "Dtpsdq",        85,   1,  'E',     "Deceleration Fuel Cut" },
   { "Degotemp",      86,   1,  'E',     "EGO Activation Temperature" },
   { "Degocountcmp",  87,   1,  'E',     "EGO Ignition Event Count" },
   { "Degodelta",     88,   1,  'E',     "EGO Step" },
   { "Degolimit",     89,   1,  'E',     "EGO +/- Limit" },
   { "Drpmoxlimit",  120,   1,  'E',     "EGO Closed-loop RPM Limit" },
   { "Degoswitchv",  122,   1,  'E',     "EGO Switch Voltage" },
   // 0-16 above

   { "Dreq_fuel",     90,   1,  'C',     "Required Fuel" },
   { "Ddivider",      91,   1,  'C',     "Injections Per Cycle" },
   { "Dalternate",    92,   1,  'C',     "Injector Staging" },
   { "Dinjopen",      93,   1,  'C',     "Injector Opening Time" },
   { "Dinjocfuel",    94,   1,  'C',     "Injector Open/Close Fuel" },
   { "Dinjpwm",       95,   1,  'C',     "Injector Current Limit" },
   { "Dinjpwmt",      96,   1,  'C',     "Injector PWM Time Threshold" },
   { "Dbattfac",      97,   1,  'C',     "Injector Opening Battery Factor" },
   { "Drpmk",         98,   2,  'C',     "RPM Constant" },
   { "Dconfig11",    116,   1,  'C',     "Configuration 11" },
   { "Dconfig12",    117,   1,  'C',     "Configuration 12" },
   { "Dconfig13",    118,   1,  'C',     "Configuration 13" },
   { "Dfastidle",    121,   1,  'C',     "Fast Idle Activation Temp" },
   // 17-29 above

   { "Drpmrangeve",  100,   8,  'V',     "VE Table RPM Range" },
   { "Dkparangeve",  108,   8,  'V',     "VE Table MAP Range" },
   { "Dve",            0,  64,  'V',     "VE Table" },
   // 30-32 above

   { "Unused",       123,   5,  '-',     "Unused data" }
};

#define dmpTableSize (sizeof(dmpTable)/sizeof(dmpDescriptor))

void msDatabase::dumpOne(FILE *dmp, int idx)
{
   dmpDescriptor *d = dmpTable+idx;
   if (d->sz == 1) {
      fprintf(dmp, "%-31s %-12s", d->name, d->id);
      fprintf(dmp, ": %3d = $%02x\n", Const(d->ofs), Const(d->ofs));
      switch (d->ofs) {
         case Dconfig11:
         {
            int c11 = Const(Dconfig11);
            fprintf(dmp, "   MAPType  = %d\n", map_from_bits(c11));
            fprintf(dmp, "   Stroke   = %d\n", stroke_from_bits(c11));
            fprintf(dmp, "   InjType  = %d\n", injectiontype_mask_from_bits(c11));
            fprintf(dmp, "   nCylbits = %d (=%d cylinders)\n", ncyl_mask_bits(c11), ncyl_mask_bits(c11)+1);
            break;
         }

         case Dconfig12:
         {
            int c12 = Const(Dconfig12);
            fprintf(dmp, "   CTS      = %d\n", cool_from_bits(c12));
            fprintf(dmp, "   MAT      = %d\n", mat_from_bits(c12));
            fprintf(dmp, "   nInj     = %d\n", ninject_from_bits(c12));
            break;
         }

         case Dconfig13:
         {
            int c13 = Const(Dconfig13);
            fprintf(dmp, "   Oddfire  = %d\n", oddfire_from_bits(c13));
            fprintf(dmp, "   WB EGO   = %d\n", wbego_from_bits(c13));
            fprintf(dmp, "   alpha-N  = %d\n", alphaN_from_bits(c13));
            fprintf(dmp, "   barocorr = %d\n", baroc_from_bits(c13));
            break;
         }
      }
   }
   else if (d->sz < 16) {
      fprintf(dmp, "%-31s [%2d] %-12s", d->name, d->sz, d->id);
      fprintf(dmp, "\n");
      for (int i = 0; i < d->sz; i++) {
         fprintf(dmp, "   [%3d] = %3d = $%02x\n", i, Const(d->ofs+i), Const(d->ofs+i));
      }
   }
   else { // Assume it's an 8x8 table.
      fprintf(dmp, "%-31s %-12s", d->name, d->id);
      fprintf(dmp, "\n          ");
      for (int c = 0; c < 8; c++) {
         fprintf(dmp, " [%3d]", c);
      }
      fprintf(dmp, "\n");
      for (int r = 0; r < 8; r++) {
         fprintf(dmp, "   [%3d] =", r);
         for (c = 0; c < 8; c++) {
            fprintf(dmp, "  %3d ", Const(d->ofs+r*8+c), Const(d->ofs+r*8+c));
         }
         fprintf(dmp, "\n");
      }
   }
}

void msDatabase::dump()
{
   FILE *dmp = fopen("MegaTune.dmp", "a");
   if (dmp == NULL)
      MessageBox("Couldn't write to MegaTune.dmp dump file.");
   else {
      const time_t t = time(NULL);
      fprintf(dmp, "MegaTune Dump %s", ctime(&t));
      fprintf(dmp, "Windows %d.%d %d.%d\n\n", _winmajor, _winminor, _osver, _winver);

      int savePage = _pageNo;
      for (_pageNo = 0; _pageNo < (_dualTable ? 2 : 1); _pageNo++) {
         fprintf(dmp, "Page %d\n", _pageNo);
         for (int i = 0; i < dmpTableSize; i++) {
            dumpOne(dmp, i);
         }
      }
      _pageNo = savePage;

      fprintf(dmp, "\n");
      fclose(dmp);
   }
}

//------------------------------------------------------------------------------

#include "vex.h"

static char BASED_CODE veMsqFilter[] = "VE Exchange (*.vex)|*.vex|All Files (*.*)|*.*";

void msDatabase::veExport()
{
   CFileDialog dlgr(FALSE, "vex", "megasquirt.vex", OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT, veMsqFilter);
   dlgr.m_ofn.lpstrInitialDir = cwd;
   if (dlgr.DoModal() == IDOK) {
      CString fName = dlgr.GetPathName();
      FILE *exp = fopen(fName, "w");
      if (exp == NULL)
         MessageBox("Couldn't export "+fName+" VE Exchange file.", "VE Export");
      else {
         fclose(exp);

         vex v;
         int savePage = _pageNo;
         for (_pageNo = 0; _pageNo < (_dualTable ? 2 : 1); _pageNo++) {
            veTable *p = v.newPage(_pageNo);
            int rpm[8];
            int lod[8];
            int ve[64];
            for (int i = 0; i < 8; i++) {
               rpm[i] = Const(Drpmrangeve+i);
               lod[i] = Const(Dkparangeve+i);
               if (alphaN()) lod[i] = pctFromTps(BYTE(lod[i]));
            }
            for (i = 0; i < 64; i++) {
               ve[i] = Const(Dve+i);
            }

            p->setSize(8, 8, rpm, lod, ve);
         }
         _pageNo = savePage;
         v.write(LPCTSTR(fName));
      }
   }
}

//------------------------------------------------------------------------------

void msDatabase::veImport()
{
   CFileDialog dlgr(TRUE, "vex", NULL, OFN_HIDEREADONLY | OFN_FILEMUSTEXIST, veMsqFilter);
   dlgr.m_ofn.lpstrInitialDir = cwd;
   if (dlgr.DoModal() == IDOK) {
      CString fName = dlgr.GetPathName();
      vex v;
      if (!v.read(LPCTSTR(fName))) MessageBox("Could not read "+fName, "VE Import");
      int savePage = _pageNo;
      for (_pageNo = 0; _pageNo < (_dualTable ? 2 : 1); _pageNo++) {
         veTable *p = v.page(_pageNo);
         if (!p) continue;

         if (p->nRPMs()  != 8) MessageBox("Table has wrong number of RPM bins", "VE Import");
         if (p->nLoads() != 8) MessageBox("Table has wrong number of load bins", "VE Import");

         bool burnIt = MessageBox("Burn VE Table to controller?", "Burn Tables", MB_YESNO | MB_DEFBUTTON1 | MB_ICONQUESTION) == IDYES;

         for (int i = 0; i < 8; i++) {
            putConstByte(Drpmrangeve+i, p->rpm(i), burnIt);
            int l = p->load(i);
            if (alphaN()) l = tpsFromPct(BYTE(l));
            putConstByte(Dkparangeve+i, l,         burnIt);
         }
         for (i = 0; i < 64; i++) {
            putConstByte(Dve+i, p->ve(i%8, i/8), burnIt);
         }
         if (burnIt && !burned()) burnConst();
      }
      _pageNo = savePage;
      v.write(LPCTSTR(fName));
   }
}

//------------------------------------------------------------------------------

bool msDatabase::loaded()    { return _loaded;    }
bool msDatabase::changed()   { return _changed;   }
bool msDatabase::burned()    { return _burned;    }

bool msDatabase::recording() { return _recording; }

//------------------------------------------------------------------------------

msDatabase mdb; // One and only one.

//------------------------------------------------------------------------------

