https://github.com/adrianariton/PMProj
Updated project: https://youtube.com/shorts/TrdBkxlq2JE?feature=share
Old version: https://youtube.com/shorts/uZILq4yrm3E?feature=share
Proiectul “Bomb Defusal Challenge” este un joc fizic interactiv inspirat de “Keep Talking and Nobody Explodes”.
Jucătorii trebuie să dezamorseze o bombă virtuală prin rezolvarea mai multor mini-jocuri (module) înainte ca timerul să ajungă la zero.
Ceea ce face acest proiect special este integrarea hardware-ului real - senzori, LED-uri, butoane și afișaje - pentru a crea o experiență tactilă autentică.
Scopul proiectului este de a crea o experiență de joc cooperativă, care necesită comunicare clară, logică și abilitatea de a lucra sub presiune. Ideea de bază este de a transforma un joc digital popular într-o experiență fizică interactivă folosind componente electronice accesibile.
Acest proiect este util ca exercițiu practic de electronică și programare, demonstrând cum pot fi integrate multiple tehnologii într-un singur sistem coerent. Pentru jucători, oferă o experiență de joc unică și antrenantă care combină elemente digitale cu cele fizice.
Componente principale:
Unitatea centrală de control (ESP32)
- Gestionează logica jocului, timerele și comunicarea între module - Afișează informații pe ecranul TTGO T-Display - Generează sunetele pentru alarmă și feedback
Modulul Giroscop/Accelerometru (MPU-6500) compatibil I2C
- Detectează orientarea și mișcarea “bombei” - Permite activarea “gimmick-ului” care necesită întoarcerea bombei când alarma sună
Modulul Simon Says Go
- Semaforul cu 3 LED-uri (roșu, galben, verde) - LED-ul “Simon” care indică când trebuie apăsat un buton - 3 butoane tactile pentru interacțiunea jucătorului
Modulul Wire Cutting
- 3 conexiuni de fire detașabile - Sistem de detecție pentru identificarea firului “tăiat” - Logică pentru determinarea firului corect bazată pe alte condiții
Modulul Bomb Disarm Code (Beta)
- Senzor de presiune BMP180 compatibil I2C utilizat ca “fingerprint scanner” - Senzor de temperatură DS18B20 pentru măsurarea căldurii degetului - Senzor de sunet pentru detectarea parolei vocale șoptite/fluierate - LED-uri pentru feedback vizual
Sistem de alarmă și feedback
- Modul amplificator audio LM386 pentru alarmă și efecte sonore - LEDuri pentru indicarea statusului (timpul rămas, greșeli)
Toate aceste module comunică cu unitatea centrală ESP32, care coordonează logica jocului și afișează informații relevante pe ecran.
Nume | Descriere |
---|---|
Placă TTGO T-Display ESP32 | Controler pentru module și unitatea centrală |
Modul MPU-6500 (accelerometru + giroscop) | Detectarea orientării bombei |
Modul senzor sunet LM393 | Detectarea parolei vocale |
Senzor BMP180 | Măsurarea presiunii pentru “fingerprint scanner” |
Senzor DS18B20 | Măsurarea temperaturii degetului |
Modul amplificator audio LM386 | Sistem de alarmă și efecte sonore |
Conectori XH2.54 | Conexiuni pentru module |
Module cu 3 LED-uri (R, Y, G) | Semafor pentru Simon Says și indicatori |
Switch-uri toggle | Controale și interacțiuni |
Senzor distanță HC-SR04P | - |
LED-uri verzi 5mm | Indicatori de stare |
LED-uri galbene 5mm | Indicatori de stare |
LED-uri roșii 3mm | Indicatori de eroare |
Butoane rotunde | Interfața pentru interacțiuni |
Condensatoare + Rezistoare (diverse) | Filtrare și stabilizare circuite |
Mini breadboarduri | Montarea circuitelor |
Fire de conexiune | Conectarea componentelor |
Deignuit pe Platformio: VSCode.
Biblioteci:
* driver/dac.h - pt sunet
* TFT_eSPI.h - pt display
* Arduino.h - pt ft putine lucruri (setup)
Timer loop:
Timer loopul se triggeruieste la fiecare tick, si mentine starea jocului prin urmatorul loop:
void IRAM_ATTR onTimer() { portENTER_CRITICAL_ISR(&timerMux); myMillis++; if (myMillis % 200 == 0) { if (is_active(GAME_SIMON_SAYS)) { if (simon_can_do_next_round){ simon_last_round_begin_milis = myMillis; simon_can_do_next_round = false; simon_round_ongoing = true; } int deltaMillisTilStart = (myMillis - simon_last_round_begin_milis); if ((myMillis - simon_last_round_begin_milis) <= simon_round_intro_time_delay_milis) { // round pending if (deltaMillisTilStart % 1000 == 0) simonOnRoundPrepareTick((simon_round_intro_time_delay_milis - deltaMillisTilStart) / 1000); } else { if (!simon_semaphore_started){ simonOnRoundStart(); simon_semaphore_started = true; } int time_since_round_semaphore_start = deltaMillisTilStart - simon_round_intro_time_delay_milis; int tick_cnt = time_since_round_semaphore_start / simon_time_delay_between_colors; if (time_since_round_semaphore_start % simon_time_delay_between_colors == 0) { if (tick_cnt <= simon_size_of_colors) simonOnRoundStartTick(tick_cnt); else { if (!simon_can_begin_pressing){ simonOnGameEnd(); } simon_can_begin_pressing = true; } } } } else if (is_active(GAME_WIRE_CUTTING)) { if (!wire_cut_game_started){ wireCutOnStart(); wire_cut_game_started = true; wire_cut_game_start_time = myMillis; } if (wire_cut_game_started) { int deltaMilisSinceWireCutStart = (myMillis - wire_cut_game_start_time); if (deltaMilisSinceWireCutStart % 1000 == 0) { int ticks = deltaMilisSinceWireCutStart / 1000; if (ticks < wire_cut_game_duration_seconds) wireCutOnGameTick(wire_cut_game_duration_seconds - ticks); else if (!wire_cut_game_ended) { wire_cut_game_ended = true; wire_cut_game_won = false; } } } } else if (is_active(GAME_PASS_UNLOCK)) { if (!pass_game_started) { pass_game_started = true; pass_game_start_time = myMillis; passUnlockOnStart(); } if (pass_game_started) { int dm = (myMillis - pass_game_start_time); if (dm % 1000 == 0) { int ticks = dm / 1000; if (ticks < pass_game_time_limit_seconds) passUnlockOnGameTick(pass_game_time_limit_seconds - ticks); else if (!pass_game_ended) { pass_game_ended = true; pass_game_won = false; } } } } } portEXIT_CRITICAL_ISR(&timerMux); }
* Fiecare callback este implemetat:
- Simon Games callbacks defns & vars:
int simon_size_of_colors = 3; char simonBufferSemaphoreSize = 0; char simonBufferCompareIdx = 0; void simonOnRoundPrepareTick(int tick_count); void simonOnRoundStart(); void simonOnRoundStartTick(int tick_count); void simonOnGameEnd();
- Wire Cut callbacks defns & vars:
volatile int wire_cut_game_duration_seconds = 20; volatile int wire_cut_game_target_wire = -1; void wireCutOnStart(); void wireCutOnGameTick(int ticks_left); void wireCutOnEnd(bool won);
- Finger scan callbacks defns & vars:
int pass_env_temperatures_read_interv_millis = 1000; // pt calibrarea temperaturii de mediu const int pass_env_temperatures_read_limit = 10; const float pass_temp_threshold_celsius = 0.5; volatile int pass_game_time_limit_seconds = 10; void passUnlockOnStart(); void passUnlockOnGameTick(int ticks_left); void passUnlockOnEnd(bool won);
! Singura folosire a delay-ului implementat nu de mine (custom) e in functia de playNote care ajuta la audio.
const int dacPin = 25; // DAC1 - GPIO25 const int frequency = 1000; // tone frequency Hz const int duration = 100; // duration in ms
void playTone(int freq, int dur_ticks=1) { int samplesPerCycle = 100; // number of samples per waveform cycle int sampleDelayUs = 1000000 / (freq * samplesPerCycle); int amplitude = 64; // 8-bit DAC mid-range (0-255) int dur_ms = dur_ticks * duration; unsigned long endTime = millis() + dur_ms; while (millis() < endTime) { for (int i = 0; i < samplesPerCycle; i++) { // Generate a sine wave sample (scaled 0-255) float sineVal = sin(2 * PI * i / samplesPerCycle); uint8_t dacVal = (uint8_t)(amplitude + amplitude * sineVal); dac_output_voltage(DAC_CHANNEL_1, dacVal); delayMicroseconds(sampleDelayUs); } } dac_output_voltage(DAC_CHANNEL_1, 128); // center output (silence) }
care nu e inclus in cele 3 laboratoare.
I2C ul se face pe pinii 26 si 27, pt ca pinul 25 e folosit pt DAC. Acesta e conectat atat la senzorl giroscopic cat si la cel de temperatura.
Senzorii de giroscop si temperatura sunt implementati cu drivere programate de mine dupa datasheets.
E.g. (girocop):
// Calculate B5 value from the datasheet int32_t computeB5(int32_t UT) { int32_t X1 = (UT - (int32_t)ac6) * ((int32_t)ac5) >> 15; int32_t X2 = ((int32_t)mc << 11) / (X1 + (int32_t)md); return X1 + X2; } float readTemperature() { int32_t UT = readRawTemperature(); int32_t B5 = computeB5(UT); // Temperature in units of 0.1 deg C float temp = ((B5 + 8) >> 4); // Convert to degrees C return temp / 10.0; } uint32_t readRawPressure() { write8(BMP180_CONTROL, BMP180_READPRESSURECMD + (oversampling << 6)); // Wait for conversion based on oversampling setting if (oversampling == BMP180_ULTRALOWPOWER) delay(5); else if (oversampling == BMP180_STANDARD) delay(8); else if (oversampling == BMP180_HIGHRES) delay(14); else delay(26); uint32_t MSB = read8(BMP180_PRESSUREDATA); uint32_t LSB = read8(BMP180_PRESSUREDATA + 1); uint32_t XLSB = read8(BMP180_PRESSUREDATA + 2); // Combine readings with proper shifting based on oversampling uint32_t raw = ((MSB << 16) + (LSB << 8) + XLSB) >> (8 - oversampling); return raw; } int32_t readPressure() { // Read raw temperature value first int32_t UT = readRawTemperature(); // Then read raw pressure value int32_t UP = readRawPressure(); // Temperature compensation int32_t B5 = computeB5(UT); // Do pressure calculations (straight from Adafruit code) int32_t B6 = B5 - 4000; int32_t X1 = ((int32_t)b2 * ((B6 * B6) >> 12)) >> 11; int32_t X2 = ((int32_t)ac2 * B6) >> 11; int32_t X3 = X1 + X2; int32_t B3 = ((((int32_t)ac1 * 4 + X3) << oversampling) + 2) / 4; X1 = ((int32_t)ac3 * B6) >> 13; X2 = ((int32_t)b1 * ((B6 * B6) >> 12)) >> 16; X3 = ((X1 + X2) + 2) >> 2; uint32_t B4 = ((uint32_t)ac4 * (uint32_t)(X3 + 32768)) >> 15; uint32_t B7 = ((uint32_t)UP - B3) * (uint32_t)(50000UL >> oversampling); int32_t p; if (B7 < 0x80000000) { p = (B7 * 2) / B4; } else { p = (B7 / B4) * 2; } X1 = (p >> 8) * (p >> 8); X1 = (X1 * 3038) >> 16; X2 = (-7357 * p) >> 16; p = p + ((X1 + X2 + (int32_t)3791) >> 4); return p; // Pressure in Pa } // Calculate altitude based on atmospheric pressure float readAltitude(float seaLevelPressure) { float pressure = readPressure(); float altitude = 44330 * (1.0 - pow(pressure / seaLevelPressure, 0.1903)); return altitude; }
* am setat frecventa la ceasul de transmisie i2c mai jos pt a compensa rezistentele de pullup puse in paralel
* Jocuri calibrabile si modificabile
* Un workflow nedependent de main loop
Fişierele se încarcă pe wiki folosind facilitatea Add Images or other files. Namespace-ul în care se încarcă fişierele este de tipul :pm:prj20??:c? sau :pm:prj20??:c?:nume_student (dacă este cazul). Exemplu: Dumitru Alin, 331CC → :pm:prj2009:cc:dumitru_alin.
Laburi folosite:
- GPIO
- Timere (flow-ul jocului)
- Intreruperi (butoane/fire)
- I2C (senzorii de temp si de giroscop comunica in tandem pe magistrala SDA SCL conectate la Pinii 25 si 26)