//
// Arduino Nano 1702A reader version 1.0
// by Giuseppe Perotti, I1EPJ
//
// 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 3 of the License, or
// (at your option) any later version.
//
// 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, see <http://www.gnu.org/licenses/>.
//

// -------- Options - #define them to enable --------

// Test code
#undef TESTRELAY  // Include relay test command
#undef TESTCALC   // Include calculations test code

// Features
#define BACKSPACE // Include backspace handling code
#define VT100     // Include use of some VT100 escape codes

// ------------------ End options -------------------

// For HEX INTEL dump routine
#define MAXHEXLINE 32 /* the maximum number of bytes to put in one line */

// Arduino NANO defines
#define AD0 10
#define AD1 11
#define AD2 12
#define AD3 A0
#define AD4 A1
#define AD5 A2
#define AD6 A3
#define AD7 A4

#define DB1 9
#define DB2 8
#define DB3 7
#define DB4 6
#define DB5 5
#define DB6 4
#define DB7 3
#define DB8 2

// Analog inputs to measure Vcc and Vdd/Vgg
// (On Arduino Nano A6 and A7 cannot be used as digital outputs)
#define VP5 A6
#define VM9 A7

// 1702A power relay control
#define PWRRLY 13

// The conditioning circuit gives Va6=Vcc/2, i.e. Va6=2,5V @Vcc=5V (M=512)
// (assuming resistors ideal values)
#define MINP5 486  // 5V*0,95
#define MAXP5 538  // 5V*1,05

// The conditioning circuit gives Va7=(10Vcc+5.1Vdd)/15.1, i.e. Va7= 0,2715V@Vdd=-9V, Vcc=5V (M=56)
// (assuming resistors ideal values)
#define MINM9 25  // -9V*1,05
#define MAXM9 87  // -9V*0,95

// Messages
#define BASEADDRSETTO "Base address set to %04XH"
#define CURRENTBASE "Current base address: %04XH"
#define CHECKFAILED "Check failed."
#define CHECKOK "Check OK."
#define COPY1 "1702A EPROM reader v1 - (C) I1EPJ 2021"
#define COPY2 "Distributed under the GPL v3 (or later, at your option)"
#define DONEREADING "Done reading, now checking..."
#define ERRORAT "Error at address %02XH: %02XH != %02XH."
#ifdef TESTRELAY
#define HELP "Available commands:\r\n"\
  "B Base address for D and V commands (in hex, default 0000H)\r\n"\
  "C Check EPROM\r\n"\
  "D Dump EPROM (HEX INTEL)\r\n"\
  "H Help (this command list)\r\n"\
  "I Info\r\n"\
  "P Check power (NOTE: power supplies are checked at every command)\r\n"\
  "R Read EPROM\r\n"\
  "V View HEX/ASCII listing\r\n"\
  "X 0=RELAY OFF, 1=RELAY ON\r\n"\
  "Commands can be issued upper or lower case and must be followed\r\nby CR and/or LF"
#else
#define HELP "Available commands:\r\n"\
  "B Base address for D and V commands (in hex, default 0000H)\r\n"\
  "C Check EPROM\r\n"\
  "D Dump EPROM (HEX INTEL)\r\n"\
  "H Help (this command list)\r\n"\
  "I Info\r\n"\
  "P Check power (NOTE: power supplies are checked at every command)\r\n"\
  "R Read EPROM\r\n"\
  "V View HEX/ASCII listing\r\n"\
  "Commands can be issued upper or lower case and must be followed\r\nby CR and/or LF"
#endif
#define INVALIDCMD "Invalid command"
#define NODATA "No data available, read EPROM first"
#define PROMPT "Ready-->"
#define PSVALUES "Vcc=Vbb=%sV Vdd=Vgg=%sV"
#define PWROK "Power values OK."
#define PWROUTOFRANGE1 "WARNING! Power voltages out of range!"
#define PWROUTOFRANGE2 "Now stopping the program, please check the power supply circuits\r\nand then reset/restart"
#define TESTINGPS "Testing power supplies: "

// Global variables
static unsigned char buf[256];
static char line[80], s[15], sVp[10], sVm[10];
static bool EPROM_read = false;
static int base_addr = 0;

// Initialization code
void setup() {
  // Set address lines as inputs, these will be set to outputs later
  pinMode(AD0, INPUT);
  pinMode(AD1, INPUT);
  pinMode(AD2, INPUT);
  pinMode(AD3, INPUT);
  pinMode(AD4, INPUT);
  pinMode(AD5, INPUT);
  pinMode(AD6, INPUT);
  pinMode(AD7, INPUT);

  // data lines are always inputs
  pinMode(DB1, INPUT);
  pinMode(DB2, INPUT);
  pinMode(DB3, INPUT);
  pinMode(DB4, INPUT);
  pinMode(DB5, INPUT);
  pinMode(DB6, INPUT);
  pinMode(DB7, INPUT);
  pinMode(DB8, INPUT);

  // 1702A power relay control
  pinMode(PWRRLY, OUTPUT);
  // Set EPROM power off
  digitalWrite(PWRRLY, LOW);

  // Initialize communications and print copyright and help
  Serial.begin(115200);
#ifdef VT100
  Serial.print("\x1b[2J"); // VT100 clear screen
  Serial.print("\x1b[H");  // VT100 cursor home
#endif
  Serial.println(COPY1);
  Serial.println(COPY2);
  Serial.println();
  Serial.println(HELP);
  Serial.println();
}

// Check power supplies voltages: returns true if OK, false otherwise
bool CheckPwr() {
  int i, valueP5, valueM9;
  float Vp, Vm;

  valueP5 = 0;
  valueM9 = 0;
  
  // Take and sum 16 readings
  for (i = 0; i < 16; i++) {
    valueP5 += analogRead(VP5);
    valueM9 += analogRead(VM9);
  };

  // Shift right values 4 places, i.e. divide by 16
  valueP5 >>= 4;
  valueM9 >>= 4;

  // Calculate +5V and -9V measured values
  // Vp = 2*5*valueP5/1024
  // Vm = (15.1*5*valueM9/1024-10*Vp)/5.1
  // (assuming resistors ideal values)
  Vp = 10.0*(float)valueP5/1024.0;
  Vm = (15.1*5*(float)valueM9/1024.0-10.0*Vp)/5.1;

#ifdef TESTCALC
  Serial.print("valueM9=");
  Serial.println(valueM9);
#endif

  // Convert float values to string (no float support in Arduino sprintf())
  dtostrf(Vp, 2, 2, sVp);
  dtostrf(Vm, 2, 2, sVm);

  // Check if voltages are in +-5% of nominal values
  if ((valueP5 > MINP5) and (valueP5 < MAXP5) and
      (valueM9 > MINM9) and (valueM9 < MAXM9))
    return true;
  else
    return false;
}

// Set address lines to input
void SetAddressIn(void) {
  pinMode(AD0, INPUT);
  pinMode(AD1, INPUT);
  pinMode(AD2, INPUT);
  pinMode(AD3, INPUT);
  pinMode(AD4, INPUT);
  pinMode(AD5, INPUT);
  pinMode(AD6, INPUT);
  pinMode(AD7, INPUT);
}

// Set address lines to output
void SetAddressOut(void) {
  pinMode(AD0, OUTPUT);
  pinMode(AD1, OUTPUT);
  pinMode(AD2, OUTPUT);
  pinMode(AD3, OUTPUT);
  pinMode(AD4, OUTPUT);
  pinMode(AD5, OUTPUT);
  pinMode(AD6, OUTPUT);
  pinMode(AD7, OUTPUT);
}

// Set AD lines to addr
void SetAddr(unsigned char addr) {
  if ((addr & 0x01) != 0) digitalWrite(AD0, HIGH); else digitalWrite(AD0, LOW);
  if ((addr & 0x02) != 0) digitalWrite(AD1, HIGH); else digitalWrite(AD1, LOW);
  if ((addr & 0x04) != 0) digitalWrite(AD2, HIGH); else digitalWrite(AD2, LOW);
  if ((addr & 0x08) != 0) digitalWrite(AD3, HIGH); else digitalWrite(AD3, LOW);
  if ((addr & 0x10) != 0) digitalWrite(AD4, HIGH); else digitalWrite(AD4, LOW);
  if ((addr & 0x20) != 0) digitalWrite(AD5, HIGH); else digitalWrite(AD5, LOW);
  if ((addr & 0x40) != 0) digitalWrite(AD6, HIGH); else digitalWrite(AD6, LOW);
  if ((addr & 0x80) != 0) digitalWrite(AD7, HIGH); else digitalWrite(AD7, LOW);
}

// Read a byte from DB lines
unsigned char ReadByte(void) {

  unsigned char val = 0;

  if (digitalRead(DB1) == HIGH) val += 1;
  if (digitalRead(DB2) == HIGH) val += 2;
  if (digitalRead(DB3) == HIGH) val += 4;
  if (digitalRead(DB4) == HIGH) val += 8;
  if (digitalRead(DB5) == HIGH) val += 16;
  if (digitalRead(DB6) == HIGH) val += 32;
  if (digitalRead(DB7) == HIGH) val += 64;
  if (digitalRead(DB8) == HIGH) val += 128;

  return val;
}

// Check data read in buf against EPROM content
bool CheckEPROM(void) {

  unsigned char rbyte;
  int addr;
  bool result = true;

  if (CheckPwr()) {
    // Activate EPROM power
    digitalWrite(PWRRLY, HIGH);
    delay(100);

    // Set address lines to OUTPUT
    SetAddressOut();

    for (addr = 0; addr < 256; addr++) {
      SetAddr(addr);
      delay(1);
      if (buf[addr] != (rbyte = ReadByte())) {
        sprintf(s, ERRORAT, addr, rbyte, buf[addr]);
        Serial.println(s);
        result = false;
      }
    }
  }

  // Set address lines to inputs
  SetAddressIn();

  // EPROM power off
  digitalWrite(PWRRLY, LOW);

  return result;
}

// Read EPROM content into buf
void ReadEPROM(void) {

  unsigned char rbyte;
  int addr;

  if (CheckPwr()) {
    // Turn power on
    digitalWrite(PWRRLY, HIGH);
    delay(100);

    // Set address lines to OUTPUT
    SetAddressOut();

    // Read all 256 bytes
    for (addr = 0; addr < 256; addr++) {
      SetAddr(addr);
      delay(1);
      buf[addr] = ReadByte();
    }
    Serial.println(DONEREADING);
    EPROM_read = true;

    if (CheckEPROM) {
      Serial.println(CHECKOK);
    } else {
      Serial.println(CHECKFAILED);
    }
  }

  // Set address lines to inputs
  SetAddressIn();

  // Turn power off
  digitalWrite(PWRRLY, LOW);
}

// Downloaded from www.pjrc.com and adapted by I1EPJ
//
// produce intel hex file output... call this routine with
// each byte to output and it's memory location. When a line
// is complete, print it on the serial (USB) port. After
// all data is written, call with end=1 (normally set to 0)
// so it will flush the data from its static buffer
//
void hex_out(int bytein, int memory_location, bool endflag)
{
  static int byte_buffer[MAXHEXLINE];
  static int last_mem, buffer_pos, buffer_addr;
  static int writing_in_progress = 0;
  register int i, sum;

  if (!writing_in_progress) {
    // initial condition setup
    last_mem = memory_location - 1;
    buffer_pos = 0;
    buffer_addr = memory_location;
    writing_in_progress = 1;
  }

  if ( (memory_location != (last_mem + 1)) || (buffer_pos >= MAXHEXLINE) \
       || ((endflag) && (buffer_pos > 0)) ) {
    // it's time to dump the buffer to a line via USB serial port
    sprintf(s, ":%02X%04X00", buffer_pos, buffer_addr);
    Serial.print(s);
    sum = buffer_pos + ((buffer_addr >> 8) & 255) + (buffer_addr & 255);
    for (i = 0; i < buffer_pos; i++) {
      sprintf(s, "%02X", byte_buffer[i] & 255);
      Serial.print(s);
      sum += byte_buffer[i] & 255;
    }
    sprintf(s, "%02X\r\n", (-sum) & 255);
    Serial.print(s);
    buffer_addr = memory_location;
    buffer_pos = 0;
  }

  if (endflag) {
    sprintf(s, ":00000001FF\r\n");  /* end of file marker */
    Serial.print(s);
    writing_in_progress = 0;
  }

  last_mem = memory_location;
  byte_buffer[buffer_pos] = bytein & 255;
  buffer_pos++;
}

// Dump buffer content in HEX INTEL format
void DumpEPROM(void) {

  unsigned char value;
  int addr;

  // Call hex_out for all 256 bytes
  for (addr = 0; addr < 256; addr++) {
    hex_out(buf[addr], addr+base_addr, false);
  }
  // End of dump
  hex_out(buf[addr], addr+base_addr, true);
}

// Executing forever
void loop() {

  char cmd[20];
  bool result;
  int addr,i;
  char c;

  if (!CheckPwr()) {
    // At least one power supply is out of specifications...
    
    // ... so power off EPROM to avoid damage...
    digitalWrite(PWRRLY,LOW);

    // ... notify user...
    Serial.println(PWROUTOFRANGE1);
    sprintf(line, PSVALUES, sVp, sVm);
    Serial.println(line);
    Serial.println(PWROUTOFRANGE2);
    // ... and  stop execution to avoid damaging the EPROM.
    for (;;);
  } else
    // Power supplies OK, we are ready!
    Serial.print(PROMPT);

    // Get command
    cmd[0]='\0';
    do {
      // Read characters, echo and put them in cmd until a CR or LF is received
      // Stop accepting characters when command buffer full
      c=Serial.read();
      if (c!=-1) { // We have a new character
        if (c>31) { // If it is not a control character, append and print it...
          if ((strlen(cmd))<19) { // ...but only if cmd buffer is not full
            strncat(cmd,&c,1);
            Serial.print((char)c);
          }
        }
 #ifdef BACKSPACE
        if (c==8) {
          // backspace
          if ((i=strlen(cmd))>0) {
            Serial.print("\x08");
            Serial.print(" ");
            Serial.print("\x08");
            cmd[i-1]='\0';
          }
        }
 #endif
      }
    } while ((c!='\r') && (c!='\n'));
    Serial.println("");
    
    // Now we have a command in cmd[]

  // Command parser
  switch (toupper(cmd[0])) {
    case '\0':
      // no command, ignore
      break;
    case 'B': // Read/set base address
      if (strlen(cmd)==1) {
        // no parameter given, show actual value
        sprintf(line,CURRENTBASE,base_addr);
        Serial.println(line);
      } else {
        base_addr = strtoul(&cmd[1],NULL,16);
        sprintf(line,BASEADDRSETTO,base_addr);
        Serial.println(line);
      }
      break;
    case 'C': // Check EPROM
      if (!EPROM_read) {
        // No data to use
        Serial.println(NODATA);
        break;
      }
      // We have data, check them against the EPROM content
      if (CheckEPROM()) {
        Serial.println(CHECKOK);
        // Errors, if any, are displayed by the CheckEPROM routine
      }
      break;
    case 'D': // Dump in HEX INTEL format
      if (!EPROM_read) {
        // No data to use
        Serial.println(NODATA);
        break;
      }
      // Have data, dump them
      DumpEPROM();
      break;
    case 'H': // Display help
      Serial.println(HELP);
      break;
    case 'I': // Display program info
      Serial.println(COPY1);
      Serial.println(COPY2);
      break;
    case 'P': // Check power supplies (+5 and -9V)
      Serial.print(TESTINGPS);
      result = CheckPwr();

      // Display results
      sprintf(line, PSVALUES, sVp, sVm);
      Serial.println(line);

      if (result)
        Serial.println(PWROK);
      else
        Serial.println(PWROUTOFRANGE1);
      break;
    case 'R': // Read EPROM
      ReadEPROM();
      break;
    case '\r': // CR/LF only, ignore
    case '\n':
      // no command
      break;
    case 'V': // HEX/ASCII listing
      if (!EPROM_read) {
        // No data to use
        Serial.println(NODATA);
        break;
      }
      for (addr=0; addr<256; addr+=16) {
        line[0]='\0';
        sprintf(s,"%04X ",addr+base_addr);
        strncat(line,s,5);
        for (i=addr; i<addr+16; i++) {
          sprintf(s,"%02X ", buf[i]);
          strncat(line,s,3);
        }
        strcat(line,"    ");
        for (i=addr; i<addr+16; i++) {
          c=buf[i];
          if ((c<32) or (c>126)) c='.';
            strncat(line,&c,1);            
        }
        Serial.println(line);
      }
      break;
#ifdef TESTRELAY
    case 'X':
      switch(cmd[1]) {
        case '1':
          digitalWrite(PWRRLY, HIGH);
          break;
        case '0':
          digitalWrite(PWRRLY, LOW);
          break;
      }
      break;
#endif
    default:
      // Not a valid command
      Serial.println(INVALIDCMD);
      break;
  }
}