From 80709be755e14c814c99765fbb3e889ca471b09a Mon Sep 17 00:00:00 2001 From: Maxim Prokhorov Date: Mon, 8 Apr 2024 00:02:42 +0300 Subject: [PATCH 1/3] Mock - timers and core scheduling Spawn a second thread for setup() & loop() Implement Core task switching routines Implement basic support for timers (tnx to Sming code for the idea of keeping timestamps in ETSTimer struct) --- cores/esp8266/Schedule.cpp | 6 +- libraries/Ticker/src/Ticker.cpp | 2 +- tests/host/Makefile | 7 +- tests/host/README.txt | 4 +- tests/host/common/Arduino.cpp | 47 +++--- tests/host/common/ArduinoCatch.cpp | 23 +++ tests/host/common/ArduinoMain.cpp | 98 +++++++---- tests/host/common/MockEsp.cpp | 3 +- tests/host/common/MockTask.cpp | 195 +++++++++++++++++++++ tests/host/common/MockTimer.cpp | 262 +++++++++++++++++++++++++++++ tests/host/common/mock.h | 18 ++ tests/restyle.sh | 2 +- tools/sdk/include/ets_sys.h | 2 +- 13 files changed, 600 insertions(+), 69 deletions(-) create mode 100644 tests/host/common/MockTask.cpp create mode 100644 tests/host/common/MockTimer.cpp diff --git a/cores/esp8266/Schedule.cpp b/cores/esp8266/Schedule.cpp index f6c650fcf9..a3138f39da 100644 --- a/cores/esp8266/Schedule.cpp +++ b/cores/esp8266/Schedule.cpp @@ -270,11 +270,9 @@ void run_scheduled_recurrent_functions() if (yieldNow) { - // because scheduled functions might last too long for watchdog etc, - // this is yield() in cont stack, but need to call cont_suspend directly - // to prevent recursion into run_scheduled_recurrent_functions() + // because scheduled functions might last too long for watchdog etc. esp_schedule(); - cont_suspend(g_pcont); + esp_suspend(); } } while (current && !done); diff --git a/libraries/Ticker/src/Ticker.cpp b/libraries/Ticker/src/Ticker.cpp index 8c45ffed8b..8dfb462527 100644 --- a/libraries/Ticker/src/Ticker.cpp +++ b/libraries/Ticker/src/Ticker.cpp @@ -74,7 +74,7 @@ void Ticker::_attach(Ticker::Milliseconds milliseconds, bool repeat) // whenever duration excedes this limit, make timer repeatable N times // in case it is really repeatable, it will reset itself and continue as usual - size_t total = 0; + uint32_t total = 0; if (milliseconds > DurationMax) { total = 1; while (milliseconds > DurationMax) { diff --git a/tests/host/Makefile b/tests/host/Makefile index f819047891..e0e15ba0fe 100644 --- a/tests/host/Makefile +++ b/tests/host/Makefile @@ -111,6 +111,7 @@ CORE_CPP_FILES := \ ) \ $(abspath $(LIBRARIES_PATH)/SDFS/src/SDFS.cpp) \ $(abspath $(LIBRARIES_PATH)/SD/src/SD.cpp) \ + $(abspath $(LIBRARIES_PATH)/Ticker/src/Ticker.cpp) \ CORE_C_FILES := \ $(addprefix $(abspath $(CORE_PATH))/,\ @@ -127,6 +128,7 @@ MOCK_CPP_FILES_COMMON := \ sdfs_mock.cpp \ WMath.cpp \ MockUART.cpp \ + MockTimer.cpp \ MockTools.cpp \ MocklwIP.cpp \ HostWiring.cpp \ @@ -139,6 +141,7 @@ MOCK_CPP_FILES := $(MOCK_CPP_FILES_COMMON) \ MOCK_CPP_FILES_EMU := $(MOCK_CPP_FILES_COMMON) \ $(addprefix $(HOST_COMMON_ABSPATH)/,\ + MockTask.cpp \ ArduinoMain.cpp \ ArduinoMainUdp.cpp \ ArduinoMainSpiffs.cpp \ @@ -184,7 +187,7 @@ PREINCLUDES := \ -include $(common)/c_types.h \ ifneq ($(D),) -OPTZ=-O0 +OPTZ=-Og DEBUG += -DDEBUG_ESP_PORT=Serial DEBUG += -DDEBUG_ESP_SSL -DDEBUG_ESP_TLS_MEM -DDEBUG_ESP_HTTP_CLIENT -DDEBUG_ESP_HTTP_SERVER -DDEBUG_ESP_CORE -DDEBUG_ESP_WIFI -DDEBUG_ESP_HTTP_UPDATE -DDEBUG_ESP_UPDATER -DDEBUG_ESP_OTA -DDEBUG_ESP_MDNS endif @@ -226,7 +229,7 @@ all: help .PHONY: CI CI: # run CI - $(MAKE) -f $(MAKEFILE) MKFLAGS="-Werror --coverage" LDFLAGS="--coverage" FORCE32=0 OPTZ=-O0 doCI + $(MAKE) -f $(MAKEFILE) MKFLAGS="-Werror --coverage" LDFLAGS="--coverage" FORCE32=0 OPTZ=-Og doCI .PHONY: doCI doCI: build-info $(OUTPUT_BINARY) valgrind test gcov diff --git a/tests/host/README.txt b/tests/host/README.txt index feb6611d52..a956acee08 100644 --- a/tests/host/README.txt +++ b/tests/host/README.txt @@ -2,7 +2,7 @@ Host Tests for Continuous Integration ------------------------------------- - make FORCE32=0 OPTZ=-O0 CI + make FORCE32=0 OPTZ=-Og CI (FORCE32=0: https://bugs.launchpad.net/ubuntu/+source/valgrind/+bug/948004) @@ -43,7 +43,7 @@ run it: Optional 'V=1' enables makefile verbosity Optional 'D=1' enables core debug messages (same as Arduino IDE's tools/debug menu) -Optional 'OPTZ=-O2' will update gcc -O option (default is -Os, -D=1 implies -O0) +Optional 'OPTZ=-O2' will update gcc -O option (default is -Os, -D=1 implies -Og) Optional 'FORCE32=0' will use native/default gcc (default is FORCE32=1 unless gcc-multilib is not detected) Optional 'R=""' (ex: R="-b -v") runs the executable with given options after build diff --git a/tests/host/common/Arduino.cpp b/tests/host/common/Arduino.cpp index 4b1d7070de..93d7a77c2b 100644 --- a/tests/host/common/Arduino.cpp +++ b/tests/host/common/Arduino.cpp @@ -15,13 +15,23 @@ #include #include - -#include +#include #include #include -static struct timeval gtod0 = { 0, 0 }; +#include +#include + +#include +#include + +namespace +{ + +timeval gtod0 = { 0, 0 }; + +} // namespace extern "C" unsigned long millis() { @@ -44,6 +54,12 @@ extern "C" unsigned long micros() extern "C" void yield() { run_scheduled_recurrent_functions(); + if (!can_yield()) + { + throw std::runtime_error("should only yield from loop()!"); + } + + esp_yield(); } extern "C" void loop_end() @@ -52,25 +68,16 @@ extern "C" void loop_end() run_scheduled_recurrent_functions(); } -extern "C" bool can_yield() -{ - return true; -} - extern "C" void optimistic_yield(uint32_t interval_us) { - (void)interval_us; -} - -extern "C" void esp_suspend() { } - -extern "C" void esp_schedule() { } + static auto last = std::chrono::steady_clock::now(); -extern "C" void esp_yield() { } - -extern "C" void esp_delay(unsigned long ms) -{ - usleep(ms * 1000); + const auto now = std::chrono::steady_clock::now(); + if (last - now > std::chrono::microseconds { interval_us }) + { + last = now; + yield(); + } } bool esp_try_delay(const uint32_t start_ms, const uint32_t timeout_ms, const uint32_t intvl_ms) @@ -99,7 +106,7 @@ extern "C" void delay(unsigned long ms) extern "C" void delayMicroseconds(unsigned int us) { - usleep(us); + std::this_thread::sleep_for(std::chrono::microseconds { us }); } #include "cont.h" diff --git a/tests/host/common/ArduinoCatch.cpp b/tests/host/common/ArduinoCatch.cpp index 5308278979..2c5ec7385a 100644 --- a/tests/host/common/ArduinoCatch.cpp +++ b/tests/host/common/ArduinoCatch.cpp @@ -16,6 +16,9 @@ #define CATCH_CONFIG_MAIN #include "ArduinoCatch.hpp" +#include +#include + std::ostream& operator<<(std::ostream& out, const String& str) { out.write(str.c_str(), str.length()); @@ -36,3 +39,23 @@ std::string StringMaker::convert(String const& str) } } // namespace Catch + +extern "C" +{ + bool can_yield() + { + return true; + } + + void esp_yield() { } + + void esp_suspend() { } + + void esp_schedule() { } + + void esp_delay(unsigned long ms) + { + std::this_thread::sleep_for(std::chrono::milliseconds(ms)); + } + +} // extern "C" diff --git a/tests/host/common/ArduinoMain.cpp b/tests/host/common/ArduinoMain.cpp index 692ea97b0e..c0eeeb5348 100644 --- a/tests/host/common/ArduinoMain.cpp +++ b/tests/host/common/ArduinoMain.cpp @@ -39,6 +39,8 @@ #include #include +#include + #define MOCK_PORT_SHIFTER 9000 bool user_exit = false; @@ -65,6 +67,18 @@ int mockverbose(const char* fmt, ...) return 0; } +static uint8_t mock_read_uart() +{ + uint8_t ch = 0; + int ret = read(STDIN_FILENO, &ch, 1); + if (ret == -1) + { + perror("read(STDIN,1)"); + return 0; + } + return (ret == 1) ? ch : 0; +} + static int mock_start_uart(void) { struct termios settings; @@ -98,31 +112,39 @@ static int mock_start_uart(void) static int mock_stop_uart(void) { if (!restore_tty) + { return 0; + } + if (!isatty(STDIN)) { perror("restoring tty: isatty(STDIN)"); return -1; } + if (tcsetattr(STDIN, TCSANOW, &initial_settings) < 0) { perror("restoring tty: tcsetattr(STDIN)"); return -1; } - printf("\e[?25h"); // show cursor - return (0); + + const char show_cursor[] = "\e[?25h"; + write(STDOUT_FILENO, &show_cursor[0], sizeof(show_cursor)); + + return 0; } -static uint8_t mock_read_uart(void) +static void mock_system_loop() { - uint8_t ch = 0; - int ret = read(STDIN, &ch, 1); - if (ret == -1) + uint8_t data = mock_read_uart(); + if (data) { - perror("read(STDIN,1)"); - return 0; + uart_new_data(UART0, data); } - return (ret == 1) ? ch : 0; + + check_incoming_udp(); + + mock::timer::loop(); } void help(const char* argv0, int exitcode) @@ -144,7 +166,8 @@ void help(const char* argv0, int exitcode) "\t (spiffs, littlefs: negative value will force mismatched size)\n" "\tgeneral:\n" "\t-c - ignore CTRL-C (send it via Serial)\n" - "\t-f - no throttle (possibly 100%%CPU)\n" + "\t-I - main thread forced sleep interval in milliseconds\n" + " (default 1ms, very likely to cause 100%%CPU when set to 0ms)\n" "\t-1 - run loop once then exit (for host testing)\n" "\t-v - verbose\n", argv0, MOCK_PORT_SHIFTER, argv0, spiffs_kb, littlefs_kb); @@ -153,7 +176,7 @@ void help(const char* argv0, int exitcode) static struct option options[] = { { "help", no_argument, NULL, 'h' }, - { "fast", no_argument, NULL, 'f' }, + { "interval", required_argument, NULL, 'I' }, { "local", no_argument, NULL, 'l' }, { "sigint", no_argument, NULL, 'c' }, { "blockinguart", no_argument, NULL, 'b' }, @@ -167,12 +190,13 @@ static struct option options[] = { { "once", no_argument, NULL, '1' }, }; -void cleanup() +void mock_stop_all() { mock_stop_udp(); mock_stop_spiffs(); mock_stop_littlefs(); mock_stop_uart(); + mock_stop_task(); } void make_fs_filename(String& name, const char* fspath, const char* argv0) @@ -192,22 +216,37 @@ void make_fs_filename(String& name, const char* fspath, const char* argv0) name = argv0; } +void loop_impl() +{ + static bool setup_done { false }; + if (!setup_done) + { + setup(); + setup_done = true; + } + + loop(); + loop_end(); +} + void control_c(int sig) { (void)sig; if (user_exit) { - fprintf(stderr, MOCK "stuck, killing\n"); - cleanup(); - exit(1); + const char stuck[] = MOCK "stuck, killing\n"; + write(STDERR_FILENO, &stuck[0], sizeof(stuck)); + mock_stop_all(); + _exit(1); } + user_exit = true; } int main(int argc, char* const argv[]) { - bool fast = false; + auto interval = std::chrono::milliseconds { 1 }; blocking_uart = false; // global signal(SIGINT, control_c); @@ -219,7 +258,7 @@ int main(int argc, char* const argv[]) for (;;) { - int n = getopt_long(argc, argv, "hlcfbvTi:S:s:L:P:1", options, NULL); + int n = getopt_long(argc, argv, "hlcbvTi:I:S:s:L:P:1", options, NULL); if (n < 0) break; switch (n) @@ -239,8 +278,8 @@ int main(int argc, char* const argv[]) case 'c': ignore_sigint = true; break; - case 'f': - fast = true; + case 'I': + interval = std::chrono::milliseconds { atoll(optarg) }; break; case 'S': spiffs_kb = atoi(optarg); @@ -300,28 +339,13 @@ int main(int argc, char* const argv[]) } // install exit handler in case Esp.restart() is called - atexit(cleanup); + atexit(mock_stop_all); // first call to millis(): now is millis() and micros() beginning millis(); - setup(); - while (!user_exit) - { - uint8_t data = mock_read_uart(); - - if (data) - uart_new_data(UART0, data); - if (!fast) - usleep(1000); // not 100% cpu, ~1000 loops per second - loop(); - loop_end(); - check_incoming_udp(); - - if (run_once) - user_exit = true; - } - cleanup(); + // loops until exit, switching between sys and user tasks + mock_loop_task(mock_system_loop, interval, user_exit); return 0; } diff --git a/tests/host/common/MockEsp.cpp b/tests/host/common/MockEsp.cpp index 18e7be6c83..a4c8e195ff 100644 --- a/tests/host/common/MockEsp.cpp +++ b/tests/host/common/MockEsp.cpp @@ -34,7 +34,8 @@ #include -#include +#include +#include #include struct rst_info resetInfo; diff --git a/tests/host/common/MockTask.cpp b/tests/host/common/MockTask.cpp new file mode 100644 index 0000000000..c7348d8fb8 --- /dev/null +++ b/tests/host/common/MockTask.cpp @@ -0,0 +1,195 @@ +/* + Arduino emulator task internals + Copyright (c) 2018 david gauchard. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal with the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + - Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimers. + + - Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimers in the + documentation and/or other materials provided with the distribution. + + - The names of its contributors may not be used to endorse or promote + products derived from this Software without specific prior written + permission. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR + OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS WITH THE SOFTWARE. +*/ + +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +namespace +{ + +// TODO ets_task(), ets_post(), etc.? afaik, no real messages are passed between tasks, just execution requests +// TODO only -std=c++20 has nice barrier api and jthread. using semaphore.h for now and plain threads +// (i.e. helping out with 'stop conditon', and a much nicer task waiting api) + +// by default, main thread is running sdk-related tasks and setup() / loop() is scheduled right after +// - every sdk task loop iteration, notify user task when it is scheduled via esp_schedule() +// and wait for the specified thread token cv until the next sdk task iteration +// - when loop() is ready to yield via esp_suspend() (either at the end of the func, or via yield()), +// notify the second thread token cv to repeat the conditions above + +std::thread user_task; + +std::atomic user_task_is_done { false }; +std::atomic scheduled { false }; + +struct Barrier { + std::atomic token { -1 }; + std::condition_variable cv; + std::mutex m; +}; + +constexpr int BARRIER_EXIT = -1; + +constexpr int BARRIER_USER = 1; +constexpr int BARRIER_SYS = 2; + +Barrier barrier {}; + +void arrive(Barrier& b, int value) +{ + b.token = value; + b.cv.notify_all(); +} + +void wait(Barrier& b, int value) +{ + std::unique_lock lock { b.m }; + b.cv.wait(lock, [&]() { return b.token == BARRIER_EXIT || b.token == value; }); +} + +ETSTimer delay_timer; + +void mock_task_wrapper() +{ + std::once_flag setup_done; + + for (;;) + { + wait(barrier, BARRIER_USER); + + std::call_once(setup_done, setup); + loop(); + loop_end(); + + esp_schedule(); + arrive(barrier, BARRIER_SYS); + + if (user_task_is_done) + { + break; + } + } +} + +} // namespace + +extern "C" bool can_yield() +{ + return std::this_thread::get_id() == user_task.get_id(); +} + +extern "C" void esp_suspend() +{ + arrive(barrier, BARRIER_SYS); + wait(barrier, BARRIER_USER); +} + +extern "C" void esp_schedule() +{ + scheduled = true; +} + +extern "C" void esp_yield() +{ + esp_schedule(); + esp_suspend(); +} + +extern "C" void esp_delay(unsigned long ms) +{ + if (ms) + { + ets_timer_setfn( + &delay_timer, + [](void*) + { + esp_schedule(); + }, + nullptr); + ets_timer_arm_new(&delay_timer, ms, 0, 1); + } + else + { + esp_schedule(); + } + + esp_suspend(); + if (ms) + { + ets_timer_disarm(&delay_timer); + } +} + +void mock_stop_task() +{ + user_task_is_done = true; + arrive(barrier, BARRIER_EXIT); +} + +void mock_loop_task(void (*system_task)(), std::chrono::milliseconds interval, + const bool& user_exit) +{ + user_task = std::thread(mock_task_wrapper); + + esp_schedule(); + for (;;) + { + system_task(); + if (interval.count() > 0) + { + std::this_thread::sleep_for(interval); + } + + if (scheduled) + { + scheduled = false; + arrive(barrier, BARRIER_USER); + wait(barrier, BARRIER_SYS); + } + + if (user_exit) + { + break; + } + } + + mock_stop_all(); + user_task.join(); +} diff --git a/tests/host/common/MockTimer.cpp b/tests/host/common/MockTimer.cpp new file mode 100644 index 0000000000..c4ad56cb9d --- /dev/null +++ b/tests/host/common/MockTimer.cpp @@ -0,0 +1,262 @@ +/* + Arduino emulation - tools + Copyright (c) 2018 david gauchard. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal with the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + - Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimers. + + - Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimers in the + documentation and/or other materials provided with the distribution. + + - The names of its contributors may not be used to endorse or promote + products derived from this Software without specific prior written + permission. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR + OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS WITH THE SOFTWARE. +*/ + +#include + +#include +#include + +#include +#include +#include + +namespace mock +{ +namespace timer +{ + namespace + { + + // since attach() uses user-provided ETSTimer* to store the data, + // the only possible time type is u32 and not the common host i64 + using Microseconds = std::chrono::duration; + using Milliseconds = std::chrono::duration; + + ETSTimer* timers { nullptr }; + std::mutex m; + + // ...but, it is possible to upcast to i64 and compare timestamps vs. + // possibly maintaining two timer lists for before and after overflow + + // checks whether lhs 'timestamp' is after rhs + constexpr bool after(Microseconds lhs, Microseconds rhs) + { + return (int64_t(rhs.count()) - int64_t(lhs.count())) < 0ll; + } + + bool after(ETSTimer* lhs, ETSTimer* rhs) + { + return after(Microseconds { lhs->timer_expire }, Microseconds { rhs->timer_expire }); + } + + // TODO move this to micros() and millis()? + Microseconds steady_clock_now() + { + using clock = std::chrono::high_resolution_clock; + const auto now = clock::now().time_since_epoch(); + + const auto micros = std::chrono::duration_cast(now); + const auto max = clock::rep { std::numeric_limits::max() }; + + return Microseconds { static_cast(micros.count() % max) }; + } + + void detach(const std::lock_guard&, ETSTimer* user_timer) + { + ETSTimer* prev = nullptr; + ETSTimer* head = timers; + while (head != nullptr) + { + if ((user_timer == head) && (prev != nullptr)) + { + prev->timer_next = head->timer_next; + break; + } + + prev = head; + head = head->timer_next; + } + + user_timer->timer_expire = 0; + user_timer->timer_next = nullptr; + } + + void detach(ETSTimer* user_timer) + { + auto lock = std::lock_guard { m }; + detach(lock, user_timer); + } + + // aka timer_insert(). attach timer to the current list of pointers, + // making sure it is sorted based on expiration time (earliest to latest) + void attach_sorted(ETSTimer* timer) + { + ETSTimer* prev = nullptr; + ETSTimer* head = timers; + while (head != nullptr) + { + if (after(head, timer)) + { + break; + } + + prev = head; + head = head->timer_next; + } + + if (prev != nullptr) + { + prev->timer_next = timer; + } + else + { + timers = timer; + } + + timer->timer_next = head; + } + + void attach(ETSTimer* user_timer, Microseconds duration, bool repeat) + { + if (duration.count() == 0) + { + return; + } + + auto lock = std::lock_guard { m }; + detach(lock, user_timer); + + const auto expire = duration + steady_clock_now(); + user_timer->timer_expire = expire.count(); + + user_timer->timer_period = repeat ? duration.count() : 0; + + attach_sorted(user_timer); + } + + void attach(ETSTimer* timer, int duration, int type, int isMillisecondsTimer) + { + const bool repeat = (type == 1); + + Microseconds musec; + if (isMillisecondsTimer == 1) + { + musec = Milliseconds { duration }; + } + else + { + musec = Microseconds { duration }; + } + + attach(timer, musec, repeat); + } + + void setfn(ETSTimer* timer, ETSTimerFunc* func, void* arg) + { + timer->timer_func = func; + timer->timer_arg = arg; + } + + struct Expired + { + ETSTimerFunc* func { nullptr }; + void* arg { nullptr }; + + Expired() = default; + + explicit operator bool() const + { + return func != nullptr; + } + + void operator()() const + { + if (func != nullptr) + { + func(arg); + } + } + }; + + void take_expired(Expired& out, const std::lock_guard&) + { + ETSTimer* expired { nullptr }; + auto now = steady_clock_now(); + + if (after(now, Microseconds { timers->timer_expire })) + { + expired = timers; + timers = timers->timer_next; + expired->timer_next = nullptr; + + if (expired->timer_period != 0) + { + expired->timer_expire = now.count() + expired->timer_period; + attach_sorted(expired); + } + } + + if (expired != nullptr) + { + out.func = expired->timer_func; + out.arg = expired->timer_arg; + } + } + + } // namespace + + void loop() + { + Expired expired; + + if (timers != nullptr) + { + auto lock = std::lock_guard { m }; + if (timers != nullptr) + { + take_expired(expired, lock); + } + } + + expired(); + } + +} // namespace timer +} // namespace mock + +extern "C" +{ + void ets_timer_arm_new(ETSTimer* timer, int duration, int type, int isMillisecondsTimer) + { + mock::timer::attach(timer, duration, type, isMillisecondsTimer); + } + + void ets_timer_setfn(ETSTimer* timer, ETSTimerFunc* fn, void* parg) + { + mock::timer::setfn(timer, fn, parg); + } + + void ets_timer_disarm(ETSTimer* timer) + { + mock::timer::detach(timer); + } + +} // extern "C" diff --git a/tests/host/common/mock.h b/tests/host/common/mock.h index b3308282f0..b1a28da41b 100644 --- a/tests/host/common/mock.h +++ b/tests/host/common/mock.h @@ -145,6 +145,16 @@ extern "C" } #endif +namespace mock +{ +namespace timer +{ + + void loop(); + +} // namespace timer +} // namespace mock + // tcp int mockSockSetup(int sock); int mockConnect(uint32_t addr, int& sock, int port); @@ -178,6 +188,14 @@ void mock_start_littlefs(const String& fname, size_t size_kb, size_t block_kb = size_t page_b = 512); void mock_stop_littlefs(); +#ifdef __cplusplus +#include +void mock_stop_task(); +void mock_loop_task(void (*)(), std::chrono::milliseconds interval, const bool& user_exit); +#endif + +void mock_stop_all(); + // #include diff --git a/tests/restyle.sh b/tests/restyle.sh index ce8e906a76..1c55a822da 100755 --- a/tests/restyle.sh +++ b/tests/restyle.sh @@ -49,7 +49,7 @@ done # TODO should not be matched, these are formatted externally # exclude=$(git submodule --quiet foreach git rev-parse --show-toplevel | grep libraries) -if [ -z $1 ] ; then +if [ -z "$1" ] ; then style=${root}/tests/clang-format-arduino.yaml find libraries \ -path libraries/ESP8266SdFat -prune -o \ diff --git a/tools/sdk/include/ets_sys.h b/tools/sdk/include/ets_sys.h index 7908f127c3..251b7402b6 100644 --- a/tools/sdk/include/ets_sys.h +++ b/tools/sdk/include/ets_sys.h @@ -211,7 +211,7 @@ void ets_timer_disarm(ETSTimer *a); int atoi(const char *nptr); int ets_strncmp(const char *s1, const char *s2, int len); int ets_strcmp(const char *s1, const char *s2); -int ets_strlen(const char *s); +size_t ets_strlen(const char *s); char *ets_strcpy(char *dest, const char *src); char *ets_strncpy(char *dest, const char *src, size_t n); char *ets_strstr(const char *haystack, const char *needle); From 750ce7a6ee53dfdf7dd5cc2ec8173e03b4872b13 Mon Sep 17 00:00:00 2001 From: Maxim Prokhorov Date: Thu, 11 Apr 2024 00:19:03 +0300 Subject: [PATCH 2/3] restyle --- tests/host/common/MockTask.cpp | 46 ++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/tests/host/common/MockTask.cpp b/tests/host/common/MockTask.cpp index c7348d8fb8..e8b97eaa79 100644 --- a/tests/host/common/MockTask.cpp +++ b/tests/host/common/MockTask.cpp @@ -59,29 +59,37 @@ std::thread user_task; std::atomic user_task_is_done { false }; std::atomic scheduled { false }; -struct Barrier { - std::atomic token { -1 }; - std::condition_variable cv; - std::mutex m; +enum class Token +{ + Default, + Exit, + User, + Sys, }; -constexpr int BARRIER_EXIT = -1; - -constexpr int BARRIER_USER = 1; -constexpr int BARRIER_SYS = 2; +struct Barrier +{ + std::atomic token { Token::Default }; + std::condition_variable cv; + std::mutex m; +}; Barrier barrier {}; -void arrive(Barrier& b, int value) +void arrive(Barrier& b, Token token) { - b.token = value; + b.token = token; b.cv.notify_all(); } -void wait(Barrier& b, int value) +void wait(Barrier& b, Token token) { std::unique_lock lock { b.m }; - b.cv.wait(lock, [&]() { return b.token == BARRIER_EXIT || b.token == value; }); + b.cv.wait(lock, + [&]() + { + return b.token == Token::Exit || b.token == token; + }); } ETSTimer delay_timer; @@ -92,14 +100,14 @@ void mock_task_wrapper() for (;;) { - wait(barrier, BARRIER_USER); + wait(barrier, Token::User); std::call_once(setup_done, setup); loop(); loop_end(); esp_schedule(); - arrive(barrier, BARRIER_SYS); + arrive(barrier, Token::Sys); if (user_task_is_done) { @@ -117,8 +125,8 @@ extern "C" bool can_yield() extern "C" void esp_suspend() { - arrive(barrier, BARRIER_SYS); - wait(barrier, BARRIER_USER); + arrive(barrier, Token::Sys); + wait(barrier, Token::User); } extern "C" void esp_schedule() @@ -160,7 +168,7 @@ extern "C" void esp_delay(unsigned long ms) void mock_stop_task() { user_task_is_done = true; - arrive(barrier, BARRIER_EXIT); + arrive(barrier, Token::Exit); } void mock_loop_task(void (*system_task)(), std::chrono::milliseconds interval, @@ -180,8 +188,8 @@ void mock_loop_task(void (*system_task)(), std::chrono::milliseconds interval, if (scheduled) { scheduled = false; - arrive(barrier, BARRIER_USER); - wait(barrier, BARRIER_SYS); + arrive(barrier, Token::User); + wait(barrier, Token::Sys); } if (user_exit) From 36386f8c7eb7db01322d6c39faf9a025bcb4e453 Mon Sep 17 00:00:00 2001 From: Maxim Prokhorov Date: Thu, 11 Apr 2024 02:48:50 +0300 Subject: [PATCH 3/3] actually run only once check stop condition after wait, grab the token value another todo might be a proper watchdog... or running apps via `timeout 30s ` or similar --- tests/host/common/ArduinoMain.cpp | 5 +- tests/host/common/MockTask.cpp | 90 +++++++++++++++++++++---------- tests/host/common/mock.h | 3 +- 3 files changed, 68 insertions(+), 30 deletions(-) diff --git a/tests/host/common/ArduinoMain.cpp b/tests/host/common/ArduinoMain.cpp index c0eeeb5348..52e10e161b 100644 --- a/tests/host/common/ArduinoMain.cpp +++ b/tests/host/common/ArduinoMain.cpp @@ -44,7 +44,6 @@ #define MOCK_PORT_SHIFTER 9000 bool user_exit = false; -bool run_once = false; const char* host_interface = nullptr; size_t spiffs_kb = 1024; size_t littlefs_kb = 1024; @@ -247,6 +246,8 @@ void control_c(int sig) int main(int argc, char* const argv[]) { auto interval = std::chrono::milliseconds { 1 }; + auto run_once = false; + blocking_uart = false; // global signal(SIGINT, control_c); @@ -345,7 +346,7 @@ int main(int argc, char* const argv[]) millis(); // loops until exit, switching between sys and user tasks - mock_loop_task(mock_system_loop, interval, user_exit); + mock_loop_task(mock_system_loop, interval, run_once, user_exit); return 0; } diff --git a/tests/host/common/MockTask.cpp b/tests/host/common/MockTask.cpp index e8b97eaa79..5b31140542 100644 --- a/tests/host/common/MockTask.cpp +++ b/tests/host/common/MockTask.cpp @@ -40,6 +40,7 @@ #include #include #include +#include namespace { @@ -54,15 +55,21 @@ namespace // - when loop() is ready to yield via esp_suspend() (either at the end of the func, or via yield()), // notify the second thread token cv to repeat the conditions above -std::thread user_task; - -std::atomic user_task_is_done { false }; +std::thread user_task; std::atomic scheduled { false }; +struct StopException: public std::exception +{ + const char* what() const noexcept(true) override + { + return "Stopped"; + } +}; + enum class Token { Default, - Exit, + Stop, User, Sys, }; @@ -82,38 +89,62 @@ void arrive(Barrier& b, Token token) b.cv.notify_all(); } -void wait(Barrier& b, Token token) +Token wait(Barrier& b, Token token) { std::unique_lock lock { b.m }; - b.cv.wait(lock, - [&]() - { - return b.token == Token::Exit || b.token == token; - }); + + Token current; + for (;;) + { + current = b.token; + if ((current == token) || (current == Token::Stop)) + { + break; + } + + b.cv.wait(lock); + } + + return current; +} + +bool wait_or_stop(Barrier& b, Token token) +{ + return Token::Stop == wait(b, token); } ETSTimer delay_timer; -void mock_task_wrapper() +void mock_task_wrapper(bool once) { std::once_flag setup_done; - for (;;) + try { - wait(barrier, Token::User); + for (;;) + { + if (wait_or_stop(barrier, Token::User)) + { + break; + } - std::call_once(setup_done, setup); - loop(); - loop_end(); + std::call_once(setup_done, setup); + loop(); + loop_end(); - esp_schedule(); - arrive(barrier, Token::Sys); + if (once) + { + arrive(barrier, Token::Stop); + break; + } - if (user_task_is_done) - { - break; + esp_schedule(); + arrive(barrier, Token::Sys); } } + catch (const StopException&) + { + } } } // namespace @@ -126,7 +157,10 @@ extern "C" bool can_yield() extern "C" void esp_suspend() { arrive(barrier, Token::Sys); - wait(barrier, Token::User); + if (wait_or_stop(barrier, Token::User)) + { + throw StopException {}; + } } extern "C" void esp_schedule() @@ -167,14 +201,13 @@ extern "C" void esp_delay(unsigned long ms) void mock_stop_task() { - user_task_is_done = true; - arrive(barrier, Token::Exit); + arrive(barrier, Token::Stop); } -void mock_loop_task(void (*system_task)(), std::chrono::milliseconds interval, +void mock_loop_task(void (*system_task)(), std::chrono::milliseconds interval, bool once, const bool& user_exit) { - user_task = std::thread(mock_task_wrapper); + user_task = std::thread(mock_task_wrapper, once); esp_schedule(); for (;;) @@ -189,7 +222,10 @@ void mock_loop_task(void (*system_task)(), std::chrono::milliseconds interval, { scheduled = false; arrive(barrier, Token::User); - wait(barrier, Token::Sys); + if (wait_or_stop(barrier, Token::Sys)) + { + break; + } } if (user_exit) diff --git a/tests/host/common/mock.h b/tests/host/common/mock.h index b1a28da41b..a4bc18976f 100644 --- a/tests/host/common/mock.h +++ b/tests/host/common/mock.h @@ -191,7 +191,8 @@ void mock_stop_littlefs(); #ifdef __cplusplus #include void mock_stop_task(); -void mock_loop_task(void (*)(), std::chrono::milliseconds interval, const bool& user_exit); +void mock_loop_task(void (*)(), std::chrono::milliseconds interval, bool once, + const bool& user_exit); #endif void mock_stop_all();