running version of Runner Game with FlipDot-Emu and GameController

This commit is contained in:
Marcel Walter 2025-10-04 21:02:39 +02:00
parent 869769dfc4
commit 4ad8e72a11
11 changed files with 972 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
.pio
.vscode/.browse.c_cpp.db*
.vscode/c_cpp_properties.json
.vscode/launch.json
.vscode/ipch
/.vscode/extensions.json

33
include/FlipDotDrv.h Normal file
View File

@ -0,0 +1,33 @@
#pragma once
#include <Arduino.h>
#include <Adafruit_GFX.h>
class FlipDotDrv
{
private:
static constexpr uint8_t templateFrameStart {0x02};
static constexpr uint8_t templateFrameAddressByte1 {'1'};
static constexpr uint8_t templateFrameAddressByte2 {'0'};
uint8_t FrameResolution[2] {};
uint8_t *FrameData {nullptr};
static constexpr uint8_t templateFrameEnd {0x03};
uint8_t FrameCrc[2] {};
uint32_t width;
uint32_t height;
uint8_t address;
uint8_t *buffer {nullptr};
uint16_t FrameDataLength;
uint16_t bufferLength;
public:
FlipDotDrv(uint8_t width, uint8_t height, uint8_t address);
~FlipDotDrv();
void sendRaw(const uint8_t* bmp, uint16_t length);
void sendCanvas(const GFXcanvas1 * canv);
};

43
include/GameRect.h Normal file
View File

@ -0,0 +1,43 @@
#pragma once
#include <Arduino.h>
class GameRect {
private:
int16_t x;
int16_t y;
int16_t w;
int16_t h;
public:
GameRect(int16_t x = 0, int16_t y = 0, int16_t w = 0, int16_t h = 0)
: x(x), y(y), w(w), h(h) {}
// Getter
int16_t getX() const { return x; }
int16_t getY() const { return y; }
int16_t getWidth() const { return w; }
int16_t getHeight() const { return h; }
// Setter
void setX(int16_t nx) { x = nx; }
void setY(int16_t ny) { y = ny; }
void setWidth(int16_t nw) { w = nw; }
void setHeight(int16_t nh) { h = nh; }
void set(int16_t nx, int16_t ny, int16_t nw, int16_t nh) {
x = nx; y = ny; w = nw; h = nh;
}
// Kollisionserkennung (wie pygame.Rect.colliderect)
bool collideRect(const GameRect& other) const {
return !(x + w <= other.x || // links von other
x >= other.x + other.w || // rechts von other
y + h <= other.y || // über other
y >= other.y + other.h); // unter other
}
// Punkt-in-Rechteck Test
bool contains(int16_t px, int16_t py) const {
return (px >= x && px < x + w && py >= y && py < y + h);
}
};

14
include/ITrigger.h Normal file
View File

@ -0,0 +1,14 @@
#pragma once
#include <Arduino.h>
class ITrigger
{
public:
ITrigger(/* args */) = default;
~ITrigger() = default;
virtual void printText(String text) = 0;
virtual void printOpen() = 0;
virtual void printClose() = 0;
};

39
include/README Normal file
View File

@ -0,0 +1,39 @@
This directory is intended for project header files.
A header file is a file containing C declarations and macro definitions
to be shared between several project source files. You request the use of a
header file in your project source file (C, C++, etc) located in `src` folder
by including it, with the C preprocessing directive `#include'.
```src/main.c
#include "header.h"
int main (void)
{
...
}
```
Including a header file produces the same results as copying the header file
into each source file that needs it. Such copying would be time-consuming
and error-prone. With a header file, the related declarations appear
in only one place. If they need to be changed, they can be changed in one
place, and programs that include the header file will automatically use the
new version when next recompiled. The header file eliminates the labor of
finding and changing all the copies as well as the risk that a failure to
find one copy will result in inconsistencies within a program.
In C, the usual convention is to give header files names that end with `.h'.
It is most portable to use only letters, digits, dashes, and underscores in
header file names, and at most one dot.
Read more about using header files in official GCC documentation:
* Include Syntax
* Include Operation
* Once-Only Headers
* Computed Includes
https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html

29
include/Telegram.h Normal file
View File

@ -0,0 +1,29 @@
#pragma once
#include <Arduino.h>
#include <UniversalTelegramBot.h>
#include <WiFiClientSecure.h>
#include "ITrigger.h"
class Telegram
{
private:
static const char *BOT_TOKEN;
static const unsigned long BOT_MTBS; // mean time between scan messages
unsigned long bot_lasttime; // last time messages' scan has been done
ITrigger &trigger;
WiFiClientSecure &secured_client;
UniversalTelegramBot bot;
void handleNewMessages(int numNewMessages);
public:
Telegram(WiFiClientSecure &sec_client, ITrigger &trig);
~Telegram();
void init();
void cyclic(unsigned long now);
};

46
lib/README Normal file
View File

@ -0,0 +1,46 @@
This directory is intended for project specific (private) libraries.
PlatformIO will compile them to static libraries and link into executable file.
The source code of each library should be placed in an own separate directory
("lib/your_library_name/[here are source files]").
For example, see a structure of the following two libraries `Foo` and `Bar`:
|--lib
| |
| |--Bar
| | |--docs
| | |--examples
| | |--src
| | |- Bar.c
| | |- Bar.h
| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html
| |
| |--Foo
| | |- Foo.c
| | |- Foo.h
| |
| |- README --> THIS FILE
|
|- platformio.ini
|--src
|- main.c
and a contents of `src/main.c`:
```
#include <Foo.h>
#include <Bar.h>
int main (void)
{
...
}
```
PlatformIO Library Dependency Finder will find automatically dependent
libraries scanning project source files.
More information about PlatformIO Library Dependency Finder
- https://docs.platformio.org/page/librarymanager/ldf.html

21
platformio.ini Normal file
View File

@ -0,0 +1,21 @@
; PlatformIO Project Configuration File
;
; Build options: build flags, source filter
; Upload options: custom upload port, speed and extra flags
; Library options: dependencies, extra library storages
; Advanced options: extra scripting
;
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html
[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
monitor_speed = 115200
lib_deps =
;witnessmenow/UniversalTelegramBot @ ^1.3.0
adafruit/Adafruit GFX Library @ ^1.11.11
build_flags=-DTELEGRAM_DEBUG
; upload_port = COM9
; monitor_port = COM9

101
src/FlipDotDrv.cpp Normal file
View File

@ -0,0 +1,101 @@
#include "FlipDotDrv.h"
#include <Fonts/FreeSans9pt7b.h>
FlipDotDrv::FlipDotDrv(uint8_t width, uint8_t height, uint8_t address)
{
this->width = width;
this->height = height;
this->address = address;
this->FrameDataLength = (width * height / 8 * 2);
this->bufferLength = 5 + FrameDataLength + 3;
this->buffer = new uint8_t[this->bufferLength];
this->FrameData = this->buffer + 5;
pinMode(5,OUTPUT);
digitalWrite(5, LOW);
Serial2.begin(4800, SERIAL_8N1, 16, 17); // (unsigned long baud, uint32_t config = 134217756U, int8_t rxPin = (int8_t)(-1), int8_t txPin = (int8_t)(-1), bool invert = false, unsigned long timeout_ms = 20000UL, uint8_t rxfifo_full_thrhd = (uint8_t)112U)
}
FlipDotDrv::~FlipDotDrv()
{
free(this->FrameData);
}
void FlipDotDrv::sendRaw(const uint8_t* bmp, uint16_t length)
{
char tmpBuff[3] = {0};
uint8_t chkSum = 0;
if (length != (this->width * this->height / 8))
{
Serial.print("length ");
Serial.print(length);
Serial.println(" [Bytes] of bitmap is invalid");
return;
}
this->buffer[0] = templateFrameStart;
this->buffer[1] = templateFrameAddressByte1;
this->buffer[2] = templateFrameAddressByte2 + this->address;
chkSum += this->buffer[1];
chkSum += this->buffer[2];
snprintf(tmpBuff, sizeof(tmpBuff), "%02X", (this->width * this->height / 8));
this->buffer[3] = tmpBuff[0];
this->buffer[4] = tmpBuff[1];
chkSum += this->buffer[3];
chkSum += this->buffer[4];
for (uint16_t i = 0; i < length; ++i)
{
snprintf(tmpBuff, sizeof(tmpBuff), "%02X", bmp[i]);
this->FrameData[i * 2 + 0] = tmpBuff[0];
this->FrameData[i * 2 + 1] = tmpBuff[1];
chkSum += tmpBuff[0];
chkSum += tmpBuff[1];
}
this->buffer[5 + FrameDataLength + 0] = templateFrameEnd;
chkSum += this->buffer[5 + FrameDataLength + 0];
chkSum ^= 0xff;
chkSum += 1;
snprintf(tmpBuff, sizeof(tmpBuff), "%02X", chkSum);
this->buffer[5 + FrameDataLength + 1] = tmpBuff[0];
this->buffer[5 + FrameDataLength + 2] = tmpBuff[1];
// digitalWrite(5, HIGH);
// delay(200);
Serial2.write(this->buffer, this->bufferLength);
Serial2.flush();
// delay(1000);
// digitalWrite(5, LOW);
}
void FlipDotDrv::sendCanvas(const GFXcanvas1 * canv)
{
GFXcanvas1 localCanvas(width, height, true);
memcpy(localCanvas.getBuffer(), canv->getBuffer(), width * height / 8);
uint8_t displayBuffer[width * height / 8] = {};
for (int i = 0; i < (this->width * this->height / 8); ++i)
{
displayBuffer[i] = (localCanvas.getPixel(i / 2, ((i % 2) * 8) + 0) << 0|
localCanvas.getPixel(i / 2, ((i % 2) * 8) + 1) << 1|
localCanvas.getPixel(i / 2, ((i % 2) * 8) + 2) << 2|
localCanvas.getPixel(i / 2, ((i % 2) * 8) + 3) << 3|
localCanvas.getPixel(i / 2, ((i % 2) * 8) + 4) << 4|
localCanvas.getPixel(i / 2, ((i % 2) * 8) + 5) << 5|
localCanvas.getPixel(i / 2, ((i % 2) * 8) + 6) << 6|
localCanvas.getPixel(i / 2, ((i % 2) * 8) + 7) << 7 );
}
this->sendRaw(displayBuffer, sizeof(displayBuffer));
}

629
src/main.cpp Normal file
View File

@ -0,0 +1,629 @@
#include <Arduino.h>
#include <Adafruit_GFX.h>
#include "FlipDotDrv.h"
#include "GameRect.h"
#include <esp_now.h>
#include <WiFi.h>
// ===== Konstanten =====
#define VERSION "v14_ESP32"
#define WIDTH 96
#define HEIGHT 16
#define FPS 30
#define FRAME_TIME_MS (1000 / FPS)
#define DISPLAY_REFRESH_TIME 1700
// ===== Event Types =====
enum GameEvent {
EVENT_NONE = 0,
EVENT_PLAYER1,
EVENT_PLAYER2,
EVENT_PLAYER3,
EVENT_PLAYER4,
EVENT_ESCAPE
};
// ===== Game States =====
enum GameState {
STATE_INIT0,
STATE_INIT1,
STATE_INIT2,
STATE_INIT3,
STATE_START,
STATE_INIT_RACE,
STATE_COUNTDOWN5,
STATE_COUNTDOWN4,
STATE_COUNTDOWN3,
STATE_COUNTDOWN2,
STATE_COUNTDOWN1,
STATE_COUNTDOWN0,
STATE_RACE,
STATE_FINISH,
STATE_DELAY
};
// ===== Klassen =====
class Runner {
private:
int16_t posy;
int16_t sizey;
int16_t sizex;
GameRect rect;
public:
Runner(int16_t y, int16_t sy)
: posy(y), sizey(sy), sizex(2) {
rect.set(0, posy, sizex, sizey);
}
void update(int16_t inc) {
sizex += inc;
rect.set(0, posy, sizex, sizey);
}
void reset() {
sizex = 2;
rect.set(0, posy, sizex, sizey);
}
const GameRect& getRect() const { return rect; }
void display(GFXcanvas1& canvas) {
canvas.fillRect(0, posy, sizex, sizey, 1);
}
};
class Wall {
private:
int16_t posx, posy;
int16_t sizex, sizey;
GameRect rect;
public:
Wall(int16_t x, int16_t y, int16_t sx, int16_t sy)
: posx(x), posy(y), sizex(sx), sizey(sy) {
rect.set(posx, posy, sizex, sizey);
}
const GameRect& getRect() const { return rect; }
void display(GFXcanvas1& canvas) {
// Schachbrett-Muster wie im Original
for (int16_t line = 0; line < sizey; line++) {
for (int16_t col = 0; col < sizex; col++) {
if ((((line % 2) + col) % 2) == 0) {
canvas.drawPixel(posx + col, posy + line, 1);
}
}
}
}
};
// ===== Globale Variablen =====
FlipDotDrv display(WIDTH, HEIGHT, 0);
GFXcanvas1 canvas(WIDTH, HEIGHT);
Runner* runner1;
Runner* runner2;
Runner* runner3;
Runner* runner4;
Wall* wall1;
Wall* wall2;
Wall* wall3;
Wall* goal;
GameState gameState = STATE_INIT0;
GameState delayedState = STATE_INIT0;
unsigned long delayTimestamp = 0;
unsigned long lastActionTime = 0;
unsigned long lastFrameTime = 0;
uint8_t winner = 0;
// GameEvent currentEvent = EVENT_NONE;
// ===== Event-Handler (von extern aufzurufen) =====
QueueHandle_t xEventQueue = NULL;
void postEvent(GameEvent event) {
if (xEventQueue)
{
if (xQueueSend(xEventQueue, ( void * ) &event, 0) == errQUEUE_FULL)
{
}
}
}
// ===== Hilfsfunktionen =====
void displayRaceTrack() {
wall1->display(canvas);
wall2->display(canvas);
wall3->display(canvas);
goal->display(canvas);
runner1->display(canvas);
runner2->display(canvas);
runner3->display(canvas);
runner4->display(canvas);
}
void displayText(const char* text, bool background, int16_t x = -1, int16_t y = -1) {
// canvas.fillScreen(0);
canvas.setFont(NULL);
canvas.setTextSize(1);
canvas.setTextColor(1);
int16_t cursor_x, cursor_y;
int16_t bounds_x, bounds_y;
uint16_t bounds_w, bounds_h;
canvas.getTextBounds(text, 0, 0, &bounds_x, &bounds_y, &bounds_w, &bounds_h);
if (x == -1)
{
// Zentriert
cursor_x = (WIDTH - bounds_w) / 2;
}
else
{
cursor_x = x;
}
if (y == -1)
{
// Zentriert
cursor_y = (HEIGHT - bounds_h) / 2;
}
else
{
cursor_y = y;
}
if (background)
{
canvas.fillRect(cursor_x - 1, cursor_y - 1, bounds_w + 1, bounds_h + 1, 0);
}
canvas.setCursor(cursor_x, cursor_y);
canvas.print(text);
}
void displayRefreshTask(void *);
void gameTask(void *);
void ioTask(void *);
void OnDataRecv(const uint8_t *mac, const uint8_t *incomingData, int len);
void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status);
// ===== Setup =====
void setup() {
Serial.begin(115200);
Serial.println("Runner Game " VERSION);
// Objekte erstellen
runner1 = new Runner(0, 1);
runner2 = new Runner(5, 1);
runner3 = new Runner(10, 1);
runner4 = new Runner(15, 1);
wall1 = new Wall(0, 2, WIDTH - 3, 2);
wall2 = new Wall(0, 7, WIDTH - 3, 2);
wall3 = new Wall(0, 12, WIDTH - 3, 2);
goal = new Wall(WIDTH - 2, 0, 2, HEIGHT);
// Initialisierung
gameState = STATE_INIT0;
lastFrameTime = millis();
lastActionTime = millis();
WiFi.mode(WIFI_STA);
Serial.print("Game ESP MAC Address: ");
Serial.println(WiFi.macAddress());
Serial.println(">>> Diese MAC-Adresse im Sender eintragen! <<<");
// ESP-NOW initialisieren
if (esp_now_init() != ESP_OK) {
Serial.println("ESP-NOW init failed");
return;
}
// Empfangs-Callback registrieren
esp_now_register_recv_cb(OnDataRecv);
esp_now_register_send_cb(OnDataSent);
Serial.println("ESP-NOW ready to receive");
Serial.println("Setup complete");
xEventQueue = xQueueCreate( 10, sizeof(GameEvent) );
xTaskCreate(gameTask, "GameTask", 4096, nullptr, 10, nullptr);
xTaskCreate(ioTask, "ioTask", 2048, nullptr, 7, nullptr);
xTaskCreate(displayRefreshTask, "displayRefreshTask", 2048, nullptr, 11, nullptr);
}
// ===== Beispiel für Button/WiFi/ESPNow Integration =====
// Diese Funktionen kannst du von deinem Input-Code aufrufen:
void onPlayer1Button() {
postEvent(EVENT_PLAYER1);
}
void onPlayer2Button() {
postEvent(EVENT_PLAYER2);
}
void onPlayer3Button() {
postEvent(EVENT_PLAYER3);
}
void onPlayer4Button() {
postEvent(EVENT_PLAYER4);
}
void onEscapeButton() {
postEvent(EVENT_ESCAPE);
}
// ===== Main Loop =====
void loop()
{
unsigned long currentTime = millis();
}
void displayRefreshTask(void *)
{
static TickType_t xLastWakeTime;
uint32_t t0;
uint32_t t1;
uint32_t t2;
uint32_t t3;
for (;;)
{
// t1 = millis();
display.sendCanvas(&canvas);
// t2 = millis();
xTaskDelayUntil(&xLastWakeTime, DISPLAY_REFRESH_TIME);
// t3 = millis();
// Serial.print("-- sent ");
// Serial.println(t2-t1);
// Serial.print("-- slept ");
// Serial.println(t3-t2);
// Serial.print("-- cycle ");
// Serial.println(t1-t0);
// t0 = t1;
}
}
void gameTask(void *)
{
for (;;)
{
unsigned long currentTime = millis();
if (currentTime - lastFrameTime < FRAME_TIME_MS) {
vTaskDelay(2);
continue;
}
lastFrameTime = currentTime;
// Event-Verarbeitung in der State Machine
GameEvent event;
do{
if( xQueueReceive( xEventQueue, &( event ), ( TickType_t ) 0 ) != pdPASS )
{
event = EVENT_NONE; // Event konsumiert
}
// // State Machine
switch (gameState) {
case STATE_INIT0:
canvas.fillScreen(1);
displayText("Init " VERSION, true);
Serial.println("Init " VERSION);
delayedState = STATE_INIT1;
delayTimestamp = currentTime + 3 * DISPLAY_REFRESH_TIME;
gameState = STATE_DELAY;
break;
case STATE_INIT1:
// Schachbrett-Muster
canvas.fillScreen(0);
for (int16_t y = 0; y < HEIGHT; y++) {
for (int16_t x = 0; x < WIDTH; x++) {
if ((((y % 2) + x) % 2) == 0) {
canvas.drawPixel(x, y, 1);
}
}
}
delayedState = STATE_INIT2;
delayTimestamp = currentTime + 3 * DISPLAY_REFRESH_TIME;
gameState = STATE_DELAY;
break;
case STATE_INIT2:
// Invertiertes Schachbrett
canvas.fillScreen(0);
for (int16_t y = 0; y < HEIGHT; y++) {
for (int16_t x = 0; x < WIDTH; x++) {
if ((((y % 2) + x) % 2) == 1) {
canvas.drawPixel(x, y, 1);
}
}
}
delayedState = STATE_INIT3;
delayTimestamp = currentTime + 3 * DISPLAY_REFRESH_TIME;
gameState = STATE_DELAY;
break;
case STATE_INIT3:
canvas.fillScreen(1);
delayedState = STATE_START;
delayTimestamp = currentTime + 3 * DISPLAY_REFRESH_TIME;
gameState = STATE_DELAY;
break;
case STATE_START:
canvas.fillScreen(0);
displayText("Knopf druecken!", false);
if (event >= EVENT_PLAYER1 && event <= EVENT_PLAYER4) {
gameState = STATE_INIT_RACE;
lastActionTime = currentTime;
}
break;
case STATE_INIT_RACE:
Serial.println("Starting race");
runner1->reset();
runner2->reset();
runner3->reset();
runner4->reset();
winner = 0;
gameState = STATE_COUNTDOWN5;
break;
case STATE_COUNTDOWN5:
canvas.fillScreen(0);
displayRaceTrack();
displayText("5", true, WIDTH / 2 + 30, HEIGHT / 2);
Serial.println("5");
delayedState = STATE_COUNTDOWN4;
delayTimestamp = currentTime + 1 * DISPLAY_REFRESH_TIME;
gameState = STATE_DELAY;
break;
case STATE_COUNTDOWN4:
canvas.fillScreen(0);
displayRaceTrack();
displayText("4", true, WIDTH / 2 + 15, HEIGHT / 2);
Serial.println("4");
delayedState = STATE_COUNTDOWN3;
delayTimestamp = currentTime + 1 * DISPLAY_REFRESH_TIME;
gameState = STATE_DELAY;
break;
case STATE_COUNTDOWN3:
canvas.fillScreen(0);
displayRaceTrack();
displayText("3", true, WIDTH / 2, HEIGHT / 2);
Serial.println("3");
delayedState = STATE_COUNTDOWN2;
delayTimestamp = currentTime + 1 * DISPLAY_REFRESH_TIME;
gameState = STATE_DELAY;
break;
case STATE_COUNTDOWN2:
canvas.fillScreen(0);
displayRaceTrack();
displayText("2", true, WIDTH / 2 - 15, HEIGHT / 2);
Serial.println("2");
delayedState = STATE_COUNTDOWN1;
delayTimestamp = currentTime + 1 * DISPLAY_REFRESH_TIME;
gameState = STATE_DELAY;
break;
case STATE_COUNTDOWN1:
canvas.fillScreen(0);
displayRaceTrack();
displayText("1", true, WIDTH / 2 - 30, HEIGHT / 2);
Serial.println("1");
delayedState = STATE_COUNTDOWN0;
delayTimestamp = currentTime + 1 * DISPLAY_REFRESH_TIME;
gameState = STATE_DELAY;
break;
case STATE_COUNTDOWN0:
canvas.fillScreen(0);
displayRaceTrack();
displayText("drueckt!", true, WIDTH / 2 - 40, 1);
displayText("schnell!", true, WIDTH / 2 - 4, 8);
// Erste Eingabe startet Rennen
if (event == EVENT_PLAYER1) {
runner1->update(1);
gameState = STATE_RACE;
lastActionTime = currentTime;
} else if (event == EVENT_PLAYER2) {
runner2->update(1);
gameState = STATE_RACE;
lastActionTime = currentTime;
} else if (event == EVENT_PLAYER3) {
runner3->update(1);
gameState = STATE_RACE;
lastActionTime = currentTime;
} else if (event == EVENT_PLAYER4) {
runner4->update(1);
gameState = STATE_RACE;
lastActionTime = currentTime;
} else if (event == EVENT_ESCAPE) {
gameState = STATE_START;
lastActionTime = currentTime;
}
break;
case STATE_RACE:
// Event-Handling
if (event == EVENT_PLAYER1) {
runner1->update(1);
lastActionTime = currentTime;
} else if (event == EVENT_PLAYER2) {
runner2->update(1);
lastActionTime = currentTime;
} else if (event == EVENT_PLAYER3) {
runner3->update(1);
lastActionTime = currentTime;
} else if (event == EVENT_PLAYER4) {
runner4->update(1);
lastActionTime = currentTime;
} else if (event == EVENT_ESCAPE) {
gameState = STATE_START;
lastActionTime = currentTime;
break;
}
// Kollisionsprüfung
if (goal->getRect().collideRect(runner1->getRect())) {
winner = 1;
gameState = STATE_FINISH;
} else if (goal->getRect().collideRect(runner2->getRect())) {
winner = 2;
gameState = STATE_FINISH;
} else if (goal->getRect().collideRect(runner3->getRect())) {
winner = 3;
gameState = STATE_FINISH;
} else if (goal->getRect().collideRect(runner4->getRect())) {
winner = 4;
gameState = STATE_FINISH;
}
// Leerlauf-Timer
if (currentTime - lastActionTime > 30000) {
gameState = STATE_START;
lastActionTime = currentTime;
} else if (currentTime - lastActionTime > 20000) {
canvas.fillScreen(0);
displayRaceTrack();
char txt[20];
snprintf(txt, sizeof(txt), "Idle %d", (int)(30 - (currentTime - lastActionTime) / 1000));
displayText(txt, WIDTH / 2 - 20, HEIGHT / 2);
Serial.println(txt);
} else {
canvas.fillScreen(0);
displayRaceTrack();
}
break;
case STATE_FINISH:
canvas.fillScreen(0);
char winnerText[20];
snprintf(winnerText, sizeof(winnerText), "Gewinner P%d!", winner);
displayText(winnerText, false);
Serial.print("Winner: ");
Serial.println(winner);
delayedState = STATE_START;
delayTimestamp = currentTime + 5 * DISPLAY_REFRESH_TIME;
gameState = STATE_DELAY;
break;
case STATE_DELAY:
if (currentTime >= delayTimestamp) {
gameState = delayedState;
}
if (event == EVENT_ESCAPE) {
gameState = STATE_START;
}
break;
}
} while (event != EVENT_NONE);
}
}
void ioTask(void *)
{
static TickType_t xLastWakeTime;
static bool old = false;
for (;;)
{
if (!digitalRead(GPIO_NUM_0) and old)
{
old = false;
onPlayer1Button();
Serial.println("Press");
}
else if (digitalRead(GPIO_NUM_0) and !old)
{
old = true;
Serial.println("Release");
}
xTaskDelayUntil( &xLastWakeTime, 10);
}
}
#define MAX_EVENTS_PER_PACKET 20
struct ButtonPacket {
uint8_t eventCount;
uint8_t playerIds[MAX_EVENTS_PER_PACKET];
uint32_t timestamps[MAX_EVENTS_PER_PACKET];
};
// ===== ESP-NOW Empfangs-Callback =====
// ===== ESP-NOW Callback =====
void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status)
{
if (status == ESP_NOW_SEND_SUCCESS)
{
Serial.println("Send OK");
}
else
{
Serial.println("Send FAIL");
}
}
void OnDataRecv(const uint8_t *mac, const uint8_t *incomingData, int len)
{
if ((len == 4) && (incomingData[0] == 'P') && (incomingData[0] == 'I') && (incomingData[0] == 'N') && (incomingData[0] == 'G'))
{
esp_err_t result = esp_now_send(mac, (uint8_t *)"PONG", 4);
}
if (len != sizeof(ButtonPacket)) {
Serial.println("Invalid packet size");
return;
}
ButtonPacket packet;
memcpy(&packet, incomingData, sizeof(ButtonPacket));
Serial.print("Received ");
Serial.print(packet.eventCount);
Serial.println(" events");
// Alle Events im Paket verarbeiten
for (int i = 0; i < packet.eventCount; i++) {
uint8_t playerId = packet.playerIds[i];
Serial.print(" Player ");
Serial.print(playerId);
Serial.print(" @ ");
Serial.println(packet.timestamps[i]);
// Event ins Game-System einspeisen
switch (playerId) {
case 1: postEvent(EVENT_PLAYER1); break;
case 2: postEvent(EVENT_PLAYER2); break;
case 3: postEvent(EVENT_PLAYER3); break;
case 4: postEvent(EVENT_PLAYER4); break;
default:
Serial.println("Invalid player ID");
break;
}
}
}

11
test/README Normal file
View File

@ -0,0 +1,11 @@
This directory is intended for PlatformIO Test Runner and project tests.
Unit Testing is a software testing method by which individual units of
source code, sets of one or more MCU program modules together with associated
control data, usage procedures, and operating procedures, are tested to
determine whether they are fit for use. Unit testing finds problems early
in the development cycle.
More information about PlatformIO Unit Testing:
- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html