/*
* Testing Wokwi for VSCode
* ... Aaand trying to get encoder working!
*/
/* PREPROCESSOR */
/* LIBRARIES */
#include <Arduino.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <BlockNot.h> /* https://github.com/EasyG0ing1/BlockNot */
#include <OneButton.h> /* https://github.com/mathertel/OneButton */
/* PINS & OBJECT SETTINGS */
// Timing
BlockNot globalTimer(20); // 20ms non-blocking timer
// LCD
#define LCD_I2C_ADDR 0x27 // LCD i2c address
#define LCD_COLUMNS 20 // Number of LCD columns
#define LCD_ROWS 4 // Number of LCD rows
LiquidCrystal_I2C lcd(LCD_I2C_ADDR, LCD_COLUMNS, LCD_ROWS);
// GPIO
#define OUTPUT_CH_A 7 // YELLOW LED
#define OUTPUT_CH_B 6 // BLUE LED
#define ENC_CLK 0 // Rotary encoder pulse input channel A pin
#define ENC_DT 1 // Rotary encoder pulse input channel B pin
#define ENC_SW 2 // Rotary encoder pushbutton pin
// Serial
#define SERIAL_BAUD_RATE 115200 // Serial monitor speed
#define NUMBER_OF_COMMANDS 4 // Total number of commands/size of serial command buffer
#define COMMAND_MAX_LENGTH 6 // Max number of serial command characters
// =================
// Global variables
// =================
// LCD & LCD menu variables
int8_t menuCounter = -2; // Determines the cursor row position in the LCD menu
int8_t currentMenu = 0; // The current menu shown: 0 = main, 1 = secondary
int8_t menuCounterMin = 0; // Menu's uppermost cursor position (LCD row)
int8_t menuCounterMax = (LCD_ROWS - 1); // Menu's lowermost cursor position (LCD row)
const String cursor = ">"; // Menu cursor
bool runFinished = false; // Flag to indicate if the run is completed
// Serial monitor commands variables
char cmdBuffer[COMMAND_MAX_LENGTH]; // Command buffer
// Other variables
int16_t distElapsed = 0; //
// =======
// Arrays
// =======
// Array structures
struct SerialCommand // Define serial commands structure
{
const char *cmd; // Command
const char *desc; // Command description
};
struct MenuItem // Define menu item array structure
{
String label; // Menu item name
String value; // Menu item value
};
// Declare array
SerialCommand serialCommands[NUMBER_OF_COMMANDS] = { // Serial commands array, easily expandable
{"help", " - Shows all available commands"},
{"quit", " - Returns to main menu"},
{"poll", " - Displays multiple variables' current values"}};
MenuItem mainMenuItems[] = { // Main menu array
{"Pulse pr m: ", ""},
{"Distance : ", ""},
{"Speed : ", ""},
{"Continue... ", ""}};
MenuItem secondMenuItems[] = { // Secondary menu array
{"Direction : ", ""},
{"Back ", ""},
{"Start ", ""}};
MenuItem runMenuItems[] = { // Run menu items
{"Target : ", ""},
{"Speed : ", ""},
{"Elapsed : ", ""},
{"Direction: ", ""}};
// Arrays containing available menu items
const uint16_t pulsesPerMeter[] = {1024, 512}; // Pulses per meter options, encoder type dependant, indexed by "ppmSel"
int8_t ppmSel = 1; // "pulsesPerMeter" index selection factor
const uint16_t distances[] = {10, 100, 300, 500, 1000}; // Distance options, meters, indexed by "distSel"
int8_t distSel = 0; // "distances" index selection factor
const uint8_t speeds[] = {10, 30, 50, 100}; // Speed options, meters per minute, indexed by "speedSel"
int8_t speedSel = 0; // "speeds" index selection factor
const String dir[] = {"Both", "Up", "Down"}; // Direction options, "Both" runs up first then down again, indexed by "dirSel"
int8_t dirSel = 1; // "dir" (direction) index selection factor: 0 = "BOTH", 1 = "UP", 2 = "DOWN"
// Encoder + Button
int prevCLK;
int encoderCount = 0;
OneButton btn; // Button object
// Take and process incoming commands from serial monitor
void debug() {
static uint8_t cmdBufferIndex = 0; // Index of command buffer
while (Serial.available() > 0)
{
char received_char = Serial.read();
if (received_char == '\n') // If a newline ("enter") is detected
{
cmdBuffer[cmdBufferIndex] = '\0'; // null terminate the C string
if (is_valid_command(cmdBuffer))
{
handle_command(cmdBuffer);
}
else
{
Serial.println(F("Invalid command!"));
Serial.println(F("Type \"help\" to see all available commands."));
}
cmdBufferIndex = 0; // reset index
}
else if (cmdBufferIndex < COMMAND_MAX_LENGTH - 1)
{
cmdBuffer[cmdBufferIndex] = received_char;
cmdBufferIndex++;
}
}
} // debug
// Execute code based on received command
void handle_command(const char *command) {
Serial.println("> Command \"" + (String)command + "\" received..."); // Echo last command
/*
******************************************************************************************
************************** ADD COMMAND ACTIONS BELOW HERE ********************************
**** FORMAT: "else if (strcmp(command, serialCommands[x].cmd) == 0) // "cmd" command" ****
******************************************************************************************
*/
if (strcmp(command, serialCommands[0].cmd) == 0) // "help" command
{
for (int i = 0; i < NUMBER_OF_COMMANDS; i++) // Print all valid commands and their descriptions to serial monitor
{
if (serialCommands[i].cmd == nullptr || strlen(serialCommands[i].cmd) == 0) // Check if the cmd field is empty
{
break; // If the cmd field is empty, break the loop
}
Serial.print(F("> "));
Serial.print(serialCommands[i].cmd);
Serial.println(serialCommands[i].desc);
}
}
else if (strcmp(command, serialCommands[1].cmd) == 0) // "quit" command
{
longPress(); // btn longPress function currently resets the program, reusing this code for this quit-command
}
else if (strcmp(command, serialCommands[2].cmd) == 0) // "poll" command
{
Serial.print(F("> menuCounter: ")); Serial.println(menuCounter);
Serial.print(F("> currentMenu: ")); Serial.println(currentMenu);
Serial.print(F("> distElapsed: ")); Serial.println((String)distElapsed + "m");
Serial.print(F("> ppmSel.....: ")); Serial.print((String)ppmSel + " >> "); Serial.println((String)pulsesPerMeter[ppmSel] + " pulses/m");
Serial.print(F("> distSel....: ")); Serial.print((String)distSel + " >> "); Serial.println((String)distances[distSel] + "m");
Serial.print(F("> speedSel...: ")); Serial.print((String)speedSel + " >> "); Serial.println((String)speeds[speedSel] + "m/min");
Serial.print(F("> dirSel.....: ")); Serial.print((String)dirSel + " >> "); Serial.println("'" + dir[dirSel] + "'");
Serial.print(F("> runFinished: ")); Serial.println((String)runFinished);
}
} // handle_command --> Used in debug function
// Compare serial input with command array, return true if matching, false otherwise
bool is_valid_command(const char *command) {
for (uint8_t i = 0; i < NUMBER_OF_COMMANDS; i++)
{
if (strcmp(command, serialCommands[i].cmd) == 0)
{
return true;
}
}
return false;
} // is_valid_command --> Used in debug function
void splashScreen(uint8_t pinA, uint8_t pinB, unsigned int blinkCount, unsigned int blinkSpeed) {
unsigned int blinkrement = 0;
// Draw border around LCD
for (uint8_t lcdRow = 0; lcdRow <= 3; lcdRow++) {
lcd.setCursor(0, lcdRow);
if (lcdRow == 0 || lcdRow == 3) { // If at either top or bottom of screen...
for (uint8_t lcdCol = 0; lcdCol <= 19; lcdCol++) {
lcd.setCursor(lcdCol, lcdRow);
lcd.print("*");
delay(10);
}
}
lcd.print("*");
lcd.setCursor(19, lcdRow);
lcd.print("*");
delay(10);
}
// Flash the LEDs
do {
digitalWrite(pinA, LOW);
digitalWrite(pinB, HIGH);
delay(blinkSpeed);
digitalWrite(pinB, LOW);
digitalWrite(pinA, HIGH);
delay(blinkSpeed);
blinkrement++;
} while(blinkrement < blinkCount);
digitalWrite(pinA, LOW);
digitalWrite(pinB, LOW);
lcd.setCursor(3, 1); // (column, row)
lcd.print("Hello, World!");
lcd.setCursor(5, 2);
lcd.print("> Pi Pico <");
delay(200);
} // splashScreen
// Ensures that a value (int input) is kept between a (int) min and (int) max value
int minMaxReset(int input, int min, int max) {
input > max ? input = min : // If input is greater than max, reset input to min
input < min ? input = max : // Else, If input is lower than min, reset input to max
input = input; // Else, no need to reset input
return input;
} // minMaxReset
// Sets the cursor on a specific point on LCD, and clears everything to the right of it. Useful for refreshing parts of LCD.
void lcdClearLn(uint8_t clearRow, uint8_t clearColumn, uint8_t lcdMaxNumOfColumns) {
lcd.setCursor(clearRow, clearColumn);
for (uint8_t i = clearColumn; i < lcdMaxNumOfColumns; i++)
{
lcd.setCursor(i, clearRow);
lcd.print((" "));
}
} // lcdClearLn
void onClick() {
// Button press detection
encoderCount = 0;
Serial.println("Button pressed, counter reset");
lcd.setCursor(0, 1);
lcd.print("Button pressed ");
lcd.setCursor(9, 0);
lcd.print(encoderCount);
lcd.print(" ");
} // onClick
// When longPress is called by btn, currently used to cancel run and/or restart the application
void longPress() {
lcd.clear();
splashScreen(OUTPUT_CH_A, OUTPUT_CH_B, 10, 70);
} // longPress
bool checkEncoder() {
bool encChanged = false;
int currentCLK = digitalRead(ENC_CLK);
if (currentCLK != prevCLK) {
if (prevCLK == HIGH && currentCLK == LOW) { // Falling edge detection
int DTstate = digitalRead(ENC_DT);
if (DTstate == HIGH) {
encoderCount++; // Clockwise
menuCounter++;
encChanged = true;
} else {
encoderCount--; // Counterclockwise
menuCounter--;
encChanged = true;
}
lcd.setCursor(9, 0);
lcd.print(encoderCount);
lcd.print(" "); // Clear extra digits
}
prevCLK = currentCLK;
}
btn.tick();
return encChanged;
} // checkEncoder
// Draws menus on LCD, takes argument: menuType 0 = main, 1 = secondary, 2 = run-menu
void showMenu(const uint8_t menuType) {
switch (menuType)
{
case 0: // Main Menu
menuCounterMin = 0;
menuCounterMax = 3;
for (uint8_t i = 0; i < 4; i++) // i = Number of menu items
{
lcd.setCursor(1, i);
lcd.print(mainMenuItems[i].label);
lcd.print(mainMenuItems[i].value);
}
moveCursor(menuCounter);
break;
case 1: // Second Menu
menuCounterMin = 0;
menuCounterMax = 2;
for (uint8_t i = 0; i < 3; i++) // i = Number of menu items
{
lcd.setCursor(1, i);
lcd.print(secondMenuItems[i].label);
lcd.print(secondMenuItems[i].value);
}
moveCursor(0);
break;
case 2: // Run-Menu
for (uint8_t i = 0; i < 4; i++) // i = Number of menu items
{
lcd.setCursor(0, i);
lcd.print(runMenuItems[i].label);
lcd.print(runMenuItems[i].value);
}
break;
}
menuCounter = minMaxReset(menuCounter, menuCounterMin, menuCounterMax); // Reset menu selector cursor position when reaching either end
} // showMenu
// Moves the selector cursor up/down on the LCD, takes row# as argument
void moveCursor(uint8_t menuPos)
{
menuPos = minMaxReset(menuPos, 0, (LCD_ROWS - 1)); // Make sure the cursor can't move past LCD row count
for (uint8_t i = 0; i < LCD_ROWS; i++) // Clean the cursor rows, i = Number of menu items
{
lcd.setCursor(0, i); // Column 1, Row 1-4
lcd.print(F(" "));
}
lcd.setCursor(0, menuPos); // Print a cursor on the LCD according to menuCounter's value, Column 1, Row 1-4
lcd.print(cursor); // Print an indicator showing current selection
} // moveCursor
// Takes action depending on the current menu and cursor position
void handleSelection() {
switch (currentMenu)
{
case 0: // If main menu
switch (menuCounter)
{
case 0: // Set pulses per meter!
ppmSel++;
ppmSel = minMaxReset(ppmSel, 0, 1);
mainMenuItems[0].value = pulsesPerMeter[ppmSel];
lcdClearLn(menuCounter, 12, (LCD_COLUMNS - 1)); // Clear everything from row 1, column 12 and out
showMenu(0); // #0 - Main Menu
break;
case 1: // Set distance!
distSel++;
distSel = minMaxReset(distSel, 0, 4);
mainMenuItems[1].value = (String)distances[distSel] + "m";
lcdClearLn(menuCounter, 12, (LCD_COLUMNS - 1)); // Clear everything from row 2, column 12 and out
showMenu(0); // #0 - Main Menu
break;
case 2: // Set speed!
speedSel++;
speedSel = minMaxReset(speedSel, 0, 3); // Keeps speed selection factor between 0 and 2
mainMenuItems[2].value = (String)speeds[speedSel] + "m/mi ";
showMenu(0); // #0 - Main Menu
break;
case 3: // Continue to second menu/"page 2"
lcd.clear();
currentMenu = 1; // Set current menu to second
showMenu(1); // #1 - Secondary Menu
break;
}
break;
case 1: // If second menu page
switch (menuCounter)
{
case 0: // Set direction!
dirSel++;
dirSel = minMaxReset(dirSel, 0, 2); // Keeps direction selection factor between 0 and 2
secondMenuItems[0].value = dir[dirSel];
lcdClearLn(menuCounter, 12, (LCD_COLUMNS - 1)); // Clear everything from row 0, column 12 and out
showMenu(1); // #1 - Secondary Menu
break;
case 1: // Back to main menu
lcd.clear();
currentMenu = 0; // #0 - Main Menu
showMenu(currentMenu);
break;
case 2: // Start
runMenuItems[0].value = (String)distances[distSel] + "m";
runMenuItems[1].value = (String)speeds[speedSel] + "m/mi";
runMenuItems[3].value = dir[dirSel];
uint8_t runDirection = 0; // 0 = Not moving, 1 = Up, 2 = Down
lcd.clear();
currentMenu = 2; // #2 - Run-menu
showMenu(currentMenu);
// run(); // run-function not yet implemented!
break;
}
break;
}
} // handleSelection
void setup() {
// Config pins
pinMode(ENC_CLK, INPUT); // Rotary encoder input channel A
pinMode(ENC_DT, INPUT); // Rotary encoder input channel B
pinMode(OUTPUT_CH_A, OUTPUT); // Encoder pulse simulation output channel A
pinMode(OUTPUT_CH_B, OUTPUT); // Encoder pulse simulation output channel B
btn.setup(ENC_SW, INPUT_PULLUP, true);
btn.attachClick(onClick);
btn.attachLongPressStart(longPress); // Fires as soon as the button is held down for 1 second
Serial.begin(SERIAL_BAUD_RATE);
Serial.println("Serial comms OK!");
lcd.init();
lcd.backlight();
splashScreen(OUTPUT_CH_A, OUTPUT_CH_B, 10, 70);
delay(1000);
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Encoder: ");
// Initialize previous states
prevCLK = digitalRead(ENC_CLK);
// Set up serial monitor for debugging and testing
Serial.begin(SERIAL_BAUD_RATE);
Serial.println(F("DEEPBox CLI Initiated!"));
Serial.println(F("Enter a valid command to get started"));
Serial.println(F("Or enter command 'help' to see a list of all available commands..."));
showMenu(0);
} // setup
void loop() {
checkEncoder();
if (globalTimer.TRIGGERED) {
debug();
}
delay(1); // delay for simulation/Wokwi, not need in real world!
} // loop