That's unfortunately not sufficient information...
Is your Coin Acceptor already connected to your coffee machine or do you have to develop this interface?
That's unfortunately not sufficient information...
Is your Coin Acceptor already connected to your coffee machine or do you have to develop this interface?
Yes dear, already connected.
There's 3 wires connector between the coin acceptor and coffee machine, 12v dc, Gnd, pulse
Where the first and second wires to power up the coin acceptor and the third wire to send the pulse from the coin acceptor to the coffee machine.
In simple words, what I did is putting an Arduino esp32 between the coin acceptor and coffee machine and using any working code that can detect the pulses from the coin acceptor then do some tricks after that send the pulse/s to the coffee machine.
I think it's one or two line of code, and I need your support guys.
Thank you
You still havenât provided sufficient information to answer your question. Please tell us:
Which ESP32 pin is connected to the coffee machine for generating the pulses?
What signal level for the pulses does the coffee machine require (5V, 3.3V, 12V, etc)?
Does the line to the coffee machine need to idle HIGH and pulse LOW, or vice-versa?
What are the minimum and maximum durations the coffee machine requires for each pulse?
What are the minimum and maximum idle times the coffee machine requires between successive pulses for the SAME COIN?
What is the minimum time the coffee machine requires between two consecutive pulses so that the second pulse will be recognized as the start of a NEW COIN?
You wonât receive a complete answer until you provide complete information.
Just to understand:
You say that exactly the pulse sequence as detected by the ESP32 is usually controlling the coffee machine?
Yes, 100%.
Ok. I have modified the Wokwi sketch as follows:
You can play around with
As default on Wokwi LOWTIME is set to 50 and HIGHTIME to 100 which makes the counting of 10 pulses quite long, but you can change it.
The function that provides the pulse sequence is doCoinOut() in MCAEmulation.h.
On Wokwi: https://wokwi.com/projects/378917741462749185
/*
Forum: https://forum.arduino.cc/t/pass-the-pulse-from-coin-acceptor-to-coffee-vending-machine-using-arduino-esp32/1178550
Wokwi: https://wokwi.com/projects/378917741462749185
*/
#include "MCAEmulation.h"
#include <LiquidCrystal_I2C.h>
#define I2C_ADDR 0x27
#define LCD_COLUMNS 20
#define LCD_LINES 4
LiquidCrystal_I2C lcd(I2C_ADDR, LCD_COLUMNS, LCD_LINES);
constexpr byte resetAmountPin = 5;
constexpr byte coinPin = 2;
constexpr byte CoinTypes = 4;
constexpr int coinPulses[CoinTypes] = {1, 2, 5, 10};
constexpr float coinValue[CoinTypes] = {1.0, 2.0, 5.0, 10.0};
volatile int impulseCountISR = 0;
volatile unsigned long lastPulseISR;
constexpr unsigned long WaitAfterLastPulseDetected = HIGHTIME+LOWTIME+10;
int impulseCount = 0;
unsigned long lastPulse;
int coinType = 0;
float totalAmount = 0.0;
void IRAM_ATTR coinInterrupt() {
impulseCountISR++;
lastPulseISR = millis();
}
void setup() {
Serial.begin(115200);
pinMode(resetAmountPin, INPUT_PULLUP);
pinMode(coinPin, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(coinPin), coinInterrupt, FALLING);
lcd.init();
lcd.backlight();
Serial.println("Start");
printAmount();
}
void loop() {
// Only required for testing START
MCAEmulation();
// Only required for testing END
if (coinInserted()) {
totalAmount += lookup(impulseCount);
printAmount();
}
checkResetAmount();
}
void printAmount() {
Serial.print("Total Amount: ");
Serial.println(totalAmount, 2);
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Total Balance: ");
lcd.setCursor(0, 1);
lcd.print(totalAmount, 2);
}
int lookup(int pulses) {
for (int i = 0; i < CoinTypes; i++) {
if (pulses == coinPulses[i]) {
return coinValue[i];
}
}
return 0;
}
void getISRData() {
noInterrupts();
impulseCount = impulseCountISR;
lastPulse = lastPulseISR;
interrupts();
}
void clearISRData() {
noInterrupts();
impulseCountISR = 0;
interrupts();
}
boolean coinInserted() {
boolean result = false;
getISRData();
if (impulseCount > 0 && millis() - lastPulse > WaitAfterLastPulseDetected) {
clearISRData();
result = true;
}
return result;
}
void checkResetAmount() {
static byte state = HIGH;
static byte lastState = HIGH;
static unsigned long lastChange;
byte actState = digitalRead(resetAmountPin);
if (actState != lastState) {
lastChange = millis();
lastState = actState;
}
if (actState != state && millis() - lastChange > 30) {
state = actState;
if (!state) {
totalAmount = 0.0;
printAmount();
}
}
}
#pragma once
struct coinStruct {
byte Pin;
byte State;
byte lastState;
unsigned long lastChange;
int Pulses;
};
constexpr byte noOfMyCoins = 4;
coinStruct myCoins[noOfMyCoins] = {
{23, HIGH, HIGH, 0, 1},
{19, HIGH, HIGH, 0, 2},
{18, HIGH, HIGH, 0, 5},
{15, HIGH, HIGH, 0, 10}
};
boolean pmSet = false;
constexpr byte coinOutPin = 4;
constexpr unsigned long HIGHTIME = 100;
constexpr unsigned long LOWTIME = 50;
unsigned long lastPulseTime = 0;
int coinOutCount = 0;
boolean lineHIGH = true;
void doCoinOut() {
if (coinOutCount > 0) {
if (lineHIGH && millis()-lastPulseTime > HIGHTIME) {
lastPulseTime = millis();
digitalWrite(coinOutPin, LOW);
lineHIGH = false;
}
if (!lineHIGH && millis()-lastPulseTime > LOWTIME) {
lastPulseTime = millis();
digitalWrite(coinOutPin, HIGH);
lineHIGH = true;
coinOutCount--;
}
}
}
boolean buttonDown(coinStruct & aCoin) {
byte bState = digitalRead(aCoin.Pin);
if (bState != aCoin.lastState) {
aCoin.lastState = bState;
aCoin.lastChange = millis();
}
if (bState != aCoin.State && millis() - aCoin.lastChange > 30) {
aCoin.State = bState;
if (bState == HIGH) return true;
}
return false;
}
void MCASetup() {
if (!pmSet) {
for (int i = 0; i < noOfMyCoins; i++) {
pinMode(myCoins[i].Pin, INPUT_PULLUP);
}
digitalWrite(coinOutPin, HIGH);
pinMode(coinOutPin, OUTPUT);
pmSet = true;
}
}
void MCAMachine() {
for (int i = 0; i < noOfMyCoins; i++) {
if (buttonDown(myCoins[i]) && coinOutCount == 0) {
coinOutCount = myCoins[i].Pulses;
/*
Serial.print("Coin Type ");
Serial.print(i+1);
Serial.print("\tPulses ");
Serial.println(coinOutCount);
*/
}
}
doCoinOut();
}
void MCAEmulation() {
MCASetup();
MCAMachine();
}
The doCoinOut() function has to be continouesly called in loop(). If the variable "coinOutCount" is greater than zero the function will produce the appropriate number of pulses.
In this application the pulse sequence is written to pin D4 and received by D2.
The assumption is that the line to the coffee machine is active LOW.
Make sure that you do not damage your board when connecting it to the coffee machine ...
Good luck!
Unfortunately, nothing worked.
If you can help me out by giving a 1 line of code to pass 1 pulse that's will be appreciated.
From my side I will handle the rest of the code, so just one line of code to transfer 1 pulse from coin acceptor to be an output to the coffee machine, even without receiving a pulses from the coin acceptor. just simple code.
That's what you got with the latest sketch, it's the function doCoinOut()
If coinOutCount is set to 1, it will create 1 pulse of the length LOWTIME. If coinOutCount is set to 2 it creates two pulses of duration LOWTIME and a pause of HIGHTIME between both.
This will send a 25ms LOW pulse out of GPIO 2:
digitalWrite(2, LOW);
delay(25);
digitalWrite(2, HIGH);
I tried this but it did not work.
then I found this code:
void loop(){
int pin2state = digitalRead(2);
digitalWrite(4,pin2state);
Serial.println(pin2state);
}
Which is working well by getting the input from the coin acceptor and redirect it to the coffee machine, the problem is that I cannot read the pin2state using Serial.print(pin2state) and declare it because it's giving 0.
any ideas?
Why?
Youâre getting the data to log⌠why fiddle with it ?
youâre trying to create a man-in-the-middle, while your current approach is a simple tap off the data line.
P.S. one wire is unlikely to be reliable. you also need a common ground at the very least.
I said in my post #10.
You can call it man in the middle.
But I need to set the pin2 state in a variable then if a Boolean value comes true then I just call digitalWrite pin 4 to be pin2state, something like this:
int pin2state;
bool QuestionAnswered;
void setup()
{
pinMode(2,INPUT);
pinMode(4,OUTPUT);
}
void loop()
{
pin2state = digitalRead(2);
while(!QuestionAnswered)
{
//Wait for the answer
}
else
{
digitalWrite(4,pin2state);
Serial.println(pin2state);
}
}
Correct me if I'm wrong, but in this current approach, the ESP32 is going to be responsible for collecting all of the pulses from multiple coins, waiting for an answer, then sending all of the pulses to the coffee machine. The above code would, at best, see one pulse, then wait for an answer, possibly missing more coin pulses while waiting, then upon successful answer, sends one pulse to the coffee machine. One answer per pulse?
There is still a lot of infomation missing which might bring a solution forward.
Here is a sketch that at least should give you the ability to measure the pulse durations (HIGH/LOW) in sequence of pulses. (The duration is only displayed if it is less than 500 msec. If you throw in several coins one after another some of the data will depend on how quickly you do that).
/*
Forum: https://forum.arduino.cc/t/pass-the-pulse-from-coin-acceptor-to-coffee-vending-machine-using-arduino-esp32/1178550
Wokwi: https://wokwi.com/projects/379021820365072385
*/
//#define WOKWI
#ifdef WOKWI
#include "MCAEmulation.h"
#endif
constexpr byte coinPin = 2;
volatile boolean ISRFired = false;
volatile unsigned long changeTimeISR;
volatile byte pinStateISR;
unsigned long changeTime;
byte pinState;
unsigned long previousChangeTime = 0;
unsigned long countPulses = 0;
void IRAM_ATTR coinInterrupt() {
changeTimeISR = millis();
pinStateISR = digitalRead(coinPin);
ISRFired = true;
}
void setup() {
Serial.begin(115200);
Serial.println("Start");
pinMode(coinPin, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(coinPin), coinInterrupt, CHANGE);
}
void loop() {
#ifdef WOKWI
// Only required for testing START
MCAEmulation();
// Only required for testing END
#endif
if (newISRData()) {
if (changeTime - previousChangeTime < 500) {
if (pinState == LOW) {
Serial.print("\tHIGH Time:\t");
} else {
Serial.print(++countPulses);
Serial.print("\tLOW Time:\t");
}
Serial.println(changeTime - previousChangeTime);
}
previousChangeTime = changeTime;
}
}
boolean newISRData() {
boolean result = false;
noInterrupts();
if (ISRFired) {
changeTime = changeTimeISR;
pinState = pinStateISR;
ISRFired = false;
result = true;
} else {
result = false;
}
interrupts();
return result;
}
8 LOW Time: 20
HIGH Time: 50
9 LOW Time: 20
HIGH Time: 50
10 LOW Time: 20
For testing purposes the sketch is also available on Wokwi with the "MCAEmulation" where you can change the simulated timing in the file MCAEmulation.h:
constexpr unsigned long HIGHTIME = 50;
constexpr unsigned long LOWTIME = 20;
I wouldnât necessarily say âimprovedâ, but hereâs how I would do it per my coding preferences. Were appropriate, I try to wrap related functionality into classes per the goals of OOP. Also, since @malekses is using an ESP32 board, I chose to take advantage of the multi-tasking capability of its built-in FreeRTOS operating system.
So, the below code implements your man-in-the-middle.
The Coin Acceptorâs pulse out pin would connect to the ESP32 pin defined by the âpulseInPinâ variable.
The âprocessCoinâ task handles each coin after it is accepted. This is where you would put your âmiddleman codeâ. I just have a comment in that location. After your âmiddlemanâ does its thing, the correct number of pulses for the coin accepted are sent out to the coffee maker via the âpulseOutPinâ pin.
Also, since I donât have an actual Coin Acceptor, I simulate one with the âsimulatedCoinPulseGeneratorâ task. Every 5 seconds, it picks a random coin from the âpairTableâ vector and sends an appropriate number of pulses out the âsimulatorOutputPinâ pin. So, to see this code run, you need to run a jumper wire from the âsimulatorOutputPinâ pin to the âpulseInPinâ pin.
The variables you may wish to modify for your implementation:
pulseInPin: Pin that receives pulses from your Coin Acceptor
pulseOutPin: Pin that sends pulses to the coffee machine
simulatorOutputPin: Pin that sends the simulated Coin Acceptor pulses â Only used in simulation when thereâs not an actual Coin Acceptor
CoffeeMachinePulseDuration: Duration of each pulse sent from ESP32 to the coffee machine
maxPulseWaitTime: The length of time the code waits after the last pulse is received from the Coin Acceptor until it declares that all pulses have been sent and it determines the coin inserted.
Also, per the code, I recommend representing all monetary amounts (coin values) as integer types rather than float. That avoids any roundoff problems. Use units of the smallest denomination for the currency in use (eg. âcentsâ).
Finally, for this example, I didnât spend any time optimizing the stack sizes specified for the FreeRTOS tasks. Some of them could probably be trimmed down.
Main .ino Code:
#include "CoinAcceptor.h"
// ------------------- For Simulation ONY -------------------------------------------------
// Delete this line in real application
void simulatedCoinPulseGenerator(void *pvParameters);
// ------------------- For Simulation ONY -------------------------------------------------
void processCoin(void *pvParameters);
void coinAccepted(CoinAcceptor::PulseValuePair);
const uint8_t pulseInPin = 13;
const uint8_t pulseOutPin = 2;
CoinAcceptor acceptor(pulseInPin, coinAccepted);
QueueHandle_t coinQueue = NULL;
void setup() {
Serial.begin(115200);
delay(3000);
Serial.println("Starting");
coinQueue = xQueueCreate(20, sizeof(CoinAcceptor::PulseValuePair));
assert(coinQueue != NULL);
BaseType_t returnCode = xTaskCreatePinnedToCore(processCoin, "Pulse Output Generator", 2500, NULL, 4, NULL, CONFIG_ARDUINO_RUNNING_CORE);
assert(returnCode != pdFAIL);
CoinAcceptor::PulseValuePairTable table {
{ 1, 1 },
{ 5, 5 },
{ 10, 10 },
{ 12, 25 },
{ 14, 50 },
{ 16, 100 }
};
acceptor.begin(table);
// ------------------- For Simulation ONY -------------------------------------------------
// Delete these lines in real application
returnCode = xTaskCreatePinnedToCore(simulatedCoinPulseGenerator, "Simulated Pulse Generator", 2500, reinterpret_cast<void*>(&table), 6, NULL, CONFIG_ARDUINO_RUNNING_CORE);
assert(returnCode != pdFAIL);
// ------------------- For Simulation ONY -------------------------------------------------
}
// Callback function. Called every time a new coin is accepted
void coinAccepted(CoinAcceptor::PulseValuePair pair) {
// Callback functions should be very short.
// Just put the coin information in a Queue to be processed outside the callback
xQueueSendToBack(coinQueue, &pair, 0);
}
// Process the coin accepted then send appropriate number of pulses to the coffee machine
void processCoin(void *pvParameters) {
const uint32_t CoffeeMachinePulseDuration = 20;
digitalWrite(pulseOutPin, HIGH);
pinMode(pulseOutPin, OUTPUT);
CoinAcceptor::PulseValuePair pair;
for (;;) {
xQueueReceive(coinQueue, &pair, portMAX_DELAY);
Serial.printf("Accepted Coin of Value: %d\n", pair.value);
//
// Do custom stuff here to process the coin accepted
//
// Send the proper number of pulses to the coffee machine
Serial.printf("Sending %d Pulse(s) to Coffee Machine\n", pair.pulses);
for (uint8_t i = 0; i < pair.pulses; i++) {
Serial.println("LOW");
digitalWrite(pulseOutPin, LOW);
vTaskDelay(CoffeeMachinePulseDuration);
Serial.println("HIGH");
digitalWrite(pulseOutPin, HIGH);
vTaskDelay(CoffeeMachinePulseDuration);
}
Serial.println();
}
}
void loop() {
}
// ------------------- For Simulation ONY -------------------------------------------------
// Delete this function in real application
// This task simulates dropping a random-value coin into the acceptor every 5 seconds
void simulatedCoinPulseGenerator(void *pvParameters) {
const uint8_t simulatorOutputPin = 12;
digitalWrite(simulatorOutputPin, HIGH);
pinMode(simulatorOutputPin, OUTPUT);
CoinAcceptor::PulseValuePairTable pairTable = *(reinterpret_cast<CoinAcceptor::PulseValuePairTable*>(pvParameters));
uint32_t tableSize = pairTable.size();
uint32_t index;
vTaskDelay(3000);
for (;;) {
index = random(0, tableSize);
Serial.printf("Simulating Dropping a Coin with Value: %d\n", pairTable[index].value);
for (uint32_t i = 0; i < pairTable[index].pulses; i++) {
digitalWrite(simulatorOutputPin, LOW);
vTaskDelay(1);
digitalWrite(simulatorOutputPin, HIGH);
vTaskDelay(1);
}
vTaskDelay(5000);
}
}
// ------------------- For Simulation ONY -------------------------------------------------
CoinAcceptor.h:
#ifndef COINACCEPTOR_H_
#define COINACCEPTOR_H_
#include "Arduino.h"
#include <vector>
#include <functional>
class CoinAcceptor {
public:
struct PulseValuePair {
uint32_t pulses;
uint32_t value; // Assume value in cents
};
using PulseValuePairTable = std::vector<CoinAcceptor::PulseValuePair>;
using CallBackFunction = std::function<void(CoinAcceptor::PulseValuePair)>;
CoinAcceptor(uint32_t pin, CallBackFunction cb) :
pulsePin(pin), callBack(cb) {
}
void begin(const PulseValuePairTable &p);
private:
using VoidFunction = std::function<void ()>;
const uint32_t maxPulseWaitTime = 100;
CallBackFunction callBack;
uint32_t pulsePin = 0;
PulseValuePairTable pairTable;
volatile bool newPulse = 0;
volatile uint32_t pulseCount = 0;
void pulseISR();
void pulseMonitor();
static void taskLauncher(void *pvParameters);
};
#endif /* COINACCEPTOR_H_ */
CoinAcceptor.cpp:
#include "CoinAcceptor.h"
#include "FunctionalInterrupt.h"
void CoinAcceptor::begin(const PulseValuePairTable &p) {
VoidFunction actualTask = nullptr;
pairTable = p;
pinMode(pulsePin, INPUT_PULLUP);
actualTask = [this]() {
this->pulseMonitor();
};
BaseType_t returnCode = xTaskCreatePinnedToCore(taskLauncher, "Pulse Monitor", 2500, reinterpret_cast<void *>(&actualTask), 6, NULL, CONFIG_ARDUINO_RUNNING_CORE);
assert(returnCode != pdFAIL);
auto isr = [this]() {
this->pulseISR();
};
attachInterrupt(digitalPinToInterrupt(pulsePin), isr, FALLING);
}
void CoinAcceptor::taskLauncher(void *pvParameters) {
VoidFunction actualTask = *(reinterpret_cast<VoidFunction *>(pvParameters));
actualTask();
}
void CoinAcceptor::pulseMonitor() {
bool coinInProgress = false;
uint32_t lastPulseTime;
PulseValuePair pair;
bool newCoinDetected;
uint32_t currentMillis;
for (;;) {
newCoinDetected = false;
currentMillis = millis();
noInterrupts();
if (newPulse) {
coinInProgress = true;
lastPulseTime = currentMillis;
newPulse = false;
}
if (coinInProgress && (currentMillis - lastPulseTime > maxPulseWaitTime)) {
for (auto &p : pairTable) {
if (p.pulses == pulseCount) {
newCoinDetected = true;
pair = p;
break;
}
}
pulseCount = 0;
coinInProgress = false;
}
interrupts();
if (newCoinDetected) {
callBack(pair);
}
vTaskDelay(5);
}
}
void CoinAcceptor::pulseISR() {
newPulse = true;
pulseCount++;
}
How does the coffee machine know which number of pulses is enough to serve a cup of coffee?
Are you sure that the pulse sequence goes directly to the coffee machine?
Or is further electronics involved that evaluates the pulse sequence and controls the coffee machine?
A very nice piece of code! I have only played a little around up to now with FreeRTOS but it looks as if I'll have to get deeper into it
Hope you don't mind that I copied it to a Wowki project where it nicely runs:
Just added a diode with a separate (inverted) output pin so that it indicates when a pulse sequence is fired. The link on Wokwi refers to your post#36 in the forum.
I consider it a really nice example how to use tasks for this issue!
Unfortunately for the real project important details are missing. I'm not sure that the machine is directly controlled by pulse sequences (how does it know the necessary amount of money inserted?). My guess is that there is a separate controller board involved (like the one that is sometimes sold as "USB Timer Control Board" together with the CH 926 ).
as offered e.g. here:
https://sintron-hk.com/products/sintron-multi-coin-acceptor-selector-ch-926-and-usb-timer-control-board-for-vending-machine-accept-6-kinds-of-coins
Then it might ease the control of the coffee machine to just one pulse (simulated button press) ...
In any case, thank you for the nice FreeRTOS example!
You are brilliant, thank you so much.
I just want to tell you that when vTaskDelay(1) this will send 1 pulse even if the denomination is different then I solve it as below:
if(pairTable[index].pulses==1){
digitalWrite(simulatorOutputPin, LOW);
vTaskDelay(20);
digitalWrite(simulatorOutputPin, HIGH);
vTaskDelay(20);
}
else if(pairTable[index].pulses==2){
digitalWrite(simulatorOutputPin, LOW);
vTaskDelay(50);
digitalWrite(simulatorOutputPin, HIGH);
vTaskDelay(50);
}
else if(pairTable[index].pulses==5){
digitalWrite(simulatorOutputPin, LOW);
vTaskDelay(75);
digitalWrite(simulatorOutputPin, HIGH);
vTaskDelay(75);
}
else if(pairTable[index].pulses==10){
digitalWrite(simulatorOutputPin, LOW);
vTaskDelay(100);
digitalWrite(simulatorOutputPin, HIGH);
vTaskDelay(100);
}
Or we can add the duration time to the PulseValuePairTable.
Thank you guys for the helpful support.
That doesn't make any sense. Why do you want to send only one pulse regardless of the coin's denomination? Why do you want the width of that pulse to change?