News:

;) This forum is the property of Proton software developers

Main Menu

How to construct a flexible menu from items in flash

Started by Peter Truman, Aug 17, 2025, 07:12 AM

Previous topic - Next topic

Peter Truman

Hi All

I'm trying to find the best way of building a flexible menu picker (18F2525 with a Newhaven 4 x 20 LCD) with a rotary encoder and momentary button
What I have is a database of upto 40 Menu items (each item is an 18chr string)

I want to call my menu procedure like this
B_Value=P_Menu("Screen Title",1,2,7,9,41,43) where the digits represent the menu items I want to choose from. My project will have many menu screens, each made up of 1 or more of the items in my list. I'm keeping my Items list in flash.

So for example
B_Value=P_Menu("Setup Menu",1,2,7,9,41) would look like:

 Setup Menu
[Power Fail Delay]
 Set Start Delay
 Set Stop Delay
 Stop Mode
 Menu Timeout
 BACK

I use the "[]" as my cursor and only the bottom 3 lines scroll up and down with my rotary encoder. There are never more than 9 items in any menu.

ChatGPT5 has been very very helpful but, its like having an endlessly patient, highly knowledgeable but quite dim firmware developer sitting next to me. If I ask it the 'right' sort of question it very quickly shows me how to proceed, but the trick is knowing the 'right' question. When I asked it to help me with this problem it came up with a solution (after a fair bit of too and fro) which did work but seems the most cumbersome and seemingly inefficient way of getting there! There is also a of converting from C to Positron!

I'd welcome any suggestions of how to do this in a clean and efficient manner. Many thanks in advance.

top204

Hello Peter

I am not sure if you are asking for a Menu system mechanism, or a system for accessing texts from a sequence held in Flash memory, that can be accessed by a menu's procedure.

If it is texts, I created a String Array mechanism quite a few years ago, that I have used several times. They can be a String held in RAM or a String held in Flash memory, and can be created with set texts for each dimension of the array. For example, the demo below shows it working:

'
'  /\\\\\\\\\
'  /\\\///////\\\
'  \/\\\    \/\\\                                                /\\\          /\\\
'  \/\\\\\\\\\\\/        /\\\\\    /\\\\\\\\\\    /\\\\\\\\  /\\\\\\\\\\\  /\\\\\\\\\\\  /\\\\\\\\\
'    \/\\\//////\\\      /\\\///\\\  \/\\\//////    /\\\/////\\\ \////\\\////  \////\\\////  \////////\\\
'    \/\\\    \//\\\    /\\\  \//\\\ \/\\\\\\\\\\  /\\\\\\\\\\\    \/\\\        \/\\\        /\\\\\\\\\\
'      \/\\\    \//\\\  \//\\\  /\\\  \////////\\\ \//\\///////      \/\\\ /\\    \/\\\ /\\  /\\\/////\\\
'      \/\\\      \//\\\  \///\\\\\/    /\\\\\\\\\\  \//\\\\\\\\\\    \//\\\\\      \//\\\\\  \//\\\\\\\\/\\
'        \///        \///    \/////    \//////////    \//////////      \/////        \/////    \////////\//
'                                  Let's find out together what makes a PIC Tick!
'
' Demonstrate RAM and Flash memory string array handling using the "String Array.inc" meta-macros
' For 18F devices only
'
' Written by Les Johnson for the Positron8 BASIC compiler
'
    Device = 18F25K20                                              ' Tell the compiler what device to compile for
    Declare Xtal = 16                                              ' Tell the compiler what frequency the device is operating at (in MHz)
'
' Setup USART1
'
    Declare Hserial1_Baud = 9600
    Declare HRSOut1_Pin = PORTC.6

    Include "String Array.inc"                                      ' Load the String array macros into the program
'
' Create a 10 string flash memory array, with each String 24 characters in length (including the null)
' The flash memory string must be an even amount of bytes (including the null)
'
    Create_FlashStringArray(MyFlashString, 10, 24) = {"My Flash String Array 0", 0,
                                                      "My Flash String Array 1", 0,
                                                      "My Flash String Array 2", 0,
                                                      "My Flash String Array 3", 0,
                                                      "My Flash String Array 4", 0,
                                                      "My Flash String Array 5", 0,
                                                      "My Flash String Array 6", 0,
                                                      "My Flash String Array 7", 0,
                                                      "My Flash String Array 8", 0,
                                                      "My Flash String Array 9", 0}
'
' Create variables here
'
    Dim bIndex As Byte                                              ' Temporary byte used by the demo
    Dim TempString As String * 32 Heap                              ' Temporary string used by the demo
'
' Create a 10 string RAM array, with each string 20 characters in length (not including the null)
'
    Create_StringArray(MyString, 10, 20) = {"RAM String Array  0", 0,
                                            "RAM String Array  1", 0,
                                            "RAM String Array  2", 0,
                                            "RAM String Array  3", 0,
                                            "RAM String Array  4", 0,
                                            "RAM String Array  5", 0,
                                            "RAM String Array  6", 0,
                                            "RAM String Array  7", 0,
                                            "RAM String Array  8", 0,
                                            "RAM String Array  9", 0}

'--------------------------------------------------------------------------------------
' Test the string array meta-macros by loading then retrieving values
'
Main:
'
' Read the RAM String array's default content
'
    For bIndex = 0 To 9
        TempString = Read_StringArray MyString, [bIndex]            ' Retrieve the RAM string array
        HRSOutLn TempString                                        ' Transmit the string's text to a serial terminal
    Next
'
' Load and Read the RAM String array
'
    Clear MyString
    For bIndex = 0 To 9
        TempString = "MyString RAM Array " + Str$(Dec bIndex)      ' Fill the string to load and retrieve
        Write_StringArray MyString,[bIndex], TempString            ' Load the RAM string array
        TempString = Read_StringArray MyString, [bIndex]            ' Retrieve the RAM string array
        HRSOutLn TempString                                        ' Transmit the string's text to a serial terminal
    Next
'
' Read the Flash memory String array
'
    For bIndex = 0 To 9
        TempString = Read_FlashStringArray MyFlashString, [bIndex]  ' Retrieve a Flash memory string array
        HRSOutLn Dec bIndex, "...", TempString                      ' Transmit the string's text to a serial terminal
    Next

The above demonstration, will transmit the texts shown below:

RAM String Array  0
RAM String Array  1
RAM String Array  2
RAM String Array  3
RAM String Array  4
RAM String Array  5
RAM String Array  6
RAM String Array  7
RAM String Array  8
RAM String Array  9
MyString RAM Array 0
MyString RAM Array 1
MyString RAM Array 2
MyString RAM Array 3
MyString RAM Array 4
MyString RAM Array 5
MyString RAM Array 6
MyString RAM Array 7
MyString RAM Array 8
MyString RAM Array 9
0...My Flash String Array 0
1...My Flash String Array 1
2...My Flash String Array 2
3...My Flash String Array 3
4...My Flash String Array 4
5...My Flash String Array 5
6...My Flash String Array 6
7...My Flash String Array 7
8...My Flash String Array 8
9...My Flash String Array 9


I have attached the String Array include file and demo to this post. The "String Array" include file contains the routines to create and read Flash memory String arrays, and create, read, and write RAM String arrays.

To access the text within an array's dimension, just use its index value, that can be set by a procedure's parameter.

Best regards
Les

top204

Something else I have found very useful in Menu Systems I have created using alphanumeric or graphic LCDs, is a mechanism to display texts in the center of the LCD's line. Instead of having to jiggle texts around, and test, then jiggle again, then test, or create a string with the full line's text.

Below is a simple procedure that will calculate the center X position on an alphanumeric LCD, based upon the length of the text to display, and display it on the line required:

'------------------------------------------------------------------------------------------------
' Display text at the center X of the LCD
' Input     : pLine holds the line to display the text on
'           : pText holds the text to display (maximum 20 characters)
' Output    : None
' Notes     : cCharsOnLine is set for a 20 characters per line LCD
'
Proc LCD_PrintAtCenter(pLine As Byte, pText As String * 20)
    Symbol cCharsOnLine = 20                                            ' The amount of characters per line on the LCD
    Symbol cLineCntr = (cCharsOnLine / 2) + 1                           ' The center X position on a line
    Dim bChars As Byte                                                  ' Holds the amount of characters in the pText String
    Dim bXpos  As Byte                                                  ' Holds the center X position on a line

    bChars = Len(pText)                                                 ' Count the amount of characters in the text
    bChars = bChars / 2                                                 ' Divide it by 2, so it can be subtracted from the full text count
    bXpos = cLineCntr - bChars                                          ' Subtract the amount of chars from the center of the LCD's line amount
    Print At pLine, bXpos, pText                                        ' Display the text
    Clear pText                                                         ' Clear the parameter string before exiting
EndProc

If ASCII values are needed to be displayed, use the Str$ function in the procedure's parameter, when it is called.

I have also created PrintAtCenter procedures for graphic LCDs, but they take a bit more calculation, and other, 'helper', procedures because of the proportional fonts used in them. But it saves hours of time, that is needed to make sure a piece of text actually sits in the middle of the display.

The concept of a procedure for the center of a display, came about because I was so bored of having to move an X position, then re-programming to see if it was in the centre, then when text or a font was changed, having to go through the whole program again, and do the same for all the texts in it. It used to take a couple of hours 'or so', on a largish program, just to position texts.

John Drew

Here's another problem I've encountered with menus.
Exanple: I have 3 different items in line. Each item may or may not change. Each of the three items are of varying lengths.

In the interests of LCD writing speed I don't want to rewrite the whole line and I don't want to have remaining letters/numerals from a longer previous item's string.

My current solution is to add spaces based on the length of the new string so there are no orphaned letters/numerals and no overwriting of the next item. This doesn't work for graphic displays due to differing fonts.

How do others solve this problem?
John

Peter Truman

I have this working quite well now thanks. Very helpful.

I have a list of strings saved in flash memory - I just look them up using a long Select, Case, Endselect structure.

For example - I have 3 inputs which each can be configured for a variety of functions. So my first setup menu is called like this B_Option = P_MenuList8("SETUP MENU", 7,8,9,10,11,12,13,14)
Each number relates to a particular string of text (sample)
Dim S_String1_F   As Flash8 = "Main Menu", 0
Dim S_String2_F   As Flash8 = "Utility Menu", 0
Dim S_String3_F   As Flash8 = "Setup Menu", 0
Dim S_String4_F   As Flash8 = "Date and Time", 0
Dim S_String5_F   As Flash8 = "View Log", 0
Dim S_String6_F   As Flash8 = "Clear Log", 0
Dim S_String7_F   As Flash8 = "Menu Timeout", 0
Dim S_String8_F   As Flash8 = "Contrast", 0
Dim S_String9_F   As Flash8 = "Pwr Fail Delay", 0
Dim S_String10_F  As Flash8 = "Input 1", 0
Dim S_String11_F  As Flash8 = "Input 2", 0
Dim S_String12_F  As Flash8 = "Input 3", 0
Dim S_String13_F  As Flash8 = "End Runtime", 0
Dim S_String14_F  As Flash8 = "Pulse Duration", 0
Dim S_String15_F  As Flash8 = "RunTime", 0
Dim S_String16_F  As Flash8 = "Pressure", 0
Dim S_String17_F  As Flash8 = "Temperature", 0
Dim S_String18_F  As Flash8 = "Flow", 0
Dim S_String19_F  As Flash8 = "Vacuum", 0
Dim S_String20_F  As Flash8 = "BACK", 0
Dim S_String21_F  As Flash8 = "Digital", 0
Dim S_String22_F  As Flash8 = "Fail HIGH", 0
Dim S_String23_F  As Flash8 = "Fail LOW", 0
Dim S_String24_F  As Flash8 = "Pulse", 0
Dim S_String25_F  As Flash8 = "Latch", 0
Dim S_String26_F  As Flash8 = "Set Scale", 0
Dim S_String27_F  As Flash8 = "Not Used", 0
Dim S_String28_F  As Flash8 = "No Action", 0
Dim S_String29_F  As Flash8 = "Fail Primary Low", 0
Dim S_String30_F  As Flash8 = "Fail Secondary Low", 0

My procedure writes the Title line, then each item from the list is scrolled in the lower 3 lines of the LDC (using "[ ]" as a cursor). I found that stopping at the top and bottom of the list is much better than wrapping around (makes more sense). The next menu is based on the results of the first menu. So if someone select 'Digital' for Input 1 then The next menu is only concerned with digital configuration. If they choose 'Pressure' then the next menu is all about scaling values etc. So I needed a good way to setup ad hoc menus. The 4 line display and rotary encoder with button work beautifully. I do have a confession though ChatGPT5 did much of the work, it just needed the right prompt to begin with (Thanks Les - I hope you are ok with that?) I'm quite sure that smarter people than me could find a better way of doing this, but the AI has helped me a great deal. I never was, and probably never will be, a very good code writer (prefer hardware) but the fact is, my productivity (and ability to earn a living) has much improved. And all for a measly US$20 month (I'm sure it will go up!)

Stephen Moss

Quote from: John Drew on Aug 18, 2025, 11:07 PMHere's another problem I've encountered with menus.
Exanple: I have 3 different items in line. Each item may or may not change. Each of the three items are of varying lengths.

In the interests of LCD writing speed I don't want to rewrite the whole line and I don't want to have remaining letters/numerals from a longer previous item's string.

My current solution is to add spaces based on the length of the new string so there are no orphaned letters/numerals and no overwriting of the next item. This doesn't work for graphic displays due to differing fonts.

How do others solve this problem?
John
If you are manually adding space to the length on the new string so that old data where the new string is shorter is overwritten, then try the following...
Print_String = "SR04 Vdd: " + Str$(Dec Sensor_Detect)                   'Create String to Print (Distance)
Print At 1,1,Print_String, Rep " "\(LCD_Num_Characters-len(Print_String))   'Add Spaces to complete Line on Display and Print
this work well for Aplanumeric LCD's where LCD_Num_Characters-len is a constant that holds the maximum number of characters in the line. It simple adds as many space characters to the end of the text string as required to make the number of characters in the string equal the specified number of characters in the line

For Alphanumeric LCD's LCD_Num_Characters-len is a fixed value as defined by the specification of the display, but it may work on graphic displays if you use a fixed width font and can therefore calculate the exact maximum number of characters you can get on a line for the font being used.
It may also work with fonts that have variable widths per character erasing most of any unwanted data where the previous line of text was longer, but you may have a little old data left over at the end of the line depending on the length of the relevant text strings as you would have to base the value for LCD_Num_Characters-len on the average number of characters you could get on a line.
Consequently, as you are not guaranteed to over write all the old data when variable width fonts are used, if text does not wrap from one line to the next you could just make the character length for the line say 3 longer than the calculated average to ensure the overwrite, otherwise I think the you may have to hold each line in an array, clear the screen and re-write them all to remove residual line data.   

I cannot remember where that code can from, but I think it was probably borrowed from a solution someone (Les?) posted somewhere at some point.