Enter Your Project for a chance to win a Nano Grand Prize bundle for the most innovative use of Arduino plus a $400 shopping cart! Back to homepage | Project14 Home | |
Monthly Themes | ||
Monthly Theme Poll |
Note: this will be a multiple part blog series on my NanoRama project. My first part covers the LCD/Joystick usage as part of a menuing system.
This will be my maiden voyage into the world of Arduino. I was one of the winners of the Nano Board Giveaway, scheduled to receive one of the Nano classics. My intent is to build a DMX diagnostic tool containing a small LCD (2 x 24 character), a pushbutton/joystick for U/I, and serial programmable memory for captured data. The plan would be to build a add-on board that would contain the LCD and other U/I components, serial memory, as well as a RS485 transceiver (ST485). The feature set that I would like to provide is the following:
- Data logging one or more slot address (to serial memory)
- Send multiple sequences (to multiple slot address) on command (buttonpush/menu select) or via a script file download into the serial memory.
This will be a useful tool for me to test/debug my products and to my clients/customers to verifying their setups.
I am using a Nano Classic board that I have received from element14 from an earlier giveaway to breadboard and test my project. So far, I have the JoyStick (Sparkfun 9082 - connected to A0-Up/Down Pot, A1-Left/Right Pot and A2-JoyStick Button Press) and LCD (old surplus - 5V, 2 x 24 character - connected to D2-5 for data 4-7, D10-R/W, D11-EN, D12-RS and serviced with the LiquidCrystal library) up and running, so I have built some menuing software for my interface. My menuing system includes multiple edit types, including Menu, SelectBox, Number Edit Box (8 bit and and 10 bit), CheckBox and a Return function. Here are some details on the menu functions:
Here is the initial splash screen. Upon startup/reset, the program displays this image for 3 seconds (Nice little plug for element14 NanoRama). When time expires, the screen reverts to the main menu level.
This is the Main menu. The '>' symbol is the menuing cursor and shows the active item. The joystick Up/Down and Left/Right controls are used to move the cursors between the menu items. The first menu item (at the '>' cursor) is a Select Box. It contains a list of valid selections for the given field (in this case there are only two valid selections, Send and Recv).
Pressing the Joystick button (pressing down on the Joystick control), changes the cursor to the show the Scroll cursor (custom character created with 'lcd.createChar()' function). In this mode, the Up/Down controls are used to step through the available selections. Pressing the Joystick button again save the selection and ends the edit mode, returning back to the normal cursor ('>').
Using either the Joystick Up or Down action results in the Select Box showing the next available item. Here the only other item in this field is displayed (Recv). Further Up or Down actions would toggle between Send and Recv. Pressing the Joystick button will revert the cursor back to menu item select cursor '>'.
Having selected Recv and then moving the cursor to 'Settings' and pressing the Joystick button, displays the Receive edit menu. This menu contains three checkbox edit fields that are used to select/deselect options during a receive run. The options include: Logging receive data to the serial memory, showing the receive data on the run menu as it is received and showing the timing measurements of the Mark/Post Mark pulse widths.
Pressing the Joystick button cause the current checkbox field to toggle between checked and uncheck. The Up/down and Left/right joystick movements cause the select cursor to move between the edit/menu fields.
Having returned to the main menu and selected Send, and moving the cursor to 'Settings' and pressing the Joystick button, displays the Send edit menu. This menu contains three number edit boxes and a menu item to advance to the second page of the Send settings. These three variables control the DMX framing pulse (Mark and Post Mark) and the frame rate that will be used to sending out DMX data packets.
Pressing the Joystick button changes the cursor to the show the number edit cursor In this mode, the Up/Down controls are used to increase (Up) or decrease (Down) the selected digit in the number field (shown by underscore cursor). The Left/Right controls are used to move between the digits. The edit values are limited by Min/Max settings, such only valid entries are shown. Pressing the Joystick button again save the selection and ends the edit mode, returning back to the normal cursor ('>').
Here is an example of having moved the joystick up while the most significant digit is selected, the digit is incremented by 1 and the value is increamented by 100.
Having moved the joystick to the right and then down, the center digit has been selected and decremented (value decreased by 10).
Notes: The joystick movements are debounced and latched, such that a release (joystick return to center position and/or pushbutton release) is needed prior to the next menu action. This prevents movements/changes from occurring extremely fast.
// include the library code: #include <LiquidCrystal.h> struct selectInfo { char action; char selectIndex; char selectItems; char selInd; }; struct numberInfo { char action; char text[5]; char unitsInd; char editStartPos; unsigned int minVal; unsigned int maxVal; char varInd; }; struct checkBoxInfo { char action; char text[8]; char offset; char checkBoxInd; }; struct menuInfo { char action; char menuTitle[10]; }; union newMenuItem { struct menuInfo menuData; struct selectInfo selectData; struct numberInfo numberData; struct checkBoxInfo checkBoxData; }; struct newMenuSet { union newMenuItem menuList[4]; void (* ptr)(char action, char index, char button); }; // Pin usage // D0 - RX // D1 - TX // D2 - LCD D7 // D3 - LCD D6 // D4 - LCD D5 // D5 - LCD D4 // D6 // D7 // D8 // D9 // D10 - LCD R/W // D11 - LCD EN // D12 - LCD RS // // A0 - UpDown Joystick // A1 - LeftRight Joystick // A2 - Switch press // A3 // A4 // A5 // // // initialize the library by associating any needed LCD interface pin // with the arduino pin number it is connected to const int rs = 12, en = 11, d4 = 5, d5 = 4, d6 = 3, d7 = 2, rw = 10; const int UpDown = 0, RightLeft = 1, SwitchPress = 2; const int UP = 1, DOWN = 2, LEFT = 3, RIGHT = 4; const int NONE = 0, MENU = 1, SELECT = 2, NUMBER_C = 3, NUMBER_I = 4, CHECKBOX = 5, RETURN = 5; const int MAX_SELECTS = 30, SEND_RECV = 0; const int INIT_MENU = 0, DRAW_MENU = 1, UPDATE_MENU = 2, KEY_PRESSED = 3, UPDATE_SELECTS = 4; LiquidCrystal lcd(rs, rw, en, d4, d5, d6, d7); int val = 0; char menuDepth = 0; char cursorPos = 0; char cursor = 0x3e; char pressActive = 0; char upDownActive = 0; char leftRightActive = 0; char editActive = 0; char editOffset = 0; char scrollActive = 0; byte upDownChr[8] = {0x04, 0x0e, 0x1f, 0x04, 0x04, 0x1f, 0x0e, 0x04}; char menuPos[4][2] = {0, 0, 12, 0, 0, 1, 12, 1}; char selectList[MAX_SELECTS][8] = {"Send", "Recv", "Live", "Script", "8-Bit", "16-Bit"}; char unitsList[MAX_SELECTS][4] = {"us", "/s", ""}; void mainMenuServer(char action, char index, char button); void sendMenuServer1(char action, char index, char button); void sendMenuServer2(char action, char index, char button); void recvMenuServer(char action, char index, char button); void addrMenuServer(char action, char index, char button); void runMenuServer(char action, char index, char button); unsigned char stepSize[3] = {100, 10, 1}; struct newMenuSet newMenuCollect[6] = { {{{.selectData = {.action = SELECT, 0, 2, 0}}, // main menu {.menuData = {.action = MENU, "Set Addr."}}, {.menuData = {.action = MENU, "Settings"}}, {.menuData = {.action = MENU, "Run"}}}, mainMenuServer}, {{{.numberData = {.action = NUMBER_C, "Mark", 0, 6, 50, 255, 0}}, // Send settings menu - page1 {.numberData = {.action = NUMBER_C, "Post", 0, 6, 0, 255, 1}}, {.numberData = {.action = NUMBER_C, "Rate", 1, 6, 1, 100, 2}}, {.menuData = {.action = MENU, "More"}}}, sendMenuServer1}, {{{.selectData = {.action = SELECT, 2, 2, 1}}, // Send settings menu - page2 {.menuData = {.action = MENU, "Save"}}, {.menuData = {.action = MENU, "Load"}}, {.menuData = {.action = MENU, "Return"}}}, sendMenuServer2}, {{{.checkBoxData = {.action = CHECKBOX, "Logging", 9, 0}}, // Receive settings menu {.checkBoxData = {.action = CHECKBOX, "Data", 6, 1}}, {.checkBoxData = {.action = CHECKBOX, "Timing", 8, 2}}, {.menuData = {.action = MENU, "Return"}}}, recvMenuServer}, {{{.numberData = {.action = NUMBER_I, "Addr", 2, 6, 1, 512, 0}}, // DMX address setting menu {.selectData = {.action = SELECT, 4, 2, 2}}, {.menuData = {.action = NONE, ""}}, {.menuData = {.action = MENU, "Return"}}}, addrMenuServer}, {{{.menuData = {.action = MENU, "Stop/Exit"}}, // Run (either send or receive) menu {.menuData = {.action = NONE, ""}}, {.menuData = {.action = NONE, ""}}, {.menuData = {.action = NONE, ""}}}, runMenuServer}, }; char selectValues[4]; unsigned char numberValues[3] = { 30, 90, 50}; unsigned int numberValues_I[3] = { 1 }; char checkBoxValues[3]; void setup() { // set up the LCD's number of columns and rows: lcd.begin(24, 2); // define special characters lcd.createChar(0, upDownChr); // Write splash screen lcd.clear(); lcd.setCursor(0, 0); lcd.print(" DMX Debug Tool"); lcd.setCursor(0, 1); lcd.print("element14-NanoRama 2020"); delay(3000); menuDepth = 0; // invoke the initialization of each menu page newMenuCollect[0].ptr(INIT_MENU, 0, 0); newMenuCollect[1].ptr(INIT_MENU, 0, 0); newMenuCollect[2].ptr(INIT_MENU, 0, 0); newMenuCollect[3].ptr(INIT_MENU, 0, 0); newMenuCollect[4].ptr(INIT_MENU, 0, 0); newMenuCollect[5].ptr(INIT_MENU, 0, 0); // draw the main menu newMenuCollect[0].ptr(DRAW_MENU, 0, 0); pressActive = 0; } void loop() { char upDown = 0; char leftRight = 0; val = analogRead(UpDown); upDown = processUpDown(val); if (editActive) { newMenuCollect[menuDepth].ptr(UPDATE_SELECTS, 0, upDown); } else { newMenuCollect[menuDepth].ptr(UPDATE_MENU, 0, upDown); } val = analogRead(RightLeft); leftRight = processLeftRight(val); if (editActive) { newMenuCollect[menuDepth].ptr(UPDATE_SELECTS, 0, leftRight); } else { newMenuCollect[menuDepth].ptr(UPDATE_MENU, 0, leftRight); } val = analogRead(SwitchPress); processPress(val); } void mainMenuServer(char action, char index, char button) { static char cursorPos; char i; char ind; switch (action) { case INIT_MENU: cursorPos = 0; break; case DRAW_MENU: drawMenu(cursorPos); break; case UPDATE_MENU: cursorPos = upDateMenu(button, cursorPos); break; case KEY_PRESSED: switch (newMenuCollect[menuDepth].menuList[cursorPos].menuData.action) { case MENU: // move to selected menu switch (cursorPos) { case 1: menuDepth = 4; newMenuCollect[menuDepth].ptr(DRAW_MENU, 0, 0); break; case 2: if (selectValues[0] == 0) { menuDepth = 1; newMenuCollect[menuDepth].ptr(DRAW_MENU, 0, 0); } else { menuDepth = 3; newMenuCollect[menuDepth].ptr(DRAW_MENU, 0, 0); } break; case 3: menuDepth = 5; newMenuCollect[menuDepth].ptr(DRAW_MENU, 0, 0); break; } break; case SELECT: processSelectAction(cursorPos, 0); break; } break; case UPDATE_SELECTS: switch(cursorPos) { case 0: ind = newMenuCollect[menuDepth].menuList[cursorPos].selectData.selInd; selectValues[ind] = upDateSelects(button, selectValues[ind], 0, 2, cursorPos); break; } break; } } void sendMenuServer1(char action, char index, char button) { static char cursorPos; switch (action) { case INIT_MENU: cursorPos = 0; break; case DRAW_MENU: drawMenu(cursorPos); break; case UPDATE_MENU: cursorPos = upDateMenu(button, cursorPos); break; case KEY_PRESSED: switch (newMenuCollect[menuDepth].menuList[cursorPos].menuData.action) { case MENU: // move to selected menu switch (cursorPos) { case 3: menuDepth = 2; newMenuCollect[menuDepth].ptr(DRAW_MENU, 0, 0); break; } break; case NUMBER_C: processSelectAction(cursorPos, '*'); break; } break; case UPDATE_SELECTS: switch(cursorPos) { case 0: case 1: case 2: upDateNumbers(button, cursorPos); break; } break; } } void sendMenuServer2(char action, char index, char button) { static char cursorPos; char ind; switch (action) { case INIT_MENU: cursorPos = 0; break; case DRAW_MENU: drawMenu(cursorPos); break; case UPDATE_MENU: cursorPos = upDateMenu(button, cursorPos); break; case KEY_PRESSED: switch (newMenuCollect[menuDepth].menuList[cursorPos].menuData.action) { case MENU: // move to selected menu switch (cursorPos) { case 1: case 2: case 3: menuDepth = 0; newMenuCollect[menuDepth].ptr(DRAW_MENU, 0, 0); break; } break; case SELECT: processSelectAction(cursorPos, 0); break; } break; case UPDATE_SELECTS: switch(cursorPos) { case 0: ind = newMenuCollect[menuDepth].menuList[cursorPos].selectData.selInd; selectValues[ind] = upDateSelects(button, selectValues[ind], 2, 2, cursorPos); break; } break; } } void recvMenuServer(char action, char index, char button) { static char cursorPos; switch (action) { case INIT_MENU: cursorPos = 0; break; case DRAW_MENU: drawMenu(cursorPos); break; case UPDATE_MENU: cursorPos = upDateMenu(button, cursorPos); break; case KEY_PRESSED: switch (newMenuCollect[menuDepth].menuList[cursorPos].menuData.action) { case MENU: // move to selected menu menuDepth = 0; newMenuCollect[menuDepth].ptr(DRAW_MENU, 0, 0); break; case CHECKBOX: processCheckBoxAction(cursorPos); break; } break; case UPDATE_SELECTS: break; } } void addrMenuServer(char action, char index, char button) { static char cursorPos; char ind; switch (action) { case INIT_MENU: cursorPos = 0; break; case DRAW_MENU: drawMenu(cursorPos); break; case UPDATE_MENU: cursorPos = upDateMenu(button, cursorPos); break; case KEY_PRESSED: switch (newMenuCollect[menuDepth].menuList[cursorPos].menuData.action) { case MENU: // move to selected menu switch (cursorPos) { case 3: menuDepth = 0; newMenuCollect[menuDepth].ptr(DRAW_MENU, 0, 0); break; } break; case SELECT: processSelectAction(cursorPos, 0); break; case NUMBER_I: processSelectAction(cursorPos, '*'); break; } break; case UPDATE_SELECTS: switch(cursorPos) { case 0: upDateNumbers(button, cursorPos); break; case 1: ind = newMenuCollect[menuDepth].menuList[cursorPos].selectData.selInd; selectValues[ind] = upDateSelects(button, selectValues[ind], 4, 2, cursorPos); break; } break; } } void runMenuServer(char action, char index, char button) { static char cursorPos; char ind; switch (action) { case INIT_MENU: cursorPos = 0; break; case DRAW_MENU: drawMenu(cursorPos); break; case UPDATE_MENU: cursorPos = upDateMenu(button, cursorPos); break; case KEY_PRESSED: switch (newMenuCollect[menuDepth].menuList[cursorPos].menuData.action) { case MENU: // move to selected menu switch (cursorPos) { case 0: menuDepth = 0; newMenuCollect[menuDepth].ptr(DRAW_MENU, 0, 0); break; } break; } break; case UPDATE_SELECTS: break; } } void drawMenu(char cursorPos) { char i; char ind; unsigned int printNum; lcd.clear(); for( i = 0; i < 4; i++) { lcd.setCursor(menuPos[i][0], menuPos[i][1]); if (i == cursorPos) { lcd.print(cursor); } else { lcd.print(" "); } switch (newMenuCollect[menuDepth].menuList[i].menuData.action) { case NONE: break; case MENU: lcd.print(newMenuCollect[menuDepth].menuList[i].menuData.menuTitle); break; case SELECT: ind = newMenuCollect[menuDepth].menuList[i].selectData.selectIndex + selectValues[newMenuCollect[menuDepth].menuList[i].selectData.selInd]; lcd.print("["); lcd.print(selectList[ind]); lcd.print("]"); break; case NUMBER_C: case NUMBER_I: lcd.print(newMenuCollect[menuDepth].menuList[i].numberData.text); lcd.print(" "); printNum = (newMenuCollect[menuDepth].menuList[i].menuData.action == NUMBER_C) ? numberValues[newMenuCollect[menuDepth].menuList[i].numberData.varInd] : numberValues_I[newMenuCollect[menuDepth].menuList[i].numberData.varInd]; print3digit(printNum); lcd.print(unitsList[newMenuCollect[menuDepth].menuList[i].numberData.unitsInd]); break; case CHECKBOX: lcd.print(newMenuCollect[menuDepth].menuList[i].checkBoxData.text); lcd.print("["); lcd.print((checkBoxValues[newMenuCollect[menuDepth].menuList[i].checkBoxData.checkBoxInd]) ? "X" : " "); lcd.print("]"); break; } } } void print3digit(unsigned int num) { int printNum; if (num < 100) { lcd.print('0'); } if (num < 10) { lcd.print('0'); } printNum = num; lcd.print(printNum); } void processSelectAction(char cursorPos, char specialCursor) { lcd.setCursor(menuPos[cursorPos][0], menuPos[cursorPos][1]); if (editActive) { // close edit function lcd.print(cursor); editActive = 0; lcd.noCursor(); } else { // open edit function lcd.write((byte)specialCursor); // convert menu mark to select mark editActive = 1; editOffset = 0; if (newMenuCollect[menuDepth].menuList[cursorPos].menuData.action == NUMBER_C || newMenuCollect[menuDepth].menuList[cursorPos].menuData.action == NUMBER_I) { lcd.setCursor(menuPos[cursorPos][0] + newMenuCollect[menuDepth].menuList[cursorPos].numberData.editStartPos, menuPos[cursorPos][1]); lcd.cursor(); } } } char upDateMenu(char dir, char cursorPos) { char newPos; newPos = cursorPos; switch (cursorPos) { case 0: if (dir == DOWN && newMenuCollect[menuDepth].menuList[2].menuData.action != NONE) { newPos = 2; } if (dir == RIGHT && newMenuCollect[menuDepth].menuList[1].menuData.action != NONE) { newPos = 1; } break; case 1: if (dir == DOWN && newMenuCollect[menuDepth].menuList[3].menuData.action != NONE) { newPos = 3; } if (dir == LEFT && newMenuCollect[menuDepth].menuList[0].menuData.action != NONE) { newPos = 0; } break; case 2: if (dir == UP && newMenuCollect[menuDepth].menuList[0].menuData.action != NONE) { newPos = 0; } if (dir == RIGHT && newMenuCollect[menuDepth].menuList[3].menuData.action != NONE) { newPos = 3; } break; case 3: if (dir == UP && newMenuCollect[menuDepth].menuList[1].menuData.action != NONE) { newPos = 1; } if (dir == LEFT && newMenuCollect[menuDepth].menuList[2].menuData.action != NONE) { newPos = 2; } break; } if (newPos != cursorPos) { eraseCursor(cursorPos); drawCursor(newPos); cursorPos = newPos; } return cursorPos; } void eraseCursor(char cPos) { lcd.setCursor(menuPos[cPos][0], menuPos[cPos][1]); lcd.print(" "); } void drawCursor(char cPos) { lcd.setCursor(menuPos[cPos][0], menuPos[cPos][1]); lcd.print(cursor); } char upDateSelects(char dir, char select, char item, char maxVal, char cursorPos) { if (scrollActive) { // wait for up/down to clear if (dir == 0) { scrollActive--; } return select; } if (dir == 0) { return select; } if (dir == UP) { scrollActive = 10; if (++select >= maxVal) { select = 0; } } else if (dir == DOWN) { scrollActive = 10; if (select == 0) { select = maxVal - 1; } else { select--; } } item += select; // convert index from relative to absolute // update LCD lcd.setCursor(menuPos[cursorPos][0] + 2, menuPos[cursorPos][1]); lcd.print(" "); lcd.setCursor(menuPos[cursorPos][0] + 2, menuPos[cursorPos][1]); lcd.print(selectList[item]); lcd.print("]"); return select; } void upDateNumbers(char button, char cursorPos) { char editPos; unsigned int maxVal; unsigned int minVal; unsigned int editVal; char changeVal; // based on cursorPos get value/limits editPos = menuPos[cursorPos][0] + newMenuCollect[menuDepth].menuList[cursorPos].numberData.editStartPos; maxVal = newMenuCollect[menuDepth].menuList[cursorPos].numberData.maxVal; minVal = newMenuCollect[menuDepth].menuList[cursorPos].numberData.minVal; if (newMenuCollect[menuDepth].menuList[cursorPos].numberData.action == NUMBER_C) { editVal = numberValues[newMenuCollect[menuDepth].menuList[cursorPos].numberData.varInd]; } else { editVal = numberValues_I[newMenuCollect[menuDepth].menuList[cursorPos].numberData.varInd]; } changeVal = 0; lcd.setCursor(editPos + editOffset, menuPos[cursorPos][1]); switch(button) { case UP: if ((maxVal - editVal) < stepSize[editOffset]) { editVal = maxVal; changeVal = 1; } else { editVal += stepSize[editOffset]; changeVal = 1; } break; case DOWN: if (editVal < stepSize[editOffset]) { editVal = minVal; changeVal = 1; } else { editVal -= stepSize[editOffset]; if (editVal < minVal) { editVal = minVal; } changeVal = 1; } break; case LEFT: editOffset = (editOffset > 0) ? editOffset - 1 : 2; lcd.setCursor(editPos + editOffset, menuPos[cursorPos][1]); break; case RIGHT: editOffset = (editOffset >= 2) ? 0 : editOffset + 1; lcd.setCursor(editPos + editOffset, menuPos[cursorPos][1]); break; } if (changeVal) { if (newMenuCollect[menuDepth].menuList[cursorPos].numberData.action == NUMBER_C) { editVal = numberValues[newMenuCollect[menuDepth].menuList[cursorPos].numberData.varInd] = editVal; } else { editVal = numberValues_I[newMenuCollect[menuDepth].menuList[cursorPos].numberData.varInd] = editVal; } // update LCD lcd.setCursor(editPos, menuPos[cursorPos][1]); print3digit(editVal); lcd.setCursor(editPos + editOffset, menuPos[cursorPos][1]); } } char processUpDown(int val) { val >>= 8; switch (val) { case 0: // ADC values 0 - 255 if (!upDownActive) { upDownActive = 10; return UP; } break; case 1: // ADC values 256 - 511 case 2: // ADC values 512 - 767 if (upDownActive) { upDownActive--; } break; case 3: if (!upDownActive) { upDownActive = 10; return DOWN; } break; // ADC values 768 - 1023 } return 0; } char processLeftRight(int val) { val >>= 8; switch (val) { case 0: // ADC values 0 - 255 if (!leftRightActive) { leftRightActive = 10; return RIGHT; } break; case 1: // ADC values 256 - 511 case 2: // ADC values 512 - 767 if (leftRightActive) { leftRightActive--; } break; case 3: // ADC values 768 - 1023 if (!leftRightActive) { leftRightActive = 10; return LEFT; } break; } return 0; } void processPress(int val) { if ((val < 256) && (pressActive == 0)) { // button pressed pressActive = 10; // let the active menu setup processing newMenuCollect[menuDepth].ptr(KEY_PRESSED, 0, 0); } else { if ((val > 768) && (pressActive != 0)) { pressActive--; } } } void processCheckBoxAction(char cursorPos) { char offset; char val; char ind; offset = newMenuCollect[menuDepth].menuList[cursorPos].checkBoxData.offset; ind = newMenuCollect[menuDepth].menuList[cursorPos].checkBoxData.checkBoxInd; val = checkBoxValues[ind]; val = (val) ? 0 : 1; // toggle Logging flag checkBoxValues[ind] = val; lcd.setCursor(menuPos[cursorPos][0] + offset, menuPos[cursorPos][1]); lcd.print((val) ? "X" : " "); }
Top Comments