// =============================
// Scope, Description, Overview
// =============================
/*
The objective of this program is to make the Arduino Nano generate quadrature 90 degrees offset pulse trains on two channels in order to mimic the output of an
ATEX/intrinsically safe, optical/NAMUR Hohner "SERIE E"/Series E (SPE-15) encoder that we use for depth measurement in EX-zone. (Internal company P/N: 104725).
The user will be able to specify the direction in which the simulated distance will go: "Up" increases elapsed distance,
"Down" decreases elapsed distance (negative if already at zero), and "Both" will have the distance travel up at first, then back down to zero again.
Most important params: distance, speed & direction. Highest speed is 100m/min and 1000m distance. Desirable distances to test are 10, 100, 300, 500 and 1000m.
Code will be run on an Arduino Nano (Atmega 328p) with a 20x4 character LCD display (i2c), and a push-button rotary encoder for user input and control.
https://www.encoder-hohner.com/product-detail/series-e/
*/
// =============
// Preprocessor
// =============
#define LCD_TYPE LCD_TYPE_I2C // TODO <<< Set LCD type (Using parallel in Wokwi, i2c in real world)
#define LCD_TYPE_I2C 1
#define LCD_TYPE_PARALLEL 2
#define LCD_TYPE_WOKWI_I2C 3
#define OPTIONAL_WOKWI_I2C_SETUP nullptr // Can add a couple of lines of code if using Wokwi I2C
#if LCD_TYPE == LCD_TYPE_I2C
#define LCD_LIBRARY <Adafruit_LiquidCrystal.h>
#define LCD_OBJECT_CONFIG Adafruit_LiquidCrystal lcd(LCD_I2C_ADR)
#elif LCD_TYPE == LCD_TYPE_PARALLEL
#define LCD_LIBRARY <LiquidCrystal.h>
#define LCD_OBJECT_CONFIG LiquidCrystal lcd(12, 11, 10, 9, 8, 7)
#elif LCD_TYPE == LCD_TYPE_WOKWI_I2C
#define LCD_LIBRARY <LiquidCrystal_I2C.h>
#define LCD_I2C_ADR 0x27
#define LCD_OBJECT_CONFIG LiquidCrystal_I2C lcd(LCD_I2C_ADR, LCD_COLUMNS, LCD_ROWS);
#define OPTIONAL_WOKWI_I2C_SETUP
lcd.init();
lcd.backlight()
#else
#error "No valid LCD type specified!"
#endif
// ==========
// Libraries
// ==========
#include <Arduino.h>
#include LCD_LIBRARY /* If i2C: https://github.com/adafruit/Adafruit_LiquidCrystal , If parallel: use std LiquidCrystal.h */
#include <Encoder.h> /* https://github.com/PaulStoffregen/Encoder */
#include <OneButton.h> /* https://github.com/mathertel/OneButton */
#include <Wire.h> /* https://github.com/esp8266/Arduino/tree/master/libraries/Wire */
#include <BlockNot.h> /* https://github.com/EasyG0ing1/BlockNot */
#include <LibPrintf.h> /* https://github.com/embeddedartistry/arduino-printf */
// #include <TimerOne.h> /* https://github.com/PaulStoffregen/TimerOne */
// ================
// Definitions
// ================
// Pins
#define ROTARY_ENCODER_CLK 3 // Rotary encoder pulse input channel A
#define ROTARY_ENCODER_DT 2 // Rotary encoder pulse input channel B
#define ROTARY_ENCODER_PUSHBUTTON 4 // Rotary encoder pushbutton
#define OUTPUT_CH_A 6 // Output channel A
#define OUTPUT_CH_B 5 // Output channel B
// LCD
#define LCD_I2C_ADR 0 // LCD i2c address
#define LCD_COLUMNS 20 // Number of LCD columns
#define LCD_ROWS 4 // Number of LCD rows
// Serial
#define SERIAL_BAUD_RATE 9600 // 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
// ========
// Objects
// ========
LCD_OBJECT_CONFIG; // If i2c: "Adafruit_LiquidCrystal lcd(LCD_I2C_ADR)", if parallel: LiquidCrystal lcd(12, 11, 10, 9, 8, 7)
Encoder Enc(ROTARY_ENCODER_CLK, ROTARY_ENCODER_DT); // Encoder object: "name"(Ch.A pin#, Ch.B pin#); Invoke: "enc.read();", "enc.write();"
OneButton btn = OneButton(ROTARY_ENCODER_PUSHBUTTON, true, true); // Button object = OneButton(pin#, active low?, internal pullup?);
BlockNot nonBlockingTimer(150); // Non-blocking timer object(milliseconds)
// =================
// 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"
// ==========
// Functions
// ==========
// Functions Declaration (needed for PlatformIO/CPP-file)
void debug();
void handle_command(const char *command);
bool is_valid_command(const char *command);
void splashScreen(const unsigned int splashDuration, const unsigned int splashBlinkDelay);
bool checkEncoder(); // Using this encoder: 62AG22-L5-040C: https://no.mouser.com/datasheet/2/626/grhl_s_a0011394895_1-2289471.pdf (3V3 version: 62VG22-L5-040C)
void lcdClearLn(uint8_t clearRow, uint8_t clearColumn, uint8_t lcdMaxNumOfColumns);
int minMaxReset(int input, int min, int max);
void handleSelection();
void run();
void outputPulses(const uint8_t travelDirection); // Makes quadrature pulses in the specified direction. Takes int argument for direction: 0 = UP, 1 = DOWN
void moveCursor(uint8_t menuPos);
void showMenu(const uint8_t menuType);
void onClick();
void longPress();
// Actual functions:
// Runs once
void setup()
{
// Config pins
pinMode(ROTARY_ENCODER_PUSHBUTTON, INPUT_PULLUP); // Rotary encoder momentary push button
pinMode(ROTARY_ENCODER_CLK, INPUT); // Rotary encoder input channel A
pinMode(ROTARY_ENCODER_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
// Config objects
btn.attachClick(onClick); // Fires as soon as a single click is detected
btn.attachLongPressStart(longPress); // Fires as soon as the button is held down for 1 second
// Initialize LCD, other devices, objects etc.
OPTIONAL_WOKWI_I2C_SETUP;
lcd.begin(LCD_COLUMNS, LCD_ROWS); // Initialize LCD, 20 columns, 4 rows
delay(100);
// Put values into arrays
mainMenuItems[0].value = pulsesPerMeter[1];
mainMenuItems[1].value = (String)distances[0] + "m";
mainMenuItems[2].value = (String)speeds[0] + "m/mi";
secondMenuItems[0].value = dir[1];
runMenuItems[0].value = (String)distances[4] + "m";
runMenuItems[1].value = (String)speeds[0] + "m/mi";
runMenuItems[2].value = (String)distElapsed + "m ";
runMenuItems[3].value = dir[0];
splashScreen(500, 100); // Display splashscreen/"welcome" greet on startup, because we're polite.
showMenu(currentMenu); // Show main menu
delay(100);
// 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..."));
} // Setup
// 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
// Creates a welcome message on the display, and briefly flashes the LEDs
void splashScreen(const unsigned int splashDuration, const unsigned int splashBlinkDelay)
{
lcd.clear();
// Charlimit--------------------
lcd.println(F(" === DEEPBox ==="));
lcd.setCursor(0, 1); // Column 1, Row 2
// Charlimit--------------------
lcd.println(F(" === V.0.0.0 ==="));
lcd.setCursor(0, 2); // Column 1, Row 3
// Charlimit--------------------
lcd.println(F(" === BAKER ==="));
lcd.setCursor(0, 3); // Column 1, Row 4
// Charlimit--------------------
lcd.println(F(" === 2023 ==="));
delay(splashDuration); // splashDuration = For how many ms splash graphics is displayed
// Blink LEDs on startup "splash screen"
uint8_t initBlink = 0;
while (initBlink < 5)
{
digitalWrite(OUTPUT_CH_A, HIGH);
delay(splashBlinkDelay);
digitalWrite(OUTPUT_CH_A, LOW);
digitalWrite(OUTPUT_CH_B, HIGH);
delay(splashBlinkDelay);
digitalWrite(OUTPUT_CH_B, LOW);
initBlink++;
}
lcd.clear();
} // splashScreen
// Read the rotary encoder's current position, detect position change and in which direction. Update menuCounter val accordingly.
bool checkEncoder()
{
static bool encoderPositionChanged = false; // Used to flag encoder position change
static long oldPos = -999; // Rotary encoder start position
static long newPos; // Encoder position when doing Enc.read()
newPos = Enc.read() / 4; // Read encoder pulses
newPos > oldPos ? menuCounter++, oldPos = newPos, encoderPositionChanged = true : // Did the encoder turn right? Decrement menuCounter
newPos < oldPos ? menuCounter--, oldPos = newPos, encoderPositionChanged = true : // Did the encoder turn left? Increment menuCounter
encoderPositionChanged = false; // Encoder position hasn't changed, do nothing
btn.tick(); // Check button state (Ref. OneButton lib)
return encoderPositionChanged;
} // checkEncoder
// 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
// 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
// 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();
break;
}
break;
}
} // handleSelection
// What to do when btn is pressed
void onClick()
{
switch (currentMenu)
{
case 0:
handleSelection();
break;
case 1:
handleSelection();
break;
case 2:
break;
}
} // onClick
// When longPress is called by btn, currently used to cancel run and/or restart the application
void longPress()
{
runFinished = false; // Breaks the completed run while loop, in case it's active
digitalWrite(OUTPUT_CH_A, LOW);
digitalWrite(OUTPUT_CH_B, LOW);
dirSel = 1; // Reset selections/settings-related variables
ppmSel = 1;
distSel = 0;
speedSel = 0;
currentMenu = 0;
menuCounter = 0;
distElapsed = 0; // Reset elapsed distance
mainMenuItems[0].value = pulsesPerMeter[ppmSel]; // Reset menu items do defaults
mainMenuItems[1].value = (String)distances[distSel] + "m";
mainMenuItems[2].value = (String)speeds[speedSel] + "m/mi";
secondMenuItems[0].value = dir[dirSel];
runMenuItems[0].value = (String)distances[4] + "m";
runMenuItems[1].value = (String)speeds[speedSel] + "m/mi";
runMenuItems[2].value = (String)distElapsed + "m ";
runMenuItems[3].value = dir[dirSel];
splashScreen(500, 100);
showMenu(currentMenu);
} // longPress
// 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
// Starts the simulation
void run()
{
runFinished = false;
currentMenu = 3; // Setting menu to 3; for some reason it gets set to 0 if only specified in "handleSelection"...
switch (dirSel)
{
case 0: // "BOTH"
lcdClearLn(3, 12, (LCD_COLUMNS - 1)); // Clear everything from row 2, column 12 and out
dirSel = 1; // Set current direction to "UP"
runMenuItems[3].value = dir[dirSel]; // Update current travel direction in menu items
showMenu(2); // Refresh menu on LCD
outputPulses(0); // "UP"
delay(1000); // Using a small, blocking delay here because nothing else should occur between the changing of direction
dirSel = 2; // Set current direction to "DOWN"
runMenuItems[3].value = dir[dirSel]; // Update current travel direction in menu items
showMenu(2); // Refresh menu on LCD
outputPulses(1); // "DOWN"
break;
case 1: // "UP"
outputPulses(0); // "UP"
break;
case 2: // "DOWN"
outputPulses(1); // "DOWN"
break;
}
if (runFinished)
{
dirSel = 1; // Reset selections/settings-related variables
ppmSel = 1;
distSel = 0;
speedSel = 0;
currentMenu = 0;
menuCounter = -2;
distElapsed = 0; // Reset elapsed distance
BlockNot endBlinkTimer(1000);
bool endBlinkState = false;
lcd.setCursor(11, 3); lcd.print(F("N/A "));
while (runFinished)
{
if (endBlinkTimer.TRIGGERED)
{
if (endBlinkState)
{
digitalWrite(OUTPUT_CH_A, LOW);
digitalWrite(OUTPUT_CH_B, LOW);
lcdClearLn(2, 11, (LCD_COLUMNS - 1)); // Clear everything from row 2, column 12 and out
endBlinkState = false;
}
else if (!endBlinkState)
{
lcd.setCursor(11, 2); lcd.print(F("DONE!"));
digitalWrite(OUTPUT_CH_A, HIGH);
digitalWrite(OUTPUT_CH_B, HIGH);
endBlinkState = true;
}
}
checkEncoder();
debug(); // TODO: Only for feedback during test, remove before production!
}
}
if (nonBlockingTimer.TRIGGERED) // 150ms
{
debug();
}
} // run
// Makes quadrature pulses in the specified direction. Takes int argument for direction: 0 = UP, 1 = DOWN
void outputPulses(const uint8_t travelDirection)
{
runFinished = false;
BlockNot temporaryTimer(5000); // TODO: Only for test purposes, remove before production!
unsigned int totalPulses = distances[distSel] * pulsesPerMeter[ppmSel]; // Total number of pulses to be generated
unsigned int pulseCycleCount = 0; // Count of pulses generated
uint8_t pulseIntervalArrayIndex = 0; // Index used to select the pulse interval from array
const uint16_t pulseIntervalsArray[8] = {11718, 3906, 2343, 1171, // Array of possible intervals (μs) between two pulses, derived from selected speed
5859, 1953, 1171, 585};
// Interval formula: 60'000'000 / (speed * pulses/meter) >> https://docs.google.com/spreadsheets/d/1iSJ3GRwYg6v-2EAzArJU7HahjW1eNBRwfr_UCq-fvGs/edit?usp=sharing
switch (ppmSel) // Determine interval array index
{
case 1: // 512
pulseIntervalArrayIndex = speedSel; // First 4 entries in interval array are for 512 pulses per meter
break;
case 0: // 1024
pulseIntervalArrayIndex = speedSel + 4; // Last 4 entries in interval array are for 1024 pulses per meter
break;
} // switch (ppmSel)
uint8_t pulseSequenceFlag = 0; // Flag used to track current sequence (ab,Ab,AB,aB,ab etc..)
const uint16_t pulseInterval = (pulseIntervalsArray[pulseIntervalArrayIndex]); // Interval: full period
const uint16_t pulseQuarterInterval = (pulseInterval / 4); // Interval: 1/4 period
BlockNot intervalTimer((pulseInterval), MICROSECONDS); // Set a microsecond timer, used to time the pulses
BlockNot intervalQuarterTimer((pulseQuarterInterval), MICROSECONDS); // Set a microsecond timer, used to time the pulses
// Spit out some data before emitting pulses. // TODO: Only for test purposes, remove before production!
Serial.print(F("> 'totalPulses'.....: ")); Serial.println(totalPulses);
Serial.print(F("> 'Interval'........: ")); Serial.println((String)pulseInterval + "μs");
Serial.print(F("> 'QuarterInterval'.: ")); Serial.println((String)pulseQuarterInterval + "μs");
Serial.print(F("> 'currentMenu'.....: ")); Serial.println(currentMenu);
digitalWrite(OUTPUT_CH_A, LOW); // Ensure both outputs are low before starting
digitalWrite(OUTPUT_CH_B, LOW);
// Generate pulses, good visualization here: https://www.usdigital.com/media/qktgqmkj/blog011-image007-pulse-multiplication.png?width=1000&height=660
while (pulseCycleCount <= totalPulses && !runFinished)
{
switch (travelDirection)
{
case 0: // UP
if (intervalQuarterTimer.TRIGGERED) // Check if the desired pulse interval has elapsed
{
switch (pulseSequenceFlag)
{
case 0:
digitalWrite(OUTPUT_CH_A, HIGH);
pulseSequenceFlag++;
break;
case 1:
digitalWrite(OUTPUT_CH_B, HIGH);
pulseSequenceFlag++;
break;
case 2:
digitalWrite(OUTPUT_CH_A, LOW);
pulseSequenceFlag++;
break;
case 3:
digitalWrite(OUTPUT_CH_B, LOW);
pulseSequenceFlag = 0;
break;
}
}
break;
case 1: // DOWN
if (intervalQuarterTimer.TRIGGERED) // Check if the desired pulse interval has elapsed
{
switch (pulseSequenceFlag)
{
case 0:
digitalWrite(OUTPUT_CH_B, HIGH);
pulseSequenceFlag++;
break;
case 1:
digitalWrite(OUTPUT_CH_A, HIGH);
pulseSequenceFlag++;
break;
case 2:
digitalWrite(OUTPUT_CH_B, LOW);
pulseSequenceFlag++;
break;
case 3:
digitalWrite(OUTPUT_CH_A, LOW);
pulseSequenceFlag = 0;
break;
}
}
break;
default: break;
}
if (intervalTimer.TRIGGERED) // Check if the pulse interval has fully elapsed
{
pulseCycleCount++;
}
if (pulseCycleCount % pulsesPerMeter[ppmSel] == 0) // Increment meters traveled every 512 or 1024 pulse cycles
{
switch (travelDirection)
{
case 0: // UP
distElapsed++;
break;
case 1: // DOWN
distElapsed--;
break;
default: break;
}
runMenuItems[2].value = (String)distElapsed + "m ";
showMenu(2);
}
if (temporaryTimer.TRIGGERED) // TODO: Only for feedback during test, remove before production!
{
Serial.print(F("> 'totalPulses'.: ")); Serial.println(totalPulses);
Serial.print(F("> 'pulseCount'..: ")); Serial.println((String)pulseCycleCount + " / " + (String)totalPulses);
Serial.print(F("> 'distElapsed'.: ")); Serial.println((String)distElapsed + "m\n");
}
if (nonBlockingTimer.TRIGGERED) // 150ms
{
debug();
checkEncoder();
}
}
runFinished = true; // Indicate that the run finished
} // outputPulses
// Runs continuously
void loop()
{
if (checkEncoder() == true) // Has the encoder changed position?
{
showMenu(currentMenu); // Refresh menu upon encoder position change
if (currentMenu == 0 || currentMenu == 1) // "If in main OR secondary menu..."
{
moveCursor(menuCounter); // Move cursor, it we're not inside the run-menu
}
}
if (nonBlockingTimer.TRIGGERED) // 150ms non-blocking timer
{
debug();
}
} // Loop