diff --git a/.gitlab/ci/build.yml b/.gitlab/ci/build.yml index 1b49dbb..b1521c5 100644 --- a/.gitlab/ci/build.yml +++ b/.gitlab/ci/build.yml @@ -130,7 +130,7 @@ build_example_usb_camera_lcd_display: matrix: - IMAGE: espressif/idf:release-v5.0 - IMAGE: espressif/idf:release-v5.1 - - IMAGE: espressif/idf:latest + # - IMAGE: espressif/idf:latest variables: EXAMPLE_DIR: examples/usb_camera_lcd_display diff --git a/examples/chatgpt_demo/CMakeLists.txt b/examples/chatgpt_demo/CMakeLists.txt index dd3b031..9c7bdc9 100644 --- a/examples/chatgpt_demo/CMakeLists.txt +++ b/examples/chatgpt_demo/CMakeLists.txt @@ -8,5 +8,5 @@ include($ENV{IDF_PATH}/tools/cmake/project.cmake) set(EXTRA_COMPONENT_DIRS ../../components ) -add_compile_options(-fdiagnostics-color=always) +add_compile_options(-fdiagnostics-color=always -Wno-ignored-qualifiers) project(chatgpt_demo) diff --git a/examples/chatgpt_demo/components/chmorgan__esp-audio-player/CMakeLists.txt b/examples/chatgpt_demo/components/chmorgan__esp-audio-player/CMakeLists.txt deleted file mode 100644 index e9c414e..0000000 --- a/examples/chatgpt_demo/components/chmorgan__esp-audio-player/CMakeLists.txt +++ /dev/null @@ -1,27 +0,0 @@ - -set(srcs - "audio_player.cpp" -) - -set(includes - "include" -) - -set(requires "") - -if(CONFIG_AUDIO_PLAYER_ENABLE_MP3) - list(APPEND srcs "audio_mp3.cpp") -endif() - -# TODO: move inside of the 'if(CONFIG_AUDIO_PLAYER_ENABLE_MP3)' when everything builds correctly -list(APPEND requires "esp-libhelix-mp3") - -if(CONFIG_AUDIO_PLAYER_ENABLE_WAV) - list(APPEND srcs "audio_wav.cpp") -endif() - -idf_component_register(SRCS "${srcs}" - REQUIRES "${requires}" - INCLUDE_DIRS "${includes}" - REQUIRES driver -) diff --git a/examples/chatgpt_demo/components/chmorgan__esp-audio-player/Kconfig b/examples/chatgpt_demo/components/chmorgan__esp-audio-player/Kconfig deleted file mode 100644 index 906d81c..0000000 --- a/examples/chatgpt_demo/components/chmorgan__esp-audio-player/Kconfig +++ /dev/null @@ -1,20 +0,0 @@ -menu "Audio playback" - - config AUDIO_PLAYER_ENABLE_MP3 - bool "Enable mp3 decoding." - default y - help - The audio player can play mp3 files using libhelix-mp3. - config AUDIO_PLAYER_ENABLE_WAV - bool "Enable wav file playback" - default y - help - Audio player can decode wave files. - - config AUDIO_PLAYER_LOG_LEVEL - int "Audio Player log level (0 none - 3 highest)" - default 0 - range 0 3 - help - Specify the verbosity of Audio Player log output. -endmenu \ No newline at end of file diff --git a/examples/chatgpt_demo/components/chmorgan__esp-audio-player/README.md b/examples/chatgpt_demo/components/chmorgan__esp-audio-player/README.md deleted file mode 100644 index 7fc989d..0000000 --- a/examples/chatgpt_demo/components/chmorgan__esp-audio-player/README.md +++ /dev/null @@ -1,58 +0,0 @@ -# Audio player component for esp32 - - -[![cppcheck-action](https://github.com/chmorgan/esp-audio-player/actions/workflows/cppcheck.yml/badge.svg)](https://github.com/chmorgan/esp-audio-player/actions/workflows/cppcheck.yml) - -## Capabilities - -* MP3 decoding (via libhelix-mp3) -* Wav/wave file decoding - -## Who is this for? - -Decode only audio playback on esp32 series of chips, where the features and footprint of esp-adf are not -necessary. - -## What about esp-adf? - -This component is not intended to compete with esp-adf, a much more fully developed -audio framework. - -It does however have a number of advantages at the moment including: - -* Fully open source (esp-adf has a number of binary modules at the moment) -* Minimal size (it's less capable, but also simpler, than esp-adf) - -## Dependencies - -For MP3 support you'll need the [esp-libhelix-mp3](https://github.com/chmorgan/esp-libhelix-mp3) component. - -## States - -```mermaid -stateDiagram-v2 - [*] --> Idle : new(), cb(IDLE) - Idle --> Playing : play(), cb(PLAYING) - Playing --> Paused : pause(), cb(PAUSE) - Paused --> Playing : resume(), cb(PLAYING) - Playing --> Playing : play(), cb(COMPLETED_PLAYING_NEXT) - Paused --> Idle : stop(), cb(IDLE) - Playing --> Idle : song complete, cb(IDLE) - [*] --> Shutdown : delete(), cb(SHUTDOWN) - Shutdown --> Idle : new(), cb(IDLE) -``` - -Note: Diagram shortens callbacks from AUDIO_PLAYER_EVENT_xxx to xxx, and functions from audio_player_xxx() to xxx(), for clarity. - - -## Release process - Pushing component to the IDF Component Registry - -The github workflow, .github/workflows/esp_upload_component.yml, pushes data to the espressif -[IDF component registry](https://components.espressif.com). - -To push a new version: - -* Apply a git tag via 'git tag vA.B.C' -* Push to the 'main' branch - -The github workflow *should* run and automatically push to the IDF component registry. diff --git a/examples/chatgpt_demo/components/chmorgan__esp-audio-player/audio_decode_types.h b/examples/chatgpt_demo/components/chmorgan__esp-audio-player/audio_decode_types.h deleted file mode 100644 index 1e6509f..0000000 --- a/examples/chatgpt_demo/components/chmorgan__esp-audio-player/audio_decode_types.h +++ /dev/null @@ -1,57 +0,0 @@ -#pragma once - -#include -#include - -typedef enum { - DECODE_STATUS_CONTINUE, /*< data remaining, call decode again */ - DECODE_STATUS_NO_DATA_CONTINUE, /*< data remaining but none in this call */ - DECODE_STATUS_DONE, /*< no data remaining to decode */ - DECODE_STATUS_ERROR /*< unrecoverable error */ -} DECODE_STATUS; - -typedef struct { - int sample_rate; - uint32_t bits_per_sample; - uint32_t channels; -} format; - -/** - * Decoded audio data ready for playback - * - * Fields in this structure are expected to be updated - * upon each cycle of the decoder, as the decoder stores - * audio data to be played back. - */ -typedef struct { - /** - * NOTE: output_samples is flushed each decode cycle - * - * NOTE: the decode format determines how to convert samples to frames, ie. - * whether these samples are stero or mono samples and what the bits per sample are - */ - uint8_t *samples; - - /** capacity of samples */ - size_t samples_capacity; - - /** - * 2x samples_capacity to allow for in-place conversion of - * mono to stereo - */ - size_t samples_capacity_max; - - /** - * Number of frames in samples, - * Note that each frame consists of 'fmt.channels' number of samples, - * for example for stereo output the number of samples is 2x the - * frame count. - */ - size_t frame_count; - - format fmt; -} decode_data; - - -#define BYTES_IN_WORD 2 -#define BITS_PER_BYTE 8 diff --git a/examples/chatgpt_demo/components/chmorgan__esp-audio-player/audio_log.h b/examples/chatgpt_demo/components/chmorgan__esp-audio-player/audio_log.h deleted file mode 100644 index 81da67c..0000000 --- a/examples/chatgpt_demo/components/chmorgan__esp-audio-player/audio_log.h +++ /dev/null @@ -1,26 +0,0 @@ -#pragma once - -#include "esp_log.h" - -#if CONFIG_AUDIO_PLAYER_LOG_LEVEL >= 1 -#define LOGI_1(FMT, ...) \ - ESP_LOGI(TAG, "[1] " FMT, ##__VA_ARGS__) -#else -#define LOGI_1(FMT, ...) { (void)TAG; } -#endif - -#if CONFIG_AUDIO_PLAYER_LOG_LEVEL >= 2 -#define LOGI_2(FMT, ...) \ - ESP_LOGI(TAG, "[2] " FMT, ##__VA_ARGS__) -#else -#define LOGI_2(FMT, ...) { (void)TAG;} -#endif - -#if CONFIG_AUDIO_PLAYER_LOG_LEVEL >= 3 -#define LOGI_3(FMT, ...) \ - ESP_LOGI(TAG, "[3] " FMT, ##__VA_ARGS__) -#define COMPILE_3(x) x -#else -#define LOGI_3(FMT, ...) { (void)TAG; } -#define COMPILE_3(x) {} -#endif diff --git a/examples/chatgpt_demo/components/chmorgan__esp-audio-player/audio_mp3.cpp b/examples/chatgpt_demo/components/chmorgan__esp-audio-player/audio_mp3.cpp deleted file mode 100644 index b8ea097..0000000 --- a/examples/chatgpt_demo/components/chmorgan__esp-audio-player/audio_mp3.cpp +++ /dev/null @@ -1,174 +0,0 @@ -#include -#include "audio_log.h" -#include "audio_mp3.h" - -static const char *TAG = "mp3"; - -bool is_mp3(uint8_t *stream) -{ - bool is_mp3_file = false; - - assert(stream); - - // see https://en.wikipedia.org/wiki/List_of_file_signatures - uint8_t magic[3]; - memcpy(magic, stream, sizeof(magic)); - if (1) { - if ((magic[0] == 0xFF) && - (magic[1] == 0xFB)) { - is_mp3_file = true; - } else if ((magic[0] == 0xFF) && - (magic[1] == 0xF3)) { - is_mp3_file = true; - } else if ((magic[0] == 0xFF) && - (magic[1] == 0xF2)) { - is_mp3_file = true; - } else if ((magic[0] == 0x49) && - (magic[1] == 0x44) && - (magic[2] == 0x33)) { /* 'ID3' */ - // fseek(stream, 0, SEEK_SET); - - /* Get ID3 head */ - mp3_id3_header_v2_t tag; - - if (memcpy(&tag, stream, sizeof(mp3_id3_header_v2_t))) { - if (memcmp("ID3", (const void *) &tag, sizeof(tag.header)) == 0) { - is_mp3_file = true; - } - } - } - } - - return is_mp3_file; -} - -/** - * @return true if data remains, false on error or end of file - */ -DECODE_STATUS decode_mp3(HMP3Decoder mp3_decoder, uint8_t **stream, uint8_t *base, uint32_t stream_len, decode_data *pData, mp3_instance *pInstance) -{ - MP3FrameInfo frame_info; - - size_t unread_bytes = pInstance->bytes_in_data_buf - (pInstance->read_ptr - pInstance->data_buf); - - /* somewhat arbitrary trigger to refill buffer - should always be enough for a full frame */ - if (unread_bytes < 1.25 * MAINBUF_SIZE && !pInstance->eof_reached) { - uint8_t *write_ptr = pInstance->data_buf + unread_bytes; - size_t free_space = pInstance->data_buf_size - unread_bytes; - - /* move last, small chunk from end of buffer to start, - then fill with new data */ - memmove(pInstance->data_buf, pInstance->read_ptr, unread_bytes); - - size_t nRead; - if ((*stream - base) < stream_len) { - if ((*stream + free_space) > (base + stream_len)) { - nRead = (base + stream_len) - *stream; - } else { - nRead = free_space; - } - } else { - nRead = 0; - } - // size_t nRead = fread(write_ptr, 1, free_space, stream); - if (nRead) { - memcpy(write_ptr, *stream, free_space); - } - (*stream) += nRead; - - pInstance->bytes_in_data_buf = unread_bytes + nRead; - pInstance->read_ptr = pInstance->data_buf; - - if (nRead == 0) { - pInstance->eof_reached = true; - } - LOGI_2("pos %ld, nRead %d, eof %d", *stream, nRead, pInstance->eof_reached); - - unread_bytes = pInstance->bytes_in_data_buf; - } - - LOGI_3("data_buf 0x%p, read 0x%p", pInstance->data_buf, pInstance->read_ptr); - - if (unread_bytes == 0) { - LOGI_1("unread_bytes == 0, status done"); - return DECODE_STATUS_DONE; - } - - /* Find MP3 sync word from read buffer */ - int offset = MP3FindSyncWord(pInstance->read_ptr, unread_bytes); - - LOGI_2("unread %d, total %d, offset 0x%x(%d)", - unread_bytes, pInstance->bytes_in_data_buf, offset, offset); - - if (offset >= 0) { - COMPILE_3(int starting_unread_bytes = unread_bytes); - uint8_t *read_ptr = pInstance->read_ptr + offset; /*!< Data start point */ - unread_bytes -= offset; - LOGI_3("read 0x%p, unread %d", read_ptr, unread_bytes); - int mp3_dec_err = MP3Decode(mp3_decoder, &read_ptr, (int *)&unread_bytes, reinterpret_cast(pData->samples), 0); - - pInstance->read_ptr = read_ptr; - - if (mp3_dec_err == ERR_MP3_NONE) { - /* Get MP3 frame info */ - MP3GetLastFrameInfo(mp3_decoder, &frame_info); - - pData->fmt.sample_rate = frame_info.samprate; - pData->fmt.bits_per_sample = frame_info.bitsPerSample; - pData->fmt.channels = frame_info.nChans; - - pData->frame_count = (frame_info.outputSamps / frame_info.nChans); - - LOGI_3("mp3: channels %d, sr %d, bps %d, frame_count %d, processed %d", - pData->fmt.channels, - pData->fmt.sample_rate, - pData->fmt.bits_per_sample, - frame_info.outputSamps, - starting_unread_bytes - unread_bytes); - } else { - if (mp3_dec_err == ERR_MP3_MAINDATA_UNDERFLOW) { - // underflow indicates MP3Decode should be called again - LOGI_1("underflow read ptr is 0x%p", read_ptr); - return DECODE_STATUS_NO_DATA_CONTINUE; - } else { - // NOTE: some mp3 files result in misdetection of mp3 frame headers - // and during decode these misdetected frames cannot be - // decoded - // - // Rather than give up on the file by returning - // DECODE_STATUS_ERROR, we ask the caller - // to continue to call us, by returning DECODE_STATUS_NO_DATA_CONTINUE. - // - // The invalid frame data is skipped over as a search for the next frame - // on the subsequent call to this function will start searching - // AFTER the misdetected frmame header, dropping the invalid data. - // - // We may want to consider a more sophisticated approach here at a later time. - ESP_LOGE(TAG, "status error %d", mp3_dec_err); - return DECODE_STATUS_DONE; - } - } - } else { - // if we are dropping data there were no frames decoded - pData->frame_count = 0; - - // drop an even count of words - size_t words_to_drop = unread_bytes / BYTES_IN_WORD; - size_t bytes_to_drop = words_to_drop * BYTES_IN_WORD; - - // if the unread bytes is less than BYTES_IN_WORD, we should drop any unread bytes - // to avoid the situation where the file could have a few extra bytes at the end - // of the file that isn't at least BYTES_IN_WORD and decoding would get stuck - if (unread_bytes < BYTES_IN_WORD) { - bytes_to_drop = unread_bytes; - } - - // shift the read_ptr to drop the bytes in the buffer - pInstance->read_ptr += bytes_to_drop; - - /* Sync word not found in frame. Drop data that was read until a word boundary */ - ESP_LOGE(TAG, "MP3 sync word not found, dropping %d bytes", bytes_to_drop); - } - - return DECODE_STATUS_CONTINUE; -} diff --git a/examples/chatgpt_demo/components/chmorgan__esp-audio-player/audio_mp3.h b/examples/chatgpt_demo/components/chmorgan__esp-audio-player/audio_mp3.h deleted file mode 100644 index 1edd2a5..0000000 --- a/examples/chatgpt_demo/components/chmorgan__esp-audio-player/audio_mp3.h +++ /dev/null @@ -1,48 +0,0 @@ -#pragma once - -#include -#include "audio_decode_types.h" -#include "mp3dec.h" - -typedef struct { - char header[3]; /*!< Always "TAG" */ - char title[30]; /*!< Audio title */ - char artist[30]; /*!< Audio artist */ - char album[30]; /*!< Album name */ - char year[4]; /*!< Char array of year */ - char comment[30]; /*!< Extra comment */ - char genre; /*!< See "https://en.wikipedia.org/wiki/ID3" */ -} __attribute__((packed)) mp3_id3_header_v1_t; - -typedef struct { - char header[3]; /*!< Always "ID3" */ - char ver; /*!< Version, equals to3 if ID3V2.3 */ - char revision; /*!< Revision, should be 0 */ - char flag; /*!< Flag byte, use Bit[7..5] only */ - char size[4]; /*!< TAG size */ -} __attribute__((packed)) mp3_id3_header_v2_t; - -typedef struct { - // Constants below - uint8_t *data_buf; - - /** number of bytes in data_buf */ - size_t data_buf_size; - - // Values that change at runtime are below - - /** - * Total bytes in data_buf, - * not the number of bytes remaining after the read_ptr - */ - size_t bytes_in_data_buf; - - /** Pointer to read location in data_buf */ - uint8_t *read_ptr; - - // set to true if the end of file has been reached - bool eof_reached; -} mp3_instance; - -bool is_mp3(uint8_t *stream); -DECODE_STATUS decode_mp3(HMP3Decoder mp3_decoder, uint8_t **stream, uint8_t *base, uint32_t stream_len, decode_data *pData, mp3_instance *pInstance); diff --git a/examples/chatgpt_demo/components/chmorgan__esp-audio-player/audio_player.cpp b/examples/chatgpt_demo/components/chmorgan__esp-audio-player/audio_player.cpp deleted file mode 100644 index 46c3123..0000000 --- a/examples/chatgpt_demo/components/chmorgan__esp-audio-player/audio_player.cpp +++ /dev/null @@ -1,609 +0,0 @@ -/** - * @file - * @version 0.1 - * - * @copyright Copyright 2021 Espressif Systems (Shanghai) Co. Ltd. - * @copyright Copyright 2022 Chris Morgan - * - * 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. - */ - -#include -#include -#include -#include -#include -#include -#include -#include -#include "esp_check.h" -#include "esp_log.h" -#include "freertos/FreeRTOS.h" -#include "freertos/task.h" -#include "freertos/queue.h" -#include "sdkconfig.h" -#include "audio_player.h" -#include "audio_wav.h" -#include "audio_mp3.h" - -static const char *TAG = "audio"; - -typedef enum { - AUDIO_PLAYER_REQUEST_NONE = 0, - AUDIO_PLAYER_REQUEST_PAUSE, /**< pause playback */ - AUDIO_PLAYER_REQUEST_RESUME, /**< resumed paused playback */ - AUDIO_PLAYER_REQUEST_PLAY, /**< initiate playing a new file */ - AUDIO_PLAYER_REQUEST_STOP, /**< stop playback */ - AUDIO_PLAYER_REQUEST_SHUTDOWN_THREAD, /**< shutdown audio playback thread */ - AUDIO_PLAYER_REQUEST_MAX -} audio_player_event_type_t; - -typedef struct { - audio_player_event_type_t type; - - // valid if type == AUDIO_PLAYER_EVENT_TYPE_PLAY - uint8_t *stream; - uint32_t stream_len; -} audio_player_event_t; - -typedef enum { - FILE_TYPE_UNKNOWN, -#if defined(CONFIG_AUDIO_PLAYER_ENABLE_MP3) - FILE_TYPE_MP3, -#endif -#if defined(CONFIG_AUDIO_PLAYER_ENABLE_WAV) - FILE_TYPE_WAV -#endif -} FILE_TYPE; - -typedef struct audio_instance { - /** - * Set to true before task is created, false immediately before the - * task is deleted. - */ - bool running; - - decode_data output; - - QueueHandle_t event_queue; - - /* **************** AUDIO CALLBACK **************** */ - audio_player_cb_t s_audio_cb; - void *audio_cb_usrt_ctx; - audio_player_state_t state; - - audio_player_config_t config; - -#if defined(CONFIG_AUDIO_PLAYER_ENABLE_WAV) - wav_instance wav_data; -#endif - -#if defined(CONFIG_AUDIO_PLAYER_ENABLE_MP3) - HMP3Decoder mp3_decoder; - mp3_instance mp3_data; -#endif -} audio_instance_t; - -static audio_instance_t instance; - -audio_player_state_t audio_player_get_state() -{ - return instance.state; -} - -esp_err_t audio_player_callback_register(audio_player_cb_t call_back, void *user_ctx) -{ - ESP_RETURN_ON_FALSE(esp_ptr_executable(reinterpret_cast(call_back)), ESP_ERR_INVALID_ARG, - TAG, "Not a valid call back"); - - instance.s_audio_cb = call_back; - instance.audio_cb_usrt_ctx = user_ctx; - - return ESP_OK; -} - -// This function is used in some optional logging functions so we don't want to -// have a cppcheck warning here -// cppcheck-suppress unusedFunction -const char *event_to_string(audio_player_callback_event_t event) -{ - switch (event) { - case AUDIO_PLAYER_CALLBACK_EVENT_IDLE: - return "AUDIO_PLAYER_CALLBACK_EVENT_IDLE"; - case AUDIO_PLAYER_CALLBACK_EVENT_COMPLETED_PLAYING_NEXT: - return "AUDIO_PLAYER_CALLBACK_EVENT_COMPLETED_PLAYING_NEXT"; - case AUDIO_PLAYER_CALLBACK_EVENT_PLAYING: - return "AUDIO_PLAYER_CALLBACK_EVENT_PLAYING"; - case AUDIO_PLAYER_CALLBACK_EVENT_PAUSE: - return "AUDIO_PLAYER_CALLBACK_EVENT_PAUSE"; - case AUDIO_PLAYER_CALLBACK_EVENT_SHUTDOWN: - return "AUDIO_PLAYER_CALLBACK_EVENT_SHUTDOWN"; - case AUDIO_PLAYER_CALLBACK_EVENT_UNKNOWN_FILE_TYPE: - return "AUDIO_PLAYER_CALLBACK_EVENT_UNKNOWN_FILE_TYPE"; - case AUDIO_PLAYER_CALLBACK_EVENT_UNKNOWN: - return "AUDIO_PLAYER_CALLBACK_EVENT_UNKNOWN"; - } - - return "unknown event"; -} - -static audio_player_callback_event_t state_to_event(audio_player_state_t state) -{ - audio_player_callback_event_t event = AUDIO_PLAYER_CALLBACK_EVENT_UNKNOWN; - - switch (state) { - case AUDIO_PLAYER_STATE_IDLE: - event = AUDIO_PLAYER_CALLBACK_EVENT_IDLE; - break; - case AUDIO_PLAYER_STATE_PAUSE: - event = AUDIO_PLAYER_CALLBACK_EVENT_PAUSE; - break; - case AUDIO_PLAYER_STATE_PLAYING: - event = AUDIO_PLAYER_CALLBACK_EVENT_PLAYING; - break; - case AUDIO_PLAYER_STATE_SHUTDOWN: - event = AUDIO_PLAYER_CALLBACK_EVENT_SHUTDOWN; - break; - }; - - return event; -} - -static void dispatch_callback(audio_instance_t *i, audio_player_callback_event_t event) -{ - LOGI_1("event '%s'", event_to_string(event)); - - if (esp_ptr_executable(reinterpret_cast(i->s_audio_cb))) { - audio_player_cb_ctx_t ctx = { - .audio_event = event, - .user_ctx = i->audio_cb_usrt_ctx, - }; - i->s_audio_cb(&ctx); - } -} - -static void set_state(audio_instance_t *i, audio_player_state_t new_state) -{ - if (i->state != new_state) { - i->state = new_state; - audio_player_callback_event_t event = state_to_event(new_state); - dispatch_callback(i, event); - } -} - -static void audio_instance_init(audio_instance_t &i) -{ - i.event_queue = NULL; - i.s_audio_cb = NULL; - i.audio_cb_usrt_ctx = NULL; - i.state = AUDIO_PLAYER_STATE_IDLE; -} - -static esp_err_t mono_to_stereo(uint32_t output_bits_per_sample, decode_data &adata) -{ - size_t data = adata.frame_count * (output_bits_per_sample / BITS_PER_BYTE); - data *= 2; - - // do we have enough space in the output buffer to convert mono to stereo? - if (data > adata.samples_capacity_max) { - ESP_LOGE(TAG, "insufficient space in output.samples to convert mono to stereo, need %d, have %d", data, adata.samples_capacity_max); - return ESP_ERR_NO_MEM; - } - - size_t new_sample_count = adata.frame_count * 2; - - // convert from back to front to allow conversion in-place - // - // NOTE: -1 is because we want to shift to the sample at position X - // but if we do (ptr + X) we end up at the sample at index X instead - // which is one further - int16_t *out = reinterpret_cast(adata.samples) + (new_sample_count - 1); - int16_t *in = reinterpret_cast(adata.samples) + (adata.frame_count - 1); - size_t samples = adata.frame_count; - while (samples) { - // write right channel - *out = *in; - out--; - - // write left channel - *out = *in; - out--; - - // move input buffer back and decrement samples - in--; - samples--; - } - - // adjust channels to 2 - adata.fmt.channels = 2; - - return ESP_OK; -} - -static esp_err_t aplay_file(audio_instance_t *i, uint8_t *stream, uint32_t stream_len) -{ - format i2s_format; - memset(&i2s_format, 0, sizeof(i2s_format)); - - esp_err_t ret = ESP_OK; - audio_player_event_t audio_event = { .type = AUDIO_PLAYER_REQUEST_NONE, .stream = NULL, .stream_len = 0 }; - - FILE_TYPE file_type = FILE_TYPE_UNKNOWN; - uint8_t *p_stream = stream; - // LOGI_1("start to decode,%p, %p, stream_len:%d", stream, p_stream, stream_len); - LOGI_1("start to decode, %p, %p, stream_len:%" PRIu32, stream, p_stream, stream_len); - - -#if defined(CONFIG_AUDIO_PLAYER_ENABLE_MP3) - if (is_mp3(p_stream)) { - file_type = FILE_TYPE_MP3; - LOGI_1("file is mp3"); - - // initialize mp3_instance - i->mp3_data.bytes_in_data_buf = 0; - i->mp3_data.read_ptr = i->mp3_data.data_buf; - i->mp3_data.eof_reached = false; - } -#endif - -#if defined(CONFIG_AUDIO_PLAYER_ENABLE_WAV) - // This can be a pointless condition depending on the build options, no reason to warn about it - // cppcheck-suppress knownConditionTrueFalse - if (file_type == FILE_TYPE_UNKNOWN) { - if (is_wav(&p_stream, &i->wav_data)) { - file_type = FILE_TYPE_WAV; - LOGI_1("file is wav"); - } - } -#endif - - // cppcheck-suppress knownConditionTrueFalse - if (file_type == FILE_TYPE_UNKNOWN) { - ESP_LOGE(TAG, "unknown file type, cleaning up"); - dispatch_callback(i, AUDIO_PLAYER_CALLBACK_EVENT_UNKNOWN_FILE_TYPE); - goto clean_up; - } - - do { - /* Process audio event sent from other task */ - if (pdPASS == xQueuePeek(i->event_queue, &audio_event, 0)) { - LOGI_2("event in queue"); - if (AUDIO_PLAYER_REQUEST_PAUSE == audio_event.type) { - // receive the pause event to take it off of the queue - xQueueReceive(i->event_queue, &audio_event, 0); - - set_state(i, AUDIO_PLAYER_STATE_PAUSE); - - // wait until an event is received that will cause playback to resume, - // stop, or change file - while (1) { - xQueuePeek(i->event_queue, &audio_event, portMAX_DELAY); - - if ((AUDIO_PLAYER_REQUEST_PLAY != audio_event.type) && - (AUDIO_PLAYER_REQUEST_STOP != audio_event.type) && - (AUDIO_PLAYER_REQUEST_RESUME != audio_event.type)) { - // receive to discard the event - xQueueReceive(i->event_queue, &audio_event, 0); - } else { - break; - } - } - - if (AUDIO_PLAYER_REQUEST_RESUME == audio_event.type) { - // receive to discard the event - xQueueReceive(i->event_queue, &audio_event, 0); - continue; - } - - // else fall out of this condition and let the below logic - // handle the other event types - } - - if ((AUDIO_PLAYER_REQUEST_STOP == audio_event.type) || - (AUDIO_PLAYER_REQUEST_PLAY == audio_event.type)) { - ret = ESP_OK; - goto clean_up; - } else { - // receive to discard the event, this event has no - // impact on the state of playback - xQueueReceive(i->event_queue, &audio_event, 0); - continue; - } - } - - set_state(i, AUDIO_PLAYER_STATE_PLAYING); - - DECODE_STATUS decode_status = DECODE_STATUS_ERROR; - - switch (file_type) { -#if defined(CONFIG_AUDIO_PLAYER_ENABLE_MP3) - case FILE_TYPE_MP3: - decode_status = decode_mp3(i->mp3_decoder, &p_stream, stream, stream_len, &i->output, &i->mp3_data); - break; -#endif -#if defined(CONFIG_AUDIO_PLAYER_ENABLE_WAV) - case FILE_TYPE_WAV: - decode_status = decode_wav(&p_stream, stream, stream_len, &i->output, &i->wav_data); - break; -#endif - case FILE_TYPE_UNKNOWN: - ESP_LOGE(TAG, "unexpected unknown file type when decoding"); - break; - } - - // break out and exit if we aren't supposed to continue decoding - if (decode_status == DECODE_STATUS_CONTINUE) { - // if mono, convert to stereo as es8311 requires stereo input - // even though it is mono output - if (i->output.fmt.channels == 1) { - LOGI_3("c == 1, mono -> stereo"); - ret = mono_to_stereo(i->output.fmt.bits_per_sample, i->output); - if (ret != ESP_OK) { - goto clean_up; - } - } - - /* Configure I2S clock if the output format changed */ - if ((i2s_format.sample_rate != i->output.fmt.sample_rate) || - (i2s_format.channels != i->output.fmt.channels) || - (i2s_format.bits_per_sample != i->output.fmt.bits_per_sample)) { - i2s_format = i->output.fmt; - LOGI_1("format change: sr=%d, bit=%" PRIu32 ", ch=%" PRIu32, - i2s_format.sample_rate, - i2s_format.bits_per_sample, - i2s_format.channels); - i2s_slot_mode_t channel_setting = (i2s_format.channels == 1) ? I2S_SLOT_MODE_MONO : I2S_SLOT_MODE_STEREO; - ret = i->config.clk_set_fn(i2s_format.sample_rate, - i2s_format.bits_per_sample, - channel_setting); - ESP_GOTO_ON_ERROR(ret, clean_up, TAG, "i2s_set_clk"); - } - - /** - * Block until all data has been accepted into the i2s driver, however - * the i2s driver has been configured with a buffer to allow for the next round of - * audio decoding to occur while the previous set of samples is finishing playback, in order - * to ensure playback without interruption. - */ - size_t i2s_bytes_written = 0; - size_t bytes_to_write = i->output.frame_count * i->output.fmt.channels * (i2s_format.bits_per_sample / 8); - LOGI_2("c %d, bps %d, bytes %d, frame_count %d", - i->output.fmt.channels, - i2s_format.bits_per_sample, - bytes_to_write, - i->output.frame_count); - - i->config.write_fn(i->output.samples, bytes_to_write, &i2s_bytes_written, portMAX_DELAY); - if (bytes_to_write != i2s_bytes_written) { - ESP_LOGE(TAG, "to write %d != written %d", bytes_to_write, i2s_bytes_written); - } - } else if (decode_status == DECODE_STATUS_NO_DATA_CONTINUE) { - LOGI_2("no data"); - } else { // DECODE_STATUS_DONE || DECODE_STATUS_ERROR - LOGI_1("breaking out of playback"); - break; - } - } while (true); - -clean_up: - return ret; -} - -static void audio_task(void *pvParam) -{ - audio_instance_t *i = static_cast(pvParam); - audio_player_event_t audio_event; - - while (true) { - // pull items off of the queue until we run into a PLAY request - while (true) { - // zero delay in the case where we are playing as we want to - // send an event indicating either - // PLAYING -> IDLE (IDLE) or PLAYING -> PLAYING (COMPLETED PLAYING NEXT) - // and thus don't want to block until the next request comes in - // in the case when there are no further requests pending - int delay = (i->state == AUDIO_PLAYER_STATE_PLAYING) ? 0 : portMAX_DELAY; - - int retval = xQueuePeek(i->event_queue, &audio_event, delay); - if (pdPASS == retval) { // item on the queue, process it - xQueueReceive(i->event_queue, &audio_event, 0); - - // if the item is a play request, process it - if (AUDIO_PLAYER_REQUEST_PLAY == audio_event.type) { - if (i->state == AUDIO_PLAYER_STATE_PLAYING) { - dispatch_callback(i, AUDIO_PLAYER_CALLBACK_EVENT_COMPLETED_PLAYING_NEXT); - } else { - set_state(i, AUDIO_PLAYER_STATE_PLAYING); - } - - break; - } else if (AUDIO_PLAYER_REQUEST_SHUTDOWN_THREAD == audio_event.type) { - set_state(i, AUDIO_PLAYER_STATE_SHUTDOWN); - i->running = false; - - // should never return - vTaskDelete(NULL); - break; - } else { - // ignore other events when not playing - } - } else { // no items on the queue - // if we are playing transition to idle and indicate the transition via callback - if (i->state == AUDIO_PLAYER_STATE_PLAYING) { - set_state(i, AUDIO_PLAYER_STATE_IDLE); - } - } - } - - i->config.mute_fn(AUDIO_PLAYER_UNMUTE); - esp_err_t ret_val = aplay_file(i, audio_event.stream, audio_event.stream_len); - if (ret_val != ESP_OK) { - ESP_LOGE(TAG, "aplay_file() %d", ret_val); - } - i->config.mute_fn(AUDIO_PLAYER_MUTE); - - // if(audio_event.stream) fclose(audio_event.stream); - } -} - -/* **************** AUDIO PLAY CONTROL **************** */ -static esp_err_t audio_send_event(audio_instance_t *i, audio_player_event_t event) -{ - ESP_RETURN_ON_FALSE(NULL != i->event_queue, ESP_ERR_INVALID_STATE, - TAG, "Audio task not started yet"); - - BaseType_t ret_val = xQueueSend(i->event_queue, &event, 0); - - ESP_RETURN_ON_FALSE(pdPASS == ret_val, ESP_ERR_INVALID_STATE, - TAG, "The last event has not been processed yet"); - - return ESP_OK; -} - -esp_err_t audio_player_play(uint8_t *stream, uint32_t file_len) -{ - LOGI_1("%s", __FUNCTION__); - audio_player_event_t event = { .type = AUDIO_PLAYER_REQUEST_PLAY, .stream = stream, .stream_len = file_len}; - return audio_send_event(&instance, event); -} - -esp_err_t audio_player_pause(void) -{ - LOGI_1("%s", __FUNCTION__); - audio_player_event_t event = { .type = AUDIO_PLAYER_REQUEST_PAUSE, .stream = NULL, .stream_len = 0}; - return audio_send_event(&instance, event); -} - -esp_err_t audio_player_resume(void) -{ - LOGI_1("%s", __FUNCTION__); - audio_player_event_t event = { .type = AUDIO_PLAYER_REQUEST_RESUME, .stream = NULL, .stream_len = 0}; - return audio_send_event(&instance, event); -} - -esp_err_t audio_player_stop(void) -{ - LOGI_1("%s", __FUNCTION__); - audio_player_event_t event = { .type = AUDIO_PLAYER_REQUEST_STOP, .stream = NULL, .stream_len = 0}; - return audio_send_event(&instance, event); -} - -/** - * Can only shut down the playback thread if the thread is not presently playing audio. - * Call audio_player_stop() - */ -static esp_err_t _internal_audio_player_shutdown_thread(void) -{ - LOGI_1("%s", __FUNCTION__); - audio_player_event_t event = { .type = AUDIO_PLAYER_REQUEST_SHUTDOWN_THREAD, .stream = NULL, .stream_len = 0 }; - return audio_send_event(&instance, event); -} - -static void cleanup_memory(audio_instance_t &i) -{ -#if defined(CONFIG_AUDIO_PLAYER_ENABLE_MP3) - if (i.mp3_decoder) { - MP3FreeDecoder(i.mp3_decoder); - } - if (i.mp3_data.data_buf) { - free(i.mp3_data.data_buf); - } -#endif - if (i.output.samples) { - free(i.output.samples); - } - - vQueueDelete(i.event_queue); -} - -esp_err_t audio_player_new(audio_player_config_t config) -{ - BaseType_t task_val; - - audio_instance_init(instance); - - instance.config = config; - - /* Audio control event queue */ - instance.event_queue = xQueueCreate(4, sizeof(audio_player_event_t)); - ESP_RETURN_ON_FALSE(NULL != instance.event_queue, -1, TAG, "xQueueCreate"); - - /** See https://github.com/ultraembedded/libhelix-mp3/blob/0a0e0673f82bc6804e5a3ddb15fb6efdcde747cd/testwrap/main.c#L74 */ - instance.output.samples_capacity = MAX_NCHAN * MAX_NGRAN * MAX_NSAMP; - instance.output.samples_capacity_max = instance.output.samples_capacity * 2; - instance.output.samples = static_cast(malloc(instance.output.samples_capacity_max)); - LOGI_1("samples_capacity %d bytes", instance.output.samples_capacity_max); - int ret = ESP_OK; - ESP_GOTO_ON_FALSE(NULL != instance.output.samples, ESP_ERR_NO_MEM, cleanup, - TAG, "Failed allocate output buffer"); - -#if defined(CONFIG_AUDIO_PLAYER_ENABLE_MP3) - instance.mp3_data.data_buf_size = MAINBUF_SIZE * 3; - instance.mp3_data.data_buf = static_cast(malloc(instance.mp3_data.data_buf_size)); - ESP_GOTO_ON_FALSE(NULL != instance.mp3_data.data_buf, ESP_ERR_NO_MEM, cleanup, - TAG, "Failed allocate mp3 data buffer"); - - instance.mp3_decoder = MP3InitDecoder(); - ESP_GOTO_ON_FALSE(NULL != instance.mp3_decoder, ESP_ERR_NO_MEM, cleanup, - TAG, "Failed create MP3 decoder"); -#endif - - instance.running = true; - task_val = xTaskCreatePinnedToCore( - (TaskFunction_t) audio_task, - "Audio Task", - 4 * 1024, - &instance, - (UBaseType_t) instance.config.priority, - NULL, - 0); - - ESP_GOTO_ON_FALSE(pdPASS == task_val, ESP_ERR_NO_MEM, cleanup, - TAG, "Failed create audio task"); - - // start muted - instance.config.mute_fn(AUDIO_PLAYER_MUTE); - - return ret; - -// At the moment when we run cppcheck there is a lack of esp-idf header files this -// means cppcheck doesn't know that ESP_GOTO_ON_FALSE() etc are making use of this label -// cppcheck-suppress unusedLabelConfiguration -cleanup: - cleanup_memory(instance); - - return ret; -} - -esp_err_t audio_player_delete() -{ - const int MAX_RETRIES = 5; - int retries = MAX_RETRIES; - while (instance.running && retries) { - // stop any playback and shutdown the thread - audio_player_stop(); - _internal_audio_player_shutdown_thread(); - - vTaskDelay(pdMS_TO_TICKS(100)); - retries--; - } - - cleanup_memory(instance); - - // if we ran out of retries, return fail code - if (retries == 0) { - return ESP_FAIL; - } - - return ESP_OK; -} diff --git a/examples/chatgpt_demo/components/chmorgan__esp-audio-player/audio_wav.cpp b/examples/chatgpt_demo/components/chmorgan__esp-audio-player/audio_wav.cpp deleted file mode 100644 index a4327b7..0000000 --- a/examples/chatgpt_demo/components/chmorgan__esp-audio-player/audio_wav.cpp +++ /dev/null @@ -1,102 +0,0 @@ -#include -#include -#include "audio_wav.h" - -static const char *TAG = "wav"; - -/** - * @param stream - * @param pInstance - Values can be considered valid if true is returned - * @return true if uint8_t is a wav uint8_t - */ -bool is_wav(uint8_t **stream, wav_instance *pInstance) { - - // assert(*stream); - memcpy(&pInstance->header, *stream, sizeof(wav_header_t)); - size_t bytes_read = sizeof(wav_header_t); - if(bytes_read != sizeof(wav_header_t)) { - return false; - } - - wav_header_t *wav_head = &pInstance->header; - if((NULL == strstr(reinterpret_cast(wav_head->ChunkID), "RIFF")) || - (NULL == strstr(reinterpret_cast(wav_head->Format), "WAVE")) - ) - { - return false; - } - - // decode chunks until we find the 'data' one - wav_subchunk_header_t subchunk; - do{ - // bytes_read = fread(&subchunk, 1, sizeof(wav_subchunk_header_t), stream); - memcpy(&subchunk, *stream + sizeof(wav_header_t), sizeof(wav_subchunk_header_t)); - bytes_read = sizeof(wav_subchunk_header_t); - - if(bytes_read != sizeof(wav_subchunk_header_t)) { - return false; - } - - if(memcmp(subchunk.SubchunkID, "data", 4) == 0) - { - // advance beyond this subchunk, it could be a 'LIST' chunk with uint8_t info or some other unhandled subchunk - // fseek(stream, subchunk.SubchunkSize, SEEK_CUR); - (*stream) += (sizeof(wav_subchunk_header_t) + sizeof(wav_header_t)); - } - }while(false); - - LOGI_2("sample_rate=%d, channels=%d, bps=%d", - wav_head->SampleRate, - wav_head->NumChannels, - wav_head->BitsPerSample); - - return true; -} - -/** - * @return true if data remains, false on error or end of uint8_t - */ -DECODE_STATUS decode_wav(uint8_t **stream, uint8_t *base, uint32_t stream_len, decode_data *pData, wav_instance *pInstance) { - // read an even multiple of frames that can fit into output_samples buffer, otherwise - // we would have to manage what happens with partial frames in the output buffer - size_t bytes_per_frame = (pInstance->header.BitsPerSample / BITS_PER_BYTE) * pInstance->header.NumChannels; - size_t frames_to_read = pData->samples_capacity / bytes_per_frame; - size_t bytes_to_read = frames_to_read * bytes_per_frame; - - // size_t bytes_read = fread(pData->samples, 1, bytes_to_read, stream); - size_t bytes_read; - if((*stream - base) < stream_len){ - if((*stream + bytes_to_read) > (base + stream_len)){ - bytes_read = (base + stream_len) - *stream; - } - else{ - bytes_read = bytes_to_read; - } - } - else{ - bytes_read = 0; - } - - if(bytes_read){ - memcpy(pData->samples, *stream, bytes_read); - } - (*stream) +=bytes_read; - - pData->fmt.channels = pInstance->header.NumChannels; - pData->fmt.bits_per_sample = pInstance->header.BitsPerSample; - pData->fmt.sample_rate = pInstance->header.SampleRate; - - if(bytes_read != 0) - { - pData->frame_count = (bytes_read / (pInstance->header.BitsPerSample / BITS_PER_BYTE)) / pInstance->header.NumChannels; - } else { - pData->frame_count = 0; - } - - LOGI_2("%p, bytes_per_frame %d, bytes_to_read %d, bytes_read %d, frame_count %d", - *stream, - bytes_per_frame, bytes_to_read, bytes_read, - pData->frame_count); - - return (bytes_read == 0) ? DECODE_STATUS_DONE : DECODE_STATUS_CONTINUE; -} diff --git a/examples/chatgpt_demo/components/chmorgan__esp-audio-player/audio_wav.h b/examples/chatgpt_demo/components/chmorgan__esp-audio-player/audio_wav.h deleted file mode 100644 index ca17a99..0000000 --- a/examples/chatgpt_demo/components/chmorgan__esp-audio-player/audio_wav.h +++ /dev/null @@ -1,34 +0,0 @@ -#pragma once - -#include -#include "audio_log.h" -#include "audio_decode_types.h" - -typedef struct { - // The "RIFF" chunk descriptor - uint8_t ChunkID[4]; - int32_t ChunkSize; - uint8_t Format[4]; - // The "fmt" sub-chunk - uint8_t Subchunk1ID[4]; - int32_t Subchunk1Size; - int16_t AudioFormat; - int16_t NumChannels; - int32_t SampleRate; - int32_t ByteRate; - int16_t BlockAlign; - int16_t BitsPerSample; -} wav_header_t; - -typedef struct { - // The "data" sub-chunk - uint8_t SubchunkID[4]; - int32_t SubchunkSize; -} wav_subchunk_header_t; - -typedef struct { - wav_header_t header; -} wav_instance; - -bool is_wav(uint8_t **stream, wav_instance *pInstance); -DECODE_STATUS decode_wav(uint8_t **stream, uint8_t *base, uint32_t stream_len, decode_data *pData, wav_instance *pInstance); diff --git a/examples/chatgpt_demo/components/chmorgan__esp-audio-player/idf_component.yml b/examples/chatgpt_demo/components/chmorgan__esp-audio-player/idf_component.yml deleted file mode 100644 index 702766c..0000000 --- a/examples/chatgpt_demo/components/chmorgan__esp-audio-player/idf_component.yml +++ /dev/null @@ -1,8 +0,0 @@ -dependencies: - chmorgan/esp-libhelix-mp3: - version: '>=1.0.0,<2.0.0' - idf: - version: '>=5.0' -description: Lightweight audio decoding component for esp processors -url: https://github.com/chmorgan/esp-audio-player -version: 1.0.4 diff --git a/examples/chatgpt_demo/components/chmorgan__esp-audio-player/include/audio_player.h b/examples/chatgpt_demo/components/chmorgan__esp-audio-player/include/audio_player.h deleted file mode 100644 index 1c63a7e..0000000 --- a/examples/chatgpt_demo/components/chmorgan__esp-audio-player/include/audio_player.h +++ /dev/null @@ -1,181 +0,0 @@ -/** - * @file - * @version 0.1 - * - * @copyright Copyright 2021 Espressif Systems (Shanghai) Co. Ltd. - * @copyright Copyright 2022 Chris Morgan - * - * 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. - */ - -/** - * Design notes - * - * - There is a distinct event for playing -> playing state transitions. - * COMPLETED_PLAYING_NEXT is helpful for users of the audio player to know - * the difference between playing and transitioning to another audio file - * vs. detecting that the audio file transitioned by looking at - * events indicating IDLE and then PLAYING within a short period of time. - * - * State machine diagram - * - * cb is the callback function registered with audio_player_callback_register() - * - * cb(PLAYING) cb(PLAYING) - * _______________________________ ____________________________________ - * | | | | - * | | | | - * | cb(IDLE) V V cb(PAUSE) | - * Idle <------------------------ Playing ----------------------------> Pause - * ^ |_____^ | - * | cb(COMPLETED_PLAYING_NEXT) | - * | | - * |______________________________________________________________________| - * cb(IDLE) - * - */ - -#pragma once - -#include -#include -#include "esp_err.h" -#include "freertos/FreeRTOS.h" -#include "driver/i2s_std.h" - -#ifdef __cplusplus -extern "C" { -#endif - -typedef enum { - AUDIO_PLAYER_STATE_IDLE, - AUDIO_PLAYER_STATE_PLAYING, - AUDIO_PLAYER_STATE_PAUSE, - AUDIO_PLAYER_STATE_SHUTDOWN -} audio_player_state_t; - -/** - * @brief Get the audio player state - * - * @return the present audio_player_state_t - */ -audio_player_state_t audio_player_get_state(); - -typedef enum { - AUDIO_PLAYER_CALLBACK_EVENT_IDLE, /**< Player is idle, not playing audio */ - AUDIO_PLAYER_CALLBACK_EVENT_COMPLETED_PLAYING_NEXT, /**< Player is playing and playing a new audio file */ - AUDIO_PLAYER_CALLBACK_EVENT_PLAYING, /**< Player is playing */ - AUDIO_PLAYER_CALLBACK_EVENT_PAUSE, /**< Player is pausing */ - AUDIO_PLAYER_CALLBACK_EVENT_SHUTDOWN, /**< Player is shutting down */ - AUDIO_PLAYER_CALLBACK_EVENT_UNKNOWN_FILE_TYPE, /**< File type is unknown */ - AUDIO_PLAYER_CALLBACK_EVENT_UNKNOWN /**< Unknown event */ -} audio_player_callback_event_t; - -typedef struct { - audio_player_callback_event_t audio_event; - void *user_ctx; -} audio_player_cb_ctx_t; - -/** Audio callback function type */ -typedef void (*audio_player_cb_t)(audio_player_cb_ctx_t *); - -/** - * @brief Play mp3 audio file. - * - * Will interrupt a present playback and start the new playback - * as soon as possible. - * - * @param fp - If ESP_OK is returned, will be fclose()ed by the audio system - * when the playback has completed or in the event of a playback error. - * If not ESP_OK returned then should be fclose()d by the caller. - * @return - * - ESP_OK: Success in queuing play request - * - Others: Fail - */ -esp_err_t audio_player_play(uint8_t *stream, uint32_t file_len); - -/** - * @brief Pause playback - * - * @return - * - ESP_OK: Success in queuing pause request - * - Others: Fail - */ -esp_err_t audio_player_pause(void); - -/** - * @brief Resume playback - * - * Has no effect if playback is not in progress - * @return esp_err_t - * - ESP_OK: Success in queuing resume request - * - Others: Fail - */ -esp_err_t audio_player_resume(void); - -/** - * @brief Stop playback - * - * Has no effect if playback is already stopped - * @return esp_err_t - * - ESP_OK: Success in queuing resume request - * - Others: Fail - */ -esp_err_t audio_player_stop(void); - -/** - * @brief Register callback for audio event - * - * @param call_back Call back function - * @param user_ctx User context - * @return - * - ESP_OK: Success - * - Others: Fail - */ -esp_err_t audio_player_callback_register(audio_player_cb_t call_back, void *user_ctx); - -typedef enum { - AUDIO_PLAYER_MUTE, - AUDIO_PLAYER_UNMUTE -} AUDIO_PLAYER_MUTE_SETTING; - -typedef esp_err_t (*audio_player_mute_fn)(AUDIO_PLAYER_MUTE_SETTING setting); -typedef esp_err_t (*audio_reconfig_std_clock)(uint32_t rate, uint32_t bits_cfg, i2s_slot_mode_t ch); -typedef esp_err_t (*audio_player_write_fn)(void *audio_buffer, size_t len, size_t *bytes_written, uint32_t timeout_ms); - -typedef struct { - audio_player_mute_fn mute_fn; - audio_reconfig_std_clock clk_set_fn; - audio_player_write_fn write_fn; - UBaseType_t priority; /*< FreeRTOS task priority */ -} audio_player_config_t; - -/** - * @brief Initialize hardware, allocate memory, create and start audio task. - * Call before any other 'audio' functions. - * - * @param port - The i2s port for output - * @return esp_err_t - */ -esp_err_t audio_player_new(audio_player_config_t config); - -/** - * @brief Shut down audio task, free allocated memory. - * - * @return esp_err_t ESP_OK upon success, ESP_FAIL if unable to shutdown due to retries exhausted - */ -esp_err_t audio_player_delete(); - -#ifdef __cplusplus -} -#endif diff --git a/examples/chatgpt_demo/main/app/app_audio.c b/examples/chatgpt_demo/main/app/app_audio.c index 875d175..a45783e 100644 --- a/examples/chatgpt_demo/main/app/app_audio.c +++ b/examples/chatgpt_demo/main/app/app_audio.c @@ -64,12 +64,48 @@ static esp_err_t audio_mute_function(AUDIO_PLAYER_MUTE_SETTING setting) // restore the voice volume upon unmuting if (setting == AUDIO_PLAYER_UNMUTE) { bsp_codec_volume_set(CONFIG_VOLUME_LEVEL, NULL); - } else { + } + return ESP_OK; +} + +static esp_err_t audio_codec_set_fs(uint32_t rate, uint32_t bits_cfg, i2s_slot_mode_t ch) +{ + esp_err_t ret = ESP_OK; + ret = bsp_codec_set_fs(rate, bits_cfg, ch); + + bsp_codec_mute_set(true); + bsp_codec_mute_set(false); + bsp_codec_volume_set(CONFIG_VOLUME_LEVEL, NULL); + vTaskDelay(pdMS_TO_TICKS(50)); + + return ret; +} + +static void audio_player_cb(audio_player_cb_ctx_t *ctx) +{ + switch (ctx->audio_event) { + case AUDIO_PLAYER_CALLBACK_EVENT_IDLE: + ESP_LOGI(TAG, "Player IDLE"); + bsp_codec_set_fs(16000, 16, 2); if (audio_play_finish_cb) { audio_play_finish_cb(); } + break; + case AUDIO_PLAYER_CALLBACK_EVENT_COMPLETED_PLAYING_NEXT: + ESP_LOGI(TAG, "Player NEXT"); + break; + case AUDIO_PLAYER_CALLBACK_EVENT_PLAYING: + ESP_LOGI(TAG, "Player PLAYING"); + break; + case AUDIO_PLAYER_CALLBACK_EVENT_PAUSE: + ESP_LOGI(TAG, "Player PAUSE"); + break; + case AUDIO_PLAYER_CALLBACK_EVENT_SHUTDOWN: + ESP_LOGI(TAG, "Player SHUTDOWN"); + break; + default: + break; } - return ESP_OK; } void audio_record_init() @@ -94,10 +130,11 @@ void audio_record_init() audio_player_config_t config = { .mute_fn = audio_mute_function, .write_fn = bsp_i2s_write, - .clk_set_fn = bsp_codec_set_fs, + .clk_set_fn = audio_codec_set_fs, .priority = 5 }; ESP_ERROR_CHECK(audio_player_new(config)); + audio_player_callback_register(audio_player_cb, NULL); } void audio_record_save(int16_t *audio_buffer, int audio_chunksize) @@ -177,7 +214,6 @@ static esp_err_t audio_record_stop() wav_head.Subchunk2Size = record_total_len; memcpy((void *)record_audio_buffer, &wav_head, sizeof(wav_header_t)); Cache_WriteBack_Addr((uint32_t)record_audio_buffer, record_total_len); - // audio_player_play(record_audio_buffer, file_total_len); #endif err: @@ -233,7 +269,6 @@ esp_err_t audio_play_task(void *filepath) total_cnt += cnt; } } while (1); - printf("audio play end, %d, %d K\r\n", total_cnt, total_cnt / 1024); EXIT: if (fp) { @@ -245,46 +280,6 @@ EXIT: return ret; } -esp_err_t audio_mp3_load(void *filepath, size_t *file_len) -{ - FILE *fp = NULL; - struct stat file_stat; - esp_err_t ret = ESP_OK; - - size_t len, total_cnt = 0; - - const size_t chunk_size = 4096; - uint8_t *buffer = malloc(chunk_size); - ESP_GOTO_ON_FALSE(NULL != buffer, ESP_FAIL, EXIT, TAG, "buffer malloc failed"); - - ESP_GOTO_ON_FALSE(-1 != stat(filepath, &file_stat), ESP_FAIL, EXIT, TAG, "Failed to stat file"); - - fp = fopen(filepath, "r"); - ESP_GOTO_ON_FALSE(NULL != fp, ESP_FAIL, EXIT, TAG, "Failed create record file"); - - do { - /* Read file in chunks into the scratch buffer */ - len = fread(buffer, 1, chunk_size, fp); - if (len <= 0) { - break; - } else if (len > 0) { - memcpy((void *)(audio_rx_buffer + total_cnt), buffer, len); - total_cnt += len; - } - } while (1); - printf("audio load end, %d, %d K\r\n", total_cnt, total_cnt / 1024); - -EXIT: - if (fp) { - fclose(fp); - } - if (buffer) { - free(buffer); - } - *file_len = total_cnt; - return ret; -} - void sr_handler_task(void *pvParam) { #if !CONFIG_BSP_BOARD_ESP32_S3_BOX_Lite @@ -317,18 +312,13 @@ void sr_handler_task(void *pvParam) if (ESP_MN_STATE_TIMEOUT == result.state) { ESP_LOGI(TAG, "ESP_MN_STATE_TIMEOUT"); audio_record_stop(); - // audio_play_task("/spiffs/echo_en_wake.wav"); - size_t len; - audio_mp3_load("/spiffs/waitPlease.mp3", &len); - if (len) { - audio_player_play(audio_rx_buffer, len); + FILE *fp = fopen("/spiffs/waitPlease.mp3", "r"); + if (fp) { + audio_player_play(fp); } - uint32_t starttime = esp_log_timestamp(); - ESP_LOGE(TAG, "[Start] start_openai, timestamp: %" PRIu32, starttime); if (WIFI_STATUS_CONNECTED_OK == wifi_connected_already()) { start_openai((uint8_t *)record_audio_buffer, record_total_len); } - ESP_LOGE(TAG, "[End] start_openai, +offset:%" PRIu32, esp_log_timestamp() - starttime); continue; } diff --git a/examples/chatgpt_demo/main/app/app_sr.c b/examples/chatgpt_demo/main/app/app_sr.c index ad0349b..2fc9a01 100644 --- a/examples/chatgpt_demo/main/app/app_sr.c +++ b/examples/chatgpt_demo/main/app/app_sr.c @@ -130,16 +130,12 @@ static void audio_detect_task(void *arg) if (local_state != res->vad_state) { local_state = res->vad_state; - if (AFE_VAD_SILENCE != local_state) { - ESP_LOGW(TAG, "%s, frame:%d", "silence", frame_keep); - } frame_keep = 0; } else { frame_keep++; } if ((100 == frame_keep) && (AFE_VAD_SILENCE == res->vad_state)) { - ESP_LOGW(TAG, "vad Time out"); sr_result_t result = { .wakenet_mode = WAKENET_NO_DETECT, .state = ESP_MN_STATE_TIMEOUT, diff --git a/examples/chatgpt_demo/main/idf_component.yml b/examples/chatgpt_demo/main/idf_component.yml index 3412728..00eb138 100644 --- a/examples/chatgpt_demo/main/idf_component.yml +++ b/examples/chatgpt_demo/main/idf_component.yml @@ -1,5 +1,6 @@ ## IDF Component Manager Manifest File dependencies: espressif/openai: "^0.3.0" - chmorgan/esp-file-iterator: "1.0.0" espressif/esp-sr: "1.3.3" + chmorgan/esp-audio-player: "1.0.6" + chmorgan/esp-file-iterator: "1.0.0" diff --git a/examples/chatgpt_demo/main/main.c b/examples/chatgpt_demo/main/main.c index 0dbb52a..f610376 100644 --- a/examples/chatgpt_demo/main/main.c +++ b/examples/chatgpt_demo/main/main.c @@ -114,7 +114,11 @@ esp_err_t start_openai(uint8_t *audio, int audio_len) OpenAI_SpeechResponse_t *speechresult = audioSpeech->speech(audioSpeech, response); uint32_t dataLength = speechresult->getLen(speechresult); char *speechptr = speechresult->getData(speechresult); - esp_err_t status = audio_player_play((uint8_t *)speechptr, dataLength); + esp_err_t status = ESP_FAIL; + FILE *fp = fmemopen((void *)speechptr, dataLength, "rb"); + if (fp) { + status = audio_player_play(fp); + } if (status != ESP_OK) { ESP_LOGE(TAG, "Error creating ChatGPT request: %s\n", esp_err_to_name(status)); diff --git a/examples/factory_demo/components/espressif__esp_schedule/.component_hash b/examples/factory_demo/components/espressif__esp_schedule/.component_hash new file mode 100644 index 0000000..43e2058 --- /dev/null +++ b/examples/factory_demo/components/espressif__esp_schedule/.component_hash @@ -0,0 +1 @@ +2cbc7e2584f523d10fbfb4ecb7d25c07fa7d7757ab23d7a127494b1a58131ffe \ No newline at end of file diff --git a/examples/factory_demo/components/espressif__esp_schedule/CMakeLists.txt b/examples/factory_demo/components/espressif__esp_schedule/CMakeLists.txt new file mode 100644 index 0000000..02eb952 --- /dev/null +++ b/examples/factory_demo/components/espressif__esp_schedule/CMakeLists.txt @@ -0,0 +1,10 @@ +set(component_srcs "src/esp_schedule.c" + "src/esp_schedule_nvs.c") + +idf_component_register(SRCS "${component_srcs}" + INCLUDE_DIRS "include" + PRIV_INCLUDE_DIRS "src" + PRIV_REQUIRES "rmaker_common" + REQUIRES "nvs_flash") + +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wno-unused-function") diff --git a/examples/chatgpt_demo/components/chmorgan__esp-audio-player/LICENSE b/examples/factory_demo/components/espressif__esp_schedule/LICENSE similarity index 100% rename from examples/chatgpt_demo/components/chmorgan__esp-audio-player/LICENSE rename to examples/factory_demo/components/espressif__esp_schedule/LICENSE diff --git a/examples/factory_demo/components/espressif__esp_schedule/README.md b/examples/factory_demo/components/espressif__esp_schedule/README.md new file mode 100644 index 0000000..3646ec0 --- /dev/null +++ b/examples/factory_demo/components/espressif__esp_schedule/README.md @@ -0,0 +1,50 @@ +# ESP Scheduling + +[![Component Registry](https://components.espressif.com/components/espressif/esp_schedule/badge.svg)](https://components.espressif.com/components/espressif/esp_schedule) + +This component is used internally by ESP RainMaker to implement schedules. + +> Note: By default, the time is w.r.t. UTC. If the timezone has been set, then the time is w.r.t. the specified timezone. + +## Test code: + +``` +#include + +void app_schedule_trigger_cb(esp_schedule_handle_t handle, void *priv_data) +{ + printf("priv_data: %.*s\n", (char *)priv_data); +} + +static char *priv_data_global = "from app"; + +void app_schedule_set() +{ + esp_schedule_config_t schedule_config = { + .name = "test", + .trigger.type = ESP_SCHEDULE_TYPE_DAYS_OF_WEEK, + .trigger.hours = 13, + .trigger.minutes = 30, + .trigger.day.repeat_days = ESP_SCHEDULE_DAY_MONDAY | ESP_SCHEDULE_DAY_THURSDAY, + .trigger_cb = app_schedule_trigger_cb, + .priv_data = priv_data_global, + }; + esp_schedule_create(&schedule_config); +} + +void app_schedule_init() +{ + uint8_t schedule_count; + esp_schedule_handle_t *schedule_list = esp_schedule_init(true, NULL, &schedule_count); + if (schedule_count > 0 && schedule_list != NULL) { + ESP_LOGI(TAG, "Found %d schedule(s) in NVS.", schedule_count); + for (size_t i = 0; i < schedule_count; i++) { + esp_schedule_config_t schedule_config; + esp_schedule_get(schedule_list[i], &schedule_config); + schedule_config.trigger_cb = app_schedule_trigger_cb; + schedule_config.priv_data = priv_data_global; + esp_schedule_edit(schedule_list[i], &schedule_config); + } + } +} +``` diff --git a/examples/factory_demo/components/espressif__esp_schedule/component.mk b/examples/factory_demo/components/espressif__esp_schedule/component.mk new file mode 100644 index 0000000..49844ec --- /dev/null +++ b/examples/factory_demo/components/espressif__esp_schedule/component.mk @@ -0,0 +1,5 @@ +COMPONENT_ADD_INCLUDEDIRS := include + +COMPONENT_SRCDIRS := src + +CFLAGS += -Wno-unused-function diff --git a/examples/factory_demo/components/espressif__esp_schedule/idf_component.yml b/examples/factory_demo/components/espressif__esp_schedule/idf_component.yml new file mode 100644 index 0000000..2bcb52a --- /dev/null +++ b/examples/factory_demo/components/espressif__esp_schedule/idf_component.yml @@ -0,0 +1,8 @@ +dependencies: + espressif/rmaker_common: + version: ~1.4.2 +description: ESP Schedules, used in RainMaker +issues: https://github.com/espressif/esp-rainmaker/issues +repository: https://github.com/espressif/esp-rainmaker.git +url: https://github.com/espressif/esp-rainmaker/tree/master/components/esp_schedule +version: 1.1.0 diff --git a/examples/factory_demo/components/espressif__esp_schedule/include/esp_schedule.h b/examples/factory_demo/components/espressif__esp_schedule/include/esp_schedule.h new file mode 100644 index 0000000..4c75ebf --- /dev/null +++ b/examples/factory_demo/components/espressif__esp_schedule/include/esp_schedule.h @@ -0,0 +1,236 @@ +/* + * SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +/** Schedule Handle */ +typedef void *esp_schedule_handle_t; + +/** Maximum length of the schedule name allowed. This value cannot be more than 16 as it is used for NVS key. */ +#define MAX_SCHEDULE_NAME_LEN 16 + +/** Callback for schedule trigger + * + * This callback is called when the schedule is triggered. + * + * @param[in] handle Schedule handle. + * @param[in] priv_data Pointer to the private data passed while creating/editing the schedule. + */ +typedef void (*esp_schedule_trigger_cb_t)(esp_schedule_handle_t handle, void *priv_data); + +/** Callback for schedule timestamp + * + * This callback is called when the next trigger timestamp of the schedule is changed. This might be useful to check if + * one time schedules have already passed while the device was powered off. + * + * @param[in] handle Schedule handle. + * @param[in] next_timestamp timestamp at which the schedule will trigger next. + * @param[in] priv_data Pointer to the user data passed while creating/editing the schedule. + */ +typedef void (*esp_schedule_timestamp_cb_t)(esp_schedule_handle_t handle, uint32_t next_timestamp, void *priv_data); + +/** Schedule type */ +typedef enum esp_schedule_type { + ESP_SCHEDULE_TYPE_INVALID = 0, + ESP_SCHEDULE_TYPE_DAYS_OF_WEEK, + ESP_SCHEDULE_TYPE_DATE, + ESP_SCHEDULE_TYPE_RELATIVE, +} esp_schedule_type_t; + +/** Schedule days. Used for ESP_SCHEDULE_TYPE_DAYS_OF_WEEK. */ +typedef enum esp_schedule_days { + ESP_SCHEDULE_DAY_ONCE = 0, + ESP_SCHEDULE_DAY_EVERYDAY = 0b1111111, + ESP_SCHEDULE_DAY_MONDAY = 1 << 0, + ESP_SCHEDULE_DAY_TUESDAY = 1 << 1, + ESP_SCHEDULE_DAY_WEDNESDAY = 1 << 2, + ESP_SCHEDULE_DAY_THURSDAY = 1 << 3, + ESP_SCHEDULE_DAY_FRIDAY = 1 << 4, + ESP_SCHEDULE_DAY_SATURDAY = 1 << 5, + ESP_SCHEDULE_DAY_SUNDAY = 1 << 6, +} esp_schedule_days_t; + +/** Schedule months. Used for ESP_SCHEDULE_TYPE_DATE. */ +typedef enum esp_schedule_months { + ESP_SCHEDULE_MONTH_ONCE = 0, + ESP_SCHEDULE_MONTH_ALL = 0b1111111, + ESP_SCHEDULE_MONTH_JANUARY = 1 << 0, + ESP_SCHEDULE_MONTH_FEBRUARY = 1 << 1, + ESP_SCHEDULE_MONTH_MARCH = 1 << 2, + ESP_SCHEDULE_MONTH_APRIL = 1 << 3, + ESP_SCHEDULE_MONTH_MAY = 1 << 4, + ESP_SCHEDULE_MONTH_JUNE = 1 << 5, + ESP_SCHEDULE_MONTH_JULY = 1 << 6, + ESP_SCHEDULE_MONTH_AUGUST = 1 << 7, + ESP_SCHEDULE_MONTH_SEPTEMBER = 1 << 8, + ESP_SCHEDULE_MONTH_OCTOBER = 1 << 9, + ESP_SCHEDULE_MONTH_NOVEMBER = 1 << 10, + ESP_SCHEDULE_MONTH_DECEMBER = 1 << 11, +} esp_schedule_months_t; + +/** Trigger details of the schedule */ +typedef struct esp_schedule_trigger { + /** Type of schedule */ + esp_schedule_type_t type; + /** Hours in 24 hour format. Accepted values: 0-23 */ + uint8_t hours; + /** Minutes in the given hour. Accepted values: 0-59. */ + uint8_t minutes; + /** For type ESP_SCHEDULE_TYPE_DAYS_OF_WEEK */ + struct { + /** 'OR' list of esp_schedule_days_t */ + uint8_t repeat_days; + } day; + /** For type ESP_SCHEDULE_TYPE_DATE */ + struct { + /** Day of the month. Accepted values: 1-31. */ + uint8_t day; + /* 'OR' list of esp_schedule_months_t */ + uint16_t repeat_months; + /** Year */ + uint16_t year; + /** If the schedule is to be repeated every year. */ + bool repeat_every_year; + } date; + /** For type ESP_SCHEDULE_TYPE_SECONDS */ + int relative_seconds; + /** Used for passing the next schedule timestamp for + * ESP_SCHEDULE_TYPE_RELATIVE */ + time_t next_scheduled_time_utc; +} esp_schedule_trigger_t; + +/** Schedule Validity + * Start and end time within which the schedule will be applicable. + */ +typedef struct esp_schedule_validity { + /* Start time as UTC timestamp */ + time_t start_time; + /* End time as UTC timestamp */ + time_t end_time; +} esp_schedule_validity_t; + +/** Schedule config */ +typedef struct esp_schedule_config { + /** Name of the schedule. This is like a primary key for the schedule. This is required. +1 for NULL termination. */ + char name[MAX_SCHEDULE_NAME_LEN + 1]; + /** Trigger details */ + esp_schedule_trigger_t trigger; + /** Trigger callback */ + esp_schedule_trigger_cb_t trigger_cb; + /** Timestamp callback */ + esp_schedule_timestamp_cb_t timestamp_cb; + /** Private data associated with the schedule. This will be passed to callbacks. */ + void *priv_data; + /** Validity of schedules. */ + esp_schedule_validity_t validity; +} esp_schedule_config_t; + +/** Initialize ESP Schedule + * + * This initializes ESP Schedule. This must be called first before calling any of the other APIs. + * This API also gets all the schedules from NVS (if it has been enabled). + * + * Note: After calling this API, the pointers to the callbacks should be updated for all the schedules by calling + * esp_schedule_get() followed by esp_schedule_edit() with the correct callbacks. + * + * @param[in] enable_nvs If NVS is to be enabled or not. + * @param[in] nvs_partition (Optional) The NVS partition to be used. If NULL is passed, the default partition is used. + * @param[out] schedule_count Number of active schedules found in NVS. + * + * @return Array of schedule handles if any schedules have been found. + * @return NULL if no schedule is found in NVS (or if NVS is not enabled). + */ +esp_schedule_handle_t *esp_schedule_init(bool enable_nvs, char *nvs_partition, uint8_t *schedule_count); + +/** Create Schedule + * + * This API can be used to create a new schedule. The schedule still needs to be enabled using + * esp_schedule_enable(). + * + * @param[in] schedule_config Configuration of the schedule to be created. + * + * @return Schedule handle if successfully created. + * @return NULL in case of error. + */ +esp_schedule_handle_t esp_schedule_create(esp_schedule_config_t *schedule_config); + +/** Remove Schedule + * + * This API can be used to remove an existing schedule. + * + * @param[in] handle Schedule handle for the schedule to be removed. + * + * @return ESP_OK on success. + * @return error in case of failure. + */ +esp_err_t esp_schedule_delete(esp_schedule_handle_t handle); + +/** Edit Schedule + * + * This API can be used to edit an existing schedule. + * The schedule name should be same as when the schedule was created. The complete config must be provided + * or the previously stored config might be over-written. + * + * Note: If a schedule is edited when it is on-going, the new changes will not be reflected. + * You will need to disable the schedule, edit it, and then enable it again. + * + * @param[in] handle Schedule handle for the schedule to be edited. + * @param[in] schedule_config Configuration of the schedule to be edited. + * + * @return ESP_OK on success. + * @return error in case of failure. + */ +esp_err_t esp_schedule_edit(esp_schedule_handle_t handle, esp_schedule_config_t *schedule_config); + +/** Enable Schedule + * + * This API can be used to enable an existing schedule. + * It can be used to enable a schedule after it has been created using esp_schedule_create() + * or if the schedule has been disabled using esp_schedule_disable(). + * + * @param[in] handle Schedule handle for the schedule to be enabled. + * + * @return ESP_OK on success. + * @return error in case of failure. + */ +esp_err_t esp_schedule_enable(esp_schedule_handle_t handle); + +/** Disable Schedule + * + * This API can be used to disable an on-going schedule. + * It does not remove the schedule, just stops it. The schedule can be enabled again using + * esp_schedule_enable(). + * + * @param[in] handle Schedule handle for the schedule to be disabled. + * + * @return ESP_OK on success. + * @return error in case of failure. + */ +esp_err_t esp_schedule_disable(esp_schedule_handle_t handle); + +/** Get Schedule + * + * This API can be used to get details of an existing schedule. + * The schedule_config is populated with the schedule details. + * + * @param[in] handle Schedule handle. + * @param[out] schedule_config Details of the schedule whose handle is passed. + * + * @return ESP_OK on success. + * @return error in case of failure. + */ +esp_err_t esp_schedule_get(esp_schedule_handle_t handle, esp_schedule_config_t *schedule_config); + +#ifdef __cplusplus +} +#endif diff --git a/examples/factory_demo/components/espressif__esp_schedule/src/esp_schedule.c b/examples/factory_demo/components/espressif__esp_schedule/src/esp_schedule.c new file mode 100644 index 0000000..017ecb3 --- /dev/null +++ b/examples/factory_demo/components/espressif__esp_schedule/src/esp_schedule.c @@ -0,0 +1,600 @@ +/* + * SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include +#include +#include +#include +#include "esp_schedule_internal.h" + +static const char *TAG = "esp_schedule"; + +#define SECONDS_TILL_2020 ((2020 - 1970) * 365 * 24 * 3600) +#define SECONDS_IN_DAY (60 * 60 * 24) + +static bool init_done = false; + +static int esp_schedule_get_no_of_days(esp_schedule_trigger_t *trigger, struct tm *current_time, struct tm *schedule_time) +{ + /* for day, monday = 0, sunday = 6. */ + int next_day = 0; + /* struct tm has tm_wday with sunday as 0. Whereas we have monday as 0. Converting struct tm to our format */ + int today = ((current_time->tm_wday + 7 - 1) % 7); + + esp_schedule_days_t today_bit = 1 << today; + uint8_t repeat_days = trigger->day.repeat_days; + int current_seconds = (current_time->tm_hour * 60 + current_time->tm_min) * 60 + current_time->tm_sec; + int schedule_seconds = (schedule_time->tm_hour * 60 + schedule_time->tm_min) * 60; + + /* Handling for one time schedule */ + if (repeat_days == ESP_SCHEDULE_DAY_ONCE) { + if (schedule_seconds > current_seconds) { + /* The schedule is today and is yet to go off */ + return 0; + } else { + /* The schedule is tomorrow */ + return 1; + } + } + + /* Handling for repeating schedules */ + /* Check if it is today */ + if ((repeat_days & today_bit)) { + if (schedule_seconds > current_seconds) { + /* The schedule is today and is yet to go off. */ + return 0; + } + } + /* Check if it is this week or next week */ + if ((repeat_days & (today_bit ^ 0xFF)) > today_bit) { + /* Next schedule is yet to come in this week */ + next_day = ffs(repeat_days & (0xFF << (today + 1))) - 1; + return (next_day - today); + } else { + /* First scheduled day of the next week */ + next_day = ffs(repeat_days) - 1; + if (next_day == today) { + /* Same day, next week */ + return 7; + } + return (7 - today + next_day); + } + + ESP_LOGE(TAG, "No of days could not be found. This should not happen."); + return 0; +} + +static uint8_t esp_schedule_get_next_month(esp_schedule_trigger_t *trigger, struct tm *current_time, struct tm *schedule_time) +{ + int current_seconds = (current_time->tm_hour * 60 + current_time->tm_min) * 60 + current_time->tm_sec; + int schedule_seconds = (schedule_time->tm_hour * 60 + schedule_time->tm_min) * 60; + /* +1 is because struct tm has months starting from 0, whereas we have them starting from 1 */ + uint8_t current_month = current_time->tm_mon + 1; + /* -1 because month_bit starts from 0b1. So for January, it should be 1 << 0. And current_month starts from 1. */ + uint16_t current_month_bit = 1 << (current_month - 1); + uint8_t next_schedule_month = 0; + uint16_t repeat_months = trigger->date.repeat_months; + + /* Check if month is not specified */ + if (repeat_months == ESP_SCHEDULE_MONTH_ONCE) { + if (trigger->date.day == current_time->tm_mday) { + /* The schedule day is same. Check if time has already passed */ + if (schedule_seconds > current_seconds) { + /* The schedule is today and is yet to go off */ + return current_month; + } else { + /* Today's time has passed */ + return (current_month + 1); + } + } else if (trigger->date.day > current_time->tm_mday) { + /* The day is yet to come in this month */ + return current_month; + } else { + /* The day has passed in the current month */ + return (current_month + 1); + } + } + + /* Check if schedule is not this year itself, it is in future. */ + if (trigger->date.year > (current_time->tm_year + 1900)) { + /* Find first schedule month of next year */ + next_schedule_month = ffs(repeat_months); + /* Year will be handled by the caller. So no need to add any additional months */ + return next_schedule_month; + } + + /* Check if schedule is this month and is yet to come */ + if (current_month_bit & repeat_months) { + if (trigger->date.day == current_time->tm_mday) { + /* The schedule day is same. Check if time has already passed */ + if (schedule_seconds > current_seconds) { + /* The schedule is today and is yet to go off */ + return current_month; + } + } + if (trigger->date.day > current_time->tm_mday) { + /* The day is yet to come in this month */ + return current_month; + } + } + + /* Check if schedule is this year */ + if ((repeat_months & (current_month_bit ^ 0xFFFF)) > current_month_bit) { + /* Next schedule month is yet to come in this year */ + next_schedule_month = ffs(repeat_months & (0xFFFF << (current_month))); + return next_schedule_month; + } + + /* Check if schedule is for this year and does not repeat */ + if (!trigger->date.repeat_every_year) { + if (trigger->date.year <= (current_time->tm_year + 1900)) { + ESP_LOGE(TAG, "Schedule does not repeat next year, but get_next_month has been called."); + return 0; + } + } + + /* Schedule is not this year */ + /* Find first schedule month of next year */ + next_schedule_month = ffs(repeat_months); + /* +12 because the schedule is next year */ + return (next_schedule_month + 12); +} + +static uint16_t esp_schedule_get_next_year(esp_schedule_trigger_t *trigger, struct tm *current_time, struct tm *schedule_time) +{ + uint16_t current_year = current_time->tm_year + 1900; + uint16_t schedule_year = trigger->date.year; + if (schedule_year > current_year) { + return schedule_year; + } + /* If the schedule is set to repeat_every_year, we return the current year */ + /* If the schedule has already passed in this year, we still return current year, as the additional months will be handled in get_next_month */ + return current_year; +} + +static uint32_t esp_schedule_get_next_schedule_time_diff(const char *schedule_name, esp_schedule_trigger_t *trigger) +{ + struct tm current_time, schedule_time; + time_t now; + char time_str[64]; + int32_t time_diff; + + /* Get current time */ + time(&now); + /* Handling ESP_SCHEDULE_TYPE_RELATIVE first since it doesn't require any + * computation based on days, hours, minutes, etc. + */ + if (trigger->type == ESP_SCHEDULE_TYPE_RELATIVE) { + /* If next scheduled time is already set, just compute the difference + * between current time and next scheduled time and return that diff. + */ + time_t target; + if (trigger->next_scheduled_time_utc > 0) { + target = (time_t)trigger->next_scheduled_time_utc; + time_diff = difftime(target, now); + } else { + target = now + (time_t)trigger->relative_seconds; + time_diff = trigger->relative_seconds; + } + localtime_r(&target, &schedule_time); + trigger->next_scheduled_time_utc = mktime(&schedule_time); + /* Print schedule time */ + memset(time_str, 0, sizeof(time_str)); + strftime(time_str, sizeof(time_str), "%c %z[%Z]", &schedule_time); + ESP_LOGI(TAG, "Schedule %s will be active on: %s. DST: %s", schedule_name, time_str, schedule_time.tm_isdst ? "Yes" : "No"); + return time_diff; + } + localtime_r(&now, ¤t_time); + + /* Get schedule time */ + localtime_r(&now, &schedule_time); + schedule_time.tm_sec = 0; + schedule_time.tm_min = trigger->minutes; + schedule_time.tm_hour = trigger->hours; + mktime(&schedule_time); + + /* Adjust schedule day */ + if (trigger->type == ESP_SCHEDULE_TYPE_DAYS_OF_WEEK) { + int no_of_days = 0; + no_of_days = esp_schedule_get_no_of_days(trigger, ¤t_time, &schedule_time); + schedule_time.tm_sec += no_of_days * SECONDS_IN_DAY; + } + if (trigger->type == ESP_SCHEDULE_TYPE_DATE) { + schedule_time.tm_mday = trigger->date.day; + schedule_time.tm_mon = esp_schedule_get_next_month(trigger, ¤t_time, &schedule_time) - 1; + schedule_time.tm_year = esp_schedule_get_next_year(trigger, ¤t_time, &schedule_time) - 1900; + if (schedule_time.tm_mon < 0) { + ESP_LOGE(TAG, "Invalid month found: %d. Setting it to next month.", schedule_time.tm_mon); + schedule_time.tm_mon = current_time.tm_mon + 1; + } + if (schedule_time.tm_mon >= 12) { + schedule_time.tm_year += schedule_time.tm_mon / 12; + schedule_time.tm_mon = schedule_time.tm_mon % 12; + } + } + mktime(&schedule_time); + + /* Adjust time according to DST */ + time_t dst_adjust = 0; + if (!current_time.tm_isdst && schedule_time.tm_isdst) { + dst_adjust = -3600; + } else if (current_time.tm_isdst && !schedule_time.tm_isdst ) { + dst_adjust = 3600; + } + ESP_LOGD(TAG, "DST adjust seconds: %lld", (long long) dst_adjust); + schedule_time.tm_sec += dst_adjust; + mktime(&schedule_time); + + /* Print schedule time */ + memset(time_str, 0, sizeof(time_str)); + strftime(time_str, sizeof(time_str), "%c %z[%Z]", &schedule_time); + ESP_LOGI(TAG, "Schedule %s will be active on: %s. DST: %s", schedule_name, time_str, schedule_time.tm_isdst ? "Yes" : "No"); + + /* Calculate difference */ + time_diff = difftime((mktime(&schedule_time)), mktime(¤t_time)); + + /* For one time schedules to check for expiry after a reboot. If NVS is enabled, this should be stored in NVS. */ + trigger->next_scheduled_time_utc = mktime(&schedule_time); + + return time_diff; +} + +static bool esp_schedule_is_expired(esp_schedule_trigger_t *trigger) +{ + time_t current_timestamp = 0; + struct tm current_time = {0}; + time(¤t_timestamp); + localtime_r(¤t_timestamp, ¤t_time); + + if (trigger->type == ESP_SCHEDULE_TYPE_RELATIVE) { + if (trigger->next_scheduled_time_utc > 0 && trigger->next_scheduled_time_utc <= current_timestamp) { + /* Relative seconds based schedule has expired */ + return true; + } else if (trigger->next_scheduled_time_utc == 0) { + /* Schedule has been disabled , so it is as good as expired. */ + return true; + } + } else if (trigger->type == ESP_SCHEDULE_TYPE_DAYS_OF_WEEK) { + if (trigger->day.repeat_days == ESP_SCHEDULE_DAY_ONCE) { + if (trigger->next_scheduled_time_utc > 0 && trigger->next_scheduled_time_utc <= current_timestamp) { + /* One time schedule has expired */ + return true; + } else if (trigger->next_scheduled_time_utc == 0) { + /* Schedule has been disabled , so it is as good as expired. */ + return true; + } + } + } else if (trigger->type == ESP_SCHEDULE_TYPE_DATE) { + if (trigger->date.repeat_months == 0) { + if (trigger->next_scheduled_time_utc > 0 && trigger->next_scheduled_time_utc <= current_timestamp) { + /* One time schedule has expired */ + return true; + } else { + return false; + } + } + if (trigger->date.repeat_every_year == true) { + return false; + } + + struct tm schedule_time = {0}; + localtime_r(¤t_timestamp, &schedule_time); + schedule_time.tm_sec = 0; + schedule_time.tm_min = trigger->minutes; + schedule_time.tm_hour = trigger->hours; + schedule_time.tm_mday = trigger->date.day; + /* For expiry, just check the last month of the repeat_months. */ + /* '-1' because struct tm has months starting from 0 and we have months starting from 1. */ + schedule_time.tm_mon = fls(trigger->date.repeat_months) - 1; + /* '-1900' because struct tm has number of years after 1900 */ + schedule_time.tm_year = trigger->date.year - 1900; + time_t schedule_timestamp = mktime(&schedule_time); + + if (schedule_timestamp < current_timestamp) { + return true; + } + } else { + /* Invalid type. Mark as expired */ + return true; + } + return false; +} + +static void esp_schedule_stop_timer(esp_schedule_t *schedule) +{ + xTimerStop(schedule->timer, portMAX_DELAY); +} + +static void esp_schedule_start_timer(esp_schedule_t *schedule) +{ + time_t current_time = 0; + time(¤t_time); + if (current_time < SECONDS_TILL_2020) { + ESP_LOGE(TAG, "Time is not updated"); + return; + } + + schedule->next_scheduled_time_diff = esp_schedule_get_next_schedule_time_diff(schedule->name, &schedule->trigger); + ESP_LOGI(TAG, "Starting a timer for %"PRIu32" seconds for schedule %s", schedule->next_scheduled_time_diff, schedule->name); + + if (schedule->timestamp_cb) { + schedule->timestamp_cb((esp_schedule_handle_t)schedule, schedule->trigger.next_scheduled_time_utc, schedule->priv_data); + } + + xTimerStop(schedule->timer, portMAX_DELAY); + xTimerChangePeriod(schedule->timer, (schedule->next_scheduled_time_diff * 1000) / portTICK_PERIOD_MS, portMAX_DELAY); +} + +static void esp_schedule_common_timer_cb(TimerHandle_t timer) +{ + void *priv_data = pvTimerGetTimerID(timer); + if (priv_data == NULL) { + return; + } + esp_schedule_t *schedule = (esp_schedule_t *)priv_data; + time_t now; + time(&now); + struct tm validity_time; + char time_str[64] = {0}; + if (schedule->validity.start_time != 0) { + if (now < schedule->validity.start_time) { + memset(time_str, 0, sizeof(time_str)); + localtime_r(&schedule->validity.start_time, &validity_time); + strftime(time_str, sizeof(time_str), "%c %z[%Z]", &validity_time); + ESP_LOGW(TAG, "Schedule %s skipped. It will be active only after: %s. DST: %s.", schedule->name, time_str, validity_time.tm_isdst ? "Yes" : "No"); + /* TODO: Start the timer such that the next time it triggeres, it will be within the valid window. + * Currently, it will just keep triggering and then get skipped if not in valid range. + */ + goto restart_schedule; + } + } + if (schedule->validity.end_time != 0) { + if (now > schedule->validity.end_time) { + localtime_r(&schedule->validity.end_time, &validity_time); + strftime(time_str, sizeof(time_str), "%c %z[%Z]", &validity_time); + ESP_LOGW(TAG, "Schedule %s skipped. It can't be active after: %s. DST: %s.", schedule->name, time_str, validity_time.tm_isdst ? "Yes" : "No"); + /* Return from here will ensure that the timer does not start again for this schedule */ + return; + } + } + ESP_LOGI(TAG, "Schedule %s triggered", schedule->name); + if (schedule->trigger_cb) { + schedule->trigger_cb((esp_schedule_handle_t)schedule, schedule->priv_data); + } + +restart_schedule: + + if (esp_schedule_is_expired(&schedule->trigger)) { + /* Not deleting the schedule here. Just not starting it again. */ + return; + } + esp_schedule_start_timer(schedule); +} + +static void esp_schedule_delete_timer(esp_schedule_t *schedule) +{ + xTimerDelete(schedule->timer, portMAX_DELAY); +} + +static void esp_schedule_create_timer(esp_schedule_t *schedule) +{ + if (esp_schedule_nvs_is_enabled()) { + /* This is just used for calculating next_scheduled_time_utc for ESP_SCHEDULE_DAY_ONCE (in case of ESP_SCHEDULE_TYPE_DAYS_OF_WEEK) or for ESP_SCHEDULE_MONTH_ONCE (in case of ESP_SCHEDULE_TYPE_DATE), and only used when NVS is enabled. And if NVS is enabled, time will already be synced and the time will be correctly calculated. */ + schedule->next_scheduled_time_diff = esp_schedule_get_next_schedule_time_diff(schedule->name, &schedule->trigger); + } + + /* Temporarily setting the timer for 1 (anything greater than 0) tick. This will get changed when xTimerChangePeriod() is called. */ + schedule->timer = xTimerCreate("schedule", 1, pdFALSE, (void *)schedule, esp_schedule_common_timer_cb); +} + +esp_err_t esp_schedule_get(esp_schedule_handle_t handle, esp_schedule_config_t *schedule_config) +{ + if (schedule_config == NULL) { + return ESP_ERR_INVALID_ARG; + } + if (handle == NULL) { + return ESP_ERR_INVALID_ARG; + } + esp_schedule_t *schedule = (esp_schedule_t *)handle; + + strcpy(schedule_config->name, schedule->name); + schedule_config->trigger.type = schedule->trigger.type; + schedule_config->trigger.hours = schedule->trigger.hours; + schedule_config->trigger.minutes = schedule->trigger.minutes; + if (schedule->trigger.type == ESP_SCHEDULE_TYPE_DAYS_OF_WEEK) { + schedule_config->trigger.day.repeat_days = schedule->trigger.day.repeat_days; + } else if (schedule->trigger.type == ESP_SCHEDULE_TYPE_DATE) { + schedule_config->trigger.date.day = schedule->trigger.date.day; + schedule_config->trigger.date.repeat_months = schedule->trigger.date.repeat_months; + schedule_config->trigger.date.year = schedule->trigger.date.year; + schedule_config->trigger.date.repeat_every_year = schedule->trigger.date.repeat_every_year; + } + + schedule_config->trigger_cb = schedule->trigger_cb; + schedule_config->timestamp_cb = schedule->timestamp_cb; + schedule_config->priv_data = schedule->priv_data; + schedule_config->validity = schedule->validity; + return ESP_OK; +} + +esp_err_t esp_schedule_enable(esp_schedule_handle_t handle) +{ + if (handle == NULL) { + return ESP_ERR_INVALID_ARG; + } + esp_schedule_t *schedule = (esp_schedule_t *)handle; + esp_schedule_start_timer(schedule); + return ESP_OK; +} + +esp_err_t esp_schedule_disable(esp_schedule_handle_t handle) +{ + if (handle == NULL) { + return ESP_ERR_INVALID_ARG; + } + esp_schedule_t *schedule = (esp_schedule_t *)handle; + esp_schedule_stop_timer(schedule); + /* Disabling a schedule should also reset the next_scheduled_time. + * It would be re-computed after enabling. + */ + schedule->trigger.next_scheduled_time_utc = 0; + return ESP_OK; +} + +static esp_err_t esp_schedule_set(esp_schedule_t *schedule, esp_schedule_config_t *schedule_config) +{ + /* Setting everything apart from name. */ + schedule->trigger.type = schedule_config->trigger.type; + if (schedule->trigger.type == ESP_SCHEDULE_TYPE_RELATIVE) { + schedule->trigger.relative_seconds = schedule_config->trigger.relative_seconds; + schedule->trigger.next_scheduled_time_utc = schedule_config->trigger.next_scheduled_time_utc; + } else { + schedule->trigger.hours = schedule_config->trigger.hours; + schedule->trigger.minutes = schedule_config->trigger.minutes; + + if (schedule->trigger.type == ESP_SCHEDULE_TYPE_DAYS_OF_WEEK) { + schedule->trigger.day.repeat_days = schedule_config->trigger.day.repeat_days; + } else if (schedule->trigger.type == ESP_SCHEDULE_TYPE_DATE) { + schedule->trigger.date.day = schedule_config->trigger.date.day; + schedule->trigger.date.repeat_months = schedule_config->trigger.date.repeat_months; + schedule->trigger.date.year = schedule_config->trigger.date.year; + schedule->trigger.date.repeat_every_year = schedule_config->trigger.date.repeat_every_year; + } + } + + schedule->trigger_cb = schedule_config->trigger_cb; + schedule->timestamp_cb = schedule_config->timestamp_cb; + schedule->priv_data = schedule_config->priv_data; + schedule->validity = schedule_config->validity; + esp_schedule_nvs_add(schedule); + return ESP_OK; +} + +esp_err_t esp_schedule_edit(esp_schedule_handle_t handle, esp_schedule_config_t *schedule_config) +{ + if (handle == NULL || schedule_config == NULL) { + return ESP_ERR_INVALID_ARG; + } + esp_schedule_t *schedule = (esp_schedule_t *)handle; + if (strncmp(schedule->name, schedule_config->name, sizeof(schedule->name)) != 0) { + ESP_LOGE(TAG, "Schedule name mismatch. Expected: %s, Passed: %s", schedule->name, schedule_config->name); + return ESP_FAIL; + } + + /* Editing a schedule with relative time should also reset it. */ + if (schedule->trigger.type == ESP_SCHEDULE_TYPE_RELATIVE) { + schedule->trigger.next_scheduled_time_utc = 0; + } + esp_schedule_set(schedule, schedule_config); + ESP_LOGD(TAG, "Schedule %s edited", schedule->name); + return ESP_OK; +} + +esp_err_t esp_schedule_delete(esp_schedule_handle_t handle) +{ + if (handle == NULL) { + return ESP_ERR_INVALID_ARG; + } + esp_schedule_t *schedule = (esp_schedule_t *)handle; + ESP_LOGI(TAG, "Deleting schedule %s", schedule->name); + if (schedule->timer) { + esp_schedule_stop_timer(schedule); + esp_schedule_delete_timer(schedule); + } + esp_schedule_nvs_remove(schedule); + free(schedule); + return ESP_OK; +} + +esp_schedule_handle_t esp_schedule_create(esp_schedule_config_t *schedule_config) +{ + if (schedule_config == NULL) { + return NULL; + } + if (strlen(schedule_config->name) <= 0) { + ESP_LOGE(TAG, "Set schedule failed. Please enter a unique valid name for the schedule."); + return NULL; + } + + if (schedule_config->trigger.type == ESP_SCHEDULE_TYPE_INVALID) { + ESP_LOGE(TAG, "Schedule type is invalid."); + return NULL; + } + + esp_schedule_t *schedule = (esp_schedule_t *)MEM_CALLOC_EXTRAM(1, sizeof(esp_schedule_t)); + if (schedule == NULL) { + ESP_LOGE(TAG, "Could not allocate handle"); + return NULL; + } + strlcpy(schedule->name, schedule_config->name, sizeof(schedule->name)); + + esp_schedule_set(schedule, schedule_config); + + esp_schedule_create_timer(schedule); + ESP_LOGD(TAG, "Schedule %s created", schedule->name); + return (esp_schedule_handle_t)schedule; +} + +esp_schedule_handle_t *esp_schedule_init(bool enable_nvs, char *nvs_partition, uint8_t *schedule_count) +{ +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0) + if (!esp_sntp_enabled()) { + ESP_LOGI(TAG, "Initializing SNTP"); + esp_sntp_setoperatingmode(SNTP_OPMODE_POLL); + esp_sntp_setservername(0, "pool.ntp.org"); + esp_sntp_init(); + } +#else + if (!sntp_enabled()) { + ESP_LOGI(TAG, "Initializing SNTP"); + sntp_setoperatingmode(SNTP_OPMODE_POLL); + sntp_setservername(0, "pool.ntp.org"); + sntp_init(); + } +#endif + + if (!enable_nvs) { + return NULL; + } + + /* Wait for time to be updated here */ + + + /* Below this is initialising schedules from NVS */ + esp_schedule_nvs_init(nvs_partition); + + /* Get handle list from NVS */ + esp_schedule_handle_t *handle_list = NULL; + *schedule_count = 0; + handle_list = esp_schedule_nvs_get_all(schedule_count); + if (handle_list == NULL) { + ESP_LOGI(TAG, "No schedules found in NVS"); + return NULL; + } + ESP_LOGI(TAG, "Schedules found in NVS: %"PRIu8, *schedule_count); + /* Start/Delete the schedules */ + esp_schedule_t *schedule = NULL; + for (size_t handle_count = 0; handle_count < *schedule_count; handle_count++) { + schedule = (esp_schedule_t *)handle_list[handle_count]; + schedule->trigger_cb = NULL; + schedule->timer = NULL; + /* Check for ONCE and expired schedules and delete them. */ + if (esp_schedule_is_expired(&schedule->trigger)) { + /* This schedule has already expired. */ + ESP_LOGI(TAG, "Schedule %s does not repeat and has already expired. Deleting it.", schedule->name); + esp_schedule_delete((esp_schedule_handle_t)schedule); + /* Removing the schedule from the list */ + handle_list[handle_count] = handle_list[*schedule_count - 1]; + (*schedule_count)--; + handle_count--; + continue; + } + esp_schedule_create_timer(schedule); + esp_schedule_start_timer(schedule); + } + init_done = true; + return handle_list; +} diff --git a/examples/factory_demo/components/espressif__esp_schedule/src/esp_schedule_internal.h b/examples/factory_demo/components/espressif__esp_schedule/src/esp_schedule_internal.h new file mode 100644 index 0000000..64c3f8a --- /dev/null +++ b/examples/factory_demo/components/espressif__esp_schedule/src/esp_schedule_internal.h @@ -0,0 +1,28 @@ +/* + * SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include + +typedef struct esp_schedule { + char name[MAX_SCHEDULE_NAME_LEN + 1]; + esp_schedule_trigger_t trigger; + uint32_t next_scheduled_time_diff; + TimerHandle_t timer; + esp_schedule_trigger_cb_t trigger_cb; + esp_schedule_timestamp_cb_t timestamp_cb; + void *priv_data; + esp_schedule_validity_t validity; +} esp_schedule_t; + +esp_err_t esp_schedule_nvs_add(esp_schedule_t *schedule); +esp_err_t esp_schedule_nvs_remove(esp_schedule_t *schedule); +esp_schedule_handle_t *esp_schedule_nvs_get_all(uint8_t *schedule_count); +bool esp_schedule_nvs_is_enabled(void); +esp_err_t esp_schedule_nvs_init(char *nvs_partition); diff --git a/examples/factory_demo/components/espressif__esp_schedule/src/esp_schedule_nvs.c b/examples/factory_demo/components/espressif__esp_schedule/src/esp_schedule_nvs.c new file mode 100644 index 0000000..acff405 --- /dev/null +++ b/examples/factory_demo/components/espressif__esp_schedule/src/esp_schedule_nvs.c @@ -0,0 +1,296 @@ +// Copyright 2020 Espressif Systems (Shanghai) PTE LTD +// +// 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. + +#include +#include +#include +#include +#include "esp_schedule_internal.h" + +static const char *TAG = "esp_schedule_nvs"; + +#define ESP_SCHEDULE_NVS_NAMESPACE "schd" +#define ESP_SCHEDULE_COUNT_KEY "schd_count" + +static char *esp_schedule_nvs_partition = NULL; +static bool nvs_enabled = false; + +esp_err_t esp_schedule_nvs_add(esp_schedule_t *schedule) +{ + if (!nvs_enabled) { + ESP_LOGD(TAG, "NVS not enabled. Not adding to NVS."); + return ESP_ERR_INVALID_STATE; + } + nvs_handle_t nvs_handle; + esp_err_t err = nvs_open_from_partition(esp_schedule_nvs_partition, ESP_SCHEDULE_NVS_NAMESPACE, NVS_READWRITE, &nvs_handle); + if (err != ESP_OK) { + ESP_LOGE(TAG, "NVS open failed with error %d", err); + return err; + } + + /* Check if this is new schedule or editing an existing schedule */ + size_t buf_size; + bool editing_schedule = true; + err = nvs_get_blob(nvs_handle, schedule->name, NULL, &buf_size); + if (err != ESP_OK) { + if (err == ESP_ERR_NVS_NOT_FOUND) { + editing_schedule = false; + } else { + ESP_LOGE(TAG, "NVS get failed with error %d", err); + nvs_close(nvs_handle); + return err; + } + } else { + ESP_LOGI(TAG, "Updating the existing schedule %s", schedule->name); + } + + err = nvs_set_blob(nvs_handle, schedule->name, schedule, sizeof(esp_schedule_t)); + if (err != ESP_OK) { + ESP_LOGE(TAG, "NVS set failed with error %d", err); + nvs_close(nvs_handle); + return err; + } + if (editing_schedule == false) { + uint8_t schedule_count; + err = nvs_get_u8(nvs_handle, ESP_SCHEDULE_COUNT_KEY, &schedule_count); + if (err != ESP_OK) { + if (err == ESP_ERR_NVS_NOT_FOUND) { + schedule_count = 0; + } else { + ESP_LOGE(TAG, "NVS get failed with error %d", err); + nvs_close(nvs_handle); + return err; + } + } + schedule_count++; + err = nvs_set_u8(nvs_handle, ESP_SCHEDULE_COUNT_KEY, schedule_count); + if (err != ESP_OK) { + ESP_LOGE(TAG, "NVS set failed for schedule count with error %d", err); + nvs_close(nvs_handle); + return err; + } + } + nvs_commit(nvs_handle); + nvs_close(nvs_handle); + ESP_LOGI(TAG, "Schedule %s added in NVS", schedule->name); + return ESP_OK; +} + +esp_err_t esp_schedule_nvs_remove_all(void) +{ + if (!nvs_enabled) { + ESP_LOGD(TAG, "NVS not enabled. Not removing from NVS."); + return ESP_ERR_INVALID_STATE; + } + nvs_handle_t nvs_handle; + esp_err_t err = nvs_open_from_partition(esp_schedule_nvs_partition, ESP_SCHEDULE_NVS_NAMESPACE, NVS_READWRITE, &nvs_handle); + if (err != ESP_OK) { + ESP_LOGE(TAG, "NVS open failed with error %d", err); + return err; + } + err = nvs_erase_all(nvs_handle); + if (err != ESP_OK) { + ESP_LOGE(TAG, "NVS erase all keys failed with error %d", err); + nvs_close(nvs_handle); + return err; + } + nvs_commit(nvs_handle); + nvs_close(nvs_handle); + ESP_LOGI(TAG, "All schedules removed from NVS"); + return ESP_OK; +} + +esp_err_t esp_schedule_nvs_remove(esp_schedule_t *schedule) +{ + if (!nvs_enabled) { + ESP_LOGD(TAG, "NVS not enabled. Not removing from NVS."); + return ESP_ERR_INVALID_STATE; + } + nvs_handle_t nvs_handle; + esp_err_t err = nvs_open_from_partition(esp_schedule_nvs_partition, ESP_SCHEDULE_NVS_NAMESPACE, NVS_READWRITE, &nvs_handle); + if (err != ESP_OK) { + ESP_LOGE(TAG, "NVS open failed with error %d", err); + return err; + } + err = nvs_erase_key(nvs_handle, schedule->name); + if (err != ESP_OK) { + ESP_LOGE(TAG, "NVS erase key failed with error %d", err); + nvs_close(nvs_handle); + return err; + } + uint8_t schedule_count; + err = nvs_get_u8(nvs_handle, ESP_SCHEDULE_COUNT_KEY, &schedule_count); + if (err != ESP_OK) { + ESP_LOGE(TAG, "NVS get failed for schedule count with error %d", err); + nvs_close(nvs_handle); + return err; + } + schedule_count--; + err = nvs_set_u8(nvs_handle, ESP_SCHEDULE_COUNT_KEY, schedule_count); + if (err != ESP_OK) { + ESP_LOGE(TAG, "NVS set failed for schedule count with error %d", err); + nvs_close(nvs_handle); + return err; + } + nvs_commit(nvs_handle); + nvs_close(nvs_handle); + ESP_LOGI(TAG, "Schedule %s removed from NVS", schedule->name); + return ESP_OK; +} + +static uint8_t esp_schedule_nvs_get_count(void) +{ + if (!nvs_enabled) { + ESP_LOGD(TAG, "NVS not enabled. Not getting count from NVS."); + return 0; + } + nvs_handle_t nvs_handle; + esp_err_t err = nvs_open_from_partition(esp_schedule_nvs_partition, ESP_SCHEDULE_NVS_NAMESPACE, NVS_READONLY, &nvs_handle); + if (err != ESP_OK) { + ESP_LOGE(TAG, "NVS open failed with error %d", err); + return 0; + } + uint8_t schedule_count; + err = nvs_get_u8(nvs_handle, ESP_SCHEDULE_COUNT_KEY, &schedule_count); + if (err != ESP_OK) { + ESP_LOGE(TAG, "NVS get failed for schedule count with error %d", err); + nvs_close(nvs_handle); + return 0; + } + nvs_close(nvs_handle); + ESP_LOGI(TAG, "Schedules in NVS: %d", schedule_count); + return schedule_count; +} + +static esp_schedule_handle_t esp_schedule_nvs_get(char *nvs_key) +{ + if (!nvs_enabled) { + ESP_LOGD(TAG, "NVS not enabled. Not getting from NVS."); + return NULL; + } + size_t buf_size; + nvs_handle_t nvs_handle; + esp_err_t err = nvs_open_from_partition(esp_schedule_nvs_partition, ESP_SCHEDULE_NVS_NAMESPACE, NVS_READONLY, &nvs_handle); + if (err != ESP_OK) { + ESP_LOGE(TAG, "NVS open failed with error %d", err); + return NULL; + } + err = nvs_get_blob(nvs_handle, nvs_key, NULL, &buf_size); + if (err != ESP_OK) { + ESP_LOGE(TAG, "NVS get failed with error %d", err); + nvs_close(nvs_handle); + return NULL; + } + esp_schedule_t *schedule = (esp_schedule_t *)malloc(buf_size); + if (schedule == NULL) { + ESP_LOGE(TAG, "Could not allocate handle"); + nvs_close(nvs_handle); + return NULL; + } + err = nvs_get_blob(nvs_handle, nvs_key, schedule, &buf_size); + if (err != ESP_OK) { + ESP_LOGE(TAG, "NVS get failed with error %d", err); + nvs_close(nvs_handle); + free(schedule); + return NULL; + } + nvs_close(nvs_handle); + ESP_LOGI(TAG, "Schedule %s found in NVS", schedule->name); + return (esp_schedule_handle_t) schedule; +} + +esp_schedule_handle_t *esp_schedule_nvs_get_all(uint8_t *schedule_count) +{ + if (!nvs_enabled) { + ESP_LOGD(TAG, "NVS not enabled. Not Initialising NVS."); + return NULL; + } + + *schedule_count = esp_schedule_nvs_get_count(); + if (*schedule_count == 0) { + ESP_LOGI(TAG, "No Entries found in NVS"); + return NULL; + } + esp_schedule_handle_t *handle_list = (esp_schedule_handle_t *)malloc(sizeof(esp_schedule_handle_t) * (*schedule_count)); + if (handle_list == NULL) { + ESP_LOGE(TAG, "Could not allocate schedule list"); + *schedule_count = 0; + return NULL; + } + int handle_count = 0; + + nvs_entry_info_t nvs_entry; +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) + nvs_iterator_t nvs_iterator = NULL; + esp_err_t err = nvs_entry_find(esp_schedule_nvs_partition, ESP_SCHEDULE_NVS_NAMESPACE, NVS_TYPE_BLOB, &nvs_iterator); + if (err != ESP_OK) { + ESP_LOGE(TAG, "No entry found in NVS"); + return NULL;; + } + while (err == ESP_OK) { + nvs_entry_info(nvs_iterator, &nvs_entry); + ESP_LOGI(TAG, "Found schedule in NVS with key: %s", nvs_entry.key); + handle_list[handle_count] = esp_schedule_nvs_get(nvs_entry.key); + if (handle_list[handle_count] != NULL) { + /* Increase count only if nvs_get was successful */ + handle_count++; + } + err = nvs_entry_next(&nvs_iterator); + } + nvs_release_iterator(nvs_iterator); +#else + nvs_iterator_t nvs_iterator = nvs_entry_find(esp_schedule_nvs_partition, ESP_SCHEDULE_NVS_NAMESPACE, NVS_TYPE_BLOB); + if (nvs_iterator == NULL) { + ESP_LOGE(TAG, "No entry found in NVS"); + return NULL;; + } + while (nvs_iterator != NULL) { + nvs_entry_info(nvs_iterator, &nvs_entry); + ESP_LOGI(TAG, "Found schedule in NVS with key: %s", nvs_entry.key); + handle_list[handle_count] = esp_schedule_nvs_get(nvs_entry.key); + if (handle_list[handle_count] != NULL) { + /* Increase count only if nvs_get was successful */ + handle_count++; + } + nvs_iterator = nvs_entry_next(nvs_iterator); + } +#endif + *schedule_count = handle_count; + ESP_LOGI(TAG, "Found %d schedules in NVS", *schedule_count); + return handle_list; +} + +bool esp_schedule_nvs_is_enabled(void) +{ + return nvs_enabled; +} + +esp_err_t esp_schedule_nvs_init(char *nvs_partition) +{ + if (nvs_enabled) { + ESP_LOGI(TAG, "NVS already enabled"); + return ESP_OK; + } + if (nvs_partition) { + esp_schedule_nvs_partition = strndup(nvs_partition, strlen(nvs_partition)); + } else { + esp_schedule_nvs_partition = strndup("nvs", strlen("nvs")); + } + if (esp_schedule_nvs_partition == NULL) { + ESP_LOGE(TAG, "Could not allocate nvs_partition"); + return ESP_ERR_NO_MEM; + } + nvs_enabled = true; + return ESP_OK; +} diff --git a/examples/factory_demo/main/app/app_led.h b/examples/factory_demo/main/app/app_led.h index ae2ba6c..cafb40d 100644 --- a/examples/factory_demo/main/app/app_led.h +++ b/examples/factory_demo/main/app/app_led.h @@ -13,7 +13,7 @@ extern "C" { #endif -esp_err_t app_pwm_led_init(int gpio_r, int gpio_g, int gpio_b); +esp_err_t app_pwm_led_init(gpio_num_t gpio_r, gpio_num_t gpio_g, gpio_num_t gpio_b); esp_err_t app_pwm_led_change_io(gpio_num_t gpio_r, gpio_num_t gpio_g, gpio_num_t gpio_b); esp_err_t app_pwm_led_deinit(void); esp_err_t app_pwm_led_set_all(uint8_t red, uint8_t green, uint8_t blue); diff --git a/examples/factory_demo/main/idf_component.yml b/examples/factory_demo/main/idf_component.yml index 34e89ad..a45cfb5 100644 --- a/examples/factory_demo/main/idf_component.yml +++ b/examples/factory_demo/main/idf_component.yml @@ -5,19 +5,9 @@ dependencies: chmorgan/esp-audio-player: "1.0.5" chmorgan/esp-file-iterator: "1.0.0" - espressif/esp_rainmaker: - path: components/esp_rainmaker - git: https://github.com/espressif/esp-rainmaker.git - version: "cc861bffc818c607e799f66c79923fd4e2d00447" - espressif/esp_schedule: - path: components/esp_schedule - git: https://github.com/espressif/esp-rainmaker.git - version: "cc861bffc818c607e799f66c79923fd4e2d00447" - espressif/rmaker_common: "1.4.2" + espressif/esp_rainmaker: "1.0.0" espressif/esp-sr: "1.4.*" espressif/led_strip: "~2.0.0" - espressif/json_generator: ^1 - espressif/json_parser: =1.0.0 espressif/qrcode: ^0.1.0 espressif/ir_learn: ^0.1.0 espressif/aht20: "^0.1.0" diff --git a/examples/factory_demo/sdkconfig.defaults b/examples/factory_demo/sdkconfig.defaults index 5db590b..158c596 100644 --- a/examples/factory_demo/sdkconfig.defaults +++ b/examples/factory_demo/sdkconfig.defaults @@ -35,6 +35,7 @@ CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y CONFIG_ESP32S3_INSTRUCTION_CACHE_32KB=y CONFIG_ESP32S3_DATA_CACHE_64KB=y CONFIG_ESP32S3_DATA_CACHE_LINE_64B=y +CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=4096 CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y CONFIG_ESP_TASK_WDT_TIMEOUT_S=7 CONFIG_ESP_IPC_TASK_STACK_SIZE=2048 diff --git a/examples/image_display/sdkconfig.ci.box b/examples/image_display/sdkconfig.ci.box index 52c1c29..e05ce02 100644 --- a/examples/image_display/sdkconfig.ci.box +++ b/examples/image_display/sdkconfig.ci.box @@ -1,2 +1 @@ CONFIG_BSP_BOARD_ESP32_S3_BOX=y -CONFIG_BSP_ESP32_S3_BOX_1=y \ No newline at end of file diff --git a/examples/image_display/sdkconfig.ci.box-3 b/examples/image_display/sdkconfig.ci.box-3 index 52ddca5..366b9a3 100644 --- a/examples/image_display/sdkconfig.ci.box-3 +++ b/examples/image_display/sdkconfig.ci.box-3 @@ -1,2 +1 @@ -CONFIG_BSP_BOARD_ESP32_S3_BOX=y -CONFIG_BSP_ESP32_S3_BOX_3=y \ No newline at end of file +CONFIG_BSP_BOARD_ESP32_S3_BOX_3=y diff --git a/examples/image_display/sdkconfig.ci.box-lite b/examples/image_display/sdkconfig.ci.box-lite index fd69632..669da0e 100644 --- a/examples/image_display/sdkconfig.ci.box-lite +++ b/examples/image_display/sdkconfig.ci.box-lite @@ -1 +1 @@ -CONFIG_BSP_BOARD_ESP32_S3_BOX_Lite=y \ No newline at end of file +CONFIG_BSP_BOARD_ESP32_S3_BOX_Lite=y diff --git a/examples/usb_camera_lcd_display/main/sdkconfig.ci.box-3 b/examples/usb_camera_lcd_display/main/sdkconfig.ci.box-3 new file mode 100644 index 0000000..366b9a3 --- /dev/null +++ b/examples/usb_camera_lcd_display/main/sdkconfig.ci.box-3 @@ -0,0 +1 @@ +CONFIG_BSP_BOARD_ESP32_S3_BOX_3=y diff --git a/examples/usb_camera_lcd_display/sdkconfig.defaults b/examples/usb_camera_lcd_display/sdkconfig.defaults index 13e5563..b772c26 100644 --- a/examples/usb_camera_lcd_display/sdkconfig.defaults +++ b/examples/usb_camera_lcd_display/sdkconfig.defaults @@ -44,8 +44,6 @@ CONFIG_ESPTOOLPY_FLASHFREQ_80M=y # # LCD # -CONFIG_BSP_LCD_RGB_BUFFER_NUMS=2 - CONFIG_BSP_LCD_DRAW_BUF_HEIGHT=150 CONFIG_BSP_LCD_DRAW_BUF_DOUBLE=y CONFIG_LV_COLOR_16_SWAP=y diff --git a/examples/usb_headset/sdkconfig.defaults b/examples/usb_headset/sdkconfig.defaults index 4b7bd3d..5c19183 100644 --- a/examples/usb_headset/sdkconfig.defaults +++ b/examples/usb_headset/sdkconfig.defaults @@ -6,7 +6,6 @@ CONFIG_IDF_TARGET="esp32s3" CONFIG_FREERTOS_HZ=1000 CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240=y CONFIG_ESP32S3_DEFAULT_CPU_FREQ_MHZ=240 -# CONFIG_BSP_TOUCH_BUTTON is not set # # LVGL diff --git a/examples/watering_demo/components/espressif__esp_schedule/.component_hash b/examples/watering_demo/components/espressif__esp_schedule/.component_hash new file mode 100644 index 0000000..43e2058 --- /dev/null +++ b/examples/watering_demo/components/espressif__esp_schedule/.component_hash @@ -0,0 +1 @@ +2cbc7e2584f523d10fbfb4ecb7d25c07fa7d7757ab23d7a127494b1a58131ffe \ No newline at end of file diff --git a/examples/watering_demo/components/espressif__esp_schedule/CMakeLists.txt b/examples/watering_demo/components/espressif__esp_schedule/CMakeLists.txt new file mode 100644 index 0000000..02eb952 --- /dev/null +++ b/examples/watering_demo/components/espressif__esp_schedule/CMakeLists.txt @@ -0,0 +1,10 @@ +set(component_srcs "src/esp_schedule.c" + "src/esp_schedule_nvs.c") + +idf_component_register(SRCS "${component_srcs}" + INCLUDE_DIRS "include" + PRIV_INCLUDE_DIRS "src" + PRIV_REQUIRES "rmaker_common" + REQUIRES "nvs_flash") + +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wno-unused-function") diff --git a/examples/watering_demo/components/espressif__esp_schedule/LICENSE b/examples/watering_demo/components/espressif__esp_schedule/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/examples/watering_demo/components/espressif__esp_schedule/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/examples/watering_demo/components/espressif__esp_schedule/README.md b/examples/watering_demo/components/espressif__esp_schedule/README.md new file mode 100644 index 0000000..3646ec0 --- /dev/null +++ b/examples/watering_demo/components/espressif__esp_schedule/README.md @@ -0,0 +1,50 @@ +# ESP Scheduling + +[![Component Registry](https://components.espressif.com/components/espressif/esp_schedule/badge.svg)](https://components.espressif.com/components/espressif/esp_schedule) + +This component is used internally by ESP RainMaker to implement schedules. + +> Note: By default, the time is w.r.t. UTC. If the timezone has been set, then the time is w.r.t. the specified timezone. + +## Test code: + +``` +#include + +void app_schedule_trigger_cb(esp_schedule_handle_t handle, void *priv_data) +{ + printf("priv_data: %.*s\n", (char *)priv_data); +} + +static char *priv_data_global = "from app"; + +void app_schedule_set() +{ + esp_schedule_config_t schedule_config = { + .name = "test", + .trigger.type = ESP_SCHEDULE_TYPE_DAYS_OF_WEEK, + .trigger.hours = 13, + .trigger.minutes = 30, + .trigger.day.repeat_days = ESP_SCHEDULE_DAY_MONDAY | ESP_SCHEDULE_DAY_THURSDAY, + .trigger_cb = app_schedule_trigger_cb, + .priv_data = priv_data_global, + }; + esp_schedule_create(&schedule_config); +} + +void app_schedule_init() +{ + uint8_t schedule_count; + esp_schedule_handle_t *schedule_list = esp_schedule_init(true, NULL, &schedule_count); + if (schedule_count > 0 && schedule_list != NULL) { + ESP_LOGI(TAG, "Found %d schedule(s) in NVS.", schedule_count); + for (size_t i = 0; i < schedule_count; i++) { + esp_schedule_config_t schedule_config; + esp_schedule_get(schedule_list[i], &schedule_config); + schedule_config.trigger_cb = app_schedule_trigger_cb; + schedule_config.priv_data = priv_data_global; + esp_schedule_edit(schedule_list[i], &schedule_config); + } + } +} +``` diff --git a/examples/watering_demo/components/espressif__esp_schedule/component.mk b/examples/watering_demo/components/espressif__esp_schedule/component.mk new file mode 100644 index 0000000..49844ec --- /dev/null +++ b/examples/watering_demo/components/espressif__esp_schedule/component.mk @@ -0,0 +1,5 @@ +COMPONENT_ADD_INCLUDEDIRS := include + +COMPONENT_SRCDIRS := src + +CFLAGS += -Wno-unused-function diff --git a/examples/watering_demo/components/espressif__esp_schedule/idf_component.yml b/examples/watering_demo/components/espressif__esp_schedule/idf_component.yml new file mode 100644 index 0000000..2bcb52a --- /dev/null +++ b/examples/watering_demo/components/espressif__esp_schedule/idf_component.yml @@ -0,0 +1,8 @@ +dependencies: + espressif/rmaker_common: + version: ~1.4.2 +description: ESP Schedules, used in RainMaker +issues: https://github.com/espressif/esp-rainmaker/issues +repository: https://github.com/espressif/esp-rainmaker.git +url: https://github.com/espressif/esp-rainmaker/tree/master/components/esp_schedule +version: 1.1.0 diff --git a/examples/watering_demo/components/espressif__esp_schedule/include/esp_schedule.h b/examples/watering_demo/components/espressif__esp_schedule/include/esp_schedule.h new file mode 100644 index 0000000..4c75ebf --- /dev/null +++ b/examples/watering_demo/components/espressif__esp_schedule/include/esp_schedule.h @@ -0,0 +1,236 @@ +/* + * SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +/** Schedule Handle */ +typedef void *esp_schedule_handle_t; + +/** Maximum length of the schedule name allowed. This value cannot be more than 16 as it is used for NVS key. */ +#define MAX_SCHEDULE_NAME_LEN 16 + +/** Callback for schedule trigger + * + * This callback is called when the schedule is triggered. + * + * @param[in] handle Schedule handle. + * @param[in] priv_data Pointer to the private data passed while creating/editing the schedule. + */ +typedef void (*esp_schedule_trigger_cb_t)(esp_schedule_handle_t handle, void *priv_data); + +/** Callback for schedule timestamp + * + * This callback is called when the next trigger timestamp of the schedule is changed. This might be useful to check if + * one time schedules have already passed while the device was powered off. + * + * @param[in] handle Schedule handle. + * @param[in] next_timestamp timestamp at which the schedule will trigger next. + * @param[in] priv_data Pointer to the user data passed while creating/editing the schedule. + */ +typedef void (*esp_schedule_timestamp_cb_t)(esp_schedule_handle_t handle, uint32_t next_timestamp, void *priv_data); + +/** Schedule type */ +typedef enum esp_schedule_type { + ESP_SCHEDULE_TYPE_INVALID = 0, + ESP_SCHEDULE_TYPE_DAYS_OF_WEEK, + ESP_SCHEDULE_TYPE_DATE, + ESP_SCHEDULE_TYPE_RELATIVE, +} esp_schedule_type_t; + +/** Schedule days. Used for ESP_SCHEDULE_TYPE_DAYS_OF_WEEK. */ +typedef enum esp_schedule_days { + ESP_SCHEDULE_DAY_ONCE = 0, + ESP_SCHEDULE_DAY_EVERYDAY = 0b1111111, + ESP_SCHEDULE_DAY_MONDAY = 1 << 0, + ESP_SCHEDULE_DAY_TUESDAY = 1 << 1, + ESP_SCHEDULE_DAY_WEDNESDAY = 1 << 2, + ESP_SCHEDULE_DAY_THURSDAY = 1 << 3, + ESP_SCHEDULE_DAY_FRIDAY = 1 << 4, + ESP_SCHEDULE_DAY_SATURDAY = 1 << 5, + ESP_SCHEDULE_DAY_SUNDAY = 1 << 6, +} esp_schedule_days_t; + +/** Schedule months. Used for ESP_SCHEDULE_TYPE_DATE. */ +typedef enum esp_schedule_months { + ESP_SCHEDULE_MONTH_ONCE = 0, + ESP_SCHEDULE_MONTH_ALL = 0b1111111, + ESP_SCHEDULE_MONTH_JANUARY = 1 << 0, + ESP_SCHEDULE_MONTH_FEBRUARY = 1 << 1, + ESP_SCHEDULE_MONTH_MARCH = 1 << 2, + ESP_SCHEDULE_MONTH_APRIL = 1 << 3, + ESP_SCHEDULE_MONTH_MAY = 1 << 4, + ESP_SCHEDULE_MONTH_JUNE = 1 << 5, + ESP_SCHEDULE_MONTH_JULY = 1 << 6, + ESP_SCHEDULE_MONTH_AUGUST = 1 << 7, + ESP_SCHEDULE_MONTH_SEPTEMBER = 1 << 8, + ESP_SCHEDULE_MONTH_OCTOBER = 1 << 9, + ESP_SCHEDULE_MONTH_NOVEMBER = 1 << 10, + ESP_SCHEDULE_MONTH_DECEMBER = 1 << 11, +} esp_schedule_months_t; + +/** Trigger details of the schedule */ +typedef struct esp_schedule_trigger { + /** Type of schedule */ + esp_schedule_type_t type; + /** Hours in 24 hour format. Accepted values: 0-23 */ + uint8_t hours; + /** Minutes in the given hour. Accepted values: 0-59. */ + uint8_t minutes; + /** For type ESP_SCHEDULE_TYPE_DAYS_OF_WEEK */ + struct { + /** 'OR' list of esp_schedule_days_t */ + uint8_t repeat_days; + } day; + /** For type ESP_SCHEDULE_TYPE_DATE */ + struct { + /** Day of the month. Accepted values: 1-31. */ + uint8_t day; + /* 'OR' list of esp_schedule_months_t */ + uint16_t repeat_months; + /** Year */ + uint16_t year; + /** If the schedule is to be repeated every year. */ + bool repeat_every_year; + } date; + /** For type ESP_SCHEDULE_TYPE_SECONDS */ + int relative_seconds; + /** Used for passing the next schedule timestamp for + * ESP_SCHEDULE_TYPE_RELATIVE */ + time_t next_scheduled_time_utc; +} esp_schedule_trigger_t; + +/** Schedule Validity + * Start and end time within which the schedule will be applicable. + */ +typedef struct esp_schedule_validity { + /* Start time as UTC timestamp */ + time_t start_time; + /* End time as UTC timestamp */ + time_t end_time; +} esp_schedule_validity_t; + +/** Schedule config */ +typedef struct esp_schedule_config { + /** Name of the schedule. This is like a primary key for the schedule. This is required. +1 for NULL termination. */ + char name[MAX_SCHEDULE_NAME_LEN + 1]; + /** Trigger details */ + esp_schedule_trigger_t trigger; + /** Trigger callback */ + esp_schedule_trigger_cb_t trigger_cb; + /** Timestamp callback */ + esp_schedule_timestamp_cb_t timestamp_cb; + /** Private data associated with the schedule. This will be passed to callbacks. */ + void *priv_data; + /** Validity of schedules. */ + esp_schedule_validity_t validity; +} esp_schedule_config_t; + +/** Initialize ESP Schedule + * + * This initializes ESP Schedule. This must be called first before calling any of the other APIs. + * This API also gets all the schedules from NVS (if it has been enabled). + * + * Note: After calling this API, the pointers to the callbacks should be updated for all the schedules by calling + * esp_schedule_get() followed by esp_schedule_edit() with the correct callbacks. + * + * @param[in] enable_nvs If NVS is to be enabled or not. + * @param[in] nvs_partition (Optional) The NVS partition to be used. If NULL is passed, the default partition is used. + * @param[out] schedule_count Number of active schedules found in NVS. + * + * @return Array of schedule handles if any schedules have been found. + * @return NULL if no schedule is found in NVS (or if NVS is not enabled). + */ +esp_schedule_handle_t *esp_schedule_init(bool enable_nvs, char *nvs_partition, uint8_t *schedule_count); + +/** Create Schedule + * + * This API can be used to create a new schedule. The schedule still needs to be enabled using + * esp_schedule_enable(). + * + * @param[in] schedule_config Configuration of the schedule to be created. + * + * @return Schedule handle if successfully created. + * @return NULL in case of error. + */ +esp_schedule_handle_t esp_schedule_create(esp_schedule_config_t *schedule_config); + +/** Remove Schedule + * + * This API can be used to remove an existing schedule. + * + * @param[in] handle Schedule handle for the schedule to be removed. + * + * @return ESP_OK on success. + * @return error in case of failure. + */ +esp_err_t esp_schedule_delete(esp_schedule_handle_t handle); + +/** Edit Schedule + * + * This API can be used to edit an existing schedule. + * The schedule name should be same as when the schedule was created. The complete config must be provided + * or the previously stored config might be over-written. + * + * Note: If a schedule is edited when it is on-going, the new changes will not be reflected. + * You will need to disable the schedule, edit it, and then enable it again. + * + * @param[in] handle Schedule handle for the schedule to be edited. + * @param[in] schedule_config Configuration of the schedule to be edited. + * + * @return ESP_OK on success. + * @return error in case of failure. + */ +esp_err_t esp_schedule_edit(esp_schedule_handle_t handle, esp_schedule_config_t *schedule_config); + +/** Enable Schedule + * + * This API can be used to enable an existing schedule. + * It can be used to enable a schedule after it has been created using esp_schedule_create() + * or if the schedule has been disabled using esp_schedule_disable(). + * + * @param[in] handle Schedule handle for the schedule to be enabled. + * + * @return ESP_OK on success. + * @return error in case of failure. + */ +esp_err_t esp_schedule_enable(esp_schedule_handle_t handle); + +/** Disable Schedule + * + * This API can be used to disable an on-going schedule. + * It does not remove the schedule, just stops it. The schedule can be enabled again using + * esp_schedule_enable(). + * + * @param[in] handle Schedule handle for the schedule to be disabled. + * + * @return ESP_OK on success. + * @return error in case of failure. + */ +esp_err_t esp_schedule_disable(esp_schedule_handle_t handle); + +/** Get Schedule + * + * This API can be used to get details of an existing schedule. + * The schedule_config is populated with the schedule details. + * + * @param[in] handle Schedule handle. + * @param[out] schedule_config Details of the schedule whose handle is passed. + * + * @return ESP_OK on success. + * @return error in case of failure. + */ +esp_err_t esp_schedule_get(esp_schedule_handle_t handle, esp_schedule_config_t *schedule_config); + +#ifdef __cplusplus +} +#endif diff --git a/examples/watering_demo/components/espressif__esp_schedule/src/esp_schedule.c b/examples/watering_demo/components/espressif__esp_schedule/src/esp_schedule.c new file mode 100644 index 0000000..017ecb3 --- /dev/null +++ b/examples/watering_demo/components/espressif__esp_schedule/src/esp_schedule.c @@ -0,0 +1,600 @@ +/* + * SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include +#include +#include +#include +#include "esp_schedule_internal.h" + +static const char *TAG = "esp_schedule"; + +#define SECONDS_TILL_2020 ((2020 - 1970) * 365 * 24 * 3600) +#define SECONDS_IN_DAY (60 * 60 * 24) + +static bool init_done = false; + +static int esp_schedule_get_no_of_days(esp_schedule_trigger_t *trigger, struct tm *current_time, struct tm *schedule_time) +{ + /* for day, monday = 0, sunday = 6. */ + int next_day = 0; + /* struct tm has tm_wday with sunday as 0. Whereas we have monday as 0. Converting struct tm to our format */ + int today = ((current_time->tm_wday + 7 - 1) % 7); + + esp_schedule_days_t today_bit = 1 << today; + uint8_t repeat_days = trigger->day.repeat_days; + int current_seconds = (current_time->tm_hour * 60 + current_time->tm_min) * 60 + current_time->tm_sec; + int schedule_seconds = (schedule_time->tm_hour * 60 + schedule_time->tm_min) * 60; + + /* Handling for one time schedule */ + if (repeat_days == ESP_SCHEDULE_DAY_ONCE) { + if (schedule_seconds > current_seconds) { + /* The schedule is today and is yet to go off */ + return 0; + } else { + /* The schedule is tomorrow */ + return 1; + } + } + + /* Handling for repeating schedules */ + /* Check if it is today */ + if ((repeat_days & today_bit)) { + if (schedule_seconds > current_seconds) { + /* The schedule is today and is yet to go off. */ + return 0; + } + } + /* Check if it is this week or next week */ + if ((repeat_days & (today_bit ^ 0xFF)) > today_bit) { + /* Next schedule is yet to come in this week */ + next_day = ffs(repeat_days & (0xFF << (today + 1))) - 1; + return (next_day - today); + } else { + /* First scheduled day of the next week */ + next_day = ffs(repeat_days) - 1; + if (next_day == today) { + /* Same day, next week */ + return 7; + } + return (7 - today + next_day); + } + + ESP_LOGE(TAG, "No of days could not be found. This should not happen."); + return 0; +} + +static uint8_t esp_schedule_get_next_month(esp_schedule_trigger_t *trigger, struct tm *current_time, struct tm *schedule_time) +{ + int current_seconds = (current_time->tm_hour * 60 + current_time->tm_min) * 60 + current_time->tm_sec; + int schedule_seconds = (schedule_time->tm_hour * 60 + schedule_time->tm_min) * 60; + /* +1 is because struct tm has months starting from 0, whereas we have them starting from 1 */ + uint8_t current_month = current_time->tm_mon + 1; + /* -1 because month_bit starts from 0b1. So for January, it should be 1 << 0. And current_month starts from 1. */ + uint16_t current_month_bit = 1 << (current_month - 1); + uint8_t next_schedule_month = 0; + uint16_t repeat_months = trigger->date.repeat_months; + + /* Check if month is not specified */ + if (repeat_months == ESP_SCHEDULE_MONTH_ONCE) { + if (trigger->date.day == current_time->tm_mday) { + /* The schedule day is same. Check if time has already passed */ + if (schedule_seconds > current_seconds) { + /* The schedule is today and is yet to go off */ + return current_month; + } else { + /* Today's time has passed */ + return (current_month + 1); + } + } else if (trigger->date.day > current_time->tm_mday) { + /* The day is yet to come in this month */ + return current_month; + } else { + /* The day has passed in the current month */ + return (current_month + 1); + } + } + + /* Check if schedule is not this year itself, it is in future. */ + if (trigger->date.year > (current_time->tm_year + 1900)) { + /* Find first schedule month of next year */ + next_schedule_month = ffs(repeat_months); + /* Year will be handled by the caller. So no need to add any additional months */ + return next_schedule_month; + } + + /* Check if schedule is this month and is yet to come */ + if (current_month_bit & repeat_months) { + if (trigger->date.day == current_time->tm_mday) { + /* The schedule day is same. Check if time has already passed */ + if (schedule_seconds > current_seconds) { + /* The schedule is today and is yet to go off */ + return current_month; + } + } + if (trigger->date.day > current_time->tm_mday) { + /* The day is yet to come in this month */ + return current_month; + } + } + + /* Check if schedule is this year */ + if ((repeat_months & (current_month_bit ^ 0xFFFF)) > current_month_bit) { + /* Next schedule month is yet to come in this year */ + next_schedule_month = ffs(repeat_months & (0xFFFF << (current_month))); + return next_schedule_month; + } + + /* Check if schedule is for this year and does not repeat */ + if (!trigger->date.repeat_every_year) { + if (trigger->date.year <= (current_time->tm_year + 1900)) { + ESP_LOGE(TAG, "Schedule does not repeat next year, but get_next_month has been called."); + return 0; + } + } + + /* Schedule is not this year */ + /* Find first schedule month of next year */ + next_schedule_month = ffs(repeat_months); + /* +12 because the schedule is next year */ + return (next_schedule_month + 12); +} + +static uint16_t esp_schedule_get_next_year(esp_schedule_trigger_t *trigger, struct tm *current_time, struct tm *schedule_time) +{ + uint16_t current_year = current_time->tm_year + 1900; + uint16_t schedule_year = trigger->date.year; + if (schedule_year > current_year) { + return schedule_year; + } + /* If the schedule is set to repeat_every_year, we return the current year */ + /* If the schedule has already passed in this year, we still return current year, as the additional months will be handled in get_next_month */ + return current_year; +} + +static uint32_t esp_schedule_get_next_schedule_time_diff(const char *schedule_name, esp_schedule_trigger_t *trigger) +{ + struct tm current_time, schedule_time; + time_t now; + char time_str[64]; + int32_t time_diff; + + /* Get current time */ + time(&now); + /* Handling ESP_SCHEDULE_TYPE_RELATIVE first since it doesn't require any + * computation based on days, hours, minutes, etc. + */ + if (trigger->type == ESP_SCHEDULE_TYPE_RELATIVE) { + /* If next scheduled time is already set, just compute the difference + * between current time and next scheduled time and return that diff. + */ + time_t target; + if (trigger->next_scheduled_time_utc > 0) { + target = (time_t)trigger->next_scheduled_time_utc; + time_diff = difftime(target, now); + } else { + target = now + (time_t)trigger->relative_seconds; + time_diff = trigger->relative_seconds; + } + localtime_r(&target, &schedule_time); + trigger->next_scheduled_time_utc = mktime(&schedule_time); + /* Print schedule time */ + memset(time_str, 0, sizeof(time_str)); + strftime(time_str, sizeof(time_str), "%c %z[%Z]", &schedule_time); + ESP_LOGI(TAG, "Schedule %s will be active on: %s. DST: %s", schedule_name, time_str, schedule_time.tm_isdst ? "Yes" : "No"); + return time_diff; + } + localtime_r(&now, ¤t_time); + + /* Get schedule time */ + localtime_r(&now, &schedule_time); + schedule_time.tm_sec = 0; + schedule_time.tm_min = trigger->minutes; + schedule_time.tm_hour = trigger->hours; + mktime(&schedule_time); + + /* Adjust schedule day */ + if (trigger->type == ESP_SCHEDULE_TYPE_DAYS_OF_WEEK) { + int no_of_days = 0; + no_of_days = esp_schedule_get_no_of_days(trigger, ¤t_time, &schedule_time); + schedule_time.tm_sec += no_of_days * SECONDS_IN_DAY; + } + if (trigger->type == ESP_SCHEDULE_TYPE_DATE) { + schedule_time.tm_mday = trigger->date.day; + schedule_time.tm_mon = esp_schedule_get_next_month(trigger, ¤t_time, &schedule_time) - 1; + schedule_time.tm_year = esp_schedule_get_next_year(trigger, ¤t_time, &schedule_time) - 1900; + if (schedule_time.tm_mon < 0) { + ESP_LOGE(TAG, "Invalid month found: %d. Setting it to next month.", schedule_time.tm_mon); + schedule_time.tm_mon = current_time.tm_mon + 1; + } + if (schedule_time.tm_mon >= 12) { + schedule_time.tm_year += schedule_time.tm_mon / 12; + schedule_time.tm_mon = schedule_time.tm_mon % 12; + } + } + mktime(&schedule_time); + + /* Adjust time according to DST */ + time_t dst_adjust = 0; + if (!current_time.tm_isdst && schedule_time.tm_isdst) { + dst_adjust = -3600; + } else if (current_time.tm_isdst && !schedule_time.tm_isdst ) { + dst_adjust = 3600; + } + ESP_LOGD(TAG, "DST adjust seconds: %lld", (long long) dst_adjust); + schedule_time.tm_sec += dst_adjust; + mktime(&schedule_time); + + /* Print schedule time */ + memset(time_str, 0, sizeof(time_str)); + strftime(time_str, sizeof(time_str), "%c %z[%Z]", &schedule_time); + ESP_LOGI(TAG, "Schedule %s will be active on: %s. DST: %s", schedule_name, time_str, schedule_time.tm_isdst ? "Yes" : "No"); + + /* Calculate difference */ + time_diff = difftime((mktime(&schedule_time)), mktime(¤t_time)); + + /* For one time schedules to check for expiry after a reboot. If NVS is enabled, this should be stored in NVS. */ + trigger->next_scheduled_time_utc = mktime(&schedule_time); + + return time_diff; +} + +static bool esp_schedule_is_expired(esp_schedule_trigger_t *trigger) +{ + time_t current_timestamp = 0; + struct tm current_time = {0}; + time(¤t_timestamp); + localtime_r(¤t_timestamp, ¤t_time); + + if (trigger->type == ESP_SCHEDULE_TYPE_RELATIVE) { + if (trigger->next_scheduled_time_utc > 0 && trigger->next_scheduled_time_utc <= current_timestamp) { + /* Relative seconds based schedule has expired */ + return true; + } else if (trigger->next_scheduled_time_utc == 0) { + /* Schedule has been disabled , so it is as good as expired. */ + return true; + } + } else if (trigger->type == ESP_SCHEDULE_TYPE_DAYS_OF_WEEK) { + if (trigger->day.repeat_days == ESP_SCHEDULE_DAY_ONCE) { + if (trigger->next_scheduled_time_utc > 0 && trigger->next_scheduled_time_utc <= current_timestamp) { + /* One time schedule has expired */ + return true; + } else if (trigger->next_scheduled_time_utc == 0) { + /* Schedule has been disabled , so it is as good as expired. */ + return true; + } + } + } else if (trigger->type == ESP_SCHEDULE_TYPE_DATE) { + if (trigger->date.repeat_months == 0) { + if (trigger->next_scheduled_time_utc > 0 && trigger->next_scheduled_time_utc <= current_timestamp) { + /* One time schedule has expired */ + return true; + } else { + return false; + } + } + if (trigger->date.repeat_every_year == true) { + return false; + } + + struct tm schedule_time = {0}; + localtime_r(¤t_timestamp, &schedule_time); + schedule_time.tm_sec = 0; + schedule_time.tm_min = trigger->minutes; + schedule_time.tm_hour = trigger->hours; + schedule_time.tm_mday = trigger->date.day; + /* For expiry, just check the last month of the repeat_months. */ + /* '-1' because struct tm has months starting from 0 and we have months starting from 1. */ + schedule_time.tm_mon = fls(trigger->date.repeat_months) - 1; + /* '-1900' because struct tm has number of years after 1900 */ + schedule_time.tm_year = trigger->date.year - 1900; + time_t schedule_timestamp = mktime(&schedule_time); + + if (schedule_timestamp < current_timestamp) { + return true; + } + } else { + /* Invalid type. Mark as expired */ + return true; + } + return false; +} + +static void esp_schedule_stop_timer(esp_schedule_t *schedule) +{ + xTimerStop(schedule->timer, portMAX_DELAY); +} + +static void esp_schedule_start_timer(esp_schedule_t *schedule) +{ + time_t current_time = 0; + time(¤t_time); + if (current_time < SECONDS_TILL_2020) { + ESP_LOGE(TAG, "Time is not updated"); + return; + } + + schedule->next_scheduled_time_diff = esp_schedule_get_next_schedule_time_diff(schedule->name, &schedule->trigger); + ESP_LOGI(TAG, "Starting a timer for %"PRIu32" seconds for schedule %s", schedule->next_scheduled_time_diff, schedule->name); + + if (schedule->timestamp_cb) { + schedule->timestamp_cb((esp_schedule_handle_t)schedule, schedule->trigger.next_scheduled_time_utc, schedule->priv_data); + } + + xTimerStop(schedule->timer, portMAX_DELAY); + xTimerChangePeriod(schedule->timer, (schedule->next_scheduled_time_diff * 1000) / portTICK_PERIOD_MS, portMAX_DELAY); +} + +static void esp_schedule_common_timer_cb(TimerHandle_t timer) +{ + void *priv_data = pvTimerGetTimerID(timer); + if (priv_data == NULL) { + return; + } + esp_schedule_t *schedule = (esp_schedule_t *)priv_data; + time_t now; + time(&now); + struct tm validity_time; + char time_str[64] = {0}; + if (schedule->validity.start_time != 0) { + if (now < schedule->validity.start_time) { + memset(time_str, 0, sizeof(time_str)); + localtime_r(&schedule->validity.start_time, &validity_time); + strftime(time_str, sizeof(time_str), "%c %z[%Z]", &validity_time); + ESP_LOGW(TAG, "Schedule %s skipped. It will be active only after: %s. DST: %s.", schedule->name, time_str, validity_time.tm_isdst ? "Yes" : "No"); + /* TODO: Start the timer such that the next time it triggeres, it will be within the valid window. + * Currently, it will just keep triggering and then get skipped if not in valid range. + */ + goto restart_schedule; + } + } + if (schedule->validity.end_time != 0) { + if (now > schedule->validity.end_time) { + localtime_r(&schedule->validity.end_time, &validity_time); + strftime(time_str, sizeof(time_str), "%c %z[%Z]", &validity_time); + ESP_LOGW(TAG, "Schedule %s skipped. It can't be active after: %s. DST: %s.", schedule->name, time_str, validity_time.tm_isdst ? "Yes" : "No"); + /* Return from here will ensure that the timer does not start again for this schedule */ + return; + } + } + ESP_LOGI(TAG, "Schedule %s triggered", schedule->name); + if (schedule->trigger_cb) { + schedule->trigger_cb((esp_schedule_handle_t)schedule, schedule->priv_data); + } + +restart_schedule: + + if (esp_schedule_is_expired(&schedule->trigger)) { + /* Not deleting the schedule here. Just not starting it again. */ + return; + } + esp_schedule_start_timer(schedule); +} + +static void esp_schedule_delete_timer(esp_schedule_t *schedule) +{ + xTimerDelete(schedule->timer, portMAX_DELAY); +} + +static void esp_schedule_create_timer(esp_schedule_t *schedule) +{ + if (esp_schedule_nvs_is_enabled()) { + /* This is just used for calculating next_scheduled_time_utc for ESP_SCHEDULE_DAY_ONCE (in case of ESP_SCHEDULE_TYPE_DAYS_OF_WEEK) or for ESP_SCHEDULE_MONTH_ONCE (in case of ESP_SCHEDULE_TYPE_DATE), and only used when NVS is enabled. And if NVS is enabled, time will already be synced and the time will be correctly calculated. */ + schedule->next_scheduled_time_diff = esp_schedule_get_next_schedule_time_diff(schedule->name, &schedule->trigger); + } + + /* Temporarily setting the timer for 1 (anything greater than 0) tick. This will get changed when xTimerChangePeriod() is called. */ + schedule->timer = xTimerCreate("schedule", 1, pdFALSE, (void *)schedule, esp_schedule_common_timer_cb); +} + +esp_err_t esp_schedule_get(esp_schedule_handle_t handle, esp_schedule_config_t *schedule_config) +{ + if (schedule_config == NULL) { + return ESP_ERR_INVALID_ARG; + } + if (handle == NULL) { + return ESP_ERR_INVALID_ARG; + } + esp_schedule_t *schedule = (esp_schedule_t *)handle; + + strcpy(schedule_config->name, schedule->name); + schedule_config->trigger.type = schedule->trigger.type; + schedule_config->trigger.hours = schedule->trigger.hours; + schedule_config->trigger.minutes = schedule->trigger.minutes; + if (schedule->trigger.type == ESP_SCHEDULE_TYPE_DAYS_OF_WEEK) { + schedule_config->trigger.day.repeat_days = schedule->trigger.day.repeat_days; + } else if (schedule->trigger.type == ESP_SCHEDULE_TYPE_DATE) { + schedule_config->trigger.date.day = schedule->trigger.date.day; + schedule_config->trigger.date.repeat_months = schedule->trigger.date.repeat_months; + schedule_config->trigger.date.year = schedule->trigger.date.year; + schedule_config->trigger.date.repeat_every_year = schedule->trigger.date.repeat_every_year; + } + + schedule_config->trigger_cb = schedule->trigger_cb; + schedule_config->timestamp_cb = schedule->timestamp_cb; + schedule_config->priv_data = schedule->priv_data; + schedule_config->validity = schedule->validity; + return ESP_OK; +} + +esp_err_t esp_schedule_enable(esp_schedule_handle_t handle) +{ + if (handle == NULL) { + return ESP_ERR_INVALID_ARG; + } + esp_schedule_t *schedule = (esp_schedule_t *)handle; + esp_schedule_start_timer(schedule); + return ESP_OK; +} + +esp_err_t esp_schedule_disable(esp_schedule_handle_t handle) +{ + if (handle == NULL) { + return ESP_ERR_INVALID_ARG; + } + esp_schedule_t *schedule = (esp_schedule_t *)handle; + esp_schedule_stop_timer(schedule); + /* Disabling a schedule should also reset the next_scheduled_time. + * It would be re-computed after enabling. + */ + schedule->trigger.next_scheduled_time_utc = 0; + return ESP_OK; +} + +static esp_err_t esp_schedule_set(esp_schedule_t *schedule, esp_schedule_config_t *schedule_config) +{ + /* Setting everything apart from name. */ + schedule->trigger.type = schedule_config->trigger.type; + if (schedule->trigger.type == ESP_SCHEDULE_TYPE_RELATIVE) { + schedule->trigger.relative_seconds = schedule_config->trigger.relative_seconds; + schedule->trigger.next_scheduled_time_utc = schedule_config->trigger.next_scheduled_time_utc; + } else { + schedule->trigger.hours = schedule_config->trigger.hours; + schedule->trigger.minutes = schedule_config->trigger.minutes; + + if (schedule->trigger.type == ESP_SCHEDULE_TYPE_DAYS_OF_WEEK) { + schedule->trigger.day.repeat_days = schedule_config->trigger.day.repeat_days; + } else if (schedule->trigger.type == ESP_SCHEDULE_TYPE_DATE) { + schedule->trigger.date.day = schedule_config->trigger.date.day; + schedule->trigger.date.repeat_months = schedule_config->trigger.date.repeat_months; + schedule->trigger.date.year = schedule_config->trigger.date.year; + schedule->trigger.date.repeat_every_year = schedule_config->trigger.date.repeat_every_year; + } + } + + schedule->trigger_cb = schedule_config->trigger_cb; + schedule->timestamp_cb = schedule_config->timestamp_cb; + schedule->priv_data = schedule_config->priv_data; + schedule->validity = schedule_config->validity; + esp_schedule_nvs_add(schedule); + return ESP_OK; +} + +esp_err_t esp_schedule_edit(esp_schedule_handle_t handle, esp_schedule_config_t *schedule_config) +{ + if (handle == NULL || schedule_config == NULL) { + return ESP_ERR_INVALID_ARG; + } + esp_schedule_t *schedule = (esp_schedule_t *)handle; + if (strncmp(schedule->name, schedule_config->name, sizeof(schedule->name)) != 0) { + ESP_LOGE(TAG, "Schedule name mismatch. Expected: %s, Passed: %s", schedule->name, schedule_config->name); + return ESP_FAIL; + } + + /* Editing a schedule with relative time should also reset it. */ + if (schedule->trigger.type == ESP_SCHEDULE_TYPE_RELATIVE) { + schedule->trigger.next_scheduled_time_utc = 0; + } + esp_schedule_set(schedule, schedule_config); + ESP_LOGD(TAG, "Schedule %s edited", schedule->name); + return ESP_OK; +} + +esp_err_t esp_schedule_delete(esp_schedule_handle_t handle) +{ + if (handle == NULL) { + return ESP_ERR_INVALID_ARG; + } + esp_schedule_t *schedule = (esp_schedule_t *)handle; + ESP_LOGI(TAG, "Deleting schedule %s", schedule->name); + if (schedule->timer) { + esp_schedule_stop_timer(schedule); + esp_schedule_delete_timer(schedule); + } + esp_schedule_nvs_remove(schedule); + free(schedule); + return ESP_OK; +} + +esp_schedule_handle_t esp_schedule_create(esp_schedule_config_t *schedule_config) +{ + if (schedule_config == NULL) { + return NULL; + } + if (strlen(schedule_config->name) <= 0) { + ESP_LOGE(TAG, "Set schedule failed. Please enter a unique valid name for the schedule."); + return NULL; + } + + if (schedule_config->trigger.type == ESP_SCHEDULE_TYPE_INVALID) { + ESP_LOGE(TAG, "Schedule type is invalid."); + return NULL; + } + + esp_schedule_t *schedule = (esp_schedule_t *)MEM_CALLOC_EXTRAM(1, sizeof(esp_schedule_t)); + if (schedule == NULL) { + ESP_LOGE(TAG, "Could not allocate handle"); + return NULL; + } + strlcpy(schedule->name, schedule_config->name, sizeof(schedule->name)); + + esp_schedule_set(schedule, schedule_config); + + esp_schedule_create_timer(schedule); + ESP_LOGD(TAG, "Schedule %s created", schedule->name); + return (esp_schedule_handle_t)schedule; +} + +esp_schedule_handle_t *esp_schedule_init(bool enable_nvs, char *nvs_partition, uint8_t *schedule_count) +{ +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0) + if (!esp_sntp_enabled()) { + ESP_LOGI(TAG, "Initializing SNTP"); + esp_sntp_setoperatingmode(SNTP_OPMODE_POLL); + esp_sntp_setservername(0, "pool.ntp.org"); + esp_sntp_init(); + } +#else + if (!sntp_enabled()) { + ESP_LOGI(TAG, "Initializing SNTP"); + sntp_setoperatingmode(SNTP_OPMODE_POLL); + sntp_setservername(0, "pool.ntp.org"); + sntp_init(); + } +#endif + + if (!enable_nvs) { + return NULL; + } + + /* Wait for time to be updated here */ + + + /* Below this is initialising schedules from NVS */ + esp_schedule_nvs_init(nvs_partition); + + /* Get handle list from NVS */ + esp_schedule_handle_t *handle_list = NULL; + *schedule_count = 0; + handle_list = esp_schedule_nvs_get_all(schedule_count); + if (handle_list == NULL) { + ESP_LOGI(TAG, "No schedules found in NVS"); + return NULL; + } + ESP_LOGI(TAG, "Schedules found in NVS: %"PRIu8, *schedule_count); + /* Start/Delete the schedules */ + esp_schedule_t *schedule = NULL; + for (size_t handle_count = 0; handle_count < *schedule_count; handle_count++) { + schedule = (esp_schedule_t *)handle_list[handle_count]; + schedule->trigger_cb = NULL; + schedule->timer = NULL; + /* Check for ONCE and expired schedules and delete them. */ + if (esp_schedule_is_expired(&schedule->trigger)) { + /* This schedule has already expired. */ + ESP_LOGI(TAG, "Schedule %s does not repeat and has already expired. Deleting it.", schedule->name); + esp_schedule_delete((esp_schedule_handle_t)schedule); + /* Removing the schedule from the list */ + handle_list[handle_count] = handle_list[*schedule_count - 1]; + (*schedule_count)--; + handle_count--; + continue; + } + esp_schedule_create_timer(schedule); + esp_schedule_start_timer(schedule); + } + init_done = true; + return handle_list; +} diff --git a/examples/watering_demo/components/espressif__esp_schedule/src/esp_schedule_internal.h b/examples/watering_demo/components/espressif__esp_schedule/src/esp_schedule_internal.h new file mode 100644 index 0000000..64c3f8a --- /dev/null +++ b/examples/watering_demo/components/espressif__esp_schedule/src/esp_schedule_internal.h @@ -0,0 +1,28 @@ +/* + * SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include + +typedef struct esp_schedule { + char name[MAX_SCHEDULE_NAME_LEN + 1]; + esp_schedule_trigger_t trigger; + uint32_t next_scheduled_time_diff; + TimerHandle_t timer; + esp_schedule_trigger_cb_t trigger_cb; + esp_schedule_timestamp_cb_t timestamp_cb; + void *priv_data; + esp_schedule_validity_t validity; +} esp_schedule_t; + +esp_err_t esp_schedule_nvs_add(esp_schedule_t *schedule); +esp_err_t esp_schedule_nvs_remove(esp_schedule_t *schedule); +esp_schedule_handle_t *esp_schedule_nvs_get_all(uint8_t *schedule_count); +bool esp_schedule_nvs_is_enabled(void); +esp_err_t esp_schedule_nvs_init(char *nvs_partition); diff --git a/examples/watering_demo/components/espressif__esp_schedule/src/esp_schedule_nvs.c b/examples/watering_demo/components/espressif__esp_schedule/src/esp_schedule_nvs.c new file mode 100644 index 0000000..acff405 --- /dev/null +++ b/examples/watering_demo/components/espressif__esp_schedule/src/esp_schedule_nvs.c @@ -0,0 +1,296 @@ +// Copyright 2020 Espressif Systems (Shanghai) PTE LTD +// +// 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. + +#include +#include +#include +#include +#include "esp_schedule_internal.h" + +static const char *TAG = "esp_schedule_nvs"; + +#define ESP_SCHEDULE_NVS_NAMESPACE "schd" +#define ESP_SCHEDULE_COUNT_KEY "schd_count" + +static char *esp_schedule_nvs_partition = NULL; +static bool nvs_enabled = false; + +esp_err_t esp_schedule_nvs_add(esp_schedule_t *schedule) +{ + if (!nvs_enabled) { + ESP_LOGD(TAG, "NVS not enabled. Not adding to NVS."); + return ESP_ERR_INVALID_STATE; + } + nvs_handle_t nvs_handle; + esp_err_t err = nvs_open_from_partition(esp_schedule_nvs_partition, ESP_SCHEDULE_NVS_NAMESPACE, NVS_READWRITE, &nvs_handle); + if (err != ESP_OK) { + ESP_LOGE(TAG, "NVS open failed with error %d", err); + return err; + } + + /* Check if this is new schedule or editing an existing schedule */ + size_t buf_size; + bool editing_schedule = true; + err = nvs_get_blob(nvs_handle, schedule->name, NULL, &buf_size); + if (err != ESP_OK) { + if (err == ESP_ERR_NVS_NOT_FOUND) { + editing_schedule = false; + } else { + ESP_LOGE(TAG, "NVS get failed with error %d", err); + nvs_close(nvs_handle); + return err; + } + } else { + ESP_LOGI(TAG, "Updating the existing schedule %s", schedule->name); + } + + err = nvs_set_blob(nvs_handle, schedule->name, schedule, sizeof(esp_schedule_t)); + if (err != ESP_OK) { + ESP_LOGE(TAG, "NVS set failed with error %d", err); + nvs_close(nvs_handle); + return err; + } + if (editing_schedule == false) { + uint8_t schedule_count; + err = nvs_get_u8(nvs_handle, ESP_SCHEDULE_COUNT_KEY, &schedule_count); + if (err != ESP_OK) { + if (err == ESP_ERR_NVS_NOT_FOUND) { + schedule_count = 0; + } else { + ESP_LOGE(TAG, "NVS get failed with error %d", err); + nvs_close(nvs_handle); + return err; + } + } + schedule_count++; + err = nvs_set_u8(nvs_handle, ESP_SCHEDULE_COUNT_KEY, schedule_count); + if (err != ESP_OK) { + ESP_LOGE(TAG, "NVS set failed for schedule count with error %d", err); + nvs_close(nvs_handle); + return err; + } + } + nvs_commit(nvs_handle); + nvs_close(nvs_handle); + ESP_LOGI(TAG, "Schedule %s added in NVS", schedule->name); + return ESP_OK; +} + +esp_err_t esp_schedule_nvs_remove_all(void) +{ + if (!nvs_enabled) { + ESP_LOGD(TAG, "NVS not enabled. Not removing from NVS."); + return ESP_ERR_INVALID_STATE; + } + nvs_handle_t nvs_handle; + esp_err_t err = nvs_open_from_partition(esp_schedule_nvs_partition, ESP_SCHEDULE_NVS_NAMESPACE, NVS_READWRITE, &nvs_handle); + if (err != ESP_OK) { + ESP_LOGE(TAG, "NVS open failed with error %d", err); + return err; + } + err = nvs_erase_all(nvs_handle); + if (err != ESP_OK) { + ESP_LOGE(TAG, "NVS erase all keys failed with error %d", err); + nvs_close(nvs_handle); + return err; + } + nvs_commit(nvs_handle); + nvs_close(nvs_handle); + ESP_LOGI(TAG, "All schedules removed from NVS"); + return ESP_OK; +} + +esp_err_t esp_schedule_nvs_remove(esp_schedule_t *schedule) +{ + if (!nvs_enabled) { + ESP_LOGD(TAG, "NVS not enabled. Not removing from NVS."); + return ESP_ERR_INVALID_STATE; + } + nvs_handle_t nvs_handle; + esp_err_t err = nvs_open_from_partition(esp_schedule_nvs_partition, ESP_SCHEDULE_NVS_NAMESPACE, NVS_READWRITE, &nvs_handle); + if (err != ESP_OK) { + ESP_LOGE(TAG, "NVS open failed with error %d", err); + return err; + } + err = nvs_erase_key(nvs_handle, schedule->name); + if (err != ESP_OK) { + ESP_LOGE(TAG, "NVS erase key failed with error %d", err); + nvs_close(nvs_handle); + return err; + } + uint8_t schedule_count; + err = nvs_get_u8(nvs_handle, ESP_SCHEDULE_COUNT_KEY, &schedule_count); + if (err != ESP_OK) { + ESP_LOGE(TAG, "NVS get failed for schedule count with error %d", err); + nvs_close(nvs_handle); + return err; + } + schedule_count--; + err = nvs_set_u8(nvs_handle, ESP_SCHEDULE_COUNT_KEY, schedule_count); + if (err != ESP_OK) { + ESP_LOGE(TAG, "NVS set failed for schedule count with error %d", err); + nvs_close(nvs_handle); + return err; + } + nvs_commit(nvs_handle); + nvs_close(nvs_handle); + ESP_LOGI(TAG, "Schedule %s removed from NVS", schedule->name); + return ESP_OK; +} + +static uint8_t esp_schedule_nvs_get_count(void) +{ + if (!nvs_enabled) { + ESP_LOGD(TAG, "NVS not enabled. Not getting count from NVS."); + return 0; + } + nvs_handle_t nvs_handle; + esp_err_t err = nvs_open_from_partition(esp_schedule_nvs_partition, ESP_SCHEDULE_NVS_NAMESPACE, NVS_READONLY, &nvs_handle); + if (err != ESP_OK) { + ESP_LOGE(TAG, "NVS open failed with error %d", err); + return 0; + } + uint8_t schedule_count; + err = nvs_get_u8(nvs_handle, ESP_SCHEDULE_COUNT_KEY, &schedule_count); + if (err != ESP_OK) { + ESP_LOGE(TAG, "NVS get failed for schedule count with error %d", err); + nvs_close(nvs_handle); + return 0; + } + nvs_close(nvs_handle); + ESP_LOGI(TAG, "Schedules in NVS: %d", schedule_count); + return schedule_count; +} + +static esp_schedule_handle_t esp_schedule_nvs_get(char *nvs_key) +{ + if (!nvs_enabled) { + ESP_LOGD(TAG, "NVS not enabled. Not getting from NVS."); + return NULL; + } + size_t buf_size; + nvs_handle_t nvs_handle; + esp_err_t err = nvs_open_from_partition(esp_schedule_nvs_partition, ESP_SCHEDULE_NVS_NAMESPACE, NVS_READONLY, &nvs_handle); + if (err != ESP_OK) { + ESP_LOGE(TAG, "NVS open failed with error %d", err); + return NULL; + } + err = nvs_get_blob(nvs_handle, nvs_key, NULL, &buf_size); + if (err != ESP_OK) { + ESP_LOGE(TAG, "NVS get failed with error %d", err); + nvs_close(nvs_handle); + return NULL; + } + esp_schedule_t *schedule = (esp_schedule_t *)malloc(buf_size); + if (schedule == NULL) { + ESP_LOGE(TAG, "Could not allocate handle"); + nvs_close(nvs_handle); + return NULL; + } + err = nvs_get_blob(nvs_handle, nvs_key, schedule, &buf_size); + if (err != ESP_OK) { + ESP_LOGE(TAG, "NVS get failed with error %d", err); + nvs_close(nvs_handle); + free(schedule); + return NULL; + } + nvs_close(nvs_handle); + ESP_LOGI(TAG, "Schedule %s found in NVS", schedule->name); + return (esp_schedule_handle_t) schedule; +} + +esp_schedule_handle_t *esp_schedule_nvs_get_all(uint8_t *schedule_count) +{ + if (!nvs_enabled) { + ESP_LOGD(TAG, "NVS not enabled. Not Initialising NVS."); + return NULL; + } + + *schedule_count = esp_schedule_nvs_get_count(); + if (*schedule_count == 0) { + ESP_LOGI(TAG, "No Entries found in NVS"); + return NULL; + } + esp_schedule_handle_t *handle_list = (esp_schedule_handle_t *)malloc(sizeof(esp_schedule_handle_t) * (*schedule_count)); + if (handle_list == NULL) { + ESP_LOGE(TAG, "Could not allocate schedule list"); + *schedule_count = 0; + return NULL; + } + int handle_count = 0; + + nvs_entry_info_t nvs_entry; +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) + nvs_iterator_t nvs_iterator = NULL; + esp_err_t err = nvs_entry_find(esp_schedule_nvs_partition, ESP_SCHEDULE_NVS_NAMESPACE, NVS_TYPE_BLOB, &nvs_iterator); + if (err != ESP_OK) { + ESP_LOGE(TAG, "No entry found in NVS"); + return NULL;; + } + while (err == ESP_OK) { + nvs_entry_info(nvs_iterator, &nvs_entry); + ESP_LOGI(TAG, "Found schedule in NVS with key: %s", nvs_entry.key); + handle_list[handle_count] = esp_schedule_nvs_get(nvs_entry.key); + if (handle_list[handle_count] != NULL) { + /* Increase count only if nvs_get was successful */ + handle_count++; + } + err = nvs_entry_next(&nvs_iterator); + } + nvs_release_iterator(nvs_iterator); +#else + nvs_iterator_t nvs_iterator = nvs_entry_find(esp_schedule_nvs_partition, ESP_SCHEDULE_NVS_NAMESPACE, NVS_TYPE_BLOB); + if (nvs_iterator == NULL) { + ESP_LOGE(TAG, "No entry found in NVS"); + return NULL;; + } + while (nvs_iterator != NULL) { + nvs_entry_info(nvs_iterator, &nvs_entry); + ESP_LOGI(TAG, "Found schedule in NVS with key: %s", nvs_entry.key); + handle_list[handle_count] = esp_schedule_nvs_get(nvs_entry.key); + if (handle_list[handle_count] != NULL) { + /* Increase count only if nvs_get was successful */ + handle_count++; + } + nvs_iterator = nvs_entry_next(nvs_iterator); + } +#endif + *schedule_count = handle_count; + ESP_LOGI(TAG, "Found %d schedules in NVS", *schedule_count); + return handle_list; +} + +bool esp_schedule_nvs_is_enabled(void) +{ + return nvs_enabled; +} + +esp_err_t esp_schedule_nvs_init(char *nvs_partition) +{ + if (nvs_enabled) { + ESP_LOGI(TAG, "NVS already enabled"); + return ESP_OK; + } + if (nvs_partition) { + esp_schedule_nvs_partition = strndup(nvs_partition, strlen(nvs_partition)); + } else { + esp_schedule_nvs_partition = strndup("nvs", strlen("nvs")); + } + if (esp_schedule_nvs_partition == NULL) { + ESP_LOGE(TAG, "Could not allocate nvs_partition"); + return ESP_ERR_NO_MEM; + } + nvs_enabled = true; + return ESP_OK; +} diff --git a/examples/watering_demo/main/idf_component.yml b/examples/watering_demo/main/idf_component.yml index a0e1c84..784f8c0 100644 --- a/examples/watering_demo/main/idf_component.yml +++ b/examples/watering_demo/main/idf_component.yml @@ -2,16 +2,6 @@ dependencies: idf: ">=5.0" - espressif/esp_rainmaker: - path: components/esp_rainmaker - git: https://github.com/espressif/esp-rainmaker.git - version: "cc861bffc818c607e799f66c79923fd4e2d00447" - espressif/esp_schedule: - path: components/esp_schedule - git: https://github.com/espressif/esp-rainmaker.git - version: "cc861bffc818c607e799f66c79923fd4e2d00447" - espressif/rmaker_common: "1.4.2" + espressif/esp_rainmaker: "1.0.0" espressif/esp-sr: "1.3.3" - espressif/json_generator: ^1 - espressif/json_parser: =1.0.0 espressif/qrcode: ^0.1.0 diff --git a/examples/watering_demo/sdkconfig.defaults b/examples/watering_demo/sdkconfig.defaults index 6293837..9fefd21 100644 --- a/examples/watering_demo/sdkconfig.defaults +++ b/examples/watering_demo/sdkconfig.defaults @@ -1,7 +1,6 @@ CONFIG_IDF_TARGET="esp32s3" CONFIG_ESPTOOLPY_FLASHMODE_QIO=y CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y -# CONFIG_ESPTOOLPY_FLASHSIZE_DETECT is not set CONFIG_PARTITION_TABLE_CUSTOM=y CONFIG_SR_MN_CN_MULTINET5_RECOGNITION_QUANT8=y CONFIG_CN_SPEECH_COMMAND_ID0="" @@ -36,6 +35,7 @@ CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240=y CONFIG_ESP32S3_INSTRUCTION_CACHE_32KB=y CONFIG_ESP32S3_DATA_CACHE_64KB=y CONFIG_ESP32S3_DATA_CACHE_LINE_64B=y +CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=4096 CONFIG_ESP32S3_SPIRAM_SUPPORT=y CONFIG_SPIRAM_MODE_OCT=y CONFIG_SPIRAM_SPEED_80M=y @@ -60,7 +60,6 @@ CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=3072 # Bluetooth controller CONFIG_BT_ENABLED=y -CONFIG_BTDM_CTRL_MODE_BLE_ONLY=y CONFIG_BT_NIMBLE_ENABLED=y CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1 CONFIG_BT_NIMBLE_ROLE_CENTRAL=n @@ -76,7 +75,6 @@ CONFIG_MBEDTLS_EXTERNAL_MEM_ALLOC=y CONFIG_BUTTON_SHORT_PRESS_TIME_MS=100 CONFIG_BUTTON_LONG_PRESS_TIME_MS=500 CONFIG_ADC_BUTTON_MAX_BUTTON_PER_CHANNEL=4 -CONFIG_IO_GLITCH_FILTER_TIME_MS=20 # BSP CONFIG_BSP_LCD_DRAW_BUF_HEIGHT=10