Browse Source

system: fixup timers and loop callbacks

* globally accessible system timer class; help out with our internal
  scheduling by always using strongly typed duration and dynamic
  time adjustment for durations longer than system limits
  (see `os_timer_t` documentation)
* our own class for scheduled callbacks which are either choosing a
  simple function pointer or std function wrapper, depending on the type
  passed into the constructor. specifically for scheduled functions,
  this allows us to filter globally scheduled functions and push them to
  the front of the queue when necessary to ensure certain order of calls
* replace Ticker instances with SystemTimer
* allow types {h, cpp} in unit tests
pull/2552/head
Maxim Prokhorov 1 year ago
parent
commit
510d68d079
29 changed files with 954 additions and 292 deletions
  1. +1
    -1
      code/espurna/debug.cpp
  2. +3
    -0
      code/espurna/espurna.h
  3. +7
    -7
      code/espurna/homeassistant.cpp
  4. +87
    -26
      code/espurna/light.cpp
  5. +1
    -2
      code/espurna/light.h
  6. +82
    -27
      code/espurna/main.cpp
  7. +7
    -7
      code/espurna/mqtt.cpp
  8. +18
    -25
      code/espurna/ntp.cpp
  9. +1
    -1
      code/espurna/ota_asynctcp.cpp
  10. +19
    -9
      code/espurna/ota_httpupdate.cpp
  11. +46
    -116
      code/espurna/relay.cpp
  12. +6
    -4
      code/espurna/rpc.cpp
  13. +1
    -1
      code/espurna/rpnrules.cpp
  14. +100
    -16
      code/espurna/system.cpp
  15. +62
    -0
      code/espurna/system.h
  16. +2
    -2
      code/espurna/terminal.cpp
  17. +117
    -0
      code/espurna/types.cpp
  18. +141
    -1
      code/espurna/types.h
  19. +1
    -3
      code/espurna/web_asyncwebprint.ipp
  20. +18
    -16
      code/espurna/wifi.cpp
  21. +1
    -3
      code/espurna/wifi.h
  22. +1
    -5
      code/espurna/ws.cpp
  23. +1
    -1
      code/espurna/ws.h
  24. +3
    -2
      code/test/unit/CMakeLists.txt
  25. +1
    -1
      code/test/unit/src/settings/settings.cpp
  26. +2
    -9
      code/test/unit/src/terminal/terminal.cpp
  27. +6
    -6
      code/test/unit/src/tuya/tuya.cpp
  28. +218
    -0
      code/test/unit/src/types/types.cpp
  29. +1
    -1
      code/test/unit/src/url/url.cpp

+ 1
- 1
code/espurna/debug.cpp View File

@ -191,7 +191,7 @@ void enable() {
void delayedEnable() {
disable();
schedule_function(enable);
::espurnaRegisterOnce(enable);
}
void send(const char* message, size_t len, Timestamp);


+ 3
- 0
code/espurna/espurna.h View File

@ -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);


+ 7
- 7
code/espurna/homeassistant.cpp View File

@ -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;
}


+ 87
- 26
code/espurna/light.cpp View File

@ -18,8 +18,6 @@ Copyright (C) 2019-2021 by Maxim Prokhorov <prokhorov dot max at outlook dot com
#include "rtcmem.h"
#include "ws.h"
#include <Ticker.h>
#include <Schedule.h>
#include <ArduinoJson.h>
#include <array>
@ -638,13 +636,52 @@ void _lightUpdateMapping(T& channels) {
}
}
template <typename T>
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<bool> _light_save_timer(false);
auto _light_report_delay = espurna::light::build::reportDelay();
Ticker _light_report_ticker;
std::forward_list<LightReportListener> _light_report;
LightTimerValue<int> _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<LightTransitionHandler> _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();
});
}


+ 1
- 2
code/espurna/light.h View File

@ -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<void()>;
using LightSequenceCallbacks = std::forward_list<LightSequenceCallback>;
using LightSequenceCallbacks = std::forward_list<espurna::Callback>;
void lightSequence(LightSequenceCallbacks);
void lightUpdateSequence(LightTransition);


+ 82
- 27
code/espurna/main.cpp View File

@ -23,6 +23,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#include "espurna.h"
#include "main.h"
#include <algorithm>
#include <utility>
// -----------------------------------------------------------------------------
// 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<LoopCallback> reload_callbacks;
bool reload_flag { false };
std::vector<LoopCallback> loop_callbacks;
espurna::duration::Milliseconds loop_delay { build::LoopDelayMin };
std::vector<LoopCallback> reload_callbacks;
bool reload_flag { false };
std::forward_list<Callback> 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() {


+ 7
- 7
code/espurna/mqtt.cpp View File

@ -13,7 +13,6 @@ Updated secure client support by Niek van der Maas < mail at niekvandermaas dot
#include <forward_list>
#include <utility>
#include <Ticker.h>
#include "system.h"
#include "mdns.h"
@ -537,7 +536,7 @@ private:
size_t _mqtt_json_payload_count { 0ul };
std::forward_list<MqttPayload> _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;
}


+ 18
- 25
code/espurna/ntp.cpp View File

@ -17,7 +17,6 @@ Copyright (C) 2019 by Maxim Prokhorov <prokhorov dot max at outlook dot com>
#include <Arduino.h>
#include <coredecls.h>
#include <Ticker.h>
#include <ctime>
#include <errno.h>
@ -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<NtpTickCallback>;
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;
}


+ 1
- 1
code/espurna/ota_asynctcp.cpp View File

@ -104,7 +104,7 @@ void disconnect() {
void onDisconnect(void* arg, AsyncClient*) {
DEBUG_MSG_P(PSTR("\n"));
otaFinalize(reinterpret_cast<BasicHttpClient*>(arg)->size, CustomResetReason::Ota, true);
schedule_function(internal::disconnect);
espurnaRegisterOnce(internal::disconnect);
}
void onTimeout(void*, AsyncClient* client, uint32_t) {


+ 19
- 9
code/espurna/ota_httpupdate.cpp View File

@ -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;


+ 46
- 116
code/espurna/relay.cpp View File

@ -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<bool>(_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<Timer*>(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<Timer> 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 <typename T>
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<RelaySaveTimer*>(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<RelaySyncTimer*>(arg)->done();
}
Callback _callback { nullptr };
bool _done { false };
bool _armed { false };
os_timer_t _timer;
bool _ready { false };
Timer _timer;
};
using Relays = std::vector<Relay>;
@ -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;
}


+ 6
- 4
code/espurna/rpc.cpp View File

@ -6,21 +6,23 @@ Copyright (C) 2020 by Maxim Prokhorov <prokhorov dot max at outlook dot com>
*/
#include "espurna.h"
#include "rpc.h"
#include <Schedule.h>
#include <cstring>
#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();


+ 1
- 1
code/espurna/rpnrules.cpp View File

@ -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);
});
}


+ 100
- 16
code/espurna/system.cpp View File

@ -8,8 +8,6 @@ Copyright (C) 2019 by Xose Pérez <xose dot perez at gmail dot com>
#include "espurna.h"
#include <Ticker.h>
#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<SystemTimer*>(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<CallbackRunner> 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


+ 62
- 0
code/espurna/system.h View File

@ -14,6 +14,8 @@ Copyright (C) 2019 by Xose Pérez <xose dot perez at gmail dot com>
#include <cstdint>
#include <limits>
#include <user_interface.h>
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> _tick;
std::unique_ptr<os_timer_t> _timer;
};
} // namespace timer
namespace heartbeat {
using Mask = int32_t;


+ 2
- 2
code/espurna/terminal.cpp View File

@ -486,7 +486,7 @@ void setup() {
auto cmd = std::make_shared<String>(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<Output> out(client_id);
api_find_and_call(*cmd, out);
});


+ 117
- 0
code/espurna/types.cpp View File

@ -0,0 +1,117 @@
/*
Part of the SYSTEM MODULE
Copyright (C) 2019-2021 by Maxim Prokhorov <prokhorov dot max at outlook dot com>
*/
#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

+ 141
- 1
code/espurna/types.h View File

@ -10,11 +10,146 @@ Copyright (C) 2019-2021 by Maxim Prokhorov <prokhorov dot max at outlook dot com
#include <Arduino.h>
#include <sys/pgmspace.h>
#include <memory>
// 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<void()>;
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 <typename T>
using remove_cvref = typename std::remove_cv<std::remove_reference<T>>::type;
template <typename T>
using is_callback = std::is_same<remove_cvref<T>, Callback>;
template <typename T>
using is_type = std::is_same<T, Type>;
template <typename T>
using type_convertible = std::is_convertible<T, Type>;
template <typename T>
using wrapper_convertible = std::is_convertible<T, WrapperType>;
// when T *can* be converted into Callback::Type
// usually, function pointer *or* lambda without capture list
template <typename T,
typename = typename std::enable_if<
is_type<T>::value
|| type_convertible<T>::value>::type>
constexpr Callback(T callback) noexcept :
_storage(Type(callback)),
_type(StorageType::Simple)
{}
// anything else convertible into std function
template <typename T,
typename = typename std::enable_if<
!is_callback<T>::value>::type,
typename = typename std::enable_if<
wrapper_convertible<T>::value>::type,
typename = typename std::enable_if<
!type_convertible<T>::value>::type>
Callback(T callback) :
_storage(WrapperType(std::move(callback))),
_type(StorageType::Wrapper)
{
static_assert(!is_callback<T>::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<uintptr_t>(ptr) & Mask) > 0;
}
#endif
const char* _ptr;
size_t _len;


+ 1
- 3
code/espurna/web_asyncwebprint.ipp View File

@ -11,8 +11,6 @@ Copyright (C) 2016-2019 by Xose Pérez <xose dot perez at gmail dot com>
#include "web.h"
#include "libs/TypeChecks.h"
#include <Schedule.h>
#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();


+ 18
- 16
code/espurna/wifi.cpp View File

@ -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<Task>;
@ -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);
}


+ 1
- 3
code/espurna/wifi.h View File

@ -21,7 +21,6 @@ Copyright (C) 2016-2019 by Xose Pérez <xose dot perez at gmail dot com>
// esp8266 re-defines enum values from tcp header... include them first
#define LWIP_INTERNAL
#include <ESP8266WiFi.h>
#include <Ticker.h>
#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


+ 1
- 5
code/espurna/ws.cpp View File

@ -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;
}


+ 1
- 1
code/espurna/ws.h View File

@ -20,7 +20,7 @@ Copyright (C) 2019 by Maxim Prokhorov <prokhorov dot max at outlook dot com>
#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


+ 3
- 2
code/test/unit/CMakeLists.txt View File

@ -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)

+ 1
- 1
code/test/unit/src/settings/settings.cpp View File

@ -7,7 +7,7 @@
#pragma GCC diagnostic warning "-Wpointer-arith"
#pragma GCC diagnostic warning "-Wstrict-overflow=5"
#include <settings_embedis.h>
#include <espurna/settings_embedis.h>
#include <array>
#include <algorithm>


+ 2
- 9
code/test/unit/src/terminal/terminal.cpp View File

@ -2,17 +2,10 @@
#include <Arduino.h>
#include <StreamString.h>
#include <libs/PrintString.h>
#include <terminal_commands.h>
#include <espurna/libs/PrintString.h>
#include <espurna/terminal_commands.h>
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 {


+ 6
- 6
code/test/unit/src/tuya/tuya.cpp View File

@ -9,12 +9,12 @@
#include <type_traits>
#include <queue>
#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 <espurna/libs/TypeChecks.h>
#include <espurna/tuya_types.h>
#include <espurna/tuya_util.h>
#include <espurna/tuya_transport.h>
#include <espurna/tuya_protocol.h>
#include <espurna/tuya_dataframe.h>
using namespace tuya;


+ 218
- 0
code/test/unit/src/types/types.cpp View File

@ -0,0 +1,218 @@
#include <unity.h>
#include <Arduino.h>
#include <espurna/types.h>
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<Callback> 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();
}

+ 1
- 1
code/test/unit/src/url/url.cpp View File

@ -1,7 +1,7 @@
#include <Arduino.h>
#include <unity.h>
#include "libs/URL.h"
#include <espurna/libs/URL.h>
void test_parse() {
URL url("http://api.thingspeak.com/update");


Loading…
Cancel
Save