From c1ba1358c4f09ab9e0b3021f625a6c34b1efbd62 Mon Sep 17 00:00:00 2001 From: yet-another-fuzzi <> Date: Sun, 29 Nov 2020 07:50:45 +0100 Subject: [PATCH] Version 1.0 --- firmware/.cproject | 32 + firmware/.project | 20 + firmware/CMakeLists.txt | 7 + firmware/Makefile | 9 + firmware/main/CMakeLists.txt | 10 + firmware/main/Kconfig.projbuild | 14 + firmware/main/component.mk | 20 + firmware/main/footer.html | 11 + firmware/main/ftcSoundBar.c | 1511 +++++++ firmware/main/header.html | 202 + firmware/main/home.html | 219 + firmware/main/img/cocktail.svg | 1 + firmware/main/img/cog.svg | 1 + firmware/main/img/favicon.ico | Bin 0 -> 3834 bytes firmware/main/img/favicon.svg | 144 + firmware/main/img/ftcsound.png | Bin 0 -> 48603 bytes firmware/main/img/ftcsoundbarlogo.svg | 149 + firmware/main/img/next.svg | 1 + firmware/main/img/play.svg | 1 + firmware/main/img/previous.svg | 1 + firmware/main/img/random.svg | 1 + firmware/main/img/redo.svg | 1 + firmware/main/img/stop.svg | 1 + firmware/main/img/volumedown.svg | 1 + firmware/main/img/volumeup.svg | 1 + firmware/main/styles.css | 138 + firmware/partitions.csv | 7 + firmware/sdkconfig | 637 +++ libftcSoundBar.so/libftcSoundBar.cpp | 910 ++++ robopro/ftcSoundBar.rpp | 5744 +++++++++++++++++++++++++ 30 files changed, 9794 insertions(+) create mode 100644 firmware/.cproject create mode 100644 firmware/.project create mode 100644 firmware/CMakeLists.txt create mode 100644 firmware/Makefile create mode 100644 firmware/main/CMakeLists.txt create mode 100644 firmware/main/Kconfig.projbuild create mode 100644 firmware/main/component.mk create mode 100644 firmware/main/footer.html create mode 100644 firmware/main/ftcSoundBar.c create mode 100644 firmware/main/header.html create mode 100644 firmware/main/home.html create mode 100644 firmware/main/img/cocktail.svg create mode 100644 firmware/main/img/cog.svg create mode 100644 firmware/main/img/favicon.ico create mode 100644 firmware/main/img/favicon.svg create mode 100644 firmware/main/img/ftcsound.png create mode 100644 firmware/main/img/ftcsoundbarlogo.svg create mode 100644 firmware/main/img/next.svg create mode 100644 firmware/main/img/play.svg create mode 100644 firmware/main/img/previous.svg create mode 100644 firmware/main/img/random.svg create mode 100644 firmware/main/img/redo.svg create mode 100644 firmware/main/img/stop.svg create mode 100644 firmware/main/img/volumedown.svg create mode 100644 firmware/main/img/volumeup.svg create mode 100644 firmware/main/styles.css create mode 100644 firmware/partitions.csv create mode 100644 firmware/sdkconfig create mode 100644 libftcSoundBar.so/libftcSoundBar.cpp create mode 100644 robopro/ftcSoundBar.rpp diff --git a/firmware/.cproject b/firmware/.cproject new file mode 100644 index 0000000..bc3a411 --- /dev/null +++ b/firmware/.cproject @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/firmware/.project b/firmware/.project new file mode 100644 index 0000000..68a4460 --- /dev/null +++ b/firmware/.project @@ -0,0 +1,20 @@ + + + ftMusicBox + + + + + + org.eclipse.cdt.core.cBuilder + clean,full,incremental, + + + + + + org.eclipse.cdt.core.cnature + org.eclipse.cdt.core.ccnature + com.espressif.idf.core.idfNature + + diff --git a/firmware/CMakeLists.txt b/firmware/CMakeLists.txt new file mode 100644 index 0000000..0483581 --- /dev/null +++ b/firmware/CMakeLists.txt @@ -0,0 +1,7 @@ +# The following lines of boilerplate have to be in your project's +# CMakeLists in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.5) + +include($ENV{ADF_PATH}/CMakeLists.txt) +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(ftcSoundBar) diff --git a/firmware/Makefile b/firmware/Makefile new file mode 100644 index 0000000..8cc2095 --- /dev/null +++ b/firmware/Makefile @@ -0,0 +1,9 @@ +# +# This is a project Makefile. It is assumed the directory this Makefile resides in is a +# project subdirectory. +# + +PROJECT_NAME := ftSoundBar + +include $(ADF_PATH)/project.mk + diff --git a/firmware/main/CMakeLists.txt b/firmware/main/CMakeLists.txt new file mode 100644 index 0000000..e37f37a --- /dev/null +++ b/firmware/main/CMakeLists.txt @@ -0,0 +1,10 @@ +# Edit following two lines to set component requirements (see docs) +set(COMPONENT_REQUIRES ) +set(COMPONENT_PRIV_REQUIRES ) + +set(COMPONENT_SRCS "ftcSoundBar.c") +set(COMPONENT_ADD_INCLUDEDIRS ".") + +set(COMPONENT_EMBED_FILES "img/cocktail.svg" "img/play.svg" "img/next.svg" "img/previous.svg" "img/stop.svg" "img/random.svg" "img/redo.svg" "img/volumeup.svg" "img/volumedown.svg" "img/cog.svg" "header.html" "footer.html" "img/favicon.ico" "styles.css" "img/ftcsoundbarlogo.svg" ) + +register_component() diff --git a/firmware/main/Kconfig.projbuild b/firmware/main/Kconfig.projbuild new file mode 100644 index 0000000..7e23439 --- /dev/null +++ b/firmware/main/Kconfig.projbuild @@ -0,0 +1,14 @@ +# put here your custom config value +menu "Example Configuration" +config ESP_WIFI_SSID + string "WiFi SSID" + default "myssid" + help + SSID (network name) for the example to connect to. + +config ESP_WIFI_PASSWORD + string "WiFi Password" + default "mypassword" + help + WiFi password (WPA or WPA2) for the example to use. +endmenu diff --git a/firmware/main/component.mk b/firmware/main/component.mk new file mode 100644 index 0000000..a419aca --- /dev/null +++ b/firmware/main/component.mk @@ -0,0 +1,20 @@ +# +# "main" pseudo-component makefile. +# +# (Uses default behaviour of compiling all source files in directory, adding 'include' to include path.) + +COMPONENT_EMBED_FILES := header.html +COMPONENT_EMBED_FILES += footer.html +COMPONENT_EMBED_FILES += ftcsoundbarlogo.svg +COMPONENT_EMBED_FILES += cocktail.svg +COMPONENT_EMBED_FILES += previous.svg +COMPONENT_EMBED_FILES += next.svg +COMPONENT_EMBED_FILES += play.svg +COMPONENT_EMBED_FILES += stop.svg +COMPONENT_EMBED_FILES += random.svg +COMPONENT_EMBED_FILES += redo.svg +COMPONENT_EMBED_FILES += volumneup.svg +COMPONENT_EMBED_FILES += volumnedown.svg +COMPONENT_EMBED_FILES += cog.svg +COMPONENT_EMBED_FILES += favicon.ico +COMPONENT_EMBED_FILES += styles.css \ No newline at end of file diff --git a/firmware/main/footer.html b/firmware/main/footer.html new file mode 100644 index 0000000..b341319 --- /dev/null +++ b/firmware/main/footer.html @@ -0,0 +1,11 @@ +
+ + +
+

+   (C) 2020 by Oliver Schmiel, Christian Bergschneider & Stefan Fuss   +

+
+
+ + \ No newline at end of file diff --git a/firmware/main/ftcSoundBar.c b/firmware/main/ftcSoundBar.c new file mode 100644 index 0000000..910e59f --- /dev/null +++ b/firmware/main/ftcSoundBar.c @@ -0,0 +1,1511 @@ + +/**************************************************************************************** + * + * ftcSoundBar - a LyraT based fischertechnik compatible music box. + * + * Version 0.1 + * + * (C) 2020 Oliver Schmied, Christian Bergschneider & Stefan Fuss + * + * Please run idf.py menuconfig, first + * + * EXAMPLE-CONFIGURATION: setting SSID & Password is helpful, but not mandatory + * COMPONENT-CONFIGURATION/FAT FILESYSTEM SUPPORT: long filename support, UTF 8 encoding + * + ****************************************************************************************/ + +#include "esp_wifi.h" +#include "esp_event_loop.h" +#include "nvs_flash.h" +#include "esp_http_client.h" +#include "esp_log.h" +#include "audio_pipeline.h" +#include "fatfs_stream.h" +#include "i2s_stream.h" +#include "mp3_decoder.h" +#include "filter_resample.h" +#include "esp_flash_partitions.h" +#include "esp_partition.h" +#include "esp_ota_ops.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/event_groups.h" +#include "input_key_service.h" +#include "sdcard_list.h" +#include "sdcard_scan.h" +#include +#include +#include +#include +#include +#include +#include "esp_vfs.h" +#include "esp_http_server.h" +#include "cJSON.h" +#include "esp_vfs_fat.h" +#include "sdmmc_cmd.h" +#include +#include "mdns.h" + +#define FIRMWARE_VERSION "v1.0" + +#define CONFIG_FILE "/sdcard/ftcSoundBar.conf" +static const char *TAG = "ftcSoundBar"; +#define FIRMWAREUPDATE "/sdcard/ftcSoundBar.bin" + +// some ftcSoundBar definitions +#define DEC_VOLUME -2 +#define INC_VOLUME -1 +#define MODE_SINGLE_TRACK 0 +#define MODE_SHUFFLE 1 +#define MODE_REPEAT 2 + +static EventGroupHandle_t wifi_event_group; +const int CONNECTED_BIT = BIT0; + +#define BUFFSIZE 1024 +static char ota_write_data[BUFFSIZE + 1] = { 0 }; + +#define BLINK_GPIO 22 + +#define FILE_PATH_MAX (ESP_VFS_PATH_MAX + 128) +#define SCRATCH_BUFSIZE (10240) + +typedef struct http_server_context { + char base_path[ESP_VFS_PATH_MAX + 1]; + char scratch[SCRATCH_BUFSIZE]; +} http_server_context_t; + +struct MusicBox { + + audio_board_handle_t board_handle; + audio_pipeline_handle_t pipeline; + audio_element_handle_t i2s_stream_writer, mp3_decoder, fatfs_stream_reader, rsp_handle; + playlist_operator_handle_t sdcard_list_handle; + uint8_t WIFI_SSID[32]; + uint8_t WIFI_PASSWORD[64]; + uint8_t TXT_AP_MODE; + char HOSTNAME[64]; + + uint16_t I2C_ADDRESS; + int active_track, mode; + TaskHandle_t xBlinky; + +} ftcSoundBar; + +void init_ftcSoundBar( void ) { + ftcSoundBar.board_handle = NULL; + ftcSoundBar.pipeline = NULL; + ftcSoundBar.i2s_stream_writer = NULL; + ftcSoundBar.mp3_decoder = NULL; + ftcSoundBar.fatfs_stream_reader = NULL; + ftcSoundBar.rsp_handle = NULL; + ftcSoundBar.active_track = 0; + ftcSoundBar.mode = MODE_SINGLE_TRACK; + ftcSoundBar.xBlinky = NULL; + + memcpy( ftcSoundBar.WIFI_SSID, CONFIG_ESP_WIFI_SSID, 32 ); + memcpy( ftcSoundBar.WIFI_PASSWORD, CONFIG_ESP_WIFI_PASSWORD, 64 ); + ftcSoundBar.TXT_AP_MODE = 0; + strcpy( ftcSoundBar.HOSTNAME, "ftcSoundBar" ); + ftcSoundBar.I2C_ADDRESS = 64; + +} + +/************************************************************************************** + * + * basic sd card functions + * + **************************************************************************************/ + +#define CONFTAG "CONFIG" + +void write_config_file( void) +{ + FILE* f; + + f = fopen(CONFIG_FILE, "w"); + + if (f == NULL) { + ESP_LOGE(CONFTAG, "Could not write %s.", CONFIG_FILE); + return; + } + + fprintf( f, "WIFI_SSID=%s\n", ftcSoundBar.WIFI_SSID); + fprintf( f, "WIFI_PASSWORD=%s\n", ftcSoundBar.WIFI_PASSWORD); + fprintf( f, "TXT_AP_MODE=%d\n", ftcSoundBar.TXT_AP_MODE); + fprintf( f, "I2C_ADDRESS=%d\n", ftcSoundBar.I2C_ADDRESS); + fprintf( f, "HOSTNAME=%s\n", ftcSoundBar.HOSTNAME); + + fclose(f); + +} + +void read_config_file( void ) +{ FILE* f; + + struct stat st; + if (stat(CONFIG_FILE, &st) == 0) { + // CONFIG_FILE exists, start reading it + f = fopen(CONFIG_FILE, "r"); + + if (f == NULL) { + ESP_LOGE(CONFTAG, "Could not read %s.", CONFIG_FILE); + return; + } + + char line[256]; + char key[256]; + char value[256]; + + while ( fscanf( f, "%s\n", line ) > 0 ) { + strcpy( key, strtok( line, "=" ) ); + strcpy( value, strtok( NULL, "=" ) ); + + if ( strcmp( key, "WIFI_SSID" ) == 0 ) { + + memcpy( ftcSoundBar.WIFI_SSID, value, 32 ); + + } else if ( strcmp( key, "WIFI_PASSWORD" ) == 0 ) { + + memcpy( ftcSoundBar.WIFI_PASSWORD, value, 64 ); + + } else if ( strcmp( key, "TXT_AP_MODE" ) == 0 ) { + + ftcSoundBar.TXT_AP_MODE = ( atoi( value ) != 0 ); + + } else if ( strcmp( key, "I2C_ADDRESS" ) == 0 ) { + + ftcSoundBar.I2C_ADDRESS = atoi( value ); + + } else if ( strcmp( key, "HOSTNAME" ) == 0 ) { + + strcpy( ftcSoundBar.HOSTNAME, value ); + + } else { + + ESP_LOGW(CONFTAG, "reading config file, ignoring pair (%s=%s)\n", key, value); + } + + } + + fclose(f); + + } else { + + // CONFIG_FILE is missing, create a sample file + ESP_LOGE(CONFTAG, "%s not found. Creating empty config file. Please set your parameters!", CONFIG_FILE); + + write_config_file(); + + } + +} + +/************************************************************************************** + * + * simple tasks + * + **************************************************************************************/ + + +void task_reboot(void *pvParameter) +{ + ESP_LOGI( TAG, "I will reboot in 0.5s..."); + vTaskDelay(500 / portTICK_RATE_MS); + esp_restart(); + +} + +void task_blinky(void *pvParameter) +{ + + gpio_pad_select_gpio(BLINK_GPIO); + /* Set the GPIO as a push/pull output */ + gpio_set_direction(BLINK_GPIO, GPIO_MODE_OUTPUT); + while( 1 ) { + /* Blink off (output low) */ + gpio_set_level(BLINK_GPIO, 0); + vTaskDelay(250 / portTICK_RATE_MS); + /* Blink on (output high) */ + gpio_set_level(BLINK_GPIO, 1); + vTaskDelay(250 / portTICK_RATE_MS); + } + +} + +#define OTATAG "OTA" + +static void __attribute__((noreturn)) task_fatal_error() +{ + ESP_LOGE(OTATAG, "Exiting task due to fatal error..."); + (void)vTaskDelete(NULL); + + while (1) { + ; + } +} + +void task_ota(void *pvParameter) +{ + esp_err_t err; + /* update handle : set by esp_ota_begin(), must be freed via esp_ota_end() */ + esp_ota_handle_t update_handle = 0 ; + const esp_partition_t *update_partition = NULL; + + ESP_LOGI(OTATAG, "Starting OTA..."); + + xTaskCreate(&task_blinky, "blinky", 512, NULL, 5, &(ftcSoundBar.xBlinky) ); + + const esp_partition_t *configured = esp_ota_get_boot_partition(); + const esp_partition_t *running = esp_ota_get_running_partition(); + + if (configured != running) { + ESP_LOGW(OTATAG, "Configured OTA boot partition at offset 0x%08x, but running from offset 0x%08x", + configured->address, running->address); + ESP_LOGW(OTATAG, "(This can happen if either the OTA boot data or preferred boot image become corrupted somehow.)"); + } + ESP_LOGI(OTATAG, "Running partition type %d subtype %d (offset 0x%08x)", + running->type, running->subtype, running->address); + + FILE *f; + f = fopen(FIRMWAREUPDATE, "rb"); + if ( f == NULL ) { + ESP_LOGI( TAG, "ftcSoundBar.bin not found"); + task_fatal_error(); + } + + update_partition = esp_ota_get_next_update_partition(NULL); + ESP_LOGI(OTATAG, "Writing to partition subtype %d at offset 0x%x", + update_partition->subtype, update_partition->address); + assert(update_partition != NULL); + + int binary_file_length = 0; + /*deal with all receive packet*/ + bool image_header_was_checked = false; + while (1) { + //int data_read = esp_http_client_read(client, ota_write_data, BUFFSIZE); + int data_read = fread( ota_write_data, 1, BUFFSIZE, f ); + ESP_LOGI( OTATAG, "data_read=%d", data_read); + if (data_read < 0) { + ESP_LOGE(OTATAG, "Error: file data read error"); + task_fatal_error(); + } else if (data_read > 0) { + if (image_header_was_checked == false) { + esp_app_desc_t new_app_info; + if (data_read > sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t) + sizeof(esp_app_desc_t)) { + // check current version with downloading + memcpy(&new_app_info, &ota_write_data[sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t)], sizeof(esp_app_desc_t)); + ESP_LOGI(OTATAG, "New firmware version: %s", new_app_info.version); + + esp_app_desc_t running_app_info; + if (esp_ota_get_partition_description(running, &running_app_info) == ESP_OK) { + ESP_LOGI(OTATAG, "Running firmware version: %s", running_app_info.version); + } + + const esp_partition_t* last_invalid_app = esp_ota_get_last_invalid_partition(); + esp_app_desc_t invalid_app_info; + if (esp_ota_get_partition_description(last_invalid_app, &invalid_app_info) == ESP_OK) { + ESP_LOGI(OTATAG, "Last invalid firmware version: %s", invalid_app_info.version); + } + + // check current version with last invalid partition + if (last_invalid_app != NULL) { + if (memcmp(invalid_app_info.version, new_app_info.version, sizeof(new_app_info.version)) == 0) { + ESP_LOGW(OTATAG, "New version is the same as invalid version."); + ESP_LOGW(OTATAG, "Previously, there was an attempt to launch the firmware with %s version, but it failed.", invalid_app_info.version); + ESP_LOGW(OTATAG, "The firmware has been rolled back to the previous version."); + task_fatal_error(); + } + } + + image_header_was_checked = true; + + err = esp_ota_begin(update_partition, OTA_SIZE_UNKNOWN, &update_handle); + if (err != ESP_OK) { + ESP_LOGE(OTATAG, "esp_ota_begin failed (%s)", esp_err_to_name(err)); + task_fatal_error(); + } + ESP_LOGI(OTATAG, "esp_ota_begin succeeded"); + } else { + ESP_LOGE(OTATAG, "received package is not fit len"); + task_fatal_error(); + } + } + err = esp_ota_write( update_handle, (const void *)ota_write_data, data_read); + if (err != ESP_OK) { + task_fatal_error(); + } + binary_file_length += data_read; + ESP_LOGD(OTATAG, "Written image length %d", binary_file_length); + } else if (data_read == 0) { + + ESP_LOGI(OTATAG, "EOF"); + break; + + } + } + ESP_LOGI(OTATAG, "Total Write binary data length: %d", binary_file_length); + + fclose(f); + + err = esp_ota_end(update_handle); + if (err != ESP_OK) { + if (err == ESP_ERR_OTA_VALIDATE_FAILED) { + ESP_LOGE(OTATAG, "Image validation failed, image is corrupted"); + } + ESP_LOGE(OTATAG, "esp_ota_end failed (%s)!", esp_err_to_name(err)); + task_fatal_error(); + } + + err = esp_ota_set_boot_partition(update_partition); + if (err != ESP_OK) { + ESP_LOGE(OTATAG, "esp_ota_set_boot_partition failed (%s)!", esp_err_to_name(err)); + task_fatal_error(); + } + ESP_LOGI(OTATAG, "Prepare to restart system!"); + + esp_restart(); + +} + +/************************************************************************************** + * + * basic API funktions + * + **************************************************************************************/ + +/** + * @brief change to next track in the playlist + * + * @param + * + * @return track's index number + */ +int next_track( ) { + + ftcSoundBar.active_track++; + + if ( ftcSoundBar.active_track >= sdcard_list_get_url_num( ftcSoundBar.sdcard_list_handle) ) { + ftcSoundBar.active_track = 0; + } + + return ftcSoundBar.active_track; +} + +/** + * @brief change to previous track in the playlist + * + * @param + * + * @return track's index number + */ +int prev_track( ) { + + ftcSoundBar.active_track--; + + if ( ftcSoundBar.active_track < 0 ) { + ftcSoundBar.active_track = sdcard_list_get_url_num( ftcSoundBar.sdcard_list_handle) -1 ; + } + + return ftcSoundBar.active_track; +} + +/** + * @brief stop running track + * + * @param + * + * @return + */ +void stop_track( void ) { + audio_pipeline_stop(ftcSoundBar.pipeline); + audio_pipeline_wait_for_stop(ftcSoundBar.pipeline); + audio_pipeline_terminate(ftcSoundBar.pipeline); +} + +/** + * @brief play actual track in the playlist. Stop - if needed - the last track before. + * + * @param + * + * @return + */ +void play_track( void ) { + char *url = NULL; + + // stop running track + if ( audio_element_get_state(ftcSoundBar.i2s_stream_writer) == AEL_STATE_RUNNING) { + stop_track(); + } + + // set next url + sdcard_list_choose( ftcSoundBar.sdcard_list_handle, ftcSoundBar.active_track, &url); + ESP_LOGI("PLAYTRACK", "URL: %s", url); + + // start track + audio_element_set_uri(ftcSoundBar.fatfs_stream_reader, url); + audio_pipeline_reset_ringbuffer(ftcSoundBar.pipeline); + audio_pipeline_reset_elements(ftcSoundBar.pipeline); + audio_pipeline_run(ftcSoundBar.pipeline); +} + +/** + * @brief change volumne + * + * @param vol DEC_VOLUME, INC_VOLUME or an absolute value between 0 and 100 + * + * @return + */ +void volume( int vol) { + + int player_volume ; + + // get actual volume + audio_hal_get_volume(ftcSoundBar.board_handle->audio_hal, &player_volume); + + // change the value + switch (vol) { + case DEC_VOLUME: player_volume -= 10; break; + case INC_VOLUME: player_volume += 10; break; + default: player_volume = vol; break; + } + + // check boundaries + if (player_volume > 100) { + player_volume = 100; + } else if (player_volume < 0) { + player_volume = 0; + } + + // set new value + audio_hal_set_volume(ftcSoundBar.board_handle->audio_hal, player_volume); + +} + +/****************************************************************** + * + * WebServer + * + ******************************************************************/ + +/** + * @brief send header of web site + * + * @param req + * + * @return error-code + */ +static esp_err_t send_header(httpd_req_t *req ) +{ + extern const unsigned char header_start[] asm("_binary_header_html_start"); + extern const unsigned char header_end[] asm("_binary_header_html_end"); + const size_t header_size = (header_end - header_start); + httpd_resp_set_type(req, "text/html"); + httpd_resp_send_chunk(req, (const char *)header_start, header_size); + return ESP_OK; +} + +/** + * @brief send footer of web site + * + * @param req + * + * @return error-code + */ +static esp_err_t send_footer(httpd_req_t *req ) +{ + extern const unsigned char footer_start[] asm("_binary_footer_html_start"); + extern const unsigned char footer_end[] asm("_binary_footer_html_end"); + const size_t footer_size = (footer_end - footer_start); + httpd_resp_set_type(req, "text/html"); + httpd_resp_send_chunk(req, (const char *)footer_start, footer_size); + return ESP_OK; +} + +/** + * @brief send favicon.ico + * + * @param req + * + * @return error-code + */ +static esp_err_t send_img(httpd_req_t *req ) +{ + extern const unsigned char ftcSound_start[] asm("_binary_favicon_ico_start"); + extern const unsigned char ftcSound_end[] asm("_binary_favicon_ico_end"); + const size_t ftcSound_size = (ftcSound_end - ftcSound_start); + + httpd_resp_set_type(req, "image/ico"); + httpd_resp_send_chunk(req, (const char *)ftcSound_start, ftcSound_size); + return ESP_OK; +} + +/** + * @brief deliver favicon.ico + * + * @param req + * + * @return error-code + */ +static esp_err_t favico_get_handler(httpd_req_t *req ) +{ + send_img(req); + httpd_resp_sendstr_chunk(req, NULL); + + return ESP_OK; +} + +static esp_err_t img_get_handler(httpd_req_t *req ) +{ + + if ( strcmp( req->uri, "/img/ftcsoundbarlogo.svg") == 0 ) { + + extern const unsigned char ftcSoundbar_start[] asm("_binary_ftcsoundbarlogo_svg_start"); + extern const unsigned char ftcSoundbar_end[] asm("_binary_ftcsoundbarlogo_svg_end"); + const size_t ftcSoundbar_size = (ftcSoundbar_end - ftcSoundbar_start); + httpd_resp_set_type(req, "image/svg+xml"); + httpd_resp_send_chunk(req, (const char *)ftcSoundbar_start, ftcSoundbar_size); + + } else if ( strcmp( req->uri, "/img/cocktail.svg") == 0 ) { + + extern const unsigned char cocktail_start[] asm("_binary_cocktail_svg_start"); + extern const unsigned char cocktail_end[] asm("_binary_cocktail_svg_end"); + const size_t cocktail_size = (cocktail_end - cocktail_start); + httpd_resp_set_type(req, "image/svg+xml"); + httpd_resp_send_chunk(req, (const char *)cocktail_start, cocktail_size); + + } else if ( strcmp( req->uri, "/img/next.svg") == 0 ) { + + extern const unsigned char next_start[] asm("_binary_next_svg_start"); + extern const unsigned char next_end[] asm("_binary_next_svg_end"); + const size_t next_size = (next_end - next_start); + httpd_resp_set_type(req, "image/svg+xml"); + httpd_resp_send_chunk(req, (const char *)next_start, next_size); + + } else if ( strcmp( req->uri, "/img/play.svg") == 0 ) { + + extern const unsigned char play_start[] asm("_binary_play_svg_start"); + extern const unsigned char play_end[] asm("_binary_play_svg_end"); + const size_t play_size = (play_end - play_start); + httpd_resp_set_type(req, "image/svg+xml"); + httpd_resp_send_chunk(req, (const char *)play_start, play_size); + + } else if ( strcmp( req->uri, "/img/previous.svg") == 0 ) { + + extern const unsigned char previous_start[] asm("_binary_previous_svg_start"); + extern const unsigned char previous_end[] asm("_binary_previous_svg_end"); + const size_t previous_size = (previous_end - previous_start); + httpd_resp_set_type(req, "image/svg+xml"); + httpd_resp_send_chunk(req, (const char *)previous_start, previous_size); + + } else if ( strcmp( req->uri, "/img/stop.svg") == 0 ) { + + extern const unsigned char stop_start[] asm("_binary_stop_svg_start"); + extern const unsigned char stop_end[] asm("_binary_stop_svg_end"); + const size_t stop_size = (stop_end - stop_start); + httpd_resp_set_type(req, "image/svg+xml"); + httpd_resp_send_chunk(req, (const char *)stop_start, stop_size); + + } else if ( strcmp( req->uri, "/img/volumeup.svg") == 0 ) { + + extern const unsigned char volumeup_start[] asm("_binary_volumeup_svg_start"); + extern const unsigned char volumeup_end[] asm("_binary_volumeup_svg_end"); + const size_t volumeup_size = (volumeup_end - volumeup_start); + httpd_resp_set_type(req, "image/svg+xml"); + httpd_resp_send_chunk(req, (const char *)volumeup_start, volumeup_size); + + } else if ( strcmp( req->uri, "/img/volumedown.svg") == 0 ) { + + extern const unsigned char volumedown_start[] asm("_binary_volumedown_svg_start"); + extern const unsigned char volumedown_end[] asm("_binary_volumedown_svg_end"); + const size_t volumedown_size = (volumedown_end - volumedown_start); + httpd_resp_set_type(req, "image/svg+xml"); + httpd_resp_send_chunk(req, (const char *)volumedown_start, volumedown_size); + + } else if ( strcmp( req->uri, "/img/random.svg") == 0 ) { + + extern const unsigned char random_start[] asm("_binary_random_svg_start"); + extern const unsigned char random_end[] asm("_binary_random_svg_end"); + const size_t random_size = (random_end - random_start); + httpd_resp_set_type(req, "image/svg+xml"); + httpd_resp_send_chunk(req, (const char *)random_start, random_size); + + } else if ( strcmp( req->uri, "/img/redo.svg") == 0 ) { + + extern const unsigned char redo_start[] asm("_binary_redo_svg_start"); + extern const unsigned char redo_end[] asm("_binary_redo_svg_end"); + const size_t redo_size = (redo_end - redo_start); + httpd_resp_set_type(req, "image/svg+xml"); + httpd_resp_send_chunk(req, (const char *)redo_start, redo_size); + + } else if ( strcmp( req->uri, "/img/cog.svg") == 0 ) { + + extern const unsigned char cog_start[] asm("_binary_cog_svg_start"); + extern const unsigned char cog_end[] asm("_binary_cog_svg_end"); + const size_t cog_size = (cog_end - cog_start); + httpd_resp_set_type(req, "image/svg+xml"); + httpd_resp_send_chunk(req, (const char *)cog_start, cog_size); + + } + + httpd_resp_sendstr_chunk(req, NULL); + + return ESP_OK; +} + +/** + * @brief deliver styles.css + * + * @param req + * + * @return error-code + */ +static esp_err_t styles_css_get_handler(httpd_req_t *req ) +{ + extern const unsigned char styles_start[] asm("_binary_styles_css_start"); + extern const unsigned char styles_end[] asm("_binary_styles_css_end"); + const size_t styles_size = (styles_end - styles_start); + + httpd_resp_set_type(req, "text/css"); + httpd_resp_send_chunk(req, (const char *)styles_start, styles_size); + + /* Send empty chunk to signal HTTP response completion */ + httpd_resp_sendstr_chunk(req, NULL); + + return ESP_OK; +} + +/** + * @brief deliver iframe active_track + * + * @param req + * + * @return error-code + */ + +static esp_err_t active_track_html_get_handler(httpd_req_t *req ) { + + char line[128]; + char *myurl; + + httpd_resp_sendstr_chunk(req, "" ); + httpd_resp_sendstr_chunk(req, "" ); + httpd_resp_sendstr_chunk(req, "" ); + httpd_resp_sendstr_chunk(req, "" ); + httpd_resp_sendstr_chunk(req, "" ); + httpd_resp_sendstr_chunk(req, "" ); + httpd_resp_sendstr_chunk(req, "" ); + httpd_resp_sendstr_chunk(req, "" ); + + // ACTIVE TRACK + if ( ESP_OK == sdcard_list_choose(ftcSoundBar.sdcard_list_handle, ftcSoundBar.active_track, &myurl) ) { + sprintf( line, "

%03d %s

", ftcSoundBar.active_track+1, &(myurl[14]) ); + httpd_resp_sendstr_chunk(req, line); + } else { + httpd_resp_sendstr_chunk(req, "

none

"); + } + + httpd_resp_sendstr_chunk(req, "" ); + httpd_resp_sendstr_chunk(req, "" ); + + // Send empty chunk to signal HTTP response completion + httpd_resp_sendstr_chunk(req, NULL); + + return ESP_OK; + +} + +/** + * @brief deliver html site setup + * + * @param req + * + * @return error-code + */ +static esp_err_t setup_html_get_handler(httpd_req_t *req ) +{ + char line[256]; + + send_header(req); + + httpd_resp_sendstr_chunk(req, "" ); + + sprintf(line, "", FIRMWARE_VERSION ); + httpd_resp_sendstr_chunk(req, line ); + + sprintf(line, "", ftcSoundBar.WIFI_SSID ); + httpd_resp_sendstr_chunk(req, line ); + + sprintf(line, "", ftcSoundBar.WIFI_PASSWORD ); + httpd_resp_sendstr_chunk(req, line ); + + char checked[20] = ""; + char display[30] = ""; + + if ( ftcSoundBar.TXT_AP_MODE) { + strcpy( checked, "checked"); + } else { + strcpy( display, "style=\"display:none\""); + } + + sprintf( line, "", checked); + httpd_resp_sendstr_chunk(req, line ); + + // sprintf( "
", display); + // httpd_resp_sendstr_chunk(req, line ); + + if ( access( FIRMWAREUPDATE, F_OK ) != -1 ) { + sprintf( line, "
", ftcSoundBar.HOSTNAME); + httpd_resp_sendstr_chunk(req, line ); + } else { + sprintf( "", ftcSoundBar.HOSTNAME); + httpd_resp_sendstr_chunk(req, line ); + } + + httpd_resp_sendstr_chunk(req, "
firmware version:%s
wifi SSID:
pre-shared key:
txt client mode:
ip address:192.168.8.100

" ); + + send_footer(req); + + /* Send empty chunk to signal HTTP response completion */ + httpd_resp_sendstr_chunk(req, NULL); + + return ESP_OK; +} + +/** + * @brief deliver main html site play + * + * @param req + * + * @return error-code + */ +static esp_err_t root_html_get_handler(httpd_req_t *req ) +{ + char line[128]; + char *myurl; + + send_header(req); + + // send_active_track(req); + httpd_resp_sendstr_chunk(req, "" ); + + httpd_resp_sendstr_chunk(req, "" ); + + /* BACKWARD */ httpd_resp_sendstr_chunk(req, ""); + + char stylePlay[20] = ""; + char styleStop[20] = ""; + + if ( audio_element_get_state(ftcSoundBar.i2s_stream_writer) != AEL_STATE_RUNNING) { + strcpy( styleStop, "style=\"display:none\"" ); + } else { + strcpy( stylePlay, "style=\"display:none\"" ); + } + + httpd_resp_sendstr_chunk(req, ""); + + /* FORWARD */ httpd_resp_sendstr_chunk(req, ""); + + // REPEAT + httpd_resp_sendstr_chunk(req, ""); + + // SHUFFLE + httpd_resp_sendstr_chunk(req, ""); + + /* V-Down */ httpd_resp_sendstr_chunk(req, ""); + + /* V-Up */ httpd_resp_sendstr_chunk(req, ""); + /* setup */ httpd_resp_sendstr_chunk(req, "" ); + httpd_resp_sendstr_chunk(req, "
"); + sprintf( line, "
", stylePlay ); + httpd_resp_sendstr_chunk(req, line ); + sprintf( line, "
", styleStop ); + httpd_resp_sendstr_chunk(req, line ); + httpd_resp_sendstr_chunk(req, "
" ); + + httpd_resp_sendstr_chunk(req, "
" ); + httpd_resp_sendstr_chunk(req, "

Playlist

"); + + httpd_resp_sendstr_chunk(req, "" ); + + for (int i=0; i < sdcard_list_get_url_num( ftcSoundBar.sdcard_list_handle ); i++) { + sdcard_list_choose(ftcSoundBar.sdcard_list_handle, i, &myurl); + + httpd_resp_sendstr_chunk(req, "" ); + + sprintf( line, "", i+1, i+1, &(myurl[14]) ); + httpd_resp_sendstr_chunk(req, line); + + httpd_resp_sendstr_chunk(req, "" ); + } + + httpd_resp_sendstr_chunk(req, "
%03d %s
" ); + + + send_footer(req); + + /* Send empty chunk to signal HTTP response completion */ + httpd_resp_sendstr_chunk(req, NULL); + + return ESP_OK; +} + +static esp_err_t track_get_handler(httpd_req_t *req) +{ char *value; + char tag[20]; + + httpd_resp_set_type(req, "application/json"); + + cJSON *root = cJSON_CreateObject(); + cJSON_AddNumberToObject(root, "tracks", sdcard_list_get_url_num( ftcSoundBar.sdcard_list_handle ) ); + cJSON_AddNumberToObject(root, "active_track", ftcSoundBar.active_track ); + + cJSON_AddNumberToObject(root, "state", audio_element_get_state(ftcSoundBar.i2s_stream_writer) ); + + for (int i=0; i < sdcard_list_get_url_num( ftcSoundBar.sdcard_list_handle ); i++) { + sdcard_list_choose(ftcSoundBar.sdcard_list_handle, i, &value); + sprintf( tag, "track#%d", i ); + cJSON_AddStringToObject(root, tag, &(value[14]) ); + } + + const char *sys_info = cJSON_Print(root); + httpd_resp_sendstr(req, sys_info); + free((void *)sys_info); + cJSON_Delete(root); + return ESP_OK; +} + +static esp_err_t volume_get_handler(httpd_req_t *req) +{ + httpd_resp_set_type(req, "application/json"); + cJSON *root = cJSON_CreateObject(); + int volume; + audio_hal_get_volume(ftcSoundBar.board_handle->audio_hal, &volume); + cJSON_AddNumberToObject(root, "volume", volume ); + const char *sys_info = cJSON_Print(root); + httpd_resp_sendstr(req, sys_info); + free((void *)sys_info); + cJSON_Delete(root); + return ESP_OK; +} + +static esp_err_t mode_get_handler(httpd_req_t *req) +{ + httpd_resp_set_type(req, "application/json"); + cJSON *root = cJSON_CreateObject(); + cJSON_AddNumberToObject(root, "mode", ftcSoundBar.mode ); + const char *sys_info = cJSON_Print(root); + httpd_resp_sendstr(req, sys_info); + free((void *)sys_info); + cJSON_Delete(root); + return ESP_OK; +} + +static char *getBody(httpd_req_t *req) +{ + int total_len = req->content_len; + int cur_len = 0; + char *buf = ((http_server_context_t *)(req->user_ctx))->scratch; + int received = 0; + if (total_len >= SCRATCH_BUFSIZE) { + /* Respond with 500 Internal Server Error */ + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "content too long"); + return NULL; + } + while (cur_len < total_len) { + received = httpd_req_recv(req, buf + cur_len, total_len); + if (received <= 0) { + /* Respond with 500 Internal Server Error */ + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to post control value"); + return NULL; + } + cur_len += received; + } + buf[total_len] = '\0'; + + return buf; +} + +static esp_err_t play_post_handler(httpd_req_t *req) { + + char *body = getBody(req); + if (body==NULL) { + return ESP_FAIL; + } + + cJSON *root = cJSON_Parse(body); + if ( root == NULL ) { return ESP_FAIL; } + + cJSON *JSONtrack = cJSON_GetObjectItem(root, "track"); + if ( JSONtrack != NULL ) { + int track = JSONtrack->valueint; + ftcSoundBar.active_track = track-1; + } + + play_track(); + cJSON_Delete(root); + httpd_resp_sendstr(req, "Post control value successfully"); + return ESP_OK; +} + +static esp_err_t config_post_handler(httpd_req_t *req) { + + ESP_LOGI( TAG, "config_post_handler"); + + char *body = getBody(req); + if (body==NULL) { + return ESP_FAIL; + } + + cJSON *root = cJSON_Parse(body); + if ( root == NULL ) { return ESP_FAIL; } + + char *wifi_ssid = cJSON_GetObjectItem(root, "wifi_ssid")->valuestring; + char *wifi_password = cJSON_GetObjectItem(root, "wifi_password")->valuestring; + char *txt_ap_mode = cJSON_GetObjectItem(root, "txt_ap_mode")->valuestring; + strcpy( (char *)ftcSoundBar.WIFI_SSID, wifi_ssid ); + strcpy( (char *)ftcSoundBar.WIFI_PASSWORD, wifi_password); + ftcSoundBar.TXT_AP_MODE = ( strcmp( txt_ap_mode, "true" ) == 0 ); + ESP_LOGI( TAG, "TXT_AP_MODE: %s",txt_ap_mode); + + write_config_file(); + + cJSON_Delete(root); + + xTaskCreate(&task_reboot, "reboot", 512, NULL, 5, NULL ); + + httpd_resp_sendstr(req, "Post control value successfully"); + return ESP_OK; +} + +static esp_err_t ota_post_handler(httpd_req_t *req) { + + char *body = getBody(req); + if (body==NULL) { + return ESP_FAIL; + } + + cJSON *root = cJSON_Parse(body); + if ( root == NULL ) { return ESP_FAIL; } + cJSON_Delete(root); + + xTaskCreate(&task_ota, "ota", 8192, NULL, 5, NULL ); + + httpd_resp_sendstr(req, "Post control value successfully"); + return ESP_OK; +} + + +static esp_err_t volume_post_handler(httpd_req_t *req) +{ + char *body = getBody(req); + if (body==NULL) { + return ESP_FAIL; + } + + cJSON *root = cJSON_Parse(body); + cJSON *JSONvolume = cJSON_GetObjectItem(root, "volume"); + cJSON *JSONrelvolume = cJSON_GetObjectItem(root, "relvolume"); + + if ( JSONvolume != NULL ) { + int vol = JSONvolume->valueint; + volume( vol ); + } else if ( JSONrelvolume != NULL ) { + int relvol = JSONrelvolume->valueint; + if ( relvol > 0 ) { + volume( INC_VOLUME ); + } else { + volume( DEC_VOLUME ); + } + + } + cJSON_Delete(root); + httpd_resp_sendstr(req, "Post control value successfully"); + return ESP_OK; +} + +static esp_err_t previous_post_handler(httpd_req_t *req) +{ + char *body = getBody(req); + if (body==NULL) { + return ESP_FAIL; + } + + prev_track(); + play_track(); + + httpd_resp_sendstr(req, "Post control value successfully"); + return ESP_OK; +} + +static esp_err_t next_post_handler(httpd_req_t *req) +{ + char *body = getBody(req); + if (body==NULL) { + return ESP_FAIL; + } + + next_track(); + play_track(); + + httpd_resp_sendstr(req, "Post control value successfully"); + return ESP_OK; +} + +static esp_err_t stop_post_handler(httpd_req_t *req) +{ + char *body = getBody(req); + if (body==NULL) { + return ESP_FAIL; + } + + stop_track(); + + httpd_resp_sendstr(req, "Post control value successfully"); + return ESP_OK; +} + +static esp_err_t mode_post_handler(httpd_req_t *req) +{ + char *body = getBody(req); + if (body==NULL) { + return ESP_FAIL; + } + + cJSON *root = cJSON_Parse(body); + int mode = cJSON_GetObjectItem(root, "mode")->valueint; + ftcSoundBar.mode = mode; + if ( audio_element_get_state(ftcSoundBar.i2s_stream_writer) != AEL_STATE_RUNNING) { + play_track(); + } + + cJSON_Delete(root); + httpd_resp_sendstr(req, "Post control value successfully"); + return ESP_OK; +} + +static esp_err_t pause_post_handler(httpd_req_t *req) +{ + char *body = getBody(req); + if (body==NULL) { + return ESP_FAIL; + } + + audio_pipeline_pause(ftcSoundBar.pipeline); + + httpd_resp_sendstr(req, "Post control value successfully"); + return ESP_OK; +} + +static esp_err_t resume_post_handler(httpd_req_t *req) +{ + char *body = getBody(req); + if (body==NULL) { + return ESP_FAIL; + } + + audio_pipeline_resume(ftcSoundBar.pipeline); + + httpd_resp_sendstr(req, "Post control value successfully"); + return ESP_OK; +} + +#define TAGWEB "WEBSERVER" + +/* Function to start the web server */ +esp_err_t start_web_server( const char *base_path ) +{ + + http_server_context_t *http_context = calloc(1, sizeof(http_server_context_t)); + strlcpy(http_context->base_path, base_path, sizeof(http_context->base_path)); + + httpd_handle_t server = NULL; + httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + config.uri_match_fn = httpd_uri_match_wildcard; + config.max_uri_handlers = 20; + config.stack_size = 20480; + + ESP_LOGI(TAGWEB, "Starting HTTP Server"); + if (httpd_start(&server, &config) != ESP_OK) { + ESP_LOGE(TAGWEB, "Failed to start web server!"); + return ESP_FAIL; + } + + // / + httpd_uri_t root_html = { .uri = "/", .method = HTTP_GET, .handler = root_html_get_handler }; + httpd_register_uri_handler(server, &root_html); + + // setup + httpd_uri_t setup_html = { .uri = "/setup", .method = HTTP_GET, .handler = setup_html_get_handler }; + httpd_register_uri_handler(server, &setup_html); + + // active_track + httpd_uri_t active_track_html = { .uri = "/active_track", .method = HTTP_GET, .handler = active_track_html_get_handler }; + httpd_register_uri_handler(server, &active_track_html); + + // styles.css + httpd_uri_t styles_css = { .uri = "/styles.css", .method = HTTP_GET, .handler = styles_css_get_handler }; + httpd_register_uri_handler(server, &styles_css); + + // favicon + httpd_uri_t favico = { .uri = "/favicon.ico", .method = HTTP_GET, .handler = favico_get_handler }; + httpd_register_uri_handler(server, &favico); + + // img/* + httpd_uri_t img = { .uri = "/img/*", .method = HTTP_GET, .handler = img_get_handler }; + httpd_register_uri_handler(server, &img); + + // API + httpd_uri_t track_get_uri = {.uri = "/api/v1/track", .method = HTTP_GET, .handler = track_get_handler }; + httpd_register_uri_handler(server, &track_get_uri); + + httpd_uri_t play_post_uri = { .uri = "/api/v1/track/play", .method = HTTP_POST, .handler = play_post_handler, .user_ctx = http_context }; + httpd_register_uri_handler(server, &play_post_uri); + + httpd_uri_t config_post_uri = { .uri = "/api/v1/config",.method = HTTP_POST, .handler = config_post_handler, .user_ctx = http_context }; + httpd_register_uri_handler(server, &config_post_uri); + + httpd_uri_t ota_post_uri = { .uri = "/api/v1/ota", .method = HTTP_POST, .handler = ota_post_handler, .user_ctx = http_context }; + httpd_register_uri_handler(server, &ota_post_uri); + + httpd_uri_t volume_post_uri = { .uri = "/api/v1/volume", .method = HTTP_POST, .handler = volume_post_handler, .user_ctx = http_context }; + httpd_register_uri_handler(server, &volume_post_uri); + + httpd_uri_t volume_get_uri = { .uri = "/api/v1/volume", .method = HTTP_GET, .handler = volume_get_handler, .user_ctx = http_context }; + httpd_register_uri_handler(server, &volume_get_uri); + + httpd_uri_t previous_post_uri = { .uri = "/api/v1/track/previous", .method = HTTP_POST, .handler = previous_post_handler, .user_ctx = http_context }; + httpd_register_uri_handler(server, &previous_post_uri); + + httpd_uri_t next_post_uri = { .uri = "/api/v1/track/next", .method = HTTP_POST, .handler = next_post_handler, .user_ctx = http_context }; + httpd_register_uri_handler(server, &next_post_uri); + + httpd_uri_t stop_post_uri = { .uri = "/api/v1/track/stop", .method = HTTP_POST, .handler = stop_post_handler, .user_ctx = http_context }; + httpd_register_uri_handler(server, &stop_post_uri); + + httpd_uri_t mode_post_uri = { .uri = "/api/v1/mode", .method = HTTP_POST, .handler = mode_post_handler, .user_ctx = http_context }; + httpd_register_uri_handler(server, &mode_post_uri); + + httpd_uri_t mode_get_uri = { .uri = "/api/v1/mode", .method = HTTP_GET, .handler = mode_get_handler, .user_ctx = http_context }; + httpd_register_uri_handler(server, &mode_get_uri); + + httpd_uri_t pause_post_uri = { .uri = "/api/v1/track/pause", .method = HTTP_POST, .handler = pause_post_handler, .user_ctx = http_context }; + httpd_register_uri_handler(server, &pause_post_uri); + + httpd_uri_t resume_post_uri = { .uri = "/api/v1/track/resume", .method = HTTP_POST, .handler = resume_post_handler, .user_ctx = http_context }; + httpd_register_uri_handler(server, &resume_post_uri); + + return ESP_OK; +} + +/****************************************************************** + * + * wifi + * + ******************************************************************/ + +#define TAGWIFI "WIFI" + +esp_err_t wifi_event_handler(void *ctx, system_event_t *event) +{ + ESP_LOGI(TAGWIFI, "wifi_event_handler"); + + switch(event->event_id) { + case SYSTEM_EVENT_STA_START: + ESP_LOGI(TAGWIFI, "SYSTEM_EVENT_STA_START"); + ESP_ERROR_CHECK( esp_wifi_connect() ); + ESP_ERROR_CHECK( tcpip_adapter_set_hostname(TCPIP_ADAPTER_IF_STA, ftcSoundBar.HOSTNAME) ); + + // set static IP? + if ( ftcSoundBar.TXT_AP_MODE) { + // DHCP off + ESP_ERROR_CHECK( tcpip_adapter_dhcpc_stop(TCPIP_ADAPTER_IF_STA)); + + // set 192.168.8.1 + tcpip_adapter_ip_info_t myip; + IP4_ADDR( &myip.gw, 192,168,8,1 ); + IP4_ADDR( &myip.ip, 192,168,8,100 ); + IP4_ADDR( &myip.netmask, 255,255,255,0); + + ESP_ERROR_CHECK( tcpip_adapter_set_ip_info(TCPIP_ADAPTER_IF_STA, &myip)); + } + + break; + case SYSTEM_EVENT_STA_GOT_IP: + ESP_LOGI(TAGWIFI, "SYSTEM_EVENT_STA_GOT_IP"); + ESP_LOGI(TAGWIFI, "Got IP: '%s'", + ip4addr_ntoa(&event->event_info.got_ip.ip_info.ip)); + xEventGroupSetBits(wifi_event_group, CONNECTED_BIT); + break; + case SYSTEM_EVENT_STA_DISCONNECTED: + ESP_LOGI(TAGWIFI, "SYSTEM_EVENT_STA_DISCONNECTED"); + ESP_ERROR_CHECK(esp_wifi_connect()); + break; + default: + break; + } + + return ESP_OK; +} + +/****************************************************************** + * + * keyboard + * + ******************************************************************/ + +#define TAGKEY "KEYBOARD" + +static esp_err_t input_key_service_cb(periph_service_handle_t handle, periph_service_event_t *evt, void *ctx) +{ + /* Handle touch pad events + to start, pause, resume, finish current song and adjust volume + */ + int player_volume; + audio_hal_get_volume(ftcSoundBar.board_handle->audio_hal, &player_volume); + + if (evt->type == INPUT_KEY_SERVICE_ACTION_CLICK_RELEASE) { + ESP_LOGI(TAGKEY, "[ * ] input key id is %d", (int)evt->data); + switch ((int)evt->data) { + case INPUT_KEY_USER_ID_PLAY: + ESP_LOGI(TAGKEY, "[ * ] [Play] input key event"); + audio_element_state_t el_state = audio_element_get_state(ftcSoundBar.i2s_stream_writer); + switch (el_state) { + case AEL_STATE_INIT : + ESP_LOGI(TAGKEY, "[ * ] Starting audio pipeline"); + audio_pipeline_run(ftcSoundBar.pipeline); + break; + case AEL_STATE_RUNNING : + ESP_LOGI(TAGKEY, "[ * ] Pausing audio pipeline"); + audio_pipeline_pause(ftcSoundBar.pipeline); + break; + case AEL_STATE_PAUSED : + ESP_LOGI(TAGKEY, "[ * ] Resuming audio pipeline"); + audio_pipeline_resume(ftcSoundBar.pipeline); + break; + default : + ESP_LOGI(TAGKEY, "[ * ] Not supported state %d", el_state); + } + break; + case INPUT_KEY_USER_ID_SET: + ESP_LOGI(TAGKEY, "[ * ] [Set] input key event"); + ESP_LOGI(TAGKEY, "[ * ] Stopped, advancing to the next song"); + next_track(); + play_track(); + break; + case INPUT_KEY_USER_ID_VOLUP: + ESP_LOGI(TAGKEY, "[ * ] [Vol+] input key event"); + volume( INC_VOLUME ); + break; + case INPUT_KEY_USER_ID_VOLDOWN: + ESP_LOGI(TAGKEY, "[ * ] [Vol-] input key event"); + volume( DEC_VOLUME ); + break; + } + } + + return ESP_OK; +} + +void sdcard_url_save_cb(void *user_data, char *url) +{ + playlist_operator_handle_t sdcard_handle = (playlist_operator_handle_t)user_data; + esp_err_t ret = sdcard_list_save(sdcard_handle, url); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Fail to save sdcard url to sdcard playlist"); + } +} + + +void app_main(void) +{ + esp_log_level_set("*", ESP_LOG_INFO); + + ftcSoundBar.sdcard_list_handle = NULL; + ftcSoundBar.active_track = 0; + + ESP_LOGI(TAG, "ftMusicBox startup" ); + ESP_LOGI(TAG, "(C) 2020 Oliver Schmiel, Christian Bergschneider, Stefan Fuss" ); + + init_ftcSoundBar(); + + xTaskCreate(&task_blinky, "blinky", 512, NULL, 5, &(ftcSoundBar.xBlinky) ); + + nvs_flash_init(); + + ESP_LOGI(TAG, "[1.0] Initialize peripherals management"); + esp_periph_config_t periph_cfg = DEFAULT_ESP_PERIPH_SET_CONFIG(); + esp_periph_set_handle_t set = esp_periph_set_init(&periph_cfg); + + ESP_LOGI(TAG, "[1.1] Initialize and start peripherals"); + audio_board_key_init(set); + audio_board_sdcard_init(set); + + ESP_LOGI(TAG, "[1.2] Set up a sdcard playlist and scan sdcard music save to it"); + sdcard_list_create(&ftcSoundBar.sdcard_list_handle); + sdcard_scan(sdcard_url_save_cb, "/sdcard", 0, (const char *[]) {"mp3"}, 1, ftcSoundBar.sdcard_list_handle); + + ESP_LOGI(TAG, "[2.0] Initialize wifi" ); + read_config_file(); + wifi_event_group = xEventGroupCreate(); + tcpip_adapter_init(); + ESP_ERROR_CHECK( esp_event_loop_init(wifi_event_handler, NULL) ); + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + ESP_ERROR_CHECK( esp_wifi_init(&cfg) ); + ESP_ERROR_CHECK( esp_wifi_set_storage(WIFI_STORAGE_RAM) ); + ESP_ERROR_CHECK( esp_wifi_set_mode( WIFI_MODE_STA ) ); + wifi_config_t sta_config = { + .sta = { + .bssid_set = false + } + }; + memcpy( sta_config.sta.ssid, ftcSoundBar.WIFI_SSID, 32); + memcpy( sta_config.sta.password, ftcSoundBar.WIFI_PASSWORD, 64); + ESP_ERROR_CHECK( esp_wifi_set_config(WIFI_IF_STA, &sta_config) ); + ESP_ERROR_CHECK( esp_wifi_start() ); + ESP_ERROR_CHECK( esp_wifi_connect() ); + + xEventGroupWaitBits(wifi_event_group, CONNECTED_BIT, + false, true, portMAX_DELAY); + ESP_LOGI(TAG, "Connect to Wifi ! Start to Connect to Server...."); + if ( ftcSoundBar.xBlinky != NULL ) { vTaskDelete( ftcSoundBar.xBlinky ); } + gpio_set_level(BLINK_GPIO, 1); + + mdns_init(); + mdns_hostname_set( ftcSoundBar.HOSTNAME ); + + ESP_LOGI(TAG, "[3.0] Start codec chip"); + ftcSoundBar.board_handle = audio_board_init(); + audio_hal_ctrl_codec(ftcSoundBar.board_handle->audio_hal, AUDIO_HAL_CODEC_MODE_DECODE, AUDIO_HAL_CTRL_START); + + ESP_LOGI(TAG, "[4.0] Create and start input key service"); + input_key_service_info_t input_key_info[] = INPUT_KEY_DEFAULT_INFO(); + input_key_service_cfg_t input_cfg = INPUT_KEY_SERVICE_DEFAULT_CONFIG(); + input_cfg.handle = set; + periph_service_handle_t input_ser = input_key_service_create(&input_cfg); + input_key_service_add_key(input_ser, input_key_info, INPUT_KEY_NUM); + periph_service_set_callback(input_ser, input_key_service_cb, (void *)ftcSoundBar.board_handle); + + ESP_LOGI(TAG, "[5.0] Create audio pipeline for playback"); + audio_pipeline_cfg_t pipeline_cfg = DEFAULT_AUDIO_PIPELINE_CONFIG(); + ftcSoundBar.pipeline = audio_pipeline_init(&pipeline_cfg); + mem_assert(pipeline); + + ESP_LOGI(TAG, "[5.1] Create i2s stream to write data to codec chip"); + i2s_stream_cfg_t i2s_cfg = I2S_STREAM_CFG_DEFAULT(); + i2s_cfg.i2s_config.sample_rate = 48000; + i2s_cfg.type = AUDIO_STREAM_WRITER; + ftcSoundBar.i2s_stream_writer = i2s_stream_init(&i2s_cfg); + + ESP_LOGI(TAG, "[5.2] Create mp3 decoder to decode mp3 file"); + mp3_decoder_cfg_t mp3_cfg = DEFAULT_MP3_DECODER_CONFIG(); + ftcSoundBar.mp3_decoder = mp3_decoder_init(&mp3_cfg); + + ESP_LOGI(TAG, "[5.3] Create resample filter"); + rsp_filter_cfg_t rsp_cfg = DEFAULT_RESAMPLE_FILTER_CONFIG(); + ftcSoundBar.rsp_handle = rsp_filter_init(&rsp_cfg); + + ESP_LOGI(TAG, "[5.4] Create fatfs stream to read data from sdcard"); + char *url = NULL; + sdcard_list_choose(ftcSoundBar.sdcard_list_handle, ftcSoundBar.active_track, &url); + fatfs_stream_cfg_t fatfs_cfg = FATFS_STREAM_CFG_DEFAULT(); + fatfs_cfg.type = AUDIO_STREAM_READER; + ftcSoundBar.fatfs_stream_reader = fatfs_stream_init(&fatfs_cfg); + audio_element_set_uri(ftcSoundBar.fatfs_stream_reader, url); + + ESP_LOGI(TAG, "[5.5] Register all elements to audio pipeline"); + audio_pipeline_register(ftcSoundBar.pipeline, ftcSoundBar.fatfs_stream_reader, "file"); + audio_pipeline_register(ftcSoundBar.pipeline, ftcSoundBar.mp3_decoder, "mp3"); + audio_pipeline_register(ftcSoundBar.pipeline, ftcSoundBar.rsp_handle, "filter"); + audio_pipeline_register(ftcSoundBar.pipeline, ftcSoundBar.i2s_stream_writer, "i2s"); + + ESP_LOGI(TAG, "[5.6] Link it together [sdcard]-->fatfs_stream-->mp3_decoder-->resample-->i2s_stream-->[codec_chip]"); + const char *link_tag[4] = {"file", "mp3", "filter", "i2s"}; + audio_pipeline_link(ftcSoundBar.pipeline, &link_tag[0], 4); + + ESP_LOGI(TAG, "[6.0] Set up event listener"); + audio_event_iface_cfg_t evt_cfg = AUDIO_EVENT_IFACE_DEFAULT_CFG(); + audio_event_iface_handle_t evt = audio_event_iface_init(&evt_cfg); + + ESP_LOGI(TAG, "[6.1] Listen for all pipeline events"); + audio_pipeline_set_listener(ftcSoundBar.pipeline, evt); + + ESP_LOGI(TAG, "[7.0] Press the keys to control music player:"); + ESP_LOGI(TAG, " [Play] to start, pause and resume, [Set] next song."); + ESP_LOGI(TAG, " [Vol-] or [Vol+] to adjust volume."); + + ESP_LOGI(TAG, "[8.0] Start Web Server"); + ESP_ERROR_CHECK( start_web_server( "localhost" ) ); + + ESP_LOGI(TAG, "[9.0] Everything started"); + + while (1) { + + /* Handle event interface messages from pipeline + to set music info and to advance to the next song + */ + audio_event_iface_msg_t msg; + esp_err_t ret = audio_event_iface_listen(evt, &msg, portMAX_DELAY); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "[ * ] Event interface error : %d", ret); + continue; + } + if (msg.source_type == AUDIO_ELEMENT_TYPE_ELEMENT) { + // Set music info for a new song to be played + if (msg.source == (void *) ftcSoundBar.mp3_decoder + && msg.cmd == AEL_MSG_CMD_REPORT_MUSIC_INFO) { + audio_element_info_t music_info = {0}; + audio_element_getinfo(ftcSoundBar.mp3_decoder, &music_info); + ESP_LOGI(TAG, "[ * ] Received music info from mp3 decoder, sample_rates=%d, bits=%d, ch=%d", + music_info.sample_rates, music_info.bits, music_info.channels); + audio_element_setinfo(ftcSoundBar.i2s_stream_writer, &music_info); + rsp_filter_set_src_info(ftcSoundBar.rsp_handle, music_info.sample_rates, music_info.channels); + continue; + } + // Advance to the next song when previous finishes + if (msg.source == (void *) ftcSoundBar.i2s_stream_writer + && msg.cmd == AEL_MSG_CMD_REPORT_STATUS) { + audio_element_state_t el_state = audio_element_get_state(ftcSoundBar.i2s_stream_writer); + if (el_state == AEL_STATE_FINISHED) { + ESP_LOGI(TAG, "[ * ] Finished, advancing to the next song"); + int new_track; + + switch (ftcSoundBar.mode) + { + case MODE_SINGLE_TRACK: + ESP_LOGI(TAG, "SINGLE_TRACK"); + break; + case MODE_SHUFFLE: + + new_track = rand() % sdcard_list_get_url_num( ftcSoundBar.sdcard_list_handle); + ftcSoundBar.active_track = new_track; + ESP_LOGI(TAG, "SHUFFLE next track=%d", new_track+1); + stop_track(); + play_track(); + break; + case MODE_REPEAT: + ESP_LOGI(TAG, "REPEAT"); + stop_track(); + play_track(); + break; + } + } + continue; + } + } + } + +} diff --git a/firmware/main/header.html b/firmware/main/header.html new file mode 100644 index 0000000..8173f1a --- /dev/null +++ b/firmware/main/header.html @@ -0,0 +1,202 @@ + + + + + + + + + + + + + + + + + + + + +ftcSoundBar + + + + + + + + + + +
+
diff --git a/firmware/main/home.html b/firmware/main/home.html new file mode 100644 index 0000000..80b1b7c --- /dev/null +++ b/firmware/main/home.html @@ -0,0 +1,219 @@ + + + + + + ftcSound + + + + + +
+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ftc:soundbar
+
+ + + + + + + + +
Playlist

001 - YetAnotherBrickInTheWall.mp3
001 - Afterlife.mp3
001 - Afterlife.mp3
001 - Afterlife.mp3
001 - Afterlife.mp3
+
+ + +
+

(C) 2020 by Oliver Schmiel, Christian Bergschneider & Stefan Fuss

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + \ No newline at end of file diff --git a/firmware/main/img/cocktail.svg b/firmware/main/img/cocktail.svg new file mode 100644 index 0000000..f99fb62 --- /dev/null +++ b/firmware/main/img/cocktail.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/firmware/main/img/cog.svg b/firmware/main/img/cog.svg new file mode 100644 index 0000000..548cb4f --- /dev/null +++ b/firmware/main/img/cog.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/firmware/main/img/favicon.ico b/firmware/main/img/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..26784403379e030b42186b46ffbb2ad83bea9ea9 GIT binary patch literal 3834 zcmV004&%004{+008|`004nN004b?008NW002DY000@xb3BE2000U( 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 literal 0 HcmV?d00001 diff --git a/firmware/main/img/favicon.svg b/firmware/main/img/favicon.svg new file mode 100644 index 0000000..dfd4a0d --- /dev/null +++ b/firmware/main/img/favicon.svg @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/firmware/main/img/ftcsound.png b/firmware/main/img/ftcsound.png new file mode 100644 index 0000000000000000000000000000000000000000..b8ac9490fca42e7631bb045fcf872030cdd1277e GIT binary patch literal 48603 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

+u!tZIA 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 Pf+NLDv literal 0 HcmV?d00001 diff --git a/firmware/main/img/ftcsoundbarlogo.svg b/firmware/main/img/ftcsoundbarlogo.svg new file mode 100644 index 0000000..67265a9 --- /dev/null +++ b/firmware/main/img/ftcsoundbarlogo.svg @@ -0,0 +1,149 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/firmware/main/img/next.svg b/firmware/main/img/next.svg new file mode 100644 index 0000000..1285160 --- /dev/null +++ b/firmware/main/img/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/firmware/main/img/play.svg b/firmware/main/img/play.svg new file mode 100644 index 0000000..ee24784 --- /dev/null +++ b/firmware/main/img/play.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/firmware/main/img/previous.svg b/firmware/main/img/previous.svg new file mode 100644 index 0000000..0fa48ca --- /dev/null +++ b/firmware/main/img/previous.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/firmware/main/img/random.svg b/firmware/main/img/random.svg new file mode 100644 index 0000000..92d38f1 --- /dev/null +++ b/firmware/main/img/random.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/firmware/main/img/redo.svg b/firmware/main/img/redo.svg new file mode 100644 index 0000000..b8e9455 --- /dev/null +++ b/firmware/main/img/redo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/firmware/main/img/stop.svg b/firmware/main/img/stop.svg new file mode 100644 index 0000000..2593b8a --- /dev/null +++ b/firmware/main/img/stop.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/firmware/main/img/volumedown.svg b/firmware/main/img/volumedown.svg new file mode 100644 index 0000000..8cba8b1 --- /dev/null +++ b/firmware/main/img/volumedown.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/firmware/main/img/volumeup.svg b/firmware/main/img/volumeup.svg new file mode 100644 index 0000000..17ac92a --- /dev/null +++ b/firmware/main/img/volumeup.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/firmware/main/styles.css b/firmware/main/styles.css new file mode 100644 index 0000000..b7e4f1c --- /dev/null +++ b/firmware/main/styles.css @@ -0,0 +1,138 @@ +body { + font-family: Trebuchet, sans-serif; + background-color: #000; + color: #989898; +} + +iframe { + border: none; + width: 100%; + overflow: hidden; + margin: 0; +} + +table { + margin-left: auto; + margin-right: auto; + width: 400; +} + +th, td { + text-align: center; + padding: 4px 4px; +} + +hr { + color: #989898; + width: 120px; + align: center; +} + +a { + color: #989898; + text-decoration: none; + height: 12px; + cursor: pointer; +} + +button { + border: none; + color: white; + padding: 15px 32px; + text-align: center; + text-decoration: none; + display: inline-block; + font-size: 16px; + margin: 4px 2px; + cursor: pointer; + background-color: #4CAF50; + transition-duration: 0.4s; +} + +input { + border: none; + color: white; + text-align: left; + text-decoration: none; + display: inline-block; + font-size: 16px; + margin: 4px 2px; + background-color: #4CAF50; +} + +.switch { + position: relative; + display: inline-block; + width: 48px; + height: 22px; +} + +.switch input { + opacity: 0; + width: 0; + height: 0; +} + +.slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + -webkit-transition: .4s; + transition: .4s; +} + +.slider:before { + position: absolute; + content: ""; + height: 16px; + width: 16px; + left: 4px; + bottom: 4px; + background-color: white; + -webkit-transition: .4s; + transition: .4s; +} + +input:checked + .slider { + background-color: #4CAF50; +} + +input:focus + .slider { + box-shadow: 0 0 1px #4CAF50; +} + +input:checked + .slider:before { + -webkit-transform: translateX(22px); + -ms-transform: translateX(22px); + transform: translateX(22px); +} + +/* Rounded sliders */ +.slider.round { + border-radius: 24px; +} + +.slider.round:before { + border-radius: 50%; +} + +.select:hover { + background-color: #989898; + color: black; +} + +.small { + font-size: 8px; + text-align: center; + color: #989898; +} + +.playlist { + width: 50px; + color: #989898; +} + diff --git a/firmware/partitions.csv b/firmware/partitions.csv new file mode 100644 index 0000000..a6cf270 --- /dev/null +++ b/firmware/partitions.csv @@ -0,0 +1,7 @@ +# ESP-IDF Partition Table +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x6000, +phy_init, data, phy, 0xf000, 0x1000, +otadata, data, ota, 0x10000, 0x2000, +ota_0, app, ota_0, 0x100000, 0x180000, +ota_1, app, ota_1, 0x280000, 0x180000, \ No newline at end of file diff --git a/firmware/sdkconfig b/firmware/sdkconfig new file mode 100644 index 0000000..a6b944a --- /dev/null +++ b/firmware/sdkconfig @@ -0,0 +1,637 @@ +# +# Automatically generated file. DO NOT EDIT. +# Espressif IoT Development Framework (ESP-IDF) Project Configuration +# +CONFIG_IDF_TARGET="esp32" +CONFIG_IDF_FIRMWARE_CHIP_ID=0x0000 + +# +# SDK tool configuration +# +CONFIG_TOOLPREFIX="xtensa-esp32-elf-" +CONFIG_MAKE_WARN_UNDEFINED_VARIABLES=y +CONFIG_APP_COMPILE_TIME_DATE=y +# CONFIG_APP_EXCLUDE_PROJECT_VER_VAR is not set +# CONFIG_APP_EXCLUDE_PROJECT_NAME_VAR is not set +# CONFIG_AUDIO_BOARD_CUSTOM is not set +CONFIG_ESP_LYRAT_V4_3_BOARD=y +# CONFIG_ESP_LYRAT_V4_2_BOARD is not set +# CONFIG_ESP_LYRATD_MSC_V2_1_BOARD is not set +# CONFIG_ESP_LYRATD_MSC_V2_2_BOARD is not set +# CONFIG_ESP_LYRAT_MINI_V1_1_BOARD is not set +# CONFIG_ESP32_KORVO_DU1906_BOARD is not set +# CONFIG_ESP32_S2_KALUGA_1_V1_2_BOARD is not set +# CONFIG_LOG_BOOTLOADER_LEVEL_NONE is not set +# CONFIG_LOG_BOOTLOADER_LEVEL_ERROR is not set +# CONFIG_LOG_BOOTLOADER_LEVEL_WARN is not set +CONFIG_LOG_BOOTLOADER_LEVEL_INFO=y +# CONFIG_LOG_BOOTLOADER_LEVEL_DEBUG is not set +# CONFIG_LOG_BOOTLOADER_LEVEL_VERBOSE is not set +CONFIG_LOG_BOOTLOADER_LEVEL=3 +# CONFIG_BOOTLOADER_VDDSDIO_BOOST_1_8V is not set +CONFIG_BOOTLOADER_VDDSDIO_BOOST_1_9V=y +# CONFIG_BOOTLOADER_FACTORY_RESET is not set +# CONFIG_BOOTLOADER_APP_TEST is not set +CONFIG_BOOTLOADER_WDT_ENABLE=y +# CONFIG_BOOTLOADER_WDT_DISABLE_IN_USER_CODE is not set +CONFIG_BOOTLOADER_WDT_TIME_MS=9000 +# CONFIG_APP_ROLLBACK_ENABLE is not set +# CONFIG_SECURE_SIGNED_APPS_NO_SECURE_BOOT is not set +# CONFIG_SECURE_BOOT_ENABLED is not set +# CONFIG_FLASH_ENCRYPTION_ENABLED is not set +# CONFIG_REC_ENG_ENABLE_VAD_ONLY is not set +# CONFIG_REC_ENG_ENABLE_VAD_WWE is not set +CONFIG_REC_ENG_ENABLE_VAD_WWE_AMR=y +# CONFIG_SR_MODEL_WN3_QUANT is not set +# CONFIG_SR_MODEL_WN4_QUANT is not set +CONFIG_SR_MODEL_WN5_QUANT=y +# CONFIG_SR_MODEL_WN6_QUANT is not set +CONFIG_SR_WN5_HILEXIN=y +# CONFIG_SR_WN5X2_HILEXIN is not set +# CONFIG_SR_WN5X3_HILEXIN is not set +# CONFIG_SR_WN5_NIHAOXIAOZHI is not set +# CONFIG_SR_WN5X2_NIHAOXIAOZHI is not set +# CONFIG_SR_WN5X3_NIHAOXIAOZHI is not set +# CONFIG_SR_WN5X3_HIJESON is not set +# CONFIG_SR_WN5X3_NIHAOXIAOXIN is not set +# CONFIG_SR_WN5_CUSTOMIZED_WORD is not set +CONFIG_SR_MN1_MODEL_QUANT=y +CONFIG_SR_MN1_CHINESE=y +# CONFIG_SR_MN1_ENGLISH is not set +CONFIG_SPEECH_COMMANDS_NUM=20 +CONFIG_CN_SPEECH_COMMAND_ID0="da kai kong tiao" +CONFIG_CN_SPEECH_COMMAND_ID1="guan bi kong tiao" +CONFIG_CN_SPEECH_COMMAND_ID2="zeng da feng su" +CONFIG_CN_SPEECH_COMMAND_ID3="jian xiao feng su" +CONFIG_CN_SPEECH_COMMAND_ID4="sheng gao yi du" +CONFIG_CN_SPEECH_COMMAND_ID5="jiang di yi du" +CONFIG_CN_SPEECH_COMMAND_ID6="zhi re mo shi" +CONFIG_CN_SPEECH_COMMAND_ID7="zhi leng mo shi" +CONFIG_CN_SPEECH_COMMAND_ID8="song feng mo shi" +CONFIG_CN_SPEECH_COMMAND_ID9="jie neng mo shi" +CONFIG_CN_SPEECH_COMMAND_ID10="guan bi jie neng mo shi" +CONFIG_CN_SPEECH_COMMAND_ID11="chu shi mo shi" +CONFIG_CN_SPEECH_COMMAND_ID12="guan bi chu shi mo shi" +CONFIG_CN_SPEECH_COMMAND_ID13="da kai lan ya" +CONFIG_CN_SPEECH_COMMAND_ID14="guan bi lan ya" +CONFIG_CN_SPEECH_COMMAND_ID15="bo fang ge qu" +CONFIG_CN_SPEECH_COMMAND_ID16="zan ting bo fang" +CONFIG_CN_SPEECH_COMMAND_ID17="ding shi yi xiao shi" +CONFIG_CN_SPEECH_COMMAND_ID18="da kai dian deng" +CONFIG_CN_SPEECH_COMMAND_ID19="guan bi dian deng" +CONFIG_CN_SPEECH_COMMAND_ID20="" +CONFIG_CN_SPEECH_COMMAND_ID21="" +CONFIG_CN_SPEECH_COMMAND_ID22="" +CONFIG_CN_SPEECH_COMMAND_ID23="" +CONFIG_CN_SPEECH_COMMAND_ID24="" +CONFIG_CN_SPEECH_COMMAND_ID25="" +CONFIG_CN_SPEECH_COMMAND_ID26="" +CONFIG_CN_SPEECH_COMMAND_ID27="" +CONFIG_CN_SPEECH_COMMAND_ID28="" +CONFIG_CN_SPEECH_COMMAND_ID29="" +CONFIG_CN_SPEECH_COMMAND_ID30="" +CONFIG_CN_SPEECH_COMMAND_ID31="" +CONFIG_CN_SPEECH_COMMAND_ID32="" +CONFIG_CN_SPEECH_COMMAND_ID33="" +CONFIG_CN_SPEECH_COMMAND_ID34="" +CONFIG_CN_SPEECH_COMMAND_ID35="" +CONFIG_CN_SPEECH_COMMAND_ID36="" +CONFIG_CN_SPEECH_COMMAND_ID37="" +CONFIG_CN_SPEECH_COMMAND_ID38="" +CONFIG_CN_SPEECH_COMMAND_ID39="" +CONFIG_CN_SPEECH_COMMAND_ID40="" +CONFIG_CN_SPEECH_COMMAND_ID41="" +CONFIG_CN_SPEECH_COMMAND_ID42="" +CONFIG_CN_SPEECH_COMMAND_ID43="" +CONFIG_CN_SPEECH_COMMAND_ID44="" +CONFIG_CN_SPEECH_COMMAND_ID45="" +CONFIG_CN_SPEECH_COMMAND_ID46="" +CONFIG_CN_SPEECH_COMMAND_ID47="" +CONFIG_CN_SPEECH_COMMAND_ID48="" +CONFIG_CN_SPEECH_COMMAND_ID49="" +CONFIG_CN_SPEECH_COMMAND_ID50="" +CONFIG_CN_SPEECH_COMMAND_ID51="" +CONFIG_CN_SPEECH_COMMAND_ID52="" +CONFIG_CN_SPEECH_COMMAND_ID53="" +CONFIG_CN_SPEECH_COMMAND_ID54="" +CONFIG_CN_SPEECH_COMMAND_ID55="" +CONFIG_CN_SPEECH_COMMAND_ID56="" +CONFIG_CN_SPEECH_COMMAND_ID57="" +CONFIG_CN_SPEECH_COMMAND_ID58="" +CONFIG_CN_SPEECH_COMMAND_ID59="" +CONFIG_CN_SPEECH_COMMAND_ID60="" +CONFIG_CN_SPEECH_COMMAND_ID61="" +CONFIG_CN_SPEECH_COMMAND_ID62="" +CONFIG_CN_SPEECH_COMMAND_ID63="" +CONFIG_CN_SPEECH_COMMAND_ID64="" +CONFIG_CN_SPEECH_COMMAND_ID65="" +CONFIG_CN_SPEECH_COMMAND_ID66="" +CONFIG_CN_SPEECH_COMMAND_ID67="" +CONFIG_CN_SPEECH_COMMAND_ID68="" +CONFIG_CN_SPEECH_COMMAND_ID69="" +CONFIG_CN_SPEECH_COMMAND_ID70="" +CONFIG_CN_SPEECH_COMMAND_ID71="" +CONFIG_CN_SPEECH_COMMAND_ID72="" +CONFIG_CN_SPEECH_COMMAND_ID73="" +CONFIG_CN_SPEECH_COMMAND_ID74="" +CONFIG_CN_SPEECH_COMMAND_ID75="" +CONFIG_CN_SPEECH_COMMAND_ID76="" +CONFIG_CN_SPEECH_COMMAND_ID77="" +CONFIG_CN_SPEECH_COMMAND_ID78="" +CONFIG_CN_SPEECH_COMMAND_ID79="" +CONFIG_CN_SPEECH_COMMAND_ID80="" +CONFIG_CN_SPEECH_COMMAND_ID81="" +CONFIG_CN_SPEECH_COMMAND_ID82="" +CONFIG_CN_SPEECH_COMMAND_ID83="" +CONFIG_CN_SPEECH_COMMAND_ID84="" +CONFIG_CN_SPEECH_COMMAND_ID85="" +CONFIG_CN_SPEECH_COMMAND_ID86="" +CONFIG_CN_SPEECH_COMMAND_ID87="" +CONFIG_CN_SPEECH_COMMAND_ID88="" +CONFIG_CN_SPEECH_COMMAND_ID89="" +CONFIG_CN_SPEECH_COMMAND_ID90="" +CONFIG_CN_SPEECH_COMMAND_ID91="" +CONFIG_CN_SPEECH_COMMAND_ID92="" +CONFIG_CN_SPEECH_COMMAND_ID93="" +CONFIG_CN_SPEECH_COMMAND_ID94="" +CONFIG_CN_SPEECH_COMMAND_ID95="" +CONFIG_CN_SPEECH_COMMAND_ID96="" +CONFIG_CN_SPEECH_COMMAND_ID97="" +CONFIG_CN_SPEECH_COMMAND_ID98="" +CONFIG_CN_SPEECH_COMMAND_ID99="" +CONFIG_ESPTOOLPY_BAUD_OTHER_VAL=115200 +# CONFIG_FLASHMODE_QIO is not set +# CONFIG_FLASHMODE_QOUT is not set +CONFIG_FLASHMODE_DIO=y +# CONFIG_FLASHMODE_DOUT is not set +CONFIG_ESPTOOLPY_FLASHMODE="dio" +# CONFIG_ESPTOOLPY_FLASHFREQ_80M is not set +CONFIG_ESPTOOLPY_FLASHFREQ_40M=y +# CONFIG_ESPTOOLPY_FLASHFREQ_26M is not set +# CONFIG_ESPTOOLPY_FLASHFREQ_20M is not set +CONFIG_ESPTOOLPY_FLASHFREQ="40m" +# CONFIG_ESPTOOLPY_FLASHSIZE_1MB is not set +# CONFIG_ESPTOOLPY_FLASHSIZE_2MB is not set +CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y +# CONFIG_ESPTOOLPY_FLASHSIZE_8MB is not set +# CONFIG_ESPTOOLPY_FLASHSIZE_16MB is not set +CONFIG_ESPTOOLPY_FLASHSIZE="4MB" +CONFIG_ESPTOOLPY_FLASHSIZE_DETECT=y +CONFIG_ESPTOOLPY_BEFORE_RESET=y +# CONFIG_ESPTOOLPY_BEFORE_NORESET is not set +CONFIG_ESPTOOLPY_BEFORE="default_reset" +CONFIG_ESPTOOLPY_AFTER_RESET=y +# CONFIG_ESPTOOLPY_AFTER_NORESET is not set +CONFIG_ESPTOOLPY_AFTER="hard_reset" +# CONFIG_MONITOR_BAUD_9600B is not set +# CONFIG_MONITOR_BAUD_57600B is not set +CONFIG_MONITOR_BAUD_115200B=y +# CONFIG_MONITOR_BAUD_230400B is not set +# CONFIG_MONITOR_BAUD_921600B is not set +# CONFIG_MONITOR_BAUD_2MB is not set +# CONFIG_MONITOR_BAUD_OTHER is not set +CONFIG_MONITOR_BAUD_OTHER_VAL=115200 +CONFIG_MONITOR_BAUD=115200 +CONFIG_ESP_WIFI_SSID="ft-txt_XXXX" +CONFIG_ESP_WIFI_PASSWORD="TXT_SECURITY_KEY" +# CONFIG_PARTITION_TABLE_SINGLE_APP is not set +# CONFIG_PARTITION_TABLE_TWO_OTA is not set +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" +CONFIG_PARTITION_TABLE_FILENAME="partitions.csv" +CONFIG_PARTITION_TABLE_OFFSET=0x8000 +CONFIG_PARTITION_TABLE_MD5=y +CONFIG_OPTIMIZATION_LEVEL_DEBUG=y +# CONFIG_OPTIMIZATION_LEVEL_RELEASE is not set +CONFIG_OPTIMIZATION_ASSERTIONS_ENABLED=y +# CONFIG_OPTIMIZATION_ASSERTIONS_SILENT is not set +# CONFIG_OPTIMIZATION_ASSERTIONS_DISABLED is not set +# CONFIG_CXX_EXCEPTIONS is not set +CONFIG_STACK_CHECK_NONE=y +# CONFIG_STACK_CHECK_NORM is not set +# CONFIG_STACK_CHECK_STRONG is not set +# CONFIG_STACK_CHECK_ALL is not set +# CONFIG_STACK_CHECK is not set +# CONFIG_WARN_WRITE_STRINGS is not set +# CONFIG_DISABLE_GCC8_WARNINGS is not set +# CONFIG_ESP32_APPTRACE_DEST_TRAX is not set +CONFIG_ESP32_APPTRACE_DEST_NONE=y +# CONFIG_ESP32_APPTRACE_ENABLE is not set +CONFIG_ESP32_APPTRACE_LOCK_ENABLE=y +# CONFIG_AWS_IOT_SDK is not set +# CONFIG_BT_ENABLED is not set +CONFIG_BTDM_CTRL_BR_EDR_SCO_DATA_PATH_EFF=0 +# CONFIG_BTDM_CTRL_AUTO_LATENCY_EFF is not set +CONFIG_BTDM_CONTROLLER_BLE_MAX_CONN_EFF=0 +CONFIG_BTDM_CONTROLLER_BR_EDR_MAX_ACL_CONN_EFF=0 +CONFIG_BTDM_CONTROLLER_BR_EDR_MAX_SYNC_CONN_EFF=0 +CONFIG_BTDM_CONTROLLER_PINNED_TO_CORE=0 +CONFIG_BT_RESERVE_DRAM=0 +# CONFIG_BLE_MESH is not set +# CONFIG_ADC_FORCE_XPD_FSM is not set +CONFIG_ADC2_DISABLE_DAC=y +# CONFIG_SPI_MASTER_IN_IRAM is not set +CONFIG_SPI_MASTER_ISR_IN_IRAM=y +# CONFIG_SPI_SLAVE_IN_IRAM is not set +CONFIG_SPI_SLAVE_ISR_IN_IRAM=y +# CONFIG_EFUSE_CUSTOM_TABLE is not set +# CONFIG_EFUSE_VIRTUAL is not set +# CONFIG_EFUSE_CODE_SCHEME_COMPAT_NONE is not set +CONFIG_EFUSE_CODE_SCHEME_COMPAT_3_4=y +# CONFIG_EFUSE_CODE_SCHEME_COMPAT_REPEAT is not set +CONFIG_EFUSE_MAX_BLK_LEN=192 +CONFIG_IDF_TARGET_ESP32=y +CONFIG_ESP32_REV_MIN_0=y +# CONFIG_ESP32_REV_MIN_1 is not set +# CONFIG_ESP32_REV_MIN_2 is not set +# CONFIG_ESP32_REV_MIN_3 is not set +CONFIG_ESP32_REV_MIN=0 +CONFIG_ESP32_DPORT_WORKAROUND=y +# CONFIG_ESP32_DEFAULT_CPU_FREQ_80 is not set +CONFIG_ESP32_DEFAULT_CPU_FREQ_160=y +# CONFIG_ESP32_DEFAULT_CPU_FREQ_240 is not set +CONFIG_ESP32_DEFAULT_CPU_FREQ_MHZ=160 +# CONFIG_SPIRAM_SUPPORT is not set +# CONFIG_MEMMAP_TRACEMEM is not set +# CONFIG_MEMMAP_TRACEMEM_TWOBANKS is not set +# CONFIG_ESP32_TRAX is not set +CONFIG_TRACEMEM_RESERVE_DRAM=0x0 +# CONFIG_TWO_UNIVERSAL_MAC_ADDRESS is not set +CONFIG_FOUR_UNIVERSAL_MAC_ADDRESS=y +CONFIG_NUMBER_OF_UNIVERSAL_MAC_ADDRESS=4 +CONFIG_SYSTEM_EVENT_QUEUE_SIZE=32 +CONFIG_SYSTEM_EVENT_TASK_STACK_SIZE=2304 +CONFIG_MAIN_TASK_STACK_SIZE=16384 +CONFIG_IPC_TASK_STACK_SIZE=1024 +CONFIG_TIMER_TASK_STACK_SIZE=3584 +CONFIG_NEWLIB_STDOUT_LINE_ENDING_CRLF=y +# CONFIG_NEWLIB_STDOUT_LINE_ENDING_LF is not set +# CONFIG_NEWLIB_STDOUT_LINE_ENDING_CR is not set +# CONFIG_NEWLIB_STDIN_LINE_ENDING_CRLF is not set +# CONFIG_NEWLIB_STDIN_LINE_ENDING_LF is not set +CONFIG_NEWLIB_STDIN_LINE_ENDING_CR=y +# CONFIG_NEWLIB_NANO_FORMAT is not set +CONFIG_CONSOLE_UART_DEFAULT=y +# CONFIG_CONSOLE_UART_CUSTOM is not set +# CONFIG_CONSOLE_UART_NONE is not set +CONFIG_CONSOLE_UART_NUM=0 +CONFIG_CONSOLE_UART_BAUDRATE=115200 +# CONFIG_ULP_COPROC_ENABLED is not set +CONFIG_ULP_COPROC_RESERVE_MEM=0 +# CONFIG_ESP32_PANIC_PRINT_HALT is not set +CONFIG_ESP32_PANIC_PRINT_REBOOT=y +# CONFIG_ESP32_PANIC_SILENT_REBOOT is not set +# CONFIG_ESP32_PANIC_GDBSTUB is not set +CONFIG_ESP32_DEBUG_OCDAWARE=y +CONFIG_ESP32_DEBUG_STUBS_ENABLE=y +CONFIG_INT_WDT=y +CONFIG_INT_WDT_TIMEOUT_MS=300 +CONFIG_INT_WDT_CHECK_CPU1=y +CONFIG_TASK_WDT=y +# CONFIG_TASK_WDT_PANIC is not set +CONFIG_TASK_WDT_TIMEOUT_S=5 +CONFIG_TASK_WDT_CHECK_IDLE_TASK_CPU0=y +CONFIG_TASK_WDT_CHECK_IDLE_TASK_CPU1=y +CONFIG_BROWNOUT_DET=y +CONFIG_BROWNOUT_DET_LVL_SEL_0=y +# CONFIG_BROWNOUT_DET_LVL_SEL_1 is not set +# CONFIG_BROWNOUT_DET_LVL_SEL_2 is not set +# CONFIG_BROWNOUT_DET_LVL_SEL_3 is not set +# CONFIG_BROWNOUT_DET_LVL_SEL_4 is not set +# CONFIG_BROWNOUT_DET_LVL_SEL_5 is not set +# CONFIG_BROWNOUT_DET_LVL_SEL_6 is not set +# CONFIG_BROWNOUT_DET_LVL_SEL_7 is not set +CONFIG_BROWNOUT_DET_LVL=0 +CONFIG_REDUCE_PHY_TX_POWER=y +CONFIG_ESP32_TIME_SYSCALL_USE_RTC_FRC1=y +# CONFIG_ESP32_TIME_SYSCALL_USE_RTC is not set +# CONFIG_ESP32_TIME_SYSCALL_USE_FRC1 is not set +# CONFIG_ESP32_TIME_SYSCALL_USE_NONE is not set +CONFIG_ESP32_RTC_CLOCK_SOURCE_INTERNAL_RC=y +# CONFIG_ESP32_RTC_CLOCK_SOURCE_EXTERNAL_CRYSTAL is not set +# CONFIG_ESP32_RTC_CLOCK_SOURCE_EXTERNAL_OSC is not set +# CONFIG_ESP32_RTC_CLOCK_SOURCE_INTERNAL_8MD256 is not set +CONFIG_ESP32_RTC_CLK_CAL_CYCLES=1024 +CONFIG_ESP32_DEEP_SLEEP_WAKEUP_DELAY=2000 +CONFIG_ESP32_XTAL_FREQ_40=y +# CONFIG_ESP32_XTAL_FREQ_26 is not set +# CONFIG_ESP32_XTAL_FREQ_AUTO is not set +CONFIG_ESP32_XTAL_FREQ=40 +# CONFIG_DISABLE_BASIC_ROM_CONSOLE is not set +# CONFIG_NO_BLOBS is not set +# CONFIG_ESP_TIMER_PROFILING is not set +# CONFIG_COMPATIBLE_PRE_V2_1_BOOTLOADERS is not set +CONFIG_ESP_ERR_TO_NAME_LOOKUP=y +CONFIG_ESP32_DPORT_DIS_INTERRUPT_LVL=5 +CONFIG_ESP32_WIFI_STATIC_RX_BUFFER_NUM=10 +CONFIG_ESP32_WIFI_DYNAMIC_RX_BUFFER_NUM=32 +# CONFIG_ESP32_WIFI_STATIC_TX_BUFFER is not set +CONFIG_ESP32_WIFI_DYNAMIC_TX_BUFFER=y +CONFIG_ESP32_WIFI_TX_BUFFER_TYPE=1 +CONFIG_ESP32_WIFI_DYNAMIC_TX_BUFFER_NUM=32 +# CONFIG_ESP32_WIFI_CSI_ENABLED is not set +CONFIG_ESP32_WIFI_AMPDU_TX_ENABLED=y +CONFIG_ESP32_WIFI_TX_BA_WIN=6 +CONFIG_ESP32_WIFI_AMPDU_RX_ENABLED=y +CONFIG_ESP32_WIFI_RX_BA_WIN=6 +CONFIG_ESP32_WIFI_NVS_ENABLED=y +CONFIG_ESP32_WIFI_TASK_PINNED_TO_CORE_0=y +# CONFIG_ESP32_WIFI_TASK_PINNED_TO_CORE_1 is not set +CONFIG_ESP32_WIFI_SOFTAP_BEACON_MAX_LEN=752 +CONFIG_ESP32_WIFI_MGMT_SBUF_NUM=32 +# CONFIG_ESP32_WIFI_DEBUG_LOG_ENABLE is not set +CONFIG_ESP32_WIFI_IRAM_OPT=y +CONFIG_ESP32_WIFI_RX_IRAM_OPT=y +CONFIG_ESP32_PHY_CALIBRATION_AND_DATA_STORAGE=y +# CONFIG_ESP32_PHY_INIT_DATA_IN_PARTITION is not set +CONFIG_ESP32_PHY_MAX_WIFI_TX_POWER=20 +CONFIG_ESP32_PHY_MAX_TX_POWER=20 +# CONFIG_PM_ENABLE is not set +CONFIG_ADC_CAL_EFUSE_TP_ENABLE=y +CONFIG_ADC_CAL_EFUSE_VREF_ENABLE=y +CONFIG_ADC_CAL_LUT_ENABLE=y +# CONFIG_EVENT_LOOP_PROFILING is not set +CONFIG_ESP_HTTP_CLIENT_ENABLE_HTTPS=y +# CONFIG_ESP_HTTP_CLIENT_ENABLE_BASIC_AUTH is not set +CONFIG_HTTPD_MAX_REQ_HDR_LEN=512 +CONFIG_HTTPD_MAX_URI_LEN=512 +CONFIG_HTTPD_ERR_RESP_NO_DELAY=y +CONFIG_HTTPD_PURGE_BUF_LEN=32 +# CONFIG_HTTPD_LOG_PURGE_DATA is not set +# CONFIG_OTA_ALLOW_HTTP is not set +# CONFIG_ESP32_ENABLE_COREDUMP_TO_FLASH is not set +# CONFIG_ESP32_ENABLE_COREDUMP_TO_UART is not set +CONFIG_ESP32_ENABLE_COREDUMP_TO_NONE=y +# CONFIG_ESP32_ENABLE_COREDUMP is not set +CONFIG_DMA_RX_BUF_NUM=10 +CONFIG_DMA_TX_BUF_NUM=10 +CONFIG_EMAC_L2_TO_L3_RX_BUF_MODE=y +CONFIG_EMAC_CHECK_LINK_PERIOD_MS=2000 +CONFIG_EMAC_TASK_PRIORITY=20 +CONFIG_EMAC_TASK_STACK_SIZE=3072 +# CONFIG_FATFS_CODEPAGE_DYNAMIC is not set +CONFIG_FATFS_CODEPAGE_437=y +# CONFIG_FATFS_CODEPAGE_720 is not set +# CONFIG_FATFS_CODEPAGE_737 is not set +# CONFIG_FATFS_CODEPAGE_771 is not set +# CONFIG_FATFS_CODEPAGE_775 is not set +# CONFIG_FATFS_CODEPAGE_850 is not set +# CONFIG_FATFS_CODEPAGE_852 is not set +# CONFIG_FATFS_CODEPAGE_855 is not set +# CONFIG_FATFS_CODEPAGE_857 is not set +# CONFIG_FATFS_CODEPAGE_860 is not set +# CONFIG_FATFS_CODEPAGE_861 is not set +# CONFIG_FATFS_CODEPAGE_862 is not set +# CONFIG_FATFS_CODEPAGE_863 is not set +# CONFIG_FATFS_CODEPAGE_864 is not set +# CONFIG_FATFS_CODEPAGE_865 is not set +# CONFIG_FATFS_CODEPAGE_866 is not set +# CONFIG_FATFS_CODEPAGE_869 is not set +# CONFIG_FATFS_CODEPAGE_932 is not set +# CONFIG_FATFS_CODEPAGE_936 is not set +# CONFIG_FATFS_CODEPAGE_949 is not set +# CONFIG_FATFS_CODEPAGE_950 is not set +CONFIG_FATFS_CODEPAGE=437 +# CONFIG_FATFS_LFN_NONE is not set +CONFIG_FATFS_LFN_HEAP=y +# CONFIG_FATFS_LFN_STACK is not set +CONFIG_FATFS_MAX_LFN=255 +# CONFIG_FATFS_API_ENCODING_ANSI_OEM is not set +# CONFIG_FATFS_API_ENCODING_UTF_16 is not set +CONFIG_FATFS_API_ENCODING_UTF_8=y +CONFIG_FATFS_FS_LOCK=0 +CONFIG_FATFS_TIMEOUT_MS=10000 +CONFIG_FATFS_PER_FILE_CACHE=y +CONFIG_MB_QUEUE_LENGTH=20 +CONFIG_MB_SERIAL_TASK_STACK_SIZE=2048 +CONFIG_MB_SERIAL_BUF_SIZE=256 +CONFIG_MB_SERIAL_TASK_PRIO=10 +# CONFIG_MB_CONTROLLER_SLAVE_ID_SUPPORT is not set +CONFIG_MB_CONTROLLER_NOTIFY_TIMEOUT=20 +CONFIG_MB_CONTROLLER_NOTIFY_QUEUE_SIZE=20 +CONFIG_MB_CONTROLLER_STACK_SIZE=4096 +CONFIG_MB_EVENT_QUEUE_TIMEOUT=20 +CONFIG_MB_TIMER_PORT_ENABLED=y +CONFIG_MB_TIMER_GROUP=0 +CONFIG_MB_TIMER_INDEX=0 +# CONFIG_FREERTOS_UNICORE is not set +CONFIG_FREERTOS_NO_AFFINITY=0x7FFFFFFF +CONFIG_FREERTOS_CORETIMER_0=y +# CONFIG_FREERTOS_CORETIMER_1 is not set +CONFIG_FREERTOS_HZ=100 +CONFIG_FREERTOS_ASSERT_ON_UNTESTED_FUNCTION=y +# CONFIG_FREERTOS_CHECK_STACKOVERFLOW_NONE is not set +# CONFIG_FREERTOS_CHECK_STACKOVERFLOW_PTRVAL is not set +CONFIG_FREERTOS_CHECK_STACKOVERFLOW_CANARY=y +# CONFIG_FREERTOS_WATCHPOINT_END_OF_STACK is not set +CONFIG_FREERTOS_INTERRUPT_BACKTRACE=y +CONFIG_FREERTOS_THREAD_LOCAL_STORAGE_POINTERS=1 +CONFIG_FREERTOS_ASSERT_FAIL_ABORT=y +# CONFIG_FREERTOS_ASSERT_FAIL_PRINT_CONTINUE is not set +# CONFIG_FREERTOS_ASSERT_DISABLE is not set +CONFIG_FREERTOS_IDLE_TASK_STACKSIZE=1536 +CONFIG_FREERTOS_ISR_STACKSIZE=1536 +# CONFIG_FREERTOS_LEGACY_HOOKS is not set +CONFIG_FREERTOS_MAX_TASK_NAME_LEN=16 +# CONFIG_SUPPORT_STATIC_ALLOCATION is not set +CONFIG_TIMER_TASK_PRIORITY=1 +CONFIG_TIMER_TASK_STACK_DEPTH=2048 +CONFIG_TIMER_QUEUE_LENGTH=10 +CONFIG_FREERTOS_QUEUE_REGISTRY_SIZE=0 +# CONFIG_FREERTOS_USE_TRACE_FACILITY is not set +# CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS is not set +# CONFIG_FREERTOS_DEBUG_INTERNALS is not set +CONFIG_FREERTOS_TASK_FUNCTION_WRAPPER=y +CONFIG_FREERTOS_CHECK_MUTEX_GIVEN_BY_OWNER=y +# CONFIG_FREERTOS_CHECK_PORT_CRITICAL_COMPLIANCE is not set +CONFIG_HEAP_POISONING_DISABLED=y +# CONFIG_HEAP_POISONING_LIGHT is not set +# CONFIG_HEAP_POISONING_COMPREHENSIVE is not set +# CONFIG_HEAP_TRACING is not set +CONFIG_LIBSODIUM_USE_MBEDTLS_SHA=y +# CONFIG_LOG_DEFAULT_LEVEL_NONE is not set +# CONFIG_LOG_DEFAULT_LEVEL_ERROR is not set +# CONFIG_LOG_DEFAULT_LEVEL_WARN is not set +CONFIG_LOG_DEFAULT_LEVEL_INFO=y +# CONFIG_LOG_DEFAULT_LEVEL_DEBUG is not set +# CONFIG_LOG_DEFAULT_LEVEL_VERBOSE is not set +CONFIG_LOG_DEFAULT_LEVEL=3 +CONFIG_LOG_COLORS=y +# CONFIG_L2_TO_L3_COPY is not set +# CONFIG_ETHARP_SUPPORT_VLAN is not set +# CONFIG_LWIP_IRAM_OPTIMIZATION is not set +CONFIG_LWIP_MAX_SOCKETS=10 +CONFIG_LWIP_RANDOMIZE_INITIAL_LOCAL_PORTS=y +# CONFIG_USE_ONLY_LWIP_SELECT is not set +CONFIG_LWIP_SO_REUSE=y +CONFIG_LWIP_SO_REUSE_RXTOALL=y +# CONFIG_LWIP_SO_RCVBUF is not set +CONFIG_LWIP_IP_FRAG=y +# CONFIG_LWIP_IP_REASSEMBLY is not set +# CONFIG_LWIP_STATS is not set +# CONFIG_LWIP_ETHARP_TRUST_IP_MAC is not set +CONFIG_ESP_GRATUITOUS_ARP=y +CONFIG_GARP_TMR_INTERVAL=60 +CONFIG_TCPIP_RECVMBOX_SIZE=32 +CONFIG_LWIP_DHCP_DOES_ARP_CHECK=y +# CONFIG_LWIP_DHCP_RESTORE_LAST_IP is not set +CONFIG_LWIP_DHCPS_LEASE_UNIT=60 +CONFIG_LWIP_DHCPS_MAX_STATION_NUM=8 +# CONFIG_LWIP_AUTOIP is not set +# CONFIG_LWIP_IPV6_AUTOCONFIG is not set +CONFIG_LWIP_NETIF_LOOPBACK=y +CONFIG_LWIP_LOOPBACK_MAX_PBUFS=8 +CONFIG_LWIP_MAX_ACTIVE_TCP=16 +CONFIG_LWIP_MAX_LISTENING_TCP=16 +CONFIG_TCP_MAXRTX=12 +CONFIG_TCP_SYNMAXRTX=6 +CONFIG_TCP_MSS=1436 +CONFIG_TCP_MSL=60000 +CONFIG_TCP_SND_BUF_DEFAULT=5744 +CONFIG_TCP_WND_DEFAULT=5744 +CONFIG_TCP_RECVMBOX_SIZE=6 +CONFIG_TCP_QUEUE_OOSEQ=y +# CONFIG_ESP_TCP_KEEP_CONNECTION_WHEN_IP_CHANGES is not set +CONFIG_TCP_OVERSIZE_MSS=y +# CONFIG_TCP_OVERSIZE_QUARTER_MSS is not set +# CONFIG_TCP_OVERSIZE_DISABLE is not set +CONFIG_LWIP_MAX_UDP_PCBS=16 +CONFIG_UDP_RECVMBOX_SIZE=6 +CONFIG_TCPIP_TASK_STACK_SIZE=3072 +CONFIG_TCPIP_TASK_AFFINITY_NO_AFFINITY=y +# CONFIG_TCPIP_TASK_AFFINITY_CPU0 is not set +# CONFIG_TCPIP_TASK_AFFINITY_CPU1 is not set +CONFIG_TCPIP_TASK_AFFINITY=0x7FFFFFFF +# CONFIG_PPP_SUPPORT is not set +# CONFIG_LWIP_MULTICAST_PING is not set +# CONFIG_LWIP_BROADCAST_PING is not set +CONFIG_LWIP_MAX_RAW_PCBS=16 +CONFIG_LWIP_DHCP_MAX_NTP_SERVERS=1 +CONFIG_LWIP_SNTP_UPDATE_DELAY=3600000 +CONFIG_MBEDTLS_INTERNAL_MEM_ALLOC=y +# CONFIG_MBEDTLS_DEFAULT_MEM_ALLOC is not set +# CONFIG_MBEDTLS_CUSTOM_MEM_ALLOC is not set +CONFIG_MBEDTLS_SSL_MAX_CONTENT_LEN=16384 +# CONFIG_MBEDTLS_ASYMMETRIC_CONTENT_LEN is not set +# CONFIG_MBEDTLS_DEBUG is not set +# CONFIG_MBEDTLS_ECP_RESTARTABLE is not set +# CONFIG_MBEDTLS_CMAC_C is not set +CONFIG_MBEDTLS_HARDWARE_AES=y +# CONFIG_MBEDTLS_HARDWARE_MPI is not set +# CONFIG_MBEDTLS_HARDWARE_SHA is not set +CONFIG_MBEDTLS_HAVE_TIME=y +# CONFIG_MBEDTLS_HAVE_TIME_DATE is not set +CONFIG_MBEDTLS_TLS_SERVER_AND_CLIENT=y +# CONFIG_MBEDTLS_TLS_SERVER_ONLY is not set +# CONFIG_MBEDTLS_TLS_CLIENT_ONLY is not set +# CONFIG_MBEDTLS_TLS_DISABLED is not set +CONFIG_MBEDTLS_TLS_SERVER=y +CONFIG_MBEDTLS_TLS_CLIENT=y +CONFIG_MBEDTLS_TLS_ENABLED=y +# CONFIG_MBEDTLS_PSK_MODES is not set +CONFIG_MBEDTLS_KEY_EXCHANGE_RSA=y +CONFIG_MBEDTLS_KEY_EXCHANGE_DHE_RSA=y +CONFIG_MBEDTLS_KEY_EXCHANGE_ELLIPTIC_CURVE=y +CONFIG_MBEDTLS_KEY_EXCHANGE_ECDHE_RSA=y +CONFIG_MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA=y +CONFIG_MBEDTLS_KEY_EXCHANGE_ECDH_ECDSA=y +CONFIG_MBEDTLS_KEY_EXCHANGE_ECDH_RSA=y +CONFIG_MBEDTLS_SSL_RENEGOTIATION=y +# CONFIG_MBEDTLS_SSL_PROTO_SSL3 is not set +CONFIG_MBEDTLS_SSL_PROTO_TLS1=y +CONFIG_MBEDTLS_SSL_PROTO_TLS1_1=y +CONFIG_MBEDTLS_SSL_PROTO_TLS1_2=y +# CONFIG_MBEDTLS_SSL_PROTO_DTLS is not set +CONFIG_MBEDTLS_SSL_ALPN=y +CONFIG_MBEDTLS_SSL_SESSION_TICKETS=y +CONFIG_MBEDTLS_AES_C=y +# CONFIG_MBEDTLS_CAMELLIA_C is not set +# CONFIG_MBEDTLS_DES_C is not set +CONFIG_MBEDTLS_RC4_DISABLED=y +# CONFIG_MBEDTLS_RC4_ENABLED_NO_DEFAULT is not set +# CONFIG_MBEDTLS_RC4_ENABLED is not set +# CONFIG_MBEDTLS_BLOWFISH_C is not set +# CONFIG_MBEDTLS_XTEA_C is not set +CONFIG_MBEDTLS_CCM_C=y +CONFIG_MBEDTLS_GCM_C=y +# CONFIG_MBEDTLS_RIPEMD160_C is not set +CONFIG_MBEDTLS_PEM_PARSE_C=y +CONFIG_MBEDTLS_PEM_WRITE_C=y +CONFIG_MBEDTLS_X509_CRL_PARSE_C=y +CONFIG_MBEDTLS_X509_CSR_PARSE_C=y +CONFIG_MBEDTLS_ECP_C=y +CONFIG_MBEDTLS_ECDH_C=y +CONFIG_MBEDTLS_ECDSA_C=y +CONFIG_MBEDTLS_ECP_DP_SECP192R1_ENABLED=y +CONFIG_MBEDTLS_ECP_DP_SECP224R1_ENABLED=y +CONFIG_MBEDTLS_ECP_DP_SECP256R1_ENABLED=y +CONFIG_MBEDTLS_ECP_DP_SECP384R1_ENABLED=y +CONFIG_MBEDTLS_ECP_DP_SECP521R1_ENABLED=y +CONFIG_MBEDTLS_ECP_DP_SECP192K1_ENABLED=y +CONFIG_MBEDTLS_ECP_DP_SECP224K1_ENABLED=y +CONFIG_MBEDTLS_ECP_DP_SECP256K1_ENABLED=y +CONFIG_MBEDTLS_ECP_DP_BP256R1_ENABLED=y +CONFIG_MBEDTLS_ECP_DP_BP384R1_ENABLED=y +CONFIG_MBEDTLS_ECP_DP_BP512R1_ENABLED=y +CONFIG_MBEDTLS_ECP_DP_CURVE25519_ENABLED=y +CONFIG_MBEDTLS_ECP_NIST_OPTIM=y +CONFIG_MDNS_MAX_SERVICES=10 +CONFIG_MQTT_PROTOCOL_311=y +CONFIG_MQTT_TRANSPORT_SSL=y +CONFIG_MQTT_TRANSPORT_WEBSOCKET=y +CONFIG_MQTT_TRANSPORT_WEBSOCKET_SECURE=y +# CONFIG_MQTT_USE_CUSTOM_CONFIG is not set +# CONFIG_MQTT_TASK_CORE_SELECTION_ENABLED is not set +# CONFIG_MQTT_CUSTOM_OUTBOX is not set +# CONFIG_OPENSSL_DEBUG is not set +CONFIG_OPENSSL_ASSERT_DO_NOTHING=y +# CONFIG_OPENSSL_ASSERT_EXIT is not set +CONFIG_ESP32_PTHREAD_TASK_PRIO_DEFAULT=5 +CONFIG_ESP32_PTHREAD_TASK_STACK_SIZE_DEFAULT=3072 +CONFIG_PTHREAD_STACK_MIN=768 +CONFIG_ESP32_DEFAULT_PTHREAD_CORE_NO_AFFINITY=y +# CONFIG_ESP32_DEFAULT_PTHREAD_CORE_0 is not set +# CONFIG_ESP32_DEFAULT_PTHREAD_CORE_1 is not set +CONFIG_ESP32_PTHREAD_TASK_CORE_DEFAULT=-1 +CONFIG_ESP32_PTHREAD_TASK_NAME_DEFAULT="pthread" +# CONFIG_SPI_FLASH_VERIFY_WRITE is not set +# CONFIG_SPI_FLASH_ENABLE_COUNTERS is not set +CONFIG_SPI_FLASH_ROM_DRIVER_PATCH=y +CONFIG_SPI_FLASH_WRITING_DANGEROUS_REGIONS_ABORTS=y +# CONFIG_SPI_FLASH_WRITING_DANGEROUS_REGIONS_FAILS is not set +# CONFIG_SPI_FLASH_WRITING_DANGEROUS_REGIONS_ALLOWED is not set +CONFIG_SPI_FLASH_YIELD_DURING_ERASE=y +CONFIG_SPI_FLASH_ERASE_YIELD_DURATION_MS=20 +CONFIG_SPI_FLASH_ERASE_YIELD_TICKS=1 +CONFIG_SPIFFS_MAX_PARTITIONS=3 +CONFIG_SPIFFS_CACHE=y +CONFIG_SPIFFS_CACHE_WR=y +# CONFIG_SPIFFS_CACHE_STATS is not set +CONFIG_SPIFFS_PAGE_CHECK=y +CONFIG_SPIFFS_GC_MAX_RUNS=10 +# CONFIG_SPIFFS_GC_STATS is not set +CONFIG_SPIFFS_PAGE_SIZE=256 +CONFIG_SPIFFS_OBJ_NAME_LEN=32 +CONFIG_SPIFFS_USE_MAGIC=y +CONFIG_SPIFFS_USE_MAGIC_LENGTH=y +CONFIG_SPIFFS_META_LENGTH=4 +CONFIG_SPIFFS_USE_MTIME=y +# CONFIG_SPIFFS_DBG is not set +# CONFIG_SPIFFS_API_DBG is not set +# CONFIG_SPIFFS_GC_DBG is not set +# CONFIG_SPIFFS_CACHE_DBG is not set +# CONFIG_SPIFFS_CHECK_DBG is not set +# CONFIG_SPIFFS_TEST_VISUALISATION is not set +CONFIG_IP_LOST_TIMER_INTERVAL=120 +CONFIG_TCPIP_LWIP=y +CONFIG_UNITY_ENABLE_FLOAT=y +CONFIG_UNITY_ENABLE_DOUBLE=y +# CONFIG_UNITY_ENABLE_COLOR is not set +CONFIG_UNITY_ENABLE_IDF_TEST_RUNNER=y +# CONFIG_UNITY_ENABLE_FIXTURE is not set +CONFIG_SUPPRESS_SELECT_DEBUG_OUTPUT=y +CONFIG_SUPPORT_TERMIOS=y +# CONFIG_WL_SECTOR_SIZE_512 is not set +CONFIG_WL_SECTOR_SIZE_4096=y +CONFIG_WL_SECTOR_SIZE=4096 +CONFIG_WIFI_PROV_SCAN_MAX_ENTRIES=16 diff --git a/libftcSoundBar.so/libftcSoundBar.cpp b/libftcSoundBar.so/libftcSoundBar.cpp new file mode 100644 index 0000000..fcd8a9c --- /dev/null +++ b/libftcSoundBar.so/libftcSoundBar.cpp @@ -0,0 +1,910 @@ +////////////////////////////////////////////////////////////////////////////////////////// +// +// ftcSoundBar Library +// +// Communicate via ROBOPro from fischertechnik TXT Controller with ftcSoundBar +// +// Version 0,90 +// +// (C) 2020 Oliver Schmiel, Christian Bergschneider & Stefan Fuss +// +// compile to libftcSoundBar.so and copy it as User ROBOPRO to /opt/knobloch/libs +// +///////////////////////////////////////////////////////////////////////////////////////// + +using namespace std; + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include "jsmn/jsmn.h" + +// Library version +#define VERSION 0.90 + +// Error codes +#define COM_OK 0 +#define COM_ERR_OpenSocket -1 +#define COM_ERR_ResolveHostname -2 +#define COM_ERR_Connect -3 +#define COM_ERR_Send -4 +#define COM_ERR_ContentTypeMissing -5 +#define COM_ERR_ContentUncomplete -6 + +// FT Codes +#define FISH_OK 0 +#define FISH_ERR 1 + +union in_addr2 { + unsigned long s_addr; + uint8_t octed[4]; +}; + +#define MAXJSON 1000 + +class JSON { + private: + int eq(jsmntok_t *tok, const char *s); + public: + char jsonData[MAXJSON]; + JSON(); + int GetParam( char *tag, char *value ); + int GetParam( char *tag, short *value ); + +}; + +JSON::JSON() { + + bzero( (char *) jsonData, MAXJSON ); + +} + +int JSON::eq(jsmntok_t *tok, const char *s) { + if (tok->type == JSMN_STRING && (int)strlen(s) == tok->end - tok->start && + strncmp(jsonData + tok->start, s, tok->end - tok->start) == 0) { + return 0; + } + return -1; +} + +int JSON::GetParam( char *tag, char *value ) { + + int i, tokens; + + jsmn_parser p; + jsmntok_t t[128]; /* We expect no more than 128 tokens */ + + jsmn_init(&p); + tokens = jsmn_parse(&p, jsonData, strlen(jsonData), t, sizeof(t) / sizeof(t[0])); + if (tokens < 0) { + return -1; + } + + if (tokens < 1 || t[0].type != JSMN_OBJECT) { + return -2; + } + + /* Loop over all keys of the root object */ + for (i = 1; i < tokens; i++) { + if (eq(&t[i], tag) == 0) { + /* We may want to do strtol() here to get numeric value */ + i++; + strncpy(value, jsonData + t[i].start, t[i].end - t[i].start); + value[ t[i].end - t[i].start ] = '\0'; + i++; + } + } + + return 0; +} + +int JSON::GetParam( char *tag, short *value ) { + + char temp[100]; + int err; + + err = GetParam( tag, temp); + if ( err == 0) { + *value = atoi( temp ); + } + + return err; + +} + +class RESTServer { + private: + in_addr2 ip; + char hostname[20]; + short port; + short lastError; + void updateHostname(); + int tcp_send_receive( char *request, char *responseHeader, int s_responseHeader, char *responseData, int s_responseData ); + public: + JSON jsonData; + RESTServer(); + void setIP( unsigned long newIP ); + short getOcted( short octed); + void setOcted( short octed, short value ); + void setPort( short newPort ); + short getPort( void ); + char *getHostname(); + int http_get( char serviceMethod[] ); + int http_post( char serviceMethod[], char jsonData[] ); + short getLastError( void ); +}; + +RESTServer ftcSoundBar; + +/** + * @brief returns the last communication error + * + * @param no parameters + * + * @return + * - COM_OK + * - COM_ERR_OpenSocket + * - COM_ERR_ResolveHostname + * - COM_ERR_Connect + * - COM_ERR_Send + * - COM_ERR_ContentTypeMissing + * - COM_ERR_ContentUncomplete + */ + short RESTServer::getLastError( void ) { + return lastError; +} + +/** + * @brief constructor, tries to get a dns reolution for ftcSoundBar + * + * @param no parameters + * + * @return no result + */ +RESTServer::RESTServer() { + // initialize structure + + // set default port to 80 + port = 80; + + // scan IP + struct hostent *ftcSoundBar; + + // ask DNS for ftcSoundBar's IP + ftcSoundBar = gethostbyname("ftcSoundBar"); + + ip.s_addr = 0; + + if ( ( ftcSoundBar != NULL ) && ( ftcSoundBar->h_addrtype == AF_INET ) && ( ftcSoundBar->h_addr_list[0] != 0 ) ) { + // DNS ok, IPv4 + ip.s_addr = *((unsigned long*) ftcSoundBar->h_addr_list[0]); + } + + // update hostname string + updateHostname(); + + port = 80; + +} + +/** + * @brief get an octed [0..3] of the ftcSoundBar's IP address + * + * @param[in] octed number of the octed + * + * @return + * - octed + */ +short RESTServer::getOcted( short octed ) { + + return ip.octed[octed]; + +} + +/** + * @brief set an octed [0..3] of the ftcSoundBar's IP address + * + * @param[in] octed number of the octed + * value value of the octed + * + * @return noting + */ +void RESTServer::setOcted( short octed, short value ) { + + ip.octed[octed] = value; + + // update hostname string + updateHostname(); + +} + +/** + * @brief set the ftcSoundBar's IP address + * + * @param[in] newIP IP Address + * + * @return noting + */ +void RESTServer::setIP( unsigned long newIP ) { + + ip.s_addr = newIP; + + // update hostname string + updateHostname(); + +} + +/** + * @brief set the port of the ftcSoundBar + * + * @param[in] port port number + * + * @return noting + */ +void RESTServer::setPort( short newPort ) { + port = newPort; +} + +/** + * @brief get the port of the ftcSoundBar + * + * @param[in] noting + * + * @return port number + */ +short RESTServer::getPort( void ) { + return port; +} + +/** + * @brief get the hostname based on an IP address in format xxx.xxx.xxx.xxx + * + * @param[in] noting + * + * @return hostname + */ +char *RESTServer::getHostname() { + return hostname; +} + +/** + * @brief internal function to update the internal hostname-string after a change on the IP Address + * + * @param[in] noting + * + * @return nothing + */ +void RESTServer::updateHostname() { + + sprintf( hostname, "%u.%u.%u.%u", ip.octed[0], ip.octed[1], ip.octed[2], ip.octed[3] ); + +} + +/** + * @brief sends a request to the RESTServer and returns the response (header + data) + * + * @param[in] request request + * responseHeader header of the response + * s_responseHeader size of responseHeader + * responseData data of the response + * s_responseData size of responseData + * + * @return + * - COM_OK + * - COM_ERR_Connect + * - COM_ERR_Send + */ +int RESTServer::tcp_send_receive( char *request, char *responseHeader, int s_responseHeader, char *responseData, int s_responseData ) { + + struct sockaddr_in serveraddr; + + int tcpSocket = socket(AF_INET, SOCK_STREAM, 0); + + // initialize serveraddr + bzero((char *) &serveraddr, sizeof(serveraddr)); + serveraddr.sin_family = AF_INET; + + // set IP + serveraddr.sin_addr.s_addr = ip.s_addr; + + // set port + serveraddr.sin_port = htons(port); + + // connect + if (connect(tcpSocket, (struct sockaddr *) &serveraddr, sizeof(serveraddr)) < 0) { + lastError = COM_ERR_Connect; + return lastError; + } + + // send + if (send(tcpSocket, request, strlen(request), 0) < 0) { + lastError = COM_ERR_Send; + return lastError; + } + + // clear response + bzero(responseHeader, s_responseHeader); + bzero(responseData, s_responseData); + + // copy value, expect 2 packets + recv(tcpSocket, responseHeader, s_responseHeader-1, 0); + recv(tcpSocket, responseData, s_responseData-1, 0); + + close(tcpSocket); + + lastError = COM_OK; + return lastError; + +} + +/** + * @brief sends a get-request to the RESTServer and stores the result internally + * + * @param[in] serviceMethod method to call + * + * @return + * - COM_OK + * - COM_ERR_Connect + * - COM_ERR_Send + * - COM_ERR_ContentTypeMissing + * - COM_ERR_ContentUncomplete + */ +int RESTServer::http_get( char serviceMethod[] ) +{ + // needed buffers + char request[1000]; + char responseHeader[1000]; + int com_status; + + // build command + sprintf(request, "GET /%s HTTP/1.1\r\nHost: %s\r\n\r\n", serviceMethod, hostname); + + // send command + com_status = tcp_send_receive( request, responseHeader, 1000, jsonData.jsonData, MAXJSON ); + if ( com_status != COM_OK ) { + lastError = com_status; + return lastError; + } + + // start checking response header + char delimiter[] = "\r\n"; + char *ptr; + int status = 0; + bool contentLength = false; + bool contentType = false; + char temp[20]; + + // split header into lines + ptr = strtok(responseHeader, delimiter); + + // analyze lines in header + while(ptr != NULL) { + + if ( strncmp( ptr, "HTTP/1.1 ", 9 ) == 0) { + // line starts with HTTP... check for HTTP status + strncpy( temp, &(ptr[9]), 3 ); + status = atoi(temp); + } + + if ( strcmp( ptr, "Content-Type: application/json" ) == 0) { + // Content-Type json + contentType = true; + } + + if ( strncmp( ptr, "Content-Length: ", 16 ) == 0) { + // check Content-Length. Attention: ContentLength starting with { + contentLength = ( (unsigned int)atoi( &(ptr[16]) ) == strlen( strstr( jsonData.jsonData, "{" ) ) ); + } + + // get next line + ptr = strtok(NULL, delimiter); + } + + if ( !contentType ) { + lastError = COM_ERR_ContentTypeMissing; + return lastError; + + } else if ( !contentLength ) { + lastError = COM_ERR_ContentUncomplete; + return lastError; + + } + + lastError = COM_OK; + return status; + +} + +/** + * @brief post a request to the RESTServer + * + * @param[in] serviceMethod method to call + * jsonData json-string to pass + * + * @return + * - COM_OK + * - COM_ERR_Connect + * - COM_ERR_Send + */ +int RESTServer::http_post( char serviceMethod[], char jsonData[] ) +{ + char request[1000]; + char responseHeader[250]; + char responseData[250]; + + sprintf(request, "POST /%s HTTP/1.1\r\nHOST: %s:%d\r\nContent-Type:application/json\r\nAccept:*/*\r\nContent-Length:%d\r\n\r\n%s", + serviceMethod, + hostname, + port, + strlen(jsonData), + jsonData); + + lastError = tcp_send_receive( request, responseHeader, 250, responseData, 250 ); + + return lastError; + +} + +extern "C" { + + /** + * @brief get the internal library version + * + * @param[in] Version Version number + * + * @return + * - FISH_OK + */ + int getVersion(double *Version) + // gets the libs Version + { + + *Version = VERSION; + + return FISH_OK; + } + + /** + * @brief get the last com-error + * + * @param[in] lastError error number + * + * @return + * - FISH_OK + */ + int getLastError(short *lastError) { + *lastError = ftcSoundBar.getLastError( ); + return FISH_OK; + } + + /** + * @brief set ftcSoundBar's port + * + * @param[in] port port number + * + * @return + * - FISH_OK + */ + int setPort(short port) { + // set servers port + ftcSoundBar.setPort( port ); + return FISH_OK; + } + + /** + * @brief get ftcSoundBar's port + * + * @param[in] port port number + * + * @return + * - FISH_OK + */ + int getPort(short *port) { + *port = ftcSoundBar.getPort( ); + return FISH_OK; + } + + /** + * @brief get ftcSoundBar's IP address/1st octed + * + * @param[in] octed value of the octed + * + * @return + * - FISH_OK + */ + int getIP0(short *octed) { + *octed = ftcSoundBar.getOcted( 0 ); + return FISH_OK; + } + + /** + * @brief get ftcSoundBar's IP address/2nd octed + * + * @param[in] octed value of the octed + * + * @return + * - FISH_OK + */ + int getIP1(short *octed) { + *octed = ftcSoundBar.getOcted( 1 ); + return FISH_OK; + } + + /** + * @brief get ftcSoundBar's IP address/3rd octed + * + * @param[in] octed value of the octed + * + * @return + * - FISH_OK + */ + int getIP2(short *octed) { + *octed = ftcSoundBar.getOcted( 2 ); + return FISH_OK; + } + + /** + * @brief get ftcSoundBar's IP address/4th octed + * + * @param[in] octed value of the octed + * + * @return + * - FISH_OK + */ + int getIP3(short *octed) { + *octed = ftcSoundBar.getOcted( 3 ); + return FISH_OK; + } + + /** + * @brief set ftcSoundBar's IP address/1st octed + * + * @param[in] ip value of the octed + * + * @return + * - FISH_OK + */ + int setIP0(short ip) { + // set 1st octed of server IP + ftcSoundBar.setOcted( 0, ip ); + return FISH_OK; + } + + /** + * @brief set ftcSoundBar's IP address/2nd octed + * + * @param[in] ip value of the octed + * + * @return + * - FISH_OK + */ + int setIP1(short ip) { + // set 2nd octed of server IP + ftcSoundBar.setOcted( 1, ip ); + return FISH_OK; + } + + /** + * @brief set ftcSoundBar's IP address/3rd octed + * + * @param[in] ip value of the octed + * + * @return + * - FISH_OK + */ + int setIP2(short ip) { + // set 3rd octed of server IP + ftcSoundBar.setOcted( 2, ip ); + return FISH_OK; + } + + /** + * @brief set ftcSoundBar's IP address/4th octed + * + * @param[in] ip value of the octed + * + * @return + * - FISH_OK + */ + int setIP3(short ip) { + // set 4th octed of server IP + ftcSoundBar.setOcted( 3, ip ); + return FISH_OK; + } + + /** + * @brief play track + * + * @param[in] track track number [1..n] + * + * @return + * - FISH_OK + */ + int play(short track) { + // play track #track + + char jsonData[100]; + + sprintf( jsonData, "{\"track\": %hi}", track ); + + ftcSoundBar.http_post( (char *) "api/v1/track/play", jsonData ); + + return FISH_OK; + + } + + /** + * @brief set volume + * + * @param[in] volumne + * + * @return + * - FISH_OK + */ + int setVolume(short volume) { + // set volumne + + char jsonData[100]; + + sprintf( jsonData, "{\"volume\": %hi}", volume ); + + ftcSoundBar.http_post( (char *) "api/v1/volume", jsonData ); + + return FISH_OK; + + } + + /** + * @brief stop the active track + * + * @param[in] none + * + * @return + * - FISH_OK + */ + int stopTrack(short dummy) { + // stops the actual track + + ftcSoundBar.http_post( (char *) "api/v1/track/stop", (char *)"" ); + + return FISH_OK; + + } + + /** + * @brief pauses the active track + * + * @param[in] none + * + * @return + * - FISH_OK + */ + int pauseTrack(short dummy) { + // pauses the actual track + + ftcSoundBar.http_post( (char *) "api/v1/track/pause", (char *)"" ); + + return FISH_OK; + + } + + /** + * @brief resumes the active track + * + * @param[in] none + * + * @return + * - FISH_OK + */ + int resumeTrack(short dummy) { + // continues the actual track + + ftcSoundBar.http_post( (char *) "api/v1/track/resume", (char *)"" ); + + return FISH_OK; + + } + + /** + * @brief play previous track + * + * @param[in] none + * + * @return + * - FISH_OK + */ + int previous(short dummy) { + // play previous track + + ftcSoundBar.http_post( (char *) "api/v1/track/previous", (char *)"" ); + + return FISH_OK; + + } + + /** + * @brief play next track + * + * @param[in] none + * + * @return + * - FISH_OK + */ + int next(short dummy) { + // play next track + + ftcSoundBar.http_post( (char *) "api/v1/track/next", (char *)"" ); + + return FISH_OK; + + } + + /** + * @brief set mode (NORMAL/SHUFFLE/REPEAT) + * + * @param[in] mode + * + * @return + * - FISH_OK + */ + int setMode(short mode) { + // set mode: 0 - normal, 1 - shuffle, 2 - repeat + + char jsonData[100]; + + sprintf( jsonData, "{\"mode\": %hi}", mode ); + + ftcSoundBar.http_post( (char *) "api/v1/mode", jsonData ); + + return FISH_OK; + + } + + /** + * @brief get mode + * + * @param[in] mode NORMAL/SHUFFLE/REPEAT + * + * @return + * - FISH_OK + */ + int getMode(short *mode) { + + int status; + + // http_get mode + status = ftcSoundBar.http_get( (char *) "api/v1/mode" ); + + if ( status != 200 ) { + return FISH_ERR; + } + + // get return value mode and test on error + if ( 0 != ftcSoundBar.jsonData.GetParam( (char *) "mode", mode ) ) { + return FISH_ERR; + } + + return FISH_OK; + + } + + /** + * @brief get number of tracks + * + * @param[in] tracks number of tracks + * + * @return + * - FISH_OK + */ + int getTracks(short *tracks) { + + int status; + + // http_get tracks + status = ftcSoundBar.http_get( (char *) "api/v1/track" ); + + if ( status != 200 ) { + return FISH_ERR; + } + + // get return value mode and test on error + if ( 0 != ftcSoundBar.jsonData.GetParam( (char *) "tracks", tracks ) ) { + return FISH_ERR; + } + + return FISH_OK; + + } + + /** + * @brief get active track + * + * @param[in] track active track + * + * @return + * - FISH_OK + */ + int getActiveTrack(short *active_track) { + + int status; + + // http_get tracks + status = ftcSoundBar.http_get( (char *) "api/v1/track" ); + + if ( status != 200 ) { + return FISH_ERR; + } + + // get return value mode and test on error + if ( 0 != ftcSoundBar.jsonData.GetParam( (char *) "active_track", active_track ) ) { + return FISH_ERR; + } + + return FISH_OK; + + } + + /** + * @brief get audio pipeline's state + * + * @param[in] state RUNNING/PAUSED/STOPPED/FINISHED... + * + * @return + * - FISH_OK + */ + int getTrackState(short *state) { + + int status; + + // http_get tracks + status = ftcSoundBar.http_get( (char *) "api/v1/track" ); + + if ( status != 200 ) { + return FISH_ERR; + } + + // get return value mode and test on error + if ( 0 != ftcSoundBar.jsonData.GetParam( (char *) "state", state ) ) { + return FISH_ERR; + } + + return FISH_OK; + + } + + /** + * @brief get volumne + * + * @param[in] volumne volume + * + * @return + * - FISH_OK + */ + int getVolume(short *volume) { + + int status; + + // http_get volume + status = ftcSoundBar.http_get( (char *) "api/v1/volume" ); + + if ( status != 200 ) { + return FISH_ERR; + } + + // get return value mode and test on error + if ( 0 != ftcSoundBar.jsonData.GetParam( (char *) "volume", volume ) ) { + return FISH_ERR; + } + + return FISH_OK; + + } + +} // extern "C" \ No newline at end of file diff --git a/robopro/ftcSoundBar.rpp b/robopro/ftcSoundBar.rpp new file mode 100644 index 0000000..dee6b28 --- /dev/null +++ b/robopro/ftcSoundBar.rpp @@ -0,0 +1,5744 @@ + + + + ftcSoundBar + wxCanvasDocument generated by wxCANVAS + 14 + 0 + 04060600 + + 2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file