Shop OBEX P1 Docs P2 Docs Learn Events
How do you build a menu for data input? — Parallax Forums

How do you build a menu for data input?

tosjduenfstosjduenfs Posts: 37
edited 2013-11-24 16:28 in Propeller 1
I've been working on a telescope tracking system and I've got most of my motor control and tracking calculations worked out but now it is time to start tying them all together. Part of the system will be a hand controller with a 12 key number pad, a directional pad with up,down,left,right and center buttons, and a small joystick for slewing the telescope manually.

When I first start up the telescope tracker I need to input information such as latitude, longitude, time, etc... I thought making the menu and getting the data would be the easiest part of the coding but it is turning out to be more difficult than expected and I'm not sure how to proceed and structure it. Here is where I have started:
CON


   _clkmode = xtal1 + pll16x 
  _xinfreq = 5_000_000


  lcdpin = 0               'serial lcd pin assignment
 
  k1 = 9                   'keypad pin assignments
  k2 = 10
  k3 = 11
  k4 = 12
  k5 = 13
  k6 = 14
  k7 = 15
   
     
OBJ


   LCD : "SparkFun_Serial_LCD"
   
PUB  Main 


   LCD.init(lcdpin,9_600,2,16)    'initialize LCD
   waitcnt(clkfreq/10 + cnt)      'wait for LCD to initialize
   LCD.cls                        'clear lcd
   lcd.cursor(2)                  'set cursor type


   lcd.str(string("ENTER LOCATION:"))  'display first page of menu:
   lcd.gotoxy(0,1)
   lcd.str(string("000"))              'ENTER LOCATION:
   lcd.putc(223)                       '000d00'W 00d00'N    'where d is the actual degree symbol
   lcd.str(string("00'W 00"))
   lcd.putc(223)
   lcd.str(string("00'N"))
   lcd.gotoxy(0,1)


   repeat
     lcd.dec(keypad)
     waitcnt(clkfreq/4+cnt)
   
PRI keypad 


'keypad sets the columns as outputs and the rows as inputs.  It sweeps each column indidually and looks for a keypress on a row. When a keypress is detected it returns the associated value  
    
    dira[k1] := dira[k3] := dira[k5] := 1
    dira[k2] := dira[k4] := dira[k6] := dira[k7] := 0


    repeat 
      
      outa[k1] := 1                'set column one high then check rows for key press
        if ina[k2] == 1
          return 2
        elseif ina[k4] == 1
          return 0
        elseif ina[k6] == 1
          return 8
        elseif ina[k7] == 1
          return 5
      outa[k1] := 0


      outa[k3] := 1               'set column three high then check rows for key press 
        if ina[k2] == 1
          return 1
        elseif ina[k4] == 1
          lcd.cursorleft                 '* button
          waitcnt(clkfreq/4+cnt)
        elseif ina[k6] == 1
          return 7
        elseif ina[k7] == 1
          return 4 
      outa[k3] := 0
      
      outa[k5] := 1               'set column five high then check rows for key press 
        if ina[k2] == 1
          return 3
        elseif ina[k4] == 1
          lcd.cursorright          '# button
          waitcnt(clkfreq/4+cnt)
        elseif ina[k6] == 1
          return 9
        elseif ina[k7] == 1
          return 6 
      outa[k5] := 0
      

Here is what I get when I run this:
http://youtu.be/kNgQ94qL5Do

I have the * and # buttons on the keypad set to move the cursor left and right. I still need to make the d pad. What I'm really trying to figure out is how to take the numbers I put on the screen and put them into the variables they represent in my code.

In the second line there are 4 variables: longitude degrees, longitude minutes, latitude degrees, latitude minutes. I'd like to be able to enter the values and be able to go back to fix mistakes before going to the next screen. I'm just not sure how to take the values I put up on the screen. If I could I would just have the lcd tell me what values are in certain positions on the screen when I ask it to but I don't think I can do that. I don't think I there was anything useful in the obex for making menus, if anyone has any ideas I would appreciate it.

Comments

  • JonnyMacJonnyMac Posts: 9,107
    edited 2013-10-25 08:44
    I've coded a couple Propeller projects with menus -- I tend to use a state machine which allows me to call main- and sub-menu methods. I also have input methods which allow me to manipulate standard numeric fields, and for one project, a clock entry. With the limited resources of a microcontroller it's a bit touch to genericise something like menus without burning up all your program space, hence you're going to have to go custom.

    In my projects I tend to define LCD menu screens in DAT tables which allows me change screens quickly with a method call (the screen displayed is based on the main-menu state). Then I call a handler for that screen which knows where the fields are and how to manipulate them.
  • YanomaniYanomani Posts: 1,524
    edited 2013-10-25 11:03
    tosjduenfs

    Till this point, your input method sounds great.

    At last, there are only two lines of text, and you wisely decided to use the first one, to explain the meaning of the second one.
    As long as you keep using defined templates to gather data input, you won't have to worry about "Insert" or "Delete" alike keys, just the ones you have in fact.

    IMHO, if you decide not using extra buttons, in a separate keypad, to input an "Enter" alike key stroke and other suitable function keys, you could use the detection of a simultaneous pressing of both * and #, to create into your decision tree, suitable branches to deal with them.
    As for storing gathered data, you can do it in a multitude of ways, its only a matter of each programmer's personal preferences.
    Testing various options will help you decide which "style" best fits yours.

    In cases like the one you has shown us at the video, my trend is to point a base register, to value place holders at local memory, and an index to the leftmost (or rightmost) item, whichever best fits; It's related to the place where the cursor was left, at the very beggining of data input screen editing routine; Then you can be confident of the following operator movements.
    Then, as the operator progresses, doing left or right movements using the "cursor positioning" keys, the index variable is incremented or decremented accordingly.

    You just have to set upper and lower limits to the index value, to don't mess with neighboring memory areas.
    For each valid keystroke, you will have only two options:
    - store the new value at its memory position, and decide if the chosen input method determines a left or right cursor movement, or not;
    - if you choose to update cursor's position, adjust the index variable accordingly. If you reached leftmost or rightmost display line position, then you must choose staying there, or executing some line wrapping code, but your current application doesn't appear to benefit of such an strategy.

    To be true, short-key-counts keypad projects, are a sure head ache to deal with, when it comes to wrapping data or text input. But its your decision, and operator's convenience, that will prevail at the end.

    Since Propellers are, by nature, multitasking processors, you can act wisely, keeping the original value, the one displayed at the beggining of data input routine, at Hub ram, and use a copy to deal with, during data input editing.
    After the input and edit process exits, thru the detection of a "Enter" keystoke, you can forward the fresh new value to Hub ram, and sign another waiting Cog's routine, to deal with the mechanical positioning system.

    As long as you are entering some new coordinates, if you chose to dinamically update its contents, in parallel with the motor steering mechanics controller, then, IMHO, I'm about to suggest you to update it from right to left, minute units first, sparing your system from doing large movements at first.
    But its just my point of view, your own choices will dictate project's final behavior.

    Good luck!:thumb:

    I hope it can help a bit

    Yanomani
  • ChrisGaddChrisGadd Posts: 310
    edited 2013-10-25 13:11
    Here's a quick program that accepts five digits from the serial terminal, packs them into a single variable, prohibits out-of-range values, and keeps track of cursor position allowing overwrites.
    Tested using my 4x20 parallel LCD, which I've commented out. Also shows the value on the serial terminal.
    {{
    
    }}
    CON
      _clkmode = xtal1 + pll16x                                                   
      _xinfreq = 5_000_000
    
    'LCD pins
    RS_pin   = 23                                           '  RS  - low for commands, high for text 
    RW_pin   = 22                                           '  R/W - low for write, high for read    
    E_pin    = 21                                           '  E   - clocks the data lines           
    D4_pin   = 16                                           '  D4 through D7 must be connected to consecutive pins
    'D5      = 17                                           '     with D4 on the low pin 
    'D6      = 18
    'D7      = 19
    
    OBJ
      FDS : "FullDuplexSerial"
    ' LCD : "LCD SPIN driver - 4x20"
    
    PUB Main | number
    ' LCD.start(E_pin,RW_pin,RS_pin,D4_pin)
      FDS.start(31,30,0,115_200)
      waitcnt(cnt + clkfreq)
    
    ' LCD.send(%0000_1111)                                                          ' Turn LCD on, cursor on, blink on
    
      number := 00000
      
      repeat
        FDS.tx($0D)
        number := update_number(number)
        FDS.tx($0D)
        FDS.str(string("Number stored as: "))
        FDS.dec(number)
                                 
    PRI update_number (number) | key, i, temp, cursor
    {
      This method creates a 5-digit number representing degrees and minutes
      To extract: degrees := number / 100
                  minutes := number // 100
      Limits maximum degrees to 359 and minutes to 59
    }
      cursor := 1
      i := 10000     
      repeat until key == $0D                                                       
    '   LCD.move(1,cursor)                                                          ' Update cursor position to show currently selected digit
        key := FDS.rx
        case key
          "0".."9":
            temp := (number - (number / i // 10 * i)) + ((key - "0") * i)           ' Subtract the old number and add the new one
            if temp / 100 < 360 and temp // 100 < 60                                ' Only update if degrees < 360 and minutes < 60
              number := temp                                                     
              i := i / 10 #> 1                                                      ' Update the divisor, limit minimum to 1
              if ++cursor == 4                                                      ' Skip over the degree sign
                cursor := 5
              cursor <#= 6
          $08:                                                                      ' $08 is backspace 
            i := i * 10 <# 10000                                                    ' Update the divisor, limit maximum to 100_000
            if --cursor == 4                                                        ' Skip over the degree sign
              cursor := 3
            cursor #>= 1
        display_number(number)
      return number
    
    PRI display_number(number)
    
    ' LCD.clear
      FDS.tx($00)                                                                   ' clear screen
      dec(number / 100,3)                                                           ' display degrees
    
    ' LCD.send("°")
      FDS.tx("°")
      dec(number // 100,2)                                                          ' display minutes
    
    PUB Dec(value,digits) | i, x                                                    ' Taken from FullDuplexSerial and modified to display fixed-width fields
    
      i := 1
      repeat digits - 1  
        i *= 10                                                                     ' Initialize divisor
      repeat digits                                         
        FDS.Tx(value / i + "0")                                                     ' Display most-significant digit
    '   LCD.send(value / i + "0")
        value //= i                                                                 ' Move remainder into value
        i /= 10                                                                     ' Update divisor                
    
  • tosjduenfstosjduenfs Posts: 37
    edited 2013-10-26 01:29
    Thanks for your input guys. When I was originally planning this I had forgotten completely about the ability to use array variables and that was the beginning of my solution. I've decided to make each menu its own method. For each menu screen, every time I enter a value that will be sent to the display it is also recorded in the appropriate column of a temporary array. As I enter the values the code looks at the column position it is currently in and if there is a defined symbol like a º or a ' then it skips it. I've also used a directional variable so that It will skip the defined symbols as I scroll left and right on the screen. This is an absolute brute force, zero elegance, tedious solution but it works. Luckily there aren't too many menu screens I have to make and most of the code is reusable. Here is one of the menu screens, try not to cringe.
    PUB EnterLocation                                          'ENTER LOCATION:
                                                               '000d00'W 00d00'N
       column := 0
       row := 1
       direction := 2
       
       bytefill(@data,0,16)                                'fill data array with zeros
    
    
       data[0] := longdeg/100                              ' breakdown longdeg into hundred tens and ones
       data[1] := (longdeg - data[0]*100) / 10
       data[2] := longdeg - data[0]*100 - data[1]*10
    
    
       data[4] := longmin/10                               'breakdown longmin into tens and ones
       data[5] := longmin - data[4]*10
    
    
       data[9] := latdeg/10                                'breakdown latdeg into tens and ones
       data[10] := latdeg - data[9]*10
    
    
       data[12] := latmin/10                               'breakdown latmin into tens and ones
       data[13] := latmin -data[12]*10
    
    
      
      
        lcd.home                            'build initial display
        lcd.str(string("ENTER LOCATION:"))  
        lcd.gotoxy(0,1)
        lcd.dec(data[0])                    'display longdeg
        lcd.dec(data[1])
        lcd.dec(data[2])
        lcd.gotoxy(3,1)              
        lcd.putc(223)                       'display longmin
        lcd.dec(data[4])
        lcd.dec(data[5])
        lcd.gotoxy(6,1)
        lcd.str(String("'W "))
        lcd.dec(data[9])                    'display latdeg
        lcd.dec(data[10])
        lcd.gotoxy(11,1)
        lcd.putc(223)                       
        lcd.dec(data[12])                   'display latmin
        lcd.dec(data[13])
        lcd.gotoxy(14,1)
        lcd.str(string("'N"))
        lcd.gotoxy(column,row)
    
    
       repeat                                             'call for keypad entry, waits until keypress is detected then moves on
         if keypad => 0 and kpdout =< 9
           lcd.dec(kpdout)                                   
           data[column-1]:= kpdout                           'enter keypad entry into data array
           longdeg := data[0]*100 + data[1]*10 + data[2]     'combine appropriate data positions into longdeg longmin latdeg and latmin
           longmin := data[4]*10 + data[5]
           latdeg := data[9]*10 + data[10]
           latmin := data[12]*10 + data[13]
             if column == 3 and direction == 2
               column := 4
             elseif column == 3 and direction == 1
               column := 2
             elseif column == 6 and direction == 2
               column := 7
             elseif column == 6 and direction == 1
               column := 5
             elseif column == 8 and direction == 2
               column := 9
             elseif column == 8 and direction == 1
               column := 7
             elseif column == 11 and direction == 2
               column := 12
             elseif column == 11 and direction == 1
               column := 10
             elseif column == 14 and direction == 2
               column := 15
             elseif column == 14 and direction == 1
               column := 13    
         elseif kpdout == 10
             if column == 3 and direction == 2
               column := 4
             elseif column == 3 and direction == 1
               column := 2
             elseif column == 6 and direction == 2
               column := 7
             elseif column == 6 and direction == 1
               column := 5
             elseif column == 8 and direction == 2
               column := 9
             elseif column == 8 and direction == 1
               column := 7
             elseif column == 11 and direction == 2
               column := 12
             elseif column == 11 and direction == 1
               column := 10
             elseif column == 14 and direction == 2
               column := 15
             elseif column == 14 and direction == 1
               column := 13    
         
         lcd.gotoxy(column,row)
         waitcnt(clkfreq / 4 + cnt)
    
  • StempileStempile Posts: 82
    edited 2013-11-24 16:28
    For my data logger project--my most complex project to date--I setup a method that controls the display while scanning the input then RETURNS() the selected menu item. I haven't made use of DAT only because I haven't learned best practice and don't full understand the limits between cog memory vs global memory. As such I haven't hit any upper limits regarding Proper resources; I suspect I am not as efficient as I could be and have been lucky so far...

    The button used is a the Parallax 5 position switch. When the method is activated it cycles through the list using lookup(). The up /dwn button inc and dec the value to control the display and selected option.
    pub setupScreen |menuOption, buttonValue
    ''Method for user interface to select menu option
    
    
       menuOption := 1
    
    
       lcd.cmd(lcd#cls)
    
    
       repeat
         lcd.move_to(1,1)                                  'Display Date to LCD, start at col/line  
         lcd.str(string(" Configuration Menu"))
          
         lcd.move_to(5,3)
         lcd.str(lookup(menuOption:string("[  Stats   ]"), string("[ Display  ]"),string("[  Setup   ]")))
    
    
         buttonValue := button.read     'scan button pins for user input
    
    
          case buttonValue
            4:  'dwn button
              menuOption ++
            2:  'Up button
              menuOption --
            3,5: 'left or right buttons do nothing
            1:   'center button quit repeat, choice selected 
                         return(menuOption)    'return selected menu item number   
    
                
         menuOption := 1 #> menuOption <# 3  'limit menuOption values to keep valid choices        
    
    
    PUB read | buttonValue
    ''Method to read the input from the 5-position switch
    
    ''read switch, return button pressed
        buttonValue := 0
        
        if ina[24] == 0
          buttonValue := 5  'left                             
    
    
        if ina[25] == 0
          buttonValue := 4  'down     
    
    
        if ina[26] == 0
          buttonValue := 3  'right 
    
    
        if ina[22] == 0
          buttonValue := 1  'center 
    
    
        if ina[23] == 0
          buttonValue := 2  'up
    
    
        pause(150) 'debounce
      
        return(buttonValue)
    
Sign In or Register to comment.