As I stated previously, I only included the keyboard parts. Please see below for the entire sketch that I took the initial keyboard code from.
// DispenseController_NanoEvery_v2.ino
// Implements keypad entry via PCF8574 (I2C), 4x20 LCD via I2C, stepper (STEP+DIR),
// a trigger button (press & release to start dispense), Move In / Move Out buttons (hold),
// Resume button to dispense leftover after limit stop, limit switch stops dispensing,
// display rows:
// Row1: Requested amount (cc)
// Row2: Amount dispensed up to limit activation (cc)
// Row3: "EMPTY" when limit active
// Row4: Current action ("Dispense","Move In","Move Out","Finish")
// Clears all saved values at power-up.
//
// Configure the pin assignments and calibration constant STEPS_PER_CC below.
#include <Wire.h>
#include <AccelStepper.h>
#include <LiquidCrystal_I2C.h>
#include <PCF8574.h>
// ----------------- USER CONFIG -----------------
const uint8_t LCD_ADDR = 0x27; // change to your LCD I2C address
const uint8_t KBD_ADDR = 0x20; // change to your PCF8574 I2C address
// PCF8574 keypad wiring: P0..P3 -> rows; P4..P7 -> cols (change if your wiring differs)
const byte ROWS = 4;
const byte COLS = 4;
char hexaKeys[ROWS][COLS] = {
{'1','2','3','A'},
{'4','5','6','B'},
{'7','8','9','C'},
{'*','0','#','D'}
};
const uint8_t kbdRowPins[ROWS] = {0,1,2,3}; // PCF8574 pins for rows
const uint8_t kbdColPins[COLS] = {4,5,6,7}; // PCF8574 pins for cols
// Stepper driver pins (Arduino digital pins)
const uint8_t STEP_PIN = 20; // step
const uint8_t DIR_PIN = 21; // direction
//const uint8_t EN_PIN = 5; // optional enable pin (active LOW). Set to 255 if unused.
// Non-keypad buttons (active LOW with INPUT_PULLUP)
const uint8_t TRIGGER_PIN = 26; // press & release to start dispense
const uint8_t MOVE_IN_PIN = 22; // hold to move in
const uint8_t MOVE_OUT_PIN = 23; // hold to move out
const uint8_t RESUME_PIN = 24; // press to dispense leftover after limit stop
// Limit switch (active LOW)
const uint8_t LIMIT_PIN = 25; // limit switch active = LOW
/* For future version
//LED + Buzzer (external, not onboard LED)
const uint8_t LED_PIN = XX;
const uint8_t BUZZER_PIN = XX;
*/
// Mechanical calibration - CHANGE THIS to match your system
// Steps required to move plunger such that 1.0 cc is dispensed.
float STEPS_PER_CC = 17.0; // <-- set to your calibrated value
// Motion tuning
const float DISPENSE_SPEED_STEPS_PER_SEC = 400.0; // speed while dispensing
const float MANUAL_MOVE_SPEED_STEPS_PER_SEC = 1000.0; // speed while Move In/Out
const float ACCEL_STEPS_PER_SEC2 = 800.0;
// ------------------------------------------------
// Objects
PCF8574 pcf(KBD_ADDR);
LiquidCrystal_I2C lcd(LCD_ADDR, 20, 4);
AccelStepper stepper(AccelStepper::DRIVER, STEP_PIN, DIR_PIN);
// Runtime state (cleared at power-up)
String inputBuffer = "";
volatile int requestedCC = 0; // user requested amount (cc)
volatile float leftoverCC = 0.0; // amount left to dispense (cc) after limit stop
volatile int dispensedCC_at_limit = 0; // integer cc dispensed up to limit activation
volatile bool isEmpty = false; // true if limit switch active
volatile bool dispensing = false;
long dispenseStartPosition = 0; // stepper position when a dispense started
long dispenseTargetPosition = 0; // stepper target (absolute)
long stepsPerCCLong = 0; // cached integer for steps per cc rounding
String actionText = "Finish";
// Keypad scan timing
unsigned long lastKbdScan = 0;
const unsigned long KBD_SCAN_INTERVAL = 40; // ms
// Debounce
unsigned long lastTriggerDebounce = 0;
const unsigned long TRIGGER_DEBOUNCE_MS = 50;
bool triggerWasPressed = false;
bool lastTriggerState = HIGH;
//Calibration State
bool inCalibration = false;
long calStartSteps = 0;
float calMeasuredCC = 0.0;
// Helper prototypes
char scanKeypad();
void updateLCD();
void startDispenseFromRequested(); // start full requested
void startDispenseCC(float cc); // start dispense of a specific cc amount (non-blocking)
void stopDispenseByLimit();
void finishDispenseNormally();
void runCalibration();
bool isButtonPressed(uint8_t pin); // debounced read (active LOW)
//void enableDriver(bool en);
void setup() {
Serial.begin(9600);
Wire.begin();
// Clear saved values upon power-up (requirement #10)
inputBuffer = "";
requestedCC = 0;
leftoverCC = 0.0;
dispensedCC_at_limit = 0;
isEmpty = false;
dispensing = false;
actionText = "Finish";
// Setup PCF8574 for keypad
if (!pcf.isConnected()) {
Serial.println("Warning: PCF8574 not responding at configured address.");
}
// initialize pins: set columns as outputs and rows as inputs
for (uint8_t i=0;i<8;i++) {
pcf.pinMode(i, INPUT); // default
pcf.digitalWrite(i, HIGH); // release
}
for (uint8_t c=0;c<COLS;c++) {
pcf.pinMode(kbdColPins[c], OUTPUT);
pcf.digitalWrite(kbdColPins[c], HIGH);
}
for (uint8_t r=0;r<ROWS;r++) {
pcf.pinMode(kbdRowPins[r], INPUT);
}
// Setup LCD
lcd.init();
lcd.backlight();
lcd.clear();
lcd.setCursor(0,0);
lcd.print("Power-up cleared");
lcd.setCursor(0,1);
lcd.print("Enter amount (cc)");
delay(700);
lcd.clear();
// Setup stepper pins & AccelStepper
pinMode(STEP_PIN, OUTPUT);
pinMode(DIR_PIN, OUTPUT);
/*if (EN_PIN != 255) {
pinMode(EN_PIN, OUTPUT);
digitalWrite(EN_PIN, HIGH); // disable driver at start (active LOW)
}
*/
stepper.setMaxSpeed(MANUAL_MOVE_SPEED_STEPS_PER_SEC);
stepper.setAcceleration(ACCEL_STEPS_PER_SEC2);
// Buttons & limit switch
pinMode(TRIGGER_PIN, INPUT_PULLUP);
pinMode(MOVE_IN_PIN, INPUT_PULLUP);
pinMode(MOVE_OUT_PIN, INPUT_PULLUP);
pinMode(RESUME_PIN, INPUT_PULLUP);
pinMode(LIMIT_PIN, INPUT_PULLUP);
// Precompute integer steps per cc (rounded) for tracking convenience
stepsPerCCLong = (long)round(STEPS_PER_CC);
updateLCD();
Serial.println("Ready. Enter amount on keypad, press # to save. '*' clears entry.");
}
// ---------------------- Main Loop ------------------------
void loop() {
// Keypad scanning (numbers, # to save, * to clear). Only used for entering requested amount.
if (millis() - lastKbdScan >= KBD_SCAN_INTERVAL) {
lastKbdScan = millis();
char k = scanKeypad();
if (k) {
Serial.print("K: "); Serial.println(k);
if (k >= '0' && k <= '9') {
if (inputBuffer.length() < 6) inputBuffer += k; // allow up to 6 digits
} else if (k == '*') {
inputBuffer = "";
requestedCC = 0;
leftoverCC = 0.0;
dispensedCC_at_limit = 0;
actionText = "Finish";
} else if (k == '#') {
if (inputBuffer.length() > 0) {
requestedCC = inputBuffer.toInt();
inputBuffer = "";
leftoverCC = (float)requestedCC; // initially leftover equals requested
dispensedCC_at_limit = 0;
actionText = "Ready";
Serial.print("Requested set to: "); Serial.println(requestedCC);
} else if (k == 'A'){
//Start Calibration Routine
inCalibration = true;
runCalibration();
// ignore empty '#'
}
}
}
}
// Read limit switch (active LOW)
bool limitActive = (digitalRead(LIMIT_PIN) == LOW);
if (limitActive && !isEmpty) {
// limit just activated
isEmpty = true;
// if dispensing, stop and compute leftover
if (dispensing) {
// compute steps moved so far during this dispense
long posNow = stepper.currentPosition();
long stepsMovedSinceStart = labs(posNow - dispenseStartPosition);
float ccDone = (float)stepsMovedSinceStart / STEPS_PER_CC;
dispensedCC_at_limit = (int)floor(ccDone + 1e-6);
float requestedF = (float)requestedCC;
leftoverCC = max(0.0f, requestedF - ccDone);
stopDispenseByLimit();
}
} else if (!limitActive && isEmpty) {
// limit released
isEmpty = false;
}
// Trigger button handling: press & release starts dispense of requested amount
// Debounce & detect press-release sequence
bool trigState = digitalRead(TRIGGER_PIN);
if (trigState != lastTriggerState) {
lastTriggerDebounce = millis();
lastTriggerState = trigState;
}
if (millis() - lastTriggerDebounce > TRIGGER_DEBOUNCE_MS) {
if (trigState == LOW) {
triggerWasPressed = true;
} else {
if (triggerWasPressed) {
triggerWasPressed = false;
// user pressed & released -> start dispense
if (!isEmpty && requestedCC > 0 && !dispensing) {
startDispenseFromRequested();
}
}
}
}
// Move In / Move Out (non-keypad physical buttons) - hold to move continuously
bool moveInPressed = (digitalRead(MOVE_IN_PIN) == LOW) && !isEmpty;
bool moveOutPressed = (digitalRead(MOVE_OUT_PIN) == LOW) && !isEmpty;
if (moveInPressed && !dispensing) {
// Move In - define direction negative (adjust sign if your assembly is reversed)
actionText = "Move In";
//enableDriver(true);
stepper.setMaxSpeed(MANUAL_MOVE_SPEED_STEPS_PER_SEC);
stepper.setSpeed(-MANUAL_MOVE_SPEED_STEPS_PER_SEC); // negative for "in"
stepper.runSpeed();
} else if (moveOutPressed && !dispensing) {
// Move Out - positive direction
actionText = "Move Out";
//enableDriver(true);
stepper.setMaxSpeed(MANUAL_MOVE_SPEED_STEPS_PER_SEC);
stepper.setSpeed(MANUAL_MOVE_SPEED_STEPS_PER_SEC);
stepper.runSpeed();
} else {
// neither held: if not dispensing, stop manual motion and optionally disable driver
if (!dispensing) {
stepper.setSpeed(0);
//enableDriver(false);
if (actionText == "Move In" || actionText == "Move Out") actionText = "Finish";
}
}
// Resume button: dispense leftover when pressed (single press)
static bool resumePrev = HIGH;
bool resumeState = digitalRead(RESUME_PIN);
if (resumeState != resumePrev) {
delay(20);
resumePrev = resumeState;
}
if (resumeState == LOW && !dispensing && leftoverCC > 0.001 && !isEmpty) {
// start dispensing leftover amount
startDispenseCC(leftoverCC);
}
// Running a dispense (non-blocking using AccelStepper)
if (dispensing) {
//enableDriver(true);
// run() returns true while still moving; run() handles acceleration & deceleration
stepper.run();
// Update dispensedCC_at_limit continuously (for display) - but it's only final if limit hit
long posNow = stepper.currentPosition();
long stepsMovedSinceStart = labs(posNow - dispenseStartPosition);
float ccDone = (float)stepsMovedSinceStart / STEPS_PER_CC;
dispensedCC_at_limit = (int)floor(ccDone + 1e-6);
// if we reached target
if (stepper.distanceToGo() == 0) {
// finished normally
finishDispenseNormally();
}
}
// Regular LCD refresh (not every loop)
static unsigned long lastLCD = 0;
if (millis() - lastLCD > 200) {
lastLCD = millis();
updateLCD();
}
}
// ---------------------- Functions ------------------------
/* scanKeypad()
Basic matrix scan using PCF8574.
Returns 0 if no key pressed, or the key char.
This routine contains a small debounce.
*/
char scanKeypad() {
for (int c = 0; c < COLS; c++) {
// release all cols
for (int cc = 0; cc < COLS; cc++) pcf.digitalWrite(kbdColPins[cc], HIGH);
// pull this column low
pcf.digitalWrite(kbdColPins[c], LOW);
delayMicroseconds(30);
for (int r = 0; r < ROWS; r++) {
int v = pcf.digitalRead(kbdRowPins[r]);
if (v == 0) { // low = pressed
delay(20);
if (pcf.digitalRead(kbdRowPins[r]) == 0) {
char k = hexaKeys[r][c];
// simple wait-for-release to avoid repeated rapid triggers
// (commented out to allow quick entries; keep as needed)
//while (digitalRead(kbdRowPins[r]) == 0) delay(5);
pcf.digitalWrite(kbdColPins[c], HIGH); // restore
return k;
}
}
}
pcf.digitalWrite(kbdColPins[c], HIGH);
}
return 0;
}
// update LCD rows per requirements 5..8
void updateLCD() {
lcd.setCursor(0,0);
// Row1: requested amount in cc
lcd.print("Req: ");
if (requestedCC > 0) {
lcd.print(requestedCC);
lcd.print(" cc");
// pad
for (int i = 0; i < 10; i++) lcd.print(' ');
} else if (inputBuffer.length() > 0) {
lcd.print(inputBuffer);
lcd.print(" cc");
for (int i = 0; i < 10; i++) lcd.print(' ');
} else {
lcd.print("0 cc");
for (int i = 0; i < 13; i++) lcd.print(' ');
}
// Row2: amount dispensed up to activation of limit (or current dispensed during motion)
lcd.setCursor(0,1);
lcd.print("Dispensed: ");
lcd.print(dispensedCC_at_limit);
lcd.print(" cc");
for (int i = 0; i < 6; i++) lcd.print(' ');
// Row3: "EMPTY" when limit active (requirement #7)
lcd.setCursor(0,2);
if (isEmpty) {
//flashEmptyAlert(true);
lcd.print("EMPTY");
for (int i = 5; i < 20; i++) lcd.print(' ');
} else {
// clear
for (int i = 0; i < 20; i++) lcd.print(' ');
//flashEmptyAlert(false);
}
// Row4: action text (Dispense, Move In, Move Out, Finish)
lcd.setCursor(0,3);
lcd.print(actionText);
for (int i = actionText.length(); i < 20; i++) lcd.print(' ');
}
// Start dispensing the currently requested amount
void startDispenseFromRequested() {
if (requestedCC <= 0) return;
startDispenseCC((float)requestedCC);
}
// Start dispensing a specific CC amount (non-blocking)
void startDispenseCC(float cc) {
if (isEmpty) {
Serial.println("Cannot dispense: EMPTY (limit active).");
actionText = "Finish";
return;
}
long steps = (long)round(cc * STEPS_PER_CC);
if (steps <= 0) return;
// prepare stepper
//enableDriver(true);
stepper.setAcceleration(ACCEL_STEPS_PER_SEC2);
stepper.setMaxSpeed(DISPENSE_SPEED_STEPS_PER_SEC);
dispenseStartPosition = stepper.currentPosition();
// relative move forward (assume positive direction dispenses)
stepper.move(steps);
dispenseTargetPosition = dispenseStartPosition + steps;
dispensing = true;
actionText = "Dispense";
Serial.print("Dispense started: "); Serial.print(cc); Serial.print(" cc (");
Serial.print(steps); Serial.println(" steps)");
}
// Called when limit switch triggered during dispense
void stopDispenseByLimit() {
if (!dispensing) return;
// Stop stepper immediately by clearing target (we can moveTo current pos)
long posNow = stepper.currentPosition();
stepper.stop(); // decelerates to stop
stepper.setCurrentPosition(posNow); // freeze position
dispensing = false;
actionText = "Finish";
// dispensedCC_at_limit and leftoverCC already computed by caller
Serial.println("Dispense stopped by limit. Marked EMPTY.");
}
// Called when dispense finishes normally (target reached)
void finishDispenseNormally() {
dispensing = false;
//enableDriver(false);
// All requested dispensed -> clear leftover
leftoverCC = 0.0;
dispensedCC_at_limit = requestedCC;
actionText = "Finish";
Serial.println("Dispense finished normally.");
}
//Calibration Function
void runCalibration(){
lcd.clear();
lcd.setCursor (0,0); lcd.print("Calibration Mode");
lcd.setCursor (0,1); lcd.print("Move plunger out");
lcd.setCursor (0,2); lcd.print("Press TRIGGER");
lcd.setCursor(0,3); lcd.print("when ready");
//Wait for user to press Trigger
while (digitalRead(TRIGGER_PIN) == HIGH);
calStartSteps - stepper.currentPosition();
lcd.clear(); lcd.setCursor(0,0); lcd.print("Enter measured cc");
//User would enter measured cc on keypad after moving a known amount
//Simplify: assume known 10cc movement
calMeasuredCC = 10.0;
long movedSteps = abs(stepper.currentPosition() - calStartSteps);
if(calMeasuredCC > 0.1){
STEPS_PER_CC = movedSteps / calMeasuredCC;
}
lcd.clear(); lcd.setCursor(0,0); lcd.print("New Cal: ");
lcd.print(STEPS_PER_CC,1); lcd.print(" steps/cc");
delay(2000);
inCalibration = false;
}
// enable/disable driver (active LOW enable)
/*void enableDriver(bool en) {
if (EN_PIN == 255) return;
if (en) digitalWrite(EN_PIN, LOW); // enable driver
else digitalWrite(EN_PIN, HIGH); // disable driver
}
*/
/* To be used in later version
void flashEmptyAlert(bool state) {
if (state) {
digitalWrite(LED_PIN, HIGH);
digitalWrite(BUZZER_PIN, HIGH);
} else {
digitalWrite(LED_PIN, LOW);
digitalWrite(BUZZER_PIN, LOW);
}
*/
As the last responder has coded, I do not see any difference between what I had and what that contributor has provided. I saw in another thread that there could be more than one instance of the library and I will need to check that.