diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8f85615..03e9125 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -23,15 +23,15 @@ jobs: - name: Test project in native environment run: pio test -e native - - name: Build console application + - name: Build native console application run: pio run -e native - - name: Build 'target' cross application - run: pio run -e target - - - name: Build 'gun' cross application + - name: Build cross Gun application run: pio run -e gun + - name: Build cross Target application + run: pio run -e target + - name: Archive test console application uses: actions/upload-artifact@v3 with: diff --git a/lib/Domain/Game.cpp b/lib/Domain/Game.cpp index 5d51835..5ca8c90 100644 --- a/lib/Domain/Game.cpp +++ b/lib/Domain/Game.cpp @@ -19,15 +19,15 @@ // 5 rounds per player static const uint8_t TOTAL_ROUNDS = 20; -Game::Game(ITargetGui *gui) { - this->gui = gui; +Game::Game(ITargetUi *ui) { + this->ui = ui; currentRound = 0; currentPlayer = &players[0]; } void Game::recordSucceededShoot() { currentPlayer->recordSucceededShoot(); - gui->displayPlayerInfo(*currentPlayer); + ui->displayPlayerInfo(*currentPlayer); } /** @@ -47,7 +47,7 @@ void Game::nextRound() { currentPlayer = &players[nextPlayerId]; currentPlayer->startRound(); - gui->displayPlayerInfo(*currentPlayer); + ui->displayPlayerInfo(*currentPlayer); } bool Game::isFinished() { @@ -60,21 +60,21 @@ void Game::changeCurrentPlayerTo(uint8_t playerIndex) { currentPlayer->endRound(); currentPlayer = &players[playerIndex]; currentPlayer->startRound(); - gui->setCurrentPlayer(playerIndex); - gui->resetTargets(); - gui->displayPlayerInfo(*currentPlayer); + ui->setCurrentPlayer(playerIndex); + ui->resetTargets(); + ui->displayPlayerInfo(*currentPlayer); } } void Game::reset() { - gui->restart(); + ui->restart(); for (Player &player : players) { player.reset(); - gui->displayPlayerInfo(player); + ui->displayPlayerInfo(player); } currentRound = 0; currentPlayer = &players[0]; currentPlayer->startRound(); - gui->displayPlayerInfo(*currentPlayer); - gui->setCurrentPlayer(0); + ui->displayPlayerInfo(*currentPlayer); + ui->setCurrentPlayer(0); } \ No newline at end of file diff --git a/lib/Domain/Game.hpp b/lib/Domain/Game.hpp index 319e92d..316a091 100644 --- a/lib/Domain/Game.hpp +++ b/lib/Domain/Game.hpp @@ -16,7 +16,7 @@ */ #pragma once -#include +#include #include #include @@ -26,7 +26,7 @@ */ class Game { - ITargetGui *gui; + ITargetUi *ui; public: static const uint8_t PLAYER_COUNT = 4; @@ -43,7 +43,7 @@ class Game { uint8_t currentRound; Player *currentPlayer; - Game(ITargetGui *); + Game(ITargetUi *ui); void recordSucceededShoot(); diff --git a/lib/Domain/BTEGui.cpp b/lib/Domain/Target/BTEGui.cpp similarity index 89% rename from lib/Domain/BTEGui.cpp rename to lib/Domain/Target/BTEGui.cpp index 57527d5..c3c90e6 100644 --- a/lib/Domain/BTEGui.cpp +++ b/lib/Domain/Target/BTEGui.cpp @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -#include +#include #if not defined(NATIVE) #include @@ -35,7 +35,7 @@ void BTEGui::_output(const char *message) { #endif } -void BTEGui::hitTarget(ITargetGui::TARGET target) { +void BTEGui::hitTarget(ITargetUi::TARGET target) { targetState |= (1 << target); char letter = TARGET_APP_LETTERS[(uint8_t)target]; @@ -43,7 +43,7 @@ void BTEGui::hitTarget(ITargetGui::TARGET target) { _output(stringBuffer); } -bool BTEGui::isTargetHit(ITargetGui::TARGET target) { +bool BTEGui::isTargetHit(ITargetUi::TARGET target) { return ((targetState & (1 << target)) == (1 << target)); } @@ -76,15 +76,23 @@ void BTEGui::displayPlayerInfo(const Player &player) { _output(stringBuffer); } -#if defined(AVR) +void BTEGui::log(const char *message) { + sprintf(stringBuffer, "%s", message); + _output(stringBuffer); +} +void BTEGui::log(uint8_t value) { + sprintf(stringBuffer, "%u", value); + _output(stringBuffer); +} + +#if not defined(NATIVE) static void sendApplication() { Serial.println(F("*.kwl")); Serial.println(F("clear_panel()")); Serial.println(F("set_grid_size(21,10)")); // current player - Serial.println( - F("add_text(4,8,xlarge,L,Current player: , 245, 240, 245,)")); + Serial.println(F("add_text(4,8,xlarge,L,Current player: , 245, 240, 245,)")); Serial.println(F("add_text(10,8,xlarge,C,1,245,240,245,M)")); // targets @@ -118,7 +126,7 @@ static void sendApplication() { Serial.println(F("add_button(19,5,25,N,|)")); Serial.println(F("add_button(0,8,30,R,|)")); - Serial.println(F("add_slider(10,9,8,100,1024,500,T ,|,1)")); + Serial.println(F("add_slider(10,9,8,1,50,1,T ,|,1)")); Serial.println(F("add_monitor(18,8,3,,1)")); Serial.println(F("set_panel_notes(-,,,)")); diff --git a/lib/Domain/BTEGui.hpp b/lib/Domain/Target/BTEGui.hpp similarity index 89% rename from lib/Domain/BTEGui.hpp rename to lib/Domain/Target/BTEGui.hpp index 52868f5..2b7090e 100644 --- a/lib/Domain/BTEGui.hpp +++ b/lib/Domain/Target/BTEGui.hpp @@ -16,7 +16,7 @@ */ #pragma once -#include +#include #include #if not defined(AVR) @@ -26,7 +26,7 @@ /** * GUI for Bluetooth Electronics interface (Android) */ -class BTEGui : public ITargetGui { +class BTEGui : public ITargetUi { uint8_t targetState; /* Bluetooth application uses a letter to identify which widget @@ -60,9 +60,11 @@ class BTEGui : public ITargetGui { #endif void setCurrentPlayer(uint8_t playerId) override; - void hitTarget(ITargetGui::TARGET target) override; - bool isTargetHit(ITargetGui::TARGET target) override; + void hitTarget(ITargetUi::TARGET target) override; + bool isTargetHit(ITargetUi::TARGET target); void resetTargets() override; void displayPlayerInfo(const Player &player) override; void restart() override; + void log(const char *) override; + void log(uint8_t value) override; }; \ No newline at end of file diff --git a/lib/Domain/Target/ITarget.hpp b/lib/Domain/Target/ITarget.hpp new file mode 100644 index 0000000..6ad5557 --- /dev/null +++ b/lib/Domain/Target/ITarget.hpp @@ -0,0 +1,50 @@ + +/* + * + * Copyright (c) 2023 Aurélien Labrosse + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#pragma once + +#include + +class ITarget { + +public: + enum State { Calibrating, Ready, Hit, Error }; + + virtual ~ITarget() {} + + /** + * @brief transition to Ready state + * + */ + virtual void reset() = 0; + + /** + * @brief transition to Calibration state + * + */ + virtual void calibrate() = 0; + + /** + * @brief transition to Hit state + * + */ + virtual void hit() = 0; + + virtual uint16_t getLuminosity() = 0; + + virtual State getState() = 0; +}; \ No newline at end of file diff --git a/lib/Domain/Target/ITargetHost.hpp b/lib/Domain/Target/ITargetHost.hpp new file mode 100644 index 0000000..6ca59f4 --- /dev/null +++ b/lib/Domain/Target/ITargetHost.hpp @@ -0,0 +1,48 @@ +/* + * + * Copyright (c) 2023 Aurélien Labrosse + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#pragma once + +#include +#include + +/** + * @brief Target host is a device managing 5 targets + * + * A target is considered hit if its luminosity value exceed (ambientValue + + * threshold) A target has a multicolor led: Off is ready, green is shot, blue + * is calibration, red is error + */ + +class ITargetHost { + + +public: + + virtual ~ITargetHost() {} + + virtual void reset() = 0; + + /** + * Notification led on + */ + virtual void ledOn() = 0; + + /** + * Notification led off + */ + virtual void ledOff() = 0; +}; \ No newline at end of file diff --git a/lib/Domain/ITargetGui.hpp b/lib/Domain/Target/ITargetUi.hpp similarity index 73% rename from lib/Domain/ITargetGui.hpp rename to lib/Domain/Target/ITargetUi.hpp index 6439432..97eceae 100644 --- a/lib/Domain/ITargetGui.hpp +++ b/lib/Domain/Target/ITargetUi.hpp @@ -16,46 +16,37 @@ */ #pragma once -#include #include +#include -class ITargetGui { +class ITargetUi { public: - - enum TARGET{ - One = 0, - Two, - Three, - Four, - Five - }; + enum TARGET { One = 0, Two, Three, Four, Five }; - virtual ~ITargetGui() {} + virtual ~ITargetUi() {} /** * Tell GUI a target has been hit - */ + */ virtual void hitTarget(TARGET) = 0; - /** - * Ask GUI if a target has been hit - */ - virtual bool isTargetHit(TARGET) = 0; - /** * Reset target hit status - */ + */ virtual void resetTargets() = 0; virtual void setCurrentPlayer(uint8_t playerId) = 0; /* - * Send player info : id, shoots and hit shoots - * - * - */ - virtual void displayPlayerInfo(const Player&) = 0; + * Send player info : id, shoots and hit shoots + * + * + */ + virtual void displayPlayerInfo(const Player &) = 0; virtual void restart() = 0; - + + virtual void log(const char *) = 0; + + virtual void log(uint8_t value) = 0; }; \ No newline at end of file diff --git a/lib/Domain/Target/LDRTarget.cpp b/lib/Domain/Target/LDRTarget.cpp new file mode 100644 index 0000000..c0e879a --- /dev/null +++ b/lib/Domain/Target/LDRTarget.cpp @@ -0,0 +1,49 @@ +/* + * + * Copyright (c) 2023 Aurélien Labrosse + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#include +#include + +LDRTarget::LDRTarget(uint8_t luminosityPin) + : ambiantLuminosity(0), luminosityPin(luminosityPin), state(Ready), + threshold(0) {} + +void LDRTarget::reset() { state = Ready; } + +void LDRTarget::calibrate() { + state = Calibrating; + getLuminosity(); + ambiantLuminosity = getLuminosity(); + ambiantLuminosity += getLuminosity(); + ambiantLuminosity += getLuminosity(); + ambiantLuminosity += getLuminosity(); + ambiantLuminosity /= 4; + reset(); +} + +boolean LDRTarget::check() { + return (getLuminosity() > (ambiantLuminosity + threshold)); +} + +void LDRTarget::hit() { state = Hit; } + +void LDRTarget::setThreshold(uint8_t threshold) { this->threshold = threshold; } + +uint16_t LDRTarget::getLuminosity() { + return analogRead(luminosityPin); +} + +ITarget::State LDRTarget::getState() { return state; } \ No newline at end of file diff --git a/lib/Domain/Target/LDRTarget.hpp b/lib/Domain/Target/LDRTarget.hpp new file mode 100644 index 0000000..6279562 --- /dev/null +++ b/lib/Domain/Target/LDRTarget.hpp @@ -0,0 +1,53 @@ + +/* + * + * Copyright (c) 2023 Aurélien Labrosse + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#pragma once +#include + +// forward declaration +class TargetHost; + +class LDRTarget : public ITarget { + + uint16_t ambiantLuminosity; + uint8_t luminosityPin; + State state; + uint8_t threshold; + +public: + LDRTarget(uint8_t luminosityPin); + + void reset() override; + void calibrate() override; + void hit() override; + uint16_t getLuminosity() override; + State getState() override; + + /** + * @brief Set the difference with ambiant luminosity level that triggers a hit + * condition + * + * @param threshold + */ + void setThreshold(uint8_t threshold); + + /** + * @brief Check if target is in hit condition + * + */ + bool check(); +}; \ No newline at end of file diff --git a/lib/Domain/Target/TargetHost.cpp b/lib/Domain/Target/TargetHost.cpp new file mode 100644 index 0000000..63249a2 --- /dev/null +++ b/lib/Domain/Target/TargetHost.cpp @@ -0,0 +1,150 @@ +/* + * + * Copyright (c) 2023 Aurélien Labrosse + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#include +#include +#include +#include +#include + +char serial_command_buffer_[16]; +SerialCommands _serial_commands(&Serial, serial_command_buffer_, + sizeof(serial_command_buffer_), "|", " "); + +static const uint8_t TRESHOLD_ADDRESS = 0; +static const uint8_t LED_PIN = 9; +static const uint8_t TARGET_A_PIN = A0; +static const uint8_t TARGET_Z_PIN = A1; +static const uint8_t TARGET_E_PIN = A2; +static const uint8_t TARGET_R_PIN = A3; +static const uint8_t TARGET_T_PIN = A4; + +/** + * Fallback method called when garbage command + * is received on the serial port + */ +static void serial_cmd_unrecognized_callback(SerialCommands *sender, + const char *cmd); + +static void serial_cmd_setThreshold_callback(SerialCommands *sender); +static void serial_cmd_changePlayer_callback(SerialCommands *sender); +static void serial_cmd_reset_callback(SerialCommands *sender); + +SerialCommand serial_cmd_setThreshold("T", &serial_cmd_setThreshold_callback); +SerialCommand serial_cmd_changePlayer("P", &serial_cmd_changePlayer_callback); +SerialCommand serial_cmd_reset("R", &serial_cmd_reset_callback); + +uint8_t TargetHost::getThreshold() { + if (thresholdCache == 0) { + thresholdCache = EEPROM.read(TRESHOLD_ADDRESS); + } + return thresholdCache; +} + +void TargetHost::storeThreshold(uint8_t threshold) { + thresholdCache = threshold; + EEPROM.update(TRESHOLD_ADDRESS, threshold); +} + +void TargetHost::ledOn() { digitalWrite(LED_PIN, HIGH); } + +void TargetHost::ledOff() { digitalWrite(LED_PIN, LOW); } + +void TargetHost::setup() { + + Serial.begin(115200); + + pinMode(LED_PIN, OUTPUT); + ledOff(); + + // load value in cache from EEPROM + getThreshold(); + + for (ITarget *target : targets) { + target->calibrate(); + } + _serial_commands.context = this; + _serial_commands.SetDefaultHandler(serial_cmd_unrecognized_callback); + _serial_commands.AddCommand(&serial_cmd_setThreshold); + _serial_commands.AddCommand(&serial_cmd_changePlayer); + _serial_commands.AddCommand(&serial_cmd_reset); + + ui->restart(); +} + +void TargetHost::loop() { + + static const uint8_t targetCount = (sizeof(targets) / sizeof(ITarget)); + for (uint8_t targetIndex = 0; targetIndex < targetCount; targetIndex++) { + ITarget *target = targets[targetIndex]; + if (target->getState() == ITarget::State::Hit) { + ITargetUi::TARGET uiTarget; + uiTarget = static_cast(targetIndex); + ui->hitTarget(uiTarget); + ledOn(); + delay(100); + ledOff(); + } + } + _serial_commands.ReadSerial(); + delay(5); +} + +void serial_cmd_unrecognized_callback(SerialCommands *sender, const char *cmd) { + sender->GetSerial()->print("Unrecognized command ["); + sender->GetSerial()->print(cmd); + sender->GetSerial()->println("]"); +} + +void serial_cmd_reset_callback(SerialCommands *sender) { + TargetHost *targetHost = static_cast(sender->context); + targetHost->game->reset(); + targetHost->ui->restart(); +} + +void serial_cmd_setThreshold_callback(SerialCommands *sender) { + + uint8_t value = atoi(sender->Next()); + TargetHost *targetHost = static_cast(sender->context); + + if (value > 0 && value != targetHost->getThreshold()) { + targetHost->storeThreshold(value); + targetHost->ui->log((const char *)F("Thrsh: ")); + targetHost->ui->log(value); + targetHost->ui->log("\n"); + } +} + +void serial_cmd_changePlayer_callback(SerialCommands *sender) { + uint8_t playerId = (uint8_t)atoi(sender->Next()); + TargetHost *targetHost = static_cast(sender->context); + targetHost->game->changeCurrentPlayerTo(playerId); +} + +void TargetHost::reset() { ui->resetTargets(); } + +TargetHost::TargetHost(Game *game, ITargetUi *ui) { + + this->game = game; + this->ui = ui; + + thresholdCache = 0; + targets[0] = new LDRTarget(A0); + targets[1] = new LDRTarget(A1); + targets[2] = new LDRTarget(A2); + targets[3] = new LDRTarget(A3); + targets[4] = new LDRTarget(A4); +} \ No newline at end of file diff --git a/lib/Domain/Target/TargetHost.hpp b/lib/Domain/Target/TargetHost.hpp new file mode 100644 index 0000000..1376de1 --- /dev/null +++ b/lib/Domain/Target/TargetHost.hpp @@ -0,0 +1,62 @@ +/* + * + * Copyright (c) 2023 Aurélien Labrosse + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#pragma once + +#include +#include +#include +#include + +/** + * Manages a collection of targets, the game logic and a GUI. + * Also manage the `threshold`, used to differentiate ambient + * luminosity level and 'hit' luminosity level. + */ +class TargetHost : public ITargetHost { + + /** + * Minimum light value to validate a hit. + * Shall be stored in EEPROM to allow a reset + * in same luminosity condition without reconfiguring + */ + uint8_t thresholdCache; + +public: + void storeThreshold(uint8_t threshold); + + /** + * Return value of difference needed between ambient light level and hit light + * level value [1;254] + */ + + uint8_t getThreshold(); + + ITarget *targets[5]; + Game *game; + ITargetUi *ui; + + TargetHost(Game *game, ITargetUi *ui); + + void ledOn() override; + void ledOff() override; + void reset() override; + + /* Specific methods */ + + void setup(); + void loop(); +}; \ No newline at end of file diff --git a/lib/Native/TestGui.hpp b/lib/Native/TestGui.hpp index e277d5c..8498c15 100644 --- a/lib/Native/TestGui.hpp +++ b/lib/Native/TestGui.hpp @@ -15,24 +15,28 @@ * along with this program. If not, see . */ #pragma once -#include +#include #include -class TestGui : public ITargetGui { +class TestGui : public ITargetUi { public: uint8_t targetState; TestGui() : targetState(0) {} - void hitTarget(ITargetGui::TARGET target) override { targetState |= (1 << target); } + void hitTarget(ITargetUi::TARGET target) override { + targetState |= (1 << target); + } - bool isTargetHit(ITargetGui::TARGET target) override { - return ( (targetState & (1 << target)) == (1 << target)); + bool isTargetHit(ITargetUi::TARGET target) { + return ((targetState & (1 << target)) == (1 << target)); } void setCurrentPlayer(uint8_t playerId) override {} void restart() override {} void resetTargets() override { targetState = 0; } void displayPlayerInfo(const Player &player) override {} + void log(const char *) override {} + void log(uint8_t value) override {} }; \ No newline at end of file diff --git a/platformio.ini b/platformio.ini index 4fd07e3..428fd48 100755 --- a/platformio.ini +++ b/platformio.ini @@ -1,12 +1,9 @@ [env] lib_compat_mode=off +lib_ldf_mode=deep -; these libraries are for AVR and simulation -; targets only, so a reset of this property -; is needed in the `native` configuration -lib_deps = - +lib_deps = #upload_protocol=custom #upload_flags = # -Pusb @@ -18,7 +15,8 @@ lib_deps = # -cavrisp2 # -cdragon_isp # -cavrispmkII -upload_command = avrdude -vv -b57600 -pm328p -c avrisp -Pcom10 $UPLOAD_FLAGS -U flash:w:$SOURCE:i +upload_command = avrdude -vv -b57600 -pm328p -c avrisp -Pcom11 $UPLOAD_FLAGS -U flash:w:$SOURCE:i + # upload_protocol=custom # upload_speed=115200 @@ -33,41 +31,37 @@ upload_command = avrdude -vv -b57600 -pm328p -c avrisp -Pcom10 $UPLOAD_FLAGS -U # '-b115200' [env:native] - lib_deps = + lib_deps = ${env.lib_deps} + SerialCommands=https://github.com/arcadien/Arduino-SerialCommands fakeit=https://github.com/FabioBatSilva/ArduinoFake.git platform=native - build_type = release + build_type = debug test_ignore = cross/* - build_flags = -DNATIVE - debug_test = noarch/test_gun + build_flags = + -DNATIVE + -std=gnu++17 + -Og -ggdb3 + debug_test = native/test_Target build_src_filter = ${env.src_filter} - - [env:native_debug] - lib_deps = ${env.lib_deps} + lib_deps = + ${env.lib_deps} + SerialCommands=https://github.com/arcadien/Arduino-SerialCommands fakeit=https://github.com/FabioBatSilva/ArduinoFake.git platform=native build_type = debug debug_build_flags = -Og -ggdb3 -g3 -DNATIVE -UAVR test_ignore = cross/* - debug_test = noarch/test_gun + debug_test = native/test_gun build_src_filter = ${env.src_filter} - - -[env:cross] - lib_deps = ${env.lib_deps} - test_framework = unity - platform = atmelavr - framework = arduino - board = ATmega328P - build_type = release - test_ignore = native/* - build_flags = -O0 - build_src_filter = ${env.src_filter} - - - [env:target] - lib_deps = +[env:target] + lib_deps = ${env.lib_deps} - SerialCommands + SerialCommands=https://github.com/arcadien/Arduino-SerialCommands + test_framework = unity framework = arduino platform = atmelavr board = ATmega328P @@ -83,12 +77,18 @@ upload_command = avrdude -vv -b57600 -pm328p -c avrisp -Pcom10 $UPLOAD_FLAGS -U lib_deps = adafruit/Adafruit SSD1306@^2.5.7 https://github.com/rocketscream/Low-Power#V1.81 + ${env.lib_deps} + framework = arduino platform = atmelavr board = ATmega328P + board_build.mcu = atmega328p + board_build.f_cpu = 16000000L build_type = release - build_flags = -DAVR + build_flags = + -DAVR + -Os + -std=gnu++17 test_ignore = native/* - noarch/* - build_src_filter = ${env.src_filter} - - \ No newline at end of file + build_src_filter = ${env.src_filter} - - \ No newline at end of file diff --git a/src/ConsoleTargets.cpp b/src/ConsoleTargets.cpp index 6e6ee73..2d504e2 100644 --- a/src/ConsoleTargets.cpp +++ b/src/ConsoleTargets.cpp @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -#include +#include #include #include #include diff --git a/src/TargetApp.cpp b/src/TargetApp.cpp index 18a914e..561cde9 100644 --- a/src/TargetApp.cpp +++ b/src/TargetApp.cpp @@ -15,164 +15,18 @@ * along with this program. If not, see . */ -#include +#include +#include #include - -#include - -#define TARGET_A_PIN A0 -#define TARGET_Z_PIN A1 -#define TARGET_E_PIN A2 -#define TARGET_R_PIN A3 -#define TARGET_T_PIN A4 - -#define LED_PIN 9 - -char serial_command_buffer_[32]; -SerialCommands serial_commands_(&Serial, serial_command_buffer_, - sizeof(serial_command_buffer_), "|", " "); -void cmd_unrecognized(SerialCommands *sender, const char *cmd); -void cmd_resetTargets(SerialCommands *sender); -void cmd_setThreshold(SerialCommands *sender); -void cmd_nextRound(SerialCommands *sender); -void cmd_changePlayer(SerialCommands *sender); - -SerialCommand cmd_nextRound_("N", &cmd_nextRound); -SerialCommand cmd_resetTargets_("R", &cmd_resetTargets); -SerialCommand cmd_setThreshold_("T", &cmd_setThreshold); -SerialCommand cmd_changePlayer_("P", &cmd_changePlayer); - -void serialPrintInfo(uint16_t value); -void ledOn(); -void ledOff(); - -// laser detection threshold -uint16_t threshold; - -// ambient light ADC value -uint16_t reference; +#include BTEGui gui; Game game(&gui); +TargetHost host(&game, &gui); void setup() { - - pinMode(LED_PIN, OUTPUT); - ledOff(); - - pinMode(TARGET_A_PIN, INPUT); - pinMode(TARGET_Z_PIN, INPUT); - pinMode(TARGET_E_PIN, INPUT); - pinMode(TARGET_R_PIN, INPUT); - pinMode(TARGET_T_PIN, INPUT); - - serial_commands_.SetDefaultHandler(cmd_unrecognized); - serial_commands_.AddCommand(&cmd_resetTargets_); - serial_commands_.AddCommand(&cmd_setThreshold_); - serial_commands_.AddCommand(&cmd_nextRound_); - serial_commands_.AddCommand(&cmd_changePlayer_); - - analogRead(TARGET_A_PIN); - analogRead(TARGET_Z_PIN); - analogRead(TARGET_E_PIN); - analogRead(TARGET_R_PIN); - analogRead(TARGET_T_PIN); - - reference = 0; - - reference += analogRead(TARGET_A_PIN); - reference += analogRead(TARGET_Z_PIN); - reference += analogRead(TARGET_E_PIN); - reference += analogRead(TARGET_R_PIN); - reference += analogRead(TARGET_T_PIN); - reference /= 5; - - // initial value sent by GUI at startup - threshold = 500; - - Serial.begin(115200); - + host.setup(); game.reset(); - serial_commands_.ReadSerial(); -} - -static void _recordHit() { - game.recordSucceededShoot(); - ledOn(); - delay(100); - ledOff(); -} - -static void _checkHit(uint16_t value, ITargetGui::TARGET target) { - if (value > threshold) { - if (!gui.isTargetHit(target)) { - gui.hitTarget(target); - _recordHit(); - } - } -} - -void loop() { - - uint16_t value1 = analogRead(A0); - uint16_t value2 = analogRead(A1); - uint16_t value3 = analogRead(A2); - uint16_t value4 = analogRead(A3); - uint16_t value5 = analogRead(A4); - - _checkHit(value1, ITargetGui::TARGET::One); - _checkHit(value2, ITargetGui::TARGET::Two); - _checkHit(value3, ITargetGui::TARGET::Three); - _checkHit(value4, ITargetGui::TARGET::Four); - _checkHit(value5, ITargetGui::TARGET::Five); - - serial_commands_.ReadSerial(); - delay(5); -} - -void serialPrintInfo(uint16_t value) { - if (value > 0) { - Serial.print(F("HV: ")); - Serial.println(value); - } -} -void cmd_unrecognized(SerialCommands *sender, const char *cmd) { - sender->GetSerial()->print("Unrecognized command ["); - sender->GetSerial()->print(cmd); - sender->GetSerial()->println("]"); -} -void cmd_setThreshold(SerialCommands *sender) { - uint16_t value = atoi(sender->Next()); - if (value > 0 && value != threshold) { - threshold = value; - Serial.print(F("VThresh: ")); - Serial.println(threshold); - Serial.print(F("Vref: ")); - Serial.println(reference); - } -} -void cmd_changePlayer(SerialCommands *sender) { - uint8_t playerId = (uint8_t)atoi(sender->Next()); - game.changeCurrentPlayerTo(playerId); -} -void cmd_resetTargets(SerialCommands *sender) { - - (void)sender; - game.reset(); - serialPrintInfo(0); - ledOn(); - delay(200); - ledOff(); - delay(200); - ledOn(); - delay(200); - ledOff(); -} -void cmd_nextRound(SerialCommands *sender) { - gui.resetTargets(); - game.nextRound(); - gui.setCurrentPlayer(game.currentPlayer->id); } -void ledOff() { digitalWrite(LED_PIN, HIGH); } -void ledOn() { digitalWrite(LED_PIN, LOW); } \ No newline at end of file +void loop() { host.loop(); } \ No newline at end of file diff --git a/test/native/test_BTEGui/BTEGui.cpp b/test/native/test_BTEGui/BTEGui.cpp index 072e834..b56b3cd 100644 --- a/test/native/test_BTEGui/BTEGui.cpp +++ b/test/native/test_BTEGui/BTEGui.cpp @@ -28,46 +28,46 @@ void expect_target_to_manage_5_targets() { BTEGui cut(fakeSerial); std::string actual; - cut.hitTarget(ITargetGui::TARGET::One); + cut.hitTarget(ITargetUi::TARGET::One); std::string expectedLine = "*AR255G255B255*"; std::getline(fakeSerial, actual); TEST_ASSERT_EQUAL_STRING_MESSAGE(expectedLine.c_str(), actual.c_str(), "Hit target shall be white"); - cut.hitTarget(ITargetGui::TARGET::Two); + cut.hitTarget(ITargetUi::TARGET::Two); expectedLine = "*ZR255G255B255*"; std::getline(fakeSerial, actual); TEST_ASSERT_EQUAL_STRING_MESSAGE(expectedLine.c_str(), actual.c_str(), "Hit target shall be white"); - cut.hitTarget(ITargetGui::TARGET::Three); + cut.hitTarget(ITargetUi::TARGET::Three); expectedLine = "*ER255G255B255*"; std::getline(fakeSerial, actual); TEST_ASSERT_EQUAL_STRING_MESSAGE(expectedLine.c_str(), actual.c_str(), "Hit target shall be white"); - cut.hitTarget(ITargetGui::TARGET::Four); + cut.hitTarget(ITargetUi::TARGET::Four); expectedLine = "*RR255G255B255*"; std::getline(fakeSerial, actual); TEST_ASSERT_EQUAL_STRING_MESSAGE(expectedLine.c_str(), actual.c_str(), "Hit target shall be white"); - cut.hitTarget(ITargetGui::TARGET::Five); + cut.hitTarget(ITargetUi::TARGET::Five); expectedLine = "*TR255G255B255*"; std::getline(fakeSerial, actual); TEST_ASSERT_EQUAL_STRING_MESSAGE(expectedLine.c_str(), actual.c_str(), "Hit target shall be white"); - TEST_ASSERT_TRUE(cut.isTargetHit(ITargetGui::TARGET::One)); - TEST_ASSERT_TRUE(cut.isTargetHit(ITargetGui::TARGET::Two)); - TEST_ASSERT_TRUE(cut.isTargetHit(ITargetGui::TARGET::Three)); - TEST_ASSERT_TRUE(cut.isTargetHit(ITargetGui::TARGET::Four)); - TEST_ASSERT_TRUE(cut.isTargetHit(ITargetGui::TARGET::Five)); + TEST_ASSERT_TRUE(cut.isTargetHit(ITargetUi::TARGET::One)); + TEST_ASSERT_TRUE(cut.isTargetHit(ITargetUi::TARGET::Two)); + TEST_ASSERT_TRUE(cut.isTargetHit(ITargetUi::TARGET::Three)); + TEST_ASSERT_TRUE(cut.isTargetHit(ITargetUi::TARGET::Four)); + TEST_ASSERT_TRUE(cut.isTargetHit(ITargetUi::TARGET::Five)); } void expect_gui_to_display_player_info() { diff --git a/test/native/test_Target/target.cpp b/test/native/test_Target/target.cpp new file mode 100644 index 0000000..d69e20f --- /dev/null +++ b/test/native/test_Target/target.cpp @@ -0,0 +1,348 @@ +/* + * + * Copyright (c) 2023 Aurélien Labrosse + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +using namespace fakeit; + +#include +#include +#include + +#include + +#define MOCKED_EEPROM_READ \ + OverloadedMethod(ArduinoFake(EEPROM), read, uint8_t(int)) + +#define MOCKED_EEPROM_UPDATE \ + OverloadedMethod(ArduinoFake(EEPROM), update, void(int, uint8_t)) + +void tearDown() {} +void setUp() { ArduinoFakeReset(); } + +static void mockForCommandTest(Mock &mockGui); +static void mockGuiForSetup(Mock &mockGui); +static void mockPrintAndPrinln(); + +static const uint8_t TRESHOLD_ADDRESS = 0; + +void expect_threshold_to_be_storable_in_eeprom() { + + // return LSB then MSB + When(MOCKED_EEPROM_READ.Using(TRESHOLD_ADDRESS)).Return(20); + When(MOCKED_EEPROM_UPDATE).AlwaysReturn(); + + Mock mockGui; + Game game(&mockGui.get()); + TargetHost app(&game, &mockGui.get()); + + app.storeThreshold(20); + TEST_ASSERT_EQUAL(20, app.getThreshold()); + + Verify(MOCKED_EEPROM_UPDATE.Using(TRESHOLD_ADDRESS, 20)).Once(); +} + +void expect_setup_to_configure_led_pin() { + Mock mockGui; + ITargetUi &ui = mockGui.get(); + Game game(&ui); + TargetHost app(&game, &ui); + When(Method(ArduinoFake(), pinMode)).Return(); + When(Method(ArduinoFake(), analogRead)).AlwaysReturn(0); + When(Method(ArduinoFake(), digitalWrite)).AlwaysReturn(); + When(MOCKED_EEPROM_READ).AlwaysReturn(0); + When(OverloadedMethod(ArduinoFake(Serial), begin, void(unsigned long))) + .AlwaysReturn(); + + mockGuiForSetup(mockGui); + + app.setup(); + + static const uint8_t LED_PIN = 9; + Verify(Method(ArduinoFake(), pinMode).Using(LED_PIN, OUTPUT)).Once(); +} + +void expect_ambient_level_to_be_sampled_at_startup_for_each_target() { + Mock mockGui; + ITargetUi &ui = mockGui.get(); + Game game(&ui); + TargetHost app(&game, &ui); + + When(Method(ArduinoFake(), pinMode)).Return(); + When(MOCKED_EEPROM_READ).AlwaysReturn(0); + When(OverloadedMethod(ArduinoFake(Serial), begin, void(unsigned long))) + .AlwaysReturn(); + + When(Method(ArduinoFake(), digitalWrite)).AlwaysReturn(); + When(Method(ArduinoFake(), analogRead)).AlwaysReturn(1000); + + mockGuiForSetup(mockGui); + + app.setup(); + + Verify(Method(ArduinoFake(), pinMode)).AtLeastOnce(); + + // Note: 5 reads for calibration + Verify(Method(ArduinoFake(), analogRead).Using(A0)).Exactly(5); + Verify(Method(ArduinoFake(), analogRead).Using(A1)).Exactly(5); + Verify(Method(ArduinoFake(), analogRead).Using(A2)).Exactly(5); + Verify(Method(ArduinoFake(), analogRead).Using(A3)).Exactly(5); + Verify(Method(ArduinoFake(), analogRead).Using(A4)).Exactly(5); +} + +void expect_status_led_to_blink_when_a_target_is_hit() { + + // Target is hit means target light value exceed threshold + + const uint8_t pins[] = {A0, A1, A2, A3, A4}; + const uint8_t LED_PIN = 9; + + for (uint8_t targetIndex = 0; targetIndex < 5; targetIndex++) { + + ArduinoFakeReset(); + + Mock mockGui; + ITargetUi &ui = mockGui.get(); + Game game(&ui); + TargetHost app(&game, &ui); + + // ARRANGE + + // threshold is 100 + When(MOCKED_EEPROM_READ.Using(TRESHOLD_ADDRESS)).Return(100); + + // and target 'targetIndex' luminosity is 150, other target luminosity is 0 + When(Method(ArduinoFake(), analogRead)).AlwaysReturn(0); + + // override only for 'analogRead(pins[targetIndex])' + When(Method(ArduinoFake(), analogRead).Using(pins[targetIndex])) + .AlwaysReturn(150); + + // blink + When(Method(ArduinoFake(), digitalWrite)).AlwaysReturn(); + When(Method(ArduinoFake(), delay)).AlwaysReturn(); + + // gui notification + When(Method(mockGui, hitTarget)).AlwaysReturn(); + + // No serial commands + When(Method(ArduinoFake(Serial), available)).AlwaysReturn(0); + + // ACT + app.loop(); + + // ASSERT + // luminosity of target has been read + Verify(Method(ArduinoFake(), analogRead).Using(pins[targetIndex])).Once(); + Verify(Method(ArduinoFake(), analogRead).Using(Ne(pins[targetIndex]))) + .Exactly(4_Times); + + // led has blink during 100 ms + Verify(Method(ArduinoFake(), digitalWrite).Using(LED_PIN, HIGH)).Once(); + Verify(Method(ArduinoFake(), digitalWrite).Using(LED_PIN, LOW)).Once(); + Verify(Method(ArduinoFake(), delay).Using(100)).Exactly(1_Times); + } +} + +void expect_gui_to_be_notified_when_a_target_is_hit() { + Mock mockGui; + ITargetUi &ui = mockGui.get(); + Game game(&ui); + TargetHost app(&game, &ui); + + // Run initialization and two loop() call. + // Ensure GUI is notified *once* when a target is hit + + // + // setup, provide a value of 10 for threshold + // + When(Method(ArduinoFake(), pinMode)).AlwaysReturn(); + When(MOCKED_EEPROM_READ.Using(TRESHOLD_ADDRESS)).AlwaysReturn(10); + When(OverloadedMethod(ArduinoFake(Serial), begin, void(unsigned long))) + .AlwaysReturn(); + + // + // blink + // + When(Method(ArduinoFake(), digitalWrite)).AlwaysReturn(); + When(Method(ArduinoFake(), delay)).AlwaysReturn(); + + // + // analog sampling, twice during setup then once during each loops + // + When(Method(ArduinoFake(), analogRead)).AlwaysReturn(1000); + When(Method(ArduinoFake(), analogRead).Using(A0)) + .Return(0, 99, 99, 99); // no hit + When(Method(ArduinoFake(), analogRead).Using(A1)) + .Return(0, 108, 118, 108); // no hit + When(Method(ArduinoFake(), analogRead).Using(A2)) + .Return(0, 104, 130, 130); // hit, then 'already hit' + When(Method(ArduinoFake(), analogRead).Using(A3)) + .Return(0, 103, 104, 103); // no hit + When(Method(ArduinoFake(), analogRead).Using(A4)) + .Return(0, 96, 200, 96); // hit + + // No serial commands + When(Method(ArduinoFake(Serial), available)).AlwaysReturn(0); + + // Setup GUI mock + mockGuiForSetup(mockGui); + + // second hit of target 2 should not + // trigger a new hit : target 2 shall + // report as already hit + + app.setup(); + app.loop(); + app.loop(); + + Verify(Method(ArduinoFake(), pinMode)).AtLeastOnce(); + Verify(Method(ArduinoFake(), analogRead)).AtLeastOnce(); + Verify(Method(mockGui, hitTarget)).Exactly(2_Times); +} + +void expect_threshold_to_be_settable_via_serial_command() { + Mock mockGui; + ITargetUi &ui = mockGui.get(); + Game game(&ui); + TargetHost app(&game, &ui); + + mockForCommandTest(mockGui); + + // + // incoming serial command + // + When(Method(ArduinoFake(Serial), available)).Return(5, 4, 3, 2, 1, 0); + When(Method(ArduinoFake(Serial), read)).Return('T', ' ', '9', '6', '|'); + + app.setup(); + app.loop(); + + Verify(Method(ArduinoFake(Serial), read)).Exactly(5_Times); + + static const uint8_t expectedThresholdValue = 96; + TEST_ASSERT_EQUAL(expectedThresholdValue, app.getThreshold()); + + // check UI feedback + Verify(OverloadedMethod(mockGui, log, void(const char *))).Exactly(2_Times); + Verify(OverloadedMethod(mockGui, log, void(uint8_t))).Exactly(1_Times); + Verify(MOCKED_EEPROM_UPDATE).Once(); +} + +void expect_player_to_be_changeable_via_serial_command() { + Mock mockGui; + Game game(&mockGui.get()); + TargetHost app(&game, &mockGui.get()); + + mockForCommandTest(mockGui); + + // + // incoming serial command + // + When(Method(ArduinoFake(Serial), available)).Return(4, 3, 2, 1, 0); + When(Method(ArduinoFake(Serial), read)).Return('P', ' ', '2', '|'); + + app.setup(); + app.loop(); + + Verify(Method(ArduinoFake(Serial), read)).Exactly(4_Times); + + static const uint8_t expectedPlayerId = 2; + TEST_ASSERT_EQUAL(expectedPlayerId, app.game->currentPlayer->id); +} + +int main(int, char **) { + UNITY_BEGIN(); + RUN_TEST(expect_threshold_to_be_storable_in_eeprom); + RUN_TEST(expect_setup_to_configure_led_pin); + RUN_TEST(expect_ambient_level_to_be_sampled_at_startup_for_each_target); + RUN_TEST(expect_status_led_to_blink_when_a_target_is_hit); + RUN_TEST(expect_gui_to_be_notified_when_a_target_is_hit); + RUN_TEST(expect_threshold_to_be_settable_via_serial_command); + RUN_TEST(expect_player_to_be_changeable_via_serial_command); + + UNITY_END(); + return 0; +} + +static void mockForCommandTest(Mock &mockGui) { + // + // setup, provide a value of 10 for threshold + // If serial command interpretation does not work, + // threshold value at end of test will be 10. + // + When(Method(ArduinoFake(), pinMode)).AlwaysReturn(); + When(MOCKED_EEPROM_READ.Using(TRESHOLD_ADDRESS)).AlwaysReturn(10); + When(MOCKED_EEPROM_UPDATE).AlwaysReturn(); + + // + // blink + // + When(Method(ArduinoFake(), digitalWrite)).AlwaysReturn(); + When(Method(ArduinoFake(), delay)).AlwaysReturn(); + When(Method(ArduinoFake(), analogRead)).AlwaysReturn(1000); + + // + // gui notification + // + mockGuiForSetup(mockGui); + When(Method(mockGui, setCurrentPlayer)).AlwaysReturn(); + When(Method(mockGui, resetTargets)).AlwaysReturn(); + When(Method(mockGui, displayPlayerInfo)).AlwaysReturn(); + + When(OverloadedMethod(ArduinoFake(Serial), begin, void(unsigned long))) + .AlwaysReturn(); + + When(Method(ArduinoFake(Serial), end)).AlwaysReturn(); + When(Method(ArduinoFake(Serial), flush)).AlwaysReturn(); + mockPrintAndPrinln(); +} + +// Mock all GUI method to return nothing +static void mockGuiForSetup(Mock &mockGui) { + When(Method(mockGui, restart)).AlwaysReturn(); + When(Method(mockGui, displayPlayerInfo)).AlwaysReturn(); + When(Method(mockGui, setCurrentPlayer)).AlwaysReturn(); + When(Method(mockGui, resetTargets)).AlwaysReturn(); + When(Method(mockGui, hitTarget)).AlwaysReturn(); + When(OverloadedMethod(mockGui, log, void(const char *))).AlwaysReturn(); + When(OverloadedMethod(mockGui, log, void(uint8_t))).AlwaysReturn(); +} + +static void mockPrintAndPrinln() { + + // clang-format off + When(OverloadedMethod(ArduinoFake(Print), print, size_t(char))).AlwaysReturn(); + When(OverloadedMethod(ArduinoFake(Print), print, size_t(const char *))).AlwaysReturn(); + When(OverloadedMethod(ArduinoFake(Print), print, size_t(unsigned char, int))).AlwaysReturn(); + When(OverloadedMethod(ArduinoFake(Print), print, size_t(int, int))).AlwaysReturn(); + When(OverloadedMethod(ArduinoFake(Print), print, size_t(long, int))).AlwaysReturn(); + When(OverloadedMethod(ArduinoFake(Print), print, size_t(double, int))).AlwaysReturn(); + When(OverloadedMethod(ArduinoFake(Print), print, size_t(unsigned int, int))).AlwaysReturn(); + When(OverloadedMethod(ArduinoFake(Print), print, size_t(unsigned long, int))).AlwaysReturn(); + + When(OverloadedMethod(ArduinoFake(Print), println, size_t())).AlwaysReturn(); + When(OverloadedMethod(ArduinoFake(Print), println, size_t(char))).AlwaysReturn(); + When(OverloadedMethod(ArduinoFake(Print), println, size_t(const char *))).AlwaysReturn(); + When(OverloadedMethod(ArduinoFake(Print), println, size_t(unsigned char, int))).AlwaysReturn(); + When(OverloadedMethod(ArduinoFake(Print), println, size_t(int, int))).AlwaysReturn(); + When(OverloadedMethod(ArduinoFake(Print), println, size_t(long, int))).AlwaysReturn(); + When(OverloadedMethod(ArduinoFake(Print), println, size_t(double, int))).AlwaysReturn(); + When(OverloadedMethod(ArduinoFake(Print), println, size_t(unsigned int, int))).AlwaysReturn(); + When(OverloadedMethod(ArduinoFake(Print), println, size_t(unsigned long, int))).AlwaysReturn(); + + // clang-format on +} \ No newline at end of file