Sunday, February 8, 2015

Arduino Retro Computer: Command Input

I've created a ScreenModel class that contains a display buffer, command buffer, and functions to interact between the two. My current plan is to have 2 operating modes: OS Mode and Program Mode. If a BASIC program is actively running, then the screen is in Program Mode, otherwise it's in OS Mode. The mode determines where keyboard inputs are processed.

I can define multiple screens and switch between them using the PageUp and PageDown keys. (Similar to how command line Linux works with the function keys.) That way a BASIC program may be running on screen #1, but the user can switch to screen #2 and enter system commands and/or execute a second program.

Please note: All of this is very subject to change. Also, the following code just highlights the command input; we'll cover the video output at a later time.

If anyone is curious my Arduino Sketch size is now 16,856 bytes (of a 258,048 byte maximum). I've got plenty of room for additional development!

Reminder of what the OS looks like (with the output window above and command window below):



Initializing the Keyboard


We covered this code earlier in the keyboard posts, but here's a quick recap in how we initialize the keyboard for input. We can poll the keyboard input with keyboard.available() and keyboard.read() - which we'll do in the primary Arduino loop() (covered below).

#include <PS2Keyboard.h>

const int KeyboardDataPin = 30;
const int KeyboardIRQpin =  2;

PS2Keyboard keyboard;

void setup()
{
  ...
  keyboard.begin(KeyboardDataPin, KeyboardIRQpin);

  // Initialize the serial input with the computer (for debugging).
  Serial.begin(9600);
  ...
}


Initializing the Screens


At this time, I'm defining only 2 screens. It's enough to test the Arduino functions without swallowing up even more of the limited RAM. I may keep it at 2 or I may increase it. The plan for this operating system is to be multitasking, not multithreading. Multiple user programs will be able to be executing at the same time, but it's all on one thread. The OS will have to alternate between the screens to process the BASIC commands.

I keep a pointer to the active screen. Pointers are defined by including a * after the variable type / before the variable name. That keeps the code a bit cleaner. Rather than keeping track of the active screen index and doing something such as:
screen[activeScreen].inputKeyboard(inputKey);
I can do:
activeScreen->inputKeyboard(inputKey);

There are many more uses of pointers throughout the code - especially when handling strings. If you don't know what they are, then I highly recommend you learn about them. (So far I've used them in very simple situations in this project; they can actually be quite powerful.) Unfortunately pointers have a bad reputation, but be cautious of anyone that tells you to "never use" a certain coding feature.

The code to initialize the screen:

const byte numberOfScreens = 2;

// Define the screens. (Each can operate in OS mode or run a program.)
ScreenModel screen[numberOfScreens];
// Keep track of which screen is active to know where to send inputs to.
ScreenModel *activeScreen;

void setup()
{
  ...
  // Initialize each of the screens (pass in the index of the screen to the constructor).
  screen[1].init((byte)1);
  screen[0].init((byte)0);

  // Set the default screen to index 0 and trigger a screen change.
  activeScreen = &screen[0];
  activeScreen->switchToScreen();
  ...
}


Main Program Loop


The main program loop monitors for input from the keyboard. If it's a Page Up or Page Down, then it switches the active screen. Otherwise, it sends the keyboard input to the active screen for processing.

void loop()
{
  // Loop through each screen and process pending actions in running programs. (Not done yet.)
  // Then check keyboard input for the current screen.
  // If the current screen is in OS mode, process inputs as OS commands.
  // If the current screen is in Program mode, send inputs to the Program.
  
  if (keyboard.available())
  {
    // read the next key
    char inputKey = keyboard.read();
    
    // Check if need to swap between OS screens.
    if (inputKey == PS2_PAGEDOWN)
    {
      // Switch to the next screen.
      Serial.print("[PgDn]");
      byte newScreenIndex = activeScreen->screenIndex + 1;
      if (newScreenIndex >= numberOfScreens)
      {
        newScreenIndex = 0;
      }
      activeScreen = &screen[newScreenIndex];
      activeScreen->switchToScreen();
    }
    else if (inputKey == PS2_PAGEUP)
    {
      // Switch to the previous screen.
      Serial.print("[PgUp]");
      byte newScreenIndex;
      if (activeScreen->screenIndex == 0)
      {
        newScreenIndex = numberOfScreens - 1;
      }
      else
      {
        newScreenIndex = activeScreen->screenIndex - 1;
      }
      activeScreen = &screen[newScreenIndex];
      activeScreen->switchToScreen();
    }
    else
    {
      // Send the command to the screen.
      activeScreen->inputKeyboard(inputKey);
    }

  }
  
  return;
}


ScreenModel Class


The bulk of the logic resides in the ScreenModel class. Rather than paste it all as one big block (then try to explain it out of the block), I'm going to look at functions individually. All of the remaining code belongs in the ScreenModel class defined here:

class ScreenModel
{

  public:

  byte screenIndex;

  bool init(byte newIndex)
  {
    screenIndex = newIndex;
    
    operatingMode = operatingModeOS;

    clearCommand();

    .. More init stuff ..

    return true;
  }

  .. A whole bunch of additional variables and functions described below ..

};


ScreenModel Mode Variables


Each screen needs to know which operating mode it is in to know how to process commands (and know if it's actively executing a program!). I've defined a simple enumeration of the operating mode and a byte to store the state.


  enum operatingModeEnum
  {
    operatingModeOS = 0,
    operatingModeProgram = 1
  };
  byte operatingMode;


ScreenModel Keyboard Input


Keyboard presses that are received in the main loop() are passed into the ScreenModel function inputKeyboard(inputKey).

inputKeyboard(inputKey) will relay that input to either  inputKeyboardOS(inputKey) or inputKeyboardProgram(inputKey) depending on the operating mode.

  // Called from the main loop() with the latest keyboard press.
  // Process the key in the OS or currently running program depending
  // on the current mode. 
  void inputKeyboard(char inputKey)
  {
    switch(operatingMode)
    {
      case operatingModeOS:
        inputKeyboardOS(inputKey);
        break;
      case operatingModeProgram:
        inputKeyboardProgram(inputKey);
        break;
    }
    return;
  }


ScreenModel Keyboard Program Input


inputKeyboardProgram(inputKey) hasn't been developed yet. At this point I just have a place holder such that if ESC is pressed it exits out of program mode.

  // Keyboard input intended to be processed by the currently running program.
  void inputKeyboardProgram(char inputKey)
  {
    if (inputKey == PS2_ESC)
    {
      operatingMode = operatingModeProgram;
    }
    // -- Add code to send to active program. --
    return;
  }


ScreenModel Keyboard OS Input


inputKeyboardOS(inputKey) will submitCommand() if enter is pressed, clearCommand() if ESC is pressed, removeCharacter() if delete is pressed, or addCharacter(inputKey) if a character / digit is pressed.

  // Keyboard input intended to be processed by the OS.
  void inputKeyboardOS(char inputKey)
  {
    // check for some of the special keys
    if (inputKey == PS2_ENTER)
    {
      // Submit the current command.
      Serial.println();
      submitCommand();
    }
    else if (inputKey == PS2_TAB)
    {
      // Autofill commands.
      Serial.print("[Tab]");
    }
    else if (inputKey == PS2_ESC)
    {
      // Clear the current command.
      clearCommand();
      Serial.print("[ESC]");
    }
    else if (inputKey == PS2_LEFTARROW)
    {
      // Navigate left through the command array.
      Serial.print("[Left]");
    }
    else if (inputKey == PS2_RIGHTARROW)
    {
      // Navigate right through the command array.
      Serial.print("[Right]");
    }
    else if (inputKey == PS2_UPARROW)
    {
      // Load previous command.
      Serial.print("[Up]");
    }
    else if (inputKey == PS2_DOWNARROW)
    {
      // Load next command.
      Serial.print("[Down]");
    }
    else if (inputKey == PS2_DELETE)
    {
      // Delete the command array character.
      Serial.print("[Del]");
      removeCharacter();
    }
    else if (inputKey >= 32 && inputKey <= 126)
    {
      // Type commands into the command array.
      Serial.print(inputKey);
      addCharacter(inputKey);
    }

    return;    
  }


ScreenModel Command Array


The lower portion of the screen is the command window. It's two lines, 50 characters per line, supporting 100 characters of input total. Each screen has its own command array.

We want the video output to specifically draw empty characters (to clear out what was previously drawn). Thus nowhere in the 100 characters will there ever be a null terminator (0), it will be an empty space instead. You will see many calls to drawCommandCharacter to specifically clear a character from the output. Again, video output will be described in a later post.

For better or worse, the entire command array can be filled; meaning there is no null terminator (not even at the last index of the array). When we have to do additional processing on the command array, we'll add in the null terminator as necessary. I'm not super excited about this approach so it's very likely to change, but it is how I wrote it a while ago (this is some of the first code I wrote for the Retro Computer, before I started the blog).

const int sizeOfCommandArray = 100;

  char commandArray[sizeOfCommandArray];
  int cursorPosition;

  // Character or digit was typed, add it to the command array.
  int addCharacter(char newChar)
  {
    commandArray[cursorPosition] = newChar;
    drawCommandCharacter(cursorPosition, newChar);
    
    return incrementCursorPosition();
  }
  
  // Delete key was pressed, delete from the command array.
  int removeCharacter()
  {
    int newPosition;
    if (cursorPosition == (sizeOfCommandArray - 1) &&
        commandArray[cursorPosition] != ' ')
    {
      // Special case if we're on the last character.
      // Delete the last character but don't decrement back.
      newPosition = cursorPosition;
      commandArray[cursorPosition] = ' ';
      drawCommandCharacter(cursorPosition, ' ');
    }
    else
    {
      // Standard case.
      // Delete the previous character and decrement back.
      newPosition = decrementCursorPosition();
      commandArray[cursorPosition] = ' ';
      drawCommandCharacter(cursorPosition, ' ');
    }
    return newPosition;
  }

  int incrementCursorPosition()
  {
    if (cursorPosition < (sizeOfCommandArray - 1))
    {
      cursorPosition ++;
    }
    return cursorPosition;
  }
  
  bool decrementCursorPosition()
  {
    if (cursorPosition > 0)
    {
      cursorPosition --;
    }
    return cursorPosition;
  }

  bool clearCommand()
  {
    for (int cursorLoop = 0; cursorLoop < sizeOfCommandArray; cursorLoop++)
    {
      if (commandArray[cursorLoop] != ' ')
      {
        drawCommandCharacter(cursorLoop, ' ');
      }
      commandArray[cursorLoop] = ' ';
      
    }
    cursorPosition = 0;

    return true;
  }


ScreenModel Submitting Commands


When an operating system command is submitted (by pressing enter), I want to display it in the output window, execute the command, and clear the input. Ultimately I also want to store the command in a history so I can jump between previously entered commands.

We're straying a little bit into how the video output works, but to be able to understand the submit code at all I need to define a couple constants:
const int numberOfScreenColumns = 50;
const int numberOfOutputColumns = numberOfScreenColumns;
const int sizeOfOutputColumnArray = numberOfOutputColumns + 1;  // Include null terminator.

The following is the start of the submit command. As more operating system commands are created, this list will grow. (And as it grows, I'll investigate alternatives to a bunch of if/elseif strcmp - a slow and CPU intensive operation.)

My next post will dive into the details of a few of these initial commands. I then may address the video output.

  bool submitCommand()
  {
    // Display the command entered in the output window.
    if (cursorPosition == 0)
    {
      // Empty command, just add a space to the output window.
      addOutputLine("");
    }
    else
    {
      // The command array is up to 100 characters (2 lines x 50 characters per line).
      // We have to split up the output into two separate output lines if the user exceeds
      // the size of one line (50 characters). 
      for (int cursorIndex = 0; cursorIndex < cursorPosition; cursorIndex += numberOfOutputColumns)
      {
        char commandLineOutput[sizeOfOutputColumnArray];
        memset(commandLineOutput, 0, sizeOfOutputColumnArray); // Ensure null terminated.
        // On the first loop through, copy the first 50 characters (numberOfOutputColumns).
        // If our cursor position exceeds 50 characters (numberOfOutputColumns), then
        // we'll loop through a second time to copy the remaining 50 characeters.
        memcpy(commandLineOutput, &commandArray[cursorIndex], numberOfOutputColumns);
        addOutputLine(commandLineOutput);
      }
    }
    
    // Parse Command
    // The command array is not null terminated, so we'll define a commandFormatted
    // array that we'll ensure is null terminated. commandFormatted is what the actual
    // command processing will be executed against.
    char commandFormatted[sizeOfCommandArray + 1];
    memcpy(commandFormatted, commandArray, sizeOfCommandArray);
    // Add null terminated.
    if (cursorPosition == (sizeOfCommandArray - 1) &&
        commandArray[cursorPosition] != ' ')
    {
      // Special case if we completely filled the command array.
      commandFormatted[sizeOfCommandArray] = 0;
    }
    else
    {
      // Standard case.
      commandFormatted[cursorPosition] = 0;
    }
    
    if (strcmp(commandFormatted, "help") == 0)
    {
      commandHelp();
    }
    else if (strcmp(commandFormatted, "memory") == 0)
    {
      commandMemory();
    }
    else if (strcmp(commandFormatted, "screen") == 0)
    {
      commandScreen();
    }
    else if (strncmp(commandFormatted, "=", 1) == 0)
    {
      commandMath(commandFormatted+1);
    }
    
    // Clear Command (And eventually save it into history.)
    clearCommand();
    
    return true;
  }


Copyright (c) 2015 Clinton Kam
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

No comments:

Post a Comment