diff --git a/data_svelte/items.json b/data_svelte/items.json index 435182da..8f52d3ce 100644 --- a/data_svelte/items.json +++ b/data_svelte/items.json @@ -8,28 +8,43 @@ }, { "global": 0, - "name": "1. График", + "name": "1. Будильник (Cron)", + "type": "Writing", + "subtype": "Cron", + "id": "cron", + "widget": "anydataDef", + "page": "Таймеры", + "descr": "Будильник", + "int": 1, + "val": "*/15 * * * * *", + "formatNextAlarm": "dd.mm.yy hh:mm:ss", + "needSave": 0, + "num": 1 + }, + { + "global": 0, + "name": "2. График", "type": "Writing", "subtype": "Loging", "id": "log", "widget": "chart2", "page": "Графики", "descr": "Температура", - "num": 1, + "num": 2, "int": 5, "logid": "t", "points": 300 }, { "global": 0, - "name": "2. График дневного расхода", + "name": "3. График дневного расхода", "type": "Writing", "subtype": "LogingDaily", "id": "log", "widget": "chart3", "page": "Графики", "descr": "Температура", - "num": 2, + "num": 3, "int": 1, "logid": "t", "points": 365, @@ -37,7 +52,7 @@ }, { "global": 0, - "name": "3. Таймер", + "name": "4. Таймер", "type": "Writing", "subtype": "Timer", "id": "timer", @@ -49,11 +64,11 @@ "ticker": 1, "repeat": 1, "needSave": 0, - "num": 3 + "num": 4 }, { "global": 0, - "name": "4. Окно ввода числа (переменная)", + "name": "5. Окно ввода числа (переменная)", "type": "Reading", "subtype": "Variable", "id": "value", @@ -63,11 +78,11 @@ "descr": "Введите число", "int": "0", "val": "0.0", - "num": 4 + "num": 5 }, { "global": 0, - "name": "5. Окно ввода времени", + "name": "6. Окно ввода времени", "type": "Reading", "subtype": "Variable", "id": "time", @@ -77,11 +92,11 @@ "descr": "Введите время", "int": "0", "val": "02:00", - "num": 5 + "num": 6 }, { "global": 0, - "name": "6. Окно ввода даты", + "name": "7. Окно ввода даты", "type": "Reading", "subtype": "Variable", "id": "time", @@ -91,11 +106,11 @@ "descr": "Введите дату", "int": "0", "val": "24.05.2022", - "num": 6 + "num": 7 }, { "global": 0, - "name": "7. Окно ввода текста", + "name": "8. Окно ввода текста", "type": "Reading", "subtype": "Variable", "id": "txt", @@ -105,11 +120,11 @@ "descr": "Введите текст", "int": "0", "val": "текст", - "num": 7 + "num": 8 }, { "global": 0, - "name": "8. Виртуальная кнопка", + "name": "9. Виртуальная кнопка", "type": "Reading", "subtype": "VButton", "id": "vbtn", @@ -119,13 +134,13 @@ "descr": "Кнопка", "int": "0", "val": "0", - "num": 8 + "num": 9 }, { "header": "Сенсоры" }, { - "name": "9. Acs712 Ток", + "name": "10. Acs712 Ток", "type": "Reading", "subtype": "Acs712", "id": "amp", @@ -135,11 +150,11 @@ "round": 3, "pin": 39, "int": 5, - "num": 9 + "num": 10 }, { "global": 0, - "name": "10. AHTXX Температура", + "name": "11. AHTXX Температура", "type": "Reading", "subtype": "AhtXXt", "id": "Temp20", @@ -150,11 +165,11 @@ "addr": "0x38", "shtType": 1, "round": 1, - "num": 10 + "num": 11 }, { "global": 0, - "name": "11. AHTXX Влажность", + "name": "12. AHTXX Влажность", "type": "Reading", "subtype": "AhtXXh", "id": "Hum20", @@ -165,11 +180,11 @@ "addr": "0x38", "shtType": 1, "round": 1, - "num": 11 + "num": 12 }, { "global": 0, - "name": "12. Аналоговый сенсор", + "name": "13. Аналоговый сенсор", "type": "Reading", "subtype": "AnalogAdc", "id": "t", @@ -183,11 +198,11 @@ "pin": 0, "int": 15, "avgSteps": 1, - "num": 12 + "num": 13 }, { "global": 0, - "name": "13. BME280 Температура", + "name": "14. BME280 Температура", "type": "Reading", "subtype": "Bme280t", "id": "tmp3", @@ -197,11 +212,11 @@ "int": 15, "addr": "0x77", "round": 1, - "num": 13 + "num": 14 }, { "global": 0, - "name": "14. BME280 Давление", + "name": "15. BME280 Давление", "type": "Reading", "subtype": "Bme280p", "id": "Press3", @@ -211,11 +226,11 @@ "int": 15, "addr": "0x77", "round": 1, - "num": 14 + "num": 15 }, { "global": 0, - "name": "15. BME280 Влажность", + "name": "16. BME280 Влажность", "type": "Reading", "subtype": "Bme280h", "id": "Hum3", @@ -225,11 +240,11 @@ "int": 15, "addr": "0x77", "round": 1, - "num": 15 + "num": 16 }, { "global": 0, - "name": "16. BMP280 Температура", + "name": "17. BMP280 Температура", "type": "Reading", "subtype": "Bmp280t", "id": "tmp3", @@ -239,11 +254,11 @@ "int": 15, "addr": "0x77", "round": 1, - "num": 16 + "num": 17 }, { "global": 0, - "name": "17. BMP280 Давление", + "name": "18. BMP280 Давление", "type": "Reading", "subtype": "Bmp280p", "id": "Press3", @@ -253,11 +268,11 @@ "int": 15, "addr": "0x77", "round": 1, - "num": 17 + "num": 18 }, { "global": 0, - "name": "18. DHT11 Температура", + "name": "19. DHT11 Температура", "type": "Reading", "subtype": "Dht1122t", "id": "tmp3", @@ -267,11 +282,11 @@ "int": 15, "pin": 0, "senstype": "dht11", - "num": 18 + "num": 19 }, { "global": 0, - "name": "19. DHT11 Влажность", + "name": "20. DHT11 Влажность", "type": "Reading", "subtype": "Dht1122h", "id": "Hum3", @@ -281,11 +296,11 @@ "int": 15, "pin": 0, "senstype": "dht11", - "num": 19 + "num": 20 }, { "global": 0, - "name": "20. DS18B20 Температура", + "name": "21. DS18B20 Температура", "type": "Reading", "subtype": "Ds18b20", "id": "dstmp", @@ -297,11 +312,11 @@ "index": 0, "addr": "", "round": 1, - "num": 20 + "num": 21 }, { "global": 0, - "name": "21. GY21 Температура", + "name": "22. GY21 Температура", "type": "Reading", "subtype": "GY21t", "id": "tmp4", @@ -310,11 +325,11 @@ "descr": "Температура", "round": 1, "int": 15, - "num": 21 + "num": 22 }, { "global": 0, - "name": "22. GY21 Влажность", + "name": "23. GY21 Влажность", "type": "Reading", "subtype": "GY21h", "id": "Hum4", @@ -323,11 +338,11 @@ "descr": "Влажность", "round": 1, "int": 15, - "num": 22 + "num": 23 }, { "global": 0, - "name": "23. HDC1080 Температура", + "name": "24. HDC1080 Температура", "type": "Reading", "subtype": "Hdc1080t", "id": "Temp1080", @@ -337,11 +352,11 @@ "int": 15, "addr": "0x40", "round": 1, - "num": 23 + "num": 24 }, { "global": 0, - "name": "24. HDC1080 Влажность", + "name": "25. HDC1080 Влажность", "type": "Reading", "subtype": "Hdc1080h", "id": "Hum1080", @@ -351,11 +366,11 @@ "int": 15, "addr": "0x40", "round": 1, - "num": 24 + "num": 25 }, { "global": 0, - "name": "25. MAX6675 Температура", + "name": "26. MAX6675 Температура", "type": "Reading", "subtype": "Max6675t", "id": "maxtmp", @@ -366,11 +381,11 @@ "DO": 12, "CS": 13, "CLK": 14, - "num": 25 + "num": 26 }, { "global": 0, - "name": "26. PZEM 004t Напряжение", + "name": "27. PZEM 004t Напряжение", "type": "Reading", "subtype": "Pzem004v", "id": "v", @@ -380,11 +395,11 @@ "int": 15, "addr": "0xF8", "round": 1, - "num": 26 + "num": 27 }, { "global": 0, - "name": "27. PZEM 004t Сила тока", + "name": "28. PZEM 004t Сила тока", "type": "Reading", "subtype": "Pzem004a", "id": "a", @@ -394,11 +409,11 @@ "int": 15, "addr": "0xF8", "round": 1, - "num": 27 + "num": 28 }, { "global": 0, - "name": "28. PZEM 004t Мощность", + "name": "29. PZEM 004t Мощность", "type": "Reading", "subtype": "Pzem004w", "id": "w", @@ -408,11 +423,11 @@ "int": 15, "addr": "0xF8", "round": 1, - "num": 28 + "num": 29 }, { "global": 0, - "name": "29. PZEM 004t Энергия", + "name": "30. PZEM 004t Энергия", "type": "Reading", "subtype": "Pzem004wh", "id": "wh", @@ -422,11 +437,11 @@ "int": 15, "addr": "0xF8", "round": 1, - "num": 29 + "num": 30 }, { "global": 0, - "name": "30. PZEM 004t Частота", + "name": "31. PZEM 004t Частота", "type": "Reading", "subtype": "Pzem004hz", "id": "hz", @@ -436,11 +451,11 @@ "int": 15, "addr": "0xF8", "round": 1, - "num": 30 + "num": 31 }, { "global": 0, - "name": "31. PZEM 004t Косинус", + "name": "32. PZEM 004t Косинус", "type": "Reading", "subtype": "Pzem004pf", "id": "pf", @@ -450,12 +465,12 @@ "int": 15, "addr": "0xF8", "round": 1, - "num": 31 + "num": 32 }, { "global": 0, - "name": "32. Сканер кнопок 433 MHz", - "num": 32, + "name": "33. Сканер кнопок 433 MHz", + "num": 33, "type": "Reading", "subtype": "RCswitch", "id": "rsw", @@ -465,7 +480,7 @@ }, { "global": 0, - "name": "33. Sht20 Температура", + "name": "34. Sht20 Температура", "type": "Reading", "subtype": "Sht20t", "id": "tmp2", @@ -474,11 +489,11 @@ "descr": "Температура", "int": 15, "round": 1, - "num": 33 + "num": 34 }, { "global": 0, - "name": "34. Sht20 Влажность", + "name": "35. Sht20 Влажность", "type": "Reading", "subtype": "Sht20h", "id": "Hum2", @@ -487,11 +502,11 @@ "descr": "Влажность", "int": 15, "round": 1, - "num": 34 + "num": 35 }, { "global": 0, - "name": "35. Sht30 Температура", + "name": "36. Sht30 Температура", "type": "Reading", "subtype": "Sht30t", "id": "tmp30", @@ -500,11 +515,11 @@ "descr": "SHT30 Температура", "int": 15, "round": 1, - "num": 35 + "num": 36 }, { "global": 0, - "name": "36. Sht30 Влажность", + "name": "37. Sht30 Влажность", "type": "Reading", "subtype": "Sht30h", "id": "Hum30", @@ -513,12 +528,12 @@ "descr": "SHT30 Влажность", "int": 15, "round": 1, - "num": 36 + "num": 37 }, { "global": 0, - "name": "37. HC-SR04 Ультразвуковой дальномер", - "num": 37, + "name": "38. HC-SR04 Ультразвуковой дальномер", + "num": 38, "type": "Reading", "subtype": "Sonar", "id": "sonar", @@ -531,7 +546,7 @@ }, { "global": 0, - "name": "38. UART", + "name": "39. UART", "type": "Reading", "subtype": "UART", "page": "", @@ -541,14 +556,14 @@ "tx": 12, "rx": 13, "speed": 9600, - "num": 38 + "num": 39 }, { "header": "Исполнительные устройства" }, { "global": 0, - "name": "39. Кнопка подключенная к пину", + "name": "40. Кнопка подключенная к пину", "type": "Writing", "subtype": "ButtonIn", "id": "btn", @@ -562,11 +577,11 @@ "pinMode": "INPUT", "debounceDelay": 50, "fixState": 0, - "num": 39 + "num": 40 }, { "global": 0, - "name": "40. Управление пином", + "name": "41. Управление пином", "type": "Writing", "subtype": "ButtonOut", "needSave": 0, @@ -577,11 +592,11 @@ "int": 0, "inv": 0, "pin": 2, - "num": 40 + "num": 41 }, { "global": 0, - "name": "41. Сервопривод", + "name": "42. Сервопривод", "type": "Writing", "subtype": "IoTServo", "id": "servo", @@ -592,11 +607,11 @@ "pin": 12, "apin": -1, "amap": "0, 4096, 0, 180", - "num": 41 + "num": 42 }, { "global": 0, - "name": "42. Расширитель портов Mcp23017", + "name": "43. Расширитель портов Mcp23017", "type": "Reading", "subtype": "Mcp23017", "id": "Mcp", @@ -606,11 +621,11 @@ "int": "0", "addr": "0x20", "index": 1, - "num": 42 + "num": 43 }, { "global": 0, - "name": "43. MP3 плеер", + "name": "44. MP3 плеер", "type": "Reading", "subtype": "Mp3", "id": "mp3", @@ -620,11 +635,11 @@ "int": 1, "pins": "14,12", "volume": 20, - "num": 43 + "num": 44 }, { "global": 0, - "name": "44. Расширитель портов Pcf8574", + "name": "45. Расширитель портов Pcf8574", "type": "Reading", "subtype": "Pcf8574", "id": "Pcf", @@ -634,11 +649,11 @@ "int": "0", "addr": "0x20", "index": 1, - "num": 44 + "num": 45 }, { "global": 0, - "name": "45. PWM ESP8266", + "name": "46. PWM ESP8266", "type": "Writing", "subtype": "Pwm8266", "id": "pwm", @@ -650,11 +665,11 @@ "freq": 5000, "val": 0, "apin": -1, - "num": 45 + "num": 46 }, { "global": 0, - "name": "46. Телеграм-Лайт", + "name": "47. Телеграм-Лайт", "type": "Writing", "subtype": "TelegramLT", "id": "tg", @@ -663,14 +678,14 @@ "descr": "", "token": "", "chatID": "", - "num": 46 + "num": 47 }, { "header": "Экраны" }, { "global": 0, - "name": "47. LCD экран 2004", + "name": "48. LCD экран 2004", "type": "Reading", "subtype": "Lcd2004", "id": "Lcd", @@ -682,10 +697,10 @@ "size": "20,4", "coord": "0,0", "id2show": "id датчика", - "num": 47 + "num": 48 }, { - "name": "48. LCD экран 1602", + "name": "49. LCD экран 1602", "type": "Reading", "subtype": "Lcd2004", "id": "Lcd", @@ -697,11 +712,11 @@ "size": "16,2", "coord": "0,0", "id2show": "id датчика", - "num": 48 + "num": 49 }, { "global": 0, - "name": "49. Strip ws2812b", + "name": "50. Strip ws2812b", "type": "Reading", "subtype": "Ws2812b", "id": "strip", @@ -717,6 +732,6 @@ "min": "15", "max": "30", "idshow": "t", - "num": 49 + "num": 50 } ] \ No newline at end of file diff --git a/platformio.ini b/platformio.ini index fc750421..c68e8c20 100644 --- a/platformio.ini +++ b/platformio.ini @@ -158,6 +158,7 @@ lib_deps = marcoschwartz/LiquidCrystal_I2C@^1.1.4 adafruit/Adafruit NeoPixel@^1.10.6 build_src_filter = + + + + + @@ -210,6 +211,7 @@ lib_deps = dfrobot/DFRobotDFPlayerMini @ ^1.0.5 adafruit/Adafruit BusIO @ ^1.13.2 marcoschwartz/LiquidCrystal_I2C@^1.1.4 + adafruit/Adafruit NeoPixel@^1.10.6 build_src_filter = + + @@ -241,4 +243,5 @@ build_src_filter = + + + + + diff --git a/src/modules/API.cpp b/src/modules/API.cpp index 7523aff7..5756874b 100644 --- a/src/modules/API.cpp +++ b/src/modules/API.cpp @@ -1,5 +1,6 @@ #include "ESPConfiguration.h" +void* getAPI_Cron(String subtype, String params); void* getAPI_Loging(String subtype, String params); void* getAPI_LogingDaily(String subtype, String params); void* getAPI_Timer(String subtype, String params); @@ -34,6 +35,7 @@ void* getAPI_Ws2812b(String subtype, String params); void* getAPI(String subtype, String params) { void* tmpAPI; +if ((tmpAPI = getAPI_Cron(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_Loging(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_LogingDaily(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_Timer(subtype, params)) != nullptr) return tmpAPI; diff --git a/src/modules/virtual/Cron/Cron.cpp b/src/modules/virtual/Cron/Cron.cpp new file mode 100644 index 00000000..2c24eb59 --- /dev/null +++ b/src/modules/virtual/Cron/Cron.cpp @@ -0,0 +1,111 @@ +#include "NTP.h" +#include "Global.h" +#include "classes/IoTItem.h" + +extern "C" { +#include "ccronexpr/ccronexpr.h" +} + + +class Cron : public IoTItem { + private: + bool _pause = false; + String _format = ""; + cron_expr _expr; + time_t _nextAlarm = 0; + + public: + Cron(String parameters): IoTItem(parameters) { + jsonRead(parameters, F("formatNextAlarm"), _format); + initCron(); + } + + void initCron() { + const char* err = NULL; + memset(&_expr, 0, sizeof(_expr)); + + cron_parse_expr(value.valS.c_str(), &_expr, &err); + if (err) { + _pause = true; + _nextAlarm = 0; + memset(&_expr, 0, sizeof(_expr)); + SerialPrint("E", "Cron", F("The Cron string did not apply."), _id); + } else + updateNextAlarm(true); + } + + void updateNextAlarm(bool forced) { + if (!_pause && _time_isTrust) { + if (forced || (_nextAlarm <= gmtTimeToLocal(unixTime))) { + // update alarm if next trigger is not yet in the future + _nextAlarm = cron_next(&_expr, gmtTimeToLocal(unixTime)); + } + } + } + + String getNextAlarmF() { + if (_pause) return "Pause"; + if (!_time_isTrust) return "No time"; + struct tm* timeinfo; + char buffer [80]; + timeinfo = localtime(&_nextAlarm); + strftime(buffer, 80, _format.c_str(), timeinfo); + return buffer; + } + + String getValue() { + return getNextAlarmF(); + } + + void setValue(const IoTValue& Value, bool genEvent) { + value = Value; + _pause = false; + initCron(); + + if (_needSave) { + jsonWriteStr_(valuesFlashJson, _id, value.valS); + needSaveValues = true; + } + + bool _needSaveBak = _needSave; + _needSave = false; + regEvent(getNextAlarmF(), F("Cron alarm"), false, false); + _needSave = _needSaveBak; + } + + void doByInterval() { + if (!_pause && _time_isTrust && (gmtTimeToLocal(unixTime) >= _nextAlarm)) { + updateNextAlarm(true); + bool _needSaveBak = _needSave; + _needSave = false; + regEvent(getNextAlarmF(), F("Cron alarm")); + _needSave = _needSaveBak; + } + } + + IoTValue execute(String command, std::vector ¶m) { + if (command == "stop") { + _pause = true; + } else if (command == "continue") { + _pause = false; + updateNextAlarm(false); + } + + bool _needSaveBak = _needSave; + _needSave = false; + regEvent(getNextAlarmF(), F("Cron alarm"), false, false); + _needSave = _needSaveBak; + + return {}; + } + + ~Cron() {}; +}; + +void* getAPI_Cron(String subtype, String param) { + if (subtype == F("Cron")) { + return new Cron(param); + } else { + return nullptr; + } +} diff --git a/src/modules/virtual/Cron/ccronexpr/ccronexpr.c b/src/modules/virtual/Cron/ccronexpr/ccronexpr.c new file mode 100644 index 00000000..d9715ff7 --- /dev/null +++ b/src/modules/virtual/Cron/ccronexpr/ccronexpr.c @@ -0,0 +1,1272 @@ +/* + * Copyright 2015, alex at staticlibs.net + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * File: ccronexpr.c + * Author: alex + * + * Created on February 24, 2015, 9:35 AM + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "ccronexpr.h" + +#define CRON_MAX_SECONDS 60 +#define CRON_MAX_MINUTES 60 +#define CRON_MAX_HOURS 24 +#define CRON_MAX_DAYS_OF_WEEK 8 +#define CRON_MAX_DAYS_OF_MONTH 32 +#define CRON_MAX_MONTHS 12 +#define CRON_MAX_YEARS_DIFF 4 + +#define CRON_CF_SECOND 0 +#define CRON_CF_MINUTE 1 +#define CRON_CF_HOUR_OF_DAY 2 +#define CRON_CF_DAY_OF_WEEK 3 +#define CRON_CF_DAY_OF_MONTH 4 +#define CRON_CF_MONTH 5 +#define CRON_CF_YEAR 6 + +#define CRON_CF_ARR_LEN 7 + +#define CRON_INVALID_INSTANT ((time_t) -1) + +static const char* const DAYS_ARR[] = { "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT" }; +#define CRON_DAYS_ARR_LEN 7 +static const char* const MONTHS_ARR[] = { "FOO", "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" }; +#define CRON_MONTHS_ARR_LEN 13 + +#define CRON_MAX_STR_LEN_TO_SPLIT 256 +#define CRON_MAX_NUM_TO_SRING 1000000000 +/* computes number of digits in decimal number */ +#define CRON_NUM_OF_DIGITS(num) (abs(num) < 10 ? 1 : \ + (abs(num) < 100 ? 2 : \ + (abs(num) < 1000 ? 3 : \ + (abs(num) < 10000 ? 4 : \ + (abs(num) < 100000 ? 5 : \ + (abs(num) < 1000000 ? 6 : \ + (abs(num) < 10000000 ? 7 : \ + (abs(num) < 100000000 ? 8 : \ + (abs(num) < 1000000000 ? 9 : 10))))))))) + +#ifndef CRON_TEST_MALLOC +#define cron_malloc(x) malloc(x); +#define cron_free(x) free(x); +#else /* CRON_TEST_MALLOC */ +void* cron_malloc(size_t n); +void cron_free(void* p); +#endif /* CRON_TEST_MALLOC */ + +/** + * Time functions from standard library. + * This part defines: cron_mktime: create time_t from tm + * cron_time: create tm from time_t + */ + +/* forward declarations for platforms that may need them */ +/* can be hidden in time.h */ +#if !defined(_WIN32) && !defined(__AVR__) && !defined(ESP8266) && !defined(ANDROID) +struct tm *gmtime_r(const time_t *timep, struct tm *result); +time_t timegm(struct tm* __tp); +struct tm *localtime_r(const time_t *timep, struct tm *result); +#endif /* PLEASE CHECK _WIN32 AND ANDROID NEEDS FOR THESE DECLARATIONS */ +#ifdef __MINGW32__ +/* To avoid warning when building with mingw */ +time_t _mkgmtime(struct tm* tm); +#endif /* __MINGW32__ */ + +/* function definitions */ +time_t cron_mktime_gm(struct tm* tm) { +#if defined(_WIN32) +/* http://stackoverflow.com/a/22557778 */ + return _mkgmtime(tm); +#elif defined(__AVR__) +/* https://www.nongnu.org/avr-libc/user-manual/group__avr__time.html */ + return mk_gmtime(tm); +#elif defined(ESP8266) + /* https://linux.die.net/man/3/timegm */ + /* http://www.catb.org/esr/time-programming/ */ + /* portable version of timegm() */ + time_t ret; + char *tz; + tz = getenv("TZ"); + if (tz) + tz = strdup(tz); + setenv("TZ", "UTC+0", 1); + tzset(); + ret = mktime(tm); + if (tz) { + setenv("TZ", tz, 1); + free(tz); + } else + unsetenv("TZ"); + tzset(); + return ret; +#elif defined(ANDROID) + /* https://github.com/adobe/chromium/blob/cfe5bf0b51b1f6b9fe239c2a3c2f2364da9967d7/base/os_compat_android.cc#L20 */ + static const time_t kTimeMax = ~(1L << (sizeof (time_t) * CHAR_BIT - 1)); + static const time_t kTimeMin = (1L << (sizeof (time_t) * CHAR_BIT - 1)); + time64_t result = timegm64(tm); + if (result < kTimeMin || result > kTimeMax) return -1; + return result; +#else + return timegm(tm); +#endif +} + +struct tm* cron_time_gm(time_t* date, struct tm* out) { +#if defined(__MINGW32__) + (void)(out); /* To avoid unused warning */ + return gmtime(date); +#elif defined(_WIN32) + errno_t err = gmtime_s(out, date); + return 0 == err ? out : NULL; +#elif defined(__AVR__) + /* https://www.nongnu.org/avr-libc/user-manual/group__avr__time.html */ + gmtime_r(date, out); + return out; +#else + return gmtime_r(date, out); +#endif +} + +time_t cron_mktime_local(struct tm* tm) { + tm->tm_isdst = -1; + return mktime(tm); +} + +struct tm* cron_time_local(time_t* date, struct tm* out) { +#if defined(_WIN32) + errno_t err = localtime_s(out, date); + return 0 == err ? out : NULL; +#elif defined(__AVR__) + /* https://www.nongnu.org/avr-libc/user-manual/group__avr__time.html */ + localtime_r(date, out); + return out; +#else + return localtime_r(date, out); +#endif +} + +/* Defining 'cron_' time functions to use use UTC (default) or local time */ +#ifndef CRON_USE_LOCAL_TIME +time_t cron_mktime(struct tm* tm) { + return cron_mktime_gm(tm); +} + +struct tm* cron_time(time_t* date, struct tm* out) { + return cron_time_gm(date, out); +} + +#else /* CRON_USE_LOCAL_TIME */ +time_t cron_mktime(struct tm* tm) { + return cron_mktime_local(tm); +} + +struct tm* cron_time(time_t* date, struct tm* out) { + return cron_time_local(date, out); +} + +#endif /* CRON_USE_LOCAL_TIME */ + +/** + * Functions. + */ + +void cron_set_bit(uint8_t* rbyte, int idx) { + uint8_t j = (uint8_t) (idx / 8); + uint8_t k = (uint8_t) (idx % 8); + + rbyte[j] |= (1 << k); +} + +void cron_del_bit(uint8_t* rbyte, int idx) { + uint8_t j = (uint8_t) (idx / 8); + uint8_t k = (uint8_t) (idx % 8); + + rbyte[j] &= ~(1 << k); +} + +uint8_t cron_get_bit(uint8_t* rbyte, int idx) { + uint8_t j = (uint8_t) (idx / 8); + uint8_t k = (uint8_t) (idx % 8); + + if (rbyte[j] & (1 << k)) { + return 1; + } else { + return 0; + } +} + +static void free_splitted(char** splitted, size_t len) { + size_t i; + if (!splitted) return; + for (i = 0; i < len; i++) { + if (splitted[i]) { + cron_free(splitted[i]); + } + } + cron_free(splitted); +} + +static char* strdupl(const char* str, size_t len) { + if (!str) return NULL; + char* res = (char*) cron_malloc(len + 1); + if (!res) return NULL; + memset(res, 0, len + 1); + memcpy(res, str, len); + return res; +} + +static unsigned int next_set_bit(uint8_t* bits, unsigned int max, unsigned int from_index, int* notfound) { + unsigned int i; + if (!bits) { + *notfound = 1; + return 0; + } + for (i = from_index; i < max; i++) { + if (cron_get_bit(bits, i)) return i; + } + *notfound = 1; + return 0; +} + +static void push_to_fields_arr(int* arr, int fi) { + int i; + if (!arr || -1 == fi) { + return; + } + for (i = 0; i < CRON_CF_ARR_LEN; i++) { + if (arr[i] == fi) return; + } + for (i = 0; i < CRON_CF_ARR_LEN; i++) { + if (-1 == arr[i]) { + arr[i] = fi; + return; + } + } +} + +static int add_to_field(struct tm* calendar, int field, int val) { + if (!calendar || -1 == field) { + return 1; + } + switch (field) { + case CRON_CF_SECOND: + calendar->tm_sec = calendar->tm_sec + val; + break; + case CRON_CF_MINUTE: + calendar->tm_min = calendar->tm_min + val; + break; + case CRON_CF_HOUR_OF_DAY: + calendar->tm_hour = calendar->tm_hour + val; + break; + case CRON_CF_DAY_OF_WEEK: /* mkgmtime ignores this field */ + case CRON_CF_DAY_OF_MONTH: + calendar->tm_mday = calendar->tm_mday + val; + break; + case CRON_CF_MONTH: + calendar->tm_mon = calendar->tm_mon + val; + break; + case CRON_CF_YEAR: + calendar->tm_year = calendar->tm_year + val; + break; + default: + return 1; /* unknown field */ + } + time_t res = cron_mktime(calendar); + if (CRON_INVALID_INSTANT == res) { + return 1; + } + return 0; +} + +/** + * Reset the calendar setting all the fields provided to zero. + */ +static int reset_min(struct tm* calendar, int field) { + if (!calendar || -1 == field) { + return 1; + } + switch (field) { + case CRON_CF_SECOND: + calendar->tm_sec = 0; + break; + case CRON_CF_MINUTE: + calendar->tm_min = 0; + break; + case CRON_CF_HOUR_OF_DAY: + calendar->tm_hour = 0; + break; + case CRON_CF_DAY_OF_WEEK: + calendar->tm_wday = 0; + break; + case CRON_CF_DAY_OF_MONTH: + calendar->tm_mday = 1; + break; + case CRON_CF_MONTH: + calendar->tm_mon = 0; + break; + case CRON_CF_YEAR: + calendar->tm_year = 0; + break; + default: + return 1; /* unknown field */ + } + time_t res = cron_mktime(calendar); + if (CRON_INVALID_INSTANT == res) { + return 1; + } + return 0; +} + +static int reset_all_min(struct tm* calendar, int* fields) { + int i; + int res = 0; + if (!calendar || !fields) { + return 1; + } + for (i = 0; i < CRON_CF_ARR_LEN; i++) { + if (-1 != fields[i]) { + res = reset_min(calendar, fields[i]); + if (0 != res) return res; + } + } + return 0; +} + +static int set_field(struct tm* calendar, int field, int val) { + if (!calendar || -1 == field) { + return 1; + } + switch (field) { + case CRON_CF_SECOND: + calendar->tm_sec = val; + break; + case CRON_CF_MINUTE: + calendar->tm_min = val; + break; + case CRON_CF_HOUR_OF_DAY: + calendar->tm_hour = val; + break; + case CRON_CF_DAY_OF_WEEK: + calendar->tm_wday = val; + break; + case CRON_CF_DAY_OF_MONTH: + calendar->tm_mday = val; + break; + case CRON_CF_MONTH: + calendar->tm_mon = val; + break; + case CRON_CF_YEAR: + calendar->tm_year = val; + break; + default: + return 1; /* unknown field */ + } + time_t res = cron_mktime(calendar); + if (CRON_INVALID_INSTANT == res) { + return 1; + } + return 0; +} + +/** + * Search the bits provided for the next set bit after the value provided, + * and reset the calendar. + */ +static unsigned int find_next(uint8_t* bits, unsigned int max, unsigned int value, struct tm* calendar, unsigned int field, unsigned int nextField, int* lower_orders, int* res_out) { + int notfound = 0; + int err = 0; + unsigned int next_value = next_set_bit(bits, max, value, ¬found); + /* roll over if needed */ + if (notfound) { + err = add_to_field(calendar, nextField, 1); + if (err) goto return_error; + err = reset_min(calendar, field); + if (err) goto return_error; + notfound = 0; + next_value = next_set_bit(bits, max, 0, ¬found); + } + if (notfound || next_value != value) { + err = set_field(calendar, field, next_value); + if (err) goto return_error; + err = reset_all_min(calendar, lower_orders); + if (err) goto return_error; + } + return next_value; + + return_error: + *res_out = 1; + return 0; +} + +static unsigned int find_next_day(struct tm* calendar, uint8_t* days_of_month, unsigned int day_of_month, uint8_t* days_of_week, unsigned int day_of_week, int* resets, int* res_out) { + int err; + unsigned int count = 0; + unsigned int max = 366; + while ((!cron_get_bit(days_of_month, day_of_month) || !cron_get_bit(days_of_week, day_of_week)) && count++ < max) { + err = add_to_field(calendar, CRON_CF_DAY_OF_MONTH, 1); + + if (err) goto return_error; + day_of_month = calendar->tm_mday; + day_of_week = calendar->tm_wday; + reset_all_min(calendar, resets); + } + return day_of_month; + + return_error: + *res_out = 1; + return 0; +} + +static int do_next(cron_expr* expr, struct tm* calendar, unsigned int dot) { + int i; + int res = 0; + int* resets = NULL; + int* empty_list = NULL; + unsigned int second = 0; + unsigned int update_second = 0; + unsigned int minute = 0; + unsigned int update_minute = 0; + unsigned int hour = 0; + unsigned int update_hour = 0; + unsigned int day_of_week = 0; + unsigned int day_of_month = 0; + unsigned int update_day_of_month = 0; + unsigned int month = 0; + unsigned int update_month = 0; + + resets = (int*) cron_malloc(CRON_CF_ARR_LEN * sizeof(int)); + if (!resets) goto return_result; + empty_list = (int*) cron_malloc(CRON_CF_ARR_LEN * sizeof(int)); + if (!empty_list) goto return_result; + for (i = 0; i < CRON_CF_ARR_LEN; i++) { + resets[i] = -1; + empty_list[i] = -1; + } + + second = calendar->tm_sec; + update_second = find_next(expr->seconds, CRON_MAX_SECONDS, second, calendar, CRON_CF_SECOND, CRON_CF_MINUTE, empty_list, &res); + if (0 != res) goto return_result; + if (second == update_second) { + push_to_fields_arr(resets, CRON_CF_SECOND); + } + + minute = calendar->tm_min; + update_minute = find_next(expr->minutes, CRON_MAX_MINUTES, minute, calendar, CRON_CF_MINUTE, CRON_CF_HOUR_OF_DAY, resets, &res); + if (0 != res) goto return_result; + if (minute == update_minute) { + push_to_fields_arr(resets, CRON_CF_MINUTE); + } else { + res = do_next(expr, calendar, dot); + if (0 != res) goto return_result; + } + + hour = calendar->tm_hour; + update_hour = find_next(expr->hours, CRON_MAX_HOURS, hour, calendar, CRON_CF_HOUR_OF_DAY, CRON_CF_DAY_OF_WEEK, resets, &res); + if (0 != res) goto return_result; + if (hour == update_hour) { + push_to_fields_arr(resets, CRON_CF_HOUR_OF_DAY); + } else { + res = do_next(expr, calendar, dot); + if (0 != res) goto return_result; + } + + day_of_week = calendar->tm_wday; + day_of_month = calendar->tm_mday; + update_day_of_month = find_next_day(calendar, expr->days_of_month, day_of_month, expr->days_of_week, day_of_week, resets, &res); + if (0 != res) goto return_result; + if (day_of_month == update_day_of_month) { + push_to_fields_arr(resets, CRON_CF_DAY_OF_MONTH); + } else { + res = do_next(expr, calendar, dot); + if (0 != res) goto return_result; + } + + month = calendar->tm_mon; /*day already adds one if no day in same month is found*/ + update_month = find_next(expr->months, CRON_MAX_MONTHS, month, calendar, CRON_CF_MONTH, CRON_CF_YEAR, resets, &res); + if (0 != res) goto return_result; + if (month != update_month) { + if (calendar->tm_year - dot > 4) { + res = -1; + goto return_result; + } + res = do_next(expr, calendar, dot); + if (0 != res) goto return_result; + } + goto return_result; + + return_result: + if (!resets || !empty_list) { + res = -1; + } + if (resets) { + cron_free(resets); + } + if (empty_list) { + cron_free(empty_list); + } + return res; +} + +static int to_upper(char* str) { + if (!str) return 1; + int i; + for (i = 0; '\0' != str[i]; i++) { + int c = (int)str[i]; + str[i] = (char) toupper(c); + } + return 0; +} + +static char* to_string(int num) { + if (abs(num) >= CRON_MAX_NUM_TO_SRING) return NULL; + char* str = (char*) cron_malloc(CRON_NUM_OF_DIGITS(num) + 1); + if (!str) return NULL; + int res = sprintf(str, "%d", num); + if (res < 0) { + cron_free(str); + return NULL; + } + return str; +} + +static char* str_replace(char *orig, const char *rep, const char *with) { + char *result; /* the return string */ + char *ins; /* the next insert point */ + char *tmp; /* varies */ + size_t len_rep; /* length of rep */ + size_t len_with; /* length of with */ + size_t len_front; /* distance between rep and end of last rep */ + int count; /* number of replacements */ + if (!orig) return NULL; + if (!rep) rep = ""; + if (!with) with = ""; + len_rep = strlen(rep); + len_with = strlen(with); + + ins = orig; + for (count = 0; NULL != (tmp = strstr(ins, rep)); ++count) { + ins = tmp + len_rep; + } + + /* first time through the loop, all the variable are set correctly + from here on, + tmp points to the end of the result string + ins points to the next occurrence of rep in orig + orig points to the remainder of orig after "end of rep" + */ + tmp = result = (char*) cron_malloc(strlen(orig) + (len_with - len_rep) * count + 1); + if (!result) return NULL; + + while (count--) { + ins = strstr(orig, rep); + len_front = ins - orig; + tmp = strncpy(tmp, orig, len_front) + len_front; + tmp = strcpy(tmp, with) + len_with; + orig += len_front + len_rep; /* move to next "end of rep" */ + } + strcpy(tmp, orig); + return result; +} + +static unsigned int parse_uint(const char* str, int* errcode) { + char* endptr; + errno = 0; + long int l = strtol(str, &endptr, 0); + if (errno == ERANGE || *endptr != '\0' || l < 0 || l > INT_MAX) { + *errcode = 1; + return 0; + } else { + *errcode = 0; + return (unsigned int) l; + } +} + +static char** split_str(const char* str, char del, size_t* len_out) { + size_t i; + size_t stlen = 0; + size_t len = 0; + int accum = 0; + char* buf = NULL; + char** res = NULL; + size_t bi = 0; + size_t ri = 0; + char* tmp; + + if (!str) goto return_error; + for (i = 0; '\0' != str[i]; i++) { + stlen += 1; + if (stlen >= CRON_MAX_STR_LEN_TO_SPLIT) goto return_error; + } + + for (i = 0; i < stlen; i++) { + int c = str[i]; + if (del == str[i]) { + if (accum > 0) { + len += 1; + accum = 0; + } + } else if (!isspace(c)) { + accum += 1; + } + } + /* tail */ + if (accum > 0) { + len += 1; + } + if (0 == len) return NULL; + + buf = (char*) cron_malloc(stlen + 1); + if (!buf) goto return_error; + memset(buf, 0, stlen + 1); + res = (char**) cron_malloc(len * sizeof(char*)); + if (!res) goto return_error; + memset(res, 0, len * sizeof(char*)); + + for (i = 0; i < stlen; i++) { + int c = str[i]; + if (del == str[i]) { + if (bi > 0) { + tmp = strdupl(buf, bi); + if (!tmp) goto return_error; + res[ri++] = tmp; + memset(buf, 0, stlen + 1); + bi = 0; + } + } else if (!isspace(c)) { + buf[bi++] = str[i]; + } + } + /* tail */ + if (bi > 0) { + tmp = strdupl(buf, bi); + if (!tmp) goto return_error; + res[ri++] = tmp; + } + cron_free(buf); + *len_out = len; + return res; + + return_error: + if (buf) { + cron_free(buf); + } + free_splitted(res, len); + *len_out = 0; + return NULL; +} + +static char* replace_ordinals(char* value, const char* const * arr, size_t arr_len) { + size_t i; + char* cur = value; + char* res = NULL; + int first = 1; + for (i = 0; i < arr_len; i++) { + char* strnum = to_string((int) i); + if (!strnum) { + if (!first) { + cron_free(cur); + } + return NULL; + } + res = str_replace(cur, arr[i], strnum); + cron_free(strnum); + if (!first) { + cron_free(cur); + } + if (!res) { + return NULL; + } + cur = res; + if (first) { + first = 0; + } + } + return res; +} + +static int has_char(char* str, char ch) { + size_t i; + size_t len = 0; + if (!str) return 0; + len = strlen(str); + for (i = 0; i < len; i++) { + if (str[i] == ch) return 1; + } + return 0; +} + +static unsigned int* get_range(char* field, unsigned int min, unsigned int max, const char** error) { + + char** parts = NULL; + size_t len = 0; + unsigned int* res = (unsigned int*) cron_malloc(2 * sizeof(unsigned int)); + if (!res) goto return_error; + + res[0] = 0; + res[1] = 0; + if (1 == strlen(field) && '*' == field[0]) { + res[0] = min; + res[1] = max - 1; + } else if (!has_char(field, '-')) { + int err = 0; + unsigned int val = parse_uint(field, &err); + if (err) { + *error = "Unsigned integer parse error 1"; + goto return_error; + } + + res[0] = val; + res[1] = val; + } else { + parts = split_str(field, '-', &len); + if (2 != len) { + *error = "Specified range requires two fields"; + goto return_error; + } + int err = 0; + res[0] = parse_uint(parts[0], &err); + if (err) { + *error = "Unsigned integer parse error 2"; + goto return_error; + } + res[1] = parse_uint(parts[1], &err); + if (err) { + *error = "Unsigned integer parse error 3"; + goto return_error; + } + } + if (res[0] >= max || res[1] >= max) { + *error = "Specified range exceeds maximum"; + goto return_error; + } + if (res[0] < min || res[1] < min) { + *error = "Specified range is less than minimum"; + goto return_error; + } + if (res[0] > res[1]) { + *error = "Specified range start exceeds range end"; + goto return_error; + } + + free_splitted(parts, len); + *error = NULL; + return res; + + return_error: + free_splitted(parts, len); + if (res) { + cron_free(res); + } + + return NULL; +} + +static void set_number_hits(const char* value, uint8_t* target, unsigned int min, unsigned int max, const char** error) { + size_t i; + unsigned int i1; + size_t len = 0; + + char** fields = split_str(value, ',', &len); + if (!fields) { + *error = "Comma split error"; + goto return_result; + } + + for (i = 0; i < len; i++) { + if (!has_char(fields[i], '/')) { + /* Not an incrementer so it must be a range (possibly empty) */ + + unsigned int* range = get_range(fields[i], min, max, error); + + if (*error) { + if (range) { + cron_free(range); + } + goto return_result; + + } + + for (i1 = range[0]; i1 <= range[1]; i1++) { + cron_set_bit(target, i1); + + } + cron_free(range); + + } else { + size_t len2 = 0; + char** split = split_str(fields[i], '/', &len2); + if (2 != len2) { + *error = "Incrementer must have two fields"; + free_splitted(split, len2); + goto return_result; + } + unsigned int* range = get_range(split[0], min, max, error); + if (*error) { + if (range) { + cron_free(range); + } + free_splitted(split, len2); + goto return_result; + } + if (!has_char(split[0], '-')) { + range[1] = max - 1; + } + int err = 0; + unsigned int delta = parse_uint(split[1], &err); + if (err) { + *error = "Unsigned integer parse error 4"; + cron_free(range); + free_splitted(split, len2); + goto return_result; + } + if (0 == delta) { + *error = "Incrementer may not be zero"; + cron_free(range); + free_splitted(split, len2); + goto return_result; + } + for (i1 = range[0]; i1 <= range[1]; i1 += delta) { + cron_set_bit(target, i1); + } + free_splitted(split, len2); + cron_free(range); + + } + } + goto return_result; + + return_result: + free_splitted(fields, len); + +} + +static void set_months(char* value, uint8_t* targ, const char** error) { + unsigned int i; + unsigned int max = 12; + + char* replaced = NULL; + + to_upper(value); + replaced = replace_ordinals(value, MONTHS_ARR, CRON_MONTHS_ARR_LEN); + if (!replaced) { + *error = "Invalid month format"; + return; + } + set_number_hits(replaced, targ, 1, max + 1, error); + cron_free(replaced); + + /* ... and then rotate it to the front of the months */ + for (i = 1; i <= max; i++) { + if (cron_get_bit(targ, i)) { + cron_set_bit(targ, i - 1); + cron_del_bit(targ, i); + } + } +} + +static void set_days_of_week(char* field, uint8_t* targ, const char** error) { + unsigned int max = 7; + char* replaced = NULL; + + if (1 == strlen(field) && '?' == field[0]) { + field[0] = '*'; + } + to_upper(field); + replaced = replace_ordinals(field, DAYS_ARR, CRON_DAYS_ARR_LEN); + if (!replaced) { + *error = "Invalid day format"; + return; + } + set_number_hits(replaced, targ, 0, max + 1, error); + cron_free(replaced); + if (cron_get_bit(targ, 7)) { + /* Sunday can be represented as 0 or 7*/ + cron_set_bit(targ, 0); + cron_del_bit(targ, 7); + } +} + +static void set_days_of_month(char* field, uint8_t* targ, const char** error) { + /* Days of month start with 1 (in Cron and Calendar) so add one */ + if (1 == strlen(field) && '?' == field[0]) { + field[0] = '*'; + } + set_number_hits(field, targ, 1, CRON_MAX_DAYS_OF_MONTH, error); +} + +void cron_parse_expr(const char* expression, cron_expr* target, const char** error) { + const char* err_local; + size_t len = 0; + char** fields = NULL; + if (!error) { + error = &err_local; + } + *error = NULL; + if (!expression) { + *error = "Invalid NULL expression"; + goto return_res; + } + if (!target) { + *error = "Invalid NULL target"; + goto return_res; + } + + fields = split_str(expression, ' ', &len); + if (len != 6) { + *error = "Invalid number of fields, expression must consist of 6 fields"; + goto return_res; + } + memset(target, 0, sizeof(*target)); + set_number_hits(fields[0], target->seconds, 0, 60, error); + if (*error) goto return_res; + set_number_hits(fields[1], target->minutes, 0, 60, error); + if (*error) goto return_res; + set_number_hits(fields[2], target->hours, 0, 24, error); + if (*error) goto return_res; + set_days_of_month(fields[3], target->days_of_month, error); + if (*error) goto return_res; + set_months(fields[4], target->months, error); + if (*error) goto return_res; + set_days_of_week(fields[5], target->days_of_week, error); + if (*error) goto return_res; + + goto return_res; + + return_res: + free_splitted(fields, len); +} + +time_t cron_next(cron_expr* expr, time_t date) { + /* + The plan: + + 1 Round up to the next whole second + + 2 If seconds match move on, otherwise find the next match: + 2.1 If next match is in the next minute then roll forwards + + 3 If minute matches move on, otherwise find the next match + 3.1 If next match is in the next hour then roll forwards + 3.2 Reset the seconds and go to 2 + + 4 If hour matches move on, otherwise find the next match + 4.1 If next match is in the next day then roll forwards, + 4.2 Reset the minutes and seconds and go to 2 + + ... + */ + if (!expr) return CRON_INVALID_INSTANT; + struct tm calval; + memset(&calval, 0, sizeof(struct tm)); + struct tm* calendar = cron_time(&date, &calval); + if (!calendar) return CRON_INVALID_INSTANT; + time_t original = cron_mktime(calendar); + if (CRON_INVALID_INSTANT == original) return CRON_INVALID_INSTANT; + + int res = do_next(expr, calendar, calendar->tm_year); + if (0 != res) return CRON_INVALID_INSTANT; + + time_t calculated = cron_mktime(calendar); + if (CRON_INVALID_INSTANT == calculated) return CRON_INVALID_INSTANT; + if (calculated == original) { + /* We arrived at the original timestamp - round up to the next whole second and try again... */ + res = add_to_field(calendar, CRON_CF_SECOND, 1); + if (0 != res) return CRON_INVALID_INSTANT; + res = do_next(expr, calendar, calendar->tm_year); + if (0 != res) return CRON_INVALID_INSTANT; + } + + return cron_mktime(calendar); +} + + +/* https://github.com/staticlibs/ccronexpr/pull/8 */ + +static unsigned int prev_set_bit(uint8_t* bits, int from_index, int to_index, int* notfound) { + int i; + if (!bits) { + *notfound = 1; + return 0; + } + for (i = from_index; i >= to_index; i--) { + if (cron_get_bit(bits, i)) return i; + } + *notfound = 1; + return 0; +} + +static int last_day_of_month(int month, int year) { + struct tm cal; + time_t t; + memset(&cal,0,sizeof(cal)); + cal.tm_sec=0; + cal.tm_min=0; + cal.tm_hour=0; + cal.tm_mon = month+1; + cal.tm_mday = 0; + cal.tm_year=year; + t=mktime(&cal); + return gmtime(&t)->tm_mday; +} + +/** + * Reset the calendar setting all the fields provided to zero. + */ +static int reset_max(struct tm* calendar, int field) { + if (!calendar || -1 == field) { + return 1; + } + switch (field) { + case CRON_CF_SECOND: + calendar->tm_sec = 59; + break; + case CRON_CF_MINUTE: + calendar->tm_min = 59; + break; + case CRON_CF_HOUR_OF_DAY: + calendar->tm_hour = 23; + break; + case CRON_CF_DAY_OF_WEEK: + calendar->tm_wday = 6; + break; + case CRON_CF_DAY_OF_MONTH: + calendar->tm_mday = last_day_of_month(calendar->tm_mon, calendar->tm_year); + break; + case CRON_CF_MONTH: + calendar->tm_mon = 11; + break; + case CRON_CF_YEAR: + /* I don't think this is supposed to happen ... */ + fprintf(stderr, "reset CRON_CF_YEAR\n"); + break; + default: + return 1; /* unknown field */ + } + time_t res = cron_mktime(calendar); + if (CRON_INVALID_INSTANT == res) { + return 1; + } + return 0; +} + +static int reset_all_max(struct tm* calendar, int* fields) { + int i; + int res = 0; + if (!calendar || !fields) { + return 1; + } + for (i = 0; i < CRON_CF_ARR_LEN; i++) { + if (-1 != fields[i]) { + res = reset_max(calendar, fields[i]); + if (0 != res) return res; + } + } + return 0; +} + +/** + * Search the bits provided for the next set bit after the value provided, + * and reset the calendar. + */ +static unsigned int find_prev(uint8_t* bits, unsigned int max, unsigned int value, struct tm* calendar, unsigned int field, unsigned int nextField, int* lower_orders, int* res_out) { + int notfound = 0; + int err = 0; + unsigned int next_value = prev_set_bit(bits, value, 0, ¬found); + /* roll under if needed */ + if (notfound) { + err = add_to_field(calendar, nextField, -1); + if (err) goto return_error; + err = reset_max(calendar, field); + if (err) goto return_error; + notfound = 0; + next_value = prev_set_bit(bits, max - 1, value, ¬found); + } + if (notfound || next_value != value) { + err = set_field(calendar, field, next_value); + if (err) goto return_error; + err = reset_all_max(calendar, lower_orders); + if (err) goto return_error; + } + return next_value; + + return_error: + *res_out = 1; + return 0; +} + +static unsigned int find_prev_day(struct tm* calendar, uint8_t* days_of_month, unsigned int day_of_month, uint8_t* days_of_week, unsigned int day_of_week, int* resets, int* res_out) { + int err; + unsigned int count = 0; + unsigned int max = 366; + while ((!cron_get_bit(days_of_month, day_of_month) || !cron_get_bit(days_of_week, day_of_week)) && count++ < max) { + err = add_to_field(calendar, CRON_CF_DAY_OF_MONTH, -1); + + if (err) goto return_error; + day_of_month = calendar->tm_mday; + day_of_week = calendar->tm_wday; + reset_all_max(calendar, resets); + } + return day_of_month; + + return_error: + *res_out = 1; + return 0; +} + +static int do_prev(cron_expr* expr, struct tm* calendar, unsigned int dot) { + int i; + int res = 0; + int* resets = NULL; + int* empty_list = NULL; + unsigned int second = 0; + unsigned int update_second = 0; + unsigned int minute = 0; + unsigned int update_minute = 0; + unsigned int hour = 0; + unsigned int update_hour = 0; + unsigned int day_of_week = 0; + unsigned int day_of_month = 0; + unsigned int update_day_of_month = 0; + unsigned int month = 0; + unsigned int update_month = 0; + + resets = (int*) cron_malloc(CRON_CF_ARR_LEN * sizeof(int)); + if (!resets) goto return_result; + empty_list = (int*) cron_malloc(CRON_CF_ARR_LEN * sizeof(int)); + if (!empty_list) goto return_result; + for (i = 0; i < CRON_CF_ARR_LEN; i++) { + resets[i] = -1; + empty_list[i] = -1; + } + + second = calendar->tm_sec; + update_second = find_prev(expr->seconds, CRON_MAX_SECONDS, second, calendar, CRON_CF_SECOND, CRON_CF_MINUTE, empty_list, &res); + if (0 != res) goto return_result; + if (second == update_second) { + push_to_fields_arr(resets, CRON_CF_SECOND); + } + + minute = calendar->tm_min; + update_minute = find_prev(expr->minutes, CRON_MAX_MINUTES, minute, calendar, CRON_CF_MINUTE, CRON_CF_HOUR_OF_DAY, resets, &res); + if (0 != res) goto return_result; + if (minute == update_minute) { + push_to_fields_arr(resets, CRON_CF_MINUTE); + } else { + res = do_prev(expr, calendar, dot); + if (0 != res) goto return_result; + } + + hour = calendar->tm_hour; + update_hour = find_prev(expr->hours, CRON_MAX_HOURS, hour, calendar, CRON_CF_HOUR_OF_DAY, CRON_CF_DAY_OF_WEEK, resets, &res); + if (0 != res) goto return_result; + if (hour == update_hour) { + push_to_fields_arr(resets, CRON_CF_HOUR_OF_DAY); + } else { + res = do_prev(expr, calendar, dot); + if (0 != res) goto return_result; + } + + day_of_week = calendar->tm_wday; + day_of_month = calendar->tm_mday; + update_day_of_month = find_prev_day(calendar, expr->days_of_month, day_of_month, expr->days_of_week, day_of_week, resets, &res); + if (0 != res) goto return_result; + if (day_of_month == update_day_of_month) { + push_to_fields_arr(resets, CRON_CF_DAY_OF_MONTH); + } else { + res = do_prev(expr, calendar, dot); + if (0 != res) goto return_result; + } + + month = calendar->tm_mon; /*day already adds one if no day in same month is found*/ + update_month = find_prev(expr->months, CRON_MAX_MONTHS, month, calendar, CRON_CF_MONTH, CRON_CF_YEAR, resets, &res); + if (0 != res) goto return_result; + if (month != update_month) { + if (dot - calendar->tm_year > CRON_MAX_YEARS_DIFF) { + res = -1; + goto return_result; + } + res = do_prev(expr, calendar, dot); + if (0 != res) goto return_result; + } + goto return_result; + + return_result: + if (!resets || !empty_list) { + res = -1; + } + if (resets) { + cron_free(resets); + } + if (empty_list) { + cron_free(empty_list); + } + return res; +} + +time_t cron_prev(cron_expr* expr, time_t date) { + /* + The plan: + + 1 Round down to a whole second + + 2 If seconds match move on, otherwise find the next match: + 2.1 If next match is in the next minute then roll forwards + + 3 If minute matches move on, otherwise find the next match + 3.1 If next match is in the next hour then roll forwards + 3.2 Reset the seconds and go to 2 + + 4 If hour matches move on, otherwise find the next match + 4.1 If next match is in the next day then roll forwards, + 4.2 Reset the minutes and seconds and go to 2 + + ... + */ + if (!expr) return CRON_INVALID_INSTANT; + struct tm calval; + memset(&calval, 0, sizeof(struct tm)); + struct tm* calendar = cron_time(&date, &calval); + if (!calendar) return CRON_INVALID_INSTANT; + time_t original = cron_mktime(calendar); + if (CRON_INVALID_INSTANT == original) return CRON_INVALID_INSTANT; + + /* calculate the previous occurrence */ + int res = do_prev(expr, calendar, calendar->tm_year); + if (0 != res) return CRON_INVALID_INSTANT; + + /* check for a match, try from the next second if one wasn't found */ + time_t calculated = cron_mktime(calendar); + if (CRON_INVALID_INSTANT == calculated) return CRON_INVALID_INSTANT; + if (calculated == original) { + /* We arrived at the original timestamp - round up to the next whole second and try again... */ + res = add_to_field(calendar, CRON_CF_SECOND, -1); + if (0 != res) return CRON_INVALID_INSTANT; + res = do_prev(expr, calendar, calendar->tm_year); + if (0 != res) return CRON_INVALID_INSTANT; + } + + return cron_mktime(calendar); +} diff --git a/src/modules/virtual/Cron/ccronexpr/ccronexpr.h b/src/modules/virtual/Cron/ccronexpr/ccronexpr.h new file mode 100644 index 00000000..8f7ad30d --- /dev/null +++ b/src/modules/virtual/Cron/ccronexpr/ccronexpr.h @@ -0,0 +1,95 @@ +/* + * Copyright 2015, alex at staticlibs.net + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * File: ccronexpr.h + * Author: alex + * + * Created on February 24, 2015, 9:35 AM + */ + +#ifndef CCRONEXPR_H +#define CCRONEXPR_H + +#define CRON_USE_LOCAL_TIME + +#if defined(__cplusplus) && !defined(CRON_COMPILE_AS_CXX) +extern "C" { +#endif + +#ifndef ANDROID +#include +#else /* ANDROID */ +#include +#endif /* ANDROID */ + +#include /*added for use if uint*_t data types*/ + +/** + * Parsed cron expression + */ +typedef struct { + uint8_t seconds[8]; + uint8_t minutes[8]; + uint8_t hours[3]; + uint8_t days_of_week[1]; + uint8_t days_of_month[4]; + uint8_t months[2]; +} cron_expr; + +/** + * Parses specified cron expression. + * + * @param expression cron expression as nul-terminated string, + * should be no longer that 256 bytes + * @param pointer to cron expression structure, it's client code responsibility + * to free/destroy it afterwards + * @param error output error message, will be set to string literal + * error message in case of error. Will be set to NULL on success. + * The error message should NOT be freed by client. + */ +void cron_parse_expr(const char* expression, cron_expr* target, const char** error); + +/** + * Uses the specified expression to calculate the next 'fire' date after + * the specified date. All dates are processed as UTC (GMT) dates + * without timezones information. To use local dates (current system timezone) + * instead of GMT compile with '-DCRON_USE_LOCAL_TIME' + * + * @param expr parsed cron expression to use in next date calculation + * @param date start date to start calculation from + * @return next 'fire' date in case of success, '((time_t) -1)' in case of error. + */ +time_t cron_next(cron_expr* expr, time_t date); + +/** + * Uses the specified expression to calculate the previous 'fire' date after + * the specified date. All dates are processed as UTC (GMT) dates + * without timezones information. To use local dates (current system timezone) + * instead of GMT compile with '-DCRON_USE_LOCAL_TIME' + * + * @param expr parsed cron expression to use in previous date calculation + * @param date start date to start calculation from + * @return previous 'fire' date in case of success, '((time_t) -1)' in case of error. + */ +time_t cron_prev(cron_expr* expr, time_t date); + + +#if defined(__cplusplus) && !defined(CRON_COMPILE_AS_CXX) +} /* extern "C"*/ +#endif + +#endif /* CCRONEXPR_H */ diff --git a/src/modules/virtual/Cron/modinfo.json b/src/modules/virtual/Cron/modinfo.json new file mode 100644 index 00000000..c305ab4e --- /dev/null +++ b/src/modules/virtual/Cron/modinfo.json @@ -0,0 +1,57 @@ +{ + "menuSection": "Виртуальные элементы", + "configItem": [ + { + "global": 0, + "name": "Будильник (Cron)", + "type": "Writing", + "subtype": "Cron", + "id": "cron", + "widget": "anydataDef", + "page": "Таймеры", + "descr": "Будильник", + "int": 1, + "val": "*/15 * * * * *", + "formatNextAlarm": "%H:%M:%S", + "needSave": 0 + } + ], + "about": { + "authorName": "Ilya Belyakov", + "authorContact": "https://t.me/Biveraxe", + "authorGit": "https://github.com/biveraxe", + "specialThanks": "", + "moduleName": "Cron", + "moduleVersion": "1.0", + "usedRam": { + "esp32_4mb": 15, + "esp8266_4mb": 15 + }, + "title": "Будильник типа Cron", + "moduleDesc": "Планировщик времени для периодического выполнения заданий в определённое время. Генерирует событие в указанное время по формату Cron https://ru.wikipedia.org/wiki/Cron ", + "propInfo": { + "formatNextAlarm": "Формат представления даты и времени срабатывания следующего уведомления. http://cppstudio.com/post/621/", + "needSave": "Требуется сохранять(1) или нет(0) состояние в энерго независимую память." + }, + "retInfo": "Содержит время следующего срабатывания.", + "funcInfo": [ + { + "name": "stop", + "descr": "Поставить процесс на паузу, при этом не будет событий.", + "params": [] + }, + { + "name": "continue", + "descr": "Продолжить выполнение с момента остановки.", + "params": [] + } + ] + }, + "defActive": true, + "usedLibs": { + "esp32_4mb": [], + "esp8266_4mb": [], + "esp8266_1mb": [], + "esp8266_1mb_ota": [] + } +} \ No newline at end of file