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