diff --git a/.github/workflows/platformio_build.yaml b/.github/workflows/platformio_build.yaml new file mode 100644 index 0000000..0786ae9 --- /dev/null +++ b/.github/workflows/platformio_build.yaml @@ -0,0 +1,29 @@ +name: PlatformIO CI - Build for Arduino + +on: [push] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - uses: actions/cache@v3 + with: + path: | + ~/.cache/pip + ~/.platformio/.cache + key: ${{ runner.os }}-pio + - uses: actions/setup-python@v4 + with: + python-version: "3.9" + - name: Install PlatformIO Core + run: pip install --upgrade platformio + + # rename src/secrets.h.example to src/secrets.h, to use a dummy values during build + - name: Copy secrets.h + run: cp ./controller/tea_poor/src/secrets.h.example ./controller/tea_poor/src/secrets.h + + - name: Build PlatformIO Project for Arduino + working-directory: ./controller/tea_poor + run: pio run -e uno_r4_wifi diff --git a/.github/workflows/platformio.yaml b/.github/workflows/platformio_tests.yaml similarity index 80% rename from .github/workflows/platformio.yaml rename to .github/workflows/platformio_tests.yaml index 460fb89..299db5f 100644 --- a/.github/workflows/platformio.yaml +++ b/.github/workflows/platformio_tests.yaml @@ -1,4 +1,4 @@ -name: PlatformIO CI +name: PlatformIO CI - Unit Tests on: [push] @@ -20,6 +20,6 @@ jobs: - name: Install PlatformIO Core run: pip install --upgrade platformio - - name: Build PlatformIO Project + - name: Run tests on the native platform working-directory: ./controller/tea_poor - run: pio run + run: pio test -e native diff --git a/controller/tea_poor/.gitignore b/controller/tea_poor/.gitignore index 89cc49c..1f7aa88 100644 --- a/controller/tea_poor/.gitignore +++ b/controller/tea_poor/.gitignore @@ -3,3 +3,6 @@ .vscode/c_cpp_properties.json .vscode/launch.json .vscode/ipch + +# hide secrets +src/secrets.h \ No newline at end of file diff --git a/controller/tea_poor/lib/Arduino/ArduinoEnvironment.h b/controller/tea_poor/lib/Arduino/ArduinoEnvironment.h new file mode 100644 index 0000000..bbb5a01 --- /dev/null +++ b/controller/tea_poor/lib/Arduino/ArduinoEnvironment.h @@ -0,0 +1,15 @@ +// Arduino environment +#ifndef ARDUINO_ENVIRONMENT_H +#define ARDUINO_ENVIRONMENT_H + +#include +#include + +class ArduinoEnvironment : public IEnvironment { + public: + unsigned long time() const override { + return millis(); + } +}; + +#endif // ARDUINO_ENVIRONMENT_H \ No newline at end of file diff --git a/controller/tea_poor/lib/RemoteControl/RemoteControl.cpp b/controller/tea_poor/lib/Arduino/RemoteControl.cpp similarity index 92% rename from controller/tea_poor/lib/RemoteControl/RemoteControl.cpp rename to controller/tea_poor/lib/Arduino/RemoteControl.cpp index c4ebe9d..16c3ec9 100644 --- a/controller/tea_poor/lib/RemoteControl/RemoteControl.cpp +++ b/controller/tea_poor/lib/Arduino/RemoteControl.cpp @@ -95,4 +95,12 @@ void RemoteControl::process() { _app.process(&client); client.stop(); } +} + +String RemoteControl::asJSONString() const { + String result = "{"; + result += "\"SSID\": \"" + _SSID + "\","; + result += "\"signal strength\": " + String(WiFi.RSSI()); + result += "}"; + return result; } \ No newline at end of file diff --git a/controller/tea_poor/lib/RemoteControl/RemoteControl.h b/controller/tea_poor/lib/Arduino/RemoteControl.h similarity index 94% rename from controller/tea_poor/lib/RemoteControl/RemoteControl.h rename to controller/tea_poor/lib/Arduino/RemoteControl.h index 78fb5e5..1447248 100644 --- a/controller/tea_poor/lib/RemoteControl/RemoteControl.h +++ b/controller/tea_poor/lib/Arduino/RemoteControl.h @@ -14,6 +14,7 @@ class RemoteControl { ~RemoteControl(); void setup(RemoteControlRoutesCallback routes); void process(); + String asJSONString() const; private: const String _SSID; const String _SSIDPassword; diff --git a/controller/tea_poor/lib/WaterPumpController/WaterPumpController.cpp b/controller/tea_poor/lib/Arduino/WaterPumpController.cpp similarity index 66% rename from controller/tea_poor/lib/WaterPumpController/WaterPumpController.cpp rename to controller/tea_poor/lib/Arduino/WaterPumpController.cpp index b8ce60f..25768ec 100644 --- a/controller/tea_poor/lib/WaterPumpController/WaterPumpController.cpp +++ b/controller/tea_poor/lib/Arduino/WaterPumpController.cpp @@ -15,24 +15,17 @@ void WaterPumpController::setup() { pinMode(_directionPin, OUTPUT); pinMode(_brakePin, OUTPUT); pinMode(_powerPin, OUTPUT); - // TODO: check that its okay to do during setup - stopPump(); + stop(); } -void WaterPumpController::pour(int milliseconds) { - startPump(); - delay(milliseconds); - stopPump(); -} - -void WaterPumpController::startPump() { - _state = PUMP_ON; +void WaterPumpController::start() { + _isRunning = true; digitalWrite(_brakePin, LOW); // release breaks analogWrite(_powerPin, 255); } -void WaterPumpController::stopPump() { +void WaterPumpController::stop() { digitalWrite(_brakePin, HIGH); // activate breaks analogWrite(_powerPin, 0); - _state = PUMP_OFF; -} + _isRunning = false; +} \ No newline at end of file diff --git a/controller/tea_poor/lib/Arduino/WaterPumpController.h b/controller/tea_poor/lib/Arduino/WaterPumpController.h new file mode 100644 index 0000000..2e55cac --- /dev/null +++ b/controller/tea_poor/lib/Arduino/WaterPumpController.h @@ -0,0 +1,23 @@ +#ifndef WATERPUMPCONTROLLER_H +#define WATERPUMPCONTROLLER_H +#include + +class WaterPumpController: public IWaterPump { +private: + const int _directionPin; + const int _brakePin; + const int _powerPin; + const int _maxPower = 255; + bool _isRunning = false; +public: + WaterPumpController(int directionPin, int brakePin, int powerPin); + virtual ~WaterPumpController() override; + + virtual void setup() override; + virtual void start() override; + virtual void stop() override; + + virtual bool isRunning() const override { return _isRunning; } +}; + +#endif \ No newline at end of file diff --git a/controller/tea_poor/lib/CommandProcessor/CommandProcessor.cpp b/controller/tea_poor/lib/CommandProcessor/CommandProcessor.cpp new file mode 100644 index 0000000..c776abf --- /dev/null +++ b/controller/tea_poor/lib/CommandProcessor/CommandProcessor.cpp @@ -0,0 +1,58 @@ +#include "CommandProcessor.h" +#include +#include + +bool isValidIntNumber(const char *str, const int maxValue, const int minValue=0) { + const int len = strlen(str); + if (len < 1) return false; + // check that string contains only digits + // first character can be '-' for negative numbers + if ((str[0] != '-') && !isdigit(str[0])) return false; + for (int i = 1; i < len; i++) { + if (!isdigit(str[i])) return false; + } + // check that number is in range + const int value = atoi(str); + if (value < minValue) return false; + if (maxValue <= value) return false; + return true; +} + +std::string CommandProcessor::status() { + std::stringstream response; + response << "{"; + // send water threshold + response << "\"water threshold\": " << _waterPumpSafeThreshold << ", "; + // send water pump status + const auto waterPumpStatus = _waterPump->status(); + const auto now = _env->time(); + const auto timeLeft = waterPumpStatus.isRunning ? waterPumpStatus.stopTime - now : 0; + response + << "\"pump\": {" + << " \"running\": " << (waterPumpStatus.isRunning ? "true, " : "false, ") + << " \"time left\": " << timeLeft + << "}"; + // end of water pump status + /////////////////////////////////// + // send remote control status + // response << "\"remote control\": " << remoteControl.asJSONString(); + // end of JSON + response << "}"; + + return response.str(); +} + +std::string CommandProcessor::pour_tea(const char *milliseconds) { + if (!isValidIntNumber(milliseconds, _waterPumpSafeThreshold)) { + // send error message as JSON + return std::string("{ \"error\": \"invalid milliseconds value\" }"); + } + // start pouring tea + _waterPump->start( atoi(milliseconds), _env->time() ); + return status(); +} + +std::string CommandProcessor::stop() { + _waterPump->stop(); + return status(); +} \ No newline at end of file diff --git a/controller/tea_poor/lib/CommandProcessor/CommandProcessor.h b/controller/tea_poor/lib/CommandProcessor/CommandProcessor.h new file mode 100644 index 0000000..a7770a0 --- /dev/null +++ b/controller/tea_poor/lib/CommandProcessor/CommandProcessor.h @@ -0,0 +1,30 @@ +// CommandProcessor class definition +#ifndef COMMANDPROCESSOR_H +#define COMMANDPROCESSOR_H + +#include +#include +#include + +// This class is used to process incoming commands +class CommandProcessor { + public: + CommandProcessor( + int waterPumpSafeThreshold, + const IEnvironmentPtr env, + const IWaterPumpSchedulerAPIPtr waterPump + ) : + _waterPumpSafeThreshold(waterPumpSafeThreshold), + _env(env), + _waterPump(waterPump) + {} + + std::string status(); + std::string pour_tea(const char *milliseconds); + std::string stop(); + private: + const int _waterPumpSafeThreshold; + const IEnvironmentPtr _env; + const IWaterPumpSchedulerAPIPtr _waterPump; +}; +#endif // COMMANDPROCESSOR_H \ No newline at end of file diff --git a/controller/tea_poor/lib/WaterPumpController/WaterPumpController.h b/controller/tea_poor/lib/WaterPumpController/WaterPumpController.h deleted file mode 100644 index 8561876..0000000 --- a/controller/tea_poor/lib/WaterPumpController/WaterPumpController.h +++ /dev/null @@ -1,28 +0,0 @@ -#ifndef WATERPUMPCONTROLLER_H -#define WATERPUMPCONTROLLER_H - -class WaterPumpController { -public: - enum EPumpState { - PUMP_OFF, - PUMP_ON - }; -private: - const int _directionPin; - const int _brakePin; - const int _powerPin; - const int _maxPower = 255; - EPumpState _state = PUMP_OFF; -public: - WaterPumpController(int directionPin, int brakePin, int powerPin); - ~WaterPumpController(); - - void setup(); - void pour(int miliseconds); - void startPump(); - void stopPump(); - - EPumpState state() const { return _state; } -}; - -#endif // WATERPUMPCONTROLLER_H diff --git a/controller/tea_poor/lib/WaterPumpScheduler/WaterPumpScheduler.cpp b/controller/tea_poor/lib/WaterPumpScheduler/WaterPumpScheduler.cpp new file mode 100644 index 0000000..edf3634 --- /dev/null +++ b/controller/tea_poor/lib/WaterPumpScheduler/WaterPumpScheduler.cpp @@ -0,0 +1,34 @@ +#include "WaterPumpScheduler.h" + +WaterPumpScheduler::WaterPumpScheduler(IWaterPumpPtr waterPump, unsigned long forceStopIntervalMs) : + _waterPump(waterPump), + _forceStopIntervalMs(forceStopIntervalMs) +{ +} + +WaterPumpScheduler::~WaterPumpScheduler() {} + +void WaterPumpScheduler::setup() { + _waterPump->setup(); +} + +void WaterPumpScheduler::start(unsigned long runTimeMs, unsigned long currentTimeMs) { + _stopTime = currentTimeMs + runTimeMs; + _waterPump->start(); +} + +void WaterPumpScheduler::stop() { + _waterPump->stop(); + _stopTime = 0; // a bit of paranoia :) +} + +void WaterPumpScheduler::tick(unsigned long currentTimeMs) { + if (_stopTime <= currentTimeMs) { + stop(); + _stopTime = currentTimeMs + _forceStopIntervalMs; // force stop after X milliseconds + } +} + +WaterPumpStatus WaterPumpScheduler::status() { + return WaterPumpStatus(_waterPump->isRunning(), _stopTime); +} \ No newline at end of file diff --git a/controller/tea_poor/lib/WaterPumpScheduler/WaterPumpScheduler.h b/controller/tea_poor/lib/WaterPumpScheduler/WaterPumpScheduler.h new file mode 100644 index 0000000..4e8aa66 --- /dev/null +++ b/controller/tea_poor/lib/WaterPumpScheduler/WaterPumpScheduler.h @@ -0,0 +1,30 @@ +#ifndef WATERPUMPSCHEDULER_H +#define WATERPUMPSCHEDULER_H + +#include +#include + +// This class is responsible for scheduling water pump +// It is used to make sure that water pump is running for a limited time +// It is also ensuring that water pump is stopped if not needed +class WaterPumpScheduler : public IWaterPumpSchedulerAPI { +private: + IWaterPumpPtr _waterPump; + unsigned long _stopTime = 0; + // each X milliseconds will force stop water pump + unsigned long _forceStopIntervalMs; +public: + WaterPumpScheduler(IWaterPumpPtr waterPump, unsigned long forceStopIntervalMs); + WaterPumpScheduler(IWaterPumpPtr waterPump) : WaterPumpScheduler(waterPump, 1000) {} + ~WaterPumpScheduler(); + + void setup(); + // for simplicity and testability we are passing current time as parameter + void tick(unsigned long currentTimeMs); + + // Public API + void start(unsigned long runTimeMs, unsigned long currentTimeMs) override; + void stop() override; + WaterPumpStatus status() override; +}; +#endif \ No newline at end of file diff --git a/controller/tea_poor/lib/interfaces/IEnvironment.h b/controller/tea_poor/lib/interfaces/IEnvironment.h new file mode 100644 index 0000000..1992884 --- /dev/null +++ b/controller/tea_poor/lib/interfaces/IEnvironment.h @@ -0,0 +1,13 @@ +#ifndef IENVIRONMENT_H +#define IENVIRONMENT_H + +#include + +class IEnvironment { + public: + virtual unsigned long time() const = 0; + virtual ~IEnvironment() {} +}; + +typedef std::shared_ptr IEnvironmentPtr; +#endif // IENVIRONMENT_H \ No newline at end of file diff --git a/controller/tea_poor/lib/interfaces/IWaterPump.h b/controller/tea_poor/lib/interfaces/IWaterPump.h new file mode 100644 index 0000000..eef9ee9 --- /dev/null +++ b/controller/tea_poor/lib/interfaces/IWaterPump.h @@ -0,0 +1,19 @@ +#ifndef IWATERPUMP_H +#define IWATERPUMP_H + +#include + +class IWaterPump { +public: + virtual ~IWaterPump() {} + + virtual void setup() = 0; + virtual void start() = 0; + virtual void stop() = 0; + + virtual bool isRunning() const = 0; +}; + +// define shared pointer alias +using IWaterPumpPtr = std::shared_ptr; +#endif \ No newline at end of file diff --git a/controller/tea_poor/lib/interfaces/IWaterPumpSchedulerAPI.h b/controller/tea_poor/lib/interfaces/IWaterPumpSchedulerAPI.h new file mode 100644 index 0000000..b82df7d --- /dev/null +++ b/controller/tea_poor/lib/interfaces/IWaterPumpSchedulerAPI.h @@ -0,0 +1,30 @@ +// IWaterPumpSchedulerAPI interface +#ifndef IWATERPUMPSCHEDULERAPI_H +#define IWATERPUMPSCHEDULERAPI_H + +#include +// pump status +struct WaterPumpStatus { +public: + bool isRunning; + unsigned long stopTime; + // copy constructor + WaterPumpStatus(const WaterPumpStatus &other) { + isRunning = other.isRunning; + stopTime = other.stopTime; + } + WaterPumpStatus(bool isRunning, unsigned long stopTime) : isRunning(isRunning), stopTime(stopTime) {} + // default constructor + WaterPumpStatus() : isRunning(false), stopTime(0) {} +}; + +class IWaterPumpSchedulerAPI { +public: + virtual ~IWaterPumpSchedulerAPI() {} + virtual void stop() = 0; + virtual void start(unsigned long runTimeMs, unsigned long currentTimeMs) = 0; + virtual WaterPumpStatus status() = 0; +}; + +using IWaterPumpSchedulerAPIPtr = std::shared_ptr; +#endif \ No newline at end of file diff --git a/controller/tea_poor/platformio.ini b/controller/tea_poor/platformio.ini index 1515112..fd827a9 100644 --- a/controller/tea_poor/platformio.ini +++ b/controller/tea_poor/platformio.ini @@ -14,3 +14,22 @@ board = uno_r4_wifi framework = arduino lib_deps = lasselukkari/aWOT@^3.5.0 +test_ignore = test_native + +[env:native] +platform = native +test_build_src = no +test_framework = googletest +build_src_filter = +<*> - +lib_ldf_mode = deep +check_flags = --verbose --enable=all --std=c++11 +build_flags = + -std=c++11 + -Wall -Wextra -Wunused + -static -static-libgcc -static-libstdc++ +; ignore libraries that are only for the Arduino +lib_ignore = Arduino +test_ignore = test_uno_r4_wifi + +[platformio] +default_envs = uno_r4_wifi \ No newline at end of file diff --git a/controller/tea_poor/src/main.cpp b/controller/tea_poor/src/main.cpp index 87b8358..7509e0b 100644 --- a/controller/tea_poor/src/main.cpp +++ b/controller/tea_poor/src/main.cpp @@ -1,54 +1,68 @@ #include +#include #include +#include #include +#include +#include "secrets.h" + +#include +#include + +IEnvironmentPtr env = std::make_shared(); // Setting up water pump -WaterPumpController waterPumpController(12, 9, 3); -// Just for safety reasons, we don't want to pour tea for too long -// Their is no reason to make it configurable and add unnecessary complexity -const int WATER_PUMP_SAFE_THRESHOLD = 10 * 1000; +auto waterPump = std::make_shared( + std::make_shared( + WATER_PUMP_DIRECTION_PIN, WATER_PUMP_BRAKE_PIN, WATER_PUMP_POWER_PIN + ) +); // setting up remote control -RemoteControl remoteControl( - "MyWiFiNetwork", // network name/SSID - "VerySecurePassword" // network password -); +RemoteControl remoteControl(WIFI_SSID, WIFI_PASSWORD); -bool isValidIntNumber(const char *str, const int maxValue, const int minValue=0) { - if (strlen(str) <= 0) return false; - const int value = atoi(str); - if (value < minValue) return false; - if (maxValue <= value) return false; - return true; -} +// build command processor +CommandProcessor commandProcessor( + WATER_PUMP_SAFE_THRESHOLD, + env, + waterPump +); -void pour_tea(Request &req, Response &res) { - char milliseconds[64]; - req.query("milliseconds", milliseconds, 64); - if (!isValidIntNumber(milliseconds, WATER_PUMP_SAFE_THRESHOLD)) { - res.println("Please specify amount of milliseconds in query parameter; pour_tea?milliseconds=10 e.g."); - res.print("Maximal allowed time is: "); - res.println(WATER_PUMP_SAFE_THRESHOLD); - return; - } - const int pouringDelayMs = atoi(milliseconds); - // actually pour tea - waterPumpController.pour(pouringDelayMs); - - // Serial.println(req.JSON()); - res.print("Poured Tea in: "); - res.print(pouringDelayMs); - res.print(" milliseconds!"); +void withExtraHeaders(Response &res) { + res.set("Access-Control-Allow-Origin", "*"); + res.set("Access-Control-Allow-Methods", "GET, POST, OPTIONS, PUT, PATCH, DELETE"); + res.set("Access-Control-Allow-Headers", "Content-Type"); + res.set("Content-Type", "application/json"); } void setup() { Serial.begin(9600); - waterPumpController.setup(); + waterPump->setup(); remoteControl.setup([](Application &app) { - app.get("/pour_tea", pour_tea); + app.get("/pour_tea", [](Request &req, Response &res) { + char milliseconds[64]; + req.query("milliseconds", milliseconds, 64); + + const auto response = commandProcessor.pour_tea(milliseconds); + withExtraHeaders(res); + res.print(response.c_str()); + }); + // stop water pump + app.get("/stop", [](Request &req, Response &res) { + const auto response = commandProcessor.stop(); + withExtraHeaders(res); + res.print(response.c_str()); + }); + // get system status + app.get("/status", [](Request &req, Response &res) { + const auto response = commandProcessor.status(); + withExtraHeaders(res); + res.print(response.c_str()); + }); }); } void loop() { + waterPump->tick(millis()); remoteControl.process(); }; \ No newline at end of file diff --git a/controller/tea_poor/src/secrets.h.example b/controller/tea_poor/src/secrets.h.example new file mode 100644 index 0000000..4fe08d5 --- /dev/null +++ b/controller/tea_poor/src/secrets.h.example @@ -0,0 +1,18 @@ +// contains user specific information that should not be shared +#ifndef SECRETS_H +#define SECRETS_H + +// WiFi network name/SSID +const char* WIFI_SSID = "MyWiFiNetwork"; +const char* WIFI_PASSWORD = "VerySecurePassword"; + +// PINs for water pump controller +const int WATER_PUMP_DIRECTION_PIN = 12; +const int WATER_PUMP_BRAKE_PIN = 9; +const int WATER_PUMP_POWER_PIN = 3; + +// Just for safety reasons, we don't want to pour tea for too long +// Their is no reason to make it configurable and add unnecessary complexity +const int WATER_PUMP_SAFE_THRESHOLD = 10 * 1000; + +#endif // SECRETS_H \ No newline at end of file diff --git a/controller/tea_poor/test/test_native/main.cpp b/controller/tea_poor/test/test_native/main.cpp new file mode 100644 index 0000000..b6f09e9 --- /dev/null +++ b/controller/tea_poor/test/test_native/main.cpp @@ -0,0 +1,13 @@ +#include + +// include tests +#include "tests/WaterPumpScheduler_test.h" +#include "tests/CommandProcessor_test.h" + +int main(int argc, char **argv) { + ::testing::InitGoogleTest(&argc, argv); + int result = RUN_ALL_TESTS(); // Intentionally ignoring the return value + (void)result; // Silence unused variable warning + // Always return zero-code and allow PlatformIO to parse results + return 0; +} \ No newline at end of file diff --git a/controller/tea_poor/test/test_native/tests/CommandProcessor_test.h b/controller/tea_poor/test/test_native/tests/CommandProcessor_test.h new file mode 100644 index 0000000..291f814 --- /dev/null +++ b/controller/tea_poor/test/test_native/tests/CommandProcessor_test.h @@ -0,0 +1,79 @@ +#include +#include +#include "mocks/FakeWaterPumpSchedulerAPI.h" +#include "mocks/FakeEnvironment.h" + +// test that pour_tea() method returns error message if milliseconds: +// - greater than threshold +// - less than 0 +// - empty string +// - not a number +TEST(CommandProcessor, pour_tea_invalid_milliseconds) { + const auto EXPECTED_ERROR_MESSAGE = "{ \"error\": \"invalid milliseconds value\" }"; + CommandProcessor commandProcessor(123, nullptr, nullptr); + + // array of invalid parameters + const char *PARAMS[] = { "1234", "-1", "", "abc" }; + for (auto param : PARAMS) { + const auto response = commandProcessor.pour_tea(param); + ASSERT_EQ(response, EXPECTED_ERROR_MESSAGE); + } +} + +// test that start pouring tea by calling pour_tea() method and its stops after T milliseconds +TEST(CommandProcessor, pour_tea) { + auto env = std::make_shared(); + env->time(2343); + auto waterPump = std::make_shared(); + CommandProcessor commandProcessor(10000, env, waterPump); + const auto response = commandProcessor.pour_tea("1234"); + ASSERT_EQ(waterPump->_log, "start(1234, 2343)\n"); +} + +// test that stop() method stops pouring tea +TEST(CommandProcessor, stop) { + auto env = std::make_shared(); + auto waterPump = std::make_shared(); + CommandProcessor commandProcessor(123, env, waterPump); + const auto response = commandProcessor.stop(); + ASSERT_EQ(waterPump->_log, "stop()\n"); +} + +// test that status() method returns JSON string with water pump status +TEST(CommandProcessor, status) { + auto env = std::make_shared(); + auto waterPump = std::make_shared(); + CommandProcessor commandProcessor(123, env, waterPump); + const auto response = commandProcessor.status(); + ASSERT_EQ(response, "{" + "\"water threshold\": 123, " + "\"pump\": {" + " \"running\": false, " + " \"time left\": 0" + "}" + "}" + ); +} + +// test that status() method returns JSON string with actual time left +TEST(CommandProcessor, status_running) { + auto env = std::make_shared(); + auto waterPump = std::make_shared(); + CommandProcessor commandProcessor(12345, env, waterPump); + + commandProcessor.pour_tea("1123"); + + env->time(123); + waterPump->_status.isRunning = true; + waterPump->_status.stopTime = 1123; + + const auto response = commandProcessor.status(); + ASSERT_EQ(response, "{" + "\"water threshold\": 12345, " + "\"pump\": {" + " \"running\": true, " + " \"time left\": 1000" + "}" + "}" + ); +} \ No newline at end of file diff --git a/controller/tea_poor/test/test_native/tests/WaterPumpScheduler_test.h b/controller/tea_poor/test/test_native/tests/WaterPumpScheduler_test.h new file mode 100644 index 0000000..138541b --- /dev/null +++ b/controller/tea_poor/test/test_native/tests/WaterPumpScheduler_test.h @@ -0,0 +1,49 @@ +#include +#include "mocks/FakeWaterPump.h" +#include + +// test that pump is stopping after given time +TEST(WaterPumpScheduler, test_pump_stops_after_given_time) { + // random time between 1 and 10 seconds + const unsigned long runTimeMs = 1000 + (rand() % 10) * 1000; + IWaterPumpPtr fakeWaterPump = std::make_shared(); + WaterPumpScheduler waterPumpScheduler(fakeWaterPump); + waterPumpScheduler.setup(); + // start water pump + unsigned long currentTimeMs = 0; + waterPumpScheduler.start(runTimeMs, currentTimeMs); + // check status + auto status = waterPumpScheduler.status(); + ASSERT_TRUE(status.isRunning); + ASSERT_EQ(status.stopTime, runTimeMs); + + while (currentTimeMs < runTimeMs) { + waterPumpScheduler.tick(currentTimeMs); + ASSERT_TRUE(fakeWaterPump->isRunning()); + currentTimeMs += 100; + } + // pump should be stopped after given time + waterPumpScheduler.tick(runTimeMs + 1); + ASSERT_FALSE(fakeWaterPump->isRunning()); +} + +// test that pump is periodically forced to stop after given time +TEST(WaterPumpScheduler, test_pump_is_periodically_forced_to_stop_after_given_time) { + IWaterPumpPtr fakeWaterPump = std::make_shared(); + WaterPumpScheduler waterPumpScheduler(fakeWaterPump, 1000); // force stop each 1 second + waterPumpScheduler.setup(); + // start water pump + unsigned long currentTimeMs = 0; + waterPumpScheduler.start(1, currentTimeMs); + currentTimeMs += 1; + waterPumpScheduler.tick(currentTimeMs); + ASSERT_FALSE(fakeWaterPump->isRunning()); // pump should be stopped after given time + + for(int i = 0; i < 10; i++) { + // emulate that pump was started again + fakeWaterPump->start(); + currentTimeMs += 1000; + waterPumpScheduler.tick(currentTimeMs); + ASSERT_FALSE(fakeWaterPump->isRunning()); // pump should be stopped + } +} \ No newline at end of file diff --git a/controller/tea_poor/test/test_native/tests/mocks/FakeEnvironment.h b/controller/tea_poor/test/test_native/tests/mocks/FakeEnvironment.h new file mode 100644 index 0000000..51407f8 --- /dev/null +++ b/controller/tea_poor/test/test_native/tests/mocks/FakeEnvironment.h @@ -0,0 +1,19 @@ +#ifndef FAKE_ENVIRONMENT_H +#define FAKE_ENVIRONMENT_H + +#include + +class FakeEnvironment : public IEnvironment { +public: + unsigned long time() const override { + return _time; + } + + void time(unsigned long time) { + _time = time; + } +private: + unsigned long _time = 0; +}; + +#endif \ No newline at end of file diff --git a/controller/tea_poor/test/test_native/tests/mocks/FakeWaterPump.h b/controller/tea_poor/test/test_native/tests/mocks/FakeWaterPump.h new file mode 100644 index 0000000..2dcf154 --- /dev/null +++ b/controller/tea_poor/test/test_native/tests/mocks/FakeWaterPump.h @@ -0,0 +1,18 @@ +#ifndef FAKE_WATER_PUMP_H +#define FAKE_WATER_PUMP_H + +#include + +// Fake water pump +class FakeWaterPump : public IWaterPump { +private: + bool _isRunning = false; +public: + void setup() override { _isRunning = false; } + void start() override { _isRunning = true; } + void stop() override { _isRunning = false; } + + bool isRunning() const override { return _isRunning; } +}; + +#endif // FAKE_WATER_PUMP_H \ No newline at end of file diff --git a/controller/tea_poor/test/test_native/tests/mocks/FakeWaterPumpSchedulerAPI.h b/controller/tea_poor/test/test_native/tests/mocks/FakeWaterPumpSchedulerAPI.h new file mode 100644 index 0000000..896046a --- /dev/null +++ b/controller/tea_poor/test/test_native/tests/mocks/FakeWaterPumpSchedulerAPI.h @@ -0,0 +1,26 @@ +// FakeWaterPumpSchedulerAPI.h is a mock class for WaterPumpSchedulerAPI.h +#ifndef FAKE_WATER_PUMP_SCHEDULER_API_H +#define FAKE_WATER_PUMP_SCHEDULER_API_H + +#include +#include + +class FakeWaterPumpSchedulerAPI : public IWaterPumpSchedulerAPI { +public: + void stop() override { + _log += "stop()\n"; + } + + void start(unsigned long runTimeMs, unsigned long currentTimeMs) override { + _log += "start(" + std::to_string(runTimeMs) + ", " + std::to_string(currentTimeMs) + ")\n"; + } + + WaterPumpStatus status() override { + return _status; + } + + WaterPumpStatus _status; + std::string _log; +}; + +#endif \ No newline at end of file