diff --git a/MilliOhmMeter_FW/src/display.cpp b/MilliOhmMeter_FW/src/display.cpp index 0c33bdd..098d686 100644 --- a/MilliOhmMeter_FW/src/display.cpp +++ b/MilliOhmMeter_FW/src/display.cpp @@ -1,48 +1,39 @@ #include "display.h" -e_displayState CurrentScreen; - void handleDisplayGui(void) { clearDisplay(); - switch (CurrentScreen) + switch (getDisplayState()) { case mainscreen: { - e_displayState nextScreen = ScreenMainHandle(); - if(nextScreen != CurrentScreen) - { - setDisplayState(nextScreen); - } - ScreenMainDrawValues(); - ScreenMainDrawButtons(); + ScreenMainHandle(); } break; case setupscreen: { - e_displayState nextScreen = ScreenSetupHandle(); - if(nextScreen != CurrentScreen) - { - setDisplayState(nextScreen); - } - ScreenSetupDrawButtons(); + ScreenSetupHandle(); + } + break; + + case gamescreen: + { + handleScreenGame(); } break; } -} -void setDisplayState(e_displayState newstate) -{ - CurrentScreen = newstate; } - void initDisplay(void) { initDisplayHall(); initDisplayMain(); + initSetupScreen(); + initScreenGame(); + setDisplayState(e_displayState::mainscreen); } void handleDisplay(void) diff --git a/MilliOhmMeter_FW/src/display.h b/MilliOhmMeter_FW/src/display.h index a1f6f6c..4f817c8 100644 --- a/MilliOhmMeter_FW/src/display.h +++ b/MilliOhmMeter_FW/src/display.h @@ -5,8 +5,10 @@ #include "display_buttons.h" #include "display_hall.h" #include "display_draw.h" +#include "display_screen.h" #include "screen_main.h" #include "screen_setup.h" +#include "screen_game.h" #include "measure.h" #include "measure_mode.h" @@ -14,5 +16,4 @@ void initDisplay(void); void handleDisplay(void); e_displayState getDisplayState(void); -void setDisplayState(e_displayState newstate); diff --git a/MilliOhmMeter_FW/src/display_buttons.cpp b/MilliOhmMeter_FW/src/display_buttons.cpp index 9d2da12..8d9326d 100644 --- a/MilliOhmMeter_FW/src/display_buttons.cpp +++ b/MilliOhmMeter_FW/src/display_buttons.cpp @@ -67,21 +67,28 @@ void c_onScreenButton::handle() { if (_physButton.isValid()) { - _pressed = _physButton.read(); + _physButton.read(); + _pressed = _physButton.pressedFor(200); if (_pressed) { - if(_mode != e_measureMode::mNone) + if(_actionFn != NULL && (actionHandled == false)) { - setMeasureMode(_mode); + _actionFn(); + actionHandled = true; } } + else if (_physButton.releasedFor(500)) + { + actionHandled = false; + } + _state = _pressed; } else if (_stateFn != NULL) { _state = _stateFn(); } - log_i("item(%s)=%d",_name.c_str(), _state); + log_d("item(%s)=%d",_name.c_str(), _state); } diff --git a/MilliOhmMeter_FW/src/display_buttons.h b/MilliOhmMeter_FW/src/display_buttons.h index 494d477..9b277d8 100644 --- a/MilliOhmMeter_FW/src/display_buttons.h +++ b/MilliOhmMeter_FW/src/display_buttons.h @@ -16,6 +16,7 @@ #define INDICATORWIDTH 29 #define INDICATORHEIGHT 13 #define INDICATORRADIUS 3 +#define HORIZONTALBUTTONS 5 typedef enum { @@ -31,6 +32,8 @@ class c_onScreenButton const String _name; const uint8_t _index; bool (*const _stateFn)(); + void (*const _actionFn)(); + bool actionHandled = false; c_button _physButton; @@ -54,10 +57,12 @@ class c_onScreenButton void setState(bool state) { _state = state; } public: - c_onScreenButton(String name, e_measureMode mode, e_buttonLoc location, uint8_t pin) : _name(name), - _index((uint8_t)mode), - _stateFn(NULL), - _physButton(pin, (uint8_t)mode) + c_onScreenButton(String name, e_measureMode mode, e_buttonLoc location, uint8_t pin,void (*const action)() ) : + _name(name), + _index((uint8_t)mode), + _stateFn(NULL), + _actionFn(action), + _physButton(pin, (uint8_t)mode) { _xpos = 1; _ypos = 1; @@ -69,10 +74,12 @@ public: _location = location; } - c_onScreenButton(String name, uint8_t index, e_buttonLoc location, uint8_t pin) : _name(name), - _index(index), - _stateFn(NULL), - _physButton(pin, index) + c_onScreenButton(String name, uint8_t index, e_buttonLoc location, uint8_t pin, void (*const action)()) : + _name(name), + _index(index), + _stateFn(NULL), + _actionFn(action), + _physButton(pin, index) { _xpos = 1; _ypos = 1; @@ -87,6 +94,7 @@ public: c_onScreenButton(String name, uint8_t index, e_buttonLoc location, bool (*stateFn)()) : _name(name), _index(index), _stateFn(stateFn), + _actionFn(NULL), _physButton(NOBUTTON, index) { _xpos = 1; diff --git a/MilliOhmMeter_FW/src/display_hall.cpp b/MilliOhmMeter_FW/src/display_hall.cpp index 7f5ce1a..64036c5 100644 --- a/MilliOhmMeter_FW/src/display_hall.cpp +++ b/MilliOhmMeter_FW/src/display_hall.cpp @@ -2,6 +2,7 @@ U8G2_SSD1322 display(U8G2_R2, OLED_MOSI, OLED_SCK, OLED_CS, OLED_DC, OLED_RST); // Enable U8G2_16BIT in u8g2.h uint64_t lastDisplayTime = 0; +bool maxFPS = false; U8G2_SSD1322 *getDisplay(void) { @@ -21,7 +22,7 @@ void initDisplayHall() void handleDisplayHall() { uint64_t currentmillis = millis(); - if (currentmillis - lastDisplayTime > SCREENREFRESH) + if ((currentmillis - lastDisplayTime > SCREENREFRESH) || maxFPS) { //display.clearBuffer(); // clear the internal memory //display.drawStr(0, 10, "Hello World!"); // write something to the internal memory @@ -30,6 +31,11 @@ void handleDisplayHall() } } +void SetFPSMax(void) +{ + maxFPS = true; +} + void clearDisplay(void) { display.clearBuffer(); diff --git a/MilliOhmMeter_FW/src/display_hall.h b/MilliOhmMeter_FW/src/display_hall.h index 011f883..b924e37 100644 --- a/MilliOhmMeter_FW/src/display_hall.h +++ b/MilliOhmMeter_FW/src/display_hall.h @@ -43,3 +43,5 @@ uint16_t getDisplayHeight(void); U8G2_SSD1322* getDisplay(void); +void SetFPSMax(void); + diff --git a/MilliOhmMeter_FW/src/display_screen.cpp b/MilliOhmMeter_FW/src/display_screen.cpp new file mode 100644 index 0000000..47e33a2 --- /dev/null +++ b/MilliOhmMeter_FW/src/display_screen.cpp @@ -0,0 +1,83 @@ +#include "display_screen.h" + +e_displayState CurrentScreen = e_displayState::mainscreen; +uint32_t lastScreenchange = 0; + +void setDisplayState(e_displayState newstate) +{ + log_i("setDisplayState to %d", newstate); + uint32_t timeNow = millis(); + // if(timeNow - lastScreenchange > MINSCREENSWITCHDELAY) + // { + CurrentScreen = newstate; + lastScreenchange = timeNow; + //} +} + +e_displayState getDisplayState() +{ + return CurrentScreen; +} + +// class functions + +void screen_c::begin(void) +{ + uint16_t IndicatorYpos = 0; + uint16_t buttonXpos = 0; + uint16_t buttonwidth = ((getDisplay()->getDisplayWidth() / _buttonCount) - CONTROLLOFFSET * 2 + 1); + + for (auto &&item : _items) + { + if (item->getLocation() == LocBottom) + { + uint16_t ypos = getDisplay()->getDisplayHeight() - 1 - CONTROLSLINE_H + 2; + item->begin(buttonXpos, ypos, buttonwidth, CONTROLSLINE_H + 6, CONTROLRADIUS); + buttonXpos+=buttonwidth-1; + item->setVisible(true); + } + else if (item->getLocation() == LocRight) + { + uint16_t IndicatorXpos = getDisplay()->getDisplayWidth() - INDICATORWIDTH; + item->begin(IndicatorXpos, IndicatorYpos, INDICATORWIDTH, INDICATORHEIGHT, INDICATORRADIUS); + item->setVisible(true); + IndicatorYpos += INDICATORHEIGHT - 1; + log_d("indicators: indicW=%d, indicH=%d ypos,%d", INDICATORWIDTH, INDICATORHEIGHT, IndicatorXpos); + } + else + { + log_e("location not implemented (%d)", item->getLocation()); + } + } + _initialized = true; +} + +void screen_c::handle(void) +{ + if (!_initialized) + { + log_e("screen not initialized! %d", _thisScreen); + return; + } + + for (auto &&i : _items) + { + i->handle(); + } +} + +void screen_c::draw(void) +{ + for (auto &&item : _items) + { + item->drawButton(); + } +} + +void screen_c::activateModeButton(void) +{ + for (auto &&item : _items) + { + item->setDisplayState((item->getIndex() == (uint8_t)getMeasureMode())); + } +} \ No newline at end of file diff --git a/MilliOhmMeter_FW/src/display_screen.h b/MilliOhmMeter_FW/src/display_screen.h new file mode 100644 index 0000000..37fbe9f --- /dev/null +++ b/MilliOhmMeter_FW/src/display_screen.h @@ -0,0 +1,40 @@ +#pragma once + +#include "Arduino.h" +#include + +#include "display_types.h" +#include "display_draw.h" +#include "display_buttons.h" +#include "measure_mode.h" + +#define MINSCREENSWITCHDELAY 1000 + +void setDisplayState(e_displayState newstate); +e_displayState getDisplayState(void); + + +class screen_c +{ + std::vector _items; + const e_displayState _thisScreen; + const uint8_t _buttonCount; + bool _initialized = false; + + +public: + screen_c(e_displayState thisScreen, uint8_t buttonCount):_thisScreen(thisScreen), _buttonCount(buttonCount) + {} + + void addItem(c_onScreenButton* pButton) + { + _items.push_back(pButton); + } + + void begin(void); + void handle(void); + void draw(void); + void activateModeButton(void); + +}; + diff --git a/MilliOhmMeter_FW/src/display_types.h b/MilliOhmMeter_FW/src/display_types.h index 765378d..17e1102 100644 --- a/MilliOhmMeter_FW/src/display_types.h +++ b/MilliOhmMeter_FW/src/display_types.h @@ -5,5 +5,6 @@ typedef enum { mainscreen, - setupscreen + setupscreen, + gamescreen } e_displayState; \ No newline at end of file diff --git a/MilliOhmMeter_FW/src/measure_mode.h b/MilliOhmMeter_FW/src/measure_mode.h index d6e7e02..cfc0867 100644 --- a/MilliOhmMeter_FW/src/measure_mode.h +++ b/MilliOhmMeter_FW/src/measure_mode.h @@ -6,13 +6,13 @@ enum e_measureMode { + mNone, mA20, mA200, mA1000, mAuto, mSetup, mLast, - mNone }; diff --git a/MilliOhmMeter_FW/src/measure_state.cpp b/MilliOhmMeter_FW/src/measure_state.cpp index 57d975b..2ad8019 100644 --- a/MilliOhmMeter_FW/src/measure_state.cpp +++ b/MilliOhmMeter_FW/src/measure_state.cpp @@ -38,13 +38,11 @@ void initMeasureState(void) void handleMeasureState(void) { uint32_t currentTime = millis(); - bool newState = false; if (currentTime - lastMeasureTime > MEASURESTATEINTERVAL) { if (currentMeasureState != lastMeasureState) { - log_i("measureState: %s", s_measureState[currentMeasureState]); - newState = true; + log_d("measureState: %s", s_measureState[currentMeasureState]); } switch (currentMeasureState) diff --git a/MilliOhmMeter_FW/src/screen_game.cpp b/MilliOhmMeter_FW/src/screen_game.cpp new file mode 100644 index 0000000..8126316 --- /dev/null +++ b/MilliOhmMeter_FW/src/screen_game.cpp @@ -0,0 +1,398 @@ +#include "screen_game.h" + +#define SCREEN_WIDTH 256 // OLED _display width, in pixels +#define SCREEN_HEIGHT 64 // OLED _display height, in pixels + +const char STR_LOADSCREEN_CREATOR[] = "Makeability Lab"; +const char STR_LOADSCREEN_APP_NAME_LINE1[] = "Flappy"; +const char STR_LOADSCREEN_APP_NAME_LINE2[] = "Bird!"; +const char STR_PRESS_FLAP_TO_PLAY[] = "Press flap to play"; +const char STR_PRESS_FLAP_TO_EXIT[] = "Press flap to exit"; + +const char STR_GAME_OVER[] = "Game Over!"; + +// Define I/O pins +const int FLAP_BUTTON_INPUT_PIN = BUTTON1; +const int TONE_OUTPUT_PIN = BUZZER1; +const int VIBROMOTOR_OUTPUT_PIN = BUZZER1; + +// for tracking fps +unsigned long _frameCount = 0; +float _fps = 0; +unsigned long _fpsStartTimeStamp = 0; + +uint32_t lastGameUpdate = 0; + +// status bar +const boolean _drawFrameCount = false; // change to show/hide frame count +const int DELAY_LOOP_MS = 5; +const int LOAD_SCREEN_SHOW_MS = 750; + +class Bird : public Rectangle +{ +public: + Bird(int x, int y, int width, int height) : Rectangle(x, y, width, height) + { + } +}; + +class Pipe : public Rectangle +{ +protected: + bool _hasPassedBird = false; + +public: + Pipe(int x, int y, int width, int height) : Rectangle(x, y, width, height) + { + } + + bool getHasPassedBird() + { + return _hasPassedBird; + } + + bool setHasPassedBird(bool hasPassedBird) + { + return _hasPassedBird = hasPassedBird; + } +}; + +const int BIRD_HEIGHT = 5; +const int BIRD_WIDTH = 10; +const int NUM_PIPES = 3; + +const int MIN_PIPE_WIDTH = 8; +const int MAX_PIPE_WIDTH = 18; // in pixels +const int MIN_PIPE_X_SPACING_DISTANCE = BIRD_WIDTH * 3; // in pixels +const int MAX_PIPE_X_SPACING_DISTANCE = 100; // in pixels +const int MIN_PIPE_Y_SPACE = BIRD_HEIGHT * 3; +const int MAX_PIPE_Y_SPACE = SCREEN_HEIGHT - BIRD_HEIGHT * 2; + +int _pipeSpeed = 2; +int _gravity = 1; // can't apply gravity every frame, apply every X time +int _points = 0; +unsigned long _gameOverTimestamp = 0; + +const int IGNORE_INPUT_AFTER_GAME_OVER_MS = 500; // ignores input for 500ms after game over + +// Initialize top pipe and bottom pipe arrays. The location/sizes don't matter +// at this point as we'll set them in setup() +Pipe _topPipes[NUM_PIPES] = {Pipe(0, 0, 0, 0), + Pipe(0, 0, 0, 0), + Pipe(0, 0, 0, 0)}; + +Pipe _bottomPipes[NUM_PIPES] = {Pipe(0, 0, 0, 0), + Pipe(0, 0, 0, 0), + Pipe(0, 0, 0, 0)}; + +Bird _bird(5, SCREEN_HEIGHT / 2, BIRD_WIDTH, BIRD_HEIGHT); + +enum GameState +{ + NEW_GAME, + PLAYING, + GAME_OVER, +}; + +GameState _gameState = NEW_GAME; + +// This is necessary for the game to work on the ESP32 +// See: +// - https://github.com/espressif/arduino-esp32/issues/1734 +// - https://github.com/Bodmer/TFT_eSPI/issues/189 +#ifndef max +#define max(a, b) (((a) > (b)) ? (a) : (b)) +#endif + +void showLoadScreen(); +void initializeGameEntities(); +void drawStatusBar(); +void nonGamePlayLoop(); +void gamePlayLoop(); +void calcFrameRate(); + +void initScreenGame() +{ + // if analog input pin 5 is unconnected, random analog + // noise will cause the call to randomSeed() to generate + // different seed numbers each time the sketch runs. + // randomSeed() will then shuffle the random function. + randomSeed(125); + + // Show load screen + showLoadScreen(); + + // Setup pipes + initializeGameEntities(); + + _fpsStartTimeStamp = millis(); + _gameState = NEW_GAME; +} + +void handleScreenGame() +{ + uint32_t timeNow = millis(); + if (timeNow - lastGameUpdate > DELAY_LOOP_MS) + { + //getDisplay()->clearDisplay(); + + drawStatusBar(); + if (_gameState == NEW_GAME || _gameState == GAME_OVER) + { + nonGamePlayLoop(); + } + else if (_gameState == PLAYING) + { + gamePlayLoop(); + } + calcFrameRate(); + } +} + +void drawText(int xText, int yText, const char* s, const uint8_t *font) +{ + getDisplay()->setFont(font); + getDisplay()->setCursor(xText, yText); + getDisplay()->print(s); +} + +void drawCenterText(int yText, const char* s, const uint8_t *font) +{ + int w = getDisplay()->getStrWidth(s); + drawText(getDisplay()->getDisplayWidth() / 2 - w / 2, yText, s, font); +} + +void nonGamePlayLoop() +{ + for (int i = 0; i < NUM_PIPES; i++) + { + _topPipes[i].draw(); + _bottomPipes[i].draw(); + } + + int flapButtonVal = digitalRead(FLAP_BUTTON_INPUT_PIN); + if (_gameState == NEW_GAME) + { + drawCenterText(15,STR_PRESS_FLAP_TO_PLAY,FONT16); + + if (flapButtonVal == LOW) + { + _gameState = PLAYING; + } + } + else if (_gameState == GAME_OVER) + { + int yText = 15; + drawCenterText(yText,STR_GAME_OVER,FONT16); + + yText = yText + 16 + 2; + drawCenterText(yText,STR_PRESS_FLAP_TO_EXIT,FONT16); + + // We ignore input a bit after game over so that user can see end game screen + // and not accidentally start a new game + if (flapButtonVal == LOW && millis() - _gameOverTimestamp >= IGNORE_INPUT_AFTER_GAME_OVER_MS) + { + // if the current state is game over, need to reset + setDisplayState(mainscreen); + } + } + + _bird.draw(); +} + +void initializeGameEntities() +{ + _points = 0; + + _bird.setY(getDisplay()->getDisplayHeight() / 2 - _bird.getHeight() / 2); + _bird.setDrawFill(true); + + const int minStartXPipeLocation = getDisplay()->getDisplayWidth() / 2; + int lastPipeX = minStartXPipeLocation; + for (int i = 0; i < NUM_PIPES; i++) + { + + int pipeX = lastPipeX + random(MIN_PIPE_X_SPACING_DISTANCE, MAX_PIPE_X_SPACING_DISTANCE); + int pipeWidth = random(MIN_PIPE_WIDTH, MAX_PIPE_WIDTH); + + int yGapBetweenPipes = random(MIN_PIPE_Y_SPACE, MAX_PIPE_Y_SPACE); + + int topPipeY = 0; + int topPipeHeight = random(0, SCREEN_HEIGHT - yGapBetweenPipes); + + int bottomPipeY = topPipeHeight + yGapBetweenPipes; + int bottomPipeHeight = SCREEN_HEIGHT - bottomPipeY; + + _topPipes[i].setLocation(pipeX, topPipeY); + _topPipes[i].setDimensions(pipeWidth, topPipeHeight); + _topPipes[i].setDrawFill(false); + + _bottomPipes[i].setLocation(pipeX, bottomPipeY); + _bottomPipes[i].setDimensions(pipeWidth, bottomPipeHeight); + _topPipes[i].setDrawFill(false); + + lastPipeX = _topPipes[i].getRight(); + } +} + +void gamePlayLoop() +{ + int flapButtonVal = digitalRead(FLAP_BUTTON_INPUT_PIN); + _bird.setY(_bird.getY() + _gravity); + + if (flapButtonVal == LOW) + { + _bird.setY(_bird.getY() - 3); + } + _bird.forceInside(0, 0, getDisplay()->getDisplayWidth(), getDisplay()->getDisplayHeight()); + + // xMaxRight tracks the furthest right pixel of the furthest right pipe + // which we will use to reposition pipes that go off the left part of screen + int xMaxRight = 0; + + // Iterate through pipes and check for collisions and scoring + for (int i = 0; i < NUM_PIPES; i++) + { + + _topPipes[i].setX(_topPipes[i].getX() - _pipeSpeed); + _bottomPipes[i].setX(_bottomPipes[i].getX() - _pipeSpeed); + + _topPipes[i].draw(); + _bottomPipes[i].draw(); + + Serial.println(_topPipes[i].toString()); + + // Check if the bird passed by the pipe + if (_topPipes[i].getRight() < _bird.getLeft()) + { + + // If we're here, the bird has passed the pipe. Check to see + // if we've marked it as passed yet. If not, then increment the score! + if (_topPipes[i].getHasPassedBird() == false) + { + _points++; + _topPipes[i].setHasPassedBird(true); + _bottomPipes[i].setHasPassedBird(true); + } + } + + // xMaxRight is used to track future placements of pipes once + // they go off the left part of the screen + if (xMaxRight < _topPipes[i].getRight()) + { + xMaxRight = _topPipes[i].getRight(); + } + + // Check for collisions and end of game + if (_topPipes[i].overlaps(_bird)) + { + _topPipes[i].setDrawFill(true); + _gameState = GAME_OVER; + _gameOverTimestamp = millis(); + } + else + { + _topPipes[i].setDrawFill(false); + } + + if (_bottomPipes[i].overlaps(_bird)) + { + _bottomPipes[i].setDrawFill(true); + _gameState = GAME_OVER; + _gameOverTimestamp = millis(); + } + else + { + _bottomPipes[i].setDrawFill(false); + } + } + + // Check for pipes that have gone off the screen to the left + // and reset them to off the screen on the right + xMaxRight = max(xMaxRight, getDisplay()->getDisplayWidth()); + for (int i = 0; i < NUM_PIPES; i++) + { + if (_topPipes[i].getRight() < 0) + { + int pipeX = xMaxRight + random(MIN_PIPE_X_SPACING_DISTANCE, MAX_PIPE_X_SPACING_DISTANCE); + int pipeWidth = random(MIN_PIPE_WIDTH, MAX_PIPE_WIDTH); + + int yGapBetweenPipes = random(MIN_PIPE_Y_SPACE, MAX_PIPE_Y_SPACE); + + int topPipeY = 0; + int topPipeHeight = random(0, SCREEN_HEIGHT - yGapBetweenPipes); + + int bottomPipeY = topPipeHeight + yGapBetweenPipes; + int bottomPipeHeight = SCREEN_HEIGHT - bottomPipeY; + + _topPipes[i].setLocation(pipeX, topPipeY); + _topPipes[i].setDimensions(pipeWidth, topPipeHeight); + _topPipes[i].setHasPassedBird(false); + + _bottomPipes[i].setLocation(pipeX, bottomPipeY); + _bottomPipes[i].setDimensions(pipeWidth, bottomPipeHeight); + _bottomPipes[i].setHasPassedBird(false); + + xMaxRight = _topPipes[i].getRight(); + } + } + + _bird.draw(); +} + +void showLoadScreen() +{ + // Clear the buffer + getDisplay()->clearDisplay(); + + // Show load screen + int yText = 10; + + drawCenterText(yText,STR_LOADSCREEN_CREATOR,FONT8); + yText = yText + 8 + 1; + drawCenterText(yText,STR_LOADSCREEN_APP_NAME_LINE1,FONT16); + yText = yText + 16 + 1; + drawCenterText(yText,STR_LOADSCREEN_APP_NAME_LINE2,FONT8 ); + + delay(LOAD_SCREEN_SHOW_MS); + getDisplay()->clearDisplay(); +} + +/** + * Call this every frame to calculate frame rate + */ +void calcFrameRate() +{ + unsigned long elapsedTime = millis() - _fpsStartTimeStamp; + _frameCount++; + if (elapsedTime > 1000) + { + _fps = _frameCount / (elapsedTime / 1000.0); + _fpsStartTimeStamp = millis(); + _frameCount = 0; + } +} + +/** + * Draws the status bar at top of screen with points and fps + */ +void drawStatusBar() +{ + // Draw accumulated points + getDisplay()->setFont(FONT8); + + getDisplay()->setCursor(0, 0); // draw points + getDisplay()->print(_points); + + // Draw frame count + if (_drawFrameCount) + { + int16_t x1; + getDisplay()->setFont(FONT8); + x1 = getDisplay()->getDisplayWidth() - getDisplay()->getStrWidth("XX.XX fps"); + getDisplay()->setCursor(x1,0); + getDisplay()->print(_fps); + getDisplay()->print(" fps"); + + } +} diff --git a/MilliOhmMeter_FW/src/screen_game.h b/MilliOhmMeter_FW/src/screen_game.h new file mode 100644 index 0000000..e8297b0 --- /dev/null +++ b/MilliOhmMeter_FW/src/screen_game.h @@ -0,0 +1,9 @@ +#pragma once + +#include "Arduino.h" +#include "shape.hpp" +#include "display_screen.h" + + +void initScreenGame(void); +void handleScreenGame(void); \ No newline at end of file diff --git a/MilliOhmMeter_FW/src/screen_main.cpp b/MilliOhmMeter_FW/src/screen_main.cpp index c11216d..83f2df2 100644 --- a/MilliOhmMeter_FW/src/screen_main.cpp +++ b/MilliOhmMeter_FW/src/screen_main.cpp @@ -1,76 +1,66 @@ #include "screen_main.h" -c_onScreenButton ma20("20m", mA20, LocBottom, BUTTON1); -c_onScreenButton ma200("200m", mA200, LocBottom, BUTTON2); -c_onScreenButton ma1000("1A", mA1000, LocBottom, BUTTON3); -c_onScreenButton mauto("Auto", mAuto, LocBottom, BUTTON4); -c_onScreenButton bsetup("Conf", 5, LocBottom, BUTTON5); +void setma20(void) +{ + setMeasureMode(e_measureMode::mA20); +} + +void setma200(void) +{ + setMeasureMode(e_measureMode::mA200); +} + +void setma1000(void) +{ + setMeasureMode(e_measureMode::mA1000); +} + +void setmAuto(void) +{ + setMeasureMode(e_measureMode::mAuto); +} + +void buttonSetup(void) +{ + setDisplayState(e_displayState::setupscreen); + log_i("Conf button pressed, go to setupscreen"); +} + +c_onScreenButton ma20("20m", mA20, LocBottom, BUTTON1, &setma20); +c_onScreenButton ma200("200m", mA200, LocBottom, BUTTON2, &setma200); +c_onScreenButton ma1000("1A", mA1000, LocBottom, BUTTON3, &setma1000); +c_onScreenButton mauto("Auto", mAuto, LocBottom, BUTTON4, &setmAuto); +c_onScreenButton bsetup("Conf", 5, LocBottom, BUTTON5, &buttonSetup); c_onScreenButton errorState("ER", 6, LocRight, &getErrorState); c_onScreenButton okState("OK", 7, LocRight, &getOkState); c_onScreenButton openState("Open", 8, LocRight, &getOpenState); c_onScreenButton wifiState("Wifi", 9, LocRight, &getWifiState); -std::vector MainScreen; +//std::vector MainScreen; +screen_c mainScreen(e_displayState::mainscreen, 5); + const e_displayState thisScreen = e_displayState::mainscreen; void initDisplayMain(void) { - log_i("Setup main screen : "); - uint16_t screenwidth = getDisplay()->getDisplayWidth(); - uint16_t buttonwidth = ((screenwidth / mLast) - CONTROLLOFFSET * 2 + 1); - uint16_t currentWidth = 0; - uint16_t ypos = getDisplay()->getDisplayHeight() - 1 - CONTROLSLINE_H + 2; - - log_i("buttons: screenW=%d, buttonW=%d, ypos=%d", screenwidth, buttonwidth, ypos); - // setup bottom buttons - ma20.begin(currentWidth, ypos, buttonwidth, CONTROLSLINE_H + 6, CONTROLRADIUS); - ma200.begin(currentWidth += (buttonwidth - 1), ypos, buttonwidth, CONTROLSLINE_H + CONTROLRADIUS, CONTROLRADIUS); - ma1000.begin(currentWidth += (buttonwidth - 1), ypos, buttonwidth, CONTROLSLINE_H + CONTROLRADIUS, CONTROLRADIUS); - mauto.begin(currentWidth += (buttonwidth - 1), ypos, buttonwidth, CONTROLSLINE_H + CONTROLRADIUS, CONTROLRADIUS); - bsetup.begin(currentWidth += (buttonwidth - 1), ypos, buttonwidth, CONTROLSLINE_H + CONTROLRADIUS, CONTROLRADIUS); - - // setup right side indicators - uint16_t currentYpos = 0; - uint16_t IndicatorXpos = screenwidth - INDICATORWIDTH; - - log_i("indicators: indicW=%d, indicH=%d ypos,%d", INDICATORWIDTH, INDICATORHEIGHT, IndicatorXpos); - - errorState.begin(IndicatorXpos, currentYpos, INDICATORWIDTH, INDICATORHEIGHT, INDICATORRADIUS); - okState.begin(IndicatorXpos, currentYpos += (INDICATORHEIGHT - 1), INDICATORWIDTH, INDICATORHEIGHT, INDICATORRADIUS); - openState.begin(IndicatorXpos, currentYpos += (INDICATORHEIGHT - 1), INDICATORWIDTH, INDICATORHEIGHT, INDICATORRADIUS); - wifiState.begin(IndicatorXpos, currentYpos += (INDICATORHEIGHT - 1), INDICATORWIDTH, INDICATORHEIGHT, INDICATORRADIUS); - // fill vector log_i("Store main screen items"); - MainScreen.push_back(&ma20); - MainScreen.push_back(&ma200); - MainScreen.push_back(&ma1000); - MainScreen.push_back(&mauto); - MainScreen.push_back(&bsetup); + mainScreen.addItem(&ma20); + mainScreen.addItem(&ma200); + mainScreen.addItem(&ma1000); + mainScreen.addItem(&mauto); + mainScreen.addItem(&bsetup); - MainScreen.push_back(&errorState); - MainScreen.push_back(&okState); - MainScreen.push_back(&openState); - MainScreen.push_back(&wifiState); + mainScreen.addItem(&errorState); + mainScreen.addItem(&okState); + mainScreen.addItem(&openState); + mainScreen.addItem(&wifiState); - for (auto &&button : MainScreen) - { - button->setVisible(true); - } + mainScreen.begin(); log_i("mainscreen OK"); } -void ScreenMainDrawButtons(void) -{ - // draw controlstrip indicators - for (auto &&thismode : MainScreen) - { - thismode->setDisplayState((thismode->getIndex() == (uint8_t)getMeasureMode())); - thismode->drawButton(); - } -} - void ScreenMainDrawValues(void) { if (getDisplay() == NULL) @@ -92,16 +82,10 @@ void ScreenMainDrawValues(void) getDisplay()->drawUTF8(60 + stringwidth + 3, 43, "mΩ"); } -e_displayState ScreenMainHandle(void) +void ScreenMainHandle(void) { - for (auto &&i : MainScreen) - { - i->handle(); - } - - if(bsetup.getState()) - { - return e_displayState::setupscreen; - } - return thisScreen; + mainScreen.activateModeButton(); + mainScreen.handle(); + mainScreen.draw(); + ScreenMainDrawValues(); } \ No newline at end of file diff --git a/MilliOhmMeter_FW/src/screen_main.h b/MilliOhmMeter_FW/src/screen_main.h index bbffb39..f278681 100644 --- a/MilliOhmMeter_FW/src/screen_main.h +++ b/MilliOhmMeter_FW/src/screen_main.h @@ -4,8 +4,8 @@ #include "display_types.h" #include "display_draw.h" #include "display_buttons.h" +#include "display_screen.h" + void initDisplayMain(void); -void ScreenMainDrawValues(void); -void ScreenMainDrawButtons(void); -e_displayState ScreenMainHandle(void); +void ScreenMainHandle(void); diff --git a/MilliOhmMeter_FW/src/screen_setup.cpp b/MilliOhmMeter_FW/src/screen_setup.cpp index 8c87341..f3f1d10 100644 --- a/MilliOhmMeter_FW/src/screen_setup.cpp +++ b/MilliOhmMeter_FW/src/screen_setup.cpp @@ -1,53 +1,42 @@ #include "screen_setup.h" -c_onScreenButton bback("<", mA20, LocBottom, BUTTON1); -c_onScreenButton bforw(">", mA200, LocBottom, BUTTON2); -c_onScreenButton bOK("OK", mA1000, LocBottom, BUTTON3); -c_onScreenButton bGame("Game", mAuto, LocBottom, BUTTON4); -c_onScreenButton bExit("Exit", 5, LocBottom, BUTTON5); +void buttonExit(void) +{ + setDisplayState(e_displayState::mainscreen); + log_i("Exit button pressed, go to mainscreen"); +} -std::vector SetupScreen; -const e_displayState thisScreen = e_displayState::setupscreen; +void buttonGame(void) +{ + setDisplayState(e_displayState::gamescreen); + SetFPSMax(); + initScreenGame(); + log_i("game button pressed, go to gamescreen"); +} + + + +c_onScreenButton bback("<", 1, LocBottom, BUTTON1, NULL); +c_onScreenButton bforw(">", 2, LocBottom, BUTTON2, NULL); +c_onScreenButton bOK("OK", 3, LocBottom, BUTTON3, NULL); +c_onScreenButton bGame("Game", 4, LocBottom, BUTTON4, &buttonGame); +c_onScreenButton bExit("Exit", 5, LocBottom, BUTTON5, &buttonExit); + +screen_c setupScreen(e_displayState::setupscreen, 5); void initSetupScreen(void) { - uint16_t screenwidth = getDisplay()->getDisplayWidth(); - uint16_t buttonwidth = ((screenwidth / mLast) - CONTROLLOFFSET * 2 + 1); - uint16_t currentWidth = 0; - uint16_t ypos = getDisplay()->getDisplayHeight() - 1 - CONTROLSLINE_H + 2; + setupScreen.addItem(&bback); + setupScreen.addItem(&bforw); + setupScreen.addItem(&bOK); + setupScreen.addItem(&bGame); + setupScreen.addItem(&bExit); - bback.begin(currentWidth, ypos, buttonwidth, CONTROLSLINE_H + 6, CONTROLRADIUS); - bforw.begin(currentWidth += (buttonwidth - 1), ypos, buttonwidth, CONTROLSLINE_H + CONTROLRADIUS, CONTROLRADIUS); - bOK.begin(currentWidth += (buttonwidth - 1), ypos, buttonwidth, CONTROLSLINE_H + CONTROLRADIUS, CONTROLRADIUS); - bGame.begin(currentWidth += (buttonwidth - 1), ypos, buttonwidth, CONTROLSLINE_H + CONTROLRADIUS, CONTROLRADIUS); - bExit.begin(currentWidth += (buttonwidth - 1), ypos, buttonwidth, CONTROLSLINE_H + CONTROLRADIUS, CONTROLRADIUS); - - SetupScreen.push_back(&bback); - SetupScreen.push_back(&bforw); - SetupScreen.push_back(&bOK); - SetupScreen.push_back(&bGame); - SetupScreen.push_back(&bExit); + setupScreen.begin(); } -void ScreenSetupDrawButtons(void) +void ScreenSetupHandle(void) { - // draw controlstrip indicators - for (auto &&thismode : SetupScreen) - { - thismode->drawButton(); - } -} - -e_displayState ScreenSetupHandle(void) -{ - for (auto &&i : SetupScreen) - { - i->handle(); - } - - if(bExit.getState()) - { - return e_displayState::mainscreen; - } - return thisScreen; + setupScreen.handle(); + setupScreen.draw(); } \ No newline at end of file diff --git a/MilliOhmMeter_FW/src/screen_setup.h b/MilliOhmMeter_FW/src/screen_setup.h index e265ab1..47941a8 100644 --- a/MilliOhmMeter_FW/src/screen_setup.h +++ b/MilliOhmMeter_FW/src/screen_setup.h @@ -3,8 +3,9 @@ #include "Arduino.h" #include "display_types.h" #include "display_buttons.h" +#include "display_screen.h" +#include "screen_game.h" void initSetupScreen(void); -e_displayState ScreenSetupHandle(void); -void ScreenSetupDrawButtons(void); +void ScreenSetupHandle(void); diff --git a/MilliOhmMeter_FW/src/shape.hpp b/MilliOhmMeter_FW/src/shape.hpp new file mode 100644 index 0000000..da0d74b --- /dev/null +++ b/MilliOhmMeter_FW/src/shape.hpp @@ -0,0 +1,493 @@ +#include "display_hall.h" + + +class Shape { + + protected: + int _x; + int _y; + int _width; + int _height; + bool _drawFill = DEFAULT_DRAW_FILL; + bool _drawBoundingBox = false; + + public: + const bool DEFAULT_DRAW_FILL = false; + + Shape(int x, int y, int width, int height) + : Shape(x, y, width, height, DEFAULT_DRAW_FILL){ + // purposefully empty + } + + Shape(int x, int y, int width, int height, bool drawFillOn) { + _x = x; + _y = y; + _width = width; + _height = height; + _drawFill = drawFillOn; + } + + /** + * @brief Toggles whether to draw the bounding box around the shape + * + * @param drawBoundingBox If true, then a bounding box will be drawn around the shape + */ + void setDrawBoundingBox(bool drawBoundingBox){ + _drawBoundingBox = drawBoundingBox; + } + + /** + * @brief Toggles whether to draw the object as filled or just an outline + * + * @param drawFill If true, then the shape will be drawn filled (solid) + */ + void setDrawFill(bool drawFill){ + _drawFill = drawFill; + } + + /** + * @brief Convenience function to set location of shape. Simply calls setX(x) and setY(y) + * + * @param x The x location of the shape + * @param y The y location of the shape + */ + void setLocation(int x, int y){ + setX(x); + setY(y); + } + + /** + * @brief Sets the y location of the shape + * + * @param y The y location of the shape + */ + void setY(int y){ + _y = y; + } + + /** + * @brief Sets the x location of the shape + * + * @param x The x location of the shape + */ + void setX(int x){ + _x = x; + } + + /** + * @brief Gets the x location of the shape + * + * @return int x location of the shape + */ + int getX() const{ + return _x; + } + + /** + * @brief Gets the y location of the shape + * + * @return int y locaiton of the shape + */ + int getY() const{ + return _y; + } + + /** + * @brief A virtual function that should be overloaded by sub-classes. Deriving + * classes should call + * + * @param display A reference to the Adafruit_SSD1306 object + */ + virtual void draw( void) { + if(_drawBoundingBox){ + getDisplay()->drawFrame(_x, _y, _width, _height); //drawRect(_x, _y, _width, _height, SSD1306_WHITE); + } + } + + /** + * @brief Set the size (width, height) of the shape. This is a virtual + * function. Deriving classes may want to handle setDimensions differently. + * For example, the Circle class forces only uses the 'width' parameter + * and sets height = width. + * + * @param width The width of the shape + * @param height The height of the shape + */ + virtual void setDimensions(int width, int height){ + _width = width; + _height = height; + } + + /** + * @brief Get the width of the shape + * + * @return int Width of the shape in pixels + */ + int getWidth() const{ + return _width; + } + + /** + * @brief Get the height of the shape + * + * @return int Height of the shape in pixels + */ + int getHeight() const{ + return _height; + } + + /** + * @brief Get the left (x) location of the shape, which is just getX() + * + * @return int The left location of the shape + */ + int getLeft() const{ + return _x; + } + + /** + * @brief Get the right (x) location, which is getX() + getWidth() + * + * @return int The right location (x) of the shape + */ + int getRight() const{ + return _x + _width; + } + + /** + * @brief Get the bottom of the shape (y), which is getY() + getHeight() + * + * @return int + */ + int getBottom() const{ + return _y + _height; + } + + /** + * @brief Get the top of the shape, which is just getY() + * + * @return int + */ + int getTop() const{ + return _y; + } + + /** + * @brief Attempts to force the x,y location of the shape into the + * provided rectangular boundary (x, y, width, height) + * + * @param x X location of boundary + * @param y Y location of boundary + * @param width Width of boundary + * @param height Height of boundary + */ + void forceInside(int x, int y, int width, int height){ + if(getTop() <= y){ + setY(y); + }else if(getBottom() >= y + height){ + setY((y + height) - getHeight() - 1); + } + + if(getLeft() <= x){ + setX(x); + }else if(getRight() >= x + width){ + setX((x + width) - getWidth() - 1); + } + } + + /** + * @brief Checks to see if the current shape overlaps with the provided shape. + * Overlap is defined as any pixel between this shape touching (equaling) + * the passed in shape + * + * @param shape The shape to check for overlap + * @return true If the shape overlaps + * @return false If the shape does not overlap + */ + virtual bool overlaps(const Shape& shape) const{ + //Serial.println("We are in overlaps shape!"); + + // based on https://stackoverflow.com/a/4098512 + return !(getRight() < shape._x || + getBottom() < shape._y || + _x > shape.getRight() || + _y > shape.getBottom()); + } + + /** + * @brief Checks if this shape contains the x,y location. If so, returns true. + * + * @param x The X location to check + * @param y The Y location to check + * @return true If this shape contains the x,y + * @return false If this shape does NOT contain the x,y + */ + virtual bool contains(int x, int y) const { + return x >= _x && // check within left edge + x <= (_x + _width) && // check within right edge + y >= _y && // check within top edge + y <= (_y + _height); // check within bottom edge + } + + /** + * @brief Gets the name of this object. Sub-classes should override this. + * + * @return String + */ + virtual String getName() const{ + return "Shape"; + } + + /** + * @brief Gets a basic toString() for the object. Sub-classes should override + * to get a more fitting description + * + * @return String + */ + virtual String toString() const{ + return (String)"x: " + _x + " y: " + _y + " width: " + _width + " height: " + _height; + } + + /** + * @brief Static function that calculates the Euclidean distance between two points + * + * @param x1 + * @param y1 + * @param x2 + * @param y2 + * @return float + */ + static float distance(int x1, int y1, int x2, int y2){ + return sqrt(pow(x2 - x1, 2) + pow(y2 - y1, 2)); + } +}; + +/** + * @brief A Rectangle shape class that extends Shape. + * + */ +class Rectangle : public Shape { + public: + Rectangle(int x, int y, int width, int height) + : Rectangle(x, y, width, height, DEFAULT_DRAW_FILL) + { + // purposefully empty + } + + Rectangle(int x, int y, int width, int height, bool drawFillOn) + : Shape(x, y, width, height) + { + // purposefully empty + } + + /** + * @brief Draw the rectangle + * + * @param display + */ + void draw (void) override{ + // Draw rectangle takes in (xTop, yTop, width, height) + // https://learn.adafruit.com/adafruit-gfx-graphics-library/graphics-primitives#rectangles-2002784-10 + if(_drawFill){ + getDisplay()->drawFrame(_x, _y, _width, _height); + }else{ + getDisplay()->drawBox(_x, _y, _width, _height); + } + + // We don't call the parent class draw() call because + // the bounding box is simple another rectangle + // Shape::draw(disp); + } + + String getName() const override{ + return "Rectangle"; + } +}; + +/** + * @brief A circular shape class that extends Shape + * + */ +class Circle : public Shape { + + public: + Circle(int xCenter, int yCenter, int radius) : + Circle(xCenter, yCenter, radius, DEFAULT_DRAW_FILL) + { + // purposefully empty + } + + Circle(int xCenter, int yCenter, int radius, bool drawFillOn) : + Shape(xCenter - radius, yCenter - radius, (radius * 2) + 1, (radius * 2) + 1, drawFillOn) + { + // purposefully empty + } + + /** + * @brief Draw the circle + * + * @param display + */ + void draw() override{ + // Draw circle takes in (xCenter, yCenter, radius) + // https://learn.adafruit.com/adafruit-gfx-graphics-library/graphics-primitives#circles-2002788-14 + int radius = getRadius(); + if(_drawFill){ + getDisplay()->drawDisc(_x + radius, _y + radius, radius); + }else{ + getDisplay()->drawCircle(_x + radius, _y + radius, radius); + } + + // Call super method + Shape::draw(); + } + + /** + * @brief Calculates the overlap between two circular objects + * + * @param circle + * @return true + * @return false + */ + bool overlaps(const Circle& circle) const { + int distanceFromCenterPoints = Shape::distance(getCenterX(), getCenterY(), circle.getCenterX(), circle.getCenterY()); + return distanceFromCenterPoints <= getRadius() + circle.getRadius(); + } + + /** + * @brief Calculates the overlap between two shapes + * + * @param shape + * @return true + * @return false + */ + bool overlaps(const Shape& shape) const override{ + if(getName().equals(shape.getName())){ + return this->overlaps((Circle&)shape); + } + + // Default to parent overlaps function + return Shape::overlaps(shape); + } + + /** + * @brief Uses the width value to set the circle diameter. The height parameter is ignored. + * + * @param width sets the circle diameter + * @param height ignored + */ + void setDimensions(int width, int height) override{ + Shape::setDimensions(width, width); + } + + /** + * @brief Get the X center of the circle + * + * @return int + */ + int getCenterX() const{ + return _x + getWidth() / 2; + } + + /** + * @brief Get the Y center of the circle + * + * @return int + */ + int getCenterY() const{ + return _y + getWidth() / 2; + } + + /** + * @brief Moves the location of the object so that the center is at x,y + * + * @param x + * @param y + * @return int + */ + void setCenter(int x, int y){ + int radius = getRadius(); + setLocation(x - radius, y - radius); + } + + /** + * @brief Get the radius of the circle + * + * @return int + */ + int getRadius() const{ + return getWidth() / 2; + } + + /** + * @brief Set the radius of the circle + * + * @param radius + */ + void setRadius(int radius){ + int size = 2 * radius; + setDimensions(size, size); + } + + /** + * @brief Get the name of the circle + * + * @return String + */ + String getName() const override{ + return "Circle"; + } +}; + +class Ball : public Circle{ + + protected: + int _xSpeed = 0; + int _ySpeed = 0; + + public: + Ball(int xCenter, int yCenter, int radius) : Circle(xCenter, yCenter, radius) + { + + } + + boolean checkYBounce(int yMin, int yMax){ + return getTop() <= yMin || getBottom() >= yMax; + } + + boolean checkXBounce(int xMin, int xMax){ + return getLeft() <= xMin || getRight() >= xMax; + } + + void setSpeed(int xSpeed, int ySpeed){ + _xSpeed = xSpeed; + _ySpeed = ySpeed; + } + + int getXSpeed() const{ + return _xSpeed; + } + + int getYSpeed() const{ + return _ySpeed; + } + + void update(){ + _x += _xSpeed; + _y += _ySpeed; + } + + int reverseYSpeed(){ + _ySpeed = _ySpeed * -1; + return _ySpeed; + } + + int reverseXSpeed(){ + _xSpeed = _xSpeed * -1; + return _xSpeed; + } + + String getName() const override{ + return "Ball"; + } +}; \ No newline at end of file