Merge branch 'feature/update_CI_latest' into 'master'

Feature/update CI build for ESP-IDF v5.3

See merge request ae_group/esp-box!98
This commit is contained in:
Xu Xin
2023-12-19 21:04:06 +08:00
49 changed files with 2729 additions and 1438 deletions
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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)
@@ -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
)
@@ -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
@@ -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.
@@ -1,57 +0,0 @@
#pragma once
#include <stdint.h>
#include <stddef.h>
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
@@ -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
@@ -1,174 +0,0 @@
#include <string.h>
#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<int16_t *>(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;
}
@@ -1,48 +0,0 @@
#pragma once
#include <stdio.h>
#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);
@@ -1,609 +0,0 @@
/**
* @file
* @version 0.1
*
* @copyright Copyright 2021 Espressif Systems (Shanghai) Co. Ltd.
* @copyright Copyright 2022 Chris Morgan <chmorgan@gmail.com>
*
* 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 <stdbool.h>
#include <stdint.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/unistd.h>
#include <sys/stat.h>
#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<void *>(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<void *>(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<int16_t *>(adata.samples) + (new_sample_count - 1);
int16_t *in = reinterpret_cast<int16_t *>(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<audio_instance_t *>(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<uint8_t *>(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<uint8_t *>(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;
}
@@ -1,102 +0,0 @@
#include <string.h>
#include <stdio.h>
#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<char *>(wav_head->ChunkID), "RIFF")) ||
(NULL == strstr(reinterpret_cast<char*>(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;
}
@@ -1,34 +0,0 @@
#pragma once
#include <stdio.h>
#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);
@@ -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
@@ -1,181 +0,0 @@
/**
* @file
* @version 0.1
*
* @copyright Copyright 2021 Espressif Systems (Shanghai) Co. Ltd.
* @copyright Copyright 2022 Chris Morgan <chmorgan@gmail.com>
*
* 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 <stddef.h>
#include <stdio.h>
#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
+43 -53
View File
@@ -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;
}
-4
View File
@@ -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,
+2 -1
View File
@@ -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"
+5 -1
View File
@@ -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));
@@ -0,0 +1 @@
2cbc7e2584f523d10fbfb4ecb7d25c07fa7d7757ab23d7a127494b1a58131ffe
@@ -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")
@@ -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 <esp_schedule.h>
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);
}
}
}
```
@@ -0,0 +1,5 @@
COMPONENT_ADD_INCLUDEDIRS := include
COMPONENT_SRCDIRS := src
CFLAGS += -Wno-unused-function
@@ -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
@@ -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 <stdint.h>
#include <time.h>
/** 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
@@ -0,0 +1,600 @@
/*
* SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <string.h>
#include <inttypes.h>
#include <esp_log.h>
#include <esp_sntp.h>
#include <esp_rmaker_utils.h>
#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, &current_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, &current_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, &current_time, &schedule_time) - 1;
schedule_time.tm_year = esp_schedule_get_next_year(trigger, &current_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(&current_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(&current_timestamp);
localtime_r(&current_timestamp, &current_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(&current_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(&current_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;
}
@@ -0,0 +1,28 @@
/*
* SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <freertos/FreeRTOS.h>
#include <freertos/timers.h>
#include <esp_schedule.h>
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);
@@ -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 <string.h>
#include <time.h>
#include <esp_log.h>
#include <nvs.h>
#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;
}
+1 -1
View File
@@ -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);
+1 -11
View File
@@ -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"
+1
View File
@@ -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
-1
View File
@@ -1,2 +1 @@
CONFIG_BSP_BOARD_ESP32_S3_BOX=y
CONFIG_BSP_ESP32_S3_BOX_1=y
+1 -2
View File
@@ -1,2 +1 @@
CONFIG_BSP_BOARD_ESP32_S3_BOX=y
CONFIG_BSP_ESP32_S3_BOX_3=y
CONFIG_BSP_BOARD_ESP32_S3_BOX_3=y
+1 -1
View File
@@ -1 +1 @@
CONFIG_BSP_BOARD_ESP32_S3_BOX_Lite=y
CONFIG_BSP_BOARD_ESP32_S3_BOX_Lite=y
@@ -0,0 +1 @@
CONFIG_BSP_BOARD_ESP32_S3_BOX_3=y
@@ -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
-1
View File
@@ -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
@@ -0,0 +1 @@
2cbc7e2584f523d10fbfb4ecb7d25c07fa7d7757ab23d7a127494b1a58131ffe
@@ -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")
@@ -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.
@@ -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 <esp_schedule.h>
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);
}
}
}
```
@@ -0,0 +1,5 @@
COMPONENT_ADD_INCLUDEDIRS := include
COMPONENT_SRCDIRS := src
CFLAGS += -Wno-unused-function
@@ -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
@@ -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 <stdint.h>
#include <time.h>
/** 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
@@ -0,0 +1,600 @@
/*
* SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <string.h>
#include <inttypes.h>
#include <esp_log.h>
#include <esp_sntp.h>
#include <esp_rmaker_utils.h>
#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, &current_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, &current_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, &current_time, &schedule_time) - 1;
schedule_time.tm_year = esp_schedule_get_next_year(trigger, &current_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(&current_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(&current_timestamp);
localtime_r(&current_timestamp, &current_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(&current_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(&current_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;
}
@@ -0,0 +1,28 @@
/*
* SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <freertos/FreeRTOS.h>
#include <freertos/timers.h>
#include <esp_schedule.h>
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);
@@ -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 <string.h>
#include <time.h>
#include <esp_log.h>
#include <nvs.h>
#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;
}
+1 -11
View File
@@ -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
+1 -3
View File
@@ -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