Feature/update CI build for ESP-IDF v5.3
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
[](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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
[](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, ¤t_time);
|
||||
|
||||
/* Get schedule time */
|
||||
localtime_r(&now, &schedule_time);
|
||||
schedule_time.tm_sec = 0;
|
||||
schedule_time.tm_min = trigger->minutes;
|
||||
schedule_time.tm_hour = trigger->hours;
|
||||
mktime(&schedule_time);
|
||||
|
||||
/* Adjust schedule day */
|
||||
if (trigger->type == ESP_SCHEDULE_TYPE_DAYS_OF_WEEK) {
|
||||
int no_of_days = 0;
|
||||
no_of_days = esp_schedule_get_no_of_days(trigger, ¤t_time, &schedule_time);
|
||||
schedule_time.tm_sec += no_of_days * SECONDS_IN_DAY;
|
||||
}
|
||||
if (trigger->type == ESP_SCHEDULE_TYPE_DATE) {
|
||||
schedule_time.tm_mday = trigger->date.day;
|
||||
schedule_time.tm_mon = esp_schedule_get_next_month(trigger, ¤t_time, &schedule_time) - 1;
|
||||
schedule_time.tm_year = esp_schedule_get_next_year(trigger, ¤t_time, &schedule_time) - 1900;
|
||||
if (schedule_time.tm_mon < 0) {
|
||||
ESP_LOGE(TAG, "Invalid month found: %d. Setting it to next month.", schedule_time.tm_mon);
|
||||
schedule_time.tm_mon = current_time.tm_mon + 1;
|
||||
}
|
||||
if (schedule_time.tm_mon >= 12) {
|
||||
schedule_time.tm_year += schedule_time.tm_mon / 12;
|
||||
schedule_time.tm_mon = schedule_time.tm_mon % 12;
|
||||
}
|
||||
}
|
||||
mktime(&schedule_time);
|
||||
|
||||
/* Adjust time according to DST */
|
||||
time_t dst_adjust = 0;
|
||||
if (!current_time.tm_isdst && schedule_time.tm_isdst) {
|
||||
dst_adjust = -3600;
|
||||
} else if (current_time.tm_isdst && !schedule_time.tm_isdst ) {
|
||||
dst_adjust = 3600;
|
||||
}
|
||||
ESP_LOGD(TAG, "DST adjust seconds: %lld", (long long) dst_adjust);
|
||||
schedule_time.tm_sec += dst_adjust;
|
||||
mktime(&schedule_time);
|
||||
|
||||
/* Print schedule time */
|
||||
memset(time_str, 0, sizeof(time_str));
|
||||
strftime(time_str, sizeof(time_str), "%c %z[%Z]", &schedule_time);
|
||||
ESP_LOGI(TAG, "Schedule %s will be active on: %s. DST: %s", schedule_name, time_str, schedule_time.tm_isdst ? "Yes" : "No");
|
||||
|
||||
/* Calculate difference */
|
||||
time_diff = difftime((mktime(&schedule_time)), mktime(¤t_time));
|
||||
|
||||
/* For one time schedules to check for expiry after a reboot. If NVS is enabled, this should be stored in NVS. */
|
||||
trigger->next_scheduled_time_utc = mktime(&schedule_time);
|
||||
|
||||
return time_diff;
|
||||
}
|
||||
|
||||
static bool esp_schedule_is_expired(esp_schedule_trigger_t *trigger)
|
||||
{
|
||||
time_t current_timestamp = 0;
|
||||
struct tm current_time = {0};
|
||||
time(¤t_timestamp);
|
||||
localtime_r(¤t_timestamp, ¤t_time);
|
||||
|
||||
if (trigger->type == ESP_SCHEDULE_TYPE_RELATIVE) {
|
||||
if (trigger->next_scheduled_time_utc > 0 && trigger->next_scheduled_time_utc <= current_timestamp) {
|
||||
/* Relative seconds based schedule has expired */
|
||||
return true;
|
||||
} else if (trigger->next_scheduled_time_utc == 0) {
|
||||
/* Schedule has been disabled , so it is as good as expired. */
|
||||
return true;
|
||||
}
|
||||
} else if (trigger->type == ESP_SCHEDULE_TYPE_DAYS_OF_WEEK) {
|
||||
if (trigger->day.repeat_days == ESP_SCHEDULE_DAY_ONCE) {
|
||||
if (trigger->next_scheduled_time_utc > 0 && trigger->next_scheduled_time_utc <= current_timestamp) {
|
||||
/* One time schedule has expired */
|
||||
return true;
|
||||
} else if (trigger->next_scheduled_time_utc == 0) {
|
||||
/* Schedule has been disabled , so it is as good as expired. */
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else if (trigger->type == ESP_SCHEDULE_TYPE_DATE) {
|
||||
if (trigger->date.repeat_months == 0) {
|
||||
if (trigger->next_scheduled_time_utc > 0 && trigger->next_scheduled_time_utc <= current_timestamp) {
|
||||
/* One time schedule has expired */
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (trigger->date.repeat_every_year == true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
struct tm schedule_time = {0};
|
||||
localtime_r(¤t_timestamp, &schedule_time);
|
||||
schedule_time.tm_sec = 0;
|
||||
schedule_time.tm_min = trigger->minutes;
|
||||
schedule_time.tm_hour = trigger->hours;
|
||||
schedule_time.tm_mday = trigger->date.day;
|
||||
/* For expiry, just check the last month of the repeat_months. */
|
||||
/* '-1' because struct tm has months starting from 0 and we have months starting from 1. */
|
||||
schedule_time.tm_mon = fls(trigger->date.repeat_months) - 1;
|
||||
/* '-1900' because struct tm has number of years after 1900 */
|
||||
schedule_time.tm_year = trigger->date.year - 1900;
|
||||
time_t schedule_timestamp = mktime(&schedule_time);
|
||||
|
||||
if (schedule_timestamp < current_timestamp) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
/* Invalid type. Mark as expired */
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void esp_schedule_stop_timer(esp_schedule_t *schedule)
|
||||
{
|
||||
xTimerStop(schedule->timer, portMAX_DELAY);
|
||||
}
|
||||
|
||||
static void esp_schedule_start_timer(esp_schedule_t *schedule)
|
||||
{
|
||||
time_t current_time = 0;
|
||||
time(¤t_time);
|
||||
if (current_time < SECONDS_TILL_2020) {
|
||||
ESP_LOGE(TAG, "Time is not updated");
|
||||
return;
|
||||
}
|
||||
|
||||
schedule->next_scheduled_time_diff = esp_schedule_get_next_schedule_time_diff(schedule->name, &schedule->trigger);
|
||||
ESP_LOGI(TAG, "Starting a timer for %"PRIu32" seconds for schedule %s", schedule->next_scheduled_time_diff, schedule->name);
|
||||
|
||||
if (schedule->timestamp_cb) {
|
||||
schedule->timestamp_cb((esp_schedule_handle_t)schedule, schedule->trigger.next_scheduled_time_utc, schedule->priv_data);
|
||||
}
|
||||
|
||||
xTimerStop(schedule->timer, portMAX_DELAY);
|
||||
xTimerChangePeriod(schedule->timer, (schedule->next_scheduled_time_diff * 1000) / portTICK_PERIOD_MS, portMAX_DELAY);
|
||||
}
|
||||
|
||||
static void esp_schedule_common_timer_cb(TimerHandle_t timer)
|
||||
{
|
||||
void *priv_data = pvTimerGetTimerID(timer);
|
||||
if (priv_data == NULL) {
|
||||
return;
|
||||
}
|
||||
esp_schedule_t *schedule = (esp_schedule_t *)priv_data;
|
||||
time_t now;
|
||||
time(&now);
|
||||
struct tm validity_time;
|
||||
char time_str[64] = {0};
|
||||
if (schedule->validity.start_time != 0) {
|
||||
if (now < schedule->validity.start_time) {
|
||||
memset(time_str, 0, sizeof(time_str));
|
||||
localtime_r(&schedule->validity.start_time, &validity_time);
|
||||
strftime(time_str, sizeof(time_str), "%c %z[%Z]", &validity_time);
|
||||
ESP_LOGW(TAG, "Schedule %s skipped. It will be active only after: %s. DST: %s.", schedule->name, time_str, validity_time.tm_isdst ? "Yes" : "No");
|
||||
/* TODO: Start the timer such that the next time it triggeres, it will be within the valid window.
|
||||
* Currently, it will just keep triggering and then get skipped if not in valid range.
|
||||
*/
|
||||
goto restart_schedule;
|
||||
}
|
||||
}
|
||||
if (schedule->validity.end_time != 0) {
|
||||
if (now > schedule->validity.end_time) {
|
||||
localtime_r(&schedule->validity.end_time, &validity_time);
|
||||
strftime(time_str, sizeof(time_str), "%c %z[%Z]", &validity_time);
|
||||
ESP_LOGW(TAG, "Schedule %s skipped. It can't be active after: %s. DST: %s.", schedule->name, time_str, validity_time.tm_isdst ? "Yes" : "No");
|
||||
/* Return from here will ensure that the timer does not start again for this schedule */
|
||||
return;
|
||||
}
|
||||
}
|
||||
ESP_LOGI(TAG, "Schedule %s triggered", schedule->name);
|
||||
if (schedule->trigger_cb) {
|
||||
schedule->trigger_cb((esp_schedule_handle_t)schedule, schedule->priv_data);
|
||||
}
|
||||
|
||||
restart_schedule:
|
||||
|
||||
if (esp_schedule_is_expired(&schedule->trigger)) {
|
||||
/* Not deleting the schedule here. Just not starting it again. */
|
||||
return;
|
||||
}
|
||||
esp_schedule_start_timer(schedule);
|
||||
}
|
||||
|
||||
static void esp_schedule_delete_timer(esp_schedule_t *schedule)
|
||||
{
|
||||
xTimerDelete(schedule->timer, portMAX_DELAY);
|
||||
}
|
||||
|
||||
static void esp_schedule_create_timer(esp_schedule_t *schedule)
|
||||
{
|
||||
if (esp_schedule_nvs_is_enabled()) {
|
||||
/* This is just used for calculating next_scheduled_time_utc for ESP_SCHEDULE_DAY_ONCE (in case of ESP_SCHEDULE_TYPE_DAYS_OF_WEEK) or for ESP_SCHEDULE_MONTH_ONCE (in case of ESP_SCHEDULE_TYPE_DATE), and only used when NVS is enabled. And if NVS is enabled, time will already be synced and the time will be correctly calculated. */
|
||||
schedule->next_scheduled_time_diff = esp_schedule_get_next_schedule_time_diff(schedule->name, &schedule->trigger);
|
||||
}
|
||||
|
||||
/* Temporarily setting the timer for 1 (anything greater than 0) tick. This will get changed when xTimerChangePeriod() is called. */
|
||||
schedule->timer = xTimerCreate("schedule", 1, pdFALSE, (void *)schedule, esp_schedule_common_timer_cb);
|
||||
}
|
||||
|
||||
esp_err_t esp_schedule_get(esp_schedule_handle_t handle, esp_schedule_config_t *schedule_config)
|
||||
{
|
||||
if (schedule_config == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
if (handle == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
esp_schedule_t *schedule = (esp_schedule_t *)handle;
|
||||
|
||||
strcpy(schedule_config->name, schedule->name);
|
||||
schedule_config->trigger.type = schedule->trigger.type;
|
||||
schedule_config->trigger.hours = schedule->trigger.hours;
|
||||
schedule_config->trigger.minutes = schedule->trigger.minutes;
|
||||
if (schedule->trigger.type == ESP_SCHEDULE_TYPE_DAYS_OF_WEEK) {
|
||||
schedule_config->trigger.day.repeat_days = schedule->trigger.day.repeat_days;
|
||||
} else if (schedule->trigger.type == ESP_SCHEDULE_TYPE_DATE) {
|
||||
schedule_config->trigger.date.day = schedule->trigger.date.day;
|
||||
schedule_config->trigger.date.repeat_months = schedule->trigger.date.repeat_months;
|
||||
schedule_config->trigger.date.year = schedule->trigger.date.year;
|
||||
schedule_config->trigger.date.repeat_every_year = schedule->trigger.date.repeat_every_year;
|
||||
}
|
||||
|
||||
schedule_config->trigger_cb = schedule->trigger_cb;
|
||||
schedule_config->timestamp_cb = schedule->timestamp_cb;
|
||||
schedule_config->priv_data = schedule->priv_data;
|
||||
schedule_config->validity = schedule->validity;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t esp_schedule_enable(esp_schedule_handle_t handle)
|
||||
{
|
||||
if (handle == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
esp_schedule_t *schedule = (esp_schedule_t *)handle;
|
||||
esp_schedule_start_timer(schedule);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t esp_schedule_disable(esp_schedule_handle_t handle)
|
||||
{
|
||||
if (handle == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
esp_schedule_t *schedule = (esp_schedule_t *)handle;
|
||||
esp_schedule_stop_timer(schedule);
|
||||
/* Disabling a schedule should also reset the next_scheduled_time.
|
||||
* It would be re-computed after enabling.
|
||||
*/
|
||||
schedule->trigger.next_scheduled_time_utc = 0;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t esp_schedule_set(esp_schedule_t *schedule, esp_schedule_config_t *schedule_config)
|
||||
{
|
||||
/* Setting everything apart from name. */
|
||||
schedule->trigger.type = schedule_config->trigger.type;
|
||||
if (schedule->trigger.type == ESP_SCHEDULE_TYPE_RELATIVE) {
|
||||
schedule->trigger.relative_seconds = schedule_config->trigger.relative_seconds;
|
||||
schedule->trigger.next_scheduled_time_utc = schedule_config->trigger.next_scheduled_time_utc;
|
||||
} else {
|
||||
schedule->trigger.hours = schedule_config->trigger.hours;
|
||||
schedule->trigger.minutes = schedule_config->trigger.minutes;
|
||||
|
||||
if (schedule->trigger.type == ESP_SCHEDULE_TYPE_DAYS_OF_WEEK) {
|
||||
schedule->trigger.day.repeat_days = schedule_config->trigger.day.repeat_days;
|
||||
} else if (schedule->trigger.type == ESP_SCHEDULE_TYPE_DATE) {
|
||||
schedule->trigger.date.day = schedule_config->trigger.date.day;
|
||||
schedule->trigger.date.repeat_months = schedule_config->trigger.date.repeat_months;
|
||||
schedule->trigger.date.year = schedule_config->trigger.date.year;
|
||||
schedule->trigger.date.repeat_every_year = schedule_config->trigger.date.repeat_every_year;
|
||||
}
|
||||
}
|
||||
|
||||
schedule->trigger_cb = schedule_config->trigger_cb;
|
||||
schedule->timestamp_cb = schedule_config->timestamp_cb;
|
||||
schedule->priv_data = schedule_config->priv_data;
|
||||
schedule->validity = schedule_config->validity;
|
||||
esp_schedule_nvs_add(schedule);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t esp_schedule_edit(esp_schedule_handle_t handle, esp_schedule_config_t *schedule_config)
|
||||
{
|
||||
if (handle == NULL || schedule_config == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
esp_schedule_t *schedule = (esp_schedule_t *)handle;
|
||||
if (strncmp(schedule->name, schedule_config->name, sizeof(schedule->name)) != 0) {
|
||||
ESP_LOGE(TAG, "Schedule name mismatch. Expected: %s, Passed: %s", schedule->name, schedule_config->name);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
/* Editing a schedule with relative time should also reset it. */
|
||||
if (schedule->trigger.type == ESP_SCHEDULE_TYPE_RELATIVE) {
|
||||
schedule->trigger.next_scheduled_time_utc = 0;
|
||||
}
|
||||
esp_schedule_set(schedule, schedule_config);
|
||||
ESP_LOGD(TAG, "Schedule %s edited", schedule->name);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t esp_schedule_delete(esp_schedule_handle_t handle)
|
||||
{
|
||||
if (handle == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
esp_schedule_t *schedule = (esp_schedule_t *)handle;
|
||||
ESP_LOGI(TAG, "Deleting schedule %s", schedule->name);
|
||||
if (schedule->timer) {
|
||||
esp_schedule_stop_timer(schedule);
|
||||
esp_schedule_delete_timer(schedule);
|
||||
}
|
||||
esp_schedule_nvs_remove(schedule);
|
||||
free(schedule);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_schedule_handle_t esp_schedule_create(esp_schedule_config_t *schedule_config)
|
||||
{
|
||||
if (schedule_config == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
if (strlen(schedule_config->name) <= 0) {
|
||||
ESP_LOGE(TAG, "Set schedule failed. Please enter a unique valid name for the schedule.");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (schedule_config->trigger.type == ESP_SCHEDULE_TYPE_INVALID) {
|
||||
ESP_LOGE(TAG, "Schedule type is invalid.");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
esp_schedule_t *schedule = (esp_schedule_t *)MEM_CALLOC_EXTRAM(1, sizeof(esp_schedule_t));
|
||||
if (schedule == NULL) {
|
||||
ESP_LOGE(TAG, "Could not allocate handle");
|
||||
return NULL;
|
||||
}
|
||||
strlcpy(schedule->name, schedule_config->name, sizeof(schedule->name));
|
||||
|
||||
esp_schedule_set(schedule, schedule_config);
|
||||
|
||||
esp_schedule_create_timer(schedule);
|
||||
ESP_LOGD(TAG, "Schedule %s created", schedule->name);
|
||||
return (esp_schedule_handle_t)schedule;
|
||||
}
|
||||
|
||||
esp_schedule_handle_t *esp_schedule_init(bool enable_nvs, char *nvs_partition, uint8_t *schedule_count)
|
||||
{
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0)
|
||||
if (!esp_sntp_enabled()) {
|
||||
ESP_LOGI(TAG, "Initializing SNTP");
|
||||
esp_sntp_setoperatingmode(SNTP_OPMODE_POLL);
|
||||
esp_sntp_setservername(0, "pool.ntp.org");
|
||||
esp_sntp_init();
|
||||
}
|
||||
#else
|
||||
if (!sntp_enabled()) {
|
||||
ESP_LOGI(TAG, "Initializing SNTP");
|
||||
sntp_setoperatingmode(SNTP_OPMODE_POLL);
|
||||
sntp_setservername(0, "pool.ntp.org");
|
||||
sntp_init();
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!enable_nvs) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Wait for time to be updated here */
|
||||
|
||||
|
||||
/* Below this is initialising schedules from NVS */
|
||||
esp_schedule_nvs_init(nvs_partition);
|
||||
|
||||
/* Get handle list from NVS */
|
||||
esp_schedule_handle_t *handle_list = NULL;
|
||||
*schedule_count = 0;
|
||||
handle_list = esp_schedule_nvs_get_all(schedule_count);
|
||||
if (handle_list == NULL) {
|
||||
ESP_LOGI(TAG, "No schedules found in NVS");
|
||||
return NULL;
|
||||
}
|
||||
ESP_LOGI(TAG, "Schedules found in NVS: %"PRIu8, *schedule_count);
|
||||
/* Start/Delete the schedules */
|
||||
esp_schedule_t *schedule = NULL;
|
||||
for (size_t handle_count = 0; handle_count < *schedule_count; handle_count++) {
|
||||
schedule = (esp_schedule_t *)handle_list[handle_count];
|
||||
schedule->trigger_cb = NULL;
|
||||
schedule->timer = NULL;
|
||||
/* Check for ONCE and expired schedules and delete them. */
|
||||
if (esp_schedule_is_expired(&schedule->trigger)) {
|
||||
/* This schedule has already expired. */
|
||||
ESP_LOGI(TAG, "Schedule %s does not repeat and has already expired. Deleting it.", schedule->name);
|
||||
esp_schedule_delete((esp_schedule_handle_t)schedule);
|
||||
/* Removing the schedule from the list */
|
||||
handle_list[handle_count] = handle_list[*schedule_count - 1];
|
||||
(*schedule_count)--;
|
||||
handle_count--;
|
||||
continue;
|
||||
}
|
||||
esp_schedule_create_timer(schedule);
|
||||
esp_schedule_start_timer(schedule);
|
||||
}
|
||||
init_done = true;
|
||||
return handle_list;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,2 +1 @@
|
||||
CONFIG_BSP_BOARD_ESP32_S3_BOX=y
|
||||
CONFIG_BSP_ESP32_S3_BOX_1=y
|
||||
@@ -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 @@
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
[](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, ¤t_time);
|
||||
|
||||
/* Get schedule time */
|
||||
localtime_r(&now, &schedule_time);
|
||||
schedule_time.tm_sec = 0;
|
||||
schedule_time.tm_min = trigger->minutes;
|
||||
schedule_time.tm_hour = trigger->hours;
|
||||
mktime(&schedule_time);
|
||||
|
||||
/* Adjust schedule day */
|
||||
if (trigger->type == ESP_SCHEDULE_TYPE_DAYS_OF_WEEK) {
|
||||
int no_of_days = 0;
|
||||
no_of_days = esp_schedule_get_no_of_days(trigger, ¤t_time, &schedule_time);
|
||||
schedule_time.tm_sec += no_of_days * SECONDS_IN_DAY;
|
||||
}
|
||||
if (trigger->type == ESP_SCHEDULE_TYPE_DATE) {
|
||||
schedule_time.tm_mday = trigger->date.day;
|
||||
schedule_time.tm_mon = esp_schedule_get_next_month(trigger, ¤t_time, &schedule_time) - 1;
|
||||
schedule_time.tm_year = esp_schedule_get_next_year(trigger, ¤t_time, &schedule_time) - 1900;
|
||||
if (schedule_time.tm_mon < 0) {
|
||||
ESP_LOGE(TAG, "Invalid month found: %d. Setting it to next month.", schedule_time.tm_mon);
|
||||
schedule_time.tm_mon = current_time.tm_mon + 1;
|
||||
}
|
||||
if (schedule_time.tm_mon >= 12) {
|
||||
schedule_time.tm_year += schedule_time.tm_mon / 12;
|
||||
schedule_time.tm_mon = schedule_time.tm_mon % 12;
|
||||
}
|
||||
}
|
||||
mktime(&schedule_time);
|
||||
|
||||
/* Adjust time according to DST */
|
||||
time_t dst_adjust = 0;
|
||||
if (!current_time.tm_isdst && schedule_time.tm_isdst) {
|
||||
dst_adjust = -3600;
|
||||
} else if (current_time.tm_isdst && !schedule_time.tm_isdst ) {
|
||||
dst_adjust = 3600;
|
||||
}
|
||||
ESP_LOGD(TAG, "DST adjust seconds: %lld", (long long) dst_adjust);
|
||||
schedule_time.tm_sec += dst_adjust;
|
||||
mktime(&schedule_time);
|
||||
|
||||
/* Print schedule time */
|
||||
memset(time_str, 0, sizeof(time_str));
|
||||
strftime(time_str, sizeof(time_str), "%c %z[%Z]", &schedule_time);
|
||||
ESP_LOGI(TAG, "Schedule %s will be active on: %s. DST: %s", schedule_name, time_str, schedule_time.tm_isdst ? "Yes" : "No");
|
||||
|
||||
/* Calculate difference */
|
||||
time_diff = difftime((mktime(&schedule_time)), mktime(¤t_time));
|
||||
|
||||
/* For one time schedules to check for expiry after a reboot. If NVS is enabled, this should be stored in NVS. */
|
||||
trigger->next_scheduled_time_utc = mktime(&schedule_time);
|
||||
|
||||
return time_diff;
|
||||
}
|
||||
|
||||
static bool esp_schedule_is_expired(esp_schedule_trigger_t *trigger)
|
||||
{
|
||||
time_t current_timestamp = 0;
|
||||
struct tm current_time = {0};
|
||||
time(¤t_timestamp);
|
||||
localtime_r(¤t_timestamp, ¤t_time);
|
||||
|
||||
if (trigger->type == ESP_SCHEDULE_TYPE_RELATIVE) {
|
||||
if (trigger->next_scheduled_time_utc > 0 && trigger->next_scheduled_time_utc <= current_timestamp) {
|
||||
/* Relative seconds based schedule has expired */
|
||||
return true;
|
||||
} else if (trigger->next_scheduled_time_utc == 0) {
|
||||
/* Schedule has been disabled , so it is as good as expired. */
|
||||
return true;
|
||||
}
|
||||
} else if (trigger->type == ESP_SCHEDULE_TYPE_DAYS_OF_WEEK) {
|
||||
if (trigger->day.repeat_days == ESP_SCHEDULE_DAY_ONCE) {
|
||||
if (trigger->next_scheduled_time_utc > 0 && trigger->next_scheduled_time_utc <= current_timestamp) {
|
||||
/* One time schedule has expired */
|
||||
return true;
|
||||
} else if (trigger->next_scheduled_time_utc == 0) {
|
||||
/* Schedule has been disabled , so it is as good as expired. */
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else if (trigger->type == ESP_SCHEDULE_TYPE_DATE) {
|
||||
if (trigger->date.repeat_months == 0) {
|
||||
if (trigger->next_scheduled_time_utc > 0 && trigger->next_scheduled_time_utc <= current_timestamp) {
|
||||
/* One time schedule has expired */
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (trigger->date.repeat_every_year == true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
struct tm schedule_time = {0};
|
||||
localtime_r(¤t_timestamp, &schedule_time);
|
||||
schedule_time.tm_sec = 0;
|
||||
schedule_time.tm_min = trigger->minutes;
|
||||
schedule_time.tm_hour = trigger->hours;
|
||||
schedule_time.tm_mday = trigger->date.day;
|
||||
/* For expiry, just check the last month of the repeat_months. */
|
||||
/* '-1' because struct tm has months starting from 0 and we have months starting from 1. */
|
||||
schedule_time.tm_mon = fls(trigger->date.repeat_months) - 1;
|
||||
/* '-1900' because struct tm has number of years after 1900 */
|
||||
schedule_time.tm_year = trigger->date.year - 1900;
|
||||
time_t schedule_timestamp = mktime(&schedule_time);
|
||||
|
||||
if (schedule_timestamp < current_timestamp) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
/* Invalid type. Mark as expired */
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void esp_schedule_stop_timer(esp_schedule_t *schedule)
|
||||
{
|
||||
xTimerStop(schedule->timer, portMAX_DELAY);
|
||||
}
|
||||
|
||||
static void esp_schedule_start_timer(esp_schedule_t *schedule)
|
||||
{
|
||||
time_t current_time = 0;
|
||||
time(¤t_time);
|
||||
if (current_time < SECONDS_TILL_2020) {
|
||||
ESP_LOGE(TAG, "Time is not updated");
|
||||
return;
|
||||
}
|
||||
|
||||
schedule->next_scheduled_time_diff = esp_schedule_get_next_schedule_time_diff(schedule->name, &schedule->trigger);
|
||||
ESP_LOGI(TAG, "Starting a timer for %"PRIu32" seconds for schedule %s", schedule->next_scheduled_time_diff, schedule->name);
|
||||
|
||||
if (schedule->timestamp_cb) {
|
||||
schedule->timestamp_cb((esp_schedule_handle_t)schedule, schedule->trigger.next_scheduled_time_utc, schedule->priv_data);
|
||||
}
|
||||
|
||||
xTimerStop(schedule->timer, portMAX_DELAY);
|
||||
xTimerChangePeriod(schedule->timer, (schedule->next_scheduled_time_diff * 1000) / portTICK_PERIOD_MS, portMAX_DELAY);
|
||||
}
|
||||
|
||||
static void esp_schedule_common_timer_cb(TimerHandle_t timer)
|
||||
{
|
||||
void *priv_data = pvTimerGetTimerID(timer);
|
||||
if (priv_data == NULL) {
|
||||
return;
|
||||
}
|
||||
esp_schedule_t *schedule = (esp_schedule_t *)priv_data;
|
||||
time_t now;
|
||||
time(&now);
|
||||
struct tm validity_time;
|
||||
char time_str[64] = {0};
|
||||
if (schedule->validity.start_time != 0) {
|
||||
if (now < schedule->validity.start_time) {
|
||||
memset(time_str, 0, sizeof(time_str));
|
||||
localtime_r(&schedule->validity.start_time, &validity_time);
|
||||
strftime(time_str, sizeof(time_str), "%c %z[%Z]", &validity_time);
|
||||
ESP_LOGW(TAG, "Schedule %s skipped. It will be active only after: %s. DST: %s.", schedule->name, time_str, validity_time.tm_isdst ? "Yes" : "No");
|
||||
/* TODO: Start the timer such that the next time it triggeres, it will be within the valid window.
|
||||
* Currently, it will just keep triggering and then get skipped if not in valid range.
|
||||
*/
|
||||
goto restart_schedule;
|
||||
}
|
||||
}
|
||||
if (schedule->validity.end_time != 0) {
|
||||
if (now > schedule->validity.end_time) {
|
||||
localtime_r(&schedule->validity.end_time, &validity_time);
|
||||
strftime(time_str, sizeof(time_str), "%c %z[%Z]", &validity_time);
|
||||
ESP_LOGW(TAG, "Schedule %s skipped. It can't be active after: %s. DST: %s.", schedule->name, time_str, validity_time.tm_isdst ? "Yes" : "No");
|
||||
/* Return from here will ensure that the timer does not start again for this schedule */
|
||||
return;
|
||||
}
|
||||
}
|
||||
ESP_LOGI(TAG, "Schedule %s triggered", schedule->name);
|
||||
if (schedule->trigger_cb) {
|
||||
schedule->trigger_cb((esp_schedule_handle_t)schedule, schedule->priv_data);
|
||||
}
|
||||
|
||||
restart_schedule:
|
||||
|
||||
if (esp_schedule_is_expired(&schedule->trigger)) {
|
||||
/* Not deleting the schedule here. Just not starting it again. */
|
||||
return;
|
||||
}
|
||||
esp_schedule_start_timer(schedule);
|
||||
}
|
||||
|
||||
static void esp_schedule_delete_timer(esp_schedule_t *schedule)
|
||||
{
|
||||
xTimerDelete(schedule->timer, portMAX_DELAY);
|
||||
}
|
||||
|
||||
static void esp_schedule_create_timer(esp_schedule_t *schedule)
|
||||
{
|
||||
if (esp_schedule_nvs_is_enabled()) {
|
||||
/* This is just used for calculating next_scheduled_time_utc for ESP_SCHEDULE_DAY_ONCE (in case of ESP_SCHEDULE_TYPE_DAYS_OF_WEEK) or for ESP_SCHEDULE_MONTH_ONCE (in case of ESP_SCHEDULE_TYPE_DATE), and only used when NVS is enabled. And if NVS is enabled, time will already be synced and the time will be correctly calculated. */
|
||||
schedule->next_scheduled_time_diff = esp_schedule_get_next_schedule_time_diff(schedule->name, &schedule->trigger);
|
||||
}
|
||||
|
||||
/* Temporarily setting the timer for 1 (anything greater than 0) tick. This will get changed when xTimerChangePeriod() is called. */
|
||||
schedule->timer = xTimerCreate("schedule", 1, pdFALSE, (void *)schedule, esp_schedule_common_timer_cb);
|
||||
}
|
||||
|
||||
esp_err_t esp_schedule_get(esp_schedule_handle_t handle, esp_schedule_config_t *schedule_config)
|
||||
{
|
||||
if (schedule_config == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
if (handle == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
esp_schedule_t *schedule = (esp_schedule_t *)handle;
|
||||
|
||||
strcpy(schedule_config->name, schedule->name);
|
||||
schedule_config->trigger.type = schedule->trigger.type;
|
||||
schedule_config->trigger.hours = schedule->trigger.hours;
|
||||
schedule_config->trigger.minutes = schedule->trigger.minutes;
|
||||
if (schedule->trigger.type == ESP_SCHEDULE_TYPE_DAYS_OF_WEEK) {
|
||||
schedule_config->trigger.day.repeat_days = schedule->trigger.day.repeat_days;
|
||||
} else if (schedule->trigger.type == ESP_SCHEDULE_TYPE_DATE) {
|
||||
schedule_config->trigger.date.day = schedule->trigger.date.day;
|
||||
schedule_config->trigger.date.repeat_months = schedule->trigger.date.repeat_months;
|
||||
schedule_config->trigger.date.year = schedule->trigger.date.year;
|
||||
schedule_config->trigger.date.repeat_every_year = schedule->trigger.date.repeat_every_year;
|
||||
}
|
||||
|
||||
schedule_config->trigger_cb = schedule->trigger_cb;
|
||||
schedule_config->timestamp_cb = schedule->timestamp_cb;
|
||||
schedule_config->priv_data = schedule->priv_data;
|
||||
schedule_config->validity = schedule->validity;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t esp_schedule_enable(esp_schedule_handle_t handle)
|
||||
{
|
||||
if (handle == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
esp_schedule_t *schedule = (esp_schedule_t *)handle;
|
||||
esp_schedule_start_timer(schedule);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t esp_schedule_disable(esp_schedule_handle_t handle)
|
||||
{
|
||||
if (handle == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
esp_schedule_t *schedule = (esp_schedule_t *)handle;
|
||||
esp_schedule_stop_timer(schedule);
|
||||
/* Disabling a schedule should also reset the next_scheduled_time.
|
||||
* It would be re-computed after enabling.
|
||||
*/
|
||||
schedule->trigger.next_scheduled_time_utc = 0;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t esp_schedule_set(esp_schedule_t *schedule, esp_schedule_config_t *schedule_config)
|
||||
{
|
||||
/* Setting everything apart from name. */
|
||||
schedule->trigger.type = schedule_config->trigger.type;
|
||||
if (schedule->trigger.type == ESP_SCHEDULE_TYPE_RELATIVE) {
|
||||
schedule->trigger.relative_seconds = schedule_config->trigger.relative_seconds;
|
||||
schedule->trigger.next_scheduled_time_utc = schedule_config->trigger.next_scheduled_time_utc;
|
||||
} else {
|
||||
schedule->trigger.hours = schedule_config->trigger.hours;
|
||||
schedule->trigger.minutes = schedule_config->trigger.minutes;
|
||||
|
||||
if (schedule->trigger.type == ESP_SCHEDULE_TYPE_DAYS_OF_WEEK) {
|
||||
schedule->trigger.day.repeat_days = schedule_config->trigger.day.repeat_days;
|
||||
} else if (schedule->trigger.type == ESP_SCHEDULE_TYPE_DATE) {
|
||||
schedule->trigger.date.day = schedule_config->trigger.date.day;
|
||||
schedule->trigger.date.repeat_months = schedule_config->trigger.date.repeat_months;
|
||||
schedule->trigger.date.year = schedule_config->trigger.date.year;
|
||||
schedule->trigger.date.repeat_every_year = schedule_config->trigger.date.repeat_every_year;
|
||||
}
|
||||
}
|
||||
|
||||
schedule->trigger_cb = schedule_config->trigger_cb;
|
||||
schedule->timestamp_cb = schedule_config->timestamp_cb;
|
||||
schedule->priv_data = schedule_config->priv_data;
|
||||
schedule->validity = schedule_config->validity;
|
||||
esp_schedule_nvs_add(schedule);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t esp_schedule_edit(esp_schedule_handle_t handle, esp_schedule_config_t *schedule_config)
|
||||
{
|
||||
if (handle == NULL || schedule_config == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
esp_schedule_t *schedule = (esp_schedule_t *)handle;
|
||||
if (strncmp(schedule->name, schedule_config->name, sizeof(schedule->name)) != 0) {
|
||||
ESP_LOGE(TAG, "Schedule name mismatch. Expected: %s, Passed: %s", schedule->name, schedule_config->name);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
/* Editing a schedule with relative time should also reset it. */
|
||||
if (schedule->trigger.type == ESP_SCHEDULE_TYPE_RELATIVE) {
|
||||
schedule->trigger.next_scheduled_time_utc = 0;
|
||||
}
|
||||
esp_schedule_set(schedule, schedule_config);
|
||||
ESP_LOGD(TAG, "Schedule %s edited", schedule->name);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t esp_schedule_delete(esp_schedule_handle_t handle)
|
||||
{
|
||||
if (handle == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
esp_schedule_t *schedule = (esp_schedule_t *)handle;
|
||||
ESP_LOGI(TAG, "Deleting schedule %s", schedule->name);
|
||||
if (schedule->timer) {
|
||||
esp_schedule_stop_timer(schedule);
|
||||
esp_schedule_delete_timer(schedule);
|
||||
}
|
||||
esp_schedule_nvs_remove(schedule);
|
||||
free(schedule);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_schedule_handle_t esp_schedule_create(esp_schedule_config_t *schedule_config)
|
||||
{
|
||||
if (schedule_config == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
if (strlen(schedule_config->name) <= 0) {
|
||||
ESP_LOGE(TAG, "Set schedule failed. Please enter a unique valid name for the schedule.");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (schedule_config->trigger.type == ESP_SCHEDULE_TYPE_INVALID) {
|
||||
ESP_LOGE(TAG, "Schedule type is invalid.");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
esp_schedule_t *schedule = (esp_schedule_t *)MEM_CALLOC_EXTRAM(1, sizeof(esp_schedule_t));
|
||||
if (schedule == NULL) {
|
||||
ESP_LOGE(TAG, "Could not allocate handle");
|
||||
return NULL;
|
||||
}
|
||||
strlcpy(schedule->name, schedule_config->name, sizeof(schedule->name));
|
||||
|
||||
esp_schedule_set(schedule, schedule_config);
|
||||
|
||||
esp_schedule_create_timer(schedule);
|
||||
ESP_LOGD(TAG, "Schedule %s created", schedule->name);
|
||||
return (esp_schedule_handle_t)schedule;
|
||||
}
|
||||
|
||||
esp_schedule_handle_t *esp_schedule_init(bool enable_nvs, char *nvs_partition, uint8_t *schedule_count)
|
||||
{
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0)
|
||||
if (!esp_sntp_enabled()) {
|
||||
ESP_LOGI(TAG, "Initializing SNTP");
|
||||
esp_sntp_setoperatingmode(SNTP_OPMODE_POLL);
|
||||
esp_sntp_setservername(0, "pool.ntp.org");
|
||||
esp_sntp_init();
|
||||
}
|
||||
#else
|
||||
if (!sntp_enabled()) {
|
||||
ESP_LOGI(TAG, "Initializing SNTP");
|
||||
sntp_setoperatingmode(SNTP_OPMODE_POLL);
|
||||
sntp_setservername(0, "pool.ntp.org");
|
||||
sntp_init();
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!enable_nvs) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Wait for time to be updated here */
|
||||
|
||||
|
||||
/* Below this is initialising schedules from NVS */
|
||||
esp_schedule_nvs_init(nvs_partition);
|
||||
|
||||
/* Get handle list from NVS */
|
||||
esp_schedule_handle_t *handle_list = NULL;
|
||||
*schedule_count = 0;
|
||||
handle_list = esp_schedule_nvs_get_all(schedule_count);
|
||||
if (handle_list == NULL) {
|
||||
ESP_LOGI(TAG, "No schedules found in NVS");
|
||||
return NULL;
|
||||
}
|
||||
ESP_LOGI(TAG, "Schedules found in NVS: %"PRIu8, *schedule_count);
|
||||
/* Start/Delete the schedules */
|
||||
esp_schedule_t *schedule = NULL;
|
||||
for (size_t handle_count = 0; handle_count < *schedule_count; handle_count++) {
|
||||
schedule = (esp_schedule_t *)handle_list[handle_count];
|
||||
schedule->trigger_cb = NULL;
|
||||
schedule->timer = NULL;
|
||||
/* Check for ONCE and expired schedules and delete them. */
|
||||
if (esp_schedule_is_expired(&schedule->trigger)) {
|
||||
/* This schedule has already expired. */
|
||||
ESP_LOGI(TAG, "Schedule %s does not repeat and has already expired. Deleting it.", schedule->name);
|
||||
esp_schedule_delete((esp_schedule_handle_t)schedule);
|
||||
/* Removing the schedule from the list */
|
||||
handle_list[handle_count] = handle_list[*schedule_count - 1];
|
||||
(*schedule_count)--;
|
||||
handle_count--;
|
||||
continue;
|
||||
}
|
||||
esp_schedule_create_timer(schedule);
|
||||
esp_schedule_start_timer(schedule);
|
||||
}
|
||||
init_done = true;
|
||||
return handle_list;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,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
|
||||
|
||||
Reference in New Issue
Block a user