Timing of controlling ticket dispenser

I'm writing a game program and it will issue tickets once a min score is reached. 1 ticket per per interval of 30 points.

The code sends an ENABLED signal to turn on the dispenser then it returns a LOW each time it sees the notch in the tickets and this is how I'm counting the tickets issued.

the following code works some times and not others.

known issues:

  1. if the program starts and the tickets are at a notch (Notch = LOW), it runs until i hit reset. If it starts on a ticket (notch = HIGH) then it will issue 1 ticket at a time, as expected.
    2.if it runs out of tickets, not sure how to stop it.

I have spent 2 days rewriting the code several different ways and just cant figure it out. I just looking for some input as I just cant get it right.

Here is a link to the dispenser

here is the signal for the control
1275_signals

Here is the code

void ticketControl () {
  //if tickets set to 0, tickets are off
  if (noTickets > 0) {
    //============ notch input ==============
    int notchState = digitalRead(ticketNotch);
    // If the notch input changed
    if (notchState != notchLastState) {
      // reset the debouncing timer
      notchPinLastTime = millis();
    }
    if ((millis() - notchPinLastTime) > notchDelay) {
      // if the button state has changed:
      if (notchState != notchNewState) {
        notchNewState = notchState;

        if (notchNewState == LOW) {
          ticketsIssued += 1;

        }
      }
    }
    // save the notchState. Next time through the loop, it'll be the notchLastState:
    notchLastState = notchState;
    //========== notch input ==========
    //========== calc tickets earnerd =========
    if (totalScore >= winningScore) {
      ticketsEarned = ((totalScore - winningScore) / ticketSpan) + 1;
      if (ticketsIssued < ticketsEarned) {
        digitalWrite(ticketEnablePin, HIGH);
      } else {
        digitalWrite(ticketEnablePin, LOW);
      }
      Serial.print("Tickets earned: " );
      Serial.print(ticketsEarned);
      Serial.print(" tickets issued: " );
      Serial.println(ticketsIssued);
    }
  } else {
    Serial.println(" TICKETS ARE OFF" );
  }



}

So far, so good. But how to you have it all wired, including the power supplies?

yea, it powered with a 12v 5a power, it only draws 1.7a at peak. power isn't my issue, its all code.

But you only show a bit of code. Why. How can we try to understand YOUR logic if half is missing?
Paul

I included only the function in question, other code has no bearing on it, but here it all is.

// constants won't change. They're used here to set pin numbers:
const int coinPin = 12;    // the number of the pushbutton pin

// Variables will change:
int coinPinNewState;             // the current coinSwState from the input pin
int coinSwLastState = LOW;   // the previous coinSwState from the input pin
int count = 0;
// the following variables are unsigned longs because the time, measured in
// milliseconds, will quickly become a bigger number than can be stored in an int.
unsigned long coinPinLastTime = 0;  // the last time the output pin was toggled
unsigned long coinPinDelay = 5;    // the debounce time; increase if the output flickers

int winningScore = 150; // plays winning song and lights winner sign, first ticket
int ticketSpan = 30;  //span between tickets
int noTickets = 1; // no tickets per ticket interval
int totalScore = 130 ;
int ticketNotch = 33; //change to 33
int ticketEnablePin = 31; //change to 31
int notchDelay = 30;

unsigned long notchPinLastTime = 0;
int notchLastState ;
int notchNewState ;
int ticketsEarned = 0;
int ticketsIssued = 0;


void setup() {
  Serial.begin(115200);
  pinMode(coinPin, INPUT);
  pinMode(ticketEnablePin, OUTPUT);
  pinMode(ticketNotch, INPUT);
  digitalWrite(ticketEnablePin, LOW);
    Serial.println("");
  Serial.println("Ready!");
}

void readInputs() {
  // read the state of the switch into a local variable:
  int coinSwState = digitalRead(coinPin);

  //============COIN INPUT=======================
  // If the switch changed, due to noise or pressing:
  if (coinSwState != coinSwLastState) {
    // reset the debouncing timer
    coinPinLastTime = millis();
  }
  if ((millis() - coinPinLastTime) > coinPinDelay) {

    // if the button state has changed:
    if (coinSwState != coinPinNewState) {
      coinPinNewState = coinSwState;

      if (coinPinNewState == LOW) {
        totalScore += 10;
        Serial.print("Score: ");
        Serial.print(totalScore);
        Serial.println("");
      }
    }
  }
  // save the coinSwState. Next time through the loop, it'll be the coinSwLastState:
  coinSwLastState = coinSwState;
  //============COIN INPUT=======================


}
void loop() {
  readInputs();
  ticketControl ();
  //
}

//int notchDelay = 30;

void ticketControl () {
  //if tickets set to 0, tickets are off
  if (noTickets > 0) {
    //============ notch input ==============
    int notchState = digitalRead(ticketNotch);
    // If the notch input changed
    if (notchState != notchLastState) {
      // reset the debouncing timer
      notchPinLastTime = millis();
    }
    if ((millis() - notchPinLastTime) > notchDelay) {
      // if the button state has changed:
      if (notchState != notchNewState) {
        notchNewState = notchState;

        if (notchNewState == LOW) {
          ticketsIssued += 1;

        }
      }
    }
    // save the notchState. Next time through the loop, it'll be the notchLastState:
    notchLastState = notchState;
    //========== notch input ==========
    //========== calc tickets earnerd =========
    if (totalScore >= winningScore) {
      ticketsEarned = ((totalScore - winningScore) / ticketSpan) + 1;
      if (ticketsIssued < ticketsEarned) {
        digitalWrite(ticketEnablePin, HIGH);
      } else {
        digitalWrite(ticketEnablePin, LOW);
      }
      Serial.print("Tickets earned: " );
      Serial.print(ticketsEarned);
      Serial.print(" tickets issued: " );
      Serial.println(ticketsIssued);
    }
  } else {
    Serial.println(" TICKETS ARE OFF" );
  }



}

at first your code has to analyse if a ticket is in a position that the notch has switched to LOW or not.
If it is LOW before you start to dispense the logic must check for state-change from LOW to HIGH indicating the first ticket has left the notch-LOW-position

What does indicate this first ticket has "moved out"?

if it is HIGH before you start the dispenser start dispenser
What does indicate this first ticket has "moved out"?

best regards Stefan

When I tried the link you provided, I got a warning about a security risk,so I aborted.
You have at least two places where your comments indicate debounce is taking place. Is your code so fast that bouncing is a problem? From what I can see, you are not doing a loop anywhere, so fast, that debouncing is warrented.
Also, what is the relationship of the ticket notch pulse to ONE ticket. IS it one for one, or something else?
Paul

That's a demonstrably false statement. For example, the code in your original post used global variables whose definitions weren't included.

the ticketControl() is being called in the main loop. If you don't debounce it, it will count multiple tickets per notch. there is 1 notch between tickets so yes 1 notch pulse = 1 ticket.

If the sensor is showing a 'notch' and it doesn't change to 'no_notch' within a short time of turning on the motor, either you are out of tickets or the motor is broken.

My earlier issue with inconsistent reading of the "notch" was because I needed a pull up resistor on the input from the notch, now I get good HIGH / LOW as expected.

I've spent a lot of time working on this and keep running into the same issue where the code to stop the dispenser if out of tickets always times out, I cannot wrap me head around why. I'm hoping I'm overlooking the obvious.

when I comment out the code for the time out delay, it works perfectly.


const int scorePin = 12;    // the number of the pushbutton pin

// Variables for score input
int scorePinNewState;             // the current scoreSwState from the input pin
int scoreSwLastState = LOW;   // the previous scoreSwState from the input pin
unsigned long scorePinLastTime = 0;  // the last time the output pin was toggled
unsigned long scorePinDelay = 5;    // the debounce time; increase if the output flickers


//variables for tickets
int winningScore = 150; // plays winning song and lights winner sign, first ticket
int ticketSpan = 30;  //score span between tickets
int noTickets = 1; // number tickets per ticket interval
int totalScore = 130 ; //temporary start at 130, typically 0

int ticketNotchPin = 33; //read ticket notch
int ticketEnablePin = 31; //turn on ticket dispenser motor
int notchDelay = 40;
unsigned long notchPinLastTime = 0;
int notchTimeOut = 2000; //time allowed before out of tickets
int notchPrevState ;
int notchCurState ;
int ticketsEarned = 0;
int ticketsIssued = 0;
boolean ticketsOut = false; // flag for is dispenser is out of tickets
boolean dispenserEnabled = false;


void setup() {
  Serial.begin(115200);
  pinMode(scorePin, INPUT);
  pinMode(ticketEnablePin, OUTPUT);
  pinMode(ticketNotchPin, INPUT);
  digitalWrite(ticketEnablePin, LOW);
  Serial.println("Ready!");
}


void loop() {
  readInputs(); //read button to increment up score
  ticketControl ();
}



void ticketControl () {
  // if noTickets set to 0, then tickets are off
  if (noTickets > 0) {
    if (ticketsOut == false) {
      int notchState = digitalRead(ticketNotchPin);
      //if dispenser status is idle then check for ticket position.
      if (!dispenserEnabled) {
        //check current state and set previous state
        if (notchState == HIGH) {
          notchPrevState = LOW;
          notchCurState = HIGH;
        } else {
          notchPrevState = HIGH;
          notchCurState = LOW;
        }
      }
      if (totalScore >= winningScore) {
        ticketsEarned = ((totalScore - winningScore) / ticketSpan) + 1;
        if (ticketsIssued < ticketsEarned) {
          digitalWrite(ticketEnablePin, HIGH);
          dispenserEnabled = true;
          // Serial.println("turn on motor " );
          // If the notch input changed 
          if (notchState != notchPrevState && notchState == LOW) {
            // reset the debouncing timer
            notchPinLastTime = millis();
            Serial.print(" Last time: ");
            Serial.println(notchPinLastTime);

          }
          if ((millis() - notchPinLastTime) > notchDelay) {
            Serial.print(" > NotchDelay: ");
            Serial.print ((millis() - notchPinLastTime));
            Serial.print(" Notch State: ");
            Serial.print(notchState);
            Serial.print(" Notch CurState: ");
            Serial.println(notchCurState);
            // if the notch state has changed:
            if (notchState != notchCurState) {
              notchCurState = notchState;

              if (notchCurState == LOW) {
                ticketsIssued += 1;
                Serial.print("Tickets earned: " );
                Serial.print(ticketsEarned);
                Serial.print(" tickets issued: " );
                Serial.println(ticketsIssued);
              }
            }
          }

          notchPrevState = notchState;
          //if no notch in 2000 ms then out of tickets
          //THIS CURRENT ALWAYS TURNS OFF DISPENSER AND SETS OUT OF TICKETS FLAG
          //WITHOUT THIS TIME OUT SECTION,  CODE WORKS AS EXPECTED.
//          if ((millis() - notchPinLastTime) > notchTimeOut) {
//            digitalWrite(ticketEnablePin, LOW);
//            Serial.println("turn OFF motor - out of tickets" );
//            ticketsOut = true;
//            Serial.print("tickets time out: ");
//            Serial.print((millis() - notchPinLastTime));
//            Serial.print(" Last time: ");
//            Serial.println(notchPinLastTime);
//          }
        } else {
          digitalWrite(ticketEnablePin, LOW);
          //Serial.println("turn OFF motor 1 " );
          Serial.print("Motor OFF Notch State: ");
          Serial.println(notchState);
          dispenserEnabled = false;
        }// end tickets earned

      }//end winning score
    } else {
      //Serial.println(" OUT OF TICKETS" );
    }
  } else {
    Serial.println(" TICKETS ARE OFF" );
  }
}//end of ticket control

void readInputs() {
  // THIS IS USED TO INCREMENT THE TOTAL SCORE
  // read the state of the switch into a local variable:
  int scoreSwState = digitalRead(scorePin);
  //============score INPUT=======================
  // If the switch changed, due to noise or pressing:
  if (scoreSwState != scoreSwLastState) {
    // reset the debouncing timer
    scorePinLastTime = millis();
  }
  if ((millis() - scorePinLastTime) > scorePinDelay) {

    // if the button state has changed:
    if (scoreSwState != scorePinNewState) {
      scorePinNewState = scoreSwState;

      if (scorePinNewState == LOW) {
        totalScore += 10;
        Serial.print("Score: ");
        Serial.println(totalScore);
        //Serial.println("");
      }
    }
  }
  // save the scoreSwState. Next time through the loop, it'll be the scoreSwLastState:
  scoreSwLastState = scoreSwState;
  //============score INPUT=======================


}

I hate to say it but I think it is time to change your design to a Finite State Machine. This will make it easier to keep track of what the dispenser is doing. You may end up with two separate state machines. One to determine the number of tickets to dispense and the other to dispense tickets.

I looked up FSM, I'm not familiar with that concept at all. I found this example, very basic but I do follow it. Not sure how to implement in my project but I'll give it a shot.

While I generally prefer this approach as well, I think this is simple enough that something like this might work.

Untested code follows:

#define ENABLE_PIN 0 // TODO change this

#define INTERRUPT_PIN 0 // TODO change this

// I have no idea how many tickets you want to do dispense. Adjust data type accordingly.
static volatile uint32_t undispensed = 0;

static void dispenseHandler(void) {
    if (undispensed) {
        undispensed--;
    }
    
    if (undispensed == 0) {
        digitalWrite(ENABLE_PIN, LOW);
    }
}

static void dispenseTickets(uint32_t count) {
    if (!count) {
        return;
    }

    noInterrupts();
    undispensed += count;
    interrupts();
    digitalWrite(ENABLE_PIN, HIGH);
}

void setup() {
  pinMode(ENABLE_PIN, OUTPUT);
  pinMode(INTERRUPT_PIN, INPUT_PULLUP);
  attachInterrupt(INTERRUPT_PIN, dispenseHandler, FALLING);
}

void loop() {
    
    // TODO implement functionality that triggers the dispense
    if (trigger) {
        dispenseTickets(amountToDispense);
    }
    
}

There cannot be any bounce on the interrupt signal from the dispenser.

You forgot to detect when the tickets run out. Or when the mechanism jams.

The FSM for dispensing a ticket would be something like:

const byte DispenserMotorPin = 2;
const int MOTOR_ON = HIGH;
const int MOTOR_OFF = LOW;

const byte NotchSensorPin = 3;
const int IS_NOT_NOTCH = HIGH;
const int IS_NOTCH = LOW;

enum DispenserStates {IDLE, FIND_START_OF_TICKET,
                      FIND_END_OF_TICKET, FIND_END_OF_NOTCH,
                      TICKET_JAM, OUT_OF_TICKETS
                     } DispenserState = IDLE;

const unsigned long NOTCH_TIMEOUT = 3000;
const unsigned long TICKET_TIMEOUT = 5000;

unsigned RemainingToDispense = 0;
unsigned long DispenseTimer;

void dispense()
{
  switch (DispenserState)
  {
    case IDLE:
      if (RemainingToDispense > 0)
      {
        DispenserState = FIND_START_OF_TICKET;
        DispenseTimer = millis();
        digitalWrite(DispenserMotorPin, MOTOR_ON);
      }
      break;

    case FIND_START_OF_TICKET:
      // If we don't get out of the 'notch' we are probably
      // out of tickets or jammed at a notch.
      if (millis() - DispenseTimer >= NOTCH_TIMEOUT)
      {
        digitalWrite(DispenserMotorPin, MOTOR_OFF);
        DispenserState = OUT_OF_TICKETS;
      }
      else
        // If we start in a notch, drive until the end of a notch is found
        if (digitalRead(NotchSensorPin) == IS_NOT_NOTCH)
        {
          DispenserState = FIND_END_OF_TICKET;
          DispenseTimer = millis();
        }
      break;

    case FIND_END_OF_TICKET:
      // If we don't get to the end of the ticket we are
      // probably jammed.
      if (millis() - DispenseTimer >= TICKET_TIMEOUT)
      {
        digitalWrite(DispenserMotorPin, MOTOR_OFF);
        DispenserState = TICKET_JAM;
      }
      else
        // Dispense the ticket until the end notch is found
        if (digitalRead(NotchSensorPin) == IS_NOTCH)
        {
          // The ticket has been dispensed!
          RemainingToDispense--;

          DispenserState = FIND_END_OF_NOTCH;
          DispenseTimer = millis();
        }
      break;

    case FIND_END_OF_NOTCH:
      // If we don't get out of the 'notch' we are probably
      // out of tickets or jammed at a notch.
      if (millis() - DispenseTimer >= NOTCH_TIMEOUT)
      {
        digitalWrite(DispenserMotorPin, MOTOR_OFF);
        DispenserState = OUT_OF_TICKETS;
      }
      else if (digitalRead(NotchSensorPin) == IS_NOT_NOTCH)
      {
        digitalWrite(DispenserMotorPin, MOTOR_OFF);
        DispenserState = IDLE;
      }
      break;

    case TICKET_JAM:
      break;

    case OUT_OF_TICKETS:
      break;
  }
}

I totally did, but I figure that could be solved with a simple timeout. I see this as a two state problem. One state where the motor is running and one where it's not, it's a matter of defining the transitions.

thank you for the input and example, i will work on this tonight

Is your "dispense()" meant to replace what nicolajna wrote for "static void dispenseTickets(uint32_t count)". I'm a bit confused, am I combining the two codes?

No. My FSM is meant to be called from loop(). Dispense tickets by adding a number to the global variable 'RemainingToDispense'.

John, thanks that worked great. the only change I made was to the RemainingToDispense variable. You can earn more tickets as they are being dispensed. I used two vaiables ticketsEarned and ticketsIssued. I certainly learned a lot from your input and it's much appreciated. I will be using FSM more in the future.

Here is my final code for the Dispenser Controller. It includes my input button for incrementing the totalScore. Now to add it to my overall program and move onto sound control....

const byte DispenserMotorPin = 31;
const int scorePin = 12;    // the number of the pushbutton pin
const int MOTOR_ON = HIGH;
const int MOTOR_OFF = LOW;

const byte NotchSensorPin = 33;
const int IS_NOT_NOTCH = HIGH;
const int IS_NOTCH = LOW;

enum DispenserStates {IDLE, FIND_START_OF_TICKET,
                      FIND_END_OF_TICKET, FIND_END_OF_NOTCH,
                      TICKET_JAM, OUT_OF_TICKETS
                     } DispenserState = IDLE;

const unsigned long NOTCH_TIMEOUT = 300;
const unsigned long TICKET_TIMEOUT = 3000;

unsigned long DispenseTimer;


//variables for tickets
int winningScore = 150; // plays winning song and lights winner sign, first ticket
int ticketSpan = 30;  //span between tickets
int noTickets = 1; // no tickets per ticket interval
int totalScore = 130 ; //temporary start at 130, typically 0
int ticketsEarned = 0;
int ticketsIssued = 0;

// Variables for score input
int scorePinNewState;             // the current scoreSwState from the input pin
int scoreSwLastState = LOW;   // the previous scoreSwState from the input pin
unsigned long scorePinLastTime = 0;  // the last time the output pin was toggled
unsigned long scorePinDelay = 5;    // the debounce time; increase if the output flickers

void setup() {
  Serial.begin(115200);
  pinMode(scorePin, INPUT);
  pinMode(DispenserMotorPin, OUTPUT);
  pinMode(NotchSensorPin, INPUT);
  digitalWrite(DispenserMotorPin, LOW);
  Serial.println("Ready!");
}

void loop() {
  readInputs(); //read button to increment up score
  if (totalScore >= winningScore) {
    ticketsEarned = (((totalScore - winningScore) / ticketSpan) + 1) * noTickets ;
    dispense();
  }

}

void dispense()
{
  switch (DispenserState)
  {
    case IDLE:
      //Serial.println("IDLE State: ");
      //if (RemainingToDispense > 0)
      if (ticketsIssued < ticketsEarned) 
        {
          DispenserState = FIND_START_OF_TICKET;
          DispenseTimer = millis();
          digitalWrite(DispenserMotorPin, MOTOR_ON);

        }
        break;

      case FIND_START_OF_TICKET:
        // If we don't get out of the 'notch' we are probably
        // out of tickets or jammed at a notch.
        //Serial.println("FIND_START_OF_TICKET State: ");
        if (millis() - DispenseTimer >= NOTCH_TIMEOUT)
        {
          digitalWrite(DispenserMotorPin, MOTOR_OFF);
          DispenserState = OUT_OF_TICKETS;
        }
        else
          // If we start in a notch, drive until the end of a notch is found
          if (digitalRead(NotchSensorPin) == IS_NOT_NOTCH)
          {
            DispenserState = FIND_END_OF_TICKET;
            DispenseTimer = millis();
          }
        break;

      case FIND_END_OF_TICKET:
        // If we don't get to the end of the ticket we are
        // probably jammed.
        //Serial.println("FIND_END_OF_TICKET State: ");
        if (millis() - DispenseTimer >= TICKET_TIMEOUT)
        {
          digitalWrite(DispenserMotorPin, MOTOR_OFF);
          DispenserState = TICKET_JAM;
        }
        else
          // Dispense the ticket until the end notch is found
          if (digitalRead(NotchSensorPin) == IS_NOTCH)
          {
            // The ticket has been dispensed!
            //RemainingToDispense--;
            ticketsIssued += 1;
            DispenserState = FIND_END_OF_NOTCH;
            DispenseTimer = millis();
          }
        break;

      case FIND_END_OF_NOTCH:
        // If we don't get out of the 'notch' we are probably
        // out of tickets or jammed at a notch.
        if (millis() - DispenseTimer >= NOTCH_TIMEOUT)
        {
          digitalWrite(DispenserMotorPin, MOTOR_OFF);
          DispenserState = OUT_OF_TICKETS;
        }
        else if (digitalRead(NotchSensorPin) == IS_NOT_NOTCH)
        {
          digitalWrite(DispenserMotorPin, MOTOR_OFF);
          DispenserState = IDLE;
        }
        break;

      case TICKET_JAM:
        Serial.println("TICKET JAM ");
        break;

      case OUT_OF_TICKETS:
        Serial.println("OUT OF TICKETS ");
        break;
      }
  }

  void readInputs() {
    // THIS IS USED TO INCREMENT THE TOTAL SCORE
    // read the state of the switch into a local variable:
    int scoreSwState = digitalRead(scorePin);
    // If the switch changed, due to noise or pressing:
    if (scoreSwState != scoreSwLastState) {
      // reset the debouncing timer
      scorePinLastTime = millis();
    }
    if ((millis() - scorePinLastTime) > scorePinDelay) {

      // if the button state has changed:
      if (scoreSwState != scorePinNewState) {
        scorePinNewState = scoreSwState;

        if (scorePinNewState == LOW) {
          totalScore += 10;
          Serial.print("Score: ");
          Serial.println(totalScore);
          //Serial.println("");
        }
      }
    }
    // save the scoreSwState. Next time through the loop, it'll be the scoreSwLastState:
    scoreSwLastState = scoreSwState;
  }