Skip to content

Instantly share code, notes, and snippets.

@rwaldron
Last active July 6, 2018 00:22
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rwaldron/82d48a427467e239d804c34e9ebb6194 to your computer and use it in GitHub Desktop.
Save rwaldron/82d48a427467e239d804c34e9ebb6194 to your computer and use it in GitHub Desktop.

In the many years that Johnny-Five has been an active project, there have been myriad requests for hardware peripheral and component device support. Implementing support almost always requires the acquisition of the actual peripheral or component device, in order to adequately develop and test the driver code; most commonly, this involves visiting SparkFun, Adafruit, Amazon, or Digi-Key, and then ordering the necessary "thing". Once in hand, the author sets their own schedule for designing, developing and testing an implementation. Johnny-Five contributors and maintainers will communicate all of these "phases", as they occur, by commenting on the original support request. Eventually, either support will land or it won't (in the past, there was no support for UART devices, this is no longer the case). There are rare cases, including one which we will discuss today, where it's not reasonable to expect a team member to acquire the hardware necessary for implementing support, usually because the peripheral or component is prohibitively expensive. This article will detail the path that I took in determining whether a prohibitively expensive component could be supported by Johnny-Five.

Recently, an issue was filed requesting support for the Pyramid Apex 5400 Bill Acceptor. This is the actual device that accepts bills for various vending purposes, such as amusements, games, lottery tickets, snacks and beverages, and kiosk applications.

Pyramid Apex 5400 Bill Acceptor

As shown on billacceptors.us, the Pyramid Apex 5400 line ranges in price from $155 to $190. Usually when I have no immediate need for such a device, and the cost exceeds that which I'm willing to expense for an open source project, I will generally take the following steps:

  1. Review the datasheet and see if I can learn how the thing works, without actually having the thing in hand.
    1. If this seems possible,
      1. Draft an implementation based on the datasheet
      2. Hand off the change set to the original requester for testing with actual hardware.
        1. Repeat steps 1.i.a and 1.i.b until confident that the implementation correctly supports the hardware.
    2. Else, encourage the reporter to author support and become a contributor to the project.

The datasheet for Pyramid's Apex series 5000 and 7000 Bill Acceptor devices is available here. After an initial read-through, I determined that it might be possible to simulate the acceptor's "Pulse Mode" operation by programming an Arduino (or similar) to behave as described in the datasheet. Also, I wanted to find a clearer version of the Configuration Card. A quick Google image search for "apex 5400 configuration card" only found similarly noisy and unclear copies of the same image found in the datasheet:

Coincidentally, this image was included in a short article from 2012, "Controlling Vending Machine Bill Acceptor with Arduino", which was about using an Arduino to interface with the same device I was researching. The approach used an interrupt service routine that would trigger on signal change, read a digital pin and increment a counter if the value is HIGH. If the value is 4, then assume a dollar has been inserted. While very limited, this example program confirmed my hunch that this system could be simulated. From there I continued searching for more examples of programs that receive and interpret pulses from the Apex 5400 Bill Acceptor—which lead me to a project called Greenbacks. Greenbacks contained a very clear example of how to interface with the device. Essentially, the program operates in the same way as the first: attach an interrupt service routine, which will trigger on signal change, read a digital pin and increment a counter if the value is HIGH. To differentiate denominations, the counter will increment for as many pulses are received while the time is captured and checked until 100m passes without a pulse—this indicates the acceptance of a bill. Once I understood how it worked, I returned to the datasheet to review configuration options:

Pulses Per Dollar Setting (only valid if DIP 6 is ON) For Quarter-based equipment (in which one pulse is worth $0.25), toggle DIP Position 7 to ON to set 4 Pulses per Dollar. Toggle DIP 7 to OFF for one pulse per dollar.

Pulse Speed Setting (only valid if DIP 6 is ON) Toggle DIP Position 8 to ON for Fast Pulse (50ms on, 50ms off), or OFF for Slow Pulse (50ms on, 300ms off).

Whenever the datasheet mentions a "DIP [Number)", that refers to the configuration DIP switch, which is shown on page 3:

Pyramid Apex Configuration DIP

However, this is only available on the Apex series 7000 models. The Apex series 5000 models are configured via the Configuration Card shown above. To turn on Pulse Mode (specific details about Pulse Mode start on page 9 of the datasheet), fill in the top right oval (labeled "Pulse/Serial") in section 1:

The Pulses Per Dollar Setting is also configured via the Configuration Card, in section 2:

The Pulse Speed is set in section 3:

Lastly, the denominations that are accepted will be configured in section 4:

But we're won't be using an actual device, so this all needs to be translated into a program that will act as a simulator. For the purpose of demonstration, our simulator will have the following configuration:

  1. Pulse Mode Operation
  2. Pulse Per Dollar: 1ppd
  3. Pulse Speed: Fast (50ms on, 50ms off)

Next, the simulator needs some way of providing a "denomination", that is: simulate the act of inserting bills. The SparkFun VKey Voltage Keypad provides 12 buttons, connected via resistor ladder, with a single analog output. Since we need 6 buttons (one each for $1, $5, $10, $20, $50, $100) this will be more than sufficient. For visual reference of the acceptance process, two LEDs will be used to indicate "Ready to accept a bill" and "Processing and accepting bill" phases. The simulator is a "black box", which can be replaced with the actual Bill Acceptor device.

Simulator circuit:

Bill acceptor simulator with manual input via SparkFun VKey Keypad

Simulator program:

#define DEBUG 0

int denominations[] = {1, 5, 10, 20, 50, 100};
int indicators[] = {
  // Phase 0: Ready for input (Green)
  6, 
  // Phase 1: Processing for acceptance, any input will be ignored (Red)
  7
};

int ppd = 1; // Any number between 1 and 127
int input = A0;
int output = 13;

unsigned long previousMs = 0; 
const long timeout = 1000;

void indicator(int phase, int state) {
  for (int i = 0; i < 2; i++) {
    digitalWrite(indicators[i], LOW);
  }
  digitalWrite(indicators[phase], state);
}

void setup() {
  #if DEBUG
  Serial.begin(9600);
  #endif
  
  pinMode(output, OUTPUT);
  pinMode(indicators[0], OUTPUT);
  pinMode(indicators[1], OUTPUT);
}

void loop() {
  indicator(0, HIGH);
  
  unsigned long currentMs = millis();
    
  int ainput = analogRead(input);
  int denomination = 0;

  if (ainput > 270 && ainput < 500) {
    indicator(1, HIGH);

    #if DEBUG
    // This was used to determine the ADC values used below.
    Serial.print("ainput: "); Serial.print(ainput); Serial.print(" ");
    Serial.println(denomination);
    #endif

    if (ainput > 480 && ainput < 495) {
      denomination = 1;
    }
  
    if (ainput > 440 && ainput < 455) {
      denomination = 5;
    }
    
    if (ainput > 400 && ainput < 415) {
      denomination = 10;
    }
    
    if (ainput > 360 && ainput < 370) {
      denomination = 20;
    }
  
    if (ainput > 320 && ainput < 330) {
      denomination = 50;
    }
  
    if (ainput > 280 && ainput < 290) {
      denomination = 100;
    }
  
    if (denomination) {
      if (currentMs - previousMs >= timeout) {
        previousMs = currentMs;

        int pulses = denomination * ppd;
      
        // P. 4, Fast Pulse 50ms on, 50ms off
        for (int i = 0; i < pulses; i++) {
          digitalWrite(output, HIGH);
          indicator(1, HIGH);
          delay(50);
          digitalWrite(output, LOW);
          indicator(1, LOW);
          delay(50);
        }
        indicator(1, HIGH);
        // https://pyramidacceptors.com/pdf/Apex_Manual.pdf
        // 100msec when bill is recognized.
        delay(100);
      }
    }
  }
}

This will produce the following square waves:

Square Waves for $1, $5, $10, $20, $50, $100

The next step was to write a Johnny-Five program that would read input from a pin on another board, which is wired directly to the output pin of the simulator:

And finally, the program itself:

const { Board, Digital } = require("johnny-five");
const board = new Board();

board.on("ready", () => {
  const acceptor = new Digital(8);
  const denominations = [1, 5, 10, 20, 50, 100];
  let denomination = 0;
  let last = 0;
  let timeout = null;

  acceptor.on("data", () => {
    let { value: pulse } = acceptor;

    if (last !== pulse && pulse === 1) {
      if (timeout) {
        clearTimeout(timeout);
      }
      denomination++;
    }

    // If the last value was a countable pulse,
    // we MIGHT be at the end, so set a timeout
    if (last === 1 && pulse === 0) {
      timeout = setTimeout(() => {
        if (denominations.includes(denomination)) {
          console.log(`$${denomination}.00`);
        }
        denomination = 0;
      }, 100);
    }

    last = pulse;
  });
});

The first line of the program pulls in the Johnny-Five dependency and uses a destructuring pattern to unpack the Board and Digital classes that will be used in the program.

const { Board, Digital } = require("johnny-five");

Then a new instance of Board is instantiated, this represents the physical board that our program will be interfacing with—not the simulator.

const board = new Board();

The rest of the program is wrapped inside a handler that's invoked when the communication with the board is initialized and "ready".

board.on("ready", () => {
  /* ... */
});

Within the "ready" handler, the program first instantiates a new Digital sensor object for pin 8, assigning it to a const variable called acceptor.

const acceptor = new Digital(8);

Then a set of program variables are declared; denominations is a list of valid dollar amounts, denomination will hold a value for the most recent bill accepted, last is the last value read by the digital sensor, and timeout will hold a reference to a pending timeout.

const denominations = [1, 5, 10, 20, 50, 100];
let denomination = 0;
let last = 0;
let timeout = null;

The acceptor object will listen for data events, invoking a handler whenever new data is received. The first line of the handler will unpack the value property from acceptor and assign it to a let variable called pulse (this is only done for the sake of readability throughout the remaining handler code).

acceptor.on("data", () => {
  let { value: pulse } = acceptor;

  /* ... */
});

The next major portion of the handler code checks if the value of last does not equal the value of pulse, and if the value of pulse equals 1 (or is "high"). If those conditions are met, clear the timeout if it's active, and increment the value of denomination.

if (last !== pulse && pulse === 1) {
  if (timeout) {
    clearTimeout(timeout);
  }
  denomination++;
}

The next step within the handler checks if the value of last is 1 and the value of pulse is 0. If those conditions are met, it's possible that the simulator is entering the "acceptance" phase (100ms LOW pulse), so define and assign a new timeout for 100ms. If the timeout is reached, validate the accumulated value in denomination against our list of denominations. If the current denomination is valid, print it to the console. Reset the value of denomination to 0.

if (last === 1 && pulse === 0) {
  timeout = setTimeout(() => {
    if (denominations.includes(denomination)) {
      console.log(`$${denomination}.00`);
    }
    denomination = 0;
  }, 100);
}

TODO:

The article is stalled until the original poster responds with confirmation that suggested program worked as expected, which confirms that the simulator is an accurate implementation.

...findings?

...conclusion

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment