Monday, July 6, 2015

Arduino Retro Computer: BASIC Interpreter (Variable Input)

Now that my BASIC interpreter has the ability to store variables, I need methods for the user to input data into a program.


I've implemented two ways for the user to interact with his program: Keyboard and Joystick. All of the code below goes inside the ScreenModel.

Input via Keyboard


Each ScreenModel in my operating system supports an OS mode and Program mode. If in OS mode, the computer just waits for a command from the user. If in Program mode, the computer is continuously executing the next line of BASIC code.

I already accepted keyboard commands in OS mode, but I didn't have any way for the user of an active program to input data. I've now implemented the Input statement for the keyboard.

I needed a flag to halt execution of the BASIC code when we're waiting for keyboard input from the user. In comes the boolean pendingProgramInput. If true, the computer will stop processing statements until the user submits his or her command (pushes enter). This will be covered more later, but I also needed to know the name of the variable that will store the keyboard input: pendingProgramInputVariableName

  bool pendingProgramInput;
  char pendingProgramInputVariableName[sizeOfVariableName+1];

  bool init(byte newIndex)
  {
    .
    .
    pendingProgramInput = false;
    memset(pendingProgramInputVariableName, 0, sizeOfVariableName+1);
    .
    .
  }


The processEvents function is called continuously to execute the user's lines of code. If we're pending program input then skip executing the code.


  bool processEvents()
  {
    switch(operatingMode)
    {
      case operatingModeOS:
        // No events to do.
        break;
      case operatingModeProgram:
        if (!pendingProgramInput)
        {
          // We're not waiting for user input, process the next command.
          programExecuteCurrentMemoryAddress();
        }
        break;
    }
    
    return true;
  }

All keyboard presses go to inputKeyboard...

  // 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;
  }

Those keyboard presses then get funneled to inputKeyboardProgram if there is an active program. The ESC key is always supported to immediately stop execution of the program.

If however we're pending program input, then store the keys into the command string (similar to OS mode).

  // Keyboard input intended to be processed by the currently running program.
  void inputKeyboardProgram(char inputKey)
  {
    if (inputKey == PS2_ESC)
    {
      stopProgram();
    }
    else if (pendingProgramInput)
    {
      if (inputKey == PS2_ENTER)
      {
        // Submit the value to the variable.
        submitProgramKeyboardInput();
      }
      else if (inputKey == PS2_ESC)
      {
        // Clear the current command.
        clearCommand();
      }
      else if (inputKey == PS2_LEFTARROW)
      {
        // Navigate left through the command array.
        // Not yet implemented.
      }
      else if (inputKey == PS2_RIGHTARROW)
      {
        // Navigate right through the command array.
        // Not yet implemented.
      }
      else if (inputKey == PS2_DELETE)
      {
        // Delete the command array character.
        removeCharacter();
      }
      else if (inputKey >= 32 && inputKey <= 126)
      {
        // Type commands into the command array.
        addCharacter(inputKey);
      }
    }

    return;
  }

When the program starts, be sure to clear the pendingProgramInput flag. (Just in case the user aborted the last program while in an input!)

  bool startProgram()
  {
    .
    .
    // Clear input flag.
    pendingProgramInput = false;

    operatingMode = operatingModeProgram;
    .
    .
  }

I created one new statement, Input, that gets read along with all the other statements inside parseProgramString:

  bool parseProgramString(char *programString)
  {
    .
    .
    else if (!strncmp(programString, "input ", 6))
    {
      parseProgramInput(programString + 6);
    }
    .
    .
  }

Parsing the input statement is actually very simple. All it does is store the name of the variable that we'll write to and sets the pendingProgramInput flag to true.

  bool parseProgramInput(char *commandString)
  {
    if (getProgramVariableType(commandString)!=undefinedType)
    {
      // Valid variable name given.
      // Store in pending input variable name and set the flag that we're accepting
      // input for the program.
      strncpy(pendingProgramInputVariableName, commandString, sizeOfVariableName+1);
      pendingProgramInput = true;
    }
    else
    {
      pendingProgramInput = false;
    }
    
    advanceNextProgramMemoryAddress();
    
    return true;
  }

The real work happens when the user pushes ENTER. submitProgramKeyboardInput gets called which writes whatever the user had entered in the command line to the variable denoted in the Input statement. The pendingProgramInput flag is also cleared so the program may resume executing.

  bool submitProgramKeyboardInput()
  {
    // User submitted a keyboard command to be saved into a variable of the program.

    // 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;
    }
    char *inputString = &commandFormatted[0];
    
    switch(getProgramVariableType(pendingProgramInputVariableName))
    {
      case stringType:
        {
          char stringValue[sizeOfCommandArray+1];
          memset(stringValue, 0, sizeOfCommandArray);
          strncpy(stringValue, inputString, sizeOfCommandArray + 1);
          programVariableWriteString(pendingProgramInputVariableName, stringValue);
        }
        break;
      case integerType:
        {
          long integerValue = strtol(inputString, &inputString, 10);
          programVariableWriteInt32(pendingProgramInputVariableName, integerValue);
        }
        break;
      case floatType:
        {
          float floatValue = (float)strtod(inputString, &inputString);
          programVariableWriteFloat32(pendingProgramInputVariableName, floatValue);
        }
        break; 
    }
    
    clearCommand();
    
    // Done with keyboard input on this variable.
    pendingProgramInput = false;
    
    return true;
  }

Input via Keyboard Example


10 print "Enter your name:"
20 input name$
30 print "How old are you?"
40 input age%
50 print "Your name is:"
60 print name$
70 print "Your age is:"
80 print age%


(Note: I'm not automatically echoing what the user types to the screen. I think I like it that way, but I may change my mind.)

Input via Joystick


Input via the joysticks is actually much easier as we can piggy back off a lot of existing logic. One new statement, read joystick #, was added. When called it creates/updates variables that store the various states of the joystick.

All the programmer must do is call read joystick then use if or print commands as desired on the joystick variables:
j1select% (1 = Joystick 1 select button pressed)
j1left% (1 = Joystick 1 left button pressed)
j1top% (1 = Joystick 1 top button pressed)
j1right% (1 = Joystick 1 right button pressed)
j1bottom% (1 = Joystick 1 bottom button pressed)
j1x! (Joystick 1 X axis from -1.0 to 1.0)
j1y! (Joystick 1 Y axis from -1.0 to 1.0)

  bool parseProgramString(char *programString)
  {
    .
    .
    else if (!strncmp(programString, "read joystick ", 14))
    {
      parseProgramReadJoystick(programString + 14);
    }
    .
    .
  }

  bool parseProgramReadJoystick(char *readString)
  {
    switch(*readString)
    {
      case '1':
        {
          programVariableWriteInt32("j1select%", (int)joysticks[0].selectPressed());
          programVariableWriteInt32("j1left%", (int)joysticks[0].leftPressed());
          programVariableWriteInt32("j1top%", (int)joysticks[0].topPressed());
          programVariableWriteInt32("j1right%", (int)joysticks[0].rightPressed());
          programVariableWriteInt32("j1bottom%", (int)joysticks[0].bottomPressed());
          programVariableWriteFloat32("j1x!", joysticks[0].analogX());
          programVariableWriteFloat32("j1y!", joysticks[0].analogY());
        }
        break;
      case '2':
        {
          programVariableWriteInt32("j2select%", (int)joysticks[1].selectPressed());
          programVariableWriteInt32("j2left%", (int)joysticks[1].leftPressed());
          programVariableWriteInt32("j2top%", (int)joysticks[1].topPressed());
          programVariableWriteInt32("j2right%", (int)joysticks[1].rightPressed());
          programVariableWriteInt32("j2bottom%", (int)joysticks[1].bottomPressed());
          programVariableWriteFloat32("j2x!", joysticks[1].analogX());
          programVariableWriteFloat32("j2y!", joysticks[1].analogY());
        }
        break;
    }

    advanceNextProgramMemoryAddress();

    return true;
  }

Input Via Joystick Example


10 read joystick 1
20 print j1x!
30 print j1y!
40 goto 10



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