diff --git a/include/croncpp.h b/include/croncpp.h new file mode 100644 index 0000000..b0037c0 --- /dev/null +++ b/include/croncpp.h @@ -0,0 +1,917 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if __cplusplus > 201402L +#include +#define CRONCPP_IS_CPP17 +#endif + +namespace cron +{ +#ifdef CRONCPP_IS_CPP17 + #define CRONCPP_STRING_VIEW std::string_view + #define CRONCPP_STRING_VIEW_NPOS std::string_view::npos + #define CRONCPP_CONSTEXPTR constexpr +#else + #define CRONCPP_STRING_VIEW std::string const & + #define CRONCPP_STRING_VIEW_NPOS std::string::npos + #define CRONCPP_CONSTEXPTR +#endif + + using cron_int = uint8_t; + + constexpr std::time_t INVALID_TIME = static_cast(-1); + + constexpr size_t INVALID_INDEX = static_cast(-1); + + class cronexpr; + + namespace detail + { + enum class cron_field + { + second, + minute, + hour_of_day, + day_of_week, + day_of_month, + month, + year + }; + + template + static bool find_next(cronexpr const & cex, + std::tm& date, + size_t const dot); + } + + struct bad_cronexpr : public std::runtime_error + { + public: + explicit bad_cronexpr(CRONCPP_STRING_VIEW message) : + std::runtime_error(message.data()) + {} + }; + + + struct cron_standard_traits + { + static const cron_int CRON_MIN_SECONDS = 0; + static const cron_int CRON_MAX_SECONDS = 59; + + static const cron_int CRON_MIN_MINUTES = 0; + static const cron_int CRON_MAX_MINUTES = 59; + + static const cron_int CRON_MIN_HOURS = 0; + static const cron_int CRON_MAX_HOURS = 23; + + static const cron_int CRON_MIN_DAYS_OF_WEEK = 0; + static const cron_int CRON_MAX_DAYS_OF_WEEK = 6; + + static const cron_int CRON_MIN_DAYS_OF_MONTH = 1; + static const cron_int CRON_MAX_DAYS_OF_MONTH = 31; + + static const cron_int CRON_MIN_MONTHS = 1; + static const cron_int CRON_MAX_MONTHS = 12; + + static const cron_int CRON_MAX_YEARS_DIFF = 4; + +#ifdef CRONCPP_IS_CPP17 + static const inline std::vector DAYS = { "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT" }; + static const inline std::vector MONTHS = { "NIL", "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" }; +#else + static std::vector& DAYS() + { + static std::vector days = { "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT" }; + return days; + } + + static std::vector& MONTHS() + { + static std::vector months = { "NIL", "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" }; + return months; + } +#endif + }; + + struct cron_oracle_traits + { + static const cron_int CRON_MIN_SECONDS = 0; + static const cron_int CRON_MAX_SECONDS = 59; + + static const cron_int CRON_MIN_MINUTES = 0; + static const cron_int CRON_MAX_MINUTES = 59; + + static const cron_int CRON_MIN_HOURS = 0; + static const cron_int CRON_MAX_HOURS = 23; + + static const cron_int CRON_MIN_DAYS_OF_WEEK = 1; + static const cron_int CRON_MAX_DAYS_OF_WEEK = 7; + + static const cron_int CRON_MIN_DAYS_OF_MONTH = 1; + static const cron_int CRON_MAX_DAYS_OF_MONTH = 31; + + static const cron_int CRON_MIN_MONTHS = 0; + static const cron_int CRON_MAX_MONTHS = 11; + + static const cron_int CRON_MAX_YEARS_DIFF = 4; + +#ifdef CRONCPP_IS_CPP17 + static const inline std::vector DAYS = { "NIL", "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT" }; + static const inline std::vector MONTHS = { "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" }; +#else + + static std::vector& DAYS() + { + static std::vector days = { "NIL", "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT" }; + return days; + } + + static std::vector& MONTHS() + { + static std::vector months = { "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" }; + return months; + } +#endif + }; + + struct cron_quartz_traits + { + static const cron_int CRON_MIN_SECONDS = 0; + static const cron_int CRON_MAX_SECONDS = 59; + + static const cron_int CRON_MIN_MINUTES = 0; + static const cron_int CRON_MAX_MINUTES = 59; + + static const cron_int CRON_MIN_HOURS = 0; + static const cron_int CRON_MAX_HOURS = 23; + + static const cron_int CRON_MIN_DAYS_OF_WEEK = 1; + static const cron_int CRON_MAX_DAYS_OF_WEEK = 7; + + static const cron_int CRON_MIN_DAYS_OF_MONTH = 1; + static const cron_int CRON_MAX_DAYS_OF_MONTH = 31; + + static const cron_int CRON_MIN_MONTHS = 1; + static const cron_int CRON_MAX_MONTHS = 12; + + static const cron_int CRON_MAX_YEARS_DIFF = 4; + +#ifdef CRONCPP_IS_CPP17 + static const inline std::vector DAYS = { "NIL", "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT" }; + static const inline std::vector MONTHS = { "NIL", "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" }; +#else + static std::vector& DAYS() + { + static std::vector days = { "NIL", "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT" }; + return days; + } + + static std::vector& MONTHS() + { + static std::vector months = { "NIL", "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" }; + return months; + } +#endif + }; + + class cronexpr; + + template + static cronexpr make_cron(CRONCPP_STRING_VIEW expr); + + class cronexpr + { + std::bitset<60> seconds; + std::bitset<60> minutes; + std::bitset<24> hours; + std::bitset<7> days_of_week; + std::bitset<31> days_of_month; + std::bitset<12> months; + std::string expr; + + friend bool operator==(cronexpr const & e1, cronexpr const & e2); + friend bool operator!=(cronexpr const & e1, cronexpr const & e2); + + template + friend bool detail::find_next(cronexpr const & cex, + std::tm& date, + size_t const dot); + + friend std::string to_cronstr(cronexpr const& cex); + friend std::string to_string(cronexpr const & cex); + + template + friend cronexpr make_cron(CRONCPP_STRING_VIEW expr); + }; + + inline bool operator==(cronexpr const & e1, cronexpr const & e2) + { + return + e1.seconds == e2.seconds && + e1.minutes == e2.minutes && + e1.hours == e2.hours && + e1.days_of_week == e2.days_of_week && + e1.days_of_month == e2.days_of_month && + e1.months == e2.months; + } + + inline bool operator!=(cronexpr const & e1, cronexpr const & e2) + { + return !(e1 == e2); + } + + inline std::string to_string(cronexpr const & cex) + { + return + cex.seconds.to_string() + " " + + cex.minutes.to_string() + " " + + cex.hours.to_string() + " " + + cex.days_of_month.to_string() + " " + + cex.months.to_string() + " " + + cex.days_of_week.to_string(); + } + + inline std::string to_cronstr(cronexpr const& cex) + { + return cex.expr; + } + + namespace utils + { + inline std::time_t tm_to_time(std::tm& date) + { + return std::mktime(&date); + } + + inline std::tm* time_to_tm(std::time_t const * date, std::tm* const out) + { +#ifdef _WIN32 + errno_t err = localtime_s(out, date); + return 0 == err ? out : nullptr; +#else + return localtime_r(date, out); +#endif + } + + inline std::tm to_tm(CRONCPP_STRING_VIEW time) + { + std::tm result; +#if __cplusplus > 201103L + std::istringstream str(time.data()); + str.imbue(std::locale(setlocale(LC_ALL, nullptr))); + + str >> std::get_time(&result, "%Y-%m-%d %H:%M:%S"); + if (str.fail()) throw std::runtime_error("Parsing date failed!"); +#else + int year = 1900; + int month = 1; + int day = 1; + int hour = 0; + int minute = 0; + int second = 0; + sscanf(time.data(), "%d-%d-%d %d:%d:%d", &year, &month, &day, &hour, &minute, &second); + result.tm_year = year - 1900; + result.tm_mon = month - 1; + result.tm_mday = day; + result.tm_hour = hour; + result.tm_min = minute; + result.tm_sec = second; +#endif + result.tm_isdst = -1; // DST info not available + + return result; + } + + inline std::string to_string(std::tm const & tm) + { +#if __cplusplus > 201103L + std::ostringstream str; + str.imbue(std::locale(setlocale(LC_ALL, nullptr))); + str << std::put_time(&tm, "%Y-%m-%d %H:%M:%S"); + if (str.fail()) throw std::runtime_error("Writing date failed!"); + + return str.str(); +#else + char buff[70] = {0}; + strftime(buff, sizeof(buff), "%Y-%m-%d %H:%M:%S", &tm); + return std::string(buff); +#endif + } + + inline std::string to_upper(std::string text) + { + std::transform(std::begin(text), std::end(text), + std::begin(text), [](char const c) { return static_cast(std::toupper(c)); }); + + return text; + } + + static std::vector split(CRONCPP_STRING_VIEW text, char const delimiter) + { + std::vector tokens; + std::string token; + std::istringstream tokenStream(text.data()); + while (std::getline(tokenStream, token, delimiter)) + { + tokens.push_back(token); + } + return tokens; + } + + CRONCPP_CONSTEXPTR inline bool contains(CRONCPP_STRING_VIEW text, char const ch) noexcept + { + return CRONCPP_STRING_VIEW_NPOS != text.find_first_of(ch); + } + } + + namespace detail + { + + inline cron_int to_cron_int(CRONCPP_STRING_VIEW text) + { + try + { + return static_cast(std::stoul(text.data())); + } + catch (std::exception const & ex) + { + throw bad_cronexpr(ex.what()); + } + } + + static std::string replace_ordinals( + std::string text, + std::vector const & replacement) + { + for (size_t i = 0; i < replacement.size(); ++i) + { + auto pos = text.find(replacement[i]); + if (std::string::npos != pos) + text.replace(pos, 3 ,std::to_string(i)); + } + + return text; + } + + static std::pair make_range( + CRONCPP_STRING_VIEW field, + cron_int const minval, + cron_int const maxval) + { + cron_int first = 0; + cron_int last = 0; + if (field.size() == 1 && field[0] == '*') + { + first = minval; + last = maxval; + } + else if (!utils::contains(field, '-')) + { + first = to_cron_int(field); + last = first; + } + else + { + auto parts = utils::split(field, '-'); + if (parts.size() != 2) + throw bad_cronexpr("Specified range requires two fields"); + + first = to_cron_int(parts[0]); + last = to_cron_int(parts[1]); + } + + if (first > maxval || last > maxval) + { + throw bad_cronexpr("Specified range exceeds maximum"); + } + if (first < minval || last < minval) + { + throw bad_cronexpr("Specified range is less than minimum"); + } + if (first > last) + { + throw bad_cronexpr("Specified range start exceeds range end"); + } + + return { first, last }; + } + + template + static void set_cron_field( + CRONCPP_STRING_VIEW value, + std::bitset& target, + cron_int const minval, + cron_int const maxval) + { + if(value.length() > 0 && value[value.length()-1] == ',') + throw bad_cronexpr("Value cannot end with comma"); + + auto fields = utils::split(value, ','); + if (fields.empty()) + throw bad_cronexpr("Expression parsing error"); + + for (auto const & field : fields) + { + if (!utils::contains(field, '/')) + { +#ifdef CRONCPP_IS_CPP17 + auto[first, last] = detail::make_range(field, minval, maxval); +#else + auto range = detail::make_range(field, minval, maxval); + auto first = range.first; + auto last = range.second; +#endif + for (cron_int i = first - minval; i <= last - minval; ++i) + { + target.set(i); + } + } + else + { + auto parts = utils::split(field, '/'); + if (parts.size() != 2) + throw bad_cronexpr("Incrementer must have two fields"); + +#ifdef CRONCPP_IS_CPP17 + auto[first, last] = detail::make_range(parts[0], minval, maxval); +#else + auto range = detail::make_range(parts[0], minval, maxval); + auto first = range.first; + auto last = range.second; +#endif + + if (!utils::contains(parts[0], '-')) + { + last = maxval; + } + + auto delta = detail::to_cron_int(parts[1]); + if(delta <= 0) + throw bad_cronexpr("Incrementer must be a positive value"); + + for (cron_int i = first - minval; i <= last - minval; i += delta) + { + target.set(i); + } + } + } + } + + template + static void set_cron_days_of_week( + std::string value, + std::bitset<7>& target) + { + auto days = utils::to_upper(value); + auto days_replaced = detail::replace_ordinals( + days, +#ifdef CRONCPP_IS_CPP17 + Traits::DAYS +#else + Traits::DAYS() +#endif + ); + + if (days_replaced.size() == 1 && days_replaced[0] == '?') + days_replaced[0] = '*'; + + set_cron_field( + days_replaced, + target, + Traits::CRON_MIN_DAYS_OF_WEEK, + Traits::CRON_MAX_DAYS_OF_WEEK); + } + + template + static void set_cron_days_of_month( + std::string value, + std::bitset<31>& target) + { + if (value.size() == 1 && value[0] == '?') + value[0] = '*'; + + set_cron_field( + value, + target, + Traits::CRON_MIN_DAYS_OF_MONTH, + Traits::CRON_MAX_DAYS_OF_MONTH); + } + + template + static void set_cron_month( + std::string value, + std::bitset<12>& target) + { + auto month = utils::to_upper(value); + auto month_replaced = replace_ordinals( + month, +#ifdef CRONCPP_IS_CPP17 + Traits::MONTHS +#else + Traits::MONTHS() +#endif + ); + + set_cron_field( + month_replaced, + target, + Traits::CRON_MIN_MONTHS, + Traits::CRON_MAX_MONTHS); + } + + template + inline size_t next_set_bit( + std::bitset const & target, + size_t /*minimum*/, + size_t /*maximum*/, + size_t offset) + { + for (auto i = offset; i < N; ++i) + { + if (target.test(i)) return i; + } + + return INVALID_INDEX; + } + + inline void add_to_field( + std::tm& date, + cron_field const field, + int const val) + { + switch (field) + { + case cron_field::second: + date.tm_sec += val; + break; + case cron_field::minute: + date.tm_min += val; + break; + case cron_field::hour_of_day: + date.tm_hour += val; + break; + case cron_field::day_of_week: + case cron_field::day_of_month: + date.tm_mday += val; + date.tm_isdst = -1; + break; + case cron_field::month: + date.tm_mon += val; + date.tm_isdst = -1; + break; + case cron_field::year: + date.tm_year += val; + break; + } + + if (INVALID_TIME == utils::tm_to_time(date)) + throw bad_cronexpr("Invalid time expression"); + } + + inline void set_field( + std::tm& date, + cron_field const field, + int const val) + { + switch (field) + { + case cron_field::second: + date.tm_sec = val; + break; + case cron_field::minute: + date.tm_min = val; + break; + case cron_field::hour_of_day: + date.tm_hour = val; + break; + case cron_field::day_of_week: + date.tm_wday = val; + break; + case cron_field::day_of_month: + date.tm_mday = val; + date.tm_isdst = -1; + break; + case cron_field::month: + date.tm_mon = val; + date.tm_isdst = -1; + break; + case cron_field::year: + date.tm_year = val; + break; + } + + if (INVALID_TIME == utils::tm_to_time(date)) + throw bad_cronexpr("Invalid time expression"); + } + + inline void reset_field( + std::tm& date, + cron_field const field) + { + switch (field) + { + case cron_field::second: + date.tm_sec = 0; + break; + case cron_field::minute: + date.tm_min = 0; + break; + case cron_field::hour_of_day: + date.tm_hour = 0; + break; + case cron_field::day_of_week: + date.tm_wday = 0; + break; + case cron_field::day_of_month: + date.tm_mday = 1; + date.tm_isdst = -1; + break; + case cron_field::month: + date.tm_mon = 0; + date.tm_isdst = -1; + break; + case cron_field::year: + date.tm_year = 0; + break; + } + + if (INVALID_TIME == utils::tm_to_time(date)) + throw bad_cronexpr("Invalid time expression"); + } + + inline void reset_all_fields( + std::tm& date, + std::bitset<7> const & marked_fields) + { + for (size_t i = 0; i < marked_fields.size(); ++i) + { + if (marked_fields.test(i)) + reset_field(date, static_cast(i)); + } + } + + inline void mark_field( + std::bitset<7> & orders, + cron_field const field) + { + if (!orders.test(static_cast(field))) + orders.set(static_cast(field)); + } + + template + static size_t find_next( + std::bitset const & target, + std::tm& date, + unsigned int const minimum, + unsigned int const maximum, + unsigned int const value, + cron_field const field, + cron_field const next_field, + std::bitset<7> const & marked_fields) + { + auto next_value = next_set_bit(target, minimum, maximum, value); + if (INVALID_INDEX == next_value) + { + add_to_field(date, next_field, 1); + reset_field(date, field); + next_value = next_set_bit(target, minimum, maximum, 0); + } + + if (INVALID_INDEX == next_value || next_value != value) + { + set_field(date, field, static_cast(next_value)); + reset_all_fields(date, marked_fields); + } + + return next_value; + } + + template + static size_t find_next_day( + std::tm& date, + std::bitset<31> const & days_of_month, + size_t day_of_month, + std::bitset<7> const & days_of_week, + size_t day_of_week, + std::bitset<7> const & marked_fields) + { + unsigned int count = 0; + unsigned int maximum = 366; + while ( + (!days_of_month.test(day_of_month - Traits::CRON_MIN_DAYS_OF_MONTH) || + !days_of_week.test(day_of_week - Traits::CRON_MIN_DAYS_OF_WEEK)) + && count++ < maximum) + { + add_to_field(date, cron_field::day_of_month, 1); + + day_of_month = date.tm_mday; + day_of_week = date.tm_wday; + + reset_all_fields(date, marked_fields); + } + + return day_of_month; + } + + template + static bool find_next(cronexpr const & cex, + std::tm& date, + size_t const dot) + { + bool res = true; + + std::bitset<7> marked_fields{ 0 }; + std::bitset<7> empty_list{ 0 }; + + unsigned int second = date.tm_sec; + auto updated_second = find_next( + cex.seconds, + date, + Traits::CRON_MIN_SECONDS, + Traits::CRON_MAX_SECONDS, + second, + cron_field::second, + cron_field::minute, + empty_list); + + if (second == updated_second) + { + mark_field(marked_fields, cron_field::second); + } + + unsigned int minute = date.tm_min; + auto update_minute = find_next( + cex.minutes, + date, + Traits::CRON_MIN_MINUTES, + Traits::CRON_MAX_MINUTES, + minute, + cron_field::minute, + cron_field::hour_of_day, + marked_fields); + if (minute == update_minute) + { + mark_field(marked_fields, cron_field::minute); + } + else + { + res = find_next(cex, date, dot); + if (!res) return res; + } + + unsigned int hour = date.tm_hour; + auto updated_hour = find_next( + cex.hours, + date, + Traits::CRON_MIN_HOURS, + Traits::CRON_MAX_HOURS, + hour, + cron_field::hour_of_day, + cron_field::day_of_week, + marked_fields); + if (hour == updated_hour) + { + mark_field(marked_fields, cron_field::hour_of_day); + } + else + { + res = find_next(cex, date, dot); + if (!res) return res; + } + + unsigned int day_of_week = date.tm_wday; + unsigned int day_of_month = date.tm_mday; + auto updated_day_of_month = find_next_day( + date, + cex.days_of_month, + day_of_month, + cex.days_of_week, + day_of_week, + marked_fields); + if (day_of_month == updated_day_of_month) + { + mark_field(marked_fields, cron_field::day_of_month); + } + else + { + res = find_next(cex, date, dot); + if (!res) return res; + } + + unsigned int month = date.tm_mon; + auto updated_month = find_next( + cex.months, + date, + Traits::CRON_MIN_MONTHS, + Traits::CRON_MAX_MONTHS, + month, + cron_field::month, + cron_field::year, + marked_fields); + if (month != updated_month) + { + if (date.tm_year - dot > Traits::CRON_MAX_YEARS_DIFF) + return false; + + res = find_next(cex, date, dot); + if (!res) return res; + } + + return res; + } + } + + template + static cronexpr make_cron(CRONCPP_STRING_VIEW expr) + { + cronexpr cex; + + if (expr.empty()) + throw bad_cronexpr("Invalid empty cron expression"); + + auto fields = utils::split(expr, ' '); + fields.erase( + std::remove_if(std::begin(fields), std::end(fields), + [](CRONCPP_STRING_VIEW s) {return s.empty(); }), + std::end(fields)); + if (fields.size() != 6) + throw bad_cronexpr("cron expression must have six fields"); + + detail::set_cron_field(fields[0], cex.seconds, Traits::CRON_MIN_SECONDS, Traits::CRON_MAX_SECONDS); + detail::set_cron_field(fields[1], cex.minutes, Traits::CRON_MIN_MINUTES, Traits::CRON_MAX_MINUTES); + detail::set_cron_field(fields[2], cex.hours, Traits::CRON_MIN_HOURS, Traits::CRON_MAX_HOURS); + + detail::set_cron_days_of_week(fields[5], cex.days_of_week); + + detail::set_cron_days_of_month(fields[3], cex.days_of_month); + + detail::set_cron_month(fields[4], cex.months); + + cex.expr = expr; + + return cex; + } + + template + static std::tm cron_next(cronexpr const & cex, std::tm date) + { + time_t original = utils::tm_to_time(date); + if (INVALID_TIME == original) return {}; + + if (!detail::find_next(cex, date, date.tm_year)) + return {}; + + time_t calculated = utils::tm_to_time(date); + if (INVALID_TIME == calculated) return {}; + + if (calculated == original) + { + add_to_field(date, detail::cron_field::second, 1); + if (!detail::find_next(cex, date, date.tm_year)) + return {}; + } + + return date; + } + + template + static std::time_t cron_next(cronexpr const & cex, std::time_t const & date) + { + std::tm val; + std::tm* dt = utils::time_to_tm(&date, &val); + if (dt == nullptr) return INVALID_TIME; + + time_t original = utils::tm_to_time(*dt); + if (INVALID_TIME == original) return INVALID_TIME; + + if(!detail::find_next(cex, *dt, dt->tm_year)) + return INVALID_TIME; + + time_t calculated = utils::tm_to_time(*dt); + if (INVALID_TIME == calculated) return calculated; + + if (calculated == original) + { + add_to_field(*dt, detail::cron_field::second, 1); + if(!detail::find_next(cex, *dt, dt->tm_year)) + return INVALID_TIME; + } + + return utils::tm_to_time(*dt); + } + + template + static std::chrono::system_clock::time_point cron_next(cronexpr const & cex, std::chrono::system_clock::time_point const & time_point) { + return std::chrono::system_clock::from_time_t(cron_next(cex, std::chrono::system_clock::to_time_t(time_point))); + } +} diff --git a/lib/RTC/PCF85063_Driver.cpp b/lib/RTC/PCF85063_Driver.cpp index a4f9af7..2a7a6ad 100644 --- a/lib/RTC/PCF85063_Driver.cpp +++ b/lib/RTC/PCF85063_Driver.cpp @@ -180,6 +180,12 @@ namespace drivers const std::string PCF85063::datetime2str(const datetime_t &datetime) { + tm dtime = datetime2tm(datetime); + const std::string buf(std::asctime(&dtime)); + return buf.substr(0, std::min(buf.find('\n'),buf.find('\r'))); + } + + const std::tm PCF85063::datetime2tm(const datetime_t& datetime) { tm dtime; dtime.tm_sec = datetime.second; dtime.tm_min = datetime.minute; @@ -188,8 +194,7 @@ namespace drivers dtime.tm_mday = datetime.day; dtime.tm_mon = datetime.month - 1; dtime.tm_year = datetime.year - 1900; // time offset in structure according cpp reference - const std::string buf(std::asctime(&dtime)); - return buf.substr(0, std::min(buf.find('\n'),buf.find('\r'))); + return dtime; } const uint8_t PCF85063::decToBcd(const int val) diff --git a/lib/RTC/PCF85063_Driver.h b/lib/RTC/PCF85063_Driver.h index b527da6..97d8cc2 100644 --- a/lib/RTC/PCF85063_Driver.h +++ b/lib/RTC/PCF85063_Driver.h @@ -68,8 +68,6 @@ namespace drivers class PCF85063 { - I2C &m_i2c; - uint8_t m_address; public: typedef struct @@ -102,13 +100,18 @@ namespace drivers const bool getAlarmFlag(uint8_t &flags); const std::string getTimeStr(); - - static const std::string datetime2str(const datetime_t &datetime); + + static const std::string datetime2str(const datetime_t &datetime); + static const std::tm datetime2tm(const datetime_t& datetime); static const PCF85063::datetime_t fromEpoch(const time_t currentTime); private: const uint8_t decToBcd(const int val); const int bcdToDec(const uint8_t val); + + private: + I2C &m_i2c; + uint8_t m_address; }; } diff --git a/src/commands.h b/src/commands.h index b56c63d..4ed206a 100644 --- a/src/commands.h +++ b/src/commands.h @@ -56,6 +56,9 @@ namespace commands static TimerHandle_t s_irrigationPumpTimer = NULL; + // define command callback type + using Command = std::function; + class Commands { Commands() = delete; @@ -86,7 +89,7 @@ namespace commands static const ArduinoJson::JsonDocument getIrrigation(const devices_t &dev, const ArduinoJson::JsonDocument ¶ms); }; - static const std::map> commandMap = { + static const std::map commandMap = { {"setConfig", Commands::setConfig}, {"getConfig", Commands::getConfig}, diff --git a/src/cronjobs.cpp b/src/cronjobs.cpp new file mode 100644 index 0000000..731823c --- /dev/null +++ b/src/cronjobs.cpp @@ -0,0 +1,139 @@ +#include +#include + +#define STACK_DEPTH 4096 +#define PRIORITY 3 + +Cron::Cron(devices_t &dev) : m_dev(dev) +{ +} + +Cron::~Cron() +{ +} + +const bool Cron::loadEvents(fs::File &file) +{ + return true; +} + +const bool Cron::addEvent(const std::string &name, const std::string &command, const std::string &expr) +{ + if (m_cronMap.contains(name)) + { + LOG_ERROR("Cron event [", name.c_str(), "] already scheduled"); + return false; + } + if (name.empty() || command.empty() || expr.empty()) + { + LOG_ERROR("Cron event invalid parameters"); + return false; + } + + try + { + const auto eventExpr(cron::make_cron(expr)); + ArduinoJson::JsonDocument action; + if (ArduinoJson::deserializeJson(action, command) != ArduinoJson::DeserializationError::Ok) + { + LOG_ERROR("Cron unable to deserialize command [", command.c_str(), "]"); + return false; + } + const auto cmd = action["cmd"].as(); + const auto params = action["params"].as(); + if (!commands::commandMap.contains(cmd)) + { + LOG_ERROR("Cron unknown command [", command.c_str(), "]"); + return false; + } + LOG_INFO("Cron added event [", name.c_str(), "]"); + drivers::PCF85063::datetime_t now; + if (!m_dev.rtc.readDatetime(now)) + { + LOG_ERROR("Cron unable to update current time"); + return false; + } + std::tm nowTm = drivers::PCF85063::datetime2tm(now); + m_cronMap[name] = std::make_tuple(cmd, eventExpr, cron::cron_next(eventExpr, nowTm), params); + } + catch (cron::bad_cronexpr const &ex) + { + LOG_ERROR("Cron failed to parse expression [", expr.c_str(), "] ->", ex.what()); + return false; + } + return true; +} + +const bool Cron::delEvent(const std::string &name) +{ + if (!m_cronMap.contains(name)) + { + LOG_WARN("Cron event [", name.c_str(), "] does not exist"); + return false; + } + m_cronMap.erase(name); + LOG_INFO("Cron removed event [", name.c_str(), "]"); + return true; +} + +void cronLoop(void* params) { + auto cron = (Cron*)(params); + while (true) { + cron->processEvents(); + delay(1000); + } +} + +void Cron::startCron() { + if (!m_cronTaskHandle) { + LOG_INFO("Cron starting loop"); + xTaskCreate(cronLoop, "cronLoop", STACK_DEPTH, this, PRIORITY, &m_cronTaskHandle); + } +} + +void Cron::stopCron() { + if (m_cronTaskHandle) { + LOG_WARN("Cron stopping loop"); + vTaskDelete(m_cronTaskHandle); + m_cronTaskHandle = NULL; + } +} + +const bool Cron::processEvents() +{ + LOG_DEBUG("Cron processEvents [", m_cronMap.size(), "]"); + + drivers::PCF85063::datetime_t now; + if (!m_dev.rtc.readDatetime(now)) + { + LOG_ERROR("Cron unable to update current time"); + return false; + } + + std::tm nowTm = drivers::PCF85063::datetime2tm(now); + + for (auto &event : m_cronMap) + { + auto &eventName = event.first; + auto &eventAction = event.second; + + auto &cmd = std::get<0>(eventAction); + auto &cronexrp = std::get<1>(eventAction); + auto &next = std::get<2>(eventAction); + auto ¶ms = std::get<3>(eventAction); + + const auto nowPoint = std::chrono::system_clock::from_time_t(std::mktime(&nowTm)); + const auto nextEventPoint = std::chrono::system_clock::from_time_t(std::mktime(&next)); + + LOG_DEBUG("Cron current time [", std::asctime(&nowTm), "]"); + LOG_DEBUG("Cron checking event [", eventName.c_str(), "] executionTime [", std::asctime(&next), "]"); + + if (nextEventPoint <= nowPoint) // execution time hs passed, run event + { + LOG_INFO("Cron executing event [", eventName.c_str(), "]"); + commands::commandMap.at(cmd)(m_dev, params); // here the magic happens + next = cron::cron_next(cronexrp, nowTm); // update next execution time only if event was executed + } // otherwise time tracking is lost + } + return true; +} \ No newline at end of file diff --git a/src/cronjobs.h b/src/cronjobs.h new file mode 100644 index 0000000..a8292f5 --- /dev/null +++ b/src/cronjobs.h @@ -0,0 +1,34 @@ +#pragma once + +#define DEBUGLOG_DEFAULT_LOG_LEVEL_DEBUG + +#include +#include + +#include + +#include + +#include +#include + +class Cron +{ + +public: + Cron(devices_t &dev); + ~Cron(); + + const bool loadEvents(fs::File &file); + const bool addEvent(const std::string &name, const std::string &command, const std::string &expr); + const bool delEvent(const std::string &name); + + void startCron(); + void stopCron(); + const bool processEvents(); + +private: + devices_t &m_dev; + TaskHandle_t m_cronTaskHandle; + std::map> m_cronMap; +}; \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 7738bbb..626467f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include @@ -53,6 +54,24 @@ void loop() //////////////// NETWORK //////////////// auto mqtt = MQTTwrapper(); //////////////// NETWORK //////////////// + + + //////////////// CRONJOB //////////////// + auto cron = Cron(devices); + ArduinoJson::JsonDocument job; + job["cmd"] = "setIrrigation"; + job["params"]["zone"] = "zone1"; + job["params"]["timeOn"] = 30; + job["params"]["timePause"] = 2; + + { + std::string buf; + serializeJson(job,buf); + LOG_INFO("Example Cronjob -> ", buf.c_str()); + cron.addEvent("exampleCronEvent", buf, "0 */10 * * * *"); + cron.startCron(); + }; + //////////////// CRONJOB //////////////// //////////////// MQTT //////////////// /////////////// CALLBACK ////////////// diff --git a/src/mqtt.cpp b/src/mqtt.cpp index 4cfe363..2f59559 100644 --- a/src/mqtt.cpp +++ b/src/mqtt.cpp @@ -2,7 +2,7 @@ #define STACK_DEPTH 8192 #define BUFFER_SIZE 2048 -#define PRIOTITY 2 +#define PRIORITY 2 MQTTwrapper::MQTTwrapper() : m_config(Config::getInstance()), m_tcp(NetworkClient()), m_client(PubSubClient(m_tcp)), m_loopHandle(NULL) { @@ -27,7 +27,7 @@ const bool MQTTwrapper::connect() LOG_INFO("MQTT client connected to", m_config.m_mqttHost.c_str()); if (m_loopHandle == NULL) { - xTaskCreate(clientLoop, "mqttLoop", STACK_DEPTH, this, PRIOTITY, &m_loopHandle); + xTaskCreate(clientLoop, "mqttLoop", STACK_DEPTH, this, PRIORITY, &m_loopHandle); m_client.setCallback(MQTTwrapper::callback); } return true;