/*
* Gray_WorkJunkCarnival
*
* Code by: Gray Mack
* License: Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0)
* https://creativecommons.org/licenses/by-nc-sa/4.0/
* Created: 03/04/2025
* One Line Description: A little game system
* Board: TBD
* Select Board: TBD
* Other Hardware: Button, LEDS, IR receiver
* Detailed Description:
* A few mini games on a 3.3v device
* Use a jadak barcode scanner that has a trigger pin and serial output of the barcode text
* A proximity sensor to activate a barcode read
* Use an LCD screen
* if barcode starts with "A:" then it is a joke card, print the barcode text to the screen
* if barcode starts with "8:" then it is a magic 8 ball request, print the familiar random text
* if it is a known code, enter the shootem game
* the gun consists of
* one cherry switch trigger,
* one IR-remote decoder that outputs high when 40khz signal is present fitted with a shield tube,
* an output LED
* 3 targets with each having: R, G, B LED, IR LED to send 40khz signal
* A small speaker to play tones
* 1 Spare Button Input
*
* Rev History:
* 03/04/2025 initial code creation
* 03/05/2025 updates 2
* 03/14/2025 building circuits, testing hardware
* 04/04/2025 integrating the results of pre-testing into the application.
* 04/09/2025 integrating the latest results of pre-testing into the application.
* 04/15/2025 game algorithm development
* 05/12/2025 merge in test code forom Gray_Test_Esp32s3_1.ino
* 05/19/2025 minor cleanup, first run bug fixes
* //2025 ...
*/
// ----[ configuration ]------------------------------------
#define CONSOLE_BAUD 115200
#define JADAK_BAUD 19200
#define STARTUP_MESSAGE "Gray_WorkJunkCarnival v1.0"
#define NUM_LEDS 1
#define NUM_TARGETS 3
// ----[ included libraries ]------------------------------------
#include <Arduino.h>
#include <Bounce2.h> // https://github.com/thomasfredericks/Bounce2 v2.71
#include <FastLED.h> // By Daniel Garcia v3.9.14
#include "Adafruit_VL53L0X.h" // v1.2.2
#include <LiquidCrystal_I2C.h> // https://github.com/johnrickman/LiquidCrystal_I2C v1.1.2 By Frank de Brabander
// ----[ pin definitions ]------------------------------------
// 3.3v
// 3.3v
// RST
const uint8_t PIN_RED_LED_PINS[NUM_TARGETS] = { 4, 9, 12 }; // target flashes red when missed
const uint8_t PIN_GREEN_LED_PINS[NUM_TARGETS] = { 5, 10, 13 }; // target flashes green when hit
const uint8_t PIN_BLUE_LED_PINS[NUM_TARGETS] = { 6, 11, 14 }; // target lights blue when active
const uint8_t PIN_BARCODE_TAKE_READING = 7;
const uint8_t PIN_RESERVED_U0RTS = 15;
const uint8_t PIN_RESERVED_U0CTS = 16;
const uint8_t PIN_UART1_TXD = 17;
const uint8_t PIN_LCD_UART = PIN_UART1_TXD;
const uint8_t PIN_UART1_RXD = 18;
const uint8_t PIN_JADAK_UART = PIN_UART1_RXD;
const uint8_t PIN_PROXIMITY = 8;
const uint8_t PIN_RESERVED_JTAG = 3;
const uint8_t PIN_RESERVED_LOG = 46;
// 9,10,11,12,13,14 see above
// 5v
// GND
// GND
const uint8_t PIN_GUN_TRIGGER = 1;
const uint8_t PIN_AVAILABLE1 = 2;
const uint8_t PIN_IR_LED_PINS[NUM_TARGETS] = { 40, 41, 42 };
const uint8_t PIN_MUZZLE_FLASH = 39;
const uint8_t PIN_SPEAKER = 38;
const uint8_t PIN_I2C_SCL = 37;
const uint8_t PIN_I2C_SDA = 36;
const uint8_t PIN_AVAILABLE2 = 35;
const uint8_t PIN_RESERVED_BOOT = 0;
const uint8_t PIN_RESERVED_VSPI = 45;
#ifndef PIN_RGB_LED
const uint8_t PIN_RGB_LED = 48;
#endif
const uint8_t PIN_IR_GUN_SENSOR = 47;
const uint8_t PIN_SPARE_BUTTON = 21;
const uint8_t PIN_RESERVED_USB_DP = 20;
const uint8_t PIN_RESERVED_USB_DM = 19;
// GND
// GND
// ----[ constants ]------------------------------------
const int8_t NO_TARGET = -1; // hit values are 0,1,2
const uint16_t IR_FREQUENCY = 56000;
const uint16_t SPEAKER_FREQUENCY = 440;
const uint16_t TURN_OFF_IR_LED = 0;
const uint16_t TURN_OFF_SPEAKER = 0;
//auto//const int LEDC_CHANNEL = 0; // LEDC channel (0-15) to run the tone command for 40khz square wave to IR LED
const int PWM_RESOLUTION = 2; // (1-16 bits)
const uint8_t DISTANCE_SENSOR_ADDRESS = 0x50;
const int DISTANCE_MINIMUM_MM = 90;
const int DISTANCE_TO_TRIGGER_BARCODE_MM = 225;
const int DISTANCE_OUT_OF_RANGE = 0;
const uint8_t LED_ON = HIGH;
const uint8_t LED_OFF = LOW;
const uint8_t PROXIMITY_SENSED = LOW;
const uint8_t PROXIMITY_ABSENT = HIGH;
const uint8_t IR_TARGET_HIT = LOW;
const uint8_t IR_TARGET_MISSED = HIGH;
const uint8_t TRIGGER_RELEASED = LOW;
const uint8_t TRIGGER_PULLED = HIGH;
const uint8_t REQUEST_BARCODE_SCAN = HIGH;
const uint8_t RELEASE_BARCODE_SCAN = LOW;
enum LedSetting : uint8_t { TurnOn, TurnOff, TurnOffAfterBeingOnForAWhile };
const uint8_t LCD_ROWS = 2;
const uint8_t LCE_COLS = 16;
enum TargetActionType : uint8_t { TargetLightLive, TargetLightHit, TargetLightMiss, TargetLightDeactivated };
// ----[ game constants ]------------------------------------
#define TARGET_GAME_BARCODE "01HS0215S0010000081"
#define MAGIC8_BARCODE "Magic8"
#define ANSWER_PREFIX_BARCODE "A:"
#define TESTMODE_BARCODE "TestMode"
enum SystemStateEnum : uint8_t {
Attract,
Hunting, TargetHit, TargetMiss, GameOver, TestMode
};
enum SoundType : uint8_t {
NoSound,
TargetLiveSound, TargetHitSound, TargetMissSound,
ContinueSound
};
const uint16_t GAME_MAX_SHOTS = 30;
const uint32_t GAME_MAX_TIME_MS = 30000;
const uint32_t JADAK_TRIGGER_TIME_MS = 10;
const uint32_t BARCODE_WAITING_TIME_MS = 45000;
const uint32_t DISPLAY_MESSAGE_TIME_MS = 30000;
const uint32_t BACKLIGHT_OFF_TIME_MS = 30000;
const uint32_t HIT_MISS_INDICATOR_TIME_MS = 200;
const uint32_t GAMEOVER_ANNOUNCE_TIME_MS = 3000;
const uint32_t TARGET_EXPIRE_TIME_MS = 1700;
const uint32_t MUZZLE_FLASH_OFF_TIME_MS = 250;
const int MAX_BARCODE_SIZE = 30;
//#define IsGameRunning() (GameStartTime != 0)
const uint8_t NO_ACTIVE_TARGET = -1;
#define MAGIC_8 "Magic 8:"
const uint8_t Magic8Responses = 20; // 10 affirmative, 5 non comittal, 5 negative
const char *Magic8Messages[Magic8Responses] = {
// ----------------
"It is certain",
"It is so", //"It is decidedly so",
"Without a doubt",
"Yes definitely",
"Rely on it", //"You may rely on it",
"As I see it, yes",
// ----------------
"Most likely",
"Outlook good",
"Yes",
"Signs point yes", //"Signs point to yes",
// ----------------
"Hazy, try again", //"Reply hazy, try again",
"Ask again later",
"I wont tell you", //"Better not tell you now",
"Unpredictable", // "Cannot predict now",
"Ask again", //"Concentrate and ask again",
// ----------------
"Perhaps not", //"Dont count on it",
"My reply is no",
"Sources say no", //"My sources say no",
"Outlook: no", // "Outlook not so good",
"Very doubtful"
};
#define LCD_INTRO "Luminex Carnival"
#define LCD_ANSWER "Answer: "
#define LCD_SCAN_THE_8_BALL "Scan the 8 ball "
#define LCD_FOR_AN_ANSWER "for an answer "
#define LCD_MAGIC_8_ANSWERS "Magic 8 answers:"
#define LCD_STUN_THE_ROBOTS "Stun the Robots"
#define LCD_OUT_OF_SHOTS "Out of Shots! "
#define LCD_OUT_OF_TIME "Out of time! "
#define LCD_TGT___SHOTS_ "Tgt: Shots: "
#define LCD_TIME___SCORE_ "Time: Score: "
#define LCD_HELLO_GUYS " Hello Guys"
#define LCD_VL53L0_BOOT_FAIL "VL53L0 boot fail"
// ----[ predeclarations ]------------------------------------
void setup();
void loop();
void EnterAttractMode();
void AttractMode();
void StartBarcodeRead();
void AnswerJoke(String answer);
void ProximityTo8ballMessage();
void AnswerMagic8();
void PrintBarcode(String barcode);
void StartTargetGame();
void ConsiderNewTarget();
bool CheckForTrigger();
bool CheckEndGame();
void EndTargetGame();
void ShowGameStatsLcd();
void ShowGameStatsConsole();
void StartTestMode();
void RunTestMode();
void PlaySound(SoundType soundEffect);
int8_t CheckForTargetHit();
uint32_t GetNextTargetPopTimeMs();
void PixelHitToRgb(uint8_t whichTarget);
int GetRangingMm(bool print = false);
void SetTargetColor(int8_t target, CRGB color);
void SetSingleTargetLight(int8_t target, TargetActionType action);
void DistanceTestLoop();
void SetBacklight(LedSetting val);
void SetMuzzleFlash(LedSetting val);
// ----[ helper classes ]-------------------------------------------
// ----[ global variables ]------------------------------------
SystemStateEnum SystemState;
bool BacklightOn = false;
bool MuzzleFlashOn = false;
// Target game variables
//bool GameRunning = false; // ?
int8_t GameScore;
int8_t GameShots;
int8_t GameTimeRemain;
int8_t CurrentTarget = NO_TARGET;
uint32_t GameStartTimeMs = 0; // changes to millis() when the target game starts and when attract mode starts (to potentially track time since user interaction)
uint32_t GameNextTargetPopTimeMs;
uint32_t LastActionTime = 0; // changes to millis() when 1 enter attract mode, 2 trigger pulled while Hunting, 3 game over reached,
// 4 answer joke, 5 proximity to 8ball, 6 answer 8ball, 7 print barcode,
// 8 start target game, 9 end target game, 10 start test mode, 11 state changes from TargetHit to Hunting so next pop time will be accurate
// used to know when to finish target hit light,target miss light,game over message,
// and when to pop new target, when to turn off lcd light
Bounce2::Button GunTrigger = Bounce2::Button();
Bounce2::Button ProximityToMagic8Ball = Bounce2::Button();
CRGB leds[NUM_LEDS];
Adafruit_VL53L0X DistanceSensor = Adafruit_VL53L0X();
int objectDistanceFromScannerMM = DISTANCE_OUT_OF_RANGE;
LiquidCrystal_I2C Lcd(0x27,20,4); // set the LCD address to 0x27 for a 16 chars and 2 line display
// ----[ code ]------------------------------------
void setup()
{
Serial.begin(CONSOLE_BAUD);
delay(500);
Serial.println(STARTUP_MESSAGE);
FastLED.addLeds<NEOPIXEL, PIN_RGB_LED>(leds, NUM_LEDS); // GRB ordering is assumed
FastLED.setBrightness(64);
Serial1.begin(JADAK_BAUD, SERIAL_8N1, PIN_UART1_RXD, PIN_UART1_TXD); // Initialize Serial1 for communication with RX1 JADAK barcode scanner
delay(500);
for (int i = 0; i < 3; i++)
{
pinMode(PIN_BLUE_LED_PINS[i], OUTPUT);
pinMode(PIN_RED_LED_PINS[i], OUTPUT);
pinMode(PIN_GREEN_LED_PINS[i], OUTPUT);
pinMode(PIN_IR_LED_PINS[i], OUTPUT);
}
pinMode(PIN_IR_GUN_SENSOR, INPUT);
pinMode(PIN_PROXIMITY, INPUT);
pinMode(PIN_BARCODE_TAKE_READING, OUTPUT);
pinMode(PIN_MUZZLE_FLASH, OUTPUT);
GunTrigger.attach(PIN_GUN_TRIGGER, INPUT_PULLUP);
GunTrigger.interval(5);
GunTrigger.setPressedState(TRIGGER_PULLED);
ProximityToMagic8Ball.attach(PIN_PROXIMITY, INPUT);
ProximityToMagic8Ball.interval(50);
ProximityToMagic8Ball.setPressedState(PROXIMITY_SENSED);
Wire.setPins(PIN_I2C_SDA, PIN_I2C_SCL);
Wire.begin();
Lcd.init();
SetBacklight(LedSetting::TurnOn);
Lcd.setCursor(0,0);
Lcd.print(LCD_HELLO_GUYS);
if(!DistanceSensor.begin(DISTANCE_SENSOR_ADDRESS, true, &Wire, Adafruit_VL53L0X::VL53L0X_SENSE_HIGH_SPEED))
{
Lcd.setCursor(0,0);
Lcd.print(LCD_VL53L0_BOOT_FAIL);
}
// Configure the LEDC peripheral for tone() capability
ledcAttach(PIN_IR_LED_PINS[0], IR_FREQUENCY, PWM_RESOLUTION);
ledcWriteTone(PIN_IR_LED_PINS[0], TURN_OFF_IR_LED);
ledcAttach(PIN_IR_LED_PINS[1], IR_FREQUENCY, PWM_RESOLUTION);
ledcWriteTone(PIN_IR_LED_PINS[1], TURN_OFF_IR_LED);
ledcAttach(PIN_IR_LED_PINS[2], IR_FREQUENCY, PWM_RESOLUTION);
ledcWriteTone(PIN_IR_LED_PINS[2], TURN_OFF_IR_LED);
ledcAttach(PIN_SPEAKER, IR_FREQUENCY, PWM_RESOLUTION);
ledcWriteTone(PIN_SPEAKER, TURN_OFF_SPEAKER);
// lcd test
for (int b=0; b<=100; b+=5)
{
Lcd.setCursor(0,1);
Lcd.print(millis());
Serial.println(millis());
delay(100);
}
Serial.println("setup complete");
Lcd.clear();
EnterAttractMode();
}
void loop()
{
switch(SystemState)
{
case SystemStateEnum::Attract:
AttractMode();
break;
case SystemStateEnum::Hunting:
if(CheckEndGame())
{
EndTargetGame();
SystemState = SystemStateEnum::GameOver;
}
ConsiderNewTarget();
if(CheckForTrigger())
{
Serial.print("Bang! ");
SetMuzzleFlash(LedSetting::TurnOn);
LastActionTime = millis();
int16_t targetHit = CheckForTargetHit();
if(targetHit != NO_TARGET && targetHit == CurrentTarget)
{
Serial.println("Target hit!");
GameScore++;
SetSingleTargetLight(CurrentTarget, TargetActionType::TargetLightHit);
PlaySound(SoundType::TargetHitSound);
SystemState = SystemStateEnum::TargetHit;
}
else if(CurrentTarget != NO_TARGET)
{
Serial.println("Target miss");
PlaySound(SoundType::TargetMissSound);
SetSingleTargetLight(CurrentTarget, TargetActionType::TargetLightMiss);
SystemState = SystemStateEnum::TargetMiss;
}
else // trigger fired but no current targets
{
Serial.println("Wasted shot");
PlaySound(SoundType::TargetMissSound);
//SystemState stays SystemStateEnum::Hunting, another shot is quickly possible
}
}
SetMuzzleFlash(LedSetting::TurnOffAfterBeingOnForAWhile);
break;
case SystemStateEnum::TargetHit:
// stage to pop a new target soon
if (millis() - LastActionTime >= HIT_MISS_INDICATOR_TIME_MS)
{
SetSingleTargetLight(CurrentTarget, TargetActionType::TargetLightDeactivated);
//digitalWrite(PIN_MUZZLE_FLASH, LED_OFF);
PixelHitToRgb(NO_ACTIVE_TARGET);
CurrentTarget = NO_ACTIVE_TARGET;
GameNextTargetPopTimeMs = GetNextTargetPopTimeMs();
LastActionTime = millis(); // so GameNextTargetPopTimeMs does not include HIT_MISS_INDICATOR_TIME_MS
SystemState = SystemStateEnum::Hunting;
}
SetMuzzleFlash(LedSetting::TurnOffAfterBeingOnForAWhile);
break;
case SystemStateEnum::TargetMiss:
// previous target is still active
if (millis() - LastActionTime >= HIT_MISS_INDICATOR_TIME_MS)
{
SetSingleTargetLight(CurrentTarget, TargetActionType::TargetLightLive);
//digitalWrite(PIN_MUZZLE_FLASH, LED_OFF);
PixelHitToRgb(NO_ACTIVE_TARGET);
SystemState = SystemStateEnum::Hunting;
}
SetMuzzleFlash(LedSetting::TurnOffAfterBeingOnForAWhile);
break;
case SystemStateEnum::GameOver:
// clear the score and go back to attract mode
if(millis() - LastActionTime >= GAMEOVER_ANNOUNCE_TIME_MS)
{
//SetSingleTargetLight(CurrentTarget, TargetActionType::TargetLightDeactivated);
//Serial.println("Game over.");
//Lcd.setCursor(0,0);
//Lcd.print("-[ Game Over ]- ");
//LastActionTime = millis();
EnterAttractMode(); //SystemState = SystemStateEnum::Attract;
}
break;
case SystemStateEnum::TestMode:
RunTestMode();
break;
}
PlaySound(SoundType::ContinueSound);
}
// ====================================================
// ----[ function ]------------------------------------
void EnterAttractMode()
{
Serial.println("EnterAttractMode");
SetBacklight(LedSetting::TurnOn);
Lcd.clear();
Lcd.setCursor(0,0);
Lcd.print(LCD_INTRO);
GameStartTimeMs = millis(); // attract start time
LastActionTime = millis();
SystemState = SystemStateEnum::Attract;
}
// ----[ function ]------------------------------------
void AttractMode()
{
// check for barcode results
if(Serial1.available())
{
String barcodeValue = Serial1.readString();
if(strncmp(barcodeValue.c_str(), TARGET_GAME_BARCODE, MAX_BARCODE_SIZE) == 0)
{
StartTargetGame();
}
else if(strncmp(barcodeValue.c_str(), MAGIC8_BARCODE, MAX_BARCODE_SIZE) == 0)
{
AnswerMagic8();
}
else if(strncmp(barcodeValue.c_str(), ANSWER_PREFIX_BARCODE, 2) == 0)
{
AnswerJoke(barcodeValue);
}
else if(strncmp(barcodeValue.c_str(), TESTMODE_BARCODE, MAX_BARCODE_SIZE) == 0)
{
StartTestMode();
}
else
{
PrintBarcode(barcodeValue);
}
}
// check distance sensor if ShouldReadBarcode
EVERY_N_MILLISECONDS(100) {
objectDistanceFromScannerMM = GetRangingMm();
}
static bool previousObjectDetected = false;
bool objectDetected = objectDistanceFromScannerMM > DISTANCE_MINIMUM_MM &&
objectDistanceFromScannerMM < DISTANCE_TO_TRIGGER_BARCODE_MM;
if(objectDetected && !previousObjectDetected)
{
StartBarcodeRead();
}
previousObjectDetected = objectDetected;
// check for hand proximity to 8ball
ProximityToMagic8Ball.update();
if(ProximityToMagic8Ball.pressed())
{
ProximityTo8ballMessage();
}
// static bool lastProximityTo8ball = false;
// bool proximityTo8ball = digitalRead(PIN_PROXIMITY) == PROXIMITY_SENSED;
// if(proximityTo8ball && !lastProximityTo8ball )
// {
// ProximityTo8ballMessage();
// }
// lastProximityTo8ball = proximityTo8ball;
SetBacklight(LedSetting::TurnOffAfterBeingOnForAWhile);
}
// ----[ function ]------------------------------------
void StartBarcodeRead()
{
Serial.println("Reading for barcode");
// Send a pulse to jadak camera
digitalWrite(PIN_BARCODE_TAKE_READING, REQUEST_BARCODE_SCAN);
delayMicroseconds(10);
digitalWrite(PIN_BARCODE_TAKE_READING, RELEASE_BARCODE_SCAN);
}
// ----[ function ]------------------------------------
void AnswerJoke(String answer)
{
Serial.print("Joke: ");
Serial.println(answer);
SetBacklight(LedSetting::TurnOn);
Lcd.clear();
Lcd.setCursor(0,0);
Lcd.print(LCD_ANSWER);
Lcd.setCursor(0,1);
Lcd.print(answer.substring(2)); // skip the "A:""
LastActionTime = millis();
}
// ----[ function ]------------------------------------
void ProximityTo8ballMessage()
{
Serial.println("Proximity to 8 ball detected");
SetBacklight(LedSetting::TurnOn);
Lcd.clear();
Lcd.setCursor(0,0);
Lcd.print(LCD_SCAN_THE_8_BALL);
Lcd.setCursor(0,1);
Lcd.print(LCD_FOR_AN_ANSWER);
LastActionTime = millis();
}
// ----[ function ]------------------------------------
void AnswerMagic8()
{
Serial.println("Magic 8 Ball Says:");
SetBacklight(LedSetting::TurnOn);
Lcd.clear();
Lcd.setCursor(0,0);
Lcd.print(LCD_MAGIC_8_ANSWERS);
int yourLuck = random(Magic8Responses);
Serial.println(Magic8Messages[yourLuck]);
Lcd.setCursor(0,1);
Lcd.print(Magic8Messages[yourLuck]);
LastActionTime = millis();
}
// ----[ function ]------------------------------------
void PrintBarcode(String barcode)
{
Serial.print("barcode seen: ");
Serial.println(barcode);
SetBacklight(LedSetting::TurnOn);
Lcd.clear();
Lcd.setCursor(0,0);
Lcd.print(barcode);
LastActionTime = millis();
}
// ====================================================
// ----[ function ]------------------------------------
void StartTargetGame()
{
Serial.println("Starting Target Game:");
SetBacklight(LedSetting::TurnOn);
Lcd.clear();
Lcd.setCursor(0,0);
Lcd.print(LCD_STUN_THE_ROBOTS);
// lastStateChangeTime = millis();
CurrentTarget = NO_ACTIVE_TARGET;
GameNextTargetPopTimeMs = GetNextTargetPopTimeMs();
GameScore = 0;
GameShots = 30;
//IsGameRunning = true;
delay(500);
Serial.println("Game started!");
SystemState = SystemStateEnum::Hunting;
GameStartTimeMs = millis();
LastActionTime = millis();
}
// ----[ function ]------------------------------------
void ConsiderNewTarget()
{
static uint32_t targetPopTimeMs = 0;
bool timeForTarget = (CurrentTarget == NO_ACTIVE_TARGET) && (millis() - LastActionTime > GameNextTargetPopTimeMs);
bool timeFortargetToExpire = (CurrentTarget != NO_ACTIVE_TARGET) && (millis() - targetPopTimeMs > TARGET_EXPIRE_TIME_MS);
if( ! timeForTarget && ! timeFortargetToExpire) return;
// pop the new target
CurrentTarget = random(NUM_TARGETS);
targetPopTimeMs = millis();
Serial.print("Target "); Serial.println(CurrentTarget);
SetSingleTargetLight(CurrentTarget, TargetActionType::TargetLightLive);
PlaySound(SoundType::TargetLiveSound);
}
// ----[ function ]------------------------------------
bool CheckForTrigger()
{
GunTrigger.update();
if(GunTrigger.pressed() && GameShots > 0)
{
GameShots--;
return true;
}
return false;
}
// ----[ function ]------------------------------------
// the game ends when out of shots or game time remain <= 0
bool CheckEndGame()
{
if(GameShots <= 0)
{
ShowGameStatsLcd();
ShowGameStatsConsole();
Serial.println(LCD_OUT_OF_SHOTS);
Lcd.setCursor(0,0);
Lcd.print(LCD_OUT_OF_SHOTS);
//delay(500);
//GameStartTimeMs = 0;
return true;
}
static int16_t lastTimeRemainSec = 0;
GameTimeRemain = int16_t((GAME_MAX_TIME_MS - (millis() - GameStartTimeMs)) / 1000);
if(GameTimeRemain != lastTimeRemainSec)
{ // every second, do countdown to screen with score
ShowGameStatsLcd();
ShowGameStatsConsole();
lastTimeRemainSec = GameTimeRemain;
}
if(GameTimeRemain <= 0)
{
Serial.println(LCD_OUT_OF_TIME);
Lcd.setCursor(0,0);
Lcd.print(LCD_OUT_OF_TIME);
//delay(500);
//GameStartTimeMs = 0;
return true;
}
return false;
}
// ----[ function ]------------------------------------
void EndTargetGame()
{
Serial.println("Game Ended!");
SetSingleTargetLight(CurrentTarget, TargetActionType::TargetLightDeactivated);
SetMuzzleFlash(LedSetting::TurnOff);
PlaySound(SoundType::NoSound);
LastActionTime = millis();
}
// ----[ function ]------------------------------------
void ShowGameStatsLcd()
{
int row = 0;
Lcd.setCursor(0,row);
Lcd.print(LCD_TGT___SHOTS_);
if(CurrentTarget != NO_TARGET)
{
Lcd.setCursor(4,row);
Lcd.print(CurrentTarget);
}
Lcd.setCursor(12,row);
Lcd.print(GameShots);
row = 1;
Lcd.setCursor(0,row);
Lcd.print(LCD_TIME___SCORE_);
if(GameTimeRemain > 0)
{
Lcd.setCursor(5,row);
Lcd.print(GameTimeRemain);
}
Lcd.setCursor(14,row);
Lcd.print(GameScore);
}
// ----[ function ]------------------------------------
void ShowGameStatsConsole()
{
Serial.print("Time:");
if(GameTimeRemain <= 0) { Serial.print("--"); }
else
{
if(GameTimeRemain < 10) { Serial.print(" "); }
Serial.print(GameTimeRemain);
}
Serial.print(" Score:");
if(GameScore < 10) Serial.print(" ");
Serial.print(GameScore);
Serial.print(" Shots:");
Serial.print(GameShots);
Serial.println();
}
// ----[ function ]------------------------------------
void StartTestMode()
{
Serial.print("TestMode");
SetBacklight(LedSetting::TurnOn);
Lcd.clear();
Lcd.setCursor(0,0);
Lcd.print("TestMode");
SystemState = SystemStateEnum::TestMode;
LastActionTime = millis();
}
// ----[ function ]------------------------------------
void RunTestMode()
{
static bool blinkToggler = false;
static int counter = 0;
int8_t targetHit = NO_TARGET;
EVERY_N_MILLISECONDS(500)
{
blinkToggler = !blinkToggler;
if(blinkToggler)
{
ledcWriteTone(PIN_SPEAKER, SPEAKER_FREQUENCY);
}
else
{
ledcWriteTone(PIN_SPEAKER, TURN_OFF_SPEAKER);
}
}
GunTrigger.update();
if(GunTrigger.pressed())
{
//digitalWrite(PIN_MUZZLE_FLASH, HIGH);
Serial.println("Bang!");
targetHit = CheckForTargetHit();
PixelHitToRgb(targetHit);
SetSingleTargetLight(targetHit, TargetActionType::TargetLightHit);
//digitalWrite(PIN_MUZZLE_FLASH, LOW);
}
// continuous reading code to test target response
digitalWrite(PIN_MUZZLE_FLASH, HIGH);
targetHit = CheckForTargetHit();
PixelHitToRgb(targetHit);
digitalWrite(PIN_MUZZLE_FLASH, LOW);
EVERY_N_MILLISECONDS(100)
{
objectDistanceFromScannerMM = GetRangingMm();
}
static bool previousObjectDetected = false;
bool objectDetected = objectDistanceFromScannerMM > DISTANCE_MINIMUM_MM &&
objectDistanceFromScannerMM < DISTANCE_TO_TRIGGER_BARCODE_MM;
if(objectDetected && !previousObjectDetected)
{
digitalWrite(PIN_BARCODE_TAKE_READING, REQUEST_BARCODE_SCAN);
delayMicroseconds(10);
digitalWrite(PIN_BARCODE_TAKE_READING, RELEASE_BARCODE_SCAN);
}
previousObjectDetected = objectDetected;
ProximityToMagic8Ball.update();
if(ProximityToMagic8Ball.pressed())
{
Serial.println("Proximity to 8 ball!");
}
bool log = objectDetected || GunTrigger.pressed();
EVERY_N_MILLISECONDS(1000)
{
log = true;
}
if(log) {
if(digitalRead(PIN_PROXIMITY) == PROXIMITY_SENSED) Serial.print(" Prox"); else Serial.print(" noProx");
if(objectDetected) Serial.print(" Obj"); else Serial.print(" noObj");
if(targetHit > 0) { Serial.print(" IrHit"); Serial.print(targetHit); Serial.print(" "); } else Serial.print(" IrMiss ");
Serial.print(" bang="); Serial.print(GunTrigger.isPressed());
Serial.print(" counter="); Serial.print(counter);
Serial.print(" dist="); Serial.println(objectDistanceFromScannerMM);
}
if(Serial1.available())
{
Serial.print("jadack says=");
Serial.println(Serial1.readString());
}
EVERY_N_MILLISECONDS(1000)
{
Lcd.setCursor(0,0);
Lcd.print(counter++);
}
}
// ----[ function ]------------------------------------
void PlaySound(SoundType soundEffect)
{
switch(soundEffect)
{
case SoundType::TargetLiveSound:
break;
case SoundType::TargetHitSound:
//ledcWriteTone(PIN_SPEAKER, 600);
break;
case SoundType::TargetMissSound:
//ledcWriteTone(PIN_SPEAKER, 60);
break;
case SoundType::ContinueSound:
break;
case SoundType::NoSound:
ledcWriteTone(PIN_SPEAKER, TURN_OFF_SPEAKER);
break;
}
}
// ----[ function ]------------------------------------
// ----[ function ]------------------------------------
// ----[ function ]------------------------------------
// ----[ function ]------------------------------------
int8_t CheckForTargetHit()
{
uint8_t target = NO_TARGET;
//ledcWriteTone(PIN_IR_LED_PINS[0], IR_FREQUENCY);
ledcChangeFrequency(PIN_IR_LED_PINS[0], IR_FREQUENCY, 2);
ledcWrite(PIN_IR_LED_PINS[0], 1);
delayMicroseconds(600);
if(digitalRead(PIN_IR_GUN_SENSOR) == IR_TARGET_HIT) target=0;
ledcWriteTone(PIN_IR_LED_PINS[0], TURN_OFF_IR_LED);
delayMicroseconds(200);
//ledcWriteTone(PIN_IR_LED_PINS[1], IR_FREQUENCY);
ledcChangeFrequency(PIN_IR_LED_PINS[1], IR_FREQUENCY, 2);
ledcWrite(PIN_IR_LED_PINS[1], 1);
delayMicroseconds(600);
if(digitalRead(PIN_IR_GUN_SENSOR) == IR_TARGET_HIT) target=1;
ledcWriteTone(PIN_IR_LED_PINS[1], TURN_OFF_IR_LED);
delayMicroseconds(200);
//ledcWriteTone(PIN_IR_LED_PINS[2], IR_FREQUENCY);
ledcChangeFrequency(PIN_IR_LED_PINS[2], IR_FREQUENCY, 2);
ledcWrite(PIN_IR_LED_PINS[2], 1);
delayMicroseconds(600);
if(digitalRead(PIN_IR_GUN_SENSOR) == IR_TARGET_HIT) target=2;
ledcWriteTone(PIN_IR_LED_PINS[2], TURN_OFF_IR_LED);
return target;
}
// ----[ function ]------------------------------------
uint32_t GetNextTargetPopTimeMs()
{
return random(1000, 2000);
}
// ----[ function ]------------------------------------
void PixelHitToRgb(uint8_t whichTarget)
{
switch(whichTarget) {
case 0: leds[0] = CRGB::Red; break;
case 1: leds[0] = CRGB::Green; break;
case 2: leds[0] = CRGB::Blue; break;
default: leds[0] = CRGB::Black; break;
}
FastLED.show();
}
// ----[ function ]------------------------------------
int GetRangingMm(bool print)
{
VL53L0X_RangingMeasurementData_t measure;
DistanceSensor.getSingleRangingMeasurement(&measure, false); // pass in 'true' to get debug data printout!
if (measure.RangeStatus != 4) { // phase failures have incorrect data
if(print) { Serial.print("Distance (mm): "); Serial.println(measure.RangeMilliMeter); }
return measure.RangeMilliMeter;
} else {
if(print) { Serial.println(" out of range "); }
return DISTANCE_OUT_OF_RANGE;
}
}
// ----[ function ]------------------------------------
void SetTargetColor(int8_t target, CRGB color)
{
if(target <= NO_TARGET || target >= NUM_TARGETS) return;
//Serial.print("TSC"); Serial.println(target);
if(color == CRGB::Red) {
digitalWrite(PIN_RED_LED_PINS[target], HIGH);
digitalWrite(PIN_GREEN_LED_PINS[target], LOW);
digitalWrite(PIN_BLUE_LED_PINS[target], LOW);
} else if(color == CRGB::Green) {
digitalWrite(PIN_RED_LED_PINS[target], LOW);
digitalWrite(PIN_GREEN_LED_PINS[target], HIGH);
digitalWrite(PIN_BLUE_LED_PINS[target], LOW);
} else if(color == CRGB::Blue) {
digitalWrite(PIN_RED_LED_PINS[target], LOW);
digitalWrite(PIN_GREEN_LED_PINS[target], LOW);
digitalWrite(PIN_BLUE_LED_PINS[target], HIGH);
} else {
digitalWrite(PIN_RED_LED_PINS[target], LOW);
digitalWrite(PIN_GREEN_LED_PINS[target], LOW);
digitalWrite(PIN_BLUE_LED_PINS[target], LOW);
}
}
// ----[ function ]------------------------------------
void SetSingleTargetLight(int8_t target, TargetActionType action)
{ // passing target NO_TARGET will Black all
//Serial.print("Target:"); Serial.print(target); Serial.print(" Action:"); Serial.println(action);
if(target != 0) SetTargetColor(0, CRGB::Black);
if(target != 1) SetTargetColor(1, CRGB::Black);
if(target != 2) SetTargetColor(2, CRGB::Black);
switch(action) {
case TargetActionType::TargetLightLive: SetTargetColor(target, CRGB::Blue); break;
case TargetActionType::TargetLightHit: SetTargetColor(target, CRGB::Green); break;
case TargetActionType::TargetLightMiss: SetTargetColor(target, CRGB::Red); break;
case TargetActionType::TargetLightDeactivated: SetTargetColor(target, CRGB::Black); break;
}
}
// ----[ function ]------------------------------------
void DistanceTestLoop()
{
while(true) {
GetRangingMm(true);
delay(200);
}
}
// ----[ function ]------------------------------------
void SetBacklight(LedSetting val)
{
if(val == LedSetting::TurnOn)
{
Lcd.backlight(); BacklightOn = true;
}
else if(val == TurnOff)
{
Lcd.noBacklight(); BacklightOn = false;
}
else // val == TurnOffAfterBeingOnForAWhile (check for inactivity)
{
if(BacklightOn && (millis() - LastActionTime > BACKLIGHT_OFF_TIME_MS))
{
Lcd.noBacklight(); BacklightOn = false;
}
}
}
// ----[ function ]------------------------------------
void SetMuzzleFlash(LedSetting val)
{
if(val == LedSetting::TurnOn)
{
digitalWrite(PIN_MUZZLE_FLASH, LED_ON); MuzzleFlashOn = true;
}
else if(val == TurnOff)
{
digitalWrite(PIN_MUZZLE_FLASH, LED_OFF); MuzzleFlashOn = false;
}
else // val == TurnOffAfterBeingOnForAWhile (check for inactivity)
{
if(MuzzleFlashOn && (millis() - LastActionTime > MUZZLE_FLASH_OFF_TIME_MS))
{
digitalWrite(PIN_MUZZLE_FLASH, LED_OFF); MuzzleFlashOn = false;
}
}
}
/* Meanwhile, A Different project
https://sensorium.github.io/Mozzi/learn/output/
https://community.m5stack.com/topic/3334/mozzi-sound-synthesis-library-for-arduino-how-to-make-it-work-on-the-core2
// before including Mozzi.h, configure external audio output mode:
#include "MozziConfigValues.h" // for named option values
#define MOZZI_AUDIO_MODE MOZZI_OUTPUT_I2S_DAC
#include <Mozzi.h>
void audioOutput(const AudioOutput f) {
// put here code to output the sample encapsulated by the structure f:
// This holds either one (mono) or two (stereo) channels, which can be
// obtained using f.l() and f.r().
// Each contains a zero-centered integer value scaled to MOZZI_AUDIO_BITS resolution
// e.g.:
myDAC.write(f.l() + MOZZI_AUDIO_BIAS, f.r() + MOZZI_AUDIO_BIAS);
}*/
Proximity
Trigger