running version of Runner Game with FlipDot-Emu and GameController
This commit is contained in:
parent
869769dfc4
commit
4ad8e72a11
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal 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
33
include/FlipDotDrv.h
Normal 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
43
include/GameRect.h
Normal 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
14
include/ITrigger.h
Normal 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
39
include/README
Normal 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
29
include/Telegram.h
Normal 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
46
lib/README
Normal 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
21
platformio.ini
Normal 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
101
src/FlipDotDrv.cpp
Normal 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
629
src/main.cpp
Normal 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
11
test/README
Normal 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
|
||||
Loading…
Reference in New Issue
Block a user