diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..59b161f --- /dev/null +++ b/.clang-format @@ -0,0 +1,31 @@ +BasedOnStyle: LLVM +AlignConsecutiveMacros: AcrossEmptyLines +AlignEscapedNewlines: DontAlign +AlignTrailingComments: false +AllowShortBlocksOnASingleLine: Never +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: None +AllowShortIfStatementsOnASingleLine: Never +AllowShortLoopsOnASingleLine: false +BreakBeforeBraces: Custom +BraceWrapping: + AfterCaseLabel: false + AfterClass: false + AfterControlStatement: Never + AfterEnum: false + AfterFunction: false + AfterNamespace: false + AfterUnion: false + BeforeCatch: false + BeforeElse: false + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyRecord: true +ColumnLimit: 0 +IndentCaseLabels: true +IndentPPDirectives: BeforeHash +IndentWidth: 4 +MaxEmptyLinesToKeep: 2 +PointerAlignment: Middle +SpaceAfterCStyleCast: true +NamespaceIndentation: All diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..d3dbff4 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = false +tab_width = 4 diff --git a/.gitea/workflows/pio_tests.yml b/.gitea/workflows/pio_tests.yml new file mode 100644 index 0000000..85dd5f2 --- /dev/null +++ b/.gitea/workflows/pio_tests.yml @@ -0,0 +1,34 @@ +name: PlatformIO flow +run-name: ${{ gitea.actor }} is testing + +on: + push: + branches: + - '*' + tags: + - '*' + +jobs: + build: + runs-on: ubuntu-22.04 + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Configure caching + uses: actions/cache@v3 + with: + path: | + ~/.cache/pip + ~/.platformio/.cache + key: ${{ runner.os }}-pio + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: 'pypy3.9' + - name: Install PlatformIO Core + run: | + python -m pip install --upgrade pip + pip install --upgrade platformio + - name: Run native unit tests + run: | + pio test diff --git a/.gitignore b/.gitignore index 259148f..12c0275 100644 --- a/.gitignore +++ b/.gitignore @@ -1,32 +1,11 @@ -# Prerequisites -*.d - -# Compiled Object files -*.slo -*.lo -*.o -*.obj - -# Precompiled Headers -*.gch -*.pch - -# Compiled Dynamic libraries -*.so -*.dylib -*.dll - -# Fortran module files -*.mod -*.smod - -# Compiled Static libraries -*.lai -*.la -*.a -*.lib - -# Executables -*.exe -*.out -*.app +.pio +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch +dist +# C Lion +.idea +cmake-build-* +CMakeLists.txt +CMakeListsPrivate.txt diff --git a/README.md b/README.md index f7a5fc6..d9e0415 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Interval - Arduino knihovna pro časování pomocí intervalů +![Build](https://git.xpablo.cz/pablo2048/Interval/actions/workflows/pio_tests.yml/badge.svg) + Koncepce programové konstrukce aplikace pro Arduino spočívá ve dvou hlavních metodách – **setup()** a **loop()**, ve které program neustále běží. Pro pohodlnější práci s obsluhou periodických procesů jsem napsal jednoduchou knihovnu, která tyto úkoly umožňuje realizovat velmi elegantním způsobem. diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..b83e14e --- /dev/null +++ b/platformio.ini @@ -0,0 +1,17 @@ +[platformio] +default_envs = native + +[env:native] +; build for desktop, not embedded device +; expects GCC to be installed and available on the computer! +platform = native +; build source code in src/ too +test_build_src = yes +;test_framework = custom ; uncomment this if you need debugging messages to be shown (or else run with `pio test -v`) +debug_test = test_interval +lib_deps = + https://github.com/FabioBatSilva/ArduinoFake/archive/refs/heads/master.zip +build_flags = + -std=gnu++17 + -DUNITY_OUTPUT_COLOR +build_type = debug diff --git a/src/interval.cpp b/src/interval.cpp index 3125c92..82b7003 100644 --- a/src/interval.cpp +++ b/src/interval.cpp @@ -1,74 +1,82 @@ #include "interval.h" -// Public Methods ////////////////////////////////////////////////////////////// -uint32_t Interval::remains() const -{ - - return _timeout - (millis() - _timefrom); +// Returns the remaining time until expiration, correctly handling overflow +uint32_t Interval::remains() const { + uint32_t currentTime = millis(); + return _timeout - (currentTime - _timefrom); } -uint32_t Interval::elapsed() const -{ - +// Returns the elapsed time since the interval started +uint32_t Interval::elapsed() const { return millis() - _timefrom; } -bool Interval::expired() -{ - bool result = false; +// Checks if the interval has expired based on the mode +bool Interval::expired() { + uint32_t currentTime = millis(); + uint32_t elapsedTime = currentTime - _timefrom; - if (_done) { - if (1 == _mode) { - // oneshot mode - if ((millis() - _timefrom) >= _timeout) { - _done = 0; - result = true; + switch (_mode) { + case ONESHOT: + // In oneshot mode, return true only once upon expiration + if (elapsedTime >= _timeout) { + if (_active) { + _active = false; + return true; + } } - } else if (2 == _mode) { - // periodic mode - if ((millis() - _timefrom) >= _timeout) { - result = true; - _timefrom = millis(); + break; + case PERIODIC: + // In periodic mode, return true and reset the timer on expiration + if (elapsedTime >= _timeout) { + _timefrom += _timeout; + if (currentTime - _timefrom >= _timeout) { + _timefrom = currentTime; + } + return true; } - } else { - // compatibility mode - if ((millis() - _timefrom) >= _timeout) - result = true; - } + break; + case COMPATIBILITY: + default: + // In compatibility mode, always return true if the interval has expired + if (_active) { + if (elapsedTime >= _timeout) { + _active = false; + return true; + } + } else { + return true; // already expired + } + break; } - return result; + return false; } -void Interval::set(uint32_t tmout) -{ - +// Sets the interval in compatibility mode +void Interval::set(const uint32_t tmout) { _timefrom = millis(); _timeout = tmout; - _mode = 0; - _done = 0xff; + _mode = COMPATIBILITY; + _active = true; } -void Interval::setOneshot(uint32_t tmout) -{ - +// Sets the interval in oneshot mode +void Interval::setOneshot(const uint32_t tmout) { _timefrom = millis(); _timeout = tmout; - _mode = 1; - _done = 0xff; + _mode = ONESHOT; + _active = true; } -void Interval::setPeriodic(uint32_t tmout) -{ - +// Sets the interval in periodic mode +void Interval::setPeriodic(const uint32_t tmout) { _timefrom = millis(); _timeout = tmout; - _mode = 2; - _done = 0xff; + _mode = PERIODIC; } -void Interval::reload() -{ - +// Reloads the interval timer (resets the starting time) +void Interval::reload() { _timefrom = millis(); - _done = 0xff; + _active = true; } diff --git a/src/interval.h b/src/interval.h index 4507f41..b4376be 100644 --- a/src/interval.h +++ b/src/interval.h @@ -1,31 +1,52 @@ #pragma once - -/* Interval - * Copyright (C) 2014, 2016, 2018, 2019, 2023 Pavel Brychta http://www.xpablo.cz +/* + * Interval + * Copyright (C) 2014, 2016, 2018, 2019, 2023, 2025 Pavel Brychta http://www.xpablo.cz * * This program is free software: you can redistribute it and/or modify it under the terms of the MIT License */ - #include -class Interval -{ -protected: - uint32_t _timefrom = 0; - uint32_t _timeout = 0; - uint8_t _mode = 0; // mode of actual operation (compatibility, oneshot, periodic) - uint8_t _done = 0xff; // compatibility mode autostart -public: +class Interval { + public: + // Enumeration for interval modes + enum IntervalMode { + COMPATIBILITY = 0, + ONESHOT = 1, + PERIODIC = 2 + }; + + protected: + uint32_t _timefrom = 0; // Start time of the interval + uint32_t _timeout = 0; // Timeout duration in milliseconds + IntervalMode _mode = COMPATIBILITY; // Current mode of operation + bool _active = true; // Active flag for oneshot and compatibilitymode + public: Interval() = default; - explicit Interval(uint32_t tmout) - : _timeout(tmout) - , _mode(2) // periodic mode autostart - {} + + // Constructor for periodic mode with autostart + explicit Interval(const uint32_t tmout) + : _timefrom(millis()), _timeout(tmout), _mode(PERIODIC) { + } + + // Checks if the interval has expired based on the current mode bool expired(); + + // Sets the interval in compatibility mode void set(uint32_t tmout); + + // Sets the interval in oneshot mode void setOneshot(uint32_t tmout); + + // Sets the interval in periodic mode void setPeriodic(uint32_t tmout); + + // Reloads the interval timer without changing mode or timeout void reload(); + + // Returns the elapsed time since the interval started [[nodiscard]] uint32_t elapsed() const; + + // Returns the remaining time until the interval expires [[nodiscard]] uint32_t remains() const; }; diff --git a/test/test_interval/main.cpp b/test/test_interval/main.cpp new file mode 100644 index 0000000..5672d2e --- /dev/null +++ b/test/test_interval/main.cpp @@ -0,0 +1,148 @@ +#include +#include +#include "interval.h" + +using namespace fakeit; + +// Global variable to simulate time in milliseconds +static uint32_t fakeMillis = 0; + +// Fake function to override millis() +uint32_t fakeMillisFunc() { + return fakeMillis; +} + +// setUp() is called before each test +void setUp(void) { + // Reset fake time + fakeMillis = 0; + ArduinoFakeReset(); + // Override millis() to use our fakeMillisFunc() + When(Method(ArduinoFake(), millis)).AlwaysDo(fakeMillisFunc); +} + +// tearDown() is called after each test (if needed) +void tearDown(void) { + // No teardown actions required +} + +// Test for Compatibility Mode (using set() method) +void test_CompatibilityMode(void) { + Interval interval; + interval.set(1000); // Set timeout to 1000ms in compatibility mode + + fakeMillis = 500; + TEST_ASSERT_FALSE(interval.expired()); // Should not be expired at 500ms + + fakeMillis = 1000; + TEST_ASSERT_TRUE(interval.expired()); // Should be expired at 1000ms + + // In compatibility mode, subsequent calls should still return true once expired + TEST_ASSERT_TRUE(interval.expired()); +} + +// Test for One-shot Mode (using setOneshot() method) +void test_OneshotMode(void) { + Interval interval; + interval.setOneshot(1000); // Set timeout to 1000ms in oneshot mode + + fakeMillis = 500; + TEST_ASSERT_FALSE(interval.expired()); // Not yet expired + + fakeMillis = 1000; + TEST_ASSERT_TRUE(interval.expired()); // Expired at 1000ms + + // Subsequent calls should return false because oneshot mode triggers only once + TEST_ASSERT_FALSE(interval.expired()); +} + +// Test for Periodic Mode (using setPeriodic() method) +void test_PeriodicMode(void) { + Interval interval; + interval.setPeriodic(1000); // Set periodic mode with 1000ms timeout + + fakeMillis = 500; + TEST_ASSERT_FALSE(interval.expired()); // Not yet expired + + fakeMillis = 1000; + TEST_ASSERT_TRUE(interval.expired()); // First period expired + + // After expiration, timer is reset; simulate next period + fakeMillis = 1500; + TEST_ASSERT_FALSE(interval.expired()); // Still within new period + + fakeMillis = 2000; + TEST_ASSERT_TRUE(interval.expired()); // Second period expired +} + +// Test for elapsed() and remains() functions +void test_elapsed_and_remains(void) { + Interval interval; + interval.set(1000); // Using compatibility mode, timeout set to 1000ms + + fakeMillis = 0; + TEST_ASSERT_EQUAL_UINT32(0, interval.elapsed()); + TEST_ASSERT_EQUAL_UINT32(1000, interval.remains()); + + fakeMillis = 500; + TEST_ASSERT_EQUAL_UINT32(500, interval.elapsed()); + TEST_ASSERT_EQUAL_UINT32(500, interval.remains()); + + fakeMillis = 1100; + TEST_ASSERT_EQUAL_UINT32(1100, interval.elapsed()); + // remains() uses unsigned arithmetic, so it will underflow if elapsed > timeout + TEST_ASSERT_EQUAL_UINT32(1000 - 1100, interval.remains()); +} + +// Test for handling overflow of millis() value +void test_overflow(void) { + Interval interval; + // Set fakeMillis to a value near the maximum of 32-bit unsigned int + fakeMillis = 0xFFFFFFF0; + interval.set(100); // Set timeout to 100ms in compatibility mode; _timefrom = 0xFFFFFFF0 + + // Simulate time just before expiration after overflow. + // After overflow, fakeMillis is low. For example, fakeMillis = 0x00000050. + // Expected elapsed time = 0x00000050 + (0x100000000 - 0xFFFFFFF0) = 0x50 + 0x10 = 96ms. + fakeMillis = 0x00000050; + TEST_ASSERT_FALSE(interval.expired()); // Not yet expired + + // Now simulate time after expiration: + // Set fakeMillis = 0x00000070. Expected elapsed time = 0x70 + 0x10 = 128ms, which is >= 100ms. + fakeMillis = 0x00000070; + TEST_ASSERT_TRUE(interval.expired()); // Should be expired due to overflow handling +} + +// Test for reload() functionality in oneshot mode +void test_reload(void) { + Interval interval; + interval.setOneshot(1000); // Set oneshot mode with 1000ms timeout + + fakeMillis = 0; + // Simulate expiration + fakeMillis = 1100; + TEST_ASSERT_TRUE(interval.expired()); // Expired at 1100ms + + // Reload the interval to reset the timer and reactivate oneshot mode + fakeMillis = 1100; + interval.reload(); + + // Immediately after reload, should not be expired + fakeMillis = 1200; + TEST_ASSERT_FALSE(interval.expired()); + + // After passing the timeout from reload, it should expire again + fakeMillis = 2200; + TEST_ASSERT_TRUE(interval.expired()); +} + +int main(int argc, char **argv) { + UNITY_BEGIN(); + RUN_TEST(test_CompatibilityMode); + RUN_TEST(test_OneshotMode); + RUN_TEST(test_PeriodicMode); + RUN_TEST(test_elapsed_and_remains); + RUN_TEST(test_overflow); + RUN_TEST(test_reload); + return UNITY_END(); +}