diff --git a/code/espurna/debug.cpp b/code/espurna/debug.cpp index 82accd6d..54e7e82e 100644 --- a/code/espurna/debug.cpp +++ b/code/espurna/debug.cpp @@ -191,7 +191,7 @@ void enable() { void delayedEnable() { disable(); - schedule_function(enable); + ::espurnaRegisterOnce(enable); } void send(const char* message, size_t len, Timestamp); diff --git a/code/espurna/espurna.h b/code/espurna/espurna.h index 34512060..a5a1a176 100644 --- a/code/espurna/espurna.h +++ b/code/espurna/espurna.h @@ -62,6 +62,9 @@ void espurnaReload(); using LoopCallback = void (*)(); void espurnaRegisterLoop(LoopCallback); +void espurnaRegisterOnce(espurna::Callback); +void espurnaRegisterOnceUnique(espurna::Callback::Type); + espurna::duration::Milliseconds espurnaLoopDelay(); void espurnaLoopDelay(espurna::duration::Milliseconds); diff --git a/code/espurna/homeassistant.cpp b/code/espurna/homeassistant.cpp index 1daa5882..3f05942e 100644 --- a/code/espurna/homeassistant.cpp +++ b/code/espurna/homeassistant.cpp @@ -905,12 +905,12 @@ enum class State { }; State state { State::Initial }; -Ticker timer; +timer::SystemTimer timer; void send(TaskPtr ptr, FlagPtr flag_ptr); void stop(bool done) { - timer.detach(); + timer.stop(); if (done) { DEBUG_MSG_P(PSTR("[HA] Stopping discovery\n")); state = State::Sent; @@ -921,8 +921,8 @@ void stop(bool done) { } void schedule(espurna::duration::Milliseconds wait, TaskPtr ptr, FlagPtr flag_ptr) { - internal::timer.once_ms_scheduled( - wait.count(), + timer.schedule_once( + wait, [ptr, flag_ptr]() { send(ptr, flag_ptr); }); @@ -989,7 +989,7 @@ void send(TaskPtr ptr, FlagPtr flag_ptr) { } // namespace internal void publishDiscovery() { - if (!mqttConnected() || internal::timer.active() || (internal::state != internal::State::Pending)) { + if (!mqttConnected() || internal::timer || (internal::state != internal::State::Pending)) { return; } @@ -1031,7 +1031,7 @@ void mqttCallback(unsigned int type, const char* topic, char* payload) { if (internal::state == internal::State::Sent) { internal::state = internal::State::Pending; } - internal::timer.detach(); + internal::timer.stop(); return; } @@ -1039,7 +1039,7 @@ void mqttCallback(unsigned int type, const char* topic, char* payload) { #if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE ::mqttSubscribe(MQTT_TOPIC_LIGHT_JSON); #endif - ::schedule_function(publishDiscovery); + ::espurnaRegisterOnce(publishDiscovery); return; } diff --git a/code/espurna/light.cpp b/code/espurna/light.cpp index 33f30ac6..1e9a35ed 100644 --- a/code/espurna/light.cpp +++ b/code/espurna/light.cpp @@ -18,8 +18,6 @@ Copyright (C) 2019-2021 by Maxim Prokhorov -#include #include #include @@ -638,13 +636,52 @@ void _lightUpdateMapping(T& channels) { } } +template +struct LightTimerValue { + using Timer = espurna::timer::SystemTimer; + using Duration = Timer::Duration; + + LightTimerValue() = delete; + constexpr LightTimerValue(T defaultValue) : + _defaultValue(defaultValue) + {} + + explicit operator bool() const { + return _value != _defaultValue; + } + + void wait_set(Duration duration, T value) { + _timer.once( + duration, + [&]() { + _value = value; + }); + } + + void reset() { + _value = _defaultValue; + } + + T get() { + const auto value = _value; + reset(); + return value; + } + +private: + T _defaultValue; + T _value; + espurna::timer::SystemTimer _timer; +}; + auto _light_save_delay = espurna::light::build::saveDelay(); bool _light_save { espurna::light::build::save() }; -Ticker _light_save_ticker; + +LightTimerValue _light_save_timer(false); auto _light_report_delay = espurna::light::build::reportDelay(); -Ticker _light_report_ticker; std::forward_list _light_report; +LightTimerValue _light_report_timer(0); bool _light_has_controls = false; bool _light_has_cold_white = false; @@ -1675,11 +1712,6 @@ private: }; struct LightSequenceHandler { - LightSequenceHandler& operator=(const LightSequenceCallbacks& callbacks) { - _callbacks = callbacks; - return *this; - } - LightSequenceHandler& operator=(LightSequenceCallbacks&& callbacks) { _callbacks = std::move(callbacks); return *this; @@ -1701,12 +1733,37 @@ private: LightSequenceCallbacks _callbacks; }; +struct LightProviderHandler { + using Timer = espurna::timer::SystemTimer; + using Duration = Timer::Duration; + + LightProviderHandler() = default; + + explicit operator bool() const { + return _ready; + } + + void stop() { + _ready = false; + _timer.stop(); + } + + void start(Duration duration) { + _timer.once(duration, + [&]() { + _ready = true; + }); + } + +private: + Timer _timer; + bool _ready { false }; +}; + LightUpdateHandler _light_update; -bool _light_provider_update = false; +LightProviderHandler _light_provider_update; LightSequenceHandler _light_sequence; - -Ticker _light_transition_ticker; std::unique_ptr _light_transition; auto _light_transition_time = espurna::light::build::transitionTime(); @@ -1805,7 +1862,7 @@ void _lightProviderUpdate() { } if (!_light_transition) { - _light_provider_update = false; + _light_provider_update.stop(); return; } @@ -1820,13 +1877,11 @@ void _lightProviderUpdate() { _light_transition.reset(nullptr); } - _light_provider_update = false; + _light_provider_update.stop(); } void _lightProviderSchedule(espurna::duration::Milliseconds next) { - _light_transition_ticker.once_ms(next.count(), []() { - _light_provider_update = true; - }); + _light_provider_update.start(next); } } // namespace @@ -2894,6 +2949,17 @@ void _lightSequenceCheck() { } } +void _lightPostLoop() { + if (_light_report_timer) { + _lightReport(_light_report_timer.get()); + } + + if (_light_save_timer) { + _light_save_timer.reset(); + _lightSaveSettings(); + } +} + void _lightUpdate() { if (!_light_update) { return; @@ -2917,18 +2983,12 @@ void _lightUpdate() { // Send current state to all available 'report' targets // (make sure to delay the report, in case lightUpdate is called repeatedly) - _light_report_ticker.once_ms( - _light_report_delay.count(), - [report]() { - _lightReport(report); - }); + _light_report_timer.wait_set(_light_report_delay, report); // Always save to RTCMEM, optionally preserve the state in the settings storage _lightSaveRtcmem(); if (save) { - _light_save_ticker.once_ms( - _light_save_delay.count(), - _lightSaveSettings); + _light_save_timer.wait_set(_light_save_delay, true); } }); } @@ -3337,7 +3397,7 @@ bool lightAdd() { _light_channels.emplace_back(LightChannel()); if (State::Scheduled != state) { state = State::Scheduled; - schedule_function([]() { + espurnaRegisterOnceUnique([]() { _lightBoot(); state = State::Done; }); @@ -3482,6 +3542,7 @@ void lightSetup() { _lightSequenceCheck(); _lightUpdate(); _lightProviderUpdate(); + _lightPostLoop(); }); } diff --git a/code/espurna/light.h b/code/espurna/light.h index a49a0674..7a754a27 100644 --- a/code/espurna/light.h +++ b/code/espurna/light.h @@ -197,8 +197,7 @@ void lightTransition(LightTransition); // Light internals are forced to be sequential. In case some actions need to happen // right after transition / channel state / state changes, it will call these functions -using LightSequenceCallback = std::function; -using LightSequenceCallbacks = std::forward_list; +using LightSequenceCallbacks = std::forward_list; void lightSequence(LightSequenceCallbacks); void lightUpdateSequence(LightTransition); diff --git a/code/espurna/main.cpp b/code/espurna/main.cpp index e6a16206..da88645a 100644 --- a/code/espurna/main.cpp +++ b/code/espurna/main.cpp @@ -23,6 +23,9 @@ along with this program. If not, see . #include "espurna.h" #include "main.h" +#include +#include + // ----------------------------------------------------------------------------- // GENERAL CALLBACKS // ----------------------------------------------------------------------------- @@ -47,24 +50,35 @@ constexpr espurna::duration::Milliseconds loopDelay() { } // namespace build namespace settings { +namespace keys { + +alignas(4) static constexpr char LoopDelay[] PROGMEM = "loopDelay"; + +} // namespace keys espurna::duration::Milliseconds loopDelay() { - return std::clamp(getSetting("loopDelay", build::loopDelay()), build::LoopDelayMin, build::LoopDelayMax); + return std::clamp(getSetting(keys::LoopDelay, build::loopDelay()), build::LoopDelayMin, build::LoopDelayMax); } } // namespace settings namespace internal { +std::vector reload_callbacks; +bool reload_flag { false }; + std::vector loop_callbacks; espurna::duration::Milliseconds loop_delay { build::LoopDelayMin }; -std::vector reload_callbacks; -bool reload_flag { false }; +std::forward_list once_callbacks; } // namespace internal -bool reload() { +void flag_reload() { + internal::reload_flag = true; +} + +bool check_reload() { if (internal::reload_flag) { internal::reload_flag = false; return true; @@ -73,18 +87,68 @@ bool reload() { return false; } +void push_reload(ReloadCallback callback) { + internal::reload_callbacks.push_back(callback); +} + +void push_loop(LoopCallback callback) { + internal::loop_callbacks.push_back(callback); +} + +duration::Milliseconds loop_delay() { + return internal::loop_delay; +} + +void loop_delay(duration::Milliseconds value) { + internal::loop_delay = value; +} + +void push_once(Callback callback) { + internal::once_callbacks.push_front(std::move(callback)); +} + +void push_once_unique(Callback::Type callback) { + auto& callbacks = internal::once_callbacks; + + auto it = std::find_if( + callbacks.begin(), + callbacks.end(), + [&](const Callback& other) { + return other == callback; + }); + + if ((it != callbacks.begin()) && (it != callbacks.end())) { + std::swap(*callbacks.begin(), *it); + return; + } + + push_once(Callback(callback)); +} + void loop() { // Reload config before running any callbacks - if (reload()) { + if (check_reload()) { for (const auto& callback : internal::reload_callbacks) { callback(); } } + // Loop callbacks, registered some time in setup() + // Notice that everything is in order of registration for (const auto& callback : internal::loop_callbacks) { callback(); } + // One-time callbacks, registered some time during runtime + // Notice that callback container is LIFO, most recently added + // callback is called first. Copy to allow container modifications. + decltype(internal::once_callbacks) once_callbacks; + once_callbacks.swap(internal::once_callbacks); + + for (const auto& callback : once_callbacks) { + callback(); + } + espurna::time::delay(internal::loop_delay); } @@ -313,43 +377,34 @@ void setup() { } // namespace main } // namespace +} // namespace espurna -bool StringView::equals(StringView other) const { - if (other._len == _len) { - if (inFlash(_ptr) && inFlash(other._ptr)) { - return _ptr == other._ptr; - } else if (inFlash(_ptr)) { - return memcmp_P(other._ptr, _ptr, _len) == 0; - } else if (inFlash(other._ptr)) { - return memcmp_P(_ptr, other._ptr, _len) == 0; - } - - return __builtin_memcmp(_ptr, other._ptr, _len) == 0; - } - - return false; +void espurnaRegisterOnce(espurna::Callback callback) { + espurna::main::push_once(std::move(callback)); } -} // namespace espurna - -void espurnaRegisterLoop(LoopCallback callback) { - espurna::main::internal::loop_callbacks.push_back(callback); +void espurnaRegisterOnceUnique(espurna::Callback::Type ptr) { + espurna::main::push_once_unique(ptr); } void espurnaRegisterReload(LoopCallback callback) { - espurna::main::internal::reload_callbacks.push_back(callback); + espurna::main::push_reload(callback); +} + +void espurnaRegisterLoop(LoopCallback callback) { + espurna::main::push_loop(callback); } void espurnaReload() { - espurna::main::internal::reload_flag = true; + espurna::main::flag_reload(); } espurna::duration::Milliseconds espurnaLoopDelay() { - return espurna::main::internal::loop_delay; + return espurna::main::loop_delay(); } void espurnaLoopDelay(espurna::duration::Milliseconds value) { - espurna::main::internal::loop_delay = value; + espurna::main::loop_delay(value); } void setup() { diff --git a/code/espurna/mqtt.cpp b/code/espurna/mqtt.cpp index 82eef8ab..b16c8f84 100644 --- a/code/espurna/mqtt.cpp +++ b/code/espurna/mqtt.cpp @@ -13,7 +13,6 @@ Updated secure client support by Niek van der Maas < mail at niekvandermaas dot #include #include -#include #include "system.h" #include "mdns.h" @@ -537,7 +536,7 @@ private: size_t _mqtt_json_payload_count { 0ul }; std::forward_list _mqtt_json_payload; -Ticker _mqtt_json_payload_flush; +espurna::timer::SystemTimer _mqtt_json_payload_flush; } // namespace @@ -795,16 +794,16 @@ void _mqttConfigure() { #if MDNS_SERVER_SUPPORT -constexpr unsigned long MqttMdnsDiscoveryInterval { 15000 }; -Ticker _mqtt_mdns_discovery; +constexpr auto MqttMdnsDiscoveryInterval = espurna::duration::Seconds(15); +espurna::timer::SystemTimer _mqtt_mdns_discovery; void _mqttMdnsStop() { - _mqtt_mdns_discovery.detach(); + _mqtt_mdns_discovery.stop(); } void _mqttMdnsDiscovery(); void _mqttMdnsSchedule() { - _mqtt_mdns_discovery.once_ms_scheduled(MqttMdnsDiscoveryInterval, _mqttMdnsDiscovery); + _mqtt_mdns_discovery.once(MqttMdnsDiscoveryInterval, _mqttMdnsDiscovery); } void _mqttMdnsDiscovery() { @@ -1369,7 +1368,8 @@ uint16_t mqttSendRaw(const char * topic, const char * message) { bool mqttSend(const char * topic, const char * message, bool force, bool retain) { if (!force && _mqtt_use_json) { mqttEnqueue(topic, message); - _mqtt_json_payload_flush.once_ms(MQTT_USE_JSON_DELAY, mqttFlush); + _mqtt_json_payload_flush.once( + espurna::duration::Milliseconds(MQTT_USE_JSON_DELAY), mqttFlush); return true; } diff --git a/code/espurna/ntp.cpp b/code/espurna/ntp.cpp index bb0d62e2..432008df 100644 --- a/code/espurna/ntp.cpp +++ b/code/espurna/ntp.cpp @@ -17,7 +17,6 @@ Copyright (C) 2019 by Maxim Prokhorov #include #include -#include #include #include @@ -592,7 +591,7 @@ void report() { } const auto info = makeInfo(); - DEBUG_MSG_P(PSTR("[NTP] Server %s\n"), internal::server.c_str()); + DEBUG_MSG_P(PSTR("[NTP] Server %s\n"), ntp::internal::server.c_str()); DEBUG_MSG_P(PSTR("[NTP] Last Sync %s (UTC)\n"), info.sync.c_str()); DEBUG_MSG_P(PSTR("[NTP] UTC Time %s\n"), info.utc.c_str()); @@ -602,11 +601,17 @@ void report() { } } +void schedule_now() { + ::espurnaRegisterOnceUnique(report); +} + } // namespace debug #endif namespace tick { +// Never allow delays less than a second, or greater than a minute +// (ref. Non-OS 3.1.1 os_timer_arm, actual minimal value is 5ms) static constexpr espurna::duration::Seconds OffsetMin { 1 }; static constexpr espurna::duration::Seconds OffsetMax { 60 }; @@ -615,7 +620,7 @@ using Callbacks = std::forward_list; namespace internal { Callbacks callbacks; -Ticker timer; +timer::SystemTimer timer; } // namespace internal @@ -660,27 +665,15 @@ void callback() { schedule(OffsetMax - espurna::duration::Seconds(local_tm.tm_sec)); } -void schedule(espurna::duration::Seconds offset) { - static bool scheduled { false }; - - // Never allow delays less than a second, or greater than a minute - // (ref. Non-OS 3.1.1 os_timer_arm, actual minimal value is 5ms) - if (!scheduled) { - scheduled = true; - internal::timer.once_scheduled( - std::clamp(offset, OffsetMin, OffsetMax).count(), - []() { - scheduled = false; - callback(); - }); - } +void schedule_now() { + ::espurnaRegisterOnceUnique(callback); } -void init() { - static bool initialized { false }; - if (!initialized) { - schedule_function(callback); - initialized = true; +void schedule(espurna::duration::Seconds offset) { + if (!internal::timer) { + internal::timer.once( + std::clamp(offset, OffsetMin, OffsetMax), + schedule_now); } } @@ -688,13 +681,13 @@ void init() { void onSystemTimeSynced() { internal::status.update(::time(nullptr)); - tick::init(); + tick::schedule_now(); #if WEB_SUPPORT wsPost(web::onData); #endif #if DEBUG_SUPPORT - schedule_function(debug::report); + debug::schedule_now(); #endif } @@ -801,7 +794,7 @@ void onStationModeGotIP(WiFiEventStationModeGotIP) { DEBUG_MSG_P(PSTR("[NTP] Updating `ntpDhcp` to ignore the DHCP values\n")); settings::dhcp(false); sntp_servermode_dhcp(0); - schedule_function(configure); + ::espurnaRegisterOnce(configure); return; } diff --git a/code/espurna/ota_asynctcp.cpp b/code/espurna/ota_asynctcp.cpp index af2a8e29..3276a067 100644 --- a/code/espurna/ota_asynctcp.cpp +++ b/code/espurna/ota_asynctcp.cpp @@ -104,7 +104,7 @@ void disconnect() { void onDisconnect(void* arg, AsyncClient*) { DEBUG_MSG_P(PSTR("\n")); otaFinalize(reinterpret_cast(arg)->size, CustomResetReason::Ota, true); - schedule_function(internal::disconnect); + espurnaRegisterOnce(internal::disconnect); } void onTimeout(void*, AsyncClient* client, uint32_t) { diff --git a/code/espurna/ota_httpupdate.cpp b/code/espurna/ota_httpupdate.cpp index b47d7689..6af3fba1 100644 --- a/code/espurna/ota_httpupdate.cpp +++ b/code/espurna/ota_httpupdate.cpp @@ -126,6 +126,12 @@ void clientFromHttps(const String& url) { #endif // SECURE_CLIENT_BEARSSL +namespace internal { + +String url; + +} // namespace internal + void clientFromUrl(const String& url) { if (url.startsWith("http://")) { clientFromHttp(url); @@ -141,6 +147,17 @@ void clientFromUrl(const String& url) { DEBUG_MSG_P(PSTR("[OTA] Incorrect URL specified\n")); } +void clientFromInternalUrl() { + const auto url = std::move(internal::url); + clientFromUrl(url); +} + +[[gnu::unused]] +void clientQueueUrl(String url) { + internal::url = std::move(url); + espurnaRegisterOnceUnique(clientFromInternalUrl); +} + #if TERMINAL_SUPPORT alignas(4) static constexpr char OtaCommand[] PROGMEM = "OTA"; @@ -173,16 +190,9 @@ void mqttCallback(unsigned int type, const char* topic, char* payload) { if (type == MQTT_MESSAGE_EVENT) { const String t = mqttMagnitude(topic); - static bool busy { false }; - - if (!busy && t.equals(MQTT_TOPIC_OTA)) { + if (!internal::url.length() && t.equals(MQTT_TOPIC_OTA)) { DEBUG_MSG_P(PSTR("[OTA] Queuing from URL: %s\n"), payload); - - const String url(payload); - schedule_function([url]() { - clientFromUrl(url); - busy = false; - }); + clientQueueUrl(payload); } return; diff --git a/code/espurna/relay.cpp b/code/espurna/relay.cpp index b219d755..d350ab93 100644 --- a/code/espurna/relay.cpp +++ b/code/espurna/relay.cpp @@ -363,15 +363,7 @@ Mode mode(size_t index) { namespace { struct Timer { - // limit is per https://www.espressif.com/sites/default/files/documentation/2c-esp8266_non_os_sdk_api_reference_en.pdf - // > 3.1.1 os_timer_arm - // > with `system_timer_reinit()`, the timer value allowed ranges from 100 to 0x0x689D0. - // > otherwise, the timer value allowed ranges from 5 to 0x68D7A3. - - static constexpr auto DurationMin = Duration { 5 }; - static constexpr auto DurationMax = Duration { espurna::duration::Hours { 1 } }; - - using TimeSource = espurna::time::CoreClock; + using Duration = timer::SystemTimer::Duration; Timer() = delete; Timer(const Timer&) = delete; @@ -384,14 +376,14 @@ struct Timer { {} ~Timer() { - stop(); + _timer.stop(); } Timer& operator=(const Timer&) = delete; Timer& operator=(Timer&&) = delete; explicit operator bool() const { - return _armed; + return static_cast(_timer); } bool operator==(const Timer& other) const { @@ -420,55 +412,29 @@ struct Timer { } void stop() { - if (_armed) { - os_timer_disarm(&_timer); - _timer = os_timer_t{}; - _armed = false; - } + _timer.stop(); } void start() { - stop(); - - auto delay = std::clamp(_duration, DurationMin, DurationMax); - os_timer_setfn(&_timer, timerCallback, this); - os_timer_arm(&_timer, delay.count(), 0); - - _start = TimeSource::now(); - _armed = true; + const auto id = _id; + const auto status = _status; + _timer.once( + (_duration.count() > 0) + ? _duration + : timer::SystemTimer::DurationMin, + [id, status]() { + relayStatus(id, status); + }); } private: - void check() { - auto elapsed = TimeSource::now() - _start; - if (elapsed <= _duration) { - auto left = std::clamp(_duration - elapsed, DurationMin, DurationMax); - if (left != DurationMin) { - os_timer_arm(&_timer, left.count(), 0); - return; - } - } - - relayStatus(_id, _status); - stop(); - } - - static void timerCallback(void* arg) { - reinterpret_cast(arg)->check(); - } - Duration _duration; size_t _id; bool _status; - TimeSource::time_point _start; - bool _armed { false }; - os_timer_t _timer {}; + timer::SystemTimer _timer; }; -constexpr Duration Timer::DurationMin; -constexpr Duration Timer::DurationMax; - namespace internal { std::forward_list timers; @@ -1087,7 +1053,8 @@ public: namespace { struct RelaySaveTimer { - using Duration = espurna::duration::Milliseconds; + using Timer = espurna::timer::SystemTimer; + using Duration = Timer::Duration; RelaySaveTimer() = default; @@ -1098,33 +1065,28 @@ struct RelaySaveTimer { RelaySaveTimer& operator=(RelaySaveTimer&&) = delete; ~RelaySaveTimer() { - stop(); + _timer.stop(); } void schedule(Duration duration) { - stop(); - - os_timer_setfn(&_timer, timerCallback, this); - os_timer_arm(&_timer, duration.count(), 0); - _armed = true; + _timer.once( + duration, + [&]() { + _ready = true; + }); } void stop() { - if (_armed) { - os_timer_disarm(&_timer); - _timer = os_timer_t{}; - _done = false; - _armed = false; - _persist = false; - } + _timer.stop(); + _persist = false; } template void process(T&& callback) { - if (_done) { + if (_ready) { callback(_persist); _persist = false; - _done = false; + _ready = false; } } @@ -1135,23 +1097,14 @@ struct RelaySaveTimer { } private: - void done() { - _done = true; - _armed = false; - } - - static void timerCallback(void* arg) { - reinterpret_cast(arg)->done(); - } - bool _persist { false }; - bool _done { false }; - bool _armed { false }; - os_timer_t _timer; + bool _ready { false }; + Timer _timer; }; struct RelaySyncTimer { - using Duration = espurna::duration::Milliseconds; + using Timer = espurna::timer::SystemTimer; + using Duration = Timer::Duration; using Callback = void(*)(); RelaySyncTimer() = default; @@ -1167,43 +1120,30 @@ struct RelaySyncTimer { } void schedule(Duration duration, Callback callback) { - stop(); - - os_timer_setfn(&_timer, timerCallback, this); - os_timer_arm(&_timer, duration.count(), 0); + _timer.once( + duration, + [&]() { + _ready = true; + }); _callback = callback; - _armed = true; } void stop() { - if (_armed) { - os_timer_disarm(&_timer); - _timer = os_timer_t{}; - _done = false; - _armed = false; - } + _timer.stop(); + _ready = false; } void process() { - if (_done) { + if (_ready) { _callback(); - _done = false; + _ready = false; } } private: - void done() { - _done = true; - } - - static void timerCallback(void* arg) { - reinterpret_cast(arg)->done(); - } - Callback _callback { nullptr }; - bool _done { false }; - bool _armed { false }; - os_timer_t _timer; + bool _ready { false }; + Timer _timer; }; using Relays = std::vector; @@ -1382,13 +1322,7 @@ public: } void change(bool) override { - static bool scheduled { false }; - if (!scheduled) { - schedule_function([]() { - flush(); - scheduled = false; - }); - } + espurnaRegisterOnceUnique(flush); } size_t relayId() const { @@ -3023,15 +2957,11 @@ void relaySetup() { bool relayAdd(RelayProviderBasePtr&& provider) { if (provider && provider->setup()) { - static bool scheduled { false }; _relays.emplace_back(std::move(provider)); - if (!scheduled) { - schedule_function([]() { - _relayConfigure(); - _relayBootAll(); - scheduled = false; - }); - } + espurnaRegisterOnceUnique([]() { + _relayConfigure(); + _relayBootAll(); + }); return true; } diff --git a/code/espurna/rpc.cpp b/code/espurna/rpc.cpp index 723d8723..1cee4aea 100644 --- a/code/espurna/rpc.cpp +++ b/code/espurna/rpc.cpp @@ -6,21 +6,23 @@ Copyright (C) 2020 by Maxim Prokhorov */ +#include "espurna.h" #include "rpc.h" -#include #include #include "system.h" #include "utils.h" +static void rpcPrepareReset() { + prepareReset(CustomResetReason::Rpc); +} + bool rpcHandleAction(const String& action) { bool result = false; if (action.equals("reboot")) { result = true; - schedule_function([]() { - prepareReset(CustomResetReason::Rpc); - }); + espurnaRegisterOnce(rpcPrepareReset); } else if (action.equals("heartbeat")) { result = true; systemScheduleHeartbeat(); diff --git a/code/espurna/rpnrules.cpp b/code/espurna/rpnrules.cpp index 25b51762..1ad1eaf7 100644 --- a/code/espurna/rpnrules.cpp +++ b/code/espurna/rpnrules.cpp @@ -1093,7 +1093,7 @@ namespace system { void sleep(uint64_t duration, RFMode mode); void scheduleSleep(uint64_t duration, RFMode mode) { - schedule_function([duration, mode]() { + espurnaRegisterOnce([duration, mode]() { sleep(duration, mode); }); } diff --git a/code/espurna/system.cpp b/code/espurna/system.cpp index 58acf5d3..ee4559cd 100644 --- a/code/espurna/system.cpp +++ b/code/espurna/system.cpp @@ -8,8 +8,6 @@ Copyright (C) 2019 by Xose Pérez #include "espurna.h" -#include - #include "rtcmem.h" #include "ws.h" #include "ntp.h" @@ -327,6 +325,87 @@ void blockingDelay(CoreClock::duration timeout) { } // namespace time +namespace timer { + +constexpr SystemTimer::Duration SystemTimer::DurationMin; +constexpr SystemTimer::Duration SystemTimer::DurationMax; + +SystemTimer::SystemTimer() = default; + +void SystemTimer::start(Duration duration, Callback callback, bool repeat) { + stop(); + if (!duration.count()) { + return; + } + + if (!_timer) { + _timer.reset(new os_timer_t{}); + } + + _callback = std::move(callback); + _armed = _timer.get(); + _repeat = repeat; + + os_timer_setfn(_timer.get(), + [](void* arg) { + reinterpret_cast(arg)->callback(); + }, + this); + + size_t total = 0; + if (duration > DurationMax) { + total = 1; + while (duration > DurationMax) { + total *= 2; + duration /= 2; + } + _tick.reset(new Tick{ + .total = total, + .count = 0, + }); + repeat = true; + } + + os_timer_arm(_armed, duration.count(), repeat); +} + +void SystemTimer::stop() { + if (_armed) { + os_timer_disarm(_armed); + _armed = nullptr; + _callback = nullptr; + _tick = nullptr; + } +} + +void SystemTimer::callback() { + if (_tick) { + ++_tick->count; + if (_tick->count < _tick->total) { + return; + } + } + + _callback(); + + if (_repeat) { + if (_tick) { + _tick->count = 0; + } + return; + } + + stop(); +} + +void SystemTimer::schedule_once(Duration duration, Callback callback) { + once(duration, [callback = std::move(callback)]() { + espurnaRegisterOnce(callback); + }); +} + +} // namespace timer + namespace { namespace memory { @@ -514,7 +593,7 @@ namespace internal { Data persistent_data { &Rtcmem->sys }; -Ticker timer; +timer::SystemTimer timer; bool flag { true }; } // namespace internal @@ -574,6 +653,11 @@ uint8_t counter() { : build::ChecksMin; } +void reset() { + DEBUG_MSG_P(PSTR("[MAIN] Resetting stability counter\n")); + internal::persistent_data.counter(build::ChecksMin); +} + void init() { const auto count = counter(); @@ -601,10 +685,7 @@ void init() { internal::persistent_data.counter(next); internal::flag = (count < build::ChecksMax); - internal::timer.once_scheduled(build::CheckTime.count(), []() { - DEBUG_MSG_P(PSTR("[MAIN] Resetting stability counter\n")); - internal::persistent_data.counter(build::ChecksMin); - }); + internal::timer.once(build::CheckTime, reset); } bool check() { @@ -830,7 +911,7 @@ struct CallbackRunner { namespace internal { -Ticker timer; +timer::SystemTimer timer; std::vector runners; bool scheduled { false }; @@ -890,11 +971,12 @@ void run() { next = BeatMin; } - internal::timer.once_ms(next.count(), schedule); + internal::timer.once(next, schedule); } void stop(Callback callback) { - auto found = std::remove_if(internal::runners.begin(), internal::runners.end(), + auto found = std::remove_if( + internal::runners.begin(), internal::runners.end(), [&](const CallbackRunner& runner) { return callback == runner.callback; }); @@ -918,7 +1000,7 @@ void push(Callback callback, Mode mode, duration::Seconds interval) { offset - msec }); - internal::timer.detach(); + internal::timer.stop(); schedule(); } @@ -972,7 +1054,7 @@ void init() { pushOnce([](Mask) { if (!espurna::boot::stability::check()) { DEBUG_MSG_P(PSTR("[MAIN] System UNSTABLE\n")); - } else if (espurna::boot::internal::timer.active()) { + } else if (espurna::boot::internal::timer) { DEBUG_MSG_P(PSTR("[MAIN] Pending stability counter reset...\n")); } return true; @@ -1035,7 +1117,7 @@ void init() { // Store reset reason both here and in for the next boot namespace internal { -Ticker reset_timer; +timer::SystemTimer reset_timer; auto reset_reason = CustomResetReason::None; void reset(CustomResetReason reason) { @@ -1070,9 +1152,11 @@ static constexpr espurna::duration::Milliseconds ShortDelayForReset { 500 }; void deferredReset(duration::Milliseconds delay, CustomResetReason reason) { DEBUG_MSG_P(PSTR("[MAIN] Requested reset: %s\n"), espurna::boot::serialize(reason).c_str()); - internal::reset_timer.once_ms(delay.count(), [reason]() { - internal::reset(reason); - }); + internal::reset_timer.once( + delay, + [reason]() { + internal::reset(reason); + }); } // SDK reserves last 16KiB on the flash for it's own means diff --git a/code/espurna/system.h b/code/espurna/system.h index a4633e70..622792c2 100644 --- a/code/espurna/system.h +++ b/code/espurna/system.h @@ -14,6 +14,8 @@ Copyright (C) 2019 by Xose Pérez #include #include +#include + struct HeapStats { uint32_t available; uint32_t usable; @@ -199,6 +201,66 @@ void blockingDelay(CoreClock::duration timeout); } // namespace time +namespace timer { + +struct SystemTimer { + using TimeSource = time::CoreClock; + using Duration = TimeSource::duration; + + static constexpr Duration DurationMin = Duration(5); + + SystemTimer(); + ~SystemTimer() { + stop(); + } + + SystemTimer(const SystemTimer&) = delete; + SystemTimer& operator=(const SystemTimer&) = delete; + + SystemTimer(SystemTimer&&) = default; + SystemTimer& operator=(SystemTimer&&) = default; + + explicit operator bool() const { + return _armed != nullptr; + } + + void once(Duration duration, Callback callback) { + start(duration, std::move(callback), false); + } + + void repeat(Duration duration, Callback callback) { + start(duration, std::move(callback), true); + } + + void schedule_once(Duration, Callback); + void stop(); + +private: + // limit is per https://www.espressif.com/sites/default/files/documentation/2c-esp8266_non_os_sdk_api_reference_en.pdf + // > 3.1.1 os_timer_arm + // > with `system_timer_reinit()`, the timer value allowed ranges from 100 to 0x0x689D0. + // > otherwise, the timer value allowed ranges from 5 to 0x68D7A3. + // with current implementation we use division by 2 until we reach value less than this one + static constexpr Duration DurationMax = Duration(6870947); + + void start(Duration, Callback, bool repeat); + void callback(); + + struct Tick { + size_t total; + size_t count; + }; + + Callback _callback { nullptr }; + os_timer_t* _armed { nullptr }; + bool _repeat { false }; + + std::unique_ptr _tick; + std::unique_ptr _timer; +}; + +} // namespace timer + namespace heartbeat { using Mask = int32_t; diff --git a/code/espurna/terminal.cpp b/code/espurna/terminal.cpp index a9df3864..d95d1214 100644 --- a/code/espurna/terminal.cpp +++ b/code/espurna/terminal.cpp @@ -486,7 +486,7 @@ void setup() { auto cmd = std::make_shared(std::move(line)); - schedule_function([cmd]() { + espurnaRegisterOnce([cmd]() { PrintString out(TCP_MSS); api_find_and_call(*cmd, out); @@ -621,7 +621,7 @@ void onAction(uint32_t client_id, const char* action, JsonObject& data) { return; } - schedule_function([cmd, client_id]() { + espurnaRegisterOnce([cmd, client_id]() { PrintLine out(client_id); api_find_and_call(*cmd, out); }); diff --git a/code/espurna/types.cpp b/code/espurna/types.cpp new file mode 100644 index 00000000..327fbf55 --- /dev/null +++ b/code/espurna/types.cpp @@ -0,0 +1,117 @@ +/* + +Part of the SYSTEM MODULE + +Copyright (C) 2019-2021 by Maxim Prokhorov + +*/ + +#include "types.h" + +namespace espurna { + +void Callback::swap(Callback& other) noexcept { + if (_type == other._type) { + switch (_type) { + case StorageType::Empty: + break; + case StorageType::Simple: + std::swap(_storage.simple, other._storage.simple); + break; + case StorageType::Wrapper: + std::swap(_storage.wrapper, other._storage.wrapper); + break; + } + return; + } + + auto moved = std::move(*this); + *this = std::move(other); + other = std::move(moved); +} + +void Callback::operator()() const { + switch (_type) { + case StorageType::Empty: + break; + case StorageType::Simple: + (*_storage.simple)(); + break; + case StorageType::Wrapper: + _storage.wrapper(); + break; + } +} + +void Callback::copy(const Callback& other) { + _type = other._type; + + switch (other._type) { + case StorageType::Empty: + break; + case StorageType::Simple: + _storage.simple = other._storage.simple; + break; + case StorageType::Wrapper: + new (&_storage.wrapper) WrapperType( + other._storage.wrapper); + break; + } +} + +void Callback::move(Callback& other) noexcept { + _type = other._type; + + switch (other._type) { + case StorageType::Empty: + break; + case StorageType::Simple: + _storage.simple = other._storage.simple; + break; + case StorageType::Wrapper: + new (&_storage.wrapper) WrapperType( + std::move(other._storage.wrapper)); + break; + } + + other._storage.simple = nullptr; + other._type = StorageType::Empty; +} + +void Callback::reset() { + switch (_type) { + case StorageType::Empty: + case StorageType::Simple: + break; + case StorageType::Wrapper: + _storage.wrapper.~WrapperType(); + break; + } + + _storage.simple = nullptr; + _type = StorageType::Empty; +} + +Callback& Callback::operator=(Callback&& other) noexcept { + reset(); + move(other); + return *this; +} + +bool StringView::equals(StringView other) const { + if (other._len == _len) { + if (inFlash(_ptr) && inFlash(other._ptr)) { + return _ptr == other._ptr; + } else if (inFlash(_ptr)) { + return memcmp_P(other._ptr, _ptr, _len) == 0; + } else if (inFlash(other._ptr)) { + return memcmp_P(_ptr, other._ptr, _len) == 0; + } + + return __builtin_memcmp(_ptr, other._ptr, _len) == 0; + } + + return false; +} + +} // namespace espurna diff --git a/code/espurna/types.h b/code/espurna/types.h index 45e7cf82..8788dc5b 100644 --- a/code/espurna/types.h +++ b/code/espurna/types.h @@ -10,11 +10,146 @@ Copyright (C) 2019-2021 by Maxim Prokhorov #include +#include + // missing in our original header extern "C" int memcmp_P(const void*, const void*, size_t); namespace espurna { +// base class for loop / oneshot / generic callbacks that do not need arguments +// *not expected* to be used instead of std function at all times. +// main purpose of this special class is to circumvent the need for rtti in +// our gcc stl implementation and retrieve the 'target function' pointer +// (*should* be different in gcc 11 / 12 though, target() became constexpr) +struct Callback { + using Type = void (*)(); + using WrapperType = std::function; + + Callback() = default; + + Callback(const Callback& other) : + _storage(nullptr), + _type(other._type) + { + copy(other); + } + + Callback& operator=(const Callback& other) { + reset(); + copy(other); + return *this; + } + + Callback(const Callback&&) = delete; + Callback(Callback&& other) noexcept : + _storage(nullptr), + _type(other._type) + { + move(other); + } + + Callback& operator=(Callback&& other) noexcept; + + template + using remove_cvref = typename std::remove_cv>::type; + + template + using is_callback = std::is_same, Callback>; + + template + using is_type = std::is_same; + + template + using type_convertible = std::is_convertible; + + template + using wrapper_convertible = std::is_convertible; + + // when T *can* be converted into Callback::Type + // usually, function pointer *or* lambda without capture list + template ::value + || type_convertible::value>::type> + constexpr Callback(T callback) noexcept : + _storage(Type(callback)), + _type(StorageType::Simple) + {} + + // anything else convertible into std function + template ::value>::type, + typename = typename std::enable_if< + wrapper_convertible::value>::type, + typename = typename std::enable_if< + !type_convertible::value>::type> + Callback(T callback) : + _storage(WrapperType(std::move(callback))), + _type(StorageType::Wrapper) + { + static_assert(!is_callback::value, ""); + } + + ~Callback() { + reset(); + } + + bool isEmpty() const { + return (_type == StorageType::Empty); + } + + bool isSimple() const { + return (_type == StorageType::Simple); + } + + bool isWrapped() const { + return (_type == StorageType::Wrapper); + } + + bool operator==(Type callback) const { + return isSimple() && (_storage.simple == callback); + } + + void swap(Callback&) noexcept; + void operator()() const; + +private: + union Storage { + WrapperType wrapper; + Type simple; + + ~Storage() { + } + + explicit Storage(WrapperType callback) : + wrapper(std::move(callback)) + {} + + constexpr explicit Storage(Type callback) : + simple(callback) + {} + + constexpr explicit Storage(std::nullptr_t) : + simple(nullptr) + {} + }; + + enum class StorageType { + Empty, + Simple, + Wrapper, + }; + + void copy(const Callback&); + void move(Callback&) noexcept; + void reset(); + + Storage _storage { nullptr }; + StorageType _type { StorageType::Empty }; +}; + // aka `std::source_location` struct SourceLocation { int line; @@ -73,7 +208,6 @@ private: bool& _handle; }; - struct StringView { StringView() = delete; ~StringView() = default; @@ -164,6 +298,11 @@ struct StringView { bool equals(StringView other) const; private: +#if defined(HOST_MOCK) + constexpr static bool inFlash(const char*) { + return false; + } +#else static bool inFlash(const char* ptr) { // common comparison would use >=0x40000000 // instead, slightly reduce the footprint by @@ -171,6 +310,7 @@ private: static constexpr uintptr_t Mask { 1 << 30 }; return (reinterpret_cast(ptr) & Mask) > 0; } +#endif const char* _ptr; size_t _len; diff --git a/code/espurna/web_asyncwebprint.ipp b/code/espurna/web_asyncwebprint.ipp index c667f5b2..b7f1ef49 100644 --- a/code/espurna/web_asyncwebprint.ipp +++ b/code/espurna/web_asyncwebprint.ipp @@ -11,8 +11,6 @@ Copyright (C) 2016-2019 by Xose Pérez #include "web.h" #include "libs/TypeChecks.h" -#include - #if WEB_SUPPORT namespace asyncwebprint { @@ -48,7 +46,7 @@ void AsyncWebPrint::scheduleFromRequest(AsyncWebPrintConfig config, AsyncWebServ }); // attach another capture to the scheduled function, so we execute as soon as we exit next loop() - schedule_function([callback, print]() { + espurnaRegisterOnce([callback, print]() { if (State::None != print->getState()) return; callback(*print.get()); print->flush(); diff --git a/code/espurna/wifi.cpp b/code/espurna/wifi.cpp index 33fdad72..d1f4a8ea 100644 --- a/code/espurna/wifi.cpp +++ b/code/espurna/wifi.cpp @@ -1107,7 +1107,7 @@ espurna::duration::Milliseconds interval() { namespace internal { -Ticker timer; +timer::SystemTimer timer; bool wait { false }; } // namespace internal @@ -1141,13 +1141,15 @@ bool wait() { } void stop() { - internal::timer.detach(); + internal::timer.stop(); +} + +void reset() { + internal::wait = false; } void start(espurna::duration::Milliseconds next) { - internal::timer.attach_ms(next.count(), []() { - internal::wait = false; - }); + internal::timer.repeat(next, reset); } } // namespace garp @@ -1503,7 +1505,7 @@ wifi::Networks preparedNetworks; bool connected { false }; bool wait { false }; -Ticker timer; +timer::SystemTimer timer; bool persist { false }; using TaskPtr = std::unique_ptr; @@ -1521,7 +1523,7 @@ bool persist() { void stop() { internal::task.reset(); - internal::timer.detach(); + internal::timer.stop(); } bool start(String&& hostname) { @@ -1530,7 +1532,7 @@ bool start(String&& hostname) { std::move(hostname), std::move(internal::preparedNetworks), build::ConnectionRetries); - internal::timer.detach(); + internal::timer.stop(); return true; } @@ -1539,7 +1541,7 @@ bool start(String&& hostname) { } void schedule(espurna::duration::Milliseconds next, internal::ActionPtr ptr) { - internal::timer.once_ms(next.count(), ptr); + internal::timer.once(next, ptr); DEBUG_MSG_P(PSTR("[WIFI] Next connection attempt in %u (ms)\n"), next.count()); } @@ -1725,7 +1727,7 @@ namespace internal { int8_t threshold { build::threshold() }; int8_t counter { build::Checks }; -Ticker timer; +timer::SystemTimer timer; void task() { if (!wifi::sta::connected()) { @@ -1749,12 +1751,12 @@ void task() { void start() { counter = build::Checks; - timer.attach_ms(build::Interval.count(), task); + timer.repeat(build::Interval, task); } void stop() { counter = build::Checks; - timer.detach(); + timer.stop(); } } // namespace internal @@ -2091,7 +2093,7 @@ namespace internal { auto timeout = build::Timeout; bool enabled { false }; -Ticker timer; +timer::SystemTimer timer; } // namespace internal @@ -2108,13 +2110,13 @@ bool enabled() { } void remove() { - internal::timer.detach(); + internal::timer.stop(); } void check(); void schedule() { - internal::timer.once_ms(internal::timeout.count(), check); + internal::timer.once(internal::timeout, check); } void check() { @@ -2342,7 +2344,7 @@ void wifi(::terminal::CommandContext&& ctx) { alignas(4) static constexpr char Reset[] PROGMEM = "WIFI.RESET"; void reset(::terminal::CommandContext&& ctx) { - wifiDisconnect(); + wifi::sta::disconnect(); wifi::settings::configure(); terminalOK(ctx); } diff --git a/code/espurna/wifi.h b/code/espurna/wifi.h index c1fd7d6f..4de43e64 100644 --- a/code/espurna/wifi.h +++ b/code/espurna/wifi.h @@ -21,7 +21,6 @@ Copyright (C) 2016-2019 by Xose Pérez // esp8266 re-defines enum values from tcp header... include them first #define LWIP_INTERNAL #include -#include #undef LWIP_INTERNAL extern "C" { @@ -94,8 +93,7 @@ void wifiStartAp(); void wifiToggleAp(); void wifiToggleSta(); -// Disconnects STA intefrace -// (and will immediatly trigger a reconnection) +// Disconnects STA intefrace, will trigger reconnection void wifiDisconnect(); // Toggle WiFi modem diff --git a/code/espurna/ws.cpp b/code/espurna/ws.cpp index 6a530d1b..274087fa 100644 --- a/code/espurna/ws.cpp +++ b/code/espurna/ws.cpp @@ -563,11 +563,7 @@ void _wsParse(AsyncWebSocketClient *client, uint8_t * payload, size_t length) { } if (strcmp(action, "reconnect") == 0) { - static Ticker timer; - timer.once_ms_scheduled(100, []() { - wifiDisconnect(); - yield(); - }); + wifiDisconnect(); return; } diff --git a/code/espurna/ws.h b/code/espurna/ws.h index be0be983..6405c8f3 100644 --- a/code/espurna/ws.h +++ b/code/espurna/ws.h @@ -20,7 +20,7 @@ Copyright (C) 2019 by Maxim Prokhorov #include "ws_utils.h" // Generalized WS lifetime callbacks. -// Each callback is kept as std::function, thus we can use complex objects, and not just basic function pointers. +// Callback could either be a lambda or a plain function pointer // // Connection start: // - on_visible will be the very first message sent, callback data will be grouped together diff --git a/code/test/unit/CMakeLists.txt b/code/test/unit/CMakeLists.txt index 7ea5abdc..4c886a80 100644 --- a/code/test/unit/CMakeLists.txt +++ b/code/test/unit/CMakeLists.txt @@ -149,10 +149,11 @@ target_link_libraries(esp8266 PUBLIC common) add_library(terminal STATIC ${ESPURNA_PATH}/code/espurna/terminal_commands.cpp ${ESPURNA_PATH}/code/espurna/terminal_parsing.cpp + ${ESPURNA_PATH}/code/espurna/types.cpp ) target_link_libraries(terminal PUBLIC esp8266) target_include_directories(terminal PUBLIC - ${ESPURNA_PATH}/code/espurna/ + ${ESPURNA_PATH}/code/ ) target_compile_options(terminal PUBLIC ${COMMON_FLAGS} @@ -180,4 +181,4 @@ function(build_tests) endforeach() endfunction() -build_tests(basic settings terminal tuya url) +build_tests(basic settings terminal tuya types url) diff --git a/code/test/unit/src/settings/settings.cpp b/code/test/unit/src/settings/settings.cpp index 1f35fee8..aaf81783 100644 --- a/code/test/unit/src/settings/settings.cpp +++ b/code/test/unit/src/settings/settings.cpp @@ -7,7 +7,7 @@ #pragma GCC diagnostic warning "-Wpointer-arith" #pragma GCC diagnostic warning "-Wstrict-overflow=5" -#include +#include #include #include diff --git a/code/test/unit/src/terminal/terminal.cpp b/code/test/unit/src/terminal/terminal.cpp index 1e741e0f..8692292f 100644 --- a/code/test/unit/src/terminal/terminal.cpp +++ b/code/test/unit/src/terminal/terminal.cpp @@ -2,17 +2,10 @@ #include #include -#include -#include +#include +#include namespace espurna { - -// no special cases for flash strings -bool StringView::equals(espurna::StringView other) const { - return _ptr == other._ptr - || (_len == other._len && (0 == __builtin_memcmp(_ptr, other._ptr, _len))); -} - namespace terminal { namespace test { namespace { diff --git a/code/test/unit/src/tuya/tuya.cpp b/code/test/unit/src/tuya/tuya.cpp index 5248cf41..0646d189 100644 --- a/code/test/unit/src/tuya/tuya.cpp +++ b/code/test/unit/src/tuya/tuya.cpp @@ -9,12 +9,12 @@ #include #include -#include "libs/TypeChecks.h" -#include "tuya_types.h" -#include "tuya_util.h" -#include "tuya_transport.h" -#include "tuya_protocol.h" -#include "tuya_dataframe.h" +#include +#include +#include +#include +#include +#include using namespace tuya; diff --git a/code/test/unit/src/types/types.cpp b/code/test/unit/src/types/types.cpp new file mode 100644 index 00000000..7bdbb03b --- /dev/null +++ b/code/test/unit/src/types/types.cpp @@ -0,0 +1,218 @@ +#include +#include + +#include + +namespace espurna { +namespace test { +namespace { + +void test_view() { + StringView view{"123456789"}; + TEST_ASSERT_EQUAL(9, view.length()); + TEST_ASSERT(view.c_str() != nullptr); + + const char expected[] = "123456789"; + TEST_ASSERT_EQUAL(__builtin_strlen(expected), view.length()); + TEST_ASSERT_EQUAL_CHAR_ARRAY( + expected, view.begin(), view.length()); +} + +void test_view_convert() { + const String origin("12345"); + StringView view(origin); + + TEST_ASSERT(origin.begin() == view.begin()); + TEST_ASSERT(origin.end() == view.end()); + TEST_ASSERT_EQUAL(origin.length(), view.length()); + + auto copy = view.toString(); + TEST_ASSERT(view.equals(copy)); + + TEST_ASSERT(origin.begin() != copy.begin()); + TEST_ASSERT_EQUAL(origin.length(), copy.length()); + TEST_ASSERT_EQUAL_CHAR_ARRAY( + origin.begin(), copy.begin(), copy.length()); + + + StringView copy_view(copy); + TEST_ASSERT_EQUAL(view.length(), copy_view.length()); + TEST_ASSERT(view.equals(copy_view)); +} + +void test_callback_empty() { + Callback callback; + TEST_ASSERT(callback.isEmpty()); +} + +void test_callback_lambda() { + static int once = 0; + Callback callback([]() { + ++once; + }); + + TEST_ASSERT(callback.isSimple()); + + callback(); + TEST_ASSERT_EQUAL(1, once); +} + +int external { 0 }; + +void increment_external() { + ++external; + ++external; +} + +void test_callback_simple() { + TEST_ASSERT_EQUAL(0, external); + + Callback callback(increment_external); + TEST_ASSERT(callback.isSimple()); + + callback(); + TEST_ASSERT_EQUAL(2, external); +} + +void test_callback_capture() { + int value = 0; + + std::vector callbacks; + callbacks.resize(3, [&]() { + ++value; + }); + + TEST_ASSERT_EQUAL(3, callbacks.size()); + TEST_ASSERT(callbacks[0].isWrapped()); + TEST_ASSERT(callbacks[1].isWrapped()); + TEST_ASSERT(callbacks[2].isWrapped()); + + for (const auto& callback : callbacks) { + callback(); + } + TEST_ASSERT_EQUAL(3, value); +} + +void test_callback_capture_copy() { + int value = 0; + Callback original([&]() { + ++value; + ++value; + ++value; + ++value; + }); + + TEST_ASSERT(original.isWrapped()); + + auto copy = original; + TEST_ASSERT(original.isWrapped()); + TEST_ASSERT(copy.isWrapped()); + + original(); + TEST_ASSERT_EQUAL(4, value); + + copy(); + TEST_ASSERT_EQUAL(8, value); +} + +void test_callback_capture_move() { + int value = 0; + Callback original([&]() { + ++value; + ++value; + ++value; + ++value; + }); + + TEST_ASSERT(original.isWrapped()); + + Callback moved(std::move(original)); + TEST_ASSERT(original.isEmpty()); + TEST_ASSERT(moved.isWrapped()); + + original(); + TEST_ASSERT_EQUAL(0, value); + + moved(); + TEST_ASSERT_EQUAL(4, value); +} + +void test_callback_assign() { + int value = 0; + Callback original([&]() { + value += 10; + }); + + TEST_ASSERT(original.isWrapped()); + + Callback copy; + TEST_ASSERT(copy.isEmpty()); + + copy = original; + TEST_ASSERT(copy.isWrapped()); + + original(); + TEST_ASSERT_EQUAL(10, value); + + copy(); + TEST_ASSERT_EQUAL(20, value); + + Callback moved; + TEST_ASSERT(moved.isEmpty()); + + moved = std::move(original); + TEST_ASSERT(original.isEmpty()); + TEST_ASSERT(moved.isWrapped()); + + original(); + TEST_ASSERT_EQUAL(20, value); + + copy(); + TEST_ASSERT_EQUAL(30, value); + + moved(); + TEST_ASSERT_EQUAL(40, value); +} + +void test_callback_swap() { + int output = 0; + + Callback good([&]() { + output = 5; + }); + + Callback bad([&]() { + output = 10; + }); + + good.swap(bad); + TEST_ASSERT_EQUAL(0, output); + + bad(); + TEST_ASSERT_EQUAL(5, output); + + good(); + TEST_ASSERT_EQUAL(10, output); +} + +} // namespace +} // namespace test +} // namespace espurna + +int main(int, char**) { + UNITY_BEGIN(); + + using namespace espurna::test; + RUN_TEST(test_view); + RUN_TEST(test_view_convert); + RUN_TEST(test_callback_empty); + RUN_TEST(test_callback_simple); + RUN_TEST(test_callback_lambda); + RUN_TEST(test_callback_capture); + RUN_TEST(test_callback_capture_copy); + RUN_TEST(test_callback_capture_move); + RUN_TEST(test_callback_assign); + RUN_TEST(test_callback_swap); + + return UNITY_END(); +} diff --git a/code/test/unit/src/url/url.cpp b/code/test/unit/src/url/url.cpp index 43fa6511..851c341f 100644 --- a/code/test/unit/src/url/url.cpp +++ b/code/test/unit/src/url/url.cpp @@ -1,7 +1,7 @@ #include #include -#include "libs/URL.h" +#include void test_parse() { URL url("http://api.thingspeak.com/update");