class Timer
{
private:
uint32_t timestamp;
uint32_t timespan;
public:
Timer()
{
}
void Start(uint32_t timespan)
{
this->timespan = timespan;
timestamp = millis();
}
bool Expired() const
{
return (millis() - timestamp) >= timespan;
}
};
// Blinker to provide a heartbeat signal.
class Blinker
{
private:
Timer timer;
const uint32_t timespan = 500;
byte pin;
public:
Blinker(byte pin)
{
this->pin = pin;
}
void Begin()
{
pinMode(pin, OUTPUT);
timer.Start(timespan);
}
void Update()
{
if (timer.Expired())
{
digitalWrite(pin, !digitalRead(pin));
timer.Start(timespan);
}
}
};
// Prompter to control blinking and beeping.
class Prompter
{
private:
Timer timer;
uint32_t toggleTimespan;
uint32_t pauseTimespan;
uint32_t numToggles;
uint32_t currentToggles;
bool indefinitely;
bool running;
byte state;
public:
Prompter(void (*function)(byte))
{
Callback = function;
}
void (*Callback)(byte);
void Start(uint32_t toggleTimespan, uint16_t numBlinks, uint32_t pauseTimespan)
{
this->toggleTimespan = toggleTimespan;
numToggles = 2 * numBlinks;
currentToggles = 0;
this->pauseTimespan = pauseTimespan;
indefinitely = false;
state = LOW;
timer.Start(toggleTimespan);
running = true;
}
void StartIndefinitely(uint32_t toggleTimespan)
{
this->toggleTimespan = toggleTimespan;
indefinitely = true;
state = LOW;
timer.Start(toggleTimespan);
running = true;
}
void Stop()
{
running = false;
currentToggles = 0;
}
void Update()
{
if (running && timer.Expired())
{
currentToggles++;
if (!indefinitely && currentToggles > numToggles)
{
state = LOW;
timer.Start(pauseTimespan);
currentToggles = 0;
}
else
{
state = state == HIGH ? LOW : HIGH;
timer.Start(toggleTimespan);
}
if (Callback != nullptr)
{
Callback(state);
}
}
}
byte State() const
{
return state;
}
bool IsRunning() const
{
return running;
}
};
class Debouncer
{
private:
Timer timer;
uint32_t debounceDelay;
byte pin;
byte state;
bool fall;
public:
Debouncer(byte pin, uint32_t debounceDelay)
{
this->pin = pin;
this->debounceDelay = debounceDelay;
}
void Begin()
{
pinMode(pin, INPUT_PULLUP);
state = digitalRead(pin);
}
byte State() const
{
return state;
}
bool Fall() const
{
return fall;
}
void Update()
{
const bool newState = digitalRead(pin);
// Hysteresis:
// If there is no change, reset the debounce timer.
// Else, compare the time difference with the debounce delay.
if (newState == state)
{
timer.Start(debounceDelay);
}
else if (timer.Expired())
{
// Successfully debounced, so reset the debounce timer and update the state.
fall = !newState && state;
state = newState;
timer.Start(debounceDelay);
return;
}
fall = false;
}
};
class Led
{
private:
byte pin;
public:
Led(byte pin)
{
this->pin = pin;
}
void Begin()
{
pinMode(pin, OUTPUT);
}
void On()
{
digitalWrite(pin, HIGH);
}
void Off()
{
digitalWrite(pin, LOW);
}
void Set(byte state)
{
digitalWrite(pin, state);
}
};
class Player
{
public:
Debouncer button;
Led led;
Player(byte buttonPin, uint32_t buttonDebounceDelay, byte ledPin) : button(buttonPin, buttonDebounceDelay), led(ledPin)
{
}
void Begin()
{
button.Begin();
led.Begin();
}
};
class Speaker
{
private:
byte pin;
public:
Speaker(byte pin)
{
this->pin = pin;
}
void Begin()
{
pinMode(pin, OUTPUT);
}
void On(uint16_t frequency)
{
tone(pin, frequency);
}
void Off()
{
noTone(pin);
}
};
// Blinker.
Blinker blinker(LED_BUILTIN);
const byte NUM_PLAYERS = 6;
// The players.
Player players[NUM_PLAYERS] =
{
// Button pin, debounce delay, LED pin.
Player(A0, 50, 2),
Player(A1, 50, 3),
Player(A2, 50, 4),
Player(A3, 50, 5),
Player(A4, 50, 6),
Player(A5, 50, 7)
};
// Speaker to prompt next action.
const byte SPEAKER_PIN = 8;
Speaker speaker(SPEAKER_PIN);
// State machine states.
enum class States : byte
{
S00_WAIT_FOR_FIRST_DEALER,
S01_FOUND_FIRST_DEALER,
S02_PROMPT_DEALER,
S03_WAIT_FOR_DEALER,
S04_PROMPT_NEXT_PLAYER,
S05_WAIT_FOR_PLAYER,
S06_ACKNOWLEDGE_PLAYER,
S07_MOVE_TO_NEXT_PLAYER,
};
States presentState;
byte currentDealerIndex;
byte currentPlayerIndex;
byte currentRoundIndex;
byte cardsPlayed;
const byte NUM_ROUNDS = 4;
const byte BLINKS_PER_ROUND[NUM_ROUNDS] = {3, 4, 5, 6};
// These values are cumulative, i.e. not cards per round.
// const byte CARDS_PLAYED_AFTER_ROUND[NUM_ROUNDS] = { 18, 42, 72, 108 }; // For game play.
const byte CARDS_PLAYED_AFTER_ROUND[NUM_ROUNDS] = {6, 12, 18, 24}; // For testing.
const uint16_t DEALER_FREQUENCY = 1000;
const uint16_t PLAYER_FREQUENCY = 1500;
const uint16_t CARD_FREQUENCY = 500;
//const uint32_t PLAYER_TIMER = 15 * 1000; // For game play.
const uint32_t PLAYER_TIMER = 5 * 1000; // For testing.
// Limits the number of beeps used to prompt the Dealer.
byte performSpeakerPrompt;
void PrompterCallback(byte prompterState)
{
switch (presentState)
{
case States::S00_WAIT_FOR_FIRST_DEALER:
for (auto &player : players)
{
player.led.Set(prompterState);
}
break;
case States::S03_WAIT_FOR_DEALER:
if (prompterState == HIGH)
{
if (performSpeakerPrompt > 0)
{
speaker.On(CARD_FREQUENCY);
performSpeakerPrompt--;
}
players[currentDealerIndex].led.On();
}
else
{
speaker.Off();
players[currentDealerIndex].led.Off();
}
break;
case States::S05_WAIT_FOR_PLAYER:
players[currentPlayerIndex].led.Set(prompterState);
break;
}
}
Prompter prompter(PrompterCallback);
void setup()
{
Serial.begin(115200);
// Initialise.
blinker.Begin();
presentState = States::S00_WAIT_FOR_FIRST_DEALER;
for (auto &player : players)
{
player.Begin();
}
speaker.Begin();
// Blink all player LEDs until one player presses their button.
prompter.StartIndefinitely(250);
Serial.println("Waiting for the first dealer.");
}
Timer promptTimer;
void loop()
{
// Update button states and blinking states.
blinker.Update();
for (auto &player : players)
{
player.button.Update();
}
prompter.Update();
//
// Next state logic.
//
// The next state defaults to the present state;
States nextState = presentState;
switch (presentState)
{
case States::S00_WAIT_FOR_FIRST_DEALER:
for (byte i = 0; i < NUM_PLAYERS; i++)
{
if (players[i].button.Fall())
{
currentDealerIndex = i;
currentPlayerIndex = (currentDealerIndex + 1) % NUM_PLAYERS;
nextState = States::S01_FOUND_FIRST_DEALER;
break;
}
}
break;
case States::S01_FOUND_FIRST_DEALER:
nextState = States::S02_PROMPT_DEALER;
break;
case States::S02_PROMPT_DEALER:
if (promptTimer.Expired())
{
nextState = States::S03_WAIT_FOR_DEALER;
}
break;
case States::S03_WAIT_FOR_DEALER:
if (players[currentDealerIndex].button.Fall())
{
currentPlayerIndex = (currentDealerIndex + 1) % NUM_PLAYERS;
nextState = States::S04_PROMPT_NEXT_PLAYER;
}
break;
case States::S04_PROMPT_NEXT_PLAYER:
if (promptTimer.Expired())
{
nextState = States::S05_WAIT_FOR_PLAYER;
}
break;
case States::S05_WAIT_FOR_PLAYER:
if (!prompter.IsRunning() && promptTimer.Expired())
{
Serial.println(" Player timer expired.");
prompter.StartIndefinitely(250);
}
if (players[currentPlayerIndex].button.Fall())
{
cardsPlayed++;
nextState = States::S06_ACKNOWLEDGE_PLAYER;
}
break;
case States::S06_ACKNOWLEDGE_PLAYER:
nextState = States::S07_MOVE_TO_NEXT_PLAYER;
break;
case States::S07_MOVE_TO_NEXT_PLAYER:
nextState = States::S05_WAIT_FOR_PLAYER;
currentPlayerIndex = ++currentPlayerIndex % NUM_PLAYERS; // Move to next player.
if (cardsPlayed >= CARDS_PLAYED_AFTER_ROUND[currentRoundIndex])
{
Serial.print(" End of round ");
Serial.println(currentRoundIndex + 1);
currentRoundIndex++;
if (currentRoundIndex >= NUM_ROUNDS)
{
currentDealerIndex = ++currentDealerIndex % NUM_PLAYERS;
currentRoundIndex = 0;
cardsPlayed = 0;
}
currentRoundIndex = currentRoundIndex % NUM_ROUNDS;
nextState = States::S02_PROMPT_DEALER;
}
else
{
nextState = States::S04_PROMPT_NEXT_PLAYER;
}
break;
default:
break;
}
if (nextState != presentState)
{
// Update the present state with the next state.
presentState = nextState;
//
// Output logic.
//
switch (presentState)
{
case States::S00_WAIT_FOR_FIRST_DEALER:
Serial.println("S00_WAIT_FOR_FIRST_DEALER");
break;
case States::S01_FOUND_FIRST_DEALER:
Serial.println("S01_FOUND_FIRST_DEALER");
Serial.print(" Found the first dealer: Player ");
Serial.println(currentDealerIndex + 1);
prompter.Stop();
for (auto &player : players)
{
player.led.Off();
}
break;
case States::S02_PROMPT_DEALER:
Serial.println("S02_PROMPT_DEALER");
players[currentDealerIndex].led.On();
// Beep the speaker for 500 ms.
speaker.On(DEALER_FREQUENCY); // Prompt the dealer. You can adjust the frequency (Hz) and duration (ms).
promptTimer.Start(500);
break;
case States::S03_WAIT_FOR_DEALER:
Serial.println("S03_WAIT_FOR_DEALER");
Serial.print(" Round ");
Serial.println(currentRoundIndex + 1);
players[currentDealerIndex].led.Off();
speaker.Off();
performSpeakerPrompt = BLINKS_PER_ROUND[currentRoundIndex]; // Limit the number of beeps to one sequence.
prompter.Start(250, BLINKS_PER_ROUND[currentRoundIndex], 500); // Blink and beep to prompt the dealer.
break;
case States::S04_PROMPT_NEXT_PLAYER:
Serial.println("S04_PROMPT_NEXT_PLAYER");
prompter.Stop();
players[currentDealerIndex].led.Off(); // Turn off blinking for current dealer.
speaker.Off();
speaker.On(PLAYER_FREQUENCY);
players[currentPlayerIndex].led.On(); // Light up LED for next player.
promptTimer.Start(500);
break;
case States::S05_WAIT_FOR_PLAYER:
Serial.println("S05_WAIT_FOR_PLAYER");
speaker.Off();
promptTimer.Start(PLAYER_TIMER);
Serial.println(" Started player timer.");
break;
case States::S06_ACKNOWLEDGE_PLAYER:
Serial.println("S06_ACKNOWLEDGE_PLAYER");
Serial.print(" cardsPlayed = ");
Serial.println(cardsPlayed);
prompter.Stop();
players[currentPlayerIndex].led.Off(); // Turn off LED for current player.
speaker.Off();
break;
case States::S07_MOVE_TO_NEXT_PLAYER:
Serial.println("S07_MOVE_TO_NEXT_PLAYER");
break;
default:
break;
}
}
}