#include #include #include #include // ── Car fixed MAC ───────────────────────────────────────────── uint8_t carMAC[] = {0x36, 0x33, 0x33, 0x33, 0x33, 0x33}; // ── Pin Definitions ─────────────────────────────────────────── #define PIN_VRX 2 #define PIN_VRY 3 #define PIN_BATT 4 #define PIN_SW 5 #define PIN_SDA 6 #define PIN_SCL 7 #define PIN_POWER 21 #define PIN_LED 10 // ── OLED ────────────────────────────────────────────────────── #define SCREEN_W 128 #define SCREEN_H 64 Adafruit_SSD1306 display(SCREEN_W, SCREEN_H, &Wire, -1); // ── Game config ─────────────────────────────────────────────── #define TOTAL_COINS 5 // ── Joystick Calibration Constants ──────────────────────────── #define JOY_CENTER 2048 // Center position of a 12-bit ADC #define JOY_DEADZONE 500 // Increase this value to require MORE physical stick movement // ── Payloads ────────────────────────────────────────────────── typedef struct { int vrx; int vry; bool sw; float battPct; } JoyData; typedef struct { int coins; int timeLeft; bool coinJustCollected; bool win; bool gameover; bool waiting; unsigned long winTimeMs; } CarData; JoyData joyData; CarData carData; volatile bool newCarData = false; // ── ESP-NOW callbacks ───────────────────────────────────────── void onSent(const wifi_tx_info_t *info, esp_now_send_status_t status) { } void onReceive(const esp_now_recv_info_t *info, const uint8_t *data, int len) { if (len == sizeof(CarData)) { memcpy(&carData, data, sizeof(CarData)); newCarData = true; } } // ── Battery percentage ──────────────────────────────────────── float readBattPct() { int raw = analogRead(PIN_BATT); float voltage = raw * (3.3f / 4095.0f) * 3.0f; float pct = (voltage - 6.0f) / (8.4f - 6.0f) * 100.0f; return constrain(pct, 0.0f, 100.0f); } // ── Draw faces ──────────────────────────────────────────────── void drawSadFace(int cx, int cy) { display.drawCircle(cx, cy, 14, SSD1306_WHITE); display.fillCircle(cx - 5, cy - 4, 2, SSD1306_WHITE); display.fillCircle(cx + 5, cy - 4, 2, SSD1306_WHITE); for (int x = -6; x <= 6; x++) { int y = cy + 6 + (x * x) / 8; display.drawPixel(cx + x, y, SSD1306_WHITE); } } void drawHappyFace(int cx, int cy) { display.drawCircle(cx, cy, 14, SSD1306_WHITE); display.fillCircle(cx - 5, cy - 4, 2, SSD1306_WHITE); display.fillCircle(cx + 5, cy - 4, 2, SSD1306_WHITE); for (int x = -6; x <= 6; x++) { int y = cy + 8 - (x * x) / 8; display.drawPixel(cx + x, y, SSD1306_WHITE); } } // ── OLED screens ────────────────────────────────────────────── void showWaiting() { display.clearDisplay(); display.setTextSize(1); display.setTextColor(SSD1306_WHITE); display.setCursor(20, 5); display.println("COIN COLLECTOR"); display.setCursor(10, 17); display.setTextSize(2); display.println(" Press SW to start"); display.setTextColor(SSD1306_BLACK, SSD1306_WHITE); display.setTextSize(1); display.setCursor(30, 55); display.println(" Good luck! "); display.display(); } void showPlaying(int coins, int timeLeft, float batt) { display.clearDisplay(); display.setTextColor(SSD1306_WHITE); // Timer countdown (big) display.setTextSize(2); display.setCursor(0, 0); int m = timeLeft / 60; int s = timeLeft % 60; display.printf("%02d:%02d", m, s); // Coins count display.setTextSize(1); display.setCursor(80, 4); display.printf("C:%d/%d", coins, TOTAL_COINS); // Battery display.setCursor(80, 16); display.printf("B:%.0f%%", batt); // Divider display.drawLine(0, 25, 128, 25, SSD1306_WHITE); // Coin icons display.setCursor(5, 30); display.setTextSize(1); display.print("Coins:"); for (int i = 0; i < TOTAL_COINS; i++) { int x = 12 + i * 24; int y = 42; if (i < coins) { display.fillCircle(x, y+6, 9, SSD1306_WHITE); display.setTextColor(SSD1306_BLACK); display.setCursor(x - 3, y+3); display.print(i + 1); display.setTextColor(SSD1306_WHITE); } else { display.drawCircle(x, y+6, 9, SSD1306_WHITE); display.setCursor(x - 3, y+3); display.print(i + 1); } } display.display(); } void showGameOver(int coins) { display.clearDisplay(); display.setTextColor(SSD1306_WHITE); drawSadFace(108, 22); display.setTextSize(1); display.setCursor(0, 0); display.println("GAME OVER :("); display.drawLine(0, 12, 90, 12, SSD1306_WHITE); display.setCursor(0, 18); display.printf("Coins: %d/%d", coins, TOTAL_COINS); display.setCursor(0, 32); display.println("Better luck"); display.println("next time!"); display.setCursor(0, 52); display.println("SW = restart"); display.display(); } void showWin(unsigned long winMs, int coins) { display.clearDisplay(); display.setTextColor(SSD1306_WHITE); drawHappyFace(108, 22); display.setTextSize(1); display.setCursor(0, 0); display.println("YOU WON! :)"); display.drawLine(0, 12, 90, 12, SSD1306_WHITE); display.setCursor(0, 18); int m = (winMs / 1000) / 60; int s = (winMs / 1000) % 60; int ms = (winMs % 1000) / 10; display.printf("Time:%02d:%02d.%02d", m, s, ms); display.setCursor(0, 32); display.printf("All %d coins!", coins); display.setCursor(0, 46); display.println("Congratulations!"); display.setCursor(0, 56); display.println("SW = play again"); display.display(); } void setup() { Serial.begin(115200); pinMode(PIN_BATT, INPUT); pinMode(PIN_SW, INPUT_PULLUP); pinMode(PIN_POWER, INPUT_PULLUP); pinMode(PIN_LED, OUTPUT); digitalWrite(PIN_LED, HIGH); Wire.begin(PIN_SDA, PIN_SCL); display.begin(SSD1306_SWITCHCAPVCC, 0x3C); display.setTextColor(SSD1306_WHITE); display.clearDisplay(); display.setCursor(0, 0); display.setTextSize(1); display.println("Starting..."); display.display(); WiFi.mode(WIFI_STA); esp_now_init(); esp_now_register_send_cb(onSent); esp_now_register_recv_cb(onReceive); esp_now_peer_info_t peer = {}; memcpy(peer.peer_addr, carMAC, 6); peer.channel = 0; peer.encrypt = false; esp_now_add_peer(&peer); showWaiting(); } void loop() { // ── Power switch ────────────────────────────────────────── if (digitalRead(PIN_POWER) == LOW) { display.clearDisplay(); display.display(); display.ssd1306_command(SSD1306_DISPLAYOFF); digitalWrite(PIN_LED, LOW); joyData.vrx = 2139; joyData.vry = 2248; joyData.sw = false; joyData.battPct = 0; esp_now_send(carMAC, (uint8_t*)&joyData, sizeof(joyData)); while (digitalRead(PIN_POWER) == LOW) { delay(100); } display.ssd1306_command(SSD1306_DISPLAYON); digitalWrite(PIN_LED, HIGH); showWaiting(); delay(300); } // ── Read raw joystick values ────────────────────────────── int rawX = analogRead(PIN_VRX); int rawY = analogRead(PIN_VRY); // ── Apply Deadzone Filter ───────────────────────────────── // If the movement is small (inside the deadzone), force it to center if (abs(rawX - JOY_CENTER) < JOY_DEADZONE) { joyData.vrx = JOY_CENTER; } else { joyData.vrx = rawX; } if (abs(rawY - JOY_CENTER) < JOY_DEADZONE) { joyData.vry = JOY_CENTER; } else { joyData.vry = rawY; } joyData.sw = (digitalRead(PIN_SW) == LOW); joyData.battPct = readBattPct(); esp_now_send(carMAC, (uint8_t*)&joyData, sizeof(joyData)); // ── Handle incoming car data ────────────────────────────── if (newCarData) { newCarData = false; if (carData.coinJustCollected) { for (int i = 0; i < 3; i++) { digitalWrite(PIN_LED, LOW); delay(150); digitalWrite(PIN_LED, HIGH); delay(150); } } if (carData.waiting) { showWaiting(); } else if (carData.win) { showWin(carData.winTimeMs, carData.coins); } else if (carData.gameover) { showGameOver(carData.coins); } else { showPlaying(carData.coins, carData.timeLeft, joyData.battPct); } } delay(50); }