From 8927a65573c26d1d7bec8995eb947d72ec5f69ce Mon Sep 17 00:00:00 2001 From: Holger Weber Date: Fri, 15 May 2026 00:54:34 +0200 Subject: [PATCH] Respect values over 11.1s. --- SPEC.md | 8 +-- include/PulseTimer.h | 3 + include/config.example.h | 2 +- include/config.h | 4 +- src/PulseTimer.cpp | 87 ++++++++++++++++++++++++++++- src/main.cpp | 2 +- test/test_pulse_timer/test_main.cpp | 20 ++++++- 7 files changed, 114 insertions(+), 12 deletions(-) diff --git a/SPEC.md b/SPEC.md index 3c087ba..94b112d 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1,8 +1,8 @@ # Tasmota remote pulse timer trigger Das Programm für einen ESP8266 soll den PulseTimer einer Tasmota Steckdose triggern. -Die Hardware hat einen Rotary Encoder sowie ein OLED Display. -Mit dem Rotary Encoder (RT) stellt man eine Zeit im Bereich von 2s bis 10s mit einer Auflösung von 100ms. -Über den RT soll der Wert pro Tick um 100ms angepasst werden können. +Die Hardware hat einen Rotary Encoder sowie ein OLED Display. +Mit dem Rotary Encoder (RT) stellt man eine Zeit im Bereich von `MIN_TIMER_MS` bis `MAX_TIMER_MS`. +Über den RT soll der Wert pro Tick bis 11,1s um 100ms angepasst werden. Ab 11,1s erlaubt Tasmota nur noch Sekundenwerte; der nächste Wert nach 11,1s ist deshalb 12s, danach wird pro Tick um 1s angepasst. Mit Druck auf den Taster des RT soll zunächst der PulseTimer eingestellt und dann getriggert werden. Die Einstellungen für die SSID und das Passwort sollen aus einer separaten config.h Datei ausgelesen werden. Dort soll auch die IP Adresse der Tasmota Steckdose eingestellt werden. Zudem dort noch den Initialwert für den Timer hinterlegen. @@ -10,7 +10,7 @@ Der Project soll mittels PlatformIO und Arduino Framework umgesetzt werden. Das ## Umsetzung Die Tasmota Steckdose wird per HTTP angesprochen. Beim Druck auf den Taster sendet das Programm: -1. `PulseTime1 ` mit `` in 100ms-Schritten. Der erlaubte Bereich 2s bis 10s entspricht damit `20` bis `100`. +1. `PulseTime1 `. Werte `1` bis `111` entsprechen 0,1s-Schritten bis 11,1s. Werte ab `112` entsprechen Sekundenwerten mit Offset 100, also z.B. `112` fuer 12s und `113` fuer 13s. 2. `Power1 ON`, um den zuvor gesetzten PulseTimer auszulösen. Die Datei `include/config.h` enthält: diff --git a/include/PulseTimer.h b/include/PulseTimer.h index c531445..b93df83 100644 --- a/include/PulseTimer.h +++ b/include/PulseTimer.h @@ -9,8 +9,11 @@ namespace trbc { constexpr uint16_t kMinTimerMs = MIN_TIMER_MS; constexpr uint16_t kMaxTimerMs = MAX_TIMER_MS; constexpr uint16_t kTimerStepMs = 100; +constexpr uint16_t kTasmotaTenthsLimitMs = 11100; +constexpr uint16_t kTasmotaSecondsStartMs = 12000; uint16_t clampTimerMs(int valueMs); +uint16_t normalizeTimerMs(int valueMs); uint16_t adjustTimerMs(uint16_t currentMs, int ticks); uint16_t tasmotaPulseTimeValue(uint16_t timerMs); diff --git a/include/config.example.h b/include/config.example.h index 630c498..c8422ce 100644 --- a/include/config.example.h +++ b/include/config.example.h @@ -7,4 +7,4 @@ #define TASMOTA_HOST "192.168.1.50" #define INITIAL_TIMER_MS 5000 #define MIN_TIMER_MS 2000 -#define MAX_TIMER_MS 10000 +#define MAX_TIMER_MS 13000 diff --git a/include/config.h b/include/config.h index bde59ab..4f74d3d 100644 --- a/include/config.h +++ b/include/config.h @@ -7,5 +7,5 @@ #define WIFI_PASSWORD "!Sternenlabor99!" #define TASMOTA_HOST "192.168.240.101" #define INITIAL_TIMER_MS 5000 -#define MIN_TIMER_MS 2000 -#define MAX_TIMER_MS 13000 +#define MIN_TIMER_MS 0 +#define MAX_TIMER_MS (2 * 60 * 1000) diff --git a/src/PulseTimer.cpp b/src/PulseTimer.cpp index 49f6834..d13c1f2 100644 --- a/src/PulseTimer.cpp +++ b/src/PulseTimer.cpp @@ -2,6 +2,30 @@ namespace trbc { +namespace { + +uint16_t highestRepresentableAtOrBelow(uint16_t valueMs) { + if (valueMs <= kTasmotaTenthsLimitMs) { + return clampTimerMs((valueMs / kTimerStepMs) * kTimerStepMs); + } + if (valueMs < kTasmotaSecondsStartMs) { + return kTasmotaTenthsLimitMs; + } + return clampTimerMs((valueMs / 1000) * 1000); +} + +uint16_t lowestRepresentableAtOrAbove(uint16_t valueMs) { + if (valueMs <= kTasmotaTenthsLimitMs) { + return clampTimerMs(((valueMs + kTimerStepMs - 1) / kTimerStepMs) * kTimerStepMs); + } + if (valueMs < kTasmotaSecondsStartMs) { + return clampTimerMs(kTasmotaSecondsStartMs); + } + return clampTimerMs(((valueMs + 999) / 1000) * 1000); +} + +} // namespace + uint16_t clampTimerMs(int valueMs) { if (valueMs < kMinTimerMs) { return kMinTimerMs; @@ -12,13 +36,70 @@ uint16_t clampTimerMs(int valueMs) { return static_cast(valueMs); } +uint16_t normalizeTimerMs(int valueMs) { + const uint16_t clamped = clampTimerMs(valueMs); + if (clamped <= kTasmotaTenthsLimitMs) { + uint16_t rounded = ((clamped + (kTimerStepMs / 2)) / kTimerStepMs) * kTimerStepMs; + if (rounded > kTasmotaTenthsLimitMs) { + rounded = kTasmotaTenthsLimitMs; + } + return clampTimerMs(rounded); + } + + uint16_t rounded = ((clamped + 500) / 1000) * 1000; + if (rounded < kTasmotaSecondsStartMs) { + rounded = kTasmotaSecondsStartMs; + } + if (rounded > kMaxTimerMs) { + return highestRepresentableAtOrBelow(kMaxTimerMs); + } + if (rounded < kMinTimerMs) { + return lowestRepresentableAtOrAbove(kMinTimerMs); + } + return rounded; +} + uint16_t adjustTimerMs(uint16_t currentMs, int ticks) { - const int adjusted = static_cast(currentMs) + ticks * kTimerStepMs; - return clampTimerMs(adjusted); + uint16_t adjusted = normalizeTimerMs(currentMs); + const int direction = ticks < 0 ? -1 : 1; + + for (int remaining = ticks < 0 ? -ticks : ticks; remaining > 0; remaining--) { + uint16_t next = adjusted; + if (direction > 0) { + if (adjusted < kTasmotaTenthsLimitMs) { + next = adjusted + kTimerStepMs; + } else if (adjusted < kTasmotaSecondsStartMs) { + next = kTasmotaSecondsStartMs; + } else { + next = adjusted + 1000; + } + if (next > kMaxTimerMs || next < adjusted) { + return highestRepresentableAtOrBelow(kMaxTimerMs); + } + } else { + if (adjusted > kTasmotaSecondsStartMs) { + next = adjusted - 1000; + } else if (adjusted > kTasmotaTenthsLimitMs) { + next = kTasmotaTenthsLimitMs; + } else { + next = adjusted - kTimerStepMs; + } + if (next < kMinTimerMs || next > adjusted) { + return lowestRepresentableAtOrAbove(kMinTimerMs); + } + } + adjusted = next; + } + + return adjusted; } uint16_t tasmotaPulseTimeValue(uint16_t timerMs) { - return clampTimerMs(timerMs) / kTimerStepMs; + const uint16_t normalized = normalizeTimerMs(timerMs); + if (normalized <= kTasmotaTenthsLimitMs) { + return normalized / kTimerStepMs; + } + return (normalized / 1000) + 100; } } // namespace trbc diff --git a/src/main.cpp b/src/main.cpp index e586a38..34ce9e8 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -20,7 +20,7 @@ constexpr unsigned long kWifiRetryMs = 10000; constexpr unsigned long kButtonDebounceMs = 35; constexpr unsigned long kStatusHoldMs = 2500; -uint16_t timerMs = trbc::clampTimerMs(INITIAL_TIMER_MS); +uint16_t timerMs = trbc::normalizeTimerMs(INITIAL_TIMER_MS); bool needsDisplayUpdate = true; unsigned long lastDisplayRefresh = 0; unsigned long lastWifiAttempt = 0; diff --git a/test/test_pulse_timer/test_main.cpp b/test/test_pulse_timer/test_main.cpp index 3bce6b2..a4018dc 100644 --- a/test/test_pulse_timer/test_main.cpp +++ b/test/test_pulse_timer/test_main.cpp @@ -13,22 +13,40 @@ void test_adjusts_in_100_ms_steps() { TEST_ASSERT_EQUAL_UINT16(4700, trbc::adjustTimerMs(5000, -3)); } +void test_adjusts_in_seconds_above_tasmota_threshold() { + TEST_ASSERT_EQUAL_UINT16(11100, trbc::adjustTimerMs(11000, 1)); + TEST_ASSERT_EQUAL_UINT16(12000, trbc::adjustTimerMs(11100, 1)); + TEST_ASSERT_EQUAL_UINT16(13000, trbc::adjustTimerMs(12000, 1)); + TEST_ASSERT_EQUAL_UINT16(12000, trbc::adjustTimerMs(13000, -1)); + TEST_ASSERT_EQUAL_UINT16(11100, trbc::adjustTimerMs(12000, -1)); +} + void test_adjustment_stays_in_range() { TEST_ASSERT_EQUAL_UINT16(MIN_TIMER_MS, trbc::adjustTimerMs(MIN_TIMER_MS, -1)); TEST_ASSERT_EQUAL_UINT16(MAX_TIMER_MS, trbc::adjustTimerMs(MAX_TIMER_MS, 1)); } +void test_normalizes_to_tasmota_representable_values() { + TEST_ASSERT_EQUAL_UINT16(5500, trbc::normalizeTimerMs(5450)); + TEST_ASSERT_EQUAL_UINT16(12000, trbc::normalizeTimerMs(11400)); + TEST_ASSERT_EQUAL_UINT16(12000, trbc::normalizeTimerMs(11500)); +} + void test_converts_to_tasmota_pulsetime_units() { TEST_ASSERT_EQUAL_UINT16(20, trbc::tasmotaPulseTimeValue(2000)); TEST_ASSERT_EQUAL_UINT16(55, trbc::tasmotaPulseTimeValue(5500)); - TEST_ASSERT_EQUAL_UINT16(100, trbc::tasmotaPulseTimeValue(10000)); + TEST_ASSERT_EQUAL_UINT16(111, trbc::tasmotaPulseTimeValue(11100)); + TEST_ASSERT_EQUAL_UINT16(112, trbc::tasmotaPulseTimeValue(12000)); + TEST_ASSERT_EQUAL_UINT16(113, trbc::tasmotaPulseTimeValue(13000)); } int main() { UNITY_BEGIN(); RUN_TEST(test_clamps_timer_range); RUN_TEST(test_adjusts_in_100_ms_steps); + RUN_TEST(test_adjusts_in_seconds_above_tasmota_threshold); RUN_TEST(test_adjustment_stays_in_range); + RUN_TEST(test_normalizes_to_tasmota_representable_values); RUN_TEST(test_converts_to_tasmota_pulsetime_units); return UNITY_END(); }