From cdd0fcf26e6ba09b0a3ec2b1ae1c26b5b9a49f9e Mon Sep 17 00:00:00 2001 From: Aurelien Labrosse Date: Fri, 24 Nov 2023 09:35:51 +0000 Subject: [PATCH 01/12] fix: Set constructor as explicit --- lib/Domain/BTEGui.hpp | 2 +- lib/Domain/Game.hpp | 2 +- lib/Domain/Player.cpp | 5 +---- lib/Domain/Player.hpp | 2 +- 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/lib/Domain/BTEGui.hpp b/lib/Domain/BTEGui.hpp index 52868f5..e316c08 100644 --- a/lib/Domain/BTEGui.hpp +++ b/lib/Domain/BTEGui.hpp @@ -56,7 +56,7 @@ class BTEGui : public ITargetGui { BTEGui() : targetState(0) {} #else std::ostream &out; - BTEGui(std::ostream &out) : targetState(0), out(out) {} + explicit BTEGui(std::ostream &out) : targetState(0), out(out) {} #endif void setCurrentPlayer(uint8_t playerId) override; diff --git a/lib/Domain/Game.hpp b/lib/Domain/Game.hpp index 319e92d..bc8615d 100644 --- a/lib/Domain/Game.hpp +++ b/lib/Domain/Game.hpp @@ -43,7 +43,7 @@ class Game { uint8_t currentRound; Player *currentPlayer; - Game(ITargetGui *); + explicit Game(ITargetGui *); void recordSucceededShoot(); diff --git a/lib/Domain/Player.cpp b/lib/Domain/Player.cpp index f874b81..1e5977d 100644 --- a/lib/Domain/Player.cpp +++ b/lib/Domain/Player.cpp @@ -37,12 +37,9 @@ void Player::reset() { totalShoots = 0; } -void Player::startRound() { - totalShoots += 5; -} +void Player::startRound() { totalShoots += 5; } void Player::endRound() { - currentRound = 0; if (currentRound < (ROUND_COUNT - 1)) { currentRound++; } diff --git a/lib/Domain/Player.hpp b/lib/Domain/Player.hpp index f610225..9027a57 100644 --- a/lib/Domain/Player.hpp +++ b/lib/Domain/Player.hpp @@ -32,7 +32,7 @@ class Player { uint8_t currentRound; uint8_t totalShoots; - Player(uint8_t id); + explicit Player(uint8_t id); uint8_t getTotalHitCount() const; From 3bd5699c197c6b320477d9e77ba09f168da56b7b Mon Sep 17 00:00:00 2001 From: Aurelien Labrosse Date: Fri, 24 Nov 2023 09:39:17 +0000 Subject: [PATCH 02/12] chore: CI Executes tests in verbose mode --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8f85615..3557901 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -21,7 +21,7 @@ jobs: run: pip install --upgrade platformio - name: Test project in native environment - run: pio test -e native + run: pio test -vv -e native - name: Build console application run: pio run -e native From 634aed7e1a9efd12ca5471ba705f21debc8552f6 Mon Sep 17 00:00:00 2001 From: Aurelien Labrosse Date: Fri, 24 Nov 2023 09:39:37 +0000 Subject: [PATCH 03/12] doc: Update README with real behaviour --- README.md | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 08b24b6..a9863ca 100755 --- a/README.md +++ b/README.md @@ -1,44 +1,48 @@ # Five In A Row -Set of two applications targetting Atmel/Microchip [ATMega328P](https://www.microchip.com/en-us/product/atmega328p), implementing shooting game. +Set of applications targetting Atmel/Microchip [ATMega328P](https://www.microchip.com/en-us/product/atmega328p), implementing shooting games. -Using a *gun*, users tries to hit *targets*. +Using a _gun_, users tries to hit _targets_ managed by a _target host_. The game is up to four players: yellow, green, red and blue. There are 5 targets. ### Game mode -#### Biathlon like -Each player turn gives 5 shots. Player tries to hit each target. Total number of shoots is counted, as well as successfull hits. +Various games will be implemented. + +#### Simple shooter + +Each player shoot until the five targets are hit. Number of shoots is counted on the gun. Successful hits are counted by the target host and sent to the remote application. ## Functional details -### target -The **target** application manages player points and turns. It communicates with an Android application implemented on top of [Bluetooth Electronics](https://www.keuwl.com/apps/bluetoothelectronics/) application. It manages the game logic and player points. +### target +The **target host** application manages player points and turns. It communicates with an Android application implemented on top of [Bluetooth Electronics](https://www.keuwl.com/apps/bluetoothelectronics/) application. It manages the game logic and player points. Various games and modes can be implemented for the host. Each of these could have a specific remote UI. ### Gun + The **Gun** application manages the gun. Using two buttons, it allows: -| Press | Button 1 | Button 2 | -|-------|------------------|--------------------------------| -| short | shoot | Begin new round (give 5 shots) | -| long | power off | continuous laser (calibration) | +| Press | Button 1 | Button 2 | +| ----- | -------- | ------------------------------ | +| short | shoot | reset shoot count | +| long | N/A | continuous laser (calibration) | Gun application manages following outputs: -* Laser -* vibrator -* 128x32 I²C display + +- Laser +- vibrator +- 128x32 I²C display ## Architecture details Some parts of the projet, implementing logic, can be used on both native and target platform. All that code is represented in the "domain" package below. -Both **target** and **Gun** application are run both native and cross environment. The current -target hardware for both is the [ATMega328p](https://www.microchip.com/en-us/product/atmega328p) chip from Atmel/Microchip. +Current target hardware for all applications is the [ATMega328p](https://www.microchip.com/en-us/product/atmega328p) chip from Atmel/Microchip. To facilitate testing and extension, user interface (Gui) and hardware abstraction (HAL) are provided using interfaces. Implementation exists for native and cross (i.e. testing) environments. -Note: The *BTEGui* implementation can be used in native environment. Based on plain string exchange, if it may be difficult to interpret. +Note: The _BTEGui_ implementation can be used in native environment. Based on plain string exchange, if it may be difficult to interpret. ![Architecture overview](architecture.png) From 6b80f446aa7d48841662b1c51ccd67ea75ce8a90 Mon Sep 17 00:00:00 2001 From: Aurelien Labrosse Date: Fri, 24 Nov 2023 09:40:18 +0000 Subject: [PATCH 04/12] feat: Implement Contactor --- lib/Domain/Contactor.cpp | 50 ++++++++ lib/Domain/Contactor.hpp | 48 ++++++++ test/noarch/test_contactor/contactor.cpp | 147 +++++++++++++++++++++++ 3 files changed, 245 insertions(+) create mode 100644 lib/Domain/Contactor.cpp create mode 100644 lib/Domain/Contactor.hpp create mode 100644 test/noarch/test_contactor/contactor.cpp diff --git a/lib/Domain/Contactor.cpp b/lib/Domain/Contactor.cpp new file mode 100644 index 0000000..a14956d --- /dev/null +++ b/lib/Domain/Contactor.cpp @@ -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 . + */ + +#include + +void Contactor::checkForLongPress(long now) { + if ((downStartTime > 0) && ((now - downStartTime) >= LONG_PRESS)) { + downStartTime = 0; + onLongPress(); + inALongPress = true; + } +} +void Contactor::onDown(long now) { + currentState = State::Down; + downStartTime = now; +} + +void Contactor::onUp(long now) { + currentState = State::Up; + checkForLongPress(now); + if (!inALongPress) { + onShortPress(); + } + inALongPress = false; + downStartTime = 0; +} + +void Contactor::processPendingEvent(long now) { + Event toProcess = pendingEvent; + pendingEvent = Event::NoEvent; + if (toProcess == Event::Released) { + onUp(now); + } else if (toProcess == Event::Pressed) { + onDown(now); + } +} \ No newline at end of file diff --git a/lib/Domain/Contactor.hpp b/lib/Domain/Contactor.hpp new file mode 100644 index 0000000..00fd659 --- /dev/null +++ b/lib/Domain/Contactor.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 + +class Contactor { + + static const int LONG_PRESS = 2000; + long downStartTime = 0; + bool inALongPress = false; + +public: + enum Event { NoEvent, Pressed, Released }; + enum State { Up, Down }; + + /** + * @brief Event can be detected in an interrupt, and processed later + * + */ + Event pendingEvent; + + State currentState; + + Contactor() : pendingEvent(Event::NoEvent), currentState(State::Up) {} + + virtual void checkForLongPress(long now); + virtual void onDown(long now); + virtual void onUp(long now); + virtual void processPendingEvent(long now); + + virtual ~Contactor() {} + + virtual void onShortPress(){}; + virtual void onLongPress(){}; +}; \ No newline at end of file diff --git a/test/noarch/test_contactor/contactor.cpp b/test/noarch/test_contactor/contactor.cpp new file mode 100644 index 0000000..38bc3d5 --- /dev/null +++ b/test/noarch/test_contactor/contactor.cpp @@ -0,0 +1,147 @@ +/* + * + * 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 + +using namespace fakeit; + +Contactor obj; +Mock spy(obj); + +void tearDown() { spy.Reset(); } + +void setUp() {} + +void expect_short_press_to_be_evaluated_on_contactor_up() { + + Spy(Method(spy, onShortPress)); + Contactor &sut = spy.get(); + + long now = 1; + + sut.onDown(now); + sut.onUp(now); + + Verify(Method(spy, onShortPress)).Once(); +} + +void expect_long_press_to_be_evaluated_on_contactor_up() { + + Spy(Method(spy, onShortPress), Method(spy, onLongPress), + Method(spy, checkForLongPress)); + Contactor &sut = spy.get(); + + long now = 1; + + sut.onDown(now); + sut.onUp(now + 2002); + + Verify(Method(spy, checkForLongPress)).Once(); + Verify(Method(spy, onLongPress)).Once(); +} + +void expect_long_press_to_not_be_called_on_up_in_a_too_short_time() { + + Spy(Method(spy, onShortPress), Method(spy, onLongPress), + Method(spy, checkForLongPress)); + Contactor &sut = spy.get(); + + long now = 1; + + sut.onDown(now); + sut.onUp(now + 100); + + Verify(Method(spy, onShortPress)).Once(); + Verify(Method(spy, checkForLongPress)).Once(); + Verify(Method(spy, onLongPress)).Exactly(0); +} + +void expect_long_press_to_be_called_on_up_in_a_sufficient_time() { + + Spy(Method(spy, onShortPress), Method(spy, onLongPress), + Method(spy, checkForLongPress)); + Contactor &sut = spy.get(); + + long now = 1; + + sut.onDown(now); + sut.onUp(now + 2500); + + Verify(Method(spy, checkForLongPress)).Once(); + Verify(Method(spy, onLongPress)).Once(); +} + +void expect_long_press_to_not_trigger_short_press_on_release() { + + Spy(Method(spy, onShortPress), Method(spy, onLongPress), + Method(spy, checkForLongPress)); + Contactor &sut = spy.get(); + + long now = 1; + + sut.onDown(now); + sut.onUp(now + 2500); + + Verify(Method(spy, onShortPress)).Exactly(0); +} + +void expect_long_press_to_not_evaluate_if_button_is_not_down() { + + Spy(Method(spy, onShortPress), Method(spy, onLongPress)); + Contactor &sut = spy.get(); + + long now = 1; + + sut.onDown(now); + sut.onUp(now + 100); + + sut.checkForLongPress(now + 3000); + + Verify(Method(spy, onShortPress)).Once(); + Verify(Method(spy, onLongPress)).Exactly(0); +} + +void expect_long_press_to_be_evaluatable_while_contactor_is_down() { + + Spy(Method(spy, onShortPress), Method(spy, onLongPress)); + Contactor &sut = spy.get(); + + long now = 1; + + sut.onDown(now); + sut.checkForLongPress(now + 2002); + + Verify(Method(spy, onShortPress)).Exactly(0); + Verify(Method(spy, onLongPress)).Once(); +} + + + +int main(int, char **) { + UNITY_BEGIN(); + RUN_TEST(expect_short_press_to_be_evaluated_on_contactor_up); + RUN_TEST(expect_long_press_to_be_evaluated_on_contactor_up); + RUN_TEST(expect_long_press_to_not_be_called_on_up_in_a_too_short_time); + RUN_TEST(expect_long_press_to_be_called_on_up_in_a_sufficient_time); + RUN_TEST(expect_long_press_to_not_trigger_short_press_on_release); + RUN_TEST(expect_long_press_to_not_evaluate_if_button_is_not_down); + RUN_TEST(expect_long_press_to_be_evaluatable_while_contactor_is_down); + + return UNITY_END(); +} From bee145fe4c6646506d6b3b0e2b572a95eedc9d0e Mon Sep 17 00:00:00 2001 From: Aurelien Labrosse Date: Fri, 24 Nov 2023 09:42:10 +0000 Subject: [PATCH 05/12] feat: Implement UI and HAL interfaces for Gun --- lib/Domain/{ => Gun}/IGunHal.hpp | 26 +++++++++++++++++++------- lib/Domain/Gun/IGunUi.hpp | 31 +++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 7 deletions(-) rename lib/Domain/{ => Gun}/IGunHal.hpp (64%) create mode 100644 lib/Domain/Gun/IGunUi.hpp diff --git a/lib/Domain/IGunHal.hpp b/lib/Domain/Gun/IGunHal.hpp similarity index 64% rename from lib/Domain/IGunHal.hpp rename to lib/Domain/Gun/IGunHal.hpp index 0f6c27f..ebc9bc8 100644 --- a/lib/Domain/IGunHal.hpp +++ b/lib/Domain/Gun/IGunHal.hpp @@ -16,18 +16,30 @@ */ #pragma once +#include + +// forward declaration +class Gun; + class IGunHal { public: virtual ~IGunHal() {} - virtual bool isButton1Pressed() = 0; - virtual bool isButton2Pressed() = 0; - virtual void shortDelay() = 0; - virtual void longDelay() = 0; - virtual void ledOn() = 0; - virtual void ledOff() = 0; + + virtual void setGun(Gun *gun) = 0; + + /* + * the 'loop' method shall be called each 10ms + */ + virtual void setupHeartbeat() = 0; + virtual bool triggerIsUp() = 0; + virtual bool buttonIsUp() = 0; virtual void laserOn() = 0; virtual void laserOff() = 0; virtual void vibrationOn() = 0; virtual void vibrationOff() = 0; - virtual void deepSleep() = 0; + virtual uint16_t getBatteryVoltageMv() = 0; + virtual uint8_t getBatteryVoltagePercent() = 0; + virtual bool isCharging() = 0; + virtual void sleep() = 0; + virtual void configureInputCallbacks() = 0; }; \ No newline at end of file diff --git a/lib/Domain/Gun/IGunUi.hpp b/lib/Domain/Gun/IGunUi.hpp new file mode 100644 index 0000000..3f4a60e --- /dev/null +++ b/lib/Domain/Gun/IGunUi.hpp @@ -0,0 +1,31 @@ +/* + * + * 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 IGunUi { + +public: + virtual void setup() = 0; + virtual void displaySplash(uint16_t timeoutMs) = 0; + virtual void displayBatteryStatus(uint16_t mv, uint8_t percent) = 0; + virtual void displayChargingStatus(bool isCharging) = 0; + virtual void displayShootCount(uint16_t shootCount) = 0; + virtual void displayCalibration() = 0; + virtual void clearCalibration() = 0; +}; \ No newline at end of file From 41b83f841d0cbd626544fbe10c2b9e3d2319e193 Mon Sep 17 00:00:00 2001 From: Aurelien Labrosse Date: Fri, 24 Nov 2023 09:43:26 +0000 Subject: [PATCH 06/12] feat: Implement ATMega328P Gun Hal --- lib/Cross/Gun/Atmega328pHal.cpp | 103 ++++++++++++++++++++++++++++++++ lib/Cross/Gun/Atmega328pHal.hpp | 49 +++++++++++++++ 2 files changed, 152 insertions(+) create mode 100644 lib/Cross/Gun/Atmega328pHal.cpp create mode 100644 lib/Cross/Gun/Atmega328pHal.hpp diff --git a/lib/Cross/Gun/Atmega328pHal.cpp b/lib/Cross/Gun/Atmega328pHal.cpp new file mode 100644 index 0000000..16121bd --- /dev/null +++ b/lib/Cross/Gun/Atmega328pHal.cpp @@ -0,0 +1,103 @@ +/* + * + * 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 + +#define BSP_TICKS_PER_SEC 100 +#define F_CPU 16000000L + +#define LASER_PIN 10 +#define VIBRATOR_PIN 6 +#define BATTERY_VOLTAGE_PIN A3 +#define CHARGING_STATE_PIN A2 +#define BUTTON_PIN 3 +#define TRIGGER_PIN 2 + +#define MIN_BAT_VOLTAGE 3000 +#define MAX_BAT_VOLTAGE 4120 + +Atmega328pHal::Atmega328pHal() { + pinMode(VIBRATOR_PIN, OUTPUT); + pinMode(LASER_PIN, OUTPUT); + pinMode(TRIGGER_PIN, INPUT_PULLUP); + pinMode(BUTTON_PIN, INPUT_PULLUP); +} + +bool Atmega328pHal::triggerIsUp() { return bit_is_set(PIND, PD2); } +bool Atmega328pHal::buttonIsUp() { return bit_is_clear(PIND, PD3); } + +/* + * the 'loop' method shall be called each 10ms + * + * Configure timer2 so that an interrupt occurs each 10ms + * and wake up the MCU if it is in sleep mode. + */ +void Atmega328pHal::setupHeartbeat() { + TCCR2A = (1U << WGM21) | (0U << WGM20); // set Timer2 in CTC mode + TCCR2B = (1U << CS22) | (1U << CS21) | (1U << CS20); // 1/1024 prescaler + TIMSK2 = (1U << OCIE2A); // enable compare Interrupt + ASSR &= ~(1U << AS2); + TCNT2 = 0U; + + // set the output-compare register based on the desired tick frequency + OCR2A = (F_CPU / BSP_TICKS_PER_SEC / 1024U) - 1U; +} + +void Atmega328pHal::laserOn() { PORTB |= (1 << PB2); } +void Atmega328pHal::laserOff() { PORTB &= ~(1 << PB2); } +void Atmega328pHal::vibrationOn() { PORTD |= (1 << PD6); } +void Atmega328pHal::vibrationOff() { PORTD &= ~(1 << PD6); } + +uint16_t Atmega328pHal::getBatteryVoltageMv() { + + analogRead(BATTERY_VOLTAGE_PIN); + float rawBatt = analogRead(BATTERY_VOLTAGE_PIN); + rawBatt += analogRead(BATTERY_VOLTAGE_PIN); + rawBatt += analogRead(BATTERY_VOLTAGE_PIN); + rawBatt += analogRead(BATTERY_VOLTAGE_PIN); + rawBatt /= 4; + uint16_t battMv = (5000 / 1023.f) * rawBatt; + + return battMv; +}; + +uint8_t Atmega328pHal::getBatteryVoltagePercent() { + return map(getBatteryVoltageMv(), MIN_BAT_VOLTAGE, MAX_BAT_VOLTAGE, 0, 100); +} + +bool Atmega328pHal::isCharging() { + return (digitalRead(CHARGING_STATE_PIN) == HIGH); +} + +void Atmega328pHal::sleep() { + LowPower.idle(SLEEP_15MS, ADC_OFF, TIMER2_ON, TIMER1_OFF, TIMER0_OFF, SPI_OFF, + USART0_OFF, TWI_OFF); +} + +void Atmega328pHal::setGun(Gun *gun) { this->gun = gun; } + +extern void buttonInterruptHandler(); +extern void triggerInterruptHandler(); + +void Atmega328pHal::configureInputCallbacks() { + attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), buttonInterruptHandler, + CHANGE); + attachInterrupt(digitalPinToInterrupt(TRIGGER_PIN), triggerInterruptHandler, + CHANGE); +} \ No newline at end of file diff --git a/lib/Cross/Gun/Atmega328pHal.hpp b/lib/Cross/Gun/Atmega328pHal.hpp new file mode 100644 index 0000000..7881bf0 --- /dev/null +++ b/lib/Cross/Gun/Atmega328pHal.hpp @@ -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 . + */ +#pragma once + +#include + +class Atmega328pHal : public IGunHal { + Gun *gun; + +public: + ~Atmega328pHal() {} + + Atmega328pHal(); + + /* + * the 'loop' method shall be called each 10ms + * + * Configure timer2 so that an interrupt occurs each 10ms + * and wake up the MCU if it is in sleep mode. + */ + void setupHeartbeat() override; + + void setGun(Gun *gun) override; + void laserOn() override; + void laserOff() override; + void vibrationOn() override; + void vibrationOff() override; + bool triggerIsUp() override; + bool buttonIsUp() override; + uint16_t getBatteryVoltageMv() override; + uint8_t getBatteryVoltagePercent() override; + bool isCharging() override; + void configureInputCallbacks() override; + void sleep() override; +}; \ No newline at end of file From 0b0a225ba1747ea73aa9eae5d88eff97dadf31d8 Mon Sep 17 00:00:00 2001 From: Aurelien Labrosse Date: Fri, 24 Nov 2023 09:44:11 +0000 Subject: [PATCH 07/12] feat: Implement SSD1306 Gun UI --- lib/Cross/Gun/SSD1306Ui.hpp | 108 ++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 lib/Cross/Gun/SSD1306Ui.hpp diff --git a/lib/Cross/Gun/SSD1306Ui.hpp b/lib/Cross/Gun/SSD1306Ui.hpp new file mode 100644 index 0000000..6fd6b28 --- /dev/null +++ b/lib/Cross/Gun/SSD1306Ui.hpp @@ -0,0 +1,108 @@ +/* + * + * 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 + +#define SCREEN_WIDTH 128 // OLED display width, in pixels +#define SCREEN_HEIGHT 32 // OLED display height, in pixels +#define OLED_RESET -1 // Reset pin # (or -1 if sharing Arduino reset pin) +// 0x3D for 128x64, 0x3C for 128x32 +#define SCREEN_ADDRESS 0x3C + +static Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); + +/** + * @brief UI on a SSD1306 OLED display wired over I²C bus + * + * Used https://rickkas7.github.io/DisplayGenerator/index.html for prototyping + */ +class SSD1306Ui : public IGunUi { +public: + SSD1306Ui() {} + + void setup() override { + if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) { + for (;;) + ; // Don't proceed, loop forever + } + } + + void displaySplash(uint16_t timeoutMs) override { + display.clearDisplay(); + display.setTextColor(WHITE); + display.setTextSize(1); + display.setFont(&FreeMonoOblique9pt7b); + display.setCursor(9, 11); + display.println("5 in a row"); + display.setCursor(4, 27); + display.println(" # # # # #"); + display.display(); + delay(timeoutMs); + display.clearDisplay(); + display.display(); + } + + void displayBatteryStatus(uint16_t mv, uint8_t percent) override { + display.setFont(NULL); + display.fillRect(100, 0, 25, 9, 0); // clear + display.setCursor(100, 2); + display.print(percent); + display.println("%"); + } + + void displayChargingStatus(bool isCharging) override { + if (isCharging) { + display.setFont(NULL); + display.setCursor(80, 15); + display.println("charging"); + } else { + display.fillRect(80, 12, 47, 11, 0); // clear + } + } + + void displayShootCount(uint16_t count) override { + display.fillRect(11, 0, 68, 30, 0); // clear + display.setFont(&FreeMonoBold18pt7b); + if (count > 999) { + count = 0; + } + if (count < 10) { + display.setCursor(15, 25); + } else { + display.setCursor(10, 25); + } + display.print(count); + display.display(); + } + + void clearCalibration() override { + display.fillRect(2, 10, 126, 22, 0); // clear + } + + void displayCalibration() override { + display.clearDisplay(); + display.setFont(&FreeMonoOblique9pt7b); + display.setCursor(2, 25); + display.print("Calibration"); + display.display(); + } +}; \ No newline at end of file From 12ac46a34cce6d0ff3b6866d918c502c0a22798b Mon Sep 17 00:00:00 2001 From: Aurelien Labrosse Date: Fri, 24 Nov 2023 09:46:11 +0000 Subject: [PATCH 08/12] feat: Implement Button --- lib/Domain/Gun/Button.cpp | 22 ++++++++++++++++++++++ lib/Domain/Gun/Button.hpp | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 lib/Domain/Gun/Button.cpp create mode 100644 lib/Domain/Gun/Button.hpp diff --git a/lib/Domain/Gun/Button.cpp b/lib/Domain/Gun/Button.cpp new file mode 100644 index 0000000..6db3ad7 --- /dev/null +++ b/lib/Domain/Gun/Button.cpp @@ -0,0 +1,22 @@ +/* + * + * 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 + +void Button::setGun(Gun *gun) { this->gun = gun; } +void Button::onShortPress() { gun->resetShoots(); } +void Button::onLongPress() { gun->toggleCalibrationMode(); } \ No newline at end of file diff --git a/lib/Domain/Gun/Button.hpp b/lib/Domain/Gun/Button.hpp new file mode 100644 index 0000000..1b6a546 --- /dev/null +++ b/lib/Domain/Gun/Button.hpp @@ -0,0 +1,32 @@ +/* + * + * 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 Gun; + +class Button : public Contactor { + + Gun *gun; + +public: + Button() : Contactor() {} + + void onShortPress() override; + void onLongPress() override; + void setGun(Gun *gun); +}; \ No newline at end of file From ff69877869d58332b60e90ac11fe16a6c2798234 Mon Sep 17 00:00:00 2001 From: Aurelien Labrosse Date: Fri, 24 Nov 2023 09:46:27 +0000 Subject: [PATCH 09/12] feat: Implement Trigger --- lib/Domain/Gun/Trigger.cpp | 25 +++++++++++++++++++++++++ lib/Domain/Gun/Trigger.hpp | 29 +++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 lib/Domain/Gun/Trigger.cpp create mode 100644 lib/Domain/Gun/Trigger.hpp diff --git a/lib/Domain/Gun/Trigger.cpp b/lib/Domain/Gun/Trigger.cpp new file mode 100644 index 0000000..706625c --- /dev/null +++ b/lib/Domain/Gun/Trigger.cpp @@ -0,0 +1,25 @@ +/* + * + * 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 + +void Trigger::setGun(Gun *gun) { this->gun = gun; } + +void Trigger::onDown(long now) { + gun->activateShoot(); + Contactor::onDown(now); +} \ No newline at end of file diff --git a/lib/Domain/Gun/Trigger.hpp b/lib/Domain/Gun/Trigger.hpp new file mode 100644 index 0000000..c669e2f --- /dev/null +++ b/lib/Domain/Gun/Trigger.hpp @@ -0,0 +1,29 @@ +/* + * + * 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 Gun; + +class Trigger : public Contactor { + Gun *gun; + +public: + Trigger() : Contactor() {} + void setGun(Gun *gun); + void onDown(long now) override; +}; \ No newline at end of file From 4fe8990a6f3286b35f27f9a83d1099f88de1c35f Mon Sep 17 00:00:00 2001 From: Aurelien Labrosse Date: Fri, 24 Nov 2023 09:47:00 +0000 Subject: [PATCH 10/12] feat: Implement Gun --- lib/Domain/Gun.cpp | 39 ----- lib/Domain/Gun.hpp | 33 ---- lib/Domain/Gun/Gun.cpp | 99 ++++++++++++ lib/Domain/Gun/Gun.hpp | 65 ++++++++ test/native/test_gun/gun.cpp | 288 +++++++++++++++++++++-------------- 5 files changed, 336 insertions(+), 188 deletions(-) delete mode 100644 lib/Domain/Gun.cpp delete mode 100644 lib/Domain/Gun.hpp create mode 100644 lib/Domain/Gun/Gun.cpp create mode 100644 lib/Domain/Gun/Gun.hpp diff --git a/lib/Domain/Gun.cpp b/lib/Domain/Gun.cpp deleted file mode 100644 index 40f5832..0000000 --- a/lib/Domain/Gun.cpp +++ /dev/null @@ -1,39 +0,0 @@ -/* - * - * 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 - -static const uint8_t SHOTS_PER_REARM = 5; - -void Gun::onButton1ShortPress() { - if (availableShots > 0) { - _hal.laserOn(); - _hal.vibrationOn(); - _hal.shortDelay(); - _hal.laserOff(); - _hal.vibrationOff(); - availableShots--; - } - -} - -void Gun::onButton1LongPress() { - _hal.deepSleep(); -} -void Gun::onButton2ShortPress() { availableShots = SHOTS_PER_REARM; } -void Gun::onButton2LongPress() { - _hal.laserOn(); -} diff --git a/lib/Domain/Gun.hpp b/lib/Domain/Gun.hpp deleted file mode 100644 index cfabb6e..0000000 --- a/lib/Domain/Gun.hpp +++ /dev/null @@ -1,33 +0,0 @@ -/* - * - * 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 - -class Gun { - - IGunHal &_hal; - -public: - uint8_t availableShots; - Gun(IGunHal &hal) : _hal(hal), availableShots(0) {} - void onButton1ShortPress(); - void onButton1LongPress(); - void onButton2ShortPress(); - void onButton2LongPress(); -}; \ No newline at end of file diff --git a/lib/Domain/Gun/Gun.cpp b/lib/Domain/Gun/Gun.cpp new file mode 100644 index 0000000..a9ff4b7 --- /dev/null +++ b/lib/Domain/Gun/Gun.cpp @@ -0,0 +1,99 @@ +/* + * + * 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 + +#define TICKS_BETWEEN_BATTERY_UI_UPDATE 100 + +Gun::Gun(IGunHal *hal, IGunUi *ui) { + this->hal = hal; + this->ui = ui; + calibrationMode = false; + shootCount = 0; + + // display at first loop + shootCycleCountdown = 0; + batteryDisplayCycleCountdown = 0; + + button.setGun(this); + trigger.setGun(this); +} + +void Gun::toggleCalibrationMode() { + calibrationMode = !calibrationMode; + if (calibrationMode) { + hal->laserOn(); + hal->vibrationOff(); + ui->displayCalibration(); + } else { + hal->laserOff(); + ui->clearCalibration(); + ui->displayShootCount(shootCount); + batteryDisplayCycleCountdown = 0; + } +} + +void Gun::resetShoots() { + shootCount = 0; + ui->displayShootCount(shootCount); +} + +void Gun::activateShoot() { + if (!calibrationMode && shootCycleCountdown == 0) { + shootCount += 1; + hal->laserOn(); + hal->vibrationOn(); + + // add 1 to allow easy detection of last tick + shootCycleCountdown = Gun::SHOOT_DURATION_TICKS + 1; + } +} + +void Gun::loop(void) { + + // 10ms per loop thanks to timer2 + millisSinceStart += 10; + + trigger.processPendingEvent(millisSinceStart); + button.processPendingEvent(millisSinceStart); + + button.checkForLongPress(millisSinceStart); + + if (shootCycleCountdown > 0) { + shootCycleCountdown--; + if (shootCycleCountdown == 1) { + hal->laserOff(); + hal->vibrationOff(); + ui->displayShootCount(shootCount); + shootCycleCountdown--; + } + } + + if (batteryDisplayCycleCountdown > 0) { + batteryDisplayCycleCountdown--; + } else { + ui->displayBatteryStatus(hal->getBatteryVoltageMv(), + hal->getBatteryVoltagePercent()); + batteryDisplayCycleCountdown = TICKS_BETWEEN_BATTERY_UI_UPDATE; + } +} + +void Gun::setup(void) { + ui->displaySplash(2000); + // mV is not used + ui->displayBatteryStatus(0, hal->getBatteryVoltagePercent()); + ui->displayShootCount(0); +} \ No newline at end of file diff --git a/lib/Domain/Gun/Gun.hpp b/lib/Domain/Gun/Gun.hpp new file mode 100644 index 0000000..cf1fda3 --- /dev/null +++ b/lib/Domain/Gun/Gun.hpp @@ -0,0 +1,65 @@ +/* + * + * 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 +#include + +class Gun { + + uint8_t shootCycleCountdown; + uint16_t batteryDisplayCycleCountdown; + long millisSinceStart = 0; + bool calibrationMode; + +public: + /* 50 ms */ + static const uint8_t SHOOT_DURATION_TICKS = 5; + + IGunHal *hal; + IGunUi *ui; + Button button; + Trigger trigger; + uint16_t shootCount; + + Gun(IGunHal *hal, IGunUi *ui); + + /** + * Reset shoot count and redisplay + */ + void resetShoots(); + + /** + * Activate laser and vibrator + */ + void activateShoot(); + + void toggleCalibrationMode(); + + /** + * @brief Gun loop shall be called at least each 10ms, + * but interrupt shall trigger loop() execution in + * a shorter delay. + * + */ + void loop(); + + void setup(); +}; \ No newline at end of file diff --git a/test/native/test_gun/gun.cpp b/test/native/test_gun/gun.cpp index 3d2d8be..4ecc02b 100644 --- a/test/native/test_gun/gun.cpp +++ b/test/native/test_gun/gun.cpp @@ -15,165 +15,221 @@ * along with this program. If not, see . */ -#include -#include +#include +#include +#include + #include #include using namespace fakeit; #define MOCK_ALL() \ - When(Method(mock, vibrationOn)).AlwaysReturn(); \ - When(Method(mock, vibrationOff)).AlwaysReturn(); \ - When(Method(mock, laserOn)).AlwaysReturn(); \ - When(Method(mock, laserOff)).AlwaysReturn(); \ - When(Method(mock, ledOn)).AlwaysReturn(); \ - When(Method(mock, ledOff)).AlwaysReturn(); \ - When(Method(mock, shortDelay)).AlwaysReturn(); \ - When(Method(mock, longDelay)).AlwaysReturn(); \ - When(Method(mock, isButton1Pressed)).AlwaysReturn(); \ - When(Method(mock, isButton2Pressed)).AlwaysReturn(); \ - When(Method(mock, deepSleep)).AlwaysReturn(); + When(Method(mockHal, triggerIsUp)).AlwaysReturn(true); \ + When(Method(mockHal, buttonIsUp)).AlwaysReturn(true); \ + When(Method(mockHal, laserOn)).AlwaysReturn(); \ + When(Method(mockHal, vibrationOn)).AlwaysReturn(); \ + When(Method(mockHal, vibrationOff)).AlwaysReturn(); \ + When(Method(mockHal, laserOff)).AlwaysReturn(); \ + When(Method(mockHal, getBatteryVoltageMv)).AlwaysReturn(5000U); \ + When(Method(mockHal, getBatteryVoltagePercent)).AlwaysReturn(100U); \ + When(Method(mockHal, sleep)).AlwaysReturn(); \ + When(Method(mockUi, displayBatteryStatus)).AlwaysReturn(); \ + When(Method(mockUi, displayShootCount)).AlwaysReturn(); \ + When(Method(mockUi, displayCalibration)).AlwaysReturn(); \ + When(Method(mockUi, clearCalibration)).AlwaysReturn(); \ + When(Method(mockUi, displaySplash)).AlwaysReturn(); void tearDown() {} void setUp() { ArduinoFakeReset(); } -void expect_shot_to_activate_laser_during_short_duration() { +void expect_gun_to_loop() { + + Mock mockUi; + Mock mockHal; - Mock mock; MOCK_ALL(); - IGunHal &hal = mock.get(); - Gun gun(hal); - // first, give 5 shots - gun.onButton2ShortPress(); + IGunUi &ui = mockUi.get(); + IGunHal &hal = mockHal.get(); + + Gun gun(&hal, &ui); + + gun.loop(); +} +void expect_gun_to_shoot_50ms_on_trigger_down() { + + Mock mockUi; + Mock mockHal; - // shot - gun.onButton1ShortPress(); + MOCK_ALL(); - Verify(Method(mock, laserOn)).Once(); - Verify(Method(mock, shortDelay)).Once(); - Verify(Method(mock, laserOff)).Once(); + IGunUi &ui = mockUi.get(); + IGunHal &hal = mockHal.get(); + + Gun gun(&hal, &ui); + + gun.trigger.pendingEvent = Contactor::Event::Pressed; + gun.loop(); + gun.loop(); + gun.trigger.pendingEvent = Contactor::Event::Released; + gun.loop(); + gun.trigger.pendingEvent = Contactor::Event::NoEvent; + gun.loop(); + gun.loop(); + gun.loop(); + gun.loop(); + + // trigger is pressed then released, + // activating laser for 50ms/5 ticks + Verify(Method(mockHal, laserOn)).Once(); + Verify(Method(mockHal, vibrationOn)).Once(); + Verify(Method(mockHal, vibrationOff)).Once(); + Verify(Method(mockHal, laserOff)).Once(); } -void expect_shot_to_activate_vibration_during_short_duration() { +void expect_ui_to_display_battery_state_at_boot_and_each_1s() { + Mock mockUi; + Mock mockHal; - Mock mock; MOCK_ALL(); - IGunHal &hal = mock.get(); - Gun gun(hal); - // first, give 5 shots - gun.onButton2ShortPress(); + IGunUi &ui = mockUi.get(); + IGunHal &hal = mockHal.get(); + + Gun gun(&hal, &ui); - // shot - gun.onButton1ShortPress(); + for (uint8_t tickCounter = 0; tickCounter <= 101; tickCounter++) { + // displayBatteryStatus call after 100 ticks + gun.loop(); + } - Verify(Method(mock, vibrationOn)).Once(); - Verify(Method(mock, shortDelay)).Once(); - Verify(Method(mock, vibrationOff)).Once(); + // display at first loop, then each second (or 100 ticks) + Verify(Method(mockUi, displayBatteryStatus)).Exactly(2); } -void expect_short_press_on_button2_to_give_5_shots() { - Mock mock; +void expect_switch_to_maintenance_after_2s_button_continuous_press() { + Mock mockUi; + Mock mockHal; + MOCK_ALL(); - IGunHal &hal = mock.get(); - Gun gun(hal); - gun.onButton2ShortPress(); - TEST_ASSERT_EQUAL_INT_MESSAGE(5, gun.availableShots, - "Shots shall have been incremented"); - - gun.onButton2ShortPress(); - TEST_ASSERT_EQUAL_INT_MESSAGE(5, gun.availableShots, - "A rearm always provide 5 shots"); + + IGunUi &ui = mockUi.get(); + IGunHal &hal = mockHal.get(); + + Gun gun(&hal, &ui); + + gun.button.pendingEvent = Contactor::Event::Pressed; + for (uint8_t tickCounter = 0; tickCounter <= 200; tickCounter++) { + // long press shall be accounted after 2s, 200 ticks + gun.loop(); + } + gun.button.pendingEvent = Contactor::Event::Released; + gun.loop(); + gun.button.pendingEvent = Contactor::Event::Pressed; + for (uint8_t tickCounter = 0; tickCounter <= 200; tickCounter++) { + // long press shall be accounted after 2s, 200 ticks + gun.loop(); + } + + // switch to calibration + Verify(Method(mockHal, laserOn)).Exactly(1); + Verify(Method(mockUi, displayCalibration)).Exactly(1); + + // back to normal + Verify(Method(mockHal, laserOff)).Exactly(1); + Verify(Method(mockUi, clearCalibration)).Exactly(1); + Verify(Method(mockUi, displayShootCount)).Exactly(1); } -void expect_shot_to_decrement_available_shot_count() { - Mock mock; +void expect_button_press_to_reset_shoot_count_and_redisplay() { + Mock mockUi; + Mock mockHal; MOCK_ALL(); - IGunHal &hal = mock.get(); - Gun gun(hal); - - // first, give 5 shots - gun.onButton2ShortPress(); - - gun.onButton1ShortPress(); - TEST_ASSERT_EQUAL_INT_MESSAGE( - 4, gun.availableShots, - "There shall be 4 more available shots after 1 shots"); - gun.onButton1ShortPress(); - gun.onButton1ShortPress(); - gun.onButton1ShortPress(); - gun.onButton1ShortPress(); - TEST_ASSERT_EQUAL_INT_MESSAGE( - 0, gun.availableShots, - "There shall be no more available shots after 5 shots"); - gun.onButton1ShortPress(); - - // no bug when shooting with a remaining of 0 - TEST_ASSERT_EQUAL_INT_MESSAGE( - 0, gun.availableShots, - "There shall be no more available shots after 5 shots"); + IGunUi &ui = mockUi.get(); + IGunHal &hal = mockHal.get(); + + Gun gun(&hal, &ui); + + gun.shootCount = 10; + + gun.button.pendingEvent = Contactor::Event::Pressed; + gun.loop(); + gun.button.pendingEvent = Contactor::Event::Released; + gun.loop(); + + TEST_ASSERT_EQUAL_MESSAGE(0, gun.shootCount, "Shoot count shall be reset"); + Verify(Method(mockUi, displayShootCount)).Exactly(1); } -void expect_shot_to_do_nothing_if_there_is_no_available_shots() { - Mock mock; +void expect_trigger_to_have_no_effect_in_calibration_mode() { + Mock mockUi; + Mock mockHal; + MOCK_ALL(); - IGunHal &hal = mock.get(); - Gun gun(hal); - - // first, give 5 shots - gun.onButton2ShortPress(); - - gun.onButton1ShortPress(); // shot 1 - gun.onButton1ShortPress(); // shot 2 - gun.onButton1ShortPress(); // shot 3 - gun.onButton1ShortPress(); // shot 4 - gun.onButton1ShortPress(); // shot 5 - gun.onButton1ShortPress(); // shot 6, which shall do nothing - - Verify(Method(mock, vibrationOn)).Exactly(5); - Verify(Method(mock, vibrationOff)).Exactly(5); - Verify(Method(mock, laserOn)).Exactly(5); - Verify(Method(mock, laserOff)).Exactly(5); - Verify(Method(mock, shortDelay)).Exactly(5); - Verify(Method(mock, vibrationOff)).Exactly(5); + + IGunUi &ui = mockUi.get(); + IGunHal &hal = mockHal.get(); + + Gun gun(&hal, &ui); + + gun.button.pendingEvent = Contactor::Event::Pressed; + for (uint8_t tickCounter = 0; tickCounter <= 200; tickCounter++) { + // long press shall be accounted after 2s, 200 ticks + gun.loop(); + } + + gun.button.pendingEvent = Contactor::Event::Released; + gun.loop(); + + gun.trigger.pendingEvent = Contactor::Event::Pressed; + gun.loop(); + gun.trigger.pendingEvent = Contactor::Event::Released; + gun.loop(); + + Verify(Method(mockHal, laserOn)).Exactly(1); + Verify(Method(mockHal, laserOff)).Exactly(0); } -void expect_long_press_on_button_two_to_activate_continuous_laser() { - Mock mock; +void expect_long_press_to_trigger_only_if_button_is_down() { + Mock mockUi; + Mock mockHal; + MOCK_ALL(); - IGunHal &hal = mock.get(); - Gun gun(hal); - gun.onButton2LongPress(); + IGunUi &ui = mockUi.get(); + IGunHal &hal = mockHal.get(); - Verify(Method(mock, laserOn)).Once(); - Verify(Method(mock, shortDelay)).Exactly(0); - Verify(Method(mock, laserOff)).Exactly(0); -} + Gun gun(&hal, &ui); + + gun.button.pendingEvent = Contactor::Event::Pressed; + gun.loop(); -void expect_long_press_on_button_one_to_trigger_deep_sleep() -{ - Mock mock; - When(Method(mock, deepSleep)).Return(); - IGunHal &hal = mock.get(); - Gun gun(hal); + gun.button.pendingEvent = Contactor::Event::Released; + gun.loop(); - gun.onButton1LongPress(); + for (uint8_t tickCounter = 0; tickCounter <= 200; tickCounter++) { + // long press shall be accounted after 2s, 200 ticks + gun.loop(); + } - Verify(Method(mock, deepSleep)).Once(); + // No long press, no switch to calibration + Verify(Method(mockHal, laserOn)).Exactly(0); + Verify(Method(mockUi, displayCalibration)).Exactly(0); + Verify(Method(mockHal, laserOff)).Exactly(0); + Verify(Method(mockUi, clearCalibration)).Exactly(0); + Verify(Method(mockUi, displayShootCount)).Exactly(1); } int main(int, char **) { UNITY_BEGIN(); - RUN_TEST(expect_short_press_on_button2_to_give_5_shots); - RUN_TEST(expect_shot_to_activate_laser_during_short_duration); - RUN_TEST(expect_shot_to_activate_vibration_during_short_duration); - RUN_TEST(expect_shot_to_decrement_available_shot_count); - RUN_TEST(expect_shot_to_do_nothing_if_there_is_no_available_shots); - RUN_TEST(expect_long_press_on_button_two_to_activate_continuous_laser); - RUN_TEST(expect_long_press_on_button_one_to_trigger_deep_sleep); + RUN_TEST(expect_gun_to_loop); + RUN_TEST(expect_gun_to_shoot_50ms_on_trigger_down); + RUN_TEST(expect_ui_to_display_battery_state_at_boot_and_each_1s); + RUN_TEST(expect_switch_to_maintenance_after_2s_button_continuous_press); + RUN_TEST(expect_long_press_to_trigger_only_if_button_is_down); + RUN_TEST(expect_button_press_to_reset_shoot_count_and_redisplay); + RUN_TEST(expect_trigger_to_have_no_effect_in_calibration_mode); UNITY_END(); return 0; From 79f31e15762ff5074692fce264eccd007ccd940e Mon Sep 17 00:00:00 2001 From: Aurelien Labrosse Date: Fri, 24 Nov 2023 09:47:21 +0000 Subject: [PATCH 11/12] feat: GunApp uses new Gun implementation --- src/GunApp.cpp | 238 ++++++++++--------------------------------------- 1 file changed, 49 insertions(+), 189 deletions(-) diff --git a/src/GunApp.cpp b/src/GunApp.cpp index f4bf797..5e195ec 100644 --- a/src/GunApp.cpp +++ b/src/GunApp.cpp @@ -15,209 +15,69 @@ * along with this program. If not, see . */ -#include -#include -#include - -#include - -#define TRIGGER_PIN 2 -#define BUTTON2_PIN 3 - -#define LASER_PIN 10 -#define OUT2_PIN 6 -#define BATTERY_VOLTAGE_PIN A3 -#define CHARGING_STATE_PIN A2 - -#define SHOOT_DURATION_CYCLE 5 -#define MIN_BAT_VOLTAGE 3000 -#define MAX_BAT_VOLTAGE 4120 - -#define SCREEN_WIDTH 128 // OLED display width, in pixels -#define SCREEN_HEIGHT 32 // OLED display height, in pixels -#define OLED_RESET -1 // Reset pin # (or -1 if sharing Arduino reset pin) -#define SCREEN_ADDRESS \ - 0x3C ///< See datasheet for Address; 0x3D for 128x64, 0x3C for 128x32 -Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); - -volatile uint16_t hitcount; -uint16_t previoushitcout; -volatile uint16_t shootCount; - -#define BSP_TICKS_PER_SEC 100 -#define F_CPU 16000000L -#define TICKS_BETWEEN_UI_UPDATE 1000 // 10 sec - -// counter for ticks while firing -volatile uint8_t shootCycleCountdown; - -// counter for ticks between battery display update -volatile uint16_t updateBatteryDisplayCycleCount; - -void shoot() { - // wake up! - if (shootCycleCountdown == 0) { - shootCount += 1; - digitalWrite(LASER_PIN, HIGH); - digitalWrite(OUT2_PIN, HIGH); - shootCycleCountdown = SHOOT_DURATION_CYCLE; - } -} -void reload() { - // wake up! - shootCount = 0; -} - -void setupHeartbeat() { - - // set Timer2 in CTC mode, 1/1024 prescaler, start the timer ticking... - TCCR2A = (1U << WGM21) | (0U << WGM20); - TCCR2B = (1U << CS22) | (1U << CS21) | (1U << CS20); // 1/2^10 - ASSR &= ~(1U << AS2); - TIMSK2 = (1U << OCIE2A); // enable TIMER2 compare Interrupt - TCNT2 = 0U; - - // set the output-compare register based on the desired tick frequency - OCR2A = (F_CPU / BSP_TICKS_PER_SEC / 1024U) - 1U; -} - -ISR(TIMER2_COMPA_vect) { - if (shootCycleCountdown > 0) { - shootCycleCountdown--; - } else if (updateBatteryDisplayCycleCount > 0) { - updateBatteryDisplayCycleCount--; +#include +#include +#include + +SSD1306Ui ui; +Atmega328pHal hal; +Gun gun(&hal, &ui); + +volatile bool applicativeEvent = false; +ISR(TIMER2_COMPA_vect) { applicativeEvent = true; } + +volatile Contactor::Event pendingTriggerEvent; +void triggerInterruptHandler() { + if (Contactor::Event::NoEvent == pendingTriggerEvent) { + if (hal.triggerIsUp()) { + pendingTriggerEvent = Contactor::Event::Released; + } else { + pendingTriggerEvent = Contactor::Event::Pressed; + } + applicativeEvent = true; } } -static void displayShootCount() { - display.fillRect(11, 0, 68, 30, 0); // clear - - if (shootCount > 999) - shootCount = 0; - - if (shootCount < 10) { - display.setCursor(15, 25); - } else { - display.setCursor(10, 25); +volatile Contactor::Event pendingButtonEvent; +void buttonInterruptHandler() { + if (Contactor::Event::NoEvent == pendingButtonEvent) { + if (hal.buttonIsUp()) { + pendingButtonEvent = Contactor::Event::Released; + } else { + pendingButtonEvent = Contactor::Event::Pressed; + } + applicativeEvent = true; } - display.print(shootCount); - display.display(); } -static void displayBatteryStatus() { - - // schedule next update - updateBatteryDisplayCycleCount = TICKS_BETWEEN_UI_UPDATE; +void loop(void) { - analogRead(BATTERY_VOLTAGE_PIN); - float rawBatt = analogRead(BATTERY_VOLTAGE_PIN); - rawBatt += analogRead(BATTERY_VOLTAGE_PIN); - rawBatt += analogRead(BATTERY_VOLTAGE_PIN); - rawBatt += analogRead(BATTERY_VOLTAGE_PIN); - rawBatt /= 4; + // other interrupt may wakeup the board + // and trigger loop(). Especially counter0 + // and counter1, used internally by the Arduino + // framework. Avoid processing these wakeups as + // applicative events using 'tick' control variable. - // https://rickkas7.github.io/DisplayGenerator/index.html + if (applicativeEvent) { + applicativeEvent = false; - float battMv = (5000 / 1023.f) * rawBatt; - bool isCharging = (digitalRead(CHARGING_STATE_PIN) == HIGH); + // wire interrupt-based events with main code + gun.trigger.pendingEvent = pendingTriggerEvent; + gun.button.pendingEvent = pendingButtonEvent; -#if defined(SERIAL_OUTPUT) - Serial.print(rawBatt); - Serial.print(';'); - Serial.print(battMv); - Serial.print(';'); - Serial.println(isCharging ? "c" : "n"); - delay(2); -#endif + gun.loop(); - uint8_t battPercent = map(battMv, MIN_BAT_VOLTAGE, MAX_BAT_VOLTAGE, 0, 100); + pendingTriggerEvent = Contactor::Event::NoEvent; + pendingButtonEvent = Contactor::Event::NoEvent; - display.setFont(NULL); - display.fillRect(79, 0, 59, 24, 0); // clear - display.setCursor(100, 2); - display.print(battPercent); - display.println("%"); - if (isCharging) { - display.setCursor(80, 15); - display.println("charging"); + hal.sleep(); } - - // prepare font for next shoot count display - display.setFont(&FreeMonoBold18pt7b); } void setup(void) { - -#if defined(SERIAL_OUTPUT) - Serial.begin(115200); - while (!Serial) { - } -#endif - if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) { -#if defined(SERIAL_OUTPUT) - Serial.println(F("SSD1306 allocation failed")); -#endif - for (;;) - ; // Don't proceed, loop forever - } - shootCount = 0; - shootCycleCountdown = 0; - updateBatteryDisplayCycleCount = TICKS_BETWEEN_UI_UPDATE; - - pinMode(LASER_PIN, OUTPUT); - digitalWrite(LASER_PIN, LOW); - - pinMode(OUT2_PIN, OUTPUT); - digitalWrite(OUT2_PIN, LOW); - - pinMode(BATTERY_VOLTAGE_PIN, INPUT); - pinMode(CHARGING_STATE_PIN, INPUT); - - pinMode(LED_BUILTIN, OUTPUT); - digitalWrite(LED_BUILTIN, LOW); - -#if defined(SERIAL_OUTPUT) - Serial.println("Ready!"); -#endif - - display.clearDisplay(); - display.setTextColor(WHITE); - display.setTextSize(1); - display.setFont(&FreeMonoOblique9pt7b); - display.setCursor(9, 11); - display.println("5 in a row"); - display.setCursor(4, 27); - display.println(" # # # # #"); - display.display(); - - delay(2000); - - display.clearDisplay(); - displayBatteryStatus(); - displayShootCount(); - - pinMode(BUTTON2_PIN, INPUT_PULLUP); - attachInterrupt(digitalPinToInterrupt(BUTTON2_PIN), reload, FALLING); - - pinMode(TRIGGER_PIN, INPUT_PULLUP); - attachInterrupt(digitalPinToInterrupt(TRIGGER_PIN), shoot, FALLING); - cli(); - setupHeartbeat(); - sei(); -} - -void loop(void) { - - if (shootCycleCountdown == 0) { - - digitalWrite(LASER_PIN, LOW); - digitalWrite(OUT2_PIN, LOW); - displayShootCount(); - - if (updateBatteryDisplayCycleCount == 0) { - displayBatteryStatus(); - } - } - LowPower.idle(SLEEP_FOREVER, ADC_OFF, TIMER2_ON, TIMER1_OFF, TIMER0_OFF, - SPI_OFF, USART0_OFF, TWI_OFF); + hal.setGun(&gun); + ui.setup(); + gun.setup(); + hal.configureInputCallbacks(); + hal.setupHeartbeat(); } \ No newline at end of file From 1cbc09e600d7ce04e3825cb8d7cbf2314f6261df Mon Sep 17 00:00:00 2001 From: Aurelien Labrosse Date: Fri, 24 Nov 2023 09:47:54 +0000 Subject: [PATCH 12/12] chore: small changes in flags --- platformio.ini | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/platformio.ini b/platformio.ini index 4fd07e3..4a04cbc 100755 --- a/platformio.ini +++ b/platformio.ini @@ -18,7 +18,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 -Pcom9 $UPLOAD_FLAGS -U flash:w:$SOURCE:i # upload_protocol=custom # upload_speed=115200 @@ -39,8 +40,8 @@ upload_command = avrdude -vv -b57600 -pm328p -c avrisp -Pcom10 $UPLOAD_FLAGS -U platform=native build_type = release test_ignore = cross/* - build_flags = -DNATIVE - debug_test = noarch/test_gun + build_flags = -DNATIVE -O0 + debug_test = noarch/test_contactor build_src_filter = ${env.src_filter} - - [env:native_debug] @@ -48,9 +49,9 @@ upload_command = avrdude -vv -b57600 -pm328p -c avrisp -Pcom10 $UPLOAD_FLAGS -U fakeit=https://github.com/FabioBatSilva/ArduinoFake.git platform=native build_type = debug - debug_build_flags = -Og -ggdb3 -g3 -DNATIVE -UAVR + debug_build_flags = -Og -ggdb3 -g3 -DNATIVE test_ignore = cross/* - debug_test = noarch/test_gun + debug_test = native/test_gun build_src_filter = ${env.src_filter} - - [env:cross] @@ -87,8 +88,8 @@ upload_command = avrdude -vv -b57600 -pm328p -c avrisp -Pcom10 $UPLOAD_FLAGS -U platform = atmelavr board = ATmega328P build_type = release - build_flags = -DAVR + build_flags = -DAVR -O2 test_ignore = native/* noarch/* - build_src_filter = ${env.src_filter} - - \ No newline at end of file + build_src_filter = ${env.src_filter} - -