/*
  Based on code written by Mark VandeWettering, K6HX, as found on his web site:
 
 http://brainwagon.org/2012/01/21/an-arduino-powered-ibm-ps2-morse-keyboard/
 
 You need four connections from the PS2 keyboard: 
 
 5V        Connects to pin 4 of the PS2 connector (and to 5v on the Arduino board)
 ground            "       3         "            (and to GND on the Arduino board)
 clock             "       5         "            (and to pin 3 on the Arduino board)
 data              "       1         "            (    "      4         "           )
 
 Pins 2 and 6 of the PS2 connector are not used. 
 
 Modified by Dr. Jack Purdum, W8TEE, Dec. 23, 2014
 
 */

// Version for the PS2 Keyboard
// using the library from http://www.pjrc.com/teensy/td_libs_PS2Keyboard.html

#include <PS2Keyboard.h>    // See above
#include <Wire.h>           // Comes with Arduino
IDE
// Get the LCD I2C Library here: 
// https://bitbucket.org/fmalpartida/new-liquidcrystal/downloads
// Move any other LCD libraries to another folder or delete them
// See Library "Docs" folder for possible commands etc.
#include <LiquidCrystal_I2C.h>

/*-----( Declare Constants )-----*/
/*-----( Deefine objects )-----*/
// set the LCD address to 0x27 for a 20 chars 4 line display
// Set the pins on the I2C chip used for LCD connections:
//                    addr, en,rw,rs,d4,d5,d6,d7,bl,blpol
LiquidCrystal_I2C lcd(0x27, 2, 1, 0, 4, 5, 6, 7, 3, POSITIVE);  // Set the LCD I2C address

#define DEBUG           1    // For debugging statements. Comment out when 
                             // not debugging and reduce code size
                             
#define CHARSONLY       1    // Only show
characters, not code

#define PS2CLOCKPIN     3
#define PS2DATAPIN      4


#define SIDETONEFREQ    700
#define ESCAPEKEY        27
#define PERCENTKEY       37
#define BACKSPACE       127      // NOTE: This should be 0x08, but my keyboard sees it as 0x7F??
#define NEWLINE         '\n'

#define LEDPIN           13
#define TONEPIN          10      // Used if you want sidetone via a small buzzer
#define LCDROWSIZE        2
#define LCDCOLSIZE       16

#define QUEUESIZE        128
#define QUEUEMASK (QUEUESIZE-1)

#define DEFAULTWPM	15	// MIN_WPM <= DEFAULT_WPM <= MAX_WPM
#define	MINWPM		 5
#define MAXWPM		50
#define NOCHARAVAILABLE -1
// ========================= Global data definitions =====================

// The coded byte values for the letters of the alphabet.
char ltab[] = {
  0b101,              // A
  0b11000,            // B

  0b11010,            //
C
  0b1100,             //
D
  0b10,               // E
  0b10010,            //
F
  0b1110,             //
G
  0b10000,            //
H
  0b100,              // I
  0b10111,            //
J
  0b1101,             //
K
  0b10100,            //
L
  0b111,              // M
  0b110,              // N
  0b1111,             //
O
  0b10110,            //
P
  0b11101,            //
Q
  0b1010,             //
R
  0b1000,             //
S
  0b11,               // T
  0b1001,             //
U
  0b10001,            //
V
  0b1011,             //
W
  0b11001,            //
X
  0b11011,            //
Y
  0b11100             //
Z
};

// The coded byte values for numbers. See text for explanation

char ntab[] = {
  0b111111,           // 0
  0b101111,           // 1
  0b100111,           // 2
  0b100011,           // 3
  0b100001,           // 4
  0b100000,           // 5
  0b110000,           // 6
  0b111000,           // 7
  0b111100,           // 8
  0b111110            //
9
};


char buff[QUEUESIZE];
char buffFullError[] = "== Buffer Full ==";
char charsLeftMsg[] = "chars left:";
byte lcdColPosition;
byte bufferActive;

int aborted = 0;
int bufferHead = 0;
int bufferTail = 0;
int charsLeft;
int charCount = 0;

int wordsPerMinute = DEFAULTWPM;         // Default speed
boolean sideTone = false;                // Default is no sidetone
boolean	speedChange = false;	         // 'true' indicates speed change requested

int ditlen = 1200 / wordsPerMinute;

PS2Keyboard kbd;        // Define keyboard object

void setup()
{
  pinMode(LEDPIN, OUTPUT);
  pinMode(TONEPIN, OUTPUT);
  
  kbd.begin(PS2DATAPIN, PS2CLOCKPIN);
  lcd.begin(LCDCOLSIZE, LCDROWSIZE);   // initialize
the lcd for 16 chars 2 lines, turn on backlight
  
  lcd.setCursor(2,0);                   // Splash screen
  lcd.print("PS2 Keyboard");
  lcd.setCursor(6,1);
  lcd.print("W8TEE");
  delay(2000);  
//  lcd.clear();
  lcdColPosition = 0;
  
#ifdef DEBUG
  Serial.begin(115200);
  Serial.println("PS2 keyboard ready:"); 
#endif
  bufferActive = 0;    // Assume no buffering 
}

void loop()
{
  ps2poll();                          // Look for a keystroke

  if (bufferHead != bufferTail) {     // If there's a
keystroke present in the buffer...
    send(BufferPopCharacter());       //
...send it along.
  }  
}

/*****
 * This method adds a character to the buffer.
 * 
 * Parameters:
 * char ch      the character to add
 * 
 * Return value:
 * void
 *****/
void BufferAdd(char ch)
{
  buff[bufferTail++] = ch;
  bufferTail &= QUEUEMASK;   
}

/*****
 * This method adds a character to the buffer. See text as to how this method shares the same method name as
 * the one above without creating a duplicate definition error
 * 
 * Parameters:
 * char ch      the character to add
 * 
 * Return value:
 * void
 *
 *  CAUTION: If any part of an existing message is in the buufer when this function is called, it is lost.
 *****/
void BufferAdd(char *s)
{

  int len;
  len = strlen(s);
  if (len > QUEUEMASK) { // If the message is too
long, kill it.
    *s = '\0';
    return;
  }
  BufferReset();         // Override anything
in buffer  
  while (*s)
    BufferAdd(*s++);
}

/*****
 * This method removes a character from the buffer.
 * 
 * Parameters:
 * void
 * 
 * Return value:
 * char        the character that is copied from the buffer
 *****/
char BufferPopCharacter()
{
  char ch;
  ch = buff[bufferHead++];
  bufferHead &= QUEUEMASK;
  return ch;
}


/*****
 * This method reset the buffer.
 * 
 * Parameters:
 * void
 * 
 * Return value:
 * void
 *****/
void BufferReset()
{
  bufferHead = 0;                // Reset to first character in buffer
  bufferTail = 0;
  charCount = 0;
  memset(buff, 0, QUEUESIZE);    // Fast clear of previous
contents
  lcdColPosition = 0;
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.write(charsLeftMsg);

}

/*****
 * This function lets you observe the keystrokes as they are sent. With buzzer, it
 * syncs with display.
 *
 * Parameters:
 * void
 * 
 * Return value:
 * void
 *****/

void ShowKeystrokes(char ch)
{
  static char tempBuffer[LCDCOLSIZE + 1];
  
  ch = toupper(ch);                    // Don't mess with lower case
  charsLeft = QUEUESIZE - bufferTail;  // How much buffer
space left?
  lcd.setCursor(0, 0);
  lcd.write(charsLeftMsg); 
  lcd.write("   ");
  lcd.setCursor(11, 0);
  lcd.print(charsLeft);    
  
  if (lcdColPosition < LCDCOLSIZE) {    // If we
haven't shown col-size characters (e.g., 16)
    tempBuffer[lcdColPosition] = ch;    // just put
them on the screen
    lcd.setCursor(lcdColPosition++, 1);
    lcd.write(ch);
  } else {                              // If we
have shown more, scroll the message as the chars come in
    memmove(tempBuffer, &tempBuffer[1], LCDCOLSIZE);
    tempBuffer[LCDCOLSIZE - 1] = ch;
    lcd.setCursor(0, 1);
    lcd.write(tempBuffer);   
  }
}

  
/*****
 * This method polls the keyboard looking for a keystroke. If a character is available, it is added to the 
 * keyboard input buffer. The inline modifier is a hint to the compiler to generate inline code if possible,
 * to improve the performance of the method.
 * 
 * Parameters:
 * void
 * 
 * Return value:
 * void
 *****/
inline void ps2poll()
{
  char ch;
  int autoSendFlag = 0;
  
  while (kbd.available()) {
    
    if (((bufferTail + 1) % QUEUESIZE) == bufferHead) {	// is buffer full ?
      lcd.setCursor(0, 0);
      lcd.write(buffFullError);
#ifdef DEBUG
      Serial.println(buffFullError);
#endif
      break;
    } else { 
      ch = kbd.read();
      ch = toupper(ch);
#ifdef DEBUG
      Serial.print("ch = ");
      Serial.println(ch, HEX);
#endif
      if (ch == 0x7f) {
#ifdef DEBUG
      Serial.println("Read backspace");
#endif
        continue;
      }
#ifdef DEBUG
      Serial.println(ch);
#endif
 
      switch (ch) {
        case ESCAPEKEY:
// case '\033':
          BufferReset();
          ClearLCD();
          
  #ifdef DEBUG
          Serial.flush();
          Serial.println("== Buffer reset ==");
  #endif
          aborted = 1;
          break;
        case PERCENTKEY:
          BufferAdd("CQ CQ CQ DE W8TEE W8TEE W8TEE
K\r\n");
          break;
        case '(':
// Start buffering without transmitting
          bufferActive = 0;
          DelayedTransmit();
          return;
        case '#':
// Change keying speed
          ChangeSendingSpeed();
          speedChange = true;
          break;
        case '~':
// Change sidetone. Default is no tone (0)
          sideTone = !sideTone;
          return;
        case '*':
// Generate random letters and numbers
                               // Left as exercise for reader!!
          break;
        default:
          BufferAdd(ch);
          break;
      }
      ShowKeystrokes(ch);    
    }
  }
}

/*****
 * This method generates a delay based on the millis() method call. This is the preferred way to perform a
 * delay because it doesn't put the board to sleep during the delay.
 * 
 * Parameters:
 * unsigned ling ms    the number of milliseconds to delay
 * 
 * Return value:
 * void
 *****/
void mydelay(unsigned long ms)
{
  unsigned long t = millis();
  while (millis()-t < ms) {
    ps2poll();
  }
}

/*****
 * This method generates a tone if speakers are hooked to the system. The TONEPIN value determines which pin
 * is used to generate the tone.
 * 
 * Parameters:
 * void
 * 
 * Return value:
 * void
 *****/
void scale()
{
  long f = 220L;
  int i;

  for (i=0; i<=12; i++) {
    tone(TONEPIN, (int)f);
    f *= 1059L;
    f /= 1000L;
#ifdef DEBUG
    Serial.println(f);
#endif
    delay(300);
  }
  noTone(TONEPIN);     
}

/*****
 * This method generates a single dit in the sequence necessary to form a character in Morse Code.
 * 
 * Parameters:
 * void
 * 
 * Return value:
 * void
 *****/
void dit()
{ 
  if (sideTone) {
    digitalWrite(LEDPIN, HIGH);
    tone(TONEPIN, SIDETONEFREQ);
    mydelay(ditlen);
    digitalWrite(LEDPIN, LOW);
    noTone(TONEPIN);
    mydelay(ditlen);
  }
#ifdef CHARSONLY
  return;
#endif

#ifdef DEBUG
  Serial.print(".");
#endif 
}

/*****
 * This method generates a single dah in the sequence necessary to form a character in Morse Code.
 * 
 * Parameters:
 * void
 * 
 * Return value:
 * void
 *****/
void dah()
{
  if (sideTone) {
    digitalWrite(LEDPIN, HIGH);
    tone(TONEPIN, SIDETONEFREQ);
    mydelay(3*ditlen);
    digitalWrite(LEDPIN, LOW);
    noTone(TONEPIN);
    mydelay(ditlen);
  }
#ifdef CHARSONLY
  return;
#endif
#ifdef DEBUG
  Serial.print("_");
#endif
}


/*****
 * This method generates the necessary dits and dahs for a particular code.
 * 
 * Parameters:
 * char code    the byte code for the letter or number to be sent as take from the ltab[] or ntab[] arrays.
 * 
 * Return value:
 * void
 *****/
void sendcode(char code)
{
  int i;

  for (i=7; i>= 0; i--) {  // Look for start
bit
    if (code & (1 << i))
      break;
  }
  for (i--; i>= 0; i--) {  // Remaining bits are the
actual Morse code
    if (code & (1 << i))
      dah();
    else
      dit();
  }
  mydelay(2*ditlen);	// space between letters
#ifdef DEBUG
  Serial.print("");
#endif
}

/*****
 * This method translates and sends the character.
 * 
 * Parameters:
 * char ch      the character to be translated and sent
 * 
 * Return value:
 * void
 *****/
void send(char ch)
{
  int index;

  ch = toupper(ch);
 
  if (speedChange) {	            // use new
speed
    ditlen = 1200 / wordsPerMinute;		
    speedChange = false;
  }  
  
  if (isalpha(ch)) {      // Is it an alpha
character?
    index = ch - 'A';     // Yep...Calculate an index into the letter array if a letter...
    sendcode(ltab[index]);
    return;
  }
  if (isdigit(ch)) {      // Is it a digit
character?
    sendcode(ntab[ch-'0']); //
Yep...Calculate an index into the numbers table if a number...
    return;
  } 
  if (ch == ' ' || ch == '\r' || ch == '\n') {  // Is it
whitespace?
    mydelay(4 * ditlen);                        // Yep.
    return;
  }     
                              
  switch (ch) {                  // Punctuation and
special characters. NOTE; Tree depth is 6, so
    case '.':                    // characters max out at 7 dit/dah combinations
      sendcode(0b1010101);
      break;
    case ',':
      sendcode(0b1110011);
      break;
    case  '!':
      sendcode(0b1101011);
      break;
    case  '?':
      sendcode(0b1001100);
      break;
    case  '/':
      sendcode(0b110010);
      break;
    case  '+':
      sendcode(0b101010);
      break;
    case  '-':
      sendcode(0b1100001);
      break;
    case  '=':
      sendcode(0b110001);
      break;
    case  '@':               
     sendcode(0b1011010);
      break;
    case '\'':               // ' apostrophe
      sendcode(0b1011110);
      break; 
    case '(': 
      sendcode(0b110110); 
      break;
    case ')': 
      sendcode(0b1101101); 
      break;
    case ':': 
      sendcode(0b1111000); 
      break;
    case ';': 
      sendcode(0b1101010); 
      break;
    case '"': 
      sendcode(0b1010010); 
      break;
     
    default:
      break;
  }

#ifdef DEBUG
  if (!aborted) {
    Serial.print(ch);
    if (ch == 13) 
      Serial.print((char) 10);
  }
#endif
  aborted = 0;
}



/*****
 * This method flashes the LED on pin 13 if the input buffer is close to being full.
 *
 * Parameters:
 *   void
 * 
 * Return value:
 *   void
 *****/

void FlashBufferFullWarning()
{
  int i;
#ifdef DEBUG
  Serial.print("************* Approaching buffer full ==============");
  //Serial.println(longMessageProcessing);
#endif

  for (i = 0; i < 10; i++) {
    digitalWrite(LEDPIN, HIGH);  // Visual
    delay(100);
    digitalWrite(LEDPIN, LOW);
    delay(100);
    tone(TONEPIN, SIDETONEFREQ);         //
Audio...if available
    delay(100);
    noTone(TONEPIN);
    delay(100);
  }
}

/*****
 * This method buffer all keystrokes after reading the leading '(' until it reads a ')'. At that
 * time, the buffer is sent to the transmitter.
 * 
 * Parameters:
 *   void
 * 
 * Return value:
 *   int          the number of characters buffered.
 *****/

int DelayedTransmit()
{
  char ch;
  int i;

  memset(buff, '\0', sizeof(char));
  
  bufferTail = 0;
  BufferReset();              // Clear the buffer and start over...
#ifdef DEBUG
  Serial.println("####  in DelayedTransmit()");
#endif        

  while (true) {
    if (kbd.available()) {
      ch = kbd.read();
      if (ch == ESCAPEKEY) {  //
They want to terminate message
        BufferReset();        // Clear all and start over
        return 0;
      }
      if (ch == '(') {
        BufferReset();        // Clear all and start over
        continue;
      }
//      ShowKeystrokes(ch);
      //charCount++;
      if (ch == ')' ||
charCount == QUEUEMASK || ch == NEWLINE) { // Is long message finished or terminated?
#ifdef DEBUG
  Serial.print("#### after closed:  charCount = ");
  Serial.println(charCount);
#endif        
        bufferActive = 0;
        lcdColPosition = 0;
        lcd.clear();
        lcd.setCursor(0,1);
        for (i = 0; i < charCount; i++) {
 #ifdef DEBUG
  Serial.print("In for send[i] = ");
  Serial.print(buff[i]);
  Serial.print("  i = ");
  Serial.println(i);
#endif        
          ShowKeystrokes(buff[i]);
          send(buff[i]);
        }
        BufferReset();              // Clear the buffer and start over...
        break;
      } else {
#ifdef DEBUG
  Serial.print("--> charCount = ");
  Serial.print(charCount);
  Serial.print(" character = ");
  Serial.println(ch);
#endif        
      ShowKeystrokes(ch);
        buff[charCount++] = ch;
        if (charCount > (QUEUEMASK - 20)) {   // Approaching full buffer
          FlashBufferFullWarning();
        }
      }
    }
  }
  return charCount;
}

/*****
 * This function allows the sending speed to change
 * 
 * Parameters:
 *   void
 * 
 * Return value:
 *   void
 *****/
void ChangeSendingSpeed()
{
  char ch;
  
  int wereDone = 0;

#ifdef DEBUG  
  Serial.print(" wordsPerMinute at start =");
  Serial.println(wordsPerMinute);     
#endif
  while (true) {
    if (kbd.available()) {
      ch=kbd.read();
#ifdef DEBUG  
      Serial.print("Speed change ch =");
      Serial.println(ch);
#endif
      switch (ch) {
        case '>':            // Increase WPM by 1
          wordsPerMinute++;
          break;
        case '<':            // Decrease WPM by 1
          wordsPerMinute--;
          break;
        case '#':
// All changes are complete
          wereDone = 1;
          break;
        default:
          break;
      }
    }
    if (wereDone)
      break;
  }
  ditlen = 1200 / wordsPerMinute;
}

/*****
 * This method simply clears the LCD display
 * 
 * Parameters:
 *   void
 * 
 * Return value:
 *   void
 *****/
void ClearLCD()
{
  lcd.clear();
  lcd.setCursor(0, 0);
}