From 2ab979403b3dacb6b715600839ed9754c190988b Mon Sep 17 00:00:00 2001 From: Mike Date: Sat, 6 Jan 2024 16:38:21 +0000 Subject: [PATCH 001/128] Set CONFIG_FREERTOS_UNICORE=y by default (#2696) Co-authored-by: mikee47 --- Sming/Arch/Esp32/Components/esp32/sdk/config/common | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Sming/Arch/Esp32/Components/esp32/sdk/config/common b/Sming/Arch/Esp32/Components/esp32/sdk/config/common index c893597df9..41c9b2c602 100644 --- a/Sming/Arch/Esp32/Components/esp32/sdk/config/common +++ b/Sming/Arch/Esp32/Components/esp32/sdk/config/common @@ -53,5 +53,8 @@ CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=n # Don't change provided flash configuration information CONFIG_ESPTOOLPY_FLASHSIZE_DETECT=n -#' We'll handle WDT initialisation ourselves thankyouverymuch +# We'll handle WDT initialisation ourselves thankyouverymuch CONFIG_ESP_TASK_WDT_INIT=n + +# Issues with dual-core CPU, see #2653 +CONFIG_FREERTOS_UNICORE=y From e890519755d9050c7fe4706b8ececaf23f705055 Mon Sep 17 00:00:00 2001 From: Mike Date: Mon, 8 Jan 2024 16:59:34 +0000 Subject: [PATCH 002/128] Fix esp32 timer issues (#2697) HostTests fails on esp32 due to following issues: **Flawed `hw_timer2_read()` implementation** Without setting update bit counter value won't change. Probably worked previously because some other code did this. **WDT with callback timers** Checked against IDF implementations and there's a missing flag which keeps interrupts initially disabled during setup. **Callbacks missing during timer testing** Offending code looks like this: ``` Timer timer; String s; s += timer; //<< Not what I expected ``` The above line actually calls the `Timer` copy constructor. Delete this and the compiler catches the error. The corrected line now reads `s += String(timer)`. --- Sming/Arch/Esp32/Components/driver/hw_timer.cpp | 3 ++- .../Arch/Esp32/Components/driver/include/driver/hw_timer.h | 1 + Sming/Core/CallbackTimer.h | 7 ++++++- tests/HostTests/host-tests-esp32.hw | 2 +- tests/HostTests/modules/Timers.cpp | 2 +- 5 files changed, 11 insertions(+), 4 deletions(-) diff --git a/Sming/Arch/Esp32/Components/driver/hw_timer.cpp b/Sming/Arch/Esp32/Components/driver/hw_timer.cpp index 9b5cc202dc..8ab470c5b0 100644 --- a/Sming/Arch/Esp32/Components/driver/hw_timer.cpp +++ b/Sming/Arch/Esp32/Components/driver/hw_timer.cpp @@ -62,7 +62,8 @@ class TimerConfig #else int source = timer_group_periph_signals.groups[group].timer_irq_id[index]; #endif - esp_intr_alloc_intrstatus(source, ESP_INTR_FLAG_IRAM, status_reg, mask, timerIsr, this, &isr_handle); + esp_intr_alloc_intrstatus(source, ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_INTRDISABLED, status_reg, mask, timerIsr, + this, &isr_handle); clear_intr_status(); enable_intr(true); } diff --git a/Sming/Arch/Esp32/Components/driver/include/driver/hw_timer.h b/Sming/Arch/Esp32/Components/driver/include/driver/hw_timer.h index 8f2a7e2ccd..1979b45072 100644 --- a/Sming/Arch/Esp32/Components/driver/include/driver/hw_timer.h +++ b/Sming/Arch/Esp32/Components/driver/include/driver/hw_timer.h @@ -147,6 +147,7 @@ uint32_t hw_timer1_read(void); __forceinline static uint32_t hw_timer2_read(void) { #if CONFIG_ESP_TIMER_IMPL_TG0_LAC + REG_WRITE(TIMG_LACTUPDATE_REG(0), 1); return REG_READ(TIMG_LACTLO_REG(0)); #elif ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0) systimer_ll_counter_snapshot(&SYSTIMER, 0); diff --git a/Sming/Core/CallbackTimer.h b/Sming/Core/CallbackTimer.h index 9844c6ed86..f56ef2dc58 100644 --- a/Sming/Core/CallbackTimer.h +++ b/Sming/Core/CallbackTimer.h @@ -33,6 +33,12 @@ template struct CallbackTimerApi { return ApiDef::typeName(); } + CallbackTimerApi() + { + } + + CallbackTimerApi(const CallbackTimerApi&) = delete; + String name() const { String s; @@ -50,7 +56,6 @@ template struct CallbackTimerApi { s += static_cast(this)->getInterval(); s += ", ticks = "; s += static_cast(this)->ticks(); - // s += ApiDef::Clock::ticks(); return s; } diff --git a/tests/HostTests/host-tests-esp32.hw b/tests/HostTests/host-tests-esp32.hw index d8b9c308fa..f56f5bd7c9 100644 --- a/tests/HostTests/host-tests-esp32.hw +++ b/tests/HostTests/host-tests-esp32.hw @@ -3,7 +3,7 @@ "base_config": "host-tests", "partitions": { "factory": { - "size": "1M" + "size": "1600K" } } } diff --git a/tests/HostTests/modules/Timers.cpp b/tests/HostTests/modules/Timers.cpp index 5e0eda4fba..2dc7bb638c 100644 --- a/tests/HostTests/modules/Timers.cpp +++ b/tests/HostTests/modules/Timers.cpp @@ -81,7 +81,7 @@ class CallbackTimerTest : public TestGroup String s; s += system_get_time(); s += " "; - s += statusTimer; + s += String(statusTimer); s += " fired, timercount = "; s += activeTimerCount; s += ", mem = "; From 8edfec9d5672a2c1f7a1a6242664a7c69c8d38e1 Mon Sep 17 00:00:00 2001 From: Mike Date: Tue, 9 Jan 2024 08:57:25 +0000 Subject: [PATCH 003/128] Add support for ESP32 IDF version 5.2 (beta) (#2694) --- .github/workflows/ci-esp32.yml | 2 +- Sming/Arch/Esp32/Components/driver/uart.cpp | 5 + .../Arch/Esp32/Components/esp32/component.mk | 113 +++++++++++++++--- Sming/Arch/Esp32/Components/esp32/src/clk.c | 6 +- Sming/Arch/Esp32/Core/Digital.cpp | 8 +- Sming/Arch/Esp32/README.rst | 16 +-- Sming/Arch/Esp32/Tools/ci/build.run.cmd | 2 +- Sming/Arch/Esp32/Tools/idf_tools.py | 28 +++-- Sming/Arch/Esp32/Tools/install.cmd | 7 +- Sming/Arch/Esp32/build.mk | 61 ++++++---- Sming/Libraries/SPI/src/Arch/Esp32/SPI.cpp | 19 ++- Tools/ci/build.cmd | 5 +- Tools/ci/setenv.ps1 | 1 - Tools/export.sh | 1 - tests/HostTests/component.mk | 15 +-- tests/HostTests/include/modules.h | 2 +- .../modules/{ => Network}/Crypto.cpp | 0 .../modules/{ => Network}/Crypto/AxHash.h | 0 .../modules/{ => Network}/Crypto/BrHash.h | 0 .../modules/{ => Network}/Crypto/EspHash.h | 0 20 files changed, 210 insertions(+), 81 deletions(-) rename tests/HostTests/modules/{ => Network}/Crypto.cpp (100%) rename tests/HostTests/modules/{ => Network}/Crypto/AxHash.h (100%) rename tests/HostTests/modules/{ => Network}/Crypto/BrHash.h (100%) rename tests/HostTests/modules/{ => Network}/Crypto/EspHash.h (100%) diff --git a/.github/workflows/ci-esp32.yml b/.github/workflows/ci-esp32.yml index 4036e42932..4796499abc 100644 --- a/.github/workflows/ci-esp32.yml +++ b/.github/workflows/ci-esp32.yml @@ -13,7 +13,7 @@ jobs: matrix: os: [ubuntu-20.04, windows-latest] variant: [esp32, esp32s2, esp32c3, esp32s3, esp32c2] - idf_version: ["4.3", "4.4", "5.0"] + idf_version: ["4.3", "4.4", "5.0", "5.2"] exclude: - variant: esp32s3 idf_version: "4.3" diff --git a/Sming/Arch/Esp32/Components/driver/uart.cpp b/Sming/Arch/Esp32/Components/driver/uart.cpp index 3e03b60339..c35f7c6aa7 100644 --- a/Sming/Arch/Esp32/Components/driver/uart.cpp +++ b/Sming/Arch/Esp32/Components/driver/uart.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -567,7 +568,11 @@ uint32_t smg_uart_set_baudrate_reg(int uart_nr, uint32_t baud_rate) uart_ll_set_baudrate(dev, baud_rate); return uart_ll_get_baudrate(dev); #else +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 2, 0) uart_ll_set_sclk(dev, UART_SCLK_DEFAULT); +#else + uart_ll_set_sclk(dev, soc_module_clk_t(UART_SCLK_DEFAULT)); +#endif uart_ll_set_baudrate(dev, baud_rate, APB_CLK_FREQ); return uart_ll_get_baudrate(dev, APB_CLK_FREQ); #endif diff --git a/Sming/Arch/Esp32/Components/esp32/component.mk b/Sming/Arch/Esp32/Components/esp32/component.mk index 5a83cd1aa1..80a45453de 100644 --- a/Sming/Arch/Esp32/Components/esp32/component.mk +++ b/Sming/Arch/Esp32/Components/esp32/component.mk @@ -14,8 +14,21 @@ ifeq ($(CREATE_EVENT_TASK),1) COMPONENT_CPPFLAGS += -DCREATE_EVENT_TASK endif -IDF_VERSION := $(firstword $(subst -, ,$(IDF_VER))) -IDF_VERSION_4 := $(filter v4%,$(IDF_VERSION)) +ifneq (,$(filter v4.%,$(IDF_VERSION))) +IDF_VERSION_4x := 1 +ifneq (,$(filter v4.3%,$(IDF_VERSION))) +IDF_VERSION_43 := 1 +else ifneq (,$(filter v4.4%,$(IDF_VERSION))) +IDF_VERSION_44 := 1 +endif +else ifneq (,$(filter v5.%,$(IDF_VERSION))) +IDF_VERSION_5x := 1 +endif +ifneq (,$(filter v5.0%,$(IDF_VERSION))) +IDF_VERSION_50 := 1 +else ifneq (,$(filter v5.2%,$(IDF_VERSION))) +IDF_VERSION_52 := 1 +endif ifneq (,$(filter esp32s3-v4.3%,$(ESP_VARIANT)-$(IDF_VER))) $(error esp32s3 requires ESP IDF v4.4 or later) @@ -32,6 +45,7 @@ SDKCONFIG_H := $(SDK_BUILD_BASE)/config/sdkconfig.h SDK_LIBDIRS := \ esp_wifi/lib/$(ESP_VARIANT) \ + esp_coex/lib/$(ESP_VARIANT) \ esp_phy/lib/$(ESP_VARIANT) \ xtensa/$(ESP_VARIANT) \ hal/$(ESP_VARIANT) \ @@ -52,6 +66,12 @@ endif ESP32_COMPONENT_PATH := $(COMPONENT_PATH) SDK_DEFAULT_PATH := $(ESP32_COMPONENT_PATH)/sdk +ifdef IDF_VERSION_52 +GLOBAL_CFLAGS += \ + -DSOC_XTAL_FREQ_MHZ=CONFIG_XTAL_FREQ \ + -DSOC_MMU_PAGE_SIZE=CONFIG_MMU_PAGE_SIZE +endif + LIBDIRS += \ $(SDK_COMPONENT_LIBDIR) \ $(SDK_BUILD_BASE)/esp-idf/mbedtls/mbedtls/library \ @@ -65,13 +85,10 @@ LIBDIRS += \ SDK_INCDIRS := \ app_update/include \ bootloader_support/include \ - bootloader_support/include_bootloader \ - driver/$(ESP_VARIANT)/include \ driver/include \ esp_pm/include \ esp_rom/include/$(ESP_VARIANT) \ esp_rom/include \ - $(ESP_VARIANT)/include \ esp_ringbuf/include \ esp_timer/include \ soc/include \ @@ -82,7 +99,6 @@ SDK_INCDIRS := \ nvs_flash/include \ esp_event/include \ lwip/lwip/src/include \ - lwip/port/esp32/include \ newlib/platform_include \ spi_flash/include \ wpa_supplicant/include \ @@ -90,19 +106,37 @@ SDK_INCDIRS := \ esp_hw_support/include \ esp_hw_support/include/soc \ hal/include \ - hal/platform_port/include \ hal/$(ESP_VARIANT)/include \ esp_system/include \ esp_common/include \ esp_netif/include \ esp_eth/include \ esp_wifi/include \ + lwip/include/apps/sntp + +ifdef IDF_VERSION_4x +SDK_INCDIRS += \ + $(ESP_VARIANT)/include +endif + +ifdef IDF_VERSION_43 +SDK_INCDIRS += \ esp_wifi/esp32/include \ - lwip/include/apps/sntp \ wpa_supplicant/include/esp_supplicant +else +SDK_INCDIRS += \ + hal/platform_port/include +endif -ifdef IDF_VERSION_4 +ifndef IDF_VERSION_52 SDK_INCDIRS += \ + driver/$(ESP_VARIANT)/include \ + lwip/port/esp32/include +endif + +ifdef IDF_VERSION_4x +SDK_INCDIRS += \ + bootloader_support/include_bootloader \ esp_adc_cal/include \ esp_ipc/include \ freertos/include \ @@ -119,13 +153,29 @@ SDK_INCDIRS += \ freertos/esp_additions/include/freertos \ driver/deprecated FREERTOS_PORTABLE := freertos/FreeRTOS-Kernel/portable +ifeq (v5.2,$(IDF_VERSION)) +SDK_INCDIRS += \ + driver/gpio/include \ + driver/ledc/include \ + driver/spi/include \ + lwip/port/include \ + lwip/port/freertos/include \ + lwip/port/esp32xx/include \ + esp_bootloader_format/include \ + esp_adc/$(ESP_VARIANT)/include \ + freertos/config/include \ + freertos/config/include/freertos +endif endif - - ifeq ($(ENABLE_BLUETOOTH),1) +ifeq (esp32s3-v5.2,$(ESP_VARIANT)-$(IDF_VERSION)) +ESP_BT_VARIANT := esp32c3 +else +ESP_BT_VARIANT := $(ESP_VARIANT) +endif SDK_INCDIRS += \ - bt/include/$(ESP_VARIANT)/include \ + bt/include/$(ESP_BT_VARIANT)/include \ bt/common/api/include/api \ bt/common/btc/profile/esp/blufi/include \ bt/common/btc/profile/esp/include \ @@ -148,17 +198,31 @@ SDK_INCDIRS += \ bt/host/nimble/nimble/nimble/host/store/config/include \ bt/host/nimble/esp-hci/include \ bt/host/nimble/port/include +ifdef IDF_VERSION_52 +SDK_INCDIRS += \ + bt/host/nimble/nimble/nimble/transport/include +endif endif ifdef IDF_TARGET_ARCH_RISCV SDK_INCDIRS += \ $(FREERTOS_PORTABLE)/riscv/include \ + $(FREERTOS_PORTABLE)/riscv/include/freertos \ + freertos/config/riscv \ + freertos/config/riscv/include \ riscv/include else SDK_INCDIRS += \ xtensa/include \ xtensa/$(ESP_VARIANT)/include \ - $(FREERTOS_PORTABLE)/xtensa/include + $(FREERTOS_PORTABLE)/xtensa/include \ + $(FREERTOS_PORTABLE)/xtensa/include/freertos +ifdef IDF_VERSION_52 +SDK_INCDIRS += \ + freertos/config/xtensa \ + freertos/config/xtensa/include \ + xtensa/deprecated_include +endif endif @@ -193,13 +257,13 @@ SDK_COMPONENTS := \ soc \ spi_flash -ifneq (,$(filter v4.3%,$(IDF_VERSION))) +ifdef IDF_VERSION_43 SDK_COMPONENTS += $(ESP_VARIANT) else SDK_COMPONENTS += esp_phy endif -ifdef IDF_VERSION_4 +ifdef IDF_VERSION_4x SDK_COMPONENTS += \ esp_ipc \ esp_adc_cal @@ -210,6 +274,12 @@ SDK_COMPONENTS += \ esp_partition endif +ifeq (v5.2,$(IDF_VERSION)) +SDK_COMPONENTS += \ + esp_mm \ + esp_coex +endif + ifneq ($(DISABLE_NETWORK),1) SDK_COMPONENTS += \ @@ -302,10 +372,14 @@ LDFLAGS_esp32s3 := \ LDFLAGS_esp32c2 := \ $(call LinkerScript,rom.newlib) \ $(call LinkerScript,rom.version) \ - $(call LinkerScript,rom.newlib-time) \ $(call LinkerScript,rom.heap) \ $(call LinkerScript,rom.mbedtls) +ifneq (v5.2,$(IDF_VERSION)) +LDFLAGS_esp32c2 += \ + $(call LinkerScript,rom.newlib-time) +endif + SDK_WRAP_SYMBOLS := SDK_UNDEF_SYMBOLS := @@ -329,7 +403,7 @@ EXTRA_LDFLAGS := \ $(call Undef,$(SDK_UNDEF_SYMBOLS)) \ $(call Wrap,$(SDK_WRAP_SYMBOLS)) -ifneq (,$(filter v4.3%,$(IDF_VERSION))) +ifdef IDF_VERSION_43 EXTRA_LDFLAGS += \ -T $(ESP_VARIANT)_out.ld \ $(call LinkerScript,project) @@ -440,3 +514,8 @@ sdk-help: ##Get SDK build options .PHONY: sdk sdk: ##Pass options to IDF builder, e.g. `make sdk -- --help` or `make sdk menuconfig` $(Q) $(SDK_BUILD) $(filter-out sdk,$(MAKECMDGOALS)) + + +.PHONY: check-incdirs +check-incdirs: ##Check IDF include paths and report any not found in this SDK version + $(Q) $(foreach d,$(SDK_INCDIRS),$(if $(wildcard $(SDK_COMPONENTS_PATH)/$d),,$(info $d))) diff --git a/Sming/Arch/Esp32/Components/esp32/src/clk.c b/Sming/Arch/Esp32/Components/esp32/src/clk.c index 724f3def0e..84777f8693 100644 --- a/Sming/Arch/Esp32/Components/esp32/src/clk.c +++ b/Sming/Arch/Esp32/Components/esp32/src/clk.c @@ -2,10 +2,12 @@ #include #include +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 2, 0) #define EXPAND(a) a #define CONCAT3a(a, b, c) a##b##c #define CONCAT3(a, b, c) CONCAT3a(a, b, c) -typedef CONCAT3(esp_pm_config_, SMING_SOC, _t) pm_config_t; +typedef CONCAT3(esp_pm_config_, SMING_SOC, _t) esp_pm_config_t; +#endif int esp_clk_cpu_freq(void); @@ -13,7 +15,7 @@ static esp_pm_lock_handle_t handle; bool system_update_cpu_freq(uint32_t freq) { - pm_config_t config = {}; + esp_pm_config_t config = {}; esp_err_t err = esp_pm_get_configuration(&config); if(err != ESP_OK) { debug_e("[PM] Failed to read PM config %u", err); diff --git a/Sming/Arch/Esp32/Core/Digital.cpp b/Sming/Arch/Esp32/Core/Digital.cpp index b5c694018f..48015d79f3 100644 --- a/Sming/Arch/Esp32/Core/Digital.cpp +++ b/Sming/Arch/Esp32/Core/Digital.cpp @@ -15,6 +15,10 @@ #include #if SOC_RTCIO_INPUT_OUTPUT_SUPPORTED #include +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 2, 0) +#define RTCIO_LL_FUNC_RTC RTCIO_FUNC_RTC +#define RTCIO_LL_FUNC_DIGITAL RTCIO_FUNC_DIGITAL +#endif #endif void pinMode(uint16_t pin, uint8_t mode) @@ -38,7 +42,7 @@ void pinMode(uint16_t pin, uint8_t mode) return; // Already in ADC mode } - rtcio_ll_function_select(rtc, RTCIO_FUNC_RTC); + rtcio_ll_function_select(rtc, RTCIO_LL_FUNC_RTC); rtcio_ll_input_disable(rtc); rtcio_ll_output_disable(rtc); rtcio_ll_pullup_disable(rtc); @@ -48,7 +52,7 @@ void pinMode(uint16_t pin, uint8_t mode) } if(rtc >= 0) { - rtcio_ll_function_select(rtc, RTCIO_FUNC_DIGITAL); + rtcio_ll_function_select(rtc, RTCIO_LL_FUNC_DIGITAL); rtcio_ll_pulldown_disable(rtc); if(mode == INPUT_PULLUP) { rtcio_ll_pullup_enable(rtc); diff --git a/Sming/Arch/Esp32/README.rst b/Sming/Arch/Esp32/README.rst index bf575ce212..9de7952ad8 100644 --- a/Sming/Arch/Esp32/README.rst +++ b/Sming/Arch/Esp32/README.rst @@ -28,12 +28,12 @@ Build variables Requirements ------------ -In order to be able to compile for the ESP32 architecture you should have ESP-IDF v4.3 installed. -Some slight changes are required to enable code to compile correctly for C++, -so a fork has been created here https://github.com/mikee47/esp-idf/tree/sming/release/v4.3 -which you may clone. +Sming requires a slightly modified version of the Espressif SDK. +You can find the SDK here https://github.com/mikee47/esp-idf/tree/sming/release/v5.2. +See `idf_versions`_ below for further details. -The Sming installers do all this for you - see :doc:`/getting-started/index`. +Using the Sming installation scripts are the recommended way to install these SDK versions. +See :doc:`/getting-started/index`. You can find further details in the `ESP-IDF documentation `__. @@ -101,14 +101,16 @@ Each variant uses a different build directory, e.g. ``out/Esp32/esp32c3/...`` to See :component-esp32:`esp32` for further details. +.. _idf_versions: + IDF versions ------------ -Sming currently supports IDF versions 4.3, 4.4 and 5.0. +Sming currently supports IDF versions 4.3, 4.4, 5.0 and 5.2. The default installed IDF version is 4.4. This can be changed as follows:: - INSTALL_IDF_VER=5.0 $SMING_HOME/../Tools/install.sh esp32 + INSTALL_IDF_VER=5.2 $SMING_HOME/../Tools/install.sh esp32 The installation script creates a soft-link in ``/opt/esp-idf`` pointing to the last version installed. Use the `IDF_PATH` environment variable or change the soft-link to select which one to use. diff --git a/Sming/Arch/Esp32/Tools/ci/build.run.cmd b/Sming/Arch/Esp32/Tools/ci/build.run.cmd index 78de751535..b1d9c8d08e 100644 --- a/Sming/Arch/Esp32/Tools/ci/build.run.cmd +++ b/Sming/Arch/Esp32/Tools/ci/build.run.cmd @@ -1,7 +1,7 @@ REM Esp32 build.run.cmd %MAKE_PARALLEL% Basic_Blink Basic_Ethernet Basic_WiFi HttpServer_ConfigNetwork DEBUG_VERBOSE_LEVEL=3 STRICT=1 || goto :error -%MAKE_PARALLEL% Basic_Ssl ENABLE_SSL=Bearssl DEBUG_VERBOSE_LEVEL=3 STRICT=1 || goto :error +%MAKE_PARALLEL% Basic_Ssl ENABLE_SSL=Axtls DEBUG_VERBOSE_LEVEL=3 STRICT=1 || goto :error REM make sure that the Ota Library sample compiles for ESP32 %MAKE_PARALLEL% -C %SMING_HOME%\Libraries\OtaUpgradeMqtt\samples\Upgrade diff --git a/Sming/Arch/Esp32/Tools/idf_tools.py b/Sming/Arch/Esp32/Tools/idf_tools.py index 8001e98652..38837cd032 100644 --- a/Sming/Arch/Esp32/Tools/idf_tools.py +++ b/Sming/Arch/Esp32/Tools/idf_tools.py @@ -9,19 +9,23 @@ def get_tool_info(soc): compiler_prefix = None compiler_version = None + gcc_path = gdb_path = None for tool in tools_info['tools']: - if soc in tool['supported_targets']: - desc = tool['description'] - if desc.startswith('Toolchain'): - compiler_prefix = tool['name'] - compiler_version = tool['versions'][0]['name'] - break - print(f"{compiler_prefix} {compiler_version}") - - # ESP32_COMPILER_PREFIX=compiler_prefix - # IDF_TARGET_ARCH_RISCV=is_riscv - - + if tool.get('install') != 'always': + continue + if soc not in tool['supported_targets']: + continue + desc = tool['description'].lower() + export_path = "/".join(tool['export_paths'][0]) + tool_name = tool['name'] + tool_version = tool['versions'][0]['name'] + path = f"{tool_name}/{tool_version}/{export_path}" + if 'gcc' in desc and not gcc_path: + gcc_path = path + if 'gdb' in desc and not gdb_path: + gdb_path = path + + print(gcc_path, gdb_path) if __name__ == '__main__': diff --git a/Sming/Arch/Esp32/Tools/install.cmd b/Sming/Arch/Esp32/Tools/install.cmd index 9f0122949b..cba5358357 100644 --- a/Sming/Arch/Esp32/Tools/install.cmd +++ b/Sming/Arch/Esp32/Tools/install.cmd @@ -16,4 +16,9 @@ set IDF_REQUIREMENTS="%IDF_PATH%\requirements.txt" if not exist "%IDF_REQUIREMENTS%" set IDF_REQUIREMENTS="%IDF_PATH%\tools\requirements\requirements.core.txt" python -m pip install -r "%IDF_REQUIREMENTS%" -if "%INSTALL_IDF_VER%"=="5.0" python "%IDF_PATH%\tools\idf_tools.py" --non-interactive install-python-env +if "%INSTALL_IDF_VER%" == "5.0" goto :install_python +if "%INSTALL_IDF_VER%" == "5.2" goto :install_python +goto :EOF + +:install_python +python "%IDF_PATH%\tools\idf_tools.py" --non-interactive install-python-env diff --git a/Sming/Arch/Esp32/build.mk b/Sming/Arch/Esp32/build.mk index 32d9b6b38a..2c615737e9 100644 --- a/Sming/Arch/Esp32/build.mk +++ b/Sming/Arch/Esp32/build.mk @@ -10,6 +10,12 @@ endif export IDF_PATH := $(call FixPath,$(IDF_PATH)) +# Extract IDF version +ifndef IDF_VER +IDF_VER := $(shell (cd $$IDF_PATH && git describe --always --tags --dirty) | cut -c 1-31) +endif +IDF_VERSION := $(firstword $(subst -, ,$(IDF_VER))) + # By default, downloaded tools will be installed under $HOME/.espressif directory # (%USERPROFILE%/.espressif on Windows). This path can be modified by setting # IDF_TOOLS_PATH variable prior to running this tool. @@ -27,40 +33,40 @@ ESP_VARIANT := $(SMING_SOC) export ESP_VARIANT IDF_TOOL_INFO := $(shell $(PYTHON) $(ARCH_TOOLS)/idf_tools.py $(SMING_SOC)) -ESP32_COMPILER_PREFIX := $(word 1,$(IDF_TOOL_INFO)) -ESP32_COMPILER_VERSION := $(word 2,$(IDF_TOOL_INFO)) -IDF_TARGET_ARCH_RISCV := $(findstring riscv,$(ESP32_COMPILER_PREFIX)) +ESP32_GCC_PATH := $(word 1,$(IDF_TOOL_INFO)) +ESP32_GDB_PATH := $(word 2,$(IDF_TOOL_INFO)) +ESP32_COMPILER_PATH := $(IDF_TOOLS_PATH)/tools/$(ESP32_GCC_PATH) +ifneq (,$(filter xtensa%,$(ESP32_GCC_PATH))) +ESP32_COMPILER_PREFIX := xtensa-$(ESP_VARIANT)-elf +else +ESP32_COMPILER_PREFIX := riscv32-esp-elf +IDF_TARGET_ARCH_RISCV := 1 +endif -# $1 => Root directory -# $2 => Sub-directory +# $1 => Tool sub-path/name define FindTool -$(lastword $(sort $(wildcard $(IDF_TOOLS_PATH)/$1/*))$2) +$(lastword $(sort $(wildcard $(IDF_TOOLS_PATH)/$1*))) endef DEBUG_VARS += ESP32_COMPILER_PATH ESP32_ULP_PATH ESP32_OPENOCD_PATH ESP32_PYTHON_PATH -ifndef ESP32_COMPILER_PATH -ESP32_COMPILER_PATH := $(IDF_TOOLS_PATH)/tools/$(ESP32_COMPILER_PREFIX)/$(ESP32_COMPILER_VERSION)/$(ESP32_COMPILER_PREFIX) -endif - ifndef ESP32_ULP_PATH -ESP32_ULP_PATH := $(call FindTool,tools/$(ESP_VARIANT)ulp-elf) +ESP32_ULP_PATH := $(call FindTool,tools/$(ESP_VARIANT)ulp-elf/) endif ifndef ESP32_OPENOCD_PATH -ESP32_OPENOCD_PATH := $(call FindTool,tools/openocd-esp32) +ESP32_OPENOCD_PATH := $(call FindTool,tools/openocd-esp32/) endif ifndef ESP32_PYTHON_PATH -ESP32_PYTHON_PATH := $(call FindTool,python_env) -ifneq (,$(wildcard $(ESP32_PYTHON_PATH)/bin)) -ESP32_PYTHON_PATH := $(ESP32_PYTHON_PATH)/bin -else ifneq (,$(wildcard $(ESP32_PYTHON_PATH)/Scripts)) -ESP32_PYTHON_PATH := $(ESP32_PYTHON_PATH)/Scripts +ESP32_PYTHON_PATH := $(call FindTool,python_env/idf$(subst v,,$(IDF_VERSION))) +ifndef ESP32_PYTHON_PATH +ESP32_PYTHON_PATH := $(dir $(PYTHON)) +else ifeq ($(UNAME),Windows) +ESP32_PYTHON := $(ESP32_PYTHON_PATH)/Scripts/python else -$(error Failed to find ESP32 Python installation) +ESP32_PYTHON := $(ESP32_PYTHON_PATH)/bin/python endif -ESP32_PYTHON = $(ESP32_PYTHON_PATH)/python endif # Required by v4.2 SDK @@ -69,7 +75,7 @@ export IDF_PYTHON_ENV_PATH=$(ESP32_PYTHON_PATH) # Add ESP-IDF tools to PATH IDF_PATH_LIST := \ $(IDF_PATH)/tools \ - $(ESP32_COMPILER_PATH)/bin \ + $(ESP32_COMPILER_PATH) \ $(ESP32_ULP_PATH)/$(ESP_VARIANT)ulp-elf-binutils/bin \ $(ESP32_OPENOCD_PATH)/openocd-esp32/bin \ $(ESP32_PYTHON_PATH)/bin \ @@ -80,7 +86,7 @@ IDF_PATH_LIST := \ ifeq ($(UNAME),Windows) DEBUG_VARS += ESP32_NINJA_PATH ifndef ESP32_NINJA_PATH -ESP32_NINJA_PATH := $(call FindTool,tools/ninja) +ESP32_NINJA_PATH := $(call FindTool,tools/ninja/) endif ifeq (,$(wildcard $(ESP32_NINJA_PATH)/ninja.exe)) $(error Failed to find NINJA) @@ -89,7 +95,7 @@ IDF_PATH_LIST += $(ESP32_NINJA_PATH) DEBUG_VARS += ESP32_IDFEXE_PATH ifndef ESP32_IDFEXE_PATH -ESP32_IDFEXE_PATH := $(call FindTool,tools/idf-exe) +ESP32_IDFEXE_PATH := $(call FindTool,tools/idf-exe/) endif IDF_PATH_LIST += $(ESP32_IDFEXE_PATH) endif @@ -102,7 +108,7 @@ space:= $(empty) $(empty) export PATH := $(subst $(space),:,$(IDF_PATH_LIST)):$(PATH) -TOOLSPEC := $(ESP32_COMPILER_PATH)/bin/$(ESP32_COMPILER_PREFIX) +TOOLSPEC := $(ESP32_COMPILER_PATH)/$(ESP32_COMPILER_PREFIX) AS := $(TOOLSPEC)-gcc CC := $(TOOLSPEC)-gcc CXX := $(TOOLSPEC)-g++ @@ -111,11 +117,14 @@ LD := $(TOOLSPEC)-gcc NM := $(TOOLSPEC)-nm OBJCOPY := $(TOOLSPEC)-objcopy OBJDUMP := $(TOOLSPEC)-objdump -GDB := $(TOOLSPEC)-gdb SIZE := $(TOOLSPEC)-size -# Extracting IDF version -IDF_VER := $(shell (cd $$IDF_PATH && git describe --always --tags --dirty) | cut -c 1-31) +ifeq (None,$(ESP32_GDB_PATH)) +GDB := $(TOOLSPEC)-gdb +else +GDB := $(IDF_TOOLS_PATH)/tools/$(ESP32_GDB_PATH)/$(ESP32_COMPILER_PREFIX)-gdb +endif + # [ Sming specific flags ] DEBUG_VARS += IDF_PATH IDF_VER diff --git a/Sming/Libraries/SPI/src/Arch/Esp32/SPI.cpp b/Sming/Libraries/SPI/src/Arch/Esp32/SPI.cpp index e09e6b918a..98e883e903 100644 --- a/Sming/Libraries/SPI/src/Arch/Esp32/SPI.cpp +++ b/Sming/Libraries/SPI/src/Arch/Esp32/SPI.cpp @@ -24,6 +24,11 @@ #include #include +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 2, 0) +#include +#include +#endif + // Use SPI hardware byte ordering so we don't need to do it in software #if defined(SOC_ESP32) || defined(SOC_ESP32S2) #define BYTE_ORDER_SUPPORTED 1 @@ -110,7 +115,11 @@ struct SpiDevice { { spi_ll_set_mosi_bitlen(info.hw, num_bits); spi_ll_set_miso_bitlen(info.hw, num_bits); +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 2, 0) spi_ll_master_user_start(info.hw); +#else + spi_ll_user_start(info.hw); +#endif } void set_mode(SpiMode mode) @@ -182,9 +191,17 @@ struct SpiDevice { */ void checkSpeed(SPISpeed& speed) { + uint32_t clock_source_hz; +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 2, 0) + clock_source_hz = SPI_LL_PERIPH_CLK_FREQ; +#else + esp_clk_tree_src_get_freq_hz(soc_module_clk_t(SPI_CLK_SRC_DEFAULT), ESP_CLK_TREE_SRC_FREQ_PRECISION_APPROX, + &clock_source_hz); +#endif + constexpr int duty_cycle{127}; spi_ll_clock_val_t clock_reg; - unsigned actual_freq = spi_ll_master_cal_clock(SPI_LL_PERIPH_CLK_FREQ, speed.frequency, duty_cycle, &clock_reg); + unsigned actual_freq = spi_ll_master_cal_clock(clock_source_hz, speed.frequency, duty_cycle, &clock_reg); speed.regVal = clock_reg; #ifdef SPI_DEBUG diff --git a/Tools/ci/build.cmd b/Tools/ci/build.cmd index 4efca9ff24..8744dc8fbf 100644 --- a/Tools/ci/build.cmd +++ b/Tools/ci/build.cmd @@ -30,8 +30,11 @@ cd /d %SMING_PROJECTS_DIR%/samples/Basic_Blink make help make list-config +if "%SMING_ARCH%" == "Esp32" set TEST_FLAGS="DISABLE_NETWORK=1" + REM HostTests must build for all architectures -%MAKE_PARALLEL% -C "%SMING_PROJECTS_DIR%/tests/HostTests" || goto :error +REM Building Bear SSL library causes memory issues with CI builds in Windows, so avoid for now +%MAKE_PARALLEL% -C "%SMING_PROJECTS_DIR%/tests/HostTests" %TEST_FLAGS% || goto :error REM Start Arch-specific tests cd /d %SMING_HOME% diff --git a/Tools/ci/setenv.ps1 b/Tools/ci/setenv.ps1 index e3d84ff351..17b6cbf472 100644 --- a/Tools/ci/setenv.ps1 +++ b/Tools/ci/setenv.ps1 @@ -37,7 +37,6 @@ if ($IsWindows) { $env:PATH = "$env:PYTHON_PATH;$env:PYTHON_PATH\Scripts;$env:PATH" $env:PYTHON = "$env:PYTHON_PATH\python" - $env:ESP32_PYTHON_PATH = "$env:PYTHON_PATH" $env:PATH = "$env:PROGRAMFILES\CMake\bin;$env:PATH" diff --git a/Tools/export.sh b/Tools/export.sh index a695329944..473c449568 100755 --- a/Tools/export.sh +++ b/Tools/export.sh @@ -36,7 +36,6 @@ export ESP_HOME=${ESP_HOME:=/opt/esp-quick-toolchain} # Esp32 export IDF_PATH=${IDF_PATH:=/opt/esp-idf} export IDF_TOOLS_PATH=${IDF_TOOLS_PATH:=/opt/esp32} -export ESP32_PYTHON_PATH=${ESP32_PYTHON_PATH:=/usr/bin} # Rp2040 export PICO_TOOLCHAIN_PATH=${PICO_TOOLCHAIN_PATH:=/opt/rp2040} diff --git a/tests/HostTests/component.mk b/tests/HostTests/component.mk index 3fd335e767..17a42e1746 100644 --- a/tests/HostTests/component.mk +++ b/tests/HostTests/component.mk @@ -14,12 +14,6 @@ COMPONENT_SRCDIRS := \ modules \ Arch/$(SMING_ARCH) -ifneq ($(DISABLE_NETWORK),1) -COMPONENT_SRCDIRS += \ - modules/Network \ - modules/Network/Arch/$(SMING_ARCH) -endif - ARDUINO_LIBRARIES := \ SmingTest \ ArduinoJson5 \ @@ -30,9 +24,16 @@ ifeq ($(SMING_ARCH),Host) endif COMPONENT_DEPENDS := \ - malloc_count \ + malloc_count + +ifneq ($(DISABLE_NETWORK),1) +COMPONENT_SRCDIRS += \ + modules/Network \ + modules/Network/Arch/$(SMING_ARCH) +COMPONENT_DEPENDS += \ axtls-8266 \ bearssl-esp8266 +endif ifeq ($(UNAME),Windows) # Network tests run on Linux only diff --git a/tests/HostTests/include/modules.h b/tests/HostTests/include/modules.h index dd302fb86b..d29f54e112 100644 --- a/tests/HostTests/include/modules.h +++ b/tests/HostTests/include/modules.h @@ -25,7 +25,7 @@ XX(String) \ XX(ArduinoString) \ XX(Wiring) \ - XX(Crypto) \ + XX_NET(Crypto) \ XX(CStringArray) \ XX(Stream) \ XX(TemplateStream) \ diff --git a/tests/HostTests/modules/Crypto.cpp b/tests/HostTests/modules/Network/Crypto.cpp similarity index 100% rename from tests/HostTests/modules/Crypto.cpp rename to tests/HostTests/modules/Network/Crypto.cpp diff --git a/tests/HostTests/modules/Crypto/AxHash.h b/tests/HostTests/modules/Network/Crypto/AxHash.h similarity index 100% rename from tests/HostTests/modules/Crypto/AxHash.h rename to tests/HostTests/modules/Network/Crypto/AxHash.h diff --git a/tests/HostTests/modules/Crypto/BrHash.h b/tests/HostTests/modules/Network/Crypto/BrHash.h similarity index 100% rename from tests/HostTests/modules/Crypto/BrHash.h rename to tests/HostTests/modules/Network/Crypto/BrHash.h diff --git a/tests/HostTests/modules/Crypto/EspHash.h b/tests/HostTests/modules/Network/Crypto/EspHash.h similarity index 100% rename from tests/HostTests/modules/Crypto/EspHash.h rename to tests/HostTests/modules/Network/Crypto/EspHash.h From 22f8c1a0ae21022a488b4646263ea9f97fb0a63d Mon Sep 17 00:00:00 2001 From: Mike Date: Tue, 9 Jan 2024 12:55:58 +0000 Subject: [PATCH 004/128] Fix esp32c3 FPU issues (#2700) * Add various linker definitions From inspection of IDF makelists * Fix floating point issues with esp32c3 Missing compiler and link flags Probably since 5.0 --------- Co-authored-by: mikee47 --- Sming/Arch/Esp32/Components/esp32/sdk/pthread.mk | 5 ++++- Sming/Arch/Esp32/Components/esp32/sdk/soc.mk | 8 ++++++++ Sming/Arch/Esp32/app.mk | 6 ++++++ Sming/Arch/Esp32/build.mk | 11 ++++++++++- 4 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 Sming/Arch/Esp32/Components/esp32/sdk/soc.mk diff --git a/Sming/Arch/Esp32/Components/esp32/sdk/pthread.mk b/Sming/Arch/Esp32/Components/esp32/sdk/pthread.mk index c24e62cb1e..77f07fc36a 100644 --- a/Sming/Arch/Esp32/Components/esp32/sdk/pthread.mk +++ b/Sming/Arch/Esp32/Components/esp32/sdk/pthread.mk @@ -4,7 +4,10 @@ SDK_UNDEF_SYMBOLS += \ pthread_include_pthread_impl \ pthread_include_pthread_cond_impl \ - pthread_include_pthread_local_storage_impl + pthread_include_pthread_cond_var_impl \ + pthread_include_pthread_local_storage_impl \ + pthread_include_pthread_rwlock_impl \ + pthread_include_pthread_semaphore_impl ifdef CONFIG_FREERTOS_ENABLE_STATIC_TASK_CLEAN_UP SDK_WRAP_SYMBOLS += vPortCleanUpTCB diff --git a/Sming/Arch/Esp32/Components/esp32/sdk/soc.mk b/Sming/Arch/Esp32/Components/esp32/sdk/soc.mk new file mode 100644 index 0000000000..276e5f6138 --- /dev/null +++ b/Sming/Arch/Esp32/Components/esp32/sdk/soc.mk @@ -0,0 +1,8 @@ +# +# soc +# + +ifeq (esp32,$(SMING_SOC)) + SDK_UNDEF_SYMBOLS += \ + esp_dport_access_reg_read +endif diff --git a/Sming/Arch/Esp32/app.mk b/Sming/Arch/Esp32/app.mk index 42271785d1..8fc7a3c151 100644 --- a/Sming/Arch/Esp32/app.mk +++ b/Sming/Arch/Esp32/app.mk @@ -9,6 +9,12 @@ LDFLAGS += \ -nostdlib \ -Wl,-static +ifdef IDF_TARGET_ARCH_RISCV + LDFLAGS += \ + -nostartfiles \ + -march=$(ESP32_RISCV_ARCH) \ + --specs=nosys.specs +endif .PHONY: application application: $(TARGET_BIN) diff --git a/Sming/Arch/Esp32/build.mk b/Sming/Arch/Esp32/build.mk index 2c615737e9..4db95bf8c7 100644 --- a/Sming/Arch/Esp32/build.mk +++ b/Sming/Arch/Esp32/build.mk @@ -41,6 +41,12 @@ ESP32_COMPILER_PREFIX := xtensa-$(ESP_VARIANT)-elf else ESP32_COMPILER_PREFIX := riscv32-esp-elf IDF_TARGET_ARCH_RISCV := 1 +# This is important as no hardware FPU is available on these SOCs +ifeq ($(IDF_VERSION),v5.2) +ESP32_RISCV_ARCH := rv32imc_zicsr_zifencei +else +ESP32_RISCV_ARCH := rv32imc +endif endif # $1 => Tool sub-path/name @@ -153,7 +159,10 @@ export PROJECT_VER # Flags which control code generation and dependency generation, both for C and C++ CPPFLAGS += -Wno-frame-address -ifndef IDF_TARGET_ARCH_RISCV +ifdef IDF_TARGET_ARCH_RISCV +CPPFLAGS += \ + -march=$(ESP32_RISCV_ARCH) +else CPPFLAGS += \ -mlongcalls \ -mtext-section-literals From d87ee6cad8d5d10823a1e724891a7b8f8ead4d71 Mon Sep 17 00:00:00 2001 From: Mike Date: Tue, 9 Jan 2024 13:27:11 +0000 Subject: [PATCH 005/128] Only esp8266 requires special handling for _F() (#2699) --- Sming/Wiring/FakePgmSpace.h | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Sming/Wiring/FakePgmSpace.h b/Sming/Wiring/FakePgmSpace.h index 3c34e8f43a..d8c68604cf 100644 --- a/Sming/Wiring/FakePgmSpace.h +++ b/Sming/Wiring/FakePgmSpace.h @@ -75,9 +75,7 @@ extern "C" { &__pstr__[0]; \ })) -#ifdef ARCH_HOST -#define _F(str) (str) -#else +#ifdef ARCH_ESP8266 /** * @brief Declare and use a flash string inline. * @param str @@ -89,7 +87,8 @@ extern "C" { LOAD_PSTR(buf, __pstr__); \ buf; \ })) - +#else +#define _F(str) (str) #endif /** From 5b9c2a7e472313bed6638c262448c45934a44492 Mon Sep 17 00:00:00 2001 From: Mike Date: Tue, 9 Jan 2024 13:27:33 +0000 Subject: [PATCH 006/128] Fix hw_timer1_read() (#2701) SDK >= 5 requies additional call --- Sming/Arch/Esp32/Components/driver/hw_timer.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sming/Arch/Esp32/Components/driver/hw_timer.cpp b/Sming/Arch/Esp32/Components/driver/hw_timer.cpp index 8ab470c5b0..5106e59c55 100644 --- a/Sming/Arch/Esp32/Components/driver/hw_timer.cpp +++ b/Sming/Arch/Esp32/Components/driver/hw_timer.cpp @@ -143,6 +143,7 @@ class TimerConfig timer_ll_get_counter_value(dev, index, &val); return val; #else + timer_ll_trigger_soft_capture(dev, index); return timer_ll_get_counter_value(dev, index); #endif } @@ -214,7 +215,7 @@ void IRAM_ATTR hw_timer1_disable(void) timer.enable_counter(false); } -uint32_t hw_timer1_read(void) +uint32_t IRAM_ATTR hw_timer1_read(void) { return timer.get_counter_value(); } From 49c9aa54e1bc3032acba8c1ce1c7f5528261ea9a Mon Sep 17 00:00:00 2001 From: Mike Date: Wed, 10 Jan 2024 08:55:28 +0000 Subject: [PATCH 007/128] Fix build error for IDF 5.0 (#2703) Re. #2701 call to `timer_ll_trigger_soft_capture` only required for IDF 5.2, as while function signature changed in IDF 5.0 it retains previous behaviour. Co-authored-by: mikee47 --- Sming/Arch/Esp32/Components/driver/hw_timer.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sming/Arch/Esp32/Components/driver/hw_timer.cpp b/Sming/Arch/Esp32/Components/driver/hw_timer.cpp index 5106e59c55..84254b7cc0 100644 --- a/Sming/Arch/Esp32/Components/driver/hw_timer.cpp +++ b/Sming/Arch/Esp32/Components/driver/hw_timer.cpp @@ -143,7 +143,9 @@ class TimerConfig timer_ll_get_counter_value(dev, index, &val); return val; #else +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 2, 0) timer_ll_trigger_soft_capture(dev, index); +#endif return timer_ll_get_counter_value(dev, index); #endif } From 2233b3dd0195395719ee43ac5e85cf4fd1e4bf43 Mon Sep 17 00:00:00 2001 From: Mike Date: Wed, 10 Jan 2024 11:55:47 +0000 Subject: [PATCH 008/128] Fix esp8266 'no network' build of HostTests (#2702) Implementations of `system_get_sdk_version()` and `system_get_chip_id()` required. --- .../Esp8266/Components/esp_no_wifi/component.mk | 6 +++++- .../Esp8266/Components/esp_no_wifi/user_interface.c | 13 +++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/Sming/Arch/Esp8266/Components/esp_no_wifi/component.mk b/Sming/Arch/Esp8266/Components/esp_no_wifi/component.mk index 3a7b21c311..b545090127 100644 --- a/Sming/Arch/Esp8266/Components/esp_no_wifi/component.mk +++ b/Sming/Arch/Esp8266/Components/esp_no_wifi/component.mk @@ -23,6 +23,7 @@ EXTRA_OBJ := extra.o $(COMPONENT_RULE)$(EXTRA_OBJ): sdk/user_interface.o $(Q) $(OBJCOPY) \ -j *UND* \ + -j .sdk.version.* \ $(addprefix -j .text.,$(NOWIFI_SYMS)) \ $(foreach f,$(NOWIFI_SYMS),--rename-section .text.$f=.iram.text) \ $< $@ @@ -40,7 +41,10 @@ endef LIBMAIN_COMMANDS += $(NOWIFI_LIBMAIN_COMMANDS) LIBDIRS += $(COMPONENT_PATH) -EXTRA_LDFLAGS := -Tno.wifi.ld -u call_user_start_local +EXTRA_LDFLAGS := \ + -Tno.wifi.ld \ + -u call_user_start_local \ + -u SDK_VERSION ## diff --git a/Sming/Arch/Esp8266/Components/esp_no_wifi/user_interface.c b/Sming/Arch/Esp8266/Components/esp_no_wifi/user_interface.c index 8bbf3e0e2f..e5cf764c8d 100644 --- a/Sming/Arch/Esp8266/Components/esp_no_wifi/user_interface.c +++ b/Sming/Arch/Esp8266/Components/esp_no_wifi/user_interface.c @@ -1,6 +1,7 @@ #include #include #include +#include bool protect_flag; bool timer2_ms_flag; @@ -337,3 +338,15 @@ int os_printf_plus(const char* format, ...) return n; } + +const char* system_get_sdk_version() +{ + extern char SDK_VERSION[]; + return SDK_VERSION; +} + +uint32_t system_get_chip_id() +{ + // from chip_id() method in esptool.py + return (MAC0 >> 24) | ((MAC1 & 0x00ffffff) << 8); +} From a70ab7dd9184c12b904735d509d13eeccd8d827b Mon Sep 17 00:00:00 2001 From: slaff Date: Wed, 10 Jan 2024 14:21:59 +0100 Subject: [PATCH 009/128] Update requirements.txt (#2704) In our CI system sometimes the installed protoc version on the system is incompatible with the installed python-protobuf version. This fix, recommended here: https://groups.google.com/g/nanopb/c/YJn3DlY20_g, should fix the issue. --- Sming/Libraries/nanopb/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/Sming/Libraries/nanopb/requirements.txt b/Sming/Libraries/nanopb/requirements.txt index b0c79cc0ec..5f2eff8594 100644 --- a/Sming/Libraries/nanopb/requirements.txt +++ b/Sming/Libraries/nanopb/requirements.txt @@ -1 +1,2 @@ +grpcio-tools protobuf From 28c7dcdb3d2a9b5ce0a7abf52abfe815f39a3583 Mon Sep 17 00:00:00 2001 From: Mike Date: Thu, 11 Jan 2024 08:29:12 +0000 Subject: [PATCH 010/128] Fix compiler warnings (#2706) * Simplify `printf_P` for architectures without restricted flash access * Fix various compiler warnings * Fix failing checks on std::ratio_divide GCC 13.2.1 has additional checks for ratio parameters. Easy fix now as we're no longer using old compilers. * Don't fail clock check as Host timers cannot be guaranteed jitter-free --- Sming/Components/Network/src/Network/MqttClient.h | 2 ++ Sming/Core/NanoTime.h | 8 +------- Sming/Libraries/OneWire/OneWire.cpp | 10 +++++----- Sming/Libraries/OneWire/OneWire.h | 10 +++++----- Sming/Wiring/FakePgmSpace.h | 8 +++++++- samples/Basic_Utility/app/utility.cpp | 6 +++--- samples/HttpServer_AJAX/app/application.cpp | 2 +- samples/HttpServer_WebSockets/app/application.cpp | 2 +- samples/UdpServer_mDNS/app/application.cpp | 2 +- tests/HostTests/modules/Clocks.cpp | 2 ++ .../HostTests/modules/Network/Arch/Host/TcpClient.cpp | 2 +- 11 files changed, 29 insertions(+), 25 deletions(-) diff --git a/Sming/Components/Network/src/Network/MqttClient.h b/Sming/Components/Network/src/Network/MqttClient.h index 4797229f03..0f82ea19ee 100644 --- a/Sming/Components/Network/src/Network/MqttClient.h +++ b/Sming/Components/Network/src/Network/MqttClient.h @@ -217,6 +217,8 @@ class MqttClient : protected TcpClient void onFinished(TcpClientState finishState) override; private: + using TcpClient::connect; // Keep compiler happy but prevent access to base method by clients + // TCP methods virtual bool onTcpReceive(TcpClient& client, char* data, int size); diff --git a/Sming/Core/NanoTime.h b/Sming/Core/NanoTime.h index 90089b4307..c0e1d67744 100644 --- a/Sming/Core/NanoTime.h +++ b/Sming/Core/NanoTime.h @@ -107,14 +107,8 @@ constexpr BasicRatio32 unitTicks[UnitMax + 1] = { * @brief Class template to define tick std::ratio type * @tparam unit * @retval std::ratio Ticks per second - * @note This would be preferable: - * `template using UnitTickRatio = std::ratio;` - * But GCC 4.8 doesn't like it (lvalue required as unary '&' operand) */ -template struct UnitTickRatio { - static constexpr uint64_t num = unitTicks[unit].num; - static constexpr uint64_t den = unitTicks[unit].den; -}; +template using UnitTickRatio = std::ratio; // Forward declarations template struct TimeConst; diff --git a/Sming/Libraries/OneWire/OneWire.cpp b/Sming/Libraries/OneWire/OneWire.cpp index cbff70d7c0..509682b6d4 100644 --- a/Sming/Libraries/OneWire/OneWire.cpp +++ b/Sming/Libraries/OneWire/OneWire.cpp @@ -151,10 +151,10 @@ void OneWire::begin(uint8_t pinOneWire) // // Returns 1 if a device asserted a presence pulse, 0 otherwise. // -uint8_t OneWire::reset(void) +uint8_t OneWire::reset() { IO_REG_TYPE mask = bitmask; - volatile IO_REG_TYPE *reg IO_REG_ASM = baseReg; + SMING_UNUSED volatile IO_REG_TYPE *reg IO_REG_ASM = baseReg; uint8_t r; uint8_t retries = 125; @@ -188,7 +188,7 @@ uint8_t OneWire::reset(void) void OneWire::write_bit(uint8_t v) { IO_REG_TYPE mask=bitmask; - volatile IO_REG_TYPE *reg IO_REG_ASM = baseReg; + SMING_UNUSED volatile IO_REG_TYPE *reg IO_REG_ASM = baseReg; if (v & 1) { noInterrupts(); @@ -213,10 +213,10 @@ void OneWire::write_bit(uint8_t v) // Read a bit. Port and bit is used to cut lookup time and provide // more certain timing. // -uint8_t OneWire::read_bit(void) +uint8_t OneWire::read_bit() { IO_REG_TYPE mask=bitmask; - volatile IO_REG_TYPE *reg IO_REG_ASM = baseReg; + SMING_UNUSED volatile IO_REG_TYPE *reg IO_REG_ASM = baseReg; uint8_t r; noInterrupts(); diff --git a/Sming/Libraries/OneWire/OneWire.h b/Sming/Libraries/OneWire/OneWire.h index d5ea8df4d3..80ca49e840 100644 --- a/Sming/Libraries/OneWire/OneWire.h +++ b/Sming/Libraries/OneWire/OneWire.h @@ -142,13 +142,13 @@ class OneWire // Perform a 1-Wire reset cycle. Returns 1 if a device responds // with a presence pulse. Returns 0 if there is no device or the // bus is shorted or otherwise held low for more than 250uS - uint8_t reset(void); + uint8_t reset(); // Issue a 1-Wire rom select command, you do the reset first. void select(const uint8_t rom[8]); // Issue a 1-Wire rom skip command, to address all on bus. - void skip(void); + void skip(); // Write a byte. If 'power' is one then the wire is held high at // the end for parasitically powered devices. You are responsible @@ -159,7 +159,7 @@ class OneWire void write_bytes(const uint8_t *buf, uint16_t count, bool power = 0); // Read a byte. - uint8_t read(void); + uint8_t read(); void read_bytes(uint8_t *buf, uint16_t count); @@ -168,14 +168,14 @@ class OneWire void write_bit(uint8_t v); // Read a bit. - uint8_t read_bit(void); + uint8_t read_bit(); // Stop forcing power onto the bus. You only need to do this if // you used the 'power' flag to write() or used a write_bit() call // and aren't about to do another read or write. You would rather // not leave this powered if you don't have to, just in case // someone shorts your bus. - void depower(void); + void depower(); #if ONEWIRE_SEARCH // Clear the search state so that if will start from the beginning again. diff --git a/Sming/Wiring/FakePgmSpace.h b/Sming/Wiring/FakePgmSpace.h index d8c68604cf..65b2a231a8 100644 --- a/Sming/Wiring/FakePgmSpace.h +++ b/Sming/Wiring/FakePgmSpace.h @@ -60,7 +60,13 @@ extern "C" { m_printf(__localF, ##__VA_ARGS__); \ })) -#define printf_P printf_P_stack +#ifdef ARCH_ESP8266 +// ESP8266 requires byte-aligned flash accesses for format string... +#define printf_P(fmt, ...) printf_P_stack(fmt, ##__VA_ARGS__) +#else +// ... but other architectures do not +#define printf_P(fmt, ...) m_printf(fmt, ##__VA_ARGS__) +#endif /** * @brief Define and use a counted flash string inline diff --git a/samples/Basic_Utility/app/utility.cpp b/samples/Basic_Utility/app/utility.cpp index 4a60753e4c..58fac25f67 100644 --- a/samples/Basic_Utility/app/utility.cpp +++ b/samples/Basic_Utility/app/utility.cpp @@ -28,7 +28,7 @@ void testWebConstants() contentType = "(NOT FOUND)"; } MimeType mimeType = ContentType::fromString(contentType); - m_printf(" %u %s: %s (#%u)\n", i, ext, contentType.c_str(), mimeType); + m_printf(" %u %s: %s (#%u)\n", i, ext, contentType.c_str(), unsigned(mimeType)); } } @@ -53,8 +53,8 @@ void init() " make run HOST_PARAMETERS='command=%s'\n", String(Command::testWebConstants).c_str()); } else { - m_printf("Command-line parameters:\n", parameters.count()); - for(int i = 0; i < parameters.count(); ++i) { + m_printf("Command-line parameters: %u\n", parameters.count()); + for(unsigned i = 0; i < parameters.count(); ++i) { auto param = parameters[i]; m_printf(" %u: text = '%s'\n", i, param.text); m_printf(" name = '%s'\n", param.getName().c_str()); diff --git a/samples/HttpServer_AJAX/app/application.cpp b/samples/HttpServer_AJAX/app/application.cpp index fda5a63e4c..ccd3595381 100644 --- a/samples/HttpServer_AJAX/app/application.cpp +++ b/samples/HttpServer_AJAX/app/application.cpp @@ -17,7 +17,7 @@ const int countInputs = sizeof(inputs) / sizeof(inputs[0]); void onIndex(HttpRequest& request, HttpResponse& response) { TemplateFileStream* tmpl = new TemplateFileStream("index.html"); - auto& vars = tmpl->variables(); + //auto& vars = tmpl->variables(); //vars["counter"] = String(counter); response.sendNamedStream(tmpl); // this template object will be deleted automatically } diff --git a/samples/HttpServer_WebSockets/app/application.cpp b/samples/HttpServer_WebSockets/app/application.cpp index 193966e636..b0cc2d3500 100644 --- a/samples/HttpServer_WebSockets/app/application.cpp +++ b/samples/HttpServer_WebSockets/app/application.cpp @@ -21,7 +21,7 @@ CUserData userGeorge("George", "I like SMING"); void onIndex(HttpRequest& request, HttpResponse& response) { auto tmpl = new TemplateFileStream(F("index.html")); - auto& vars = tmpl->variables(); + //auto& vars = tmpl->variables(); //vars["counter"] = String(counter); response.sendNamedStream(tmpl); // this template object will be deleted automatically } diff --git a/samples/UdpServer_mDNS/app/application.cpp b/samples/UdpServer_mDNS/app/application.cpp index 15638491f0..f9497b0c2c 100644 --- a/samples/UdpServer_mDNS/app/application.cpp +++ b/samples/UdpServer_mDNS/app/application.cpp @@ -71,7 +71,7 @@ void test() auto submit = [&](const String& name, ResourceType type) { Query query; - auto question = query.addQuestion(name, type); + query.addQuestion(name, type); printMessage(Serial, query); responder.onMessage(query); }; diff --git a/tests/HostTests/modules/Clocks.cpp b/tests/HostTests/modules/Clocks.cpp index fe9803b767..0b15d025ea 100644 --- a/tests/HostTests/modules/Clocks.cpp +++ b/tests/HostTests/modules/Clocks.cpp @@ -53,10 +53,12 @@ template class ClockTestTemplate : public TestG debug_w("Ratio: x %f", float(elapsedTicks) / (time - startTime)); uint32_t us = Micros::ticksToTime(elapsedTicks); debug_w("Apparent time: %u", us); +#ifndef ARCH_HOST // Up-timers may report 0 if inactive if(endTicks != 0 || startTicks != 0) { REQUIRE(abs(int(us - duration)) < 500); // Allow some latitude } +#endif } } diff --git a/tests/HostTests/modules/Network/Arch/Host/TcpClient.cpp b/tests/HostTests/modules/Network/Arch/Host/TcpClient.cpp index 0a9a9383ef..42d9963ada 100644 --- a/tests/HostTests/modules/Network/Arch/Host/TcpClient.cpp +++ b/tests/HostTests/modules/Network/Arch/Host/TcpClient.cpp @@ -43,7 +43,7 @@ class TcpClientTest : public TestGroup server->setKeepAlive(USHRT_MAX); // disable connection timeout // Tcp Client - bool connected = client.connect(WifiStation.getIP(), port); + SMING_UNUSED bool connected = client.connect(WifiStation.getIP(), port); debug_d("Connected: %d", connected); TEST_CASE("TcpClient::send stream") From eba0793f706faf9a4a2be70fc4cf01e77f1618b7 Mon Sep 17 00:00:00 2001 From: Mike Date: Thu, 11 Jan 2024 08:31:11 +0000 Subject: [PATCH 011/128] Provide better implementation for `FileStream::id()` (#2705) --- Sming/Core/Data/Stream/IFS/FileStream.cpp | 27 ++++++++++++++++++----- tests/HostTests/modules/Files.cpp | 1 + 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/Sming/Core/Data/Stream/IFS/FileStream.cpp b/Sming/Core/Data/Stream/IFS/FileStream.cpp index a046ad3ef3..9cdf0eb02c 100644 --- a/Sming/Core/Data/Stream/IFS/FileStream.cpp +++ b/Sming/Core/Data/Stream/IFS/FileStream.cpp @@ -10,8 +10,6 @@ #include "FileStream.h" -#define ETAG_SIZE 16 - namespace IFS { void FileStream::attach(FileHandle file, size_t size) @@ -172,10 +170,27 @@ String FileStream::id() const return nullptr; } - char buf[ETAG_SIZE]; - m_snprintf(buf, ETAG_SIZE, _F("00f-%x-%x0-%x"), stat.id, stat.size, stat.name.length); - - return String(buf); + /* + Jan 2024. How ETAGs are generated is not specified. Previous implementation was like this: + + m_snprintf(buf, ETAG_SIZE, _F("00f-%x-%x0-%x"), stat.id, stat.size, stat.name.length); + + Issues are: + - on some architectures compiler warns about differing types %x vs arguments + - name can change without length being affected; if name changes then file lookup would fail anyway + - modification timestamp is a good metric, should be incorporated + */ + String id; + id += "00f-"; + id += String(stat.id, HEX); + id += '-'; + id += String(stat.size, HEX); + id += '-'; + id += String(stat.mtime, HEX); + id += '-'; + id += String(stat.name.length, HEX); + + return id; } bool FileStream::truncate(size_t newSize) diff --git a/tests/HostTests/modules/Files.cpp b/tests/HostTests/modules/Files.cpp index 05eef51a4b..ed25524c7f 100644 --- a/tests/HostTests/modules/Files.cpp +++ b/tests/HostTests/modules/Files.cpp @@ -88,6 +88,7 @@ class FilesTest : public TestGroup { fileSetContent(testFileName, testContent); FileStream fs(testFileName); + Serial << testFileName << " ID: " << fs.id() << endl; res = fs.truncate(100); pos = fs.getPos(); size = fs.getSize(); From 45d0f952d60c1d608974c2c9058ea236b1e33c8e Mon Sep 17 00:00:00 2001 From: Mike Date: Thu, 11 Jan 2024 12:16:22 +0000 Subject: [PATCH 012/128] Fix esp32 IDF version number handling for patch releases (#2707) * Fix esp32 IDF version number handling for minor releases For example where branch is tagged 'v5.0.5-173-g9d6770dfbb' we require just 'v5.0' to identify toolsets, python, etc. * Fix bluetooth build with esp32s3 for latest IDF 5.0 --- Sming/Arch/Esp32/Components/esp32/component.mk | 2 +- Sming/Arch/Esp32/build.mk | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Sming/Arch/Esp32/Components/esp32/component.mk b/Sming/Arch/Esp32/Components/esp32/component.mk index 80a45453de..bab9ebc984 100644 --- a/Sming/Arch/Esp32/Components/esp32/component.mk +++ b/Sming/Arch/Esp32/Components/esp32/component.mk @@ -169,7 +169,7 @@ endif endif ifeq ($(ENABLE_BLUETOOTH),1) -ifeq (esp32s3-v5.2,$(ESP_VARIANT)-$(IDF_VERSION)) +ifeq (esp32s3-1,$(ESP_VARIANT)-$(IDF_VERSION_5x)) ESP_BT_VARIANT := esp32c3 else ESP_BT_VARIANT := $(ESP_VARIANT) diff --git a/Sming/Arch/Esp32/build.mk b/Sming/Arch/Esp32/build.mk index 4db95bf8c7..c1b1fee5a1 100644 --- a/Sming/Arch/Esp32/build.mk +++ b/Sming/Arch/Esp32/build.mk @@ -12,9 +12,12 @@ export IDF_PATH := $(call FixPath,$(IDF_PATH)) # Extract IDF version ifndef IDF_VER +# e.g. v5.2-beta1-265-g405b8b5512 or v5.0.5-173-g9d6770dfbb IDF_VER := $(shell (cd $$IDF_PATH && git describe --always --tags --dirty) | cut -c 1-31) endif -IDF_VERSION := $(firstword $(subst -, ,$(IDF_VER))) +# Now just vmajor.minor +IDF_VERSION := $(subst ., ,$(firstword $(subst -, ,$(IDF_VER)))) +IDF_VERSION := $(firstword $(IDF_VERSION)).$(word 2,$(IDF_VERSION)) # By default, downloaded tools will be installed under $HOME/.espressif directory # (%USERPROFILE%/.espressif on Windows). This path can be modified by setting From 517312948d633e1a36e075b9d0908ba829391224 Mon Sep 17 00:00:00 2001 From: Mike Date: Fri, 12 Jan 2024 10:25:45 +0000 Subject: [PATCH 013/128] Fix bluetooth build with esp32s3 for latest IDF 4.4 (#2708) It appears esp32s3 just uses esp32c3 for all IDF versions now. --- Sming/Arch/Esp32/Components/esp32/component.mk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sming/Arch/Esp32/Components/esp32/component.mk b/Sming/Arch/Esp32/Components/esp32/component.mk index bab9ebc984..8a78673a58 100644 --- a/Sming/Arch/Esp32/Components/esp32/component.mk +++ b/Sming/Arch/Esp32/Components/esp32/component.mk @@ -169,7 +169,7 @@ endif endif ifeq ($(ENABLE_BLUETOOTH),1) -ifeq (esp32s3-1,$(ESP_VARIANT)-$(IDF_VERSION_5x)) +ifeq (esp32s3,$(ESP_VARIANT)) ESP_BT_VARIANT := esp32c3 else ESP_BT_VARIANT := $(ESP_VARIANT) From 3d86fb369f6bdb7b7c0fd6ed41f8a47e4335332b Mon Sep 17 00:00:00 2001 From: Mike Date: Tue, 16 Jan 2024 08:31:07 +0000 Subject: [PATCH 014/128] Fix esp8266 timer1 testing (#2709) This PR fixes an issue with `HostTests` which hangs consistently when testing Timer1 (used by HardwareTimer) on the Esp8266. The problem occurs whilst checking each timer for consistency against system time. This happens because the test code doesn't set a callback interrupt routine. It is only a problem on the Esp8266 because the callback routine is set directly on the interrupt vector. Other architectures use indirection and so do nothing if the callback isn't set. NB. Doing this with the Esp8266 would increase interrupt latency which would affect PWM timing, etc. Normal code uses the `HardwareTimer` class which contains logic to ensure this doesn't happen during setup. Timer1 has only a 23-bit counter so fails test with /16 divisor as it wraps at around 1.7s (test duration is 2 seconds). Fixed by restricting duration to timer maximum (less 1ms). Elapsed tick calculation check also improved by first checking whether timer is an UP or DOWN counter, and using timer maximum tick value to handle wraps correctly. This PR also reduces the number of test loops on the Host (from 2000 to 50). Note: Bug was introduced in #2456. Timer1 needed to be correctly configured (but inactive) to produce correct result for Esp32. --- Sming/Core/HardwareTimer.h | 3 +-- tests/HostTests/modules/Clocks.cpp | 36 +++++++++++++++++++++++------- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/Sming/Core/HardwareTimer.h b/Sming/Core/HardwareTimer.h index 364f21ea50..a95f4a24b6 100644 --- a/Sming/Core/HardwareTimer.h +++ b/Sming/Core/HardwareTimer.h @@ -141,11 +141,10 @@ class Timer1Api : public CallbackTimerApi> template uint8_t Timer1Api::state; template uint32_t Timer1Api::interval; -template - /** * @brief Hardware Timer class template with selectable divider and interrupt mode */ +template using HardwareTimer1 = CallbackTimer>; /** diff --git a/tests/HostTests/modules/Clocks.cpp b/tests/HostTests/modules/Clocks.cpp index 0b15d025ea..b6c78fc775 100644 --- a/tests/HostTests/modules/Clocks.cpp +++ b/tests/HostTests/modules/Clocks.cpp @@ -26,7 +26,11 @@ template class ClockTestTemplate : public TestG { printLimits(); - for(unsigned i = 0; i < 2000; ++i) { + unsigned loopCount{2000}; +#ifdef ARCH_HOST + loopCount = 50; +#endif + while(loopCount--) { auto value = os_random(); check(value); check(value); @@ -37,19 +41,30 @@ template class ClockTestTemplate : public TestG TEST_CASE("vs. system time") { - constexpr uint32_t duration{2000000}; - auto startTime = system_get_time(); + // Determine whether this is an up or down-counter auto startTicks = Clock::ticks(); + os_delay_us(100); + auto endTicks = Clock::ticks(); + bool isDownCounter = (endTicks < startTicks); + debug_w("%s is %s counter", Clock::typeName(), isDownCounter ? "DOWN" : "UP"); + + // Run for a second or two and check timer ticks correspond approximately with system clock + constexpr uint64_t maxDuration = Clock::maxTicks().template as() - 5000ULL; + constexpr uint32_t duration = std::min(2000000ULL, maxDuration); + auto startTime = system_get_time(); + startTicks = Clock::ticks(); uint32_t time; - while((time = system_get_time()) < startTime + duration) { + while((time = system_get_time()) - startTime < duration) { // } - auto endTicks = Clock::ticks(); - // Handle both up and down counters - auto elapsedTicks = (endTicks >= startTicks) ? endTicks - startTicks : startTicks - endTicks; + endTicks = Clock::ticks(); + if(isDownCounter) { + std::swap(startTicks, endTicks); + } + uint32_t elapsedTicks = (endTicks - startTicks) % (Clock::maxTicks() + 1); debug_w("System time elapsed: %u", time - startTime); - debug_w("%s ticks: %u", Clock::typeName(), elapsedTicks); + debug_w("Ticks: %u (%u - %u)", elapsedTicks, startTicks, endTicks); debug_w("Ratio: x %f", float(elapsedTicks) / (time - startTime)); uint32_t us = Micros::ticksToTime(elapsedTicks); debug_w("Apparent time: %u", us); @@ -252,10 +267,15 @@ template class Timer1ClockTestTemplate : public ClockTestTemplate, uint32_t> { public: + static void IRAM_ATTR callback(void*) + { + } + void execute() override { // Configure the hardware to match selected clock divider Timer1Api timer; + timer.setCallback(callback, nullptr); timer.arm(false); ClockTestTemplate, uint32_t>::execute(); From 306488e4f1ade4021eb4c849a3f56bd117bef0bd Mon Sep 17 00:00:00 2001 From: Mike Date: Tue, 16 Jan 2024 08:32:23 +0000 Subject: [PATCH 015/128] Add note in documentation about behaviour of one-shot Hardware timer with Esp8266 (#2713) --- .../Components/driver/include/driver/hw_timer.h | 7 ++++++- docs/source/framework/timers/hardware-timer.rst | 14 ++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/Sming/Arch/Esp8266/Components/driver/include/driver/hw_timer.h b/Sming/Arch/Esp8266/Components/driver/include/driver/hw_timer.h index 069f8dafc1..1d7a7459f7 100644 --- a/Sming/Arch/Esp8266/Components/driver/include/driver/hw_timer.h +++ b/Sming/Arch/Esp8266/Components/driver/include/driver/hw_timer.h @@ -80,7 +80,12 @@ void IRAM_ATTR hw_timer1_attach_interrupt(hw_timer_source_type_t source_type, hw * @brief Enable the timer * @param div * @param intr_type - * @param auto_load + * @param auto_load true for repeating timer, false for one-shot + * + * Note: With one-shot timer application callback must stop the timer when it is no longer required. + * This is to reduce callback latency. + * If this is not done, timer will trigger again when timer counter wraps around to 0. + * For /16 divisor this is only 1.7s. */ inline void IRAM_ATTR hw_timer1_enable(hw_timer_clkdiv_t div, hw_timer_intr_type_t intr_type, bool auto_load) { diff --git a/docs/source/framework/timers/hardware-timer.rst b/docs/source/framework/timers/hardware-timer.rst index 713edb7be7..ee27c2932c 100644 --- a/docs/source/framework/timers/hardware-timer.rst +++ b/docs/source/framework/timers/hardware-timer.rst @@ -11,5 +11,19 @@ The API for hardware (and :doc:`timer-queue` timers) is identical, implemented u :cpp:class:`CallbackTimer` class template for best performance. +.. note:: + + **Applies to Esp8266 architecture only.** + + As with all Callback timers, the :cpp:type:`HardwareTimer` can be one-shot or repeating. + + With the Esp8266 a one-shot timer will repeat after it has been triggered, but only after the timer + counter wraps around. The Esp8266 only has 23 bits of resolution so with a clock divider of 16 + this will happen after about 1.7s. + + Because the Esp8266 lacks any proper PWM hardware the timer latency is critical. + Adding any additional code to the callback is therefore left to the programmer. + + .. doxygengroup:: hardware_timer :members: From fa90b7ac0cd24fbf9fb29e86599971c15551b25e Mon Sep 17 00:00:00 2001 From: Mike Date: Tue, 16 Jan 2024 08:33:39 +0000 Subject: [PATCH 016/128] Fix Adafruit_GFX `writeColor` implementation for ESP32 (#2712) Fixes #2711 The problem occurs with a code path for ESP32 which buffers up to 32 pixels at a time for transfer. However, the first call to 'writePixels' clears the buffer to black (0 - empty response data). If the fill colour is black no abnormal operation is observed, but any other colour and at most 32 pixels get filled with the requested colour. The benefit of these optimisations is minimal so the library is patched to simplify code path for all architectures. --- Sming/Libraries/.patches/Adafruit_GFX.patch | 161 +++++++++++++++++++- 1 file changed, 156 insertions(+), 5 deletions(-) diff --git a/Sming/Libraries/.patches/Adafruit_GFX.patch b/Sming/Libraries/.patches/Adafruit_GFX.patch index 04d95e8163..acaa94e23b 100644 --- a/Sming/Libraries/.patches/Adafruit_GFX.patch +++ b/Sming/Libraries/.patches/Adafruit_GFX.patch @@ -1,5 +1,5 @@ diff --git a/Adafruit_SPITFT.cpp b/Adafruit_SPITFT.cpp -index b78d5ce..d2d02b1 100644 +index b78d5ce..f919595 100644 --- a/Adafruit_SPITFT.cpp +++ b/Adafruit_SPITFT.cpp @@ -35,6 +35,10 @@ @@ -59,7 +59,7 @@ index b78d5ce..d2d02b1 100644 // avoid paramater-not-used complaints (void)block; - (void)bigEndian; -- + -#if defined(ESP32) - if (connection == TFT_HARD_SPI) { - if (!bigEndian) { @@ -71,7 +71,7 @@ index b78d5ce..d2d02b1 100644 - } -#elif defined(ARDUINO_NRF52_ADAFRUIT) && \ - defined(NRF52840_XXAA) // Adafruit nRF52 use SPIM3 DMA at 32Mhz -- if (!bigEndian) { + if (!bigEndian) { - swapBytes(colors, len); // convert little-to-big endian for display - } - hwspi._spi->transfer(colors, NULL, 2 * len); // NULL RX to avoid overwrite @@ -82,8 +82,8 @@ index b78d5ce..d2d02b1 100644 - return; -#elif defined(ARDUINO_ARCH_RP2040) - spi_inst_t *pi_spi = hwspi._spi == &SPI ? spi0 : spi1; - - if (!bigEndian) { +- +- if (!bigEndian) { - // switch to 16-bit writes - hw_write_masked(&spi_get_hw(pi_spi)->cr0, 15 << SPI_SSPCR0_DSS_LSB, - SPI_SSPCR0_DSS_BITS); @@ -211,6 +211,157 @@ index b78d5ce..d2d02b1 100644 } /*! +@@ -1184,150 +1034,6 @@ void Adafruit_SPITFT::writeColor(uint16_t color, uint32_t len) { + + uint8_t hi = color >> 8, lo = color; + +-#if defined(ESP32) // ESP32 has a special SPI pixel-writing function... +- if (connection == TFT_HARD_SPI) { +-#define SPI_MAX_PIXELS_AT_ONCE 32 +-#define TMPBUF_LONGWORDS (SPI_MAX_PIXELS_AT_ONCE + 1) / 2 +-#define TMPBUF_PIXELS (TMPBUF_LONGWORDS * 2) +- static uint32_t temp[TMPBUF_LONGWORDS]; +- uint32_t c32 = color * 0x00010001; +- uint16_t bufLen = (len < TMPBUF_PIXELS) ? len : TMPBUF_PIXELS, xferLen, +- fillLen; +- // Fill temp buffer 32 bits at a time +- fillLen = (bufLen + 1) / 2; // Round up to next 32-bit boundary +- for (uint32_t t = 0; t < fillLen; t++) { +- temp[t] = c32; +- } +- // Issue pixels in blocks from temp buffer +- while (len) { // While pixels remain +- xferLen = (bufLen < len) ? bufLen : len; // How many this pass? +- writePixels((uint16_t *)temp, xferLen); +- len -= xferLen; +- } +- return; +- } +-#elif defined(ARDUINO_NRF52_ADAFRUIT) && \ +- defined(NRF52840_XXAA) // Adafruit nRF52840 use SPIM3 DMA at 32Mhz +- // at most 2 scan lines +- uint32_t const pixbufcount = min(len, ((uint32_t)2 * width())); +- uint16_t *pixbuf = (uint16_t *)rtos_malloc(2 * pixbufcount); +- +- // use SPI3 DMA if we could allocate buffer, else fall back to writing each +- // pixel loop below +- if (pixbuf) { +- uint16_t const swap_color = __builtin_bswap16(color); +- +- // fill buffer with color +- for (uint32_t i = 0; i < pixbufcount; i++) { +- pixbuf[i] = swap_color; +- } +- +- while (len) { +- uint32_t const count = min(len, pixbufcount); +- writePixels(pixbuf, count, true, true); +- len -= count; +- } +- +- rtos_free(pixbuf); +- return; +- } +-#else // !ESP32 +-#if defined(USE_SPI_DMA) && (defined(__SAMD51__) || defined(ARDUINO_SAMD_ZERO)) +- if (((connection == TFT_HARD_SPI) || (connection == TFT_PARALLEL)) && +- (len >= 16)) { // Don't bother with DMA on short pixel runs +- int i, d, numDescriptors; +- if (hi == lo) { // If high & low bytes are same... +- onePixelBuf = color; +- // Can do this with a relatively short descriptor list, +- // each transferring a max of 32,767 (not 32,768) pixels. +- // This won't run off the end of the allocated descriptor list, +- // since we're using much larger chunks per descriptor here. +- numDescriptors = (len + 32766) / 32767; +- for (d = 0; d < numDescriptors; d++) { +- int count = (len < 32767) ? len : 32767; +- descriptor[d].SRCADDR.reg = (uint32_t)&onePixelBuf; +- descriptor[d].BTCTRL.bit.SRCINC = 0; +- descriptor[d].BTCNT.reg = count * 2; +- descriptor[d].DESCADDR.reg = (uint32_t)&descriptor[d + 1]; +- len -= count; +- } +- descriptor[d - 1].DESCADDR.reg = 0; +- } else { +- // If high and low bytes are distinct, it's necessary to fill +- // a buffer with pixel data (swapping high and low bytes because +- // TFT and SAMD are different endianisms) and create a longer +- // descriptor list pointing repeatedly to this data. We can do +- // this slightly faster working 2 pixels (32 bits) at a time. +- uint32_t *pixelPtr = (uint32_t *)pixelBuf[0], +- twoPixels = __builtin_bswap16(color) * 0x00010001; +- // We can avoid some or all of the buffer-filling if the color +- // is the same as last time... +- if (color == lastFillColor) { +- // If length is longer than prior instance, fill only the +- // additional pixels in the buffer and update lastFillLen. +- if (len > lastFillLen) { +- int fillStart = lastFillLen / 2, +- fillEnd = (((len < maxFillLen) ? len : maxFillLen) + 1) / 2; +- for (i = fillStart; i < fillEnd; i++) +- pixelPtr[i] = twoPixels; +- lastFillLen = fillEnd * 2; +- } // else do nothing, don't set pixels or change lastFillLen +- } else { +- int fillEnd = (((len < maxFillLen) ? len : maxFillLen) + 1) / 2; +- for (i = 0; i < fillEnd; i++) +- pixelPtr[i] = twoPixels; +- lastFillLen = fillEnd * 2; +- lastFillColor = color; +- } +- +- numDescriptors = (len + maxFillLen - 1) / maxFillLen; +- for (d = 0; d < numDescriptors; d++) { +- int pixels = (len < maxFillLen) ? len : maxFillLen, bytes = pixels * 2; +- descriptor[d].SRCADDR.reg = (uint32_t)pixelPtr + bytes; +- descriptor[d].BTCTRL.bit.SRCINC = 1; +- descriptor[d].BTCNT.reg = bytes; +- descriptor[d].DESCADDR.reg = (uint32_t)&descriptor[d + 1]; +- len -= pixels; +- } +- descriptor[d - 1].DESCADDR.reg = 0; +- } +- memcpy(dptr, &descriptor[0], sizeof(DmacDescriptor)); +-#if defined(__SAMD51__) +- if (connection == TFT_PARALLEL) { +- // Switch WR pin to PWM or CCL +- pinPeripheral(tft8._wr, wrPeripheral); +- } +-#endif // end __SAMD51__ +- +- dma_busy = true; +- dma.startJob(); +- if (connection == TFT_PARALLEL) +- dma.trigger(); +- while (dma_busy) +- ; // Wait for completion +- // Unfortunately blocking is necessary. An earlier version returned +- // immediately and checked dma_busy on startWrite() instead, but it +- // turns out to be MUCH slower on many graphics operations (as when +- // drawing lines, pixel-by-pixel), perhaps because it's a volatile +- // type and doesn't cache. Working on this. +-#if defined(__SAMD51__) || defined(ARDUINO_SAMD_ZERO) +- if (connection == TFT_HARD_SPI) { +- // SAMD51: SPI DMA seems to leave the SPI peripheral in a freaky +- // state on completion. Workaround is to explicitly set it back... +- // (5/17/2019: apparently SAMD21 too, in certain cases, observed +- // with ST7789 display.) +- hwspi._spi->setDataMode(hwspi._mode); +- } else { +- pinPeripheral(tft8._wr, PIO_OUTPUT); // Switch WR back to GPIO +- } +-#endif // end __SAMD51__ +- return; +- } +-#endif // end USE_SPI_DMA +-#endif // end !ESP32 +- +- // All other cases (non-DMA hard SPI, bitbang SPI, parallel)... +- + if (connection == TFT_HARD_SPI) { + #if defined(ESP8266) + do { diff --git a/Adafruit_SPITFT.h b/Adafruit_SPITFT.h index 7f5d80f..9725e13 100644 From 02a403f81344b5884f8cd5e385b1abb04e9325d8 Mon Sep 17 00:00:00 2001 From: Mike Date: Wed, 31 Jan 2024 13:22:13 +0000 Subject: [PATCH 017/128] Fix Esp32 hardware timer callbacks (#2716) Fix esp32 hardware timer callbacks PR #2697 was supposed to fix the WDT from hardware timer interrupts. It did this by disabling interrupts (and therefore callbacks) completely. The actual reason for the WDT is twofold: 1. IDF version 5 changed some low-level HAL calls to accept bit masks instead of timer index. 2. Argument to interrupt callback wasn't stored or passed correctly so user-provided callback got junk for this parameter. This PR fixes those issues and fixes incomplete testing in HostTests. Also checked Basic_Blink sample using HardwareTimer. --- .../Arch/Esp32/Components/driver/hw_timer.cpp | 12 ++++++++---- .../Components/esp32/src/include/esp_clk.h | 4 ++++ tests/HostTests/modules/Timers.cpp | 18 +++++++++++++----- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/Sming/Arch/Esp32/Components/driver/hw_timer.cpp b/Sming/Arch/Esp32/Components/driver/hw_timer.cpp index 84254b7cc0..ebba941797 100644 --- a/Sming/Arch/Esp32/Components/driver/hw_timer.cpp +++ b/Sming/Arch/Esp32/Components/driver/hw_timer.cpp @@ -54,6 +54,7 @@ class TimerConfig } this->callback = callback; + this->arg = arg; uint32_t status_reg = reinterpret_cast(timer_ll_get_intr_status_reg(dev)); uint32_t mask = 1 << index; @@ -62,9 +63,8 @@ class TimerConfig #else int source = timer_group_periph_signals.groups[group].timer_irq_id[index]; #endif - esp_intr_alloc_intrstatus(source, ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_INTRDISABLED, status_reg, mask, timerIsr, - this, &isr_handle); clear_intr_status(); + esp_intr_alloc_intrstatus(source, ESP_INTR_FLAG_IRAM, status_reg, mask, timerIsr, this, &isr_handle); enable_intr(true); } @@ -84,13 +84,17 @@ class TimerConfig timer_ll_intr_disable(dev, index); } #else - timer_ll_enable_intr(dev, index, state); + timer_ll_enable_intr(dev, TIMER_LL_EVENT_ALARM(index), state); #endif } void __forceinline clear_intr_status() { +#if ESP_IDF_VERSION_MAJOR < 5 timer_ll_clear_intr_status(dev, index); +#else + timer_ll_clear_intr_status(dev, TIMER_LL_EVENT_ALARM(index)); +#endif } void __forceinline set_alarm_value(uint64_t value) @@ -165,7 +169,7 @@ class TimerConfig auto& timer = *static_cast(arg); if(timer.callback != nullptr) { - timer.callback(arg); + timer.callback(timer.arg); } timer.clear_intr_status(); diff --git a/Sming/Arch/Esp32/Components/esp32/src/include/esp_clk.h b/Sming/Arch/Esp32/Components/esp32/src/include/esp_clk.h index 8579d2d67e..acf627a4e7 100644 --- a/Sming/Arch/Esp32/Components/esp32/src/include/esp_clk.h +++ b/Sming/Arch/Esp32/Components/esp32/src/include/esp_clk.h @@ -1,7 +1,11 @@ #pragma once #include +#if ESP_IDF_VERSION_MAJOR < 5 #include +#else +#include +#endif #include #include diff --git a/tests/HostTests/modules/Timers.cpp b/tests/HostTests/modules/Timers.cpp index 2dc7bb638c..361627e98d 100644 --- a/tests/HostTests/modules/Timers.cpp +++ b/tests/HostTests/modules/Timers.cpp @@ -29,10 +29,12 @@ class CallbackTimerTest : public TestGroup { System.queueCallback( [](void* param) { - Serial.println(_F("timer1 expired")); - auto tmr = static_cast(param); - if(++tmr->count == 5) { - tmr->stop(); + auto self = static_cast(param); + ++self->timer1.count; + Serial << self->timer1.count << _F(" timer1 expired") << endl; + if(self->timer1.count == 5) { + self->timer1.stop(); + self->timer1complete(); } }, arg); @@ -58,9 +60,15 @@ class CallbackTimerTest : public TestGroup SHOW_SIZE(Timer1TestApi); SHOW_SIZE(HardwareTimerTest); - timer1.initializeMs<500>(timer1Callback, &timer1); + timer1.initializeMs<500>(timer1Callback, this); timer1.start(); + Serial << _F("Waiting for timer1 callback test to complete") << endl; + pending(); + } + + void timer1complete() + { statusTimer.initializeMs<1000>([this]() { bool done = false; ++statusTimerCount; From 8939766cb38ef540d8296c3ae4bbc0998f88b91a Mon Sep 17 00:00:00 2001 From: slaff Date: Wed, 31 Jan 2024 14:22:59 +0100 Subject: [PATCH 018/128] Fixed typos reported by codespell. (#2717) --- Sming/Libraries/MFRC522/MFRC522.cpp | 2 +- Sming/Libraries/MFRC522/README.rst | 2 +- Sming/Libraries/Ota/README.rst | 2 +- Sming/building.rst | 2 +- docs/source/information/develop/ci.rst | 2 +- samples/HttpServer_WebSockets/Kconfig | 4 ++-- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Sming/Libraries/MFRC522/MFRC522.cpp b/Sming/Libraries/MFRC522/MFRC522.cpp index 31ae38c33c..5b79dff3d5 100644 --- a/Sming/Libraries/MFRC522/MFRC522.cpp +++ b/Sming/Libraries/MFRC522/MFRC522.cpp @@ -423,7 +423,7 @@ byte MFRC522::PCD_CommunicateWithPICC( byte command, ///< The command to execut if (n & 0x01) { // Timer interrupt - nothing received in 25ms return STATUS_TIMEOUT; } - if (--i == 0) { // The emergency break. If all other condions fail we will eventually terminate on this one after 35.7ms. Communication with the MFRC522 might be down. + if (--i == 0) { // The emergency break. If all other conditions fail we will eventually terminate on this one after 35.7ms. Communication with the MFRC522 might be down. return STATUS_TIMEOUT; } } diff --git a/Sming/Libraries/MFRC522/README.rst b/Sming/Libraries/MFRC522/README.rst index 2196eb0678..77c9513b42 100644 --- a/Sming/Libraries/MFRC522/README.rst +++ b/Sming/Libraries/MFRC522/README.rst @@ -255,7 +255,7 @@ Troubleshooting #. Check your pin settings/variables in the code, see `Pin Layout`_ . #. Check your pin header soldering. Maybe you have cold solder joints. #. Check voltage. Most breakouts work with 3.3V. - #. SPI only works with 3.3V, most breakouts seem 5V tollerant, but try a level shifter. + #. SPI only works with 3.3V, most breakouts seem 5V tolerant, but try a level shifter. #. SPI does not like long connections. Try shorter connections. #. SPI does not like prototyping boards. Try soldered connections. #. According to reports #101, #126 and #131, there may be a problem with the soldering on the MFRC522 breakout. You could fix this on your own. diff --git a/Sming/Libraries/Ota/README.rst b/Sming/Libraries/Ota/README.rst index 03ce9a3549..79fb577e2b 100644 --- a/Sming/Libraries/Ota/README.rst +++ b/Sming/Libraries/Ota/README.rst @@ -10,7 +10,7 @@ This architecture-agnostic component adds support for Over-The-Air upgrades. Usage ----- -1. Add ``COMPONENT_DEPENDS += Ota`` to your application componenent.mk file. +1. Add ``COMPONENT_DEPENDS += Ota`` to your application component.mk file. 2. Add these lines to your application:: #include diff --git a/Sming/building.rst b/Sming/building.rst index 81fe2114f1..3a8cc8d18b 100644 --- a/Sming/building.rst +++ b/Sming/building.rst @@ -286,7 +286,7 @@ Component --------- The purpose of a Component is to encapsulate related elements for -selective inclusion in a project, for easy sharing and re-use: +selective inclusion in a project, for easy sharing and reuse: - **Shared Library** with associated header files - **App Code** Source files to be compiled directly into the user’s diff --git a/docs/source/information/develop/ci.rst b/docs/source/information/develop/ci.rst index 74581accd6..30062b52d0 100644 --- a/docs/source/information/develop/ci.rst +++ b/docs/source/information/develop/ci.rst @@ -52,7 +52,7 @@ There are two mechanisms available. GitHub Actions ~~~~~~~~~~~~~~ -The ``library.yml`` re-useable workflow is provided, which takes care of these tasks: +The ``library.yml`` reusable workflow is provided, which takes care of these tasks: - Checking in the library to test - Checking in the Sming framework diff --git a/samples/HttpServer_WebSockets/Kconfig b/samples/HttpServer_WebSockets/Kconfig index 8744832f6b..ef96b02ac4 100644 --- a/samples/HttpServer_WebSockets/Kconfig +++ b/samples/HttpServer_WebSockets/Kconfig @@ -4,8 +4,8 @@ # mainmenu "Sample Configuration" - menu "Command Hanler" + menu "Command Handler" config ENABLE_CMD_HANDLER bool "Enable command handler functionality" default y - endmenu \ No newline at end of file + endmenu From d9d97bd52359a501f62359611027cdaf4b7e194a Mon Sep 17 00:00:00 2001 From: Mike Date: Thu, 7 Mar 2024 09:04:41 +0000 Subject: [PATCH 019/128] Fix HttpUpgrader duplicate stream de-allocation (#2722) --- .../Components/rboot/src/Network/RbootHttpUpdater.cpp | 11 ++++------- Sming/Libraries/OtaNetwork/src/HttpUpgrader.cpp | 11 ++++------- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/Sming/Components/rboot/src/Network/RbootHttpUpdater.cpp b/Sming/Components/rboot/src/Network/RbootHttpUpdater.cpp index 883dd60dad..95bf2a843a 100644 --- a/Sming/Components/rboot/src/Network/RbootHttpUpdater.cpp +++ b/Sming/Components/rboot/src/Network/RbootHttpUpdater.cpp @@ -49,17 +49,19 @@ void RbootHttpUpdater::start() int RbootHttpUpdater::itemComplete(HttpConnection& client, bool success) { + auto& it = items[currentItem]; + if(!success) { + it.stream.release(); // Owned by HttpRequest updateFailed(); return -1; } - auto& it = items[currentItem]; debug_d("Finished: URL: %s, Offset: 0x%X, Length: %u", it.url.c_str(), it.stream->getStartAddress(), it.stream->available()); it.size = it.stream->available(); - it.stream = nullptr; // the actual deletion will happen outside of this class + it.stream.release(); // the actual deletion will happen outside of this class currentItem++; return 0; @@ -77,11 +79,6 @@ int RbootHttpUpdater::updateComplete(HttpConnection& client, bool success) debug_d(" - item: %u, addr: 0x%X, url: %s", i, items[i].targetOffset, items[i].url.c_str()); } - if(!success) { - updateFailed(); - return -1; - } - if(updateDelegate) { updateDelegate(*this, true); } diff --git a/Sming/Libraries/OtaNetwork/src/HttpUpgrader.cpp b/Sming/Libraries/OtaNetwork/src/HttpUpgrader.cpp index 90b893391f..a45be2c22f 100644 --- a/Sming/Libraries/OtaNetwork/src/HttpUpgrader.cpp +++ b/Sming/Libraries/OtaNetwork/src/HttpUpgrader.cpp @@ -49,17 +49,19 @@ void HttpUpgrader::start() int HttpUpgrader::itemComplete(HttpConnection& client, bool success) { + auto& it = items[currentItem]; + if(!success) { + it.stream.release(); // Owned by HttpRequest updateFailed(); return -1; } - auto& it = items[currentItem]; debug_d("Finished: URL: %s, Offset: 0x%X, Length: %u", it.url.c_str(), it.partition.address(), it.stream->available()); it.size = it.stream->available(); - it.stream = nullptr; // the actual deletion will happen outside of this class + it.stream.release(); // the actual deletion will happen outside of this class currentItem++; return 0; @@ -77,11 +79,6 @@ int HttpUpgrader::updateComplete(HttpConnection& client, bool success) debug_d(" - item: %u, addr: 0x%X, url: %s", i, items[i].partition.address(), items[i].url.c_str()); } - if(!success) { - updateFailed(); - return -1; - } - if(updateDelegate) { updateDelegate(*this, true); } From 1e03c46add5413dc0790af7eb4af11a4a6f65bdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arthur=20Crippa=20B=C3=BArigo?= Date: Thu, 29 Feb 2024 14:31:17 -0300 Subject: [PATCH 020/128] TcpClient does not send incomplete packets. --- Sming/Components/Network/src/Network/TcpClient.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sming/Components/Network/src/Network/TcpClient.cpp b/Sming/Components/Network/src/Network/TcpClient.cpp index b0c1e7b45e..56b151352d 100644 --- a/Sming/Components/Network/src/Network/TcpClient.cpp +++ b/Sming/Components/Network/src/Network/TcpClient.cpp @@ -49,11 +49,13 @@ bool TcpClient::send(const char* data, uint16_t len, bool forceCloseAfterSent) memoryStream = new MemoryDataStream(); } - if(memoryStream->write(data, len) != len) { + if(!memoryStream->ensureCapacity(memoryStream->getSize() + len)) { debug_e("TcpClient::send ERROR: Unable to store %d bytes in buffer", len); return false; } + memoryStream->write(data, len); + return send(memoryStream, forceCloseAfterSent); } From 8ba44cbb00a638b56b671bc8e037190fbb00b947 Mon Sep 17 00:00:00 2001 From: Mike Date: Fri, 8 Mar 2024 13:07:26 +0000 Subject: [PATCH 021/128] Improvements to OtaNetwork Http upgrader (#2725) * Fetch one item at a time * Enable Host for testing OTA * Add check in case item list is empty --------- Co-authored-by: mikee47 --- Sming/Arch/Host/spiffs-two-roms.hw | 3 + .../Libraries/OtaNetwork/src/HttpUpgrader.cpp | 89 ++++++++++--------- .../src/include/Ota/Network/HttpUpgrader.h | 7 +- samples/Basic_Ota/component.mk | 2 +- 4 files changed, 55 insertions(+), 46 deletions(-) diff --git a/Sming/Arch/Host/spiffs-two-roms.hw b/Sming/Arch/Host/spiffs-two-roms.hw index 80128f0191..f993b97396 100644 --- a/Sming/Arch/Host/spiffs-two-roms.hw +++ b/Sming/Arch/Host/spiffs-two-roms.hw @@ -2,6 +2,9 @@ "name": "Two ROM slots with single SPIFFS", "base_config": "spiffs", "partitions": { + "rom0": { + "subtype": "ota_0" + }, "rom1": { "address": "0x108000", "size": "992K", diff --git a/Sming/Libraries/OtaNetwork/src/HttpUpgrader.cpp b/Sming/Libraries/OtaNetwork/src/HttpUpgrader.cpp index a45be2c22f..75d6674900 100644 --- a/Sming/Libraries/OtaNetwork/src/HttpUpgrader.cpp +++ b/Sming/Libraries/OtaNetwork/src/HttpUpgrader.cpp @@ -17,33 +17,37 @@ namespace Network { void HttpUpgrader::start() { - for(unsigned i = 0; i < items.count(); i++) { - auto& it = items[i]; - debug_d("Download file:\r\n" - " (%u) %s -> %s @ 0x%X", - currentItem, it.url.c_str(), it.partition.name().c_str(), it.partition.address()); - - HttpRequest* request; - if(baseRequest != nullptr) { - request = baseRequest->clone(); - request->setURL(it.url); - } else { - request = new HttpRequest(it.url); - } - - request->setMethod(HTTP_GET); - request->setResponseStream(it.getStream()); - - if(i == items.count() - 1) { - request->onRequestComplete(RequestCompletedDelegate(&HttpUpgrader::updateComplete, this)); - } else { - request->onRequestComplete(RequestCompletedDelegate(&HttpUpgrader::itemComplete, this)); - } - - if(!send(request)) { - debug_e("ERROR: Rejected sending new request."); - break; - } + fetchNextItem(); +} + +void HttpUpgrader::fetchNextItem() +{ + if(currentItem >= items.count()) { + return; + } + + auto& it = items[currentItem]; + debug_d("Download file:\r\n" + " (%u) %s -> %s @ 0x%X", + currentItem, it.url.c_str(), it.partition.name().c_str(), it.partition.address()); + + HttpRequest* request; + if(baseRequest != nullptr) { + request = baseRequest->clone(); + request->setURL(it.url); + } else { + request = new HttpRequest(it.url); + } + + request->setMethod(HTTP_GET); + request->setResponseStream(it.getStream()); + + request->onRequestComplete(RequestCompletedDelegate(&HttpUpgrader::itemComplete, this)); + + if(!send(request)) { + debug_e("ERROR: Rejected sending new request."); + it.stream.release(); + downloadFailed(); } } @@ -52,43 +56,44 @@ int HttpUpgrader::itemComplete(HttpConnection& client, bool success) auto& it = items[currentItem]; if(!success) { - it.stream.release(); // Owned by HttpRequest - updateFailed(); + it.stream.release(); + downloadFailed(); return -1; } - debug_d("Finished: URL: %s, Offset: 0x%X, Length: %u", it.url.c_str(), it.partition.address(), - it.stream->available()); - it.size = it.stream->available(); + debug_d("Finished: URL: %s, Offset: 0x%X, Length: %u", it.url.c_str(), it.partition.address(), it.size); + it.stream.release(); // the actual deletion will happen outside of this class currentItem++; + if(currentItem < items.count()) { + fetchNextItem(); + } else { + downloadComplete(); + } + return 0; } -int HttpUpgrader::updateComplete(HttpConnection& client, bool success) +void HttpUpgrader::downloadComplete() { - int hasError = itemComplete(client, success); - if(hasError != 0) { - return hasError; - } - +#if DEBUG_VERBOSE_LEVEL >= DBG debug_d("\r\nFirmware download finished!"); for(unsigned i = 0; i < items.count(); i++) { - debug_d(" - item: %u, addr: 0x%X, url: %s", i, items[i].partition.address(), items[i].url.c_str()); + auto& it = items[i]; + debug_d(" - item: %u, addr: 0x%X, size: 0x%X, url: %s", i, it.partition.address(), it.size, it.url.c_str()); } +#endif if(updateDelegate) { updateDelegate(*this, true); } applyUpdate(); - - return 0; } -void HttpUpgrader::updateFailed() +void HttpUpgrader::downloadFailed() { debug_e("\r\nFirmware download failed.."); if(updateDelegate) { diff --git a/Sming/Libraries/OtaNetwork/src/include/Ota/Network/HttpUpgrader.h b/Sming/Libraries/OtaNetwork/src/include/Ota/Network/HttpUpgrader.h index 957a19f148..4d1e8f3259 100644 --- a/Sming/Libraries/OtaNetwork/src/include/Ota/Network/HttpUpgrader.h +++ b/Sming/Libraries/OtaNetwork/src/include/Ota/Network/HttpUpgrader.h @@ -124,10 +124,11 @@ class HttpUpgrader : protected HttpClient protected: void applyUpdate(); - void updateFailed(); + void downloadFailed(); + void downloadComplete(); + void fetchNextItem(); - virtual int itemComplete(HttpConnection& client, bool success); - virtual int updateComplete(HttpConnection& client, bool success); + int itemComplete(HttpConnection& client, bool success); protected: ItemList items; diff --git a/samples/Basic_Ota/component.mk b/samples/Basic_Ota/component.mk index b46c243c8c..b6f8b1d241 100644 --- a/samples/Basic_Ota/component.mk +++ b/samples/Basic_Ota/component.mk @@ -1,4 +1,4 @@ -COMPONENT_SOC := esp* +COMPONENT_SOC := esp* host COMPONENT_DEPENDS := OtaNetwork HWCONFIG := ota From a0b1746d0d22418533da949a957441355b78cb05 Mon Sep 17 00:00:00 2001 From: Mike Date: Mon, 11 Mar 2024 08:10:08 +0000 Subject: [PATCH 022/128] Fix mechanism for OTA upgrade from Sming 4.2 (#2728) * Fix PartitionTable bool() operator * Allow automatic typecasting from Device::Type to Partition::FullType * Update documentation --- Sming/Components/Storage/README.rst | 1 + Sming/Components/Storage/ota-migration.rst | 107 ++++++++++++++++++ .../Storage/src/include/Storage/Device.h | 1 + .../src/include/Storage/PartitionTable.h | 2 +- 4 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 Sming/Components/Storage/ota-migration.rst diff --git a/Sming/Components/Storage/README.rst b/Sming/Components/Storage/README.rst index 6cd6ac162f..f2818c0f13 100644 --- a/Sming/Components/Storage/README.rst +++ b/Sming/Components/Storage/README.rst @@ -91,6 +91,7 @@ When planning OTA updates please check that the displayed partition map correspo For example, the partition table requires a free sector so must not overlap other partitions. Your OTA update process must include a step to write the partition table to the correct location. +See :doc:`ota-migration`. It is not necessary to update the bootloader. See :component:`rboot` for further information. diff --git a/Sming/Components/Storage/ota-migration.rst b/Sming/Components/Storage/ota-migration.rst new file mode 100644 index 0000000000..bcd3044997 --- /dev/null +++ b/Sming/Components/Storage/ota-migration.rst @@ -0,0 +1,107 @@ +Partition table migration +========================= + +.. highlight:: c++ + +This guidance applies to esp8266 devices only. + +Sming after v4.2 requires a valid partition table. +If existing devices running previous versions of Sming require updating via OTA then +an intermediate firmware should be created which installs this partition table. + +After rBoot hands control to the SDK entrypoint, the ``user_pre_init()`` function is called. +This function is documented in the NON-OS-SDK guide and was introduced in version 3. +It is called by the SDK before user_init() so nothing else in the framework has yet been initialised, +including any C++ static initialisers. + +Sming uses this function to read the partition table into memory. +Applications may override this function to perform any custom upgrade operations. + +Add to application's `component.mk`: + +.. code-block:: make + + EXTRA_LDFLAGS := $(call Wrap,user_pre_init) + USER_CFLAGS += -DPARTITION_TABLE_OFFSET=$(PARTITION_TABLE_OFFSET) + +Add this to your application:: + + // Support updating legacy devices without partition tables (Sming 4.2 and earlier) + #ifdef ARCH_ESP8266 + + namespace + { + // Note: This file won't exist on initial build! + IMPORT_FSTR(partitionTableData, PROJECT_DIR "/out/Esp8266/debug/firmware/partitions.bin") + } + + extern "C" void __wrap_user_pre_init(void) + { + static_assert(PARTITION_TABLE_OFFSET == 0x3fa000, "Bad PTO"); + Storage::initialize(); + auto& flash = *Storage::spiFlash; + if(!flash.partitions()) { + LOAD_FSTR(data, partitionTableData) + flash.erase_range(PARTITION_TABLE_OFFSET, flash.getBlockSize()); + flash.write(PARTITION_TABLE_OFFSET, data, partitionTableData.size()); + flash.loadPartitions(PARTITION_TABLE_OFFSET); + } + + extern void __real_user_pre_init(void); + __real_user_pre_init(); + } + + #endif // ARCH_ESP8266 + +.. note:: + + You will get a 'file not found' error because the partition table gets built *after* compiling the application. + You can run ``make partmap-build`` manually first to get around this. + + +An alternative method is to build the partition table layout in code, so there are no external file dependencies:: + + // Support updating legacy devices without partition tables (Sming 4.2 and earlier) + #ifdef ARCH_ESP8266 + + #include + + extern "C" void __wrap_user_pre_init(void) + { + static_assert(PARTITION_TABLE_OFFSET == 0x3fa000, "Bad PTO"); + + Storage::initialize(); + + auto& flash = *Storage::spiFlash; + if(!flash.partitions()) { + using FullType = Storage::Partition::FullType; + using SubType = Storage::Partition::SubType; + #define PT_ENTRY(name, fulltype, offset, size) \ + { ESP_PARTITION_MAGIC, FullType(fulltype).type, FullType(fulltype).subtype, offset, size, name, 0 } + + static constexpr Storage::esp_partition_info_t partitionTableData[] PROGMEM{ + PT_ENTRY("spiFlash", Storage::Device::Type::flash, 0, 0x400000), + PT_ENTRY("rom0", SubType::App::ota0, 0x2000, 0xf8000), + PT_ENTRY("rom1", SubType::App::ota1, 0x102000, 0xf8000), + PT_ENTRY("spiffs0", SubType::Data::spiffs, 0x200000, 0xc0000), + PT_ENTRY("spiffs1", SubType::Data::spiffs, 0x2c0000, 0xc0000), + PT_ENTRY("rf_cal", SubType::Data::rfCal, 0x3fb000, 0x1000), + PT_ENTRY("phy_init", SubType::Data::phy, 0x3fc000, 0x1000), + PT_ENTRY("sys_param", SubType::Data::sysParam, 0x3fd000, 0x3000), + }; + + uint8_t buffer[sizeof(partitionTableData)]; + memcpy(buffer, partitionTableData, sizeof(partitionTableData)); + flash.erase_range(PARTITION_TABLE_OFFSET, flash.getBlockSize()); + flash.write(PARTITION_TABLE_OFFSET, buffer, sizeof(buffer)); + flash.loadPartitions(PARTITION_TABLE_OFFSET); + } + + extern void __real_user_pre_init(void); + __real_user_pre_init(); + } + + #endif // ARCH_ESP8266 + + +The above examples are provided as templates and should be modified as required and tested thoroughly! diff --git a/Sming/Components/Storage/src/include/Storage/Device.h b/Sming/Components/Storage/src/include/Storage/Device.h index 7ebaa32df8..b05017e069 100644 --- a/Sming/Components/Storage/src/include/Storage/Device.h +++ b/Sming/Components/Storage/src/include/Storage/Device.h @@ -40,6 +40,7 @@ class Device : public LinkedObjectTemplate * @brief Storage type */ enum class Type : uint8_t { + partitionType = uint8_t(Partition::Type::storage), #define XX(type, value, desc) type = value, STORAGE_TYPE_MAP(XX) #undef XX diff --git a/Sming/Components/Storage/src/include/Storage/PartitionTable.h b/Sming/Components/Storage/src/include/Storage/PartitionTable.h index b54fcee3f3..2497178f76 100644 --- a/Sming/Components/Storage/src/include/Storage/PartitionTable.h +++ b/Sming/Components/Storage/src/include/Storage/PartitionTable.h @@ -24,7 +24,7 @@ class PartitionTable explicit operator bool() const { - return mEntries.isEmpty(); + return !mEntries.isEmpty(); } /** From ffb1b3a336a1a49cd97681720a7bf4b23821b3ca Mon Sep 17 00:00:00 2001 From: Mike Date: Thu, 14 Mar 2024 14:55:56 +0000 Subject: [PATCH 023/128] PartitionStream requires blockErase, other OTA-related improvements (#2729) Leaving `blockErase` parameter at `false` when constructing a `PartitionStream` will result in corruption unless partition has already been erased. This is not always immediately apparent so can be difficult to diagnose. - **Breaking change** Replacing `blockErase` with an enumerated `mode` parameter enforces read-only behaviour by default (new feature and a good thing), with write access more explicitly defined. It's a simple change to existing code and will ensure it gets manually checked to ensure correct behaviour. - Update the `Basic_Ota` sample to check that an OTA update is not being attempted on the active running partition. This can happen with esp8266, for example, when running in temporary boot mode. (NB. Other checks may include active filing system partitions but that is application-specific.) - Update notes on migrating from Sming 4.2 to include updating boot sector. (Learning points froim #2727.) - Remove redundant flashsize calculation from rboot and update version string to `Sming v1.5` (from v1.4.2). Note this should have been done in #2258. - Fix rboot README regarding use of slot 2, should correspond with subtype `ota_2` not `ota_1`. --- .../Esp8266/Components/esp8266/startup.cpp | 11 ++++- .../Esp8266/Components/esp_no_wifi/README.rst | 6 +++ Sming/Components/Storage/ota-migration.rst | 49 +++++++++++++++---- .../Storage/src/PartitionStream.cpp | 6 ++- .../src/include/Storage/PartitionStream.h | 47 ++++++++++++++++-- Sming/Components/rboot/README.rst | 2 +- Sming/Components/rboot/rboot | 2 +- docs/source/information/flash.rst | 17 +++---- docs/source/upgrading/5.1-5.2.rst | 13 +++++ samples/Basic_Ota/app/application.cpp | 13 ++++- 10 files changed, 137 insertions(+), 29 deletions(-) create mode 100644 docs/source/upgrading/5.1-5.2.rst diff --git a/Sming/Arch/Esp8266/Components/esp8266/startup.cpp b/Sming/Arch/Esp8266/Components/esp8266/startup.cpp index 79d1d9794b..f8f6aa807d 100644 --- a/Sming/Arch/Esp8266/Components/esp8266/startup.cpp +++ b/Sming/Arch/Esp8266/Components/esp8266/startup.cpp @@ -18,6 +18,7 @@ extern void init(); extern void cpp_core_initialize(); +// Normal entry point for user application code from SDK extern "C" void user_init(void) { // Initialise hardware timers @@ -39,12 +40,20 @@ extern "C" void user_init(void) gdb_init(); + /* + * Load partition information. + * Normally this is done in user_pre_init() but if building without WiFi + * (via esp_no_wifi Component) then user_pre_init() is not called as none of the + * SDK-related partitions are required. + * Calling this a second time is a no-op. + */ Storage::initialize(); init(); // User code init } -extern "C" void ICACHE_FLASH_ATTR WEAK_ATTR user_pre_init(void) +// SDK 3+ calls this method to configure partitions +extern "C" void user_pre_init(void) { Storage::initialize(); diff --git a/Sming/Arch/Esp8266/Components/esp_no_wifi/README.rst b/Sming/Arch/Esp8266/Components/esp_no_wifi/README.rst index a81ca6fc75..2127075d74 100644 --- a/Sming/Arch/Esp8266/Components/esp_no_wifi/README.rst +++ b/Sming/Arch/Esp8266/Components/esp_no_wifi/README.rst @@ -24,6 +24,12 @@ The SDKnoWiFi implements the startup code and some system functions but contains which is provided by the SDK and in other parts of the Sming framework. We need to provide replacement functions to interoperate correctly with the remaining SDK code. +Advantages +---------- + +- Reduces code image size when networking/WiFi is not required +- Eliminates need for SDK-specific partitions (rf_cal, phy_init, sys_param) + Process ------- diff --git a/Sming/Components/Storage/ota-migration.rst b/Sming/Components/Storage/ota-migration.rst index bcd3044997..f16b951bc8 100644 --- a/Sming/Components/Storage/ota-migration.rst +++ b/Sming/Components/Storage/ota-migration.rst @@ -14,8 +14,11 @@ This function is documented in the NON-OS-SDK guide and was introduced in versio It is called by the SDK before user_init() so nothing else in the framework has yet been initialised, including any C++ static initialisers. -Sming uses this function to read the partition table into memory. -Applications may override this function to perform any custom upgrade operations. +Sming uses this function to read the partition table and pass the required information to the SDK +(via :c:func:`partition_table_regist`). + +By overriding (``wrapping``) this function, applications have the opportunity to perform any +additional checks or upgrades on the partition table before proceeding. Add to application's `component.mk`: @@ -33,20 +36,43 @@ Add this to your application:: { // Note: This file won't exist on initial build! IMPORT_FSTR(partitionTableData, PROJECT_DIR "/out/Esp8266/debug/firmware/partitions.bin") + + // Optionally include updated bootloader + // IMPORT_FSTR(rbootData, PROJECT_DIR "/out/Esp8266/debug/firmware/rboot.bin") } + // Called by SDK extern "C" void __wrap_user_pre_init(void) { + // Make sure code is compiled with expected partition table offset - adjust as required static_assert(PARTITION_TABLE_OFFSET == 0x3fa000, "Bad PTO"); + + // Attempt to load the partition table Storage::initialize(); - auto& flash = *Storage::spiFlash; + + auto& flash = *Storage::spiFlash; if(!flash.partitions()) { - LOAD_FSTR(data, partitionTableData) - flash.erase_range(PARTITION_TABLE_OFFSET, flash.getBlockSize()); - flash.write(PARTITION_TABLE_OFFSET, data, partitionTableData.size()); + /* + * No partitions were found, which implies this device was previously + * running with a pre Sming 4.3 firmware. + */ + // Write our partition table data to flash + { + LOAD_FSTR(data, partitionTableData) + flash.erase_range(PARTITION_TABLE_OFFSET, flash.getBlockSize()); + flash.write(PARTITION_TABLE_OFFSET, data, partitionTableData.size()); + } + + // Load the newly written partition information flash.loadPartitions(PARTITION_TABLE_OFFSET); + + // Optionally update bootloader + // LOAD_FSTR(data, rbootData) + // flash.erase_range(0, flash.getBlockSize()); + // flash.write(0, data, rbootData.size()); } + // Pass control back to Sming extern void __real_user_pre_init(void); __real_user_pre_init(); } @@ -64,6 +90,7 @@ An alternative method is to build the partition table layout in code, so there a // Support updating legacy devices without partition tables (Sming 4.2 and earlier) #ifdef ARCH_ESP8266 + // Need low-level definition for the on-flash `esp_partition_info_t` structure #include extern "C" void __wrap_user_pre_init(void) @@ -77,14 +104,15 @@ An alternative method is to build the partition table layout in code, so there a using FullType = Storage::Partition::FullType; using SubType = Storage::Partition::SubType; #define PT_ENTRY(name, fulltype, offset, size) \ - { ESP_PARTITION_MAGIC, FullType(fulltype).type, FullType(fulltype).subtype, offset, size, name, 0 } + { Storage::ESP_PARTITION_MAGIC, FullType(fulltype).type, FullType(fulltype).subtype, offset, size, name, 0 } + // Amend this layout as required so it corresponds with your existing device static constexpr Storage::esp_partition_info_t partitionTableData[] PROGMEM{ PT_ENTRY("spiFlash", Storage::Device::Type::flash, 0, 0x400000), PT_ENTRY("rom0", SubType::App::ota0, 0x2000, 0xf8000), - PT_ENTRY("rom1", SubType::App::ota1, 0x102000, 0xf8000), - PT_ENTRY("spiffs0", SubType::Data::spiffs, 0x200000, 0xc0000), - PT_ENTRY("spiffs1", SubType::Data::spiffs, 0x2c0000, 0xc0000), + PT_ENTRY("spiffs0", SubType::Data::spiffs, 0x100000, 0xc0000), + PT_ENTRY("rom1", SubType::App::ota1, 0x202000, 0xf8000), + PT_ENTRY("spiffs1", SubType::Data::spiffs, 0x300000, 0xc0000), PT_ENTRY("rf_cal", SubType::Data::rfCal, 0x3fb000, 0x1000), PT_ENTRY("phy_init", SubType::Data::phy, 0x3fc000, 0x1000), PT_ENTRY("sys_param", SubType::Data::sysParam, 0x3fd000, 0x3000), @@ -103,5 +131,6 @@ An alternative method is to build the partition table layout in code, so there a #endif // ARCH_ESP8266 +This example is based on a typical Sming 4.0 4MByte flash layout as for the ``Basic_rBoot`` sample application. The above examples are provided as templates and should be modified as required and tested thoroughly! diff --git a/Sming/Components/Storage/src/PartitionStream.cpp b/Sming/Components/Storage/src/PartitionStream.cpp index beec49dd7d..07980de01b 100644 --- a/Sming/Components/Storage/src/PartitionStream.cpp +++ b/Sming/Components/Storage/src/PartitionStream.cpp @@ -46,12 +46,16 @@ int PartitionStream::seekFrom(int offset, SeekOrigin origin) size_t PartitionStream::write(const uint8_t* data, size_t length) { + if(mode < Mode::Write) { + return 0; + } + auto len = std::min(size_t(size - writePos), length); if(len == 0) { return 0; } - if(blockErase) { + if(mode == Mode::BlockErase) { auto endPos = writePos + len; if(endPos > erasePos) { size_t blockSize = partition.getBlockSize(); diff --git a/Sming/Components/Storage/src/include/Storage/PartitionStream.h b/Sming/Components/Storage/src/include/Storage/PartitionStream.h index af0b02ddd5..41b5cd9070 100644 --- a/Sming/Components/Storage/src/include/Storage/PartitionStream.h +++ b/Sming/Components/Storage/src/include/Storage/PartitionStream.h @@ -15,6 +15,12 @@ namespace Storage { +enum class Mode { + ReadOnly, + Write, ///< Write but do not erase, region should be pre-erased + BlockErase, ///< Erase blocks as required before writing +}; + /** * @brief Stream operating directory on a Storage partition * @@ -33,9 +39,11 @@ class PartitionStream : public ReadWriteStream * @param blockErase Set to true to erase blocks before writing * * If blockErase is false then region must be pre-erased before writing. + * + * @deprecated Use `mode` parameter instead of `blockErase` */ - PartitionStream(Partition partition, storage_size_t offset, size_t size, bool blockErase = false) - : partition(partition), startOffset(offset), size(size), blockErase(blockErase) + SMING_DEPRECATED PartitionStream(Partition partition, storage_size_t offset, size_t size, bool blockErase) + : PartitionStream(partition, offset, size, blockErase ? Mode::BlockErase : Mode::ReadOnly) { } @@ -45,9 +53,38 @@ class PartitionStream : public ReadWriteStream * @param blockErase Set to true to erase blocks before writing * * If blockErase is false then partition must be pre-erased before writing. + * + * @deprecated Use `mode` parameter instead of `blockErase` + */ + SMING_DEPRECATED PartitionStream(Partition partition, bool blockErase) + : PartitionStream(partition, blockErase ? Mode::BlockErase : Mode::ReadOnly) + { + } + + /** + * @brief Access part of a partition using a stream + * @param partition + * @param offset Limit access to this starting offset + * @param size Limit access to this number of bytes from starting offset + * @param mode + * @note When writing in Mode::BlockErase, block erasure is only performed at the + * start of each block. Therefore if `offset` is not a block boundary then the corresponding + * block will *not* be erased first. + */ + PartitionStream(Partition partition, storage_size_t offset, size_t size, Mode mode = Mode::ReadOnly) + : partition(partition), startOffset(offset), size(size), mode(mode) + { + } + + /** + * @brief Access entire partition using stream + * @param partition + * @param mode + * + * If blockErase is false then partition must be pre-erased before writing. */ - PartitionStream(Partition partition, bool blockErase = false) - : partition(partition), startOffset{0}, size(partition.size()), blockErase(blockErase) + PartitionStream(Partition partition, Mode mode = Mode::ReadOnly) + : partition(partition), startOffset{0}, size(partition.size()), mode(mode) { } @@ -74,7 +111,7 @@ class PartitionStream : public ReadWriteStream uint32_t writePos{0}; uint32_t readPos{0}; uint32_t erasePos{0}; - bool blockErase; + Mode mode; }; } // namespace Storage diff --git a/Sming/Components/rboot/README.rst b/Sming/Components/rboot/README.rst index 95ddeb0203..7ce6c629ef 100644 --- a/Sming/Components/rboot/README.rst +++ b/Sming/Components/rboot/README.rst @@ -104,7 +104,7 @@ To enable slot 2, set these values: "address": "0x100000", "size": "512K", "type": "app", - "subtype": "ota_1" + "subtype": "ota_2" } } } diff --git a/Sming/Components/rboot/rboot b/Sming/Components/rboot/rboot index cc00071f59..4159458fe2 160000 --- a/Sming/Components/rboot/rboot +++ b/Sming/Components/rboot/rboot @@ -1 +1 @@ -Subproject commit cc00071f593ef7d55c0c0a7b97828173f9cda89b +Subproject commit 4159458fe25f93399a204e73d99331c37536f288 diff --git a/docs/source/information/flash.rst b/docs/source/information/flash.rst index 884da9ccc4..e999b304ac 100644 --- a/docs/source/information/flash.rst +++ b/docs/source/information/flash.rst @@ -21,16 +21,15 @@ A typical layout for a 4MByte device might look like this: (hex) (if any) (KB) (if applicable) ======= =============== ==== ========================= =================================================== 000000 1 rboot.bin Boot loader - 001000 4 rBoot configuration - 002000 4 Partition table - 003000 4 esp_init_data_default.bin PHY configuration data - 004000 12 blank.bin System parameter area - 006000 4 blank.bin RF Calibration data (Initialised to FFh) - 006000 4 Reserved - 008000 ROM_0_ADDR rom0.bin First ROM image - 100000 RBOOT_SPIFFS_0 - 208000 ROM_1_ADDR rom1.bin Second ROM image + 001000 4 rBoot configuration + 002000 ROM_0_ADDR rom0.bin First ROM image + 102000 ROM_1_ADDR rom1.bin Second ROM image + 200000 RBOOT_SPIFFS_0 300000 RBOOT_SPIFFS_1 + 3FA000 4 Partition table + 3FB000 4 blank.bin RF Calibration data (Initialised to FFh) + 3FC000 4 esp_init_data_default.bin PHY configuration data + 3FD000 12 blank.bin System parameter area ======= =============== ==== ========================= =================================================== diff --git a/docs/source/upgrading/5.1-5.2.rst b/docs/source/upgrading/5.1-5.2.rst new file mode 100644 index 0000000000..0dcbfa40f3 --- /dev/null +++ b/docs/source/upgrading/5.1-5.2.rst @@ -0,0 +1,13 @@ +From v5.1 to v5.2 +================= + +.. highlight:: c++ + +**Breaking change** + +The :cpp:class:`Storage::PartitionStream` constructors with ``blockErase`` parameter have been deprecated. +The intended default behaviour is read-only, however previously this also allowed writing without block erase. +This can result in corrupted flash contents where the flash has not been explicitly erased beforehand. + +The new constructors instead use a :cpp:enum:`Storage::Mode` so behaviour is more explicit. +The default is read-only and writes will now be failed. diff --git a/samples/Basic_Ota/app/application.cpp b/samples/Basic_Ota/app/application.cpp index 8b446c70b6..2663252230 100644 --- a/samples/Basic_Ota/app/application.cpp +++ b/samples/Basic_Ota/app/application.cpp @@ -54,6 +54,16 @@ void doUpgrade() // select rom slot to flash auto part = ota.getNextBootPartition(); + /* + * Applications should always include a sanity check to ensure partitions being updated are + * not in use. This should always included the application partition but should also consider + * filing system partitions, etc. which may be actively in use. + */ + if(part == ota.getRunningPartition()) { + Serial << F("May be running in temporary mode. Please reboot and try again.") << endl; + return; + } + #ifndef RBOOT_TWO_ROMS // flash rom to position indicated in the rBoot config rom table otaUpdater->addItem(ROM_0_URL, part); @@ -67,7 +77,8 @@ void doUpgrade() auto spiffsPart = findSpiffsPartition(part); if(spiffsPart) { // use user supplied values (defaults for 4mb flash in hardware config) - otaUpdater->addItem(SPIFFS_URL, spiffsPart, new Storage::PartitionStream(spiffsPart)); + otaUpdater->addItem(SPIFFS_URL, spiffsPart, + new Storage::PartitionStream(spiffsPart, Storage::Mode::BlockErase)); } // request switch and reboot on success From 415241384f8dcdb92761c5b541f5e64a4709d59b Mon Sep 17 00:00:00 2001 From: Mike Date: Mon, 18 Mar 2024 09:50:39 +0000 Subject: [PATCH 024/128] USB library broken with ESP32 IDF 5.2 (#2733) Remove from supported SOC list for this version until fix implemented. --- Sming/Libraries/USB | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sming/Libraries/USB b/Sming/Libraries/USB index ae08687c90..8df168d42a 160000 --- a/Sming/Libraries/USB +++ b/Sming/Libraries/USB @@ -1 +1 @@ -Subproject commit ae08687c900582a94db86d4cff1c492317e52307 +Subproject commit 8df168d42ad4e595398541255e4a0b791f04fda9 From 1054a0c249efb18d734298350a6a951d21655aa2 Mon Sep 17 00:00:00 2001 From: Mike Date: Mon, 18 Mar 2024 09:53:25 +0000 Subject: [PATCH 025/128] Update CI build to ubuntu-latest (22.04) (#2735) Use ubuntu-latest (22.04) for CI **Add repo for clang** Keep using clang-8 **Use default gcc (11)** Was gcc 9 **Fix FatIFS library test failure** exfat-utils has moved to exfatprogs kernel contains exfat support, but requires linux-modules-extra-azure Recent versions of fsck.vfat require volume label matches boot sector **Explicitly use python 3.12 for CI** Default is python 3.8 which is a bit old. ESP32 IDF 4.3/4.4 breaks with later versions but a sensible default is 3.12. --- .github/workflows/ci-esp32.yml | 13 +++++++++---- .github/workflows/ci.yml | 15 ++++++++++----- .github/workflows/library.yml | 23 ++++++++++++++--------- Sming/Components/IFS | 2 +- Sming/Libraries/FatIFS | 2 +- Tools/install.sh | 10 +++++----- 6 files changed, 40 insertions(+), 25 deletions(-) diff --git a/.github/workflows/ci-esp32.yml b/.github/workflows/ci-esp32.yml index 4796499abc..f849ea2033 100644 --- a/.github/workflows/ci-esp32.yml +++ b/.github/workflows/ci-esp32.yml @@ -11,7 +11,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-20.04, windows-latest] + os: [ubuntu-latest, windows-latest] variant: [esp32, esp32s2, esp32c3, esp32s3, esp32c2] idf_version: ["4.3", "4.4", "5.0", "5.2"] exclude: @@ -39,7 +39,12 @@ jobs: git config --global --add core.autocrlf input - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 + + - name: Setup python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.idf_version == '4.3' && '3.8' || '3.12' }} - name: Configure environment shell: pwsh @@ -48,7 +53,7 @@ jobs: "SMING_HOME=" + (Resolve-Path "Sming").path >> $env:GITHUB_ENV - name: Install build tools for Ubuntu - if: ${{ matrix.os == 'ubuntu-20.04' }} + if: ${{ matrix.os == 'ubuntu-latest' }} run: | Tools/ci/install.sh @@ -59,7 +64,7 @@ jobs: Tools/ci/install.cmd - name: Build and test for ${{matrix.variant}} with IDF v${{matrix.idf_version}} on Ubuntu - if: ${{ matrix.os == 'ubuntu-20.04' }} + if: ${{ matrix.os == 'ubuntu-latest' }} run: | source $SMING_HOME/../Tools/export.sh Tools/ci/build.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7173fb515d..f565d8a942 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-20.04, windows-latest] + os: [ubuntu-latest, windows-latest] variant: [esp8266, host, rp2040] include: - variant: esp8266 @@ -37,7 +37,12 @@ jobs: git config --global --add core.autocrlf input - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 + + - name: Setup python + uses: actions/setup-python@v5 + with: + python-version: "3.12" - name: Configure environment shell: pwsh @@ -46,7 +51,7 @@ jobs: "SMING_HOME=" + (Resolve-Path "Sming").path >> $env:GITHUB_ENV - name: Install build tools for Ubuntu - if: ${{ matrix.os == 'ubuntu-20.04' }} + if: ${{ matrix.os == 'ubuntu-latest' }} run: | Tools/ci/install.sh @@ -57,9 +62,9 @@ jobs: Tools/ci/install.cmd - name: Build and test for ${{matrix.variant}} on Ubuntu - env: + env: CLANG_FORMAT: clang-format-8 - if: ${{ matrix.os == 'ubuntu-20.04' }} + if: ${{ matrix.os == 'ubuntu-latest' }} run: | source $SMING_HOME/../Tools/export.sh $CLANG_FORMAT --version diff --git a/.github/workflows/library.yml b/.github/workflows/library.yml index 531b04f0a6..55c26fd07c 100644 --- a/.github/workflows/library.yml +++ b/.github/workflows/library.yml @@ -21,7 +21,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-20.04, windows-latest] + os: [ubuntu-latest, windows-latest] variant: [esp8266, host, esp32, esp32s2, esp32c3, rp2040] include: - variant: esp8266 @@ -49,7 +49,12 @@ jobs: git config --global --add core.autocrlf input - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 + + - name: Setup python + uses: actions/setup-python@v5 + with: + python-version: "3.12" - name: Create library alias if: ${{ inputs.alias }} @@ -71,23 +76,23 @@ jobs: "SMING_SOC=${{ matrix.variant }}" >> $env:GITHUB_ENV - name: Install build tools for Ubuntu - if: ${{ matrix.os == 'ubuntu-20.04' }} + if: ${{ matrix.os == 'ubuntu-latest' }} run: | - . $SMING_HOME/../Tools/export.sh - $SMING_HOME/../Tools/ci/install.sh $SMING_ARCH + $SMING_HOME/../Tools/ci/install.sh - name: Install build tools for Windows if: ${{ matrix.os == 'windows-latest' }} run: | - . "$env:SMING_HOME/../Tools/ci/setenv.ps1" - . "$env:SMING_HOME/../Tools/ci/install.cmd" + cd $env:SMING_HOME/.. + . Tools/ci/setenv.ps1 + Tools/ci/install.cmd - name: Build and Test for ${{matrix.arch}} on Ubuntu env: CLANG_FORMAT: clang-format-8 - if: ${{ matrix.os == 'ubuntu-20.04' }} + if: ${{ matrix.os == 'ubuntu-latest' }} run: | - . $SMING_HOME/../Tools/export.sh + source $SMING_HOME/../Tools/export.sh make -j$(nproc) -f $CI_MAKEFILE - name: Build and Test for ${{matrix.arch}} on Windows diff --git a/Sming/Components/IFS b/Sming/Components/IFS index 36637f4e20..ec9a902121 160000 --- a/Sming/Components/IFS +++ b/Sming/Components/IFS @@ -1 +1 @@ -Subproject commit 36637f4e207e14d2484b9b827c573f1ba54392cd +Subproject commit ec9a902121afaebd882b7627907e8fffa78c7b5a diff --git a/Sming/Libraries/FatIFS b/Sming/Libraries/FatIFS index b4c11f4f28..33474a7d56 160000 --- a/Sming/Libraries/FatIFS +++ b/Sming/Libraries/FatIFS @@ -1 +1 @@ -Subproject commit b4c11f4f28deb99fe48c25a16f0f72915202c05d +Subproject commit 33474a7d560b6b27bbb0b1effa4b06c96361ff88 diff --git a/Tools/install.sh b/Tools/install.sh index 939aa68942..555edbc7e2 100755 --- a/Tools/install.sh +++ b/Tools/install.sh @@ -96,18 +96,18 @@ fi if [ -n "$APPVEYOR" ] || [ -n "$GITHUB_ACTION" ]; then + # Provide repo. for clang-format-8 on Ubuntu 22.04 + sudo apt-add-repository -y 'deb http://mirrors.kernel.org/ubuntu focal main universe' sudo apt-get -y update $PKG_INSTALL \ clang-format-8 \ - g++-9-multilib \ + g++-multilib \ python3-setuptools \ ninja-build \ - exfat-fuse \ - exfat-utils \ + linux-modules-extra-azure \ + exfatprogs \ $EXTRA_PACKAGES - sudo update-alternatives --set gcc /usr/bin/gcc-9 - else MACHINE_PACKAGES="" From b9029a20185b9f69a27f03b0c6353f3153775f4a Mon Sep 17 00:00:00 2001 From: Mike Date: Mon, 18 Mar 2024 09:56:15 +0000 Subject: [PATCH 026/128] Fix esp8266 bootloader building and add diagnostic tools (#2734) This PR focuses on an issue discovered in #2727 when attempting to update the esp8266 boot partition from code during a firmware update. It was discovered that doing this renders the device unbootable because the boot sector header information is incorrect. This is because the required `SPI_SIZE`, `SPI_MODE` and `SPI_FREQ` values were never passed to the rboot makefile when building. When flashing the boot partition normally, these parameters get passed to esptool, which modifies the provided boot image. I wasn't aware until now that it did this! **Build rboot as variant** The solution to the main problem is to build rboot as a variant - see https://sming.readthedocs.io/en/latest/_inc/Sming/building.html#library-variants. The location of the bootloader firmware (in `out/Esp8266/debug/firmware/rboot.bin`) is unchanged, but is overwritten by the appropriate variant after every build. This should ensure consistency with selected settings. **Remove RBOOT_INTEGRATION** As we've forked rboot some time ago this isn't necessary, so we can simplify the code a bit. **Update esptool to v4.6.2 (from v4.4)** Version 4.7 has issues with earlier python versions and breaks with IDF 4.4/4.3. **Add `bootinfo` build target** Adds `make bootinfo` for esp architectures which shows details of the boot partition, including header fields so we can check it's correct. **Add `esptool` build target** Makes for easier access to esptool. --- Sming/Arch/Esp32/app.mk | 8 ++++ Sming/Components/esptool/component.mk | 8 ++++ Sming/Components/esptool/esptool | 2 +- Sming/Components/rboot/component.mk | 44 ++++++++++++++----- .../rboot/include/rboot-integration.h | 13 ------ Sming/Components/rboot/rboot | 2 +- 6 files changed, 51 insertions(+), 26 deletions(-) delete mode 100644 Sming/Components/rboot/include/rboot-integration.h diff --git a/Sming/Arch/Esp32/app.mk b/Sming/Arch/Esp32/app.mk index 8fc7a3c151..32f75955d9 100644 --- a/Sming/Arch/Esp32/app.mk +++ b/Sming/Arch/Esp32/app.mk @@ -38,3 +38,11 @@ endif $(TARGET_BIN): $(TARGET_OUT) $(Q) $(ESPTOOL_CMDLINE) elf2image --min-rev $(CHIP_REV_MIN) --elf-sha256-offset 0xb0 $(ESPTOOL_EXTRA_ARGS) $(flashimageoptions) -o $@ $< + + +##@Flashing + +.PHONY: bootinfo +bootinfo: $(FLASH_BOOT_LOADER) ##Show bootloader information + $(info $(FLASH_BOOT_LOADER):) + $(Q) $(ESPTOOL_CMDLINE) image_info -v2 $(FLASH_BOOT_LOADER) diff --git a/Sming/Components/esptool/component.mk b/Sming/Components/esptool/component.mk index 3f82c8e219..330f82a5b1 100644 --- a/Sming/Components/esptool/component.mk +++ b/Sming/Components/esptool/component.mk @@ -86,3 +86,11 @@ endef define EraseFlash $(call ESPTOOL_EXECUTE,erase_flash) endef + + +##@Flashing + +.PHONY: esptool +esptool: ##Pass options to esptool, e.g. `make esptool -- --help` or `make esptool image_info` + $(Q) $(ESPTOOL_CMDLINE) $(filter-out $@,$(MAKECMDGOALS)) + diff --git a/Sming/Components/esptool/esptool b/Sming/Components/esptool/esptool index b1fce755be..8c5f47f8d0 160000 --- a/Sming/Components/esptool/esptool +++ b/Sming/Components/esptool/esptool @@ -1 +1 @@ -Subproject commit b1fce755be2f2c66161e309649aefa4b21604de9 +Subproject commit 8c5f47f8d0be6bbfc2668791653fc8af75f3cd2b diff --git a/Sming/Components/rboot/component.mk b/Sming/Components/rboot/component.mk index a11795b443..6670b0f5af 100644 --- a/Sming/Components/rboot/component.mk +++ b/Sming/Components/rboot/component.mk @@ -30,6 +30,17 @@ endif # => APP +# Build bootloader variant based on these variable values +RBOOT_VARS := \ + PARTITION_TABLE_OFFSET \ + RBOOT_RTC_ENABLED \ + RBOOT_GPIO_ENABLED \ + RBOOT_GPIO_SKIP_ENABLED \ + RBOOT_SILENT \ + SPI_SPEED \ + SPI_MODE \ + SPI_SIZE + # rBoot options CONFIG_VARS += RBOOT_RTC_ENABLED RBOOT_GPIO_ENABLED RBOOT_GPIO_SKIP_ENABLED RBOOT_RTC_ENABLED ?= 0 @@ -102,12 +113,6 @@ RBOOT_ROM_1_BIN := $(FW_BASE)/$(RBOOT_ROM_1).bin COMPONENT_APPCODE := rboot/appcode -APP_CFLAGS += -DRBOOT_INTEGRATION - -# these are exported for use by the rBoot Makefile -export RBOOT_BUILD_BASE := $(abspath $(COMPONENT_BUILD_DIR)) -export RBOOT_FW_BASE := $(abspath $(FW_BASE)) -export ESPTOOL2 # multiple roms per 1mb block? ifeq ($(RBOOT_TWO_ROMS),1) @@ -143,12 +148,20 @@ ifdef RBOOT_EMULATION FLASH_BOOT_CHUNKS = 0x00000=$(BLANK_BIN) FLASH_RBOOT_ERASE_CONFIG_CHUNKS := 0x01000=$(BLANK_BIN) else -export RBOOT_ROM0_ADDR -export RBOOT_ROM1_ADDR +export ESPTOOL2 +export $(RBOOT_VARS) +RBOOT_VARSTR := $(foreach v,$(RBOOT_VARS),$v=$($v)) +RBOOT_HASH := $(call CalculateVariantHash,RBOOT_VARSTR) +export RBOOT_BUILD_BASE := $(COMPONENT_BUILD_BASE)/$(RBOOT_HASH) +RBOOT_VAR_BIN := $(RBOOT_BUILD_BASE)/rboot.bin RBOOT_BIN := $(FW_BASE)/rboot.bin -CUSTOM_TARGETS += $(RBOOT_BIN) -$(RBOOT_BIN): - $(Q) $(MAKE) -C $(RBOOT_DIR)/rboot PARTITION_TABLE_OFFSET=$(PARTITION_TABLE_OFFSET) $(RBOOT_CFLAGS) +CUSTOM_TARGETS += build_rboot +$(RBOOT_VAR_BIN): $(ESPTOOL2) + $(Q) $(MAKE) -C $(RBOOT_DIR)/rboot $(RBOOT_CFLAGS) + +.PHONY: build_rboot +build_rboot: $(RBOOT_VAR_BIN) + cp $(RBOOT_VAR_BIN) $(RBOOT_BIN) EXTRA_LDFLAGS := -u Cache_Read_Enable_New @@ -207,4 +220,13 @@ $(RBOOT_ROM_1_BIN): $(TARGET_OUT_1) endif + +##@Flashing + +.PHONY: bootinfo +bootinfo: ##Show bootloader information + $(info $(RBOOT_BIN):) + $(Q) $(ESPTOOL_CMDLINE) image_info -v2 $(RBOOT_BIN) + + endif # RBOOT_EMULATION diff --git a/Sming/Components/rboot/include/rboot-integration.h b/Sming/Components/rboot/include/rboot-integration.h deleted file mode 100644 index f67a97b8d1..0000000000 --- a/Sming/Components/rboot/include/rboot-integration.h +++ /dev/null @@ -1,13 +0,0 @@ -/**** - * Sming Framework Project - Open Source framework for high efficiency native ESP8266 development. - * Created 2015 by Skurydin Alexey - * http://github.com/SmingHub/Sming - * All files of the Sming Core are provided under the LGPL v3 license. - * - * rboot-integration.h - * - ****/ - -#pragma once - -#include diff --git a/Sming/Components/rboot/rboot b/Sming/Components/rboot/rboot index 4159458fe2..b397c5b0c7 160000 --- a/Sming/Components/rboot/rboot +++ b/Sming/Components/rboot/rboot @@ -1 +1 @@ -Subproject commit 4159458fe25f93399a204e73d99331c37536f288 +Subproject commit b397c5b0c75f911b2fe1ebdde20d1567ab42f756 From 07cd096a300bea17e7a22e51b9b7a14325963838 Mon Sep 17 00:00:00 2001 From: Mike Date: Mon, 18 Mar 2024 13:27:10 +0000 Subject: [PATCH 027/128] Use `std::make_unique` (#2736) This PR improves use of `std::unique_ptr` by using `std::make_unique` where appropriate instead of `reset()`, and also updating some class constructors to use direct assignment. https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#r23-use-make_unique-to-make-unique_ptrs * Use `std::make_unique` and assignment instead of reset() * Revert changes where allocations shouldn't be default-initialised --- .../Esp32/Services/Profiling/TaskStat.cpp | 2 +- .../Arch/Host/Components/driver/hw_timer.cpp | 2 +- .../Host/Components/driver/uart_server.cpp | 6 +-- Sming/Arch/Rp2040/Components/rp2040/src/.cs | 0 .../rp2040/src/include/esp_systemapi.h | 1 - .../Rp2040/Components/rp2040/src/tasks.cpp | 12 ++--- .../Rp2040/Components/rp2040/src/wifi.cpp | 50 +++++++++---------- .../Hosted/Transport/TcpClientTransport.h | 3 +- .../Arch/Esp32/Platform/StationImpl.cpp | 2 +- .../Arch/Esp8266/Platform/AccessPointImpl.cpp | 2 +- .../rboot/include/Network/RbootHttpUpdater.h | 5 +- Sming/Components/ssl/Axtls/AxConnection.h | 2 +- .../ssl/BearSsl/BrClientConnection.cpp | 4 +- Sming/Components/ssl/src/Session.cpp | 2 +- Sming/Core/Data/CsvReader.h | 3 +- .../Core/Data/Stream/EndlessMemoryStream.cpp | 2 +- Sming/Core/Data/Stream/SectionStream.cpp | 2 +- Sming/Libraries/DiskStorage | 2 +- Sming/Libraries/HardwareSPI | 2 +- Sming/Libraries/IOControl | 2 +- .../src/include/Ota/Network/HttpUpgrader.h | 2 +- Sming/Libraries/Yeelight/YeelightBulb.cpp | 2 +- samples/Basic_Audio/app/application.cpp | 2 +- samples/Basic_Ota/app/application.cpp | 2 +- .../Basic_Serial/include/SerialTransmitDemo.h | 3 +- 25 files changed, 56 insertions(+), 61 deletions(-) create mode 100644 Sming/Arch/Rp2040/Components/rp2040/src/.cs diff --git a/Sming/Arch/Esp32/Services/Profiling/TaskStat.cpp b/Sming/Arch/Esp32/Services/Profiling/TaskStat.cpp index 97a69e98db..8a83c1e20b 100644 --- a/Sming/Arch/Esp32/Services/Profiling/TaskStat.cpp +++ b/Sming/Arch/Esp32/Services/Profiling/TaskStat.cpp @@ -27,7 +27,7 @@ struct TaskStat::Info { TaskStat::TaskStat(Print& out) : out(out) { #if CONFIG_FREERTOS_USE_TRACE_FACILITY && CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS - taskInfo.reset(new Info[2]); + taskInfo = std::make_unique(2); #endif } diff --git a/Sming/Arch/Host/Components/driver/hw_timer.cpp b/Sming/Arch/Host/Components/driver/hw_timer.cpp index e0d8f2c28c..0ceacae8fa 100644 --- a/Sming/Arch/Host/Components/driver/hw_timer.cpp +++ b/Sming/Arch/Host/Components/driver/hw_timer.cpp @@ -205,7 +205,7 @@ static std::unique_ptr timer1; void hw_timer_init(void) { - timer1.reset(new CTimerThread("Timer1")); + timer1 = std::make_unique("Timer1"); } void hw_timer_cleanup() diff --git a/Sming/Arch/Host/Components/driver/uart_server.cpp b/Sming/Arch/Host/Components/driver/uart_server.cpp index aadc360bd1..86bcdb8ed3 100644 --- a/Sming/Arch/Host/Components/driver/uart_server.cpp +++ b/Sming/Arch/Host/Components/driver/uart_server.cpp @@ -170,9 +170,9 @@ void startup(const Config& config) auto& server = servers[i]; if(devname == nullptr) { - server.reset(new CUartPort(i)); + server = std::make_unique(i); } else { - server.reset(new CUartDevice(i, devname, config.baud[i])); + server = std::make_unique(i, devname, config.baud[i]); } server->execute(); } @@ -450,7 +450,7 @@ void* CUartDevice::thread_routine() while(!done) { if(txsem.timedwait(IDLE_SLEEP_MS * 1000)) { if(uart != nullptr && !device) { - device.reset(new SerialDevice); + device = std::make_unique(); char res = device->openDevice(deviceName, uart->baud_rate); if(res != 1) { host_debug_e("UART%u error %d opening serial device '%s'", uart_nr, res, deviceName); diff --git a/Sming/Arch/Rp2040/Components/rp2040/src/.cs b/Sming/Arch/Rp2040/Components/rp2040/src/.cs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Sming/Arch/Rp2040/Components/rp2040/src/include/esp_systemapi.h b/Sming/Arch/Rp2040/Components/rp2040/src/include/esp_systemapi.h index b3b9dc7a59..071ee8ecfe 100644 --- a/Sming/Arch/Rp2040/Components/rp2040/src/include/esp_systemapi.h +++ b/Sming/Arch/Rp2040/Components/rp2040/src/include/esp_systemapi.h @@ -32,7 +32,6 @@ extern void ets_wdt_enable(void); extern void ets_wdt_disable(void); extern void wdt_feed(void); - /** @brief Disable interrupts * @retval Current interrupt level * @note Hardware timer is unaffected if operating in non-maskable mode diff --git a/Sming/Arch/Rp2040/Components/rp2040/src/tasks.cpp b/Sming/Arch/Rp2040/Components/rp2040/src/tasks.cpp index cabd0903fe..64fd7b6164 100644 --- a/Sming/Arch/Rp2040/Components/rp2040/src/tasks.cpp +++ b/Sming/Arch/Rp2040/Components/rp2040/src/tasks.cpp @@ -4,17 +4,17 @@ #include #include -namespace { - +namespace +{ class TaskQueue { public: static void init() { - spinlock = spin_lock_claim_unused(true); + spinlock = spin_lock_claim_unused(true); } - TaskQueue(os_task_t callback, uint8_t length): callback(callback) + TaskQueue(os_task_t callback, uint8_t length) : callback(callback) { queue_init_with_spinlock(&queue, sizeof(os_event_t), length, spinlock); } @@ -26,7 +26,7 @@ class TaskQueue bool __forceinline post(os_signal_t sig, os_param_t par) { - os_event_t event {sig, par}; + os_event_t event{sig, par}; return queue_try_add(&queue, &event); } @@ -60,7 +60,7 @@ const uint8_t SYSTEM_TASK_QUEUE_LENGTH = 8; TaskQueue* task_queues[SYSTEM_TASK_PRIO + 1]; -}; +}; // namespace bool system_os_task(os_task_t callback, os_task_priority_t prio, os_event_t* events, uint8_t qlen) { diff --git a/Sming/Arch/Rp2040/Components/rp2040/src/wifi.cpp b/Sming/Arch/Rp2040/Components/rp2040/src/wifi.cpp index 4c87e488a7..d048995da7 100644 --- a/Sming/Arch/Rp2040/Components/rp2040/src/wifi.cpp +++ b/Sming/Arch/Rp2040/Components/rp2040/src/wifi.cpp @@ -19,21 +19,20 @@ extern "C" { IMPORT_FSTR_ARRAY_LOCAL(cyw43_firmware, uint8_t, CYW43_FIRMWARE) #endif -namespace { - +namespace +{ #define BUFFER_SIZE 16384 #define DICT_SIZE 32767 -class Decompressor { +class Decompressor +{ public: - explicit Decompressor(const FSTR::ObjectBase& data) + explicit Decompressor(const FSTR::ObjectBase& data) : stream(new FlashMemoryStream(data)) { - stream.reset(new FlashMemoryStream(data)); } - explicit Decompressor(Storage::Partition part) + explicit Decompressor(Storage::Partition part) : stream(new Storage::PartitionStream(part)) { - stream.reset(new Storage::PartitionStream(part)); } bool init() @@ -41,24 +40,24 @@ class Decompressor { uzlib_init(); uzlib_uncompress_init(&state, dict, DICT_SIZE); state.source_read_cb = read_source; - int res = uzlib_gzip_parse_header(&state); - if (res != TINF_OK) { + int res = uzlib_gzip_parse_header(&state); + if(res != TINF_OK) { debug_e("[CYW] bad GZIP header %d", res); return false; } - return true; + return true; } bool read(void* dest, size_t length) { - state.dest = static_cast(dest); - state.dest_limit = state.dest + length; - int res = uzlib_uncompress_chksum(&state); + state.dest = static_cast(dest); + state.dest_limit = state.dest + length; + int res = uzlib_uncompress_chksum(&state); if(res != TINF_OK) { debug_e("[CYW] Decompress error %d", res); return false; } - return true; + return true; } private: @@ -77,35 +76,36 @@ class Decompressor { return *self->state.source++; } - struct uzlib_uncomp state{}; + struct uzlib_uncomp state { + }; uint8_t src_buffer[BUFFER_SIZE]{}; - uint8_t dict[DICT_SIZE]; + uint8_t dict[DICT_SIZE]; std::unique_ptr stream; }; std::unique_ptr decompressor; -} +} // namespace int cyw43_storage_init() { #ifdef CYW43_FIRMWARE - decompressor.reset(new Decompressor(cyw43_firmware)); + decompressor = std::make_unique(cyw43_firmware); #else auto part = Storage::findPartition("cyw43_fw"); if(!part) { debug_e("Failed to find CYW43 firmware partition"); } else { - decompressor.reset(new Decompressor(part)); + decompressor = std::make_unique(part); } #endif - if (!decompressor || !decompressor->init()) { + if(!decompressor || !decompressor->init()) { decompressor.reset(); - return -1; - } + return -1; + } - return 0; + return 0; } uint32_t cyw43_storage_read(void* dest, uint32_t length) @@ -117,9 +117,9 @@ uint32_t cyw43_storage_read(void* dest, uint32_t length) if(!decompressor->read(dest, length)) { decompressor.reset(); return 0; - } + } - return length; + return length; } void cyw43_storage_cleanup() diff --git a/Sming/Components/Hosted/include/Hosted/Transport/TcpClientTransport.h b/Sming/Components/Hosted/include/Hosted/Transport/TcpClientTransport.h index 4ab6e1c9ac..0971b0c496 100644 --- a/Sming/Components/Hosted/include/Hosted/Transport/TcpClientTransport.h +++ b/Sming/Components/Hosted/include/Hosted/Transport/TcpClientTransport.h @@ -25,10 +25,9 @@ namespace Transport class TcpClientTransport : public TcpTransport { public: - TcpClientTransport(TcpClient& client) + TcpClientTransport(TcpClient& client) : stream(new TcpClientStream(client)) { client.setReceiveDelegate(TcpClientDataDelegate(&TcpClientTransport::process, this)); - stream.reset(new TcpClientStream(client)); } protected: diff --git a/Sming/Components/Network/Arch/Esp32/Platform/StationImpl.cpp b/Sming/Components/Network/Arch/Esp32/Platform/StationImpl.cpp index 73921ae8e7..303dea5fb6 100644 --- a/Sming/Components/Network/Arch/Esp32/Platform/StationImpl.cpp +++ b/Sming/Components/Network/Arch/Esp32/Platform/StationImpl.cpp @@ -522,7 +522,7 @@ bool StationImpl::smartConfigStart(SmartConfigType sctype, SmartConfigDelegate c return false; } - smartConfigEventInfo.reset(new SmartConfigEventInfo{}); + smartConfigEventInfo = std::make_unique(); if(!smartConfigEventInfo) { return false; } diff --git a/Sming/Components/Network/Arch/Esp8266/Platform/AccessPointImpl.cpp b/Sming/Components/Network/Arch/Esp8266/Platform/AccessPointImpl.cpp index 775dc07f70..d83ef86dd3 100644 --- a/Sming/Components/Network/Arch/Esp8266/Platform/AccessPointImpl.cpp +++ b/Sming/Components/Network/Arch/Esp8266/Platform/AccessPointImpl.cpp @@ -82,7 +82,7 @@ bool AccessPointImpl::config(const String& ssid, String password, AUTH_MODE mode debugf("AP configuration was updated"); } else { debugf("Set AP configuration in background"); - runConfig.reset(new softap_config(config)); + runConfig = std::make_unique(config); } } else { debugf("AP configuration loaded"); diff --git a/Sming/Components/rboot/include/Network/RbootHttpUpdater.h b/Sming/Components/rboot/include/Network/RbootHttpUpdater.h index c17ba2db7b..fda0ce60c8 100644 --- a/Sming/Components/rboot/include/Network/RbootHttpUpdater.h +++ b/Sming/Components/rboot/include/Network/RbootHttpUpdater.h @@ -37,15 +37,14 @@ class RbootHttpUpdater : protected HttpClient std::unique_ptr stream; // (optional) output stream to use. Item(String url, uint32_t targetOffset, size_t size, RbootOutputStream* stream) - : url(url), targetOffset(targetOffset), size(size) + : url(url), targetOffset(targetOffset), size(size), stream(stream) { - this->stream.reset(stream); } RbootOutputStream* getStream() { if(!stream) { - stream.reset(new RbootOutputStream(targetOffset, size)); + stream = std::make_unique(targetOffset, size); } return stream.get(); } diff --git a/Sming/Components/ssl/Axtls/AxConnection.h b/Sming/Components/ssl/Axtls/AxConnection.h index a73b7aedea..1099094b80 100644 --- a/Sming/Components/ssl/Axtls/AxConnection.h +++ b/Sming/Components/ssl/Axtls/AxConnection.h @@ -61,7 +61,7 @@ class AxConnection : public Connection const Certificate* getCertificate() const override { if(!certificate && ssl->x509_ctx != nullptr) { - certificate.reset(new AxCertificate(ssl)); + certificate = std::make_unique(ssl); } return certificate.get(); diff --git a/Sming/Components/ssl/BearSsl/BrClientConnection.cpp b/Sming/Components/ssl/BearSsl/BrClientConnection.cpp index ed41eab55d..35906f9172 100644 --- a/Sming/Components/ssl/BearSsl/BrClientConnection.cpp +++ b/Sming/Components/ssl/BearSsl/BrClientConnection.cpp @@ -69,8 +69,8 @@ void BrClientConnection::startCert(uint32_t length) return; } - certificate.reset(new BrCertificate); - x509Decoder.reset(new X509Decoder(&certificate->subject, &certificate->issuer)); + certificate = std::make_unique(); + x509Decoder = std::make_unique(&certificate->subject, &certificate->issuer); auto& types = context.session.validators.fingerprintTypes; resetHash(certSha1Context, types.contains(Fingerprint::Type::CertSha1)); diff --git a/Sming/Components/ssl/src/Session.cpp b/Sming/Components/ssl/src/Session.cpp index c896799ce5..115325894f 100644 --- a/Sming/Components/ssl/src/Session.cpp +++ b/Sming/Components/ssl/src/Session.cpp @@ -200,7 +200,7 @@ void Session::handshakeComplete(bool success) // If requested, take a copy of the session ID for later re-use if(options.sessionResume) { if(!sessionId) { - sessionId.reset(new SessionId); + sessionId = std::make_unique(); } *sessionId = connection->getSessionId(); } diff --git a/Sming/Core/Data/CsvReader.h b/Sming/Core/Data/CsvReader.h index a8aa9cc151..596a62a27e 100644 --- a/Sming/Core/Data/CsvReader.h +++ b/Sming/Core/Data/CsvReader.h @@ -47,10 +47,9 @@ class CsvReader */ CsvReader(IDataSourceStream* source, char fieldSeparator = ',', const CStringArray& headings = nullptr, size_t maxLineLength = 2048) - : fieldSeparator(fieldSeparator), userHeadingsProvided(headings), maxLineLength(maxLineLength), + : source(source), fieldSeparator(fieldSeparator), userHeadingsProvided(headings), maxLineLength(maxLineLength), headings(headings) { - this->source.reset(source); reset(); } diff --git a/Sming/Core/Data/Stream/EndlessMemoryStream.cpp b/Sming/Core/Data/Stream/EndlessMemoryStream.cpp index 50eb5e7f8a..b5690166ed 100644 --- a/Sming/Core/Data/Stream/EndlessMemoryStream.cpp +++ b/Sming/Core/Data/Stream/EndlessMemoryStream.cpp @@ -27,7 +27,7 @@ bool EndlessMemoryStream::seek(int len) size_t EndlessMemoryStream::write(const uint8_t* buffer, size_t size) { if(!stream) { - stream.reset(new MemoryDataStream()); + stream = std::make_unique(); } return stream->write(buffer, size); diff --git a/Sming/Core/Data/Stream/SectionStream.cpp b/Sming/Core/Data/Stream/SectionStream.cpp index 56e663e52f..9ae9c977f1 100644 --- a/Sming/Core/Data/Stream/SectionStream.cpp +++ b/Sming/Core/Data/Stream/SectionStream.cpp @@ -22,7 +22,7 @@ void SectionStream::scanSource(uint8_t maxSections) constexpr size_t bufSize{512}; char buffer[bufSize]; - sections.reset(new Section[maxSections]{}); + sections = std::make_unique(maxSections); size_t offset{0}; while(sectionCount < maxSections && !stream->isFinished()) { diff --git a/Sming/Libraries/DiskStorage b/Sming/Libraries/DiskStorage index c467f3f0f9..3c649c03d4 160000 --- a/Sming/Libraries/DiskStorage +++ b/Sming/Libraries/DiskStorage @@ -1 +1 @@ -Subproject commit c467f3f0f9f504aecf134ec45780971da1595399 +Subproject commit 3c649c03d4a2e7b21c3648184ad8ebe9e0723c96 diff --git a/Sming/Libraries/HardwareSPI b/Sming/Libraries/HardwareSPI index 7f6516c7eb..5c8a52121a 160000 --- a/Sming/Libraries/HardwareSPI +++ b/Sming/Libraries/HardwareSPI @@ -1 +1 @@ -Subproject commit 7f6516c7eb24a3b3bf92e5f64c1722a268b0d553 +Subproject commit 5c8a52121aa788495c21172ceb724cfc1975411b diff --git a/Sming/Libraries/IOControl b/Sming/Libraries/IOControl index f860a2ae08..b84671a34d 160000 --- a/Sming/Libraries/IOControl +++ b/Sming/Libraries/IOControl @@ -1 +1 @@ -Subproject commit f860a2ae08d70819f4d4487f9ed559dc0164615c +Subproject commit b84671a34d452c49a0743b33070942c4bae3a474 diff --git a/Sming/Libraries/OtaNetwork/src/include/Ota/Network/HttpUpgrader.h b/Sming/Libraries/OtaNetwork/src/include/Ota/Network/HttpUpgrader.h index 4d1e8f3259..edf0d1502b 100644 --- a/Sming/Libraries/OtaNetwork/src/include/Ota/Network/HttpUpgrader.h +++ b/Sming/Libraries/OtaNetwork/src/include/Ota/Network/HttpUpgrader.h @@ -46,7 +46,7 @@ class HttpUpgrader : protected HttpClient ReadWriteStream* getStream() { if(!stream) { - stream.reset(new Ota::UpgradeOutputStream(partition)); + stream = std::make_unique(partition); } return stream.get(); } diff --git a/Sming/Libraries/Yeelight/YeelightBulb.cpp b/Sming/Libraries/Yeelight/YeelightBulb.cpp index b7d23e77a0..132ec579c7 100644 --- a/Sming/Libraries/Yeelight/YeelightBulb.cpp +++ b/Sming/Libraries/Yeelight/YeelightBulb.cpp @@ -34,7 +34,7 @@ bool YeelightBulb::connect() return true; } - connection.reset(new TcpClient(TcpClientDataDelegate(&YeelightBulb::onResponse, this))); + connection = std::make_unique(TcpClientDataDelegate(&YeelightBulb::onResponse, this)); connection->setTimeOut(USHRT_MAX); // Stay connected forever diff --git a/samples/Basic_Audio/app/application.cpp b/samples/Basic_Audio/app/application.cpp index d0a2f3e1be..30c549a672 100644 --- a/samples/Basic_Audio/app/application.cpp +++ b/samples/Basic_Audio/app/application.cpp @@ -57,7 +57,7 @@ static struct { Serial << _F("Generating sine wave table @ ") << frequency << _F(" Hz, ") << sampleCount << _F(" samples") << endl; - samples.reset(new uint16_t[sampleCount]); + samples = std::make_unique(sampleCount); if(!samples) { debug_e("Memory allocation failed"); return false; diff --git a/samples/Basic_Ota/app/application.cpp b/samples/Basic_Ota/app/application.cpp index 2663252230..f9aa5f7153 100644 --- a/samples/Basic_Ota/app/application.cpp +++ b/samples/Basic_Ota/app/application.cpp @@ -49,7 +49,7 @@ void doUpgrade() Serial.println(F("Updating...")); // need a clean object, otherwise if run before and failed will not run again - otaUpdater.reset(new Ota::Network::HttpUpgrader); + otaUpdater = std::make_unique(); // select rom slot to flash auto part = ota.getNextBootPartition(); diff --git a/samples/Basic_Serial/include/SerialTransmitDemo.h b/samples/Basic_Serial/include/SerialTransmitDemo.h index 2e9d032cb7..0b0afa31c9 100644 --- a/samples/Basic_Serial/include/SerialTransmitDemo.h +++ b/samples/Basic_Serial/include/SerialTransmitDemo.h @@ -8,9 +8,8 @@ class SerialTransmitDemo { public: - SerialTransmitDemo(HardwareSerial& serial, IDataSourceStream* stream) : serial(serial) + SerialTransmitDemo(HardwareSerial& serial, IDataSourceStream* stream) : serial(serial), stream(stream) { - this->stream.reset(stream); serial.onTransmitComplete(TransmitCompleteDelegate(&SerialTransmitDemo::onTransmitComplete, this)); } From 53524905fb093fce046df5cf20e629443762954a Mon Sep 17 00:00:00 2001 From: Mike Date: Tue, 19 Mar 2024 14:01:26 +0000 Subject: [PATCH 028/128] Update USB library (#2737) This PR fixes USB support for ESP IDF 5.2. Note that host mode is still not yet supported for esp32sx. Improvements: - Update to tinyusb master - Move initialisation code into separate Arch source file, patch to support ESP32 IDF 5.2 (not yet in tinyusb upstream) - Add CI support (speeds up library testing) Fixes: - Lazy-initialise flush timer: Causes problems with static Device objects and esp32sx as timer's aren't yet initialised - Enable python UTF8 mode for usbconfig tool (Windows) - UsbSerial conditional on CDC - Don't implement descriptor callbacks unless building for device - Ensure HID device is ready when sendReport called: get div-by-zero exception on esp32s2 otherwise - Fix `Basic_IFS` sample with `ENABLE_USB_STORAGE`: only for rp2040, no host support yet for esp32sx Tested with rp2040, esp32s2. --- Sming/Libraries/USB | 2 +- samples/Basic_IFS/basic_ifs.usbcfg | 7 ++++++- samples/Basic_IFS/component.mk | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Sming/Libraries/USB b/Sming/Libraries/USB index 8df168d42a..2e3e524172 160000 --- a/Sming/Libraries/USB +++ b/Sming/Libraries/USB @@ -1 +1 @@ -Subproject commit 8df168d42ad4e595398541255e4a0b791f04fda9 +Subproject commit 2e3e524172505835bca270dc7f198034bab01a10 diff --git a/samples/Basic_IFS/basic_ifs.usbcfg b/samples/Basic_IFS/basic_ifs.usbcfg index 60dfe5cc7e..bb6d4a335e 100644 --- a/samples/Basic_IFS/basic_ifs.usbcfg +++ b/samples/Basic_IFS/basic_ifs.usbcfg @@ -1,5 +1,10 @@ { "host": { - "msc": 1 + "hub": { + "port-count": 4 + }, + "msc": { + "maxlun": 4 + } } } \ No newline at end of file diff --git a/samples/Basic_IFS/component.mk b/samples/Basic_IFS/component.mk index 33da075365..f14a80567c 100644 --- a/samples/Basic_IFS/component.mk +++ b/samples/Basic_IFS/component.mk @@ -24,7 +24,7 @@ endif CONFIG_VARS += ENABLE_USB_STORAGE ifeq ($(ENABLE_USB_STORAGE),1) COMPONENT_CXXFLAGS += -DENABLE_USB_STORAGE -COMPONENT_DEPENDS += USB +COMPONENT_DEPENDS += USB FatIFS USB_CONFIG := basic_ifs.usbcfg endif From a7d3862fa59694bb8e04c02c686809429ded7eb6 Mon Sep 17 00:00:00 2001 From: Mike Date: Thu, 21 Mar 2024 12:57:34 +0000 Subject: [PATCH 029/128] Fix crashing rboot (#2738) Restore missing IRAM_ATTR declaration Co-authored-by: mikee47 --- Sming/Components/rboot/rboot | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sming/Components/rboot/rboot b/Sming/Components/rboot/rboot index b397c5b0c7..2b0fb65c7f 160000 --- a/Sming/Components/rboot/rboot +++ b/Sming/Components/rboot/rboot @@ -1 +1 @@ -Subproject commit b397c5b0c75f911b2fe1ebdde20d1567ab42f756 +Subproject commit 2b0fb65c7f2ce430865496c22e6a2801174811ba From ffe12d656dd11867fec7146b2204e3373ff3f8f5 Mon Sep 17 00:00:00 2001 From: Mike Date: Thu, 21 Mar 2024 15:50:54 +0000 Subject: [PATCH 030/128] Move CommandProcessing into Libraries (#2740) Since the `CommandProcessing` library is entirely optional and not a core dependency for Sming, move it into `Libraries`. Also fix a few naming inconsistencies, comments and remove outdated documentation. --- .../CommandProcessing/README.rst | 0 .../CommandProcessing/component.mk | 0 .../CommandProcessing/samples/.cs | 0 .../samples/Arducam/Makefile | 0 .../samples/Arducam/README.rst | 0 .../samples/Arducam/app/ArduCamCommand.cpp | 0 .../samples/Arducam/app/application.cpp | 0 .../samples/Arducam/component.mk | 0 .../samples/Arducam/include/ArduCamCommand.h | 0 .../samples/Arducam/web/build/Sming.png | Bin .../web/build/grids-responsive-min.css | 0 .../samples/Arducam/web/build/index.html | 0 .../samples/Arducam/web/build/layout.css | 0 .../samples/Arducam/web/build/minified.js | 0 .../samples/Arducam/web/build/pure-min.css | 0 .../samples/CommandLine/Makefile | 0 .../samples/CommandLine/README.rst | 0 .../samples/CommandLine/app/application.cpp | 0 .../samples/CommandLine/component.mk | 0 .../samples/CommandLine/files/index.html | 0 .../CommandLine/web/build/bootstrap.css.gz | Bin .../samples/CommandLine/web/build/index.html | 0 .../CommandLine/web/build/jquery.js.gz | Bin .../samples/CommandLine/web/dev/bootstrap.css | 0 .../samples/CommandLine/web/dev/index.html | 0 .../samples/CommandLine/web/dev/jquery.js | 0 .../samples/TelnetServer/Makefile | 0 .../samples/TelnetServer/README.rst | 0 .../samples/TelnetServer/app/application.cpp | 0 .../samples/TelnetServer/component.mk | 0 .../CommandProcessing/src/.cs | 0 .../src/CommandProcessing/Command.h | 0 .../src/CommandProcessing/Handler.cpp | 20 ++-- .../src/CommandProcessing/Handler.h | 14 +-- .../src/CommandProcessing/Utils.cpp | 0 .../src/CommandProcessing/Utils.h | 0 docs/source/information/command-handler.rst | 90 ------------------ docs/source/information/index.rst | 1 - 38 files changed, 17 insertions(+), 108 deletions(-) rename Sming/{Components => Libraries}/CommandProcessing/README.rst (100%) rename Sming/{Components => Libraries}/CommandProcessing/component.mk (100%) rename Sming/{Components => Libraries}/CommandProcessing/samples/.cs (100%) rename Sming/{Components => Libraries}/CommandProcessing/samples/Arducam/Makefile (100%) rename Sming/{Components => Libraries}/CommandProcessing/samples/Arducam/README.rst (100%) rename Sming/{Components => Libraries}/CommandProcessing/samples/Arducam/app/ArduCamCommand.cpp (100%) rename Sming/{Components => Libraries}/CommandProcessing/samples/Arducam/app/application.cpp (100%) rename Sming/{Components => Libraries}/CommandProcessing/samples/Arducam/component.mk (100%) rename Sming/{Components => Libraries}/CommandProcessing/samples/Arducam/include/ArduCamCommand.h (100%) rename Sming/{Components => Libraries}/CommandProcessing/samples/Arducam/web/build/Sming.png (100%) rename Sming/{Components => Libraries}/CommandProcessing/samples/Arducam/web/build/grids-responsive-min.css (100%) rename Sming/{Components => Libraries}/CommandProcessing/samples/Arducam/web/build/index.html (100%) rename Sming/{Components => Libraries}/CommandProcessing/samples/Arducam/web/build/layout.css (100%) rename Sming/{Components => Libraries}/CommandProcessing/samples/Arducam/web/build/minified.js (100%) rename Sming/{Components => Libraries}/CommandProcessing/samples/Arducam/web/build/pure-min.css (100%) rename Sming/{Components => Libraries}/CommandProcessing/samples/CommandLine/Makefile (100%) rename Sming/{Components => Libraries}/CommandProcessing/samples/CommandLine/README.rst (100%) rename Sming/{Components => Libraries}/CommandProcessing/samples/CommandLine/app/application.cpp (100%) rename Sming/{Components => Libraries}/CommandProcessing/samples/CommandLine/component.mk (100%) rename Sming/{Components => Libraries}/CommandProcessing/samples/CommandLine/files/index.html (100%) rename Sming/{Components => Libraries}/CommandProcessing/samples/CommandLine/web/build/bootstrap.css.gz (100%) rename Sming/{Components => Libraries}/CommandProcessing/samples/CommandLine/web/build/index.html (100%) rename Sming/{Components => Libraries}/CommandProcessing/samples/CommandLine/web/build/jquery.js.gz (100%) rename Sming/{Components => Libraries}/CommandProcessing/samples/CommandLine/web/dev/bootstrap.css (100%) rename Sming/{Components => Libraries}/CommandProcessing/samples/CommandLine/web/dev/index.html (100%) rename Sming/{Components => Libraries}/CommandProcessing/samples/CommandLine/web/dev/jquery.js (100%) rename Sming/{Components => Libraries}/CommandProcessing/samples/TelnetServer/Makefile (100%) rename Sming/{Components => Libraries}/CommandProcessing/samples/TelnetServer/README.rst (100%) rename Sming/{Components => Libraries}/CommandProcessing/samples/TelnetServer/app/application.cpp (100%) rename Sming/{Components => Libraries}/CommandProcessing/samples/TelnetServer/component.mk (100%) rename Sming/{Components => Libraries}/CommandProcessing/src/.cs (100%) rename Sming/{Components => Libraries}/CommandProcessing/src/CommandProcessing/Command.h (100%) rename Sming/{Components => Libraries}/CommandProcessing/src/CommandProcessing/Handler.cpp (90%) rename Sming/{Components => Libraries}/CommandProcessing/src/CommandProcessing/Handler.h (91%) rename Sming/{Components => Libraries}/CommandProcessing/src/CommandProcessing/Utils.cpp (100%) rename Sming/{Components => Libraries}/CommandProcessing/src/CommandProcessing/Utils.h (100%) delete mode 100644 docs/source/information/command-handler.rst diff --git a/Sming/Components/CommandProcessing/README.rst b/Sming/Libraries/CommandProcessing/README.rst similarity index 100% rename from Sming/Components/CommandProcessing/README.rst rename to Sming/Libraries/CommandProcessing/README.rst diff --git a/Sming/Components/CommandProcessing/component.mk b/Sming/Libraries/CommandProcessing/component.mk similarity index 100% rename from Sming/Components/CommandProcessing/component.mk rename to Sming/Libraries/CommandProcessing/component.mk diff --git a/Sming/Components/CommandProcessing/samples/.cs b/Sming/Libraries/CommandProcessing/samples/.cs similarity index 100% rename from Sming/Components/CommandProcessing/samples/.cs rename to Sming/Libraries/CommandProcessing/samples/.cs diff --git a/Sming/Components/CommandProcessing/samples/Arducam/Makefile b/Sming/Libraries/CommandProcessing/samples/Arducam/Makefile similarity index 100% rename from Sming/Components/CommandProcessing/samples/Arducam/Makefile rename to Sming/Libraries/CommandProcessing/samples/Arducam/Makefile diff --git a/Sming/Components/CommandProcessing/samples/Arducam/README.rst b/Sming/Libraries/CommandProcessing/samples/Arducam/README.rst similarity index 100% rename from Sming/Components/CommandProcessing/samples/Arducam/README.rst rename to Sming/Libraries/CommandProcessing/samples/Arducam/README.rst diff --git a/Sming/Components/CommandProcessing/samples/Arducam/app/ArduCamCommand.cpp b/Sming/Libraries/CommandProcessing/samples/Arducam/app/ArduCamCommand.cpp similarity index 100% rename from Sming/Components/CommandProcessing/samples/Arducam/app/ArduCamCommand.cpp rename to Sming/Libraries/CommandProcessing/samples/Arducam/app/ArduCamCommand.cpp diff --git a/Sming/Components/CommandProcessing/samples/Arducam/app/application.cpp b/Sming/Libraries/CommandProcessing/samples/Arducam/app/application.cpp similarity index 100% rename from Sming/Components/CommandProcessing/samples/Arducam/app/application.cpp rename to Sming/Libraries/CommandProcessing/samples/Arducam/app/application.cpp diff --git a/Sming/Components/CommandProcessing/samples/Arducam/component.mk b/Sming/Libraries/CommandProcessing/samples/Arducam/component.mk similarity index 100% rename from Sming/Components/CommandProcessing/samples/Arducam/component.mk rename to Sming/Libraries/CommandProcessing/samples/Arducam/component.mk diff --git a/Sming/Components/CommandProcessing/samples/Arducam/include/ArduCamCommand.h b/Sming/Libraries/CommandProcessing/samples/Arducam/include/ArduCamCommand.h similarity index 100% rename from Sming/Components/CommandProcessing/samples/Arducam/include/ArduCamCommand.h rename to Sming/Libraries/CommandProcessing/samples/Arducam/include/ArduCamCommand.h diff --git a/Sming/Components/CommandProcessing/samples/Arducam/web/build/Sming.png b/Sming/Libraries/CommandProcessing/samples/Arducam/web/build/Sming.png similarity index 100% rename from Sming/Components/CommandProcessing/samples/Arducam/web/build/Sming.png rename to Sming/Libraries/CommandProcessing/samples/Arducam/web/build/Sming.png diff --git a/Sming/Components/CommandProcessing/samples/Arducam/web/build/grids-responsive-min.css b/Sming/Libraries/CommandProcessing/samples/Arducam/web/build/grids-responsive-min.css similarity index 100% rename from Sming/Components/CommandProcessing/samples/Arducam/web/build/grids-responsive-min.css rename to Sming/Libraries/CommandProcessing/samples/Arducam/web/build/grids-responsive-min.css diff --git a/Sming/Components/CommandProcessing/samples/Arducam/web/build/index.html b/Sming/Libraries/CommandProcessing/samples/Arducam/web/build/index.html similarity index 100% rename from Sming/Components/CommandProcessing/samples/Arducam/web/build/index.html rename to Sming/Libraries/CommandProcessing/samples/Arducam/web/build/index.html diff --git a/Sming/Components/CommandProcessing/samples/Arducam/web/build/layout.css b/Sming/Libraries/CommandProcessing/samples/Arducam/web/build/layout.css similarity index 100% rename from Sming/Components/CommandProcessing/samples/Arducam/web/build/layout.css rename to Sming/Libraries/CommandProcessing/samples/Arducam/web/build/layout.css diff --git a/Sming/Components/CommandProcessing/samples/Arducam/web/build/minified.js b/Sming/Libraries/CommandProcessing/samples/Arducam/web/build/minified.js similarity index 100% rename from Sming/Components/CommandProcessing/samples/Arducam/web/build/minified.js rename to Sming/Libraries/CommandProcessing/samples/Arducam/web/build/minified.js diff --git a/Sming/Components/CommandProcessing/samples/Arducam/web/build/pure-min.css b/Sming/Libraries/CommandProcessing/samples/Arducam/web/build/pure-min.css similarity index 100% rename from Sming/Components/CommandProcessing/samples/Arducam/web/build/pure-min.css rename to Sming/Libraries/CommandProcessing/samples/Arducam/web/build/pure-min.css diff --git a/Sming/Components/CommandProcessing/samples/CommandLine/Makefile b/Sming/Libraries/CommandProcessing/samples/CommandLine/Makefile similarity index 100% rename from Sming/Components/CommandProcessing/samples/CommandLine/Makefile rename to Sming/Libraries/CommandProcessing/samples/CommandLine/Makefile diff --git a/Sming/Components/CommandProcessing/samples/CommandLine/README.rst b/Sming/Libraries/CommandProcessing/samples/CommandLine/README.rst similarity index 100% rename from Sming/Components/CommandProcessing/samples/CommandLine/README.rst rename to Sming/Libraries/CommandProcessing/samples/CommandLine/README.rst diff --git a/Sming/Components/CommandProcessing/samples/CommandLine/app/application.cpp b/Sming/Libraries/CommandProcessing/samples/CommandLine/app/application.cpp similarity index 100% rename from Sming/Components/CommandProcessing/samples/CommandLine/app/application.cpp rename to Sming/Libraries/CommandProcessing/samples/CommandLine/app/application.cpp diff --git a/Sming/Components/CommandProcessing/samples/CommandLine/component.mk b/Sming/Libraries/CommandProcessing/samples/CommandLine/component.mk similarity index 100% rename from Sming/Components/CommandProcessing/samples/CommandLine/component.mk rename to Sming/Libraries/CommandProcessing/samples/CommandLine/component.mk diff --git a/Sming/Components/CommandProcessing/samples/CommandLine/files/index.html b/Sming/Libraries/CommandProcessing/samples/CommandLine/files/index.html similarity index 100% rename from Sming/Components/CommandProcessing/samples/CommandLine/files/index.html rename to Sming/Libraries/CommandProcessing/samples/CommandLine/files/index.html diff --git a/Sming/Components/CommandProcessing/samples/CommandLine/web/build/bootstrap.css.gz b/Sming/Libraries/CommandProcessing/samples/CommandLine/web/build/bootstrap.css.gz similarity index 100% rename from Sming/Components/CommandProcessing/samples/CommandLine/web/build/bootstrap.css.gz rename to Sming/Libraries/CommandProcessing/samples/CommandLine/web/build/bootstrap.css.gz diff --git a/Sming/Components/CommandProcessing/samples/CommandLine/web/build/index.html b/Sming/Libraries/CommandProcessing/samples/CommandLine/web/build/index.html similarity index 100% rename from Sming/Components/CommandProcessing/samples/CommandLine/web/build/index.html rename to Sming/Libraries/CommandProcessing/samples/CommandLine/web/build/index.html diff --git a/Sming/Components/CommandProcessing/samples/CommandLine/web/build/jquery.js.gz b/Sming/Libraries/CommandProcessing/samples/CommandLine/web/build/jquery.js.gz similarity index 100% rename from Sming/Components/CommandProcessing/samples/CommandLine/web/build/jquery.js.gz rename to Sming/Libraries/CommandProcessing/samples/CommandLine/web/build/jquery.js.gz diff --git a/Sming/Components/CommandProcessing/samples/CommandLine/web/dev/bootstrap.css b/Sming/Libraries/CommandProcessing/samples/CommandLine/web/dev/bootstrap.css similarity index 100% rename from Sming/Components/CommandProcessing/samples/CommandLine/web/dev/bootstrap.css rename to Sming/Libraries/CommandProcessing/samples/CommandLine/web/dev/bootstrap.css diff --git a/Sming/Components/CommandProcessing/samples/CommandLine/web/dev/index.html b/Sming/Libraries/CommandProcessing/samples/CommandLine/web/dev/index.html similarity index 100% rename from Sming/Components/CommandProcessing/samples/CommandLine/web/dev/index.html rename to Sming/Libraries/CommandProcessing/samples/CommandLine/web/dev/index.html diff --git a/Sming/Components/CommandProcessing/samples/CommandLine/web/dev/jquery.js b/Sming/Libraries/CommandProcessing/samples/CommandLine/web/dev/jquery.js similarity index 100% rename from Sming/Components/CommandProcessing/samples/CommandLine/web/dev/jquery.js rename to Sming/Libraries/CommandProcessing/samples/CommandLine/web/dev/jquery.js diff --git a/Sming/Components/CommandProcessing/samples/TelnetServer/Makefile b/Sming/Libraries/CommandProcessing/samples/TelnetServer/Makefile similarity index 100% rename from Sming/Components/CommandProcessing/samples/TelnetServer/Makefile rename to Sming/Libraries/CommandProcessing/samples/TelnetServer/Makefile diff --git a/Sming/Components/CommandProcessing/samples/TelnetServer/README.rst b/Sming/Libraries/CommandProcessing/samples/TelnetServer/README.rst similarity index 100% rename from Sming/Components/CommandProcessing/samples/TelnetServer/README.rst rename to Sming/Libraries/CommandProcessing/samples/TelnetServer/README.rst diff --git a/Sming/Components/CommandProcessing/samples/TelnetServer/app/application.cpp b/Sming/Libraries/CommandProcessing/samples/TelnetServer/app/application.cpp similarity index 100% rename from Sming/Components/CommandProcessing/samples/TelnetServer/app/application.cpp rename to Sming/Libraries/CommandProcessing/samples/TelnetServer/app/application.cpp diff --git a/Sming/Components/CommandProcessing/samples/TelnetServer/component.mk b/Sming/Libraries/CommandProcessing/samples/TelnetServer/component.mk similarity index 100% rename from Sming/Components/CommandProcessing/samples/TelnetServer/component.mk rename to Sming/Libraries/CommandProcessing/samples/TelnetServer/component.mk diff --git a/Sming/Components/CommandProcessing/src/.cs b/Sming/Libraries/CommandProcessing/src/.cs similarity index 100% rename from Sming/Components/CommandProcessing/src/.cs rename to Sming/Libraries/CommandProcessing/src/.cs diff --git a/Sming/Components/CommandProcessing/src/CommandProcessing/Command.h b/Sming/Libraries/CommandProcessing/src/CommandProcessing/Command.h similarity index 100% rename from Sming/Components/CommandProcessing/src/CommandProcessing/Command.h rename to Sming/Libraries/CommandProcessing/src/CommandProcessing/Command.h diff --git a/Sming/Components/CommandProcessing/src/CommandProcessing/Handler.cpp b/Sming/Libraries/CommandProcessing/src/CommandProcessing/Handler.cpp similarity index 90% rename from Sming/Components/CommandProcessing/src/CommandProcessing/Handler.cpp rename to Sming/Libraries/CommandProcessing/src/CommandProcessing/Handler.cpp index 540d7b3d49..03bec19a4e 100644 --- a/Sming/Components/CommandProcessing/src/CommandProcessing/Handler.cpp +++ b/Sming/Libraries/CommandProcessing/src/CommandProcessing/Handler.cpp @@ -96,11 +96,11 @@ void Handler::processCommandLine(const String& cmdString) void Handler::registerSystemCommands() { String system = F("system"); - registerCommand({F("status"), F("Displays System Information"), system, {&Handler::procesStatusCommand, this}}); - registerCommand({F("echo"), F("Displays command entered"), system, {&Handler::procesEchoCommand, this}}); - registerCommand({F("help"), F("Displays all available commands"), system, {&Handler::procesHelpCommand, this}}); - registerCommand({F("debugon"), F("Set Serial debug on"), system, {&Handler::procesDebugOnCommand, this}}); - registerCommand({F("debugoff"), F("Set Serial debug off"), system, {&Handler::procesDebugOffCommand, this}}); + registerCommand({F("status"), F("Displays System Information"), system, {&Handler::processStatusCommand, this}}); + registerCommand({F("echo"), F("Displays command entered"), system, {&Handler::processEchoCommand, this}}); + registerCommand({F("help"), F("Displays all available commands"), system, {&Handler::processHelpCommand, this}}); + registerCommand({F("debugon"), F("Set Serial debug on"), system, {&Handler::processDebugOnCommand, this}}); + registerCommand({F("debugoff"), F("Set Serial debug off"), system, {&Handler::processDebugOffCommand, this}}); registerCommand({F("command"), F("Use verbose/silent/prompt as command options"), system, @@ -143,7 +143,7 @@ bool Handler::unregisterCommand(Command reqDelegate) } } -void Handler::procesHelpCommand(String commandLine, ReadWriteStream& outputStream) +void Handler::processHelpCommand(String commandLine, ReadWriteStream& outputStream) { debug_d("HelpCommand entered"); outputStream.println(_F("Commands available are :")); @@ -152,7 +152,7 @@ void Handler::procesHelpCommand(String commandLine, ReadWriteStream& outputStrea } } -void Handler::procesStatusCommand(String commandLine, ReadWriteStream& outputStream) +void Handler::processStatusCommand(String commandLine, ReadWriteStream& outputStream) { debug_d("StatusCommand entered"); outputStream << _F("Sming Framework Version : " SMING_VERSION) << endl; @@ -161,19 +161,19 @@ void Handler::procesStatusCommand(String commandLine, ReadWriteStream& outputStr outputStream << _F("System Start Reason : ") << system_get_rst_info()->reason << endl; } -void Handler::procesEchoCommand(String commandLine, ReadWriteStream& outputStream) +void Handler::processEchoCommand(String commandLine, ReadWriteStream& outputStream) { debug_d("HelpCommand entered"); outputStream << _F("You entered : '") << commandLine << '\'' << endl; } -void Handler::procesDebugOnCommand(String commandLine, ReadWriteStream& outputStream) +void Handler::processDebugOnCommand(String commandLine, ReadWriteStream& outputStream) { // Serial.systemDebugOutput(true); // outputStream.println(_F("Debug set to : On")); } -void Handler::procesDebugOffCommand(String commandLine, ReadWriteStream& outputStream) +void Handler::processDebugOffCommand(String commandLine, ReadWriteStream& outputStream) { // Serial.systemDebugOutput(false); // outputStream.println(_F("Debug set to : Off")); diff --git a/Sming/Components/CommandProcessing/src/CommandProcessing/Handler.h b/Sming/Libraries/CommandProcessing/src/CommandProcessing/Handler.h similarity index 91% rename from Sming/Components/CommandProcessing/src/CommandProcessing/Handler.h rename to Sming/Libraries/CommandProcessing/src/CommandProcessing/Handler.h index 25a7f6569e..d75e2f6b55 100644 --- a/Sming/Components/CommandProcessing/src/CommandProcessing/Handler.h +++ b/Sming/Libraries/CommandProcessing/src/CommandProcessing/Handler.h @@ -58,7 +58,7 @@ class Handler */ void setOutputStream(ReadWriteStream* stream, bool owned = true) { - if(outputStream != nullptr && ownedStream) { + if(ownedStream) { delete outputStream; } @@ -125,7 +125,7 @@ class Handler /** @brief Get the command delegate for a command * @param commandString Command to query - * @retval CommandDelegate The command delegate matching the command + * @retval Command The command delegate matching the command */ Command getCommandDelegate(const String& commandString); @@ -217,11 +217,11 @@ class Handler bool ownedStream = true; LineBuffer commandBuf; - void procesHelpCommand(String commandLine, ReadWriteStream& outputStream); - void procesStatusCommand(String commandLine, ReadWriteStream& outputStream); - void procesEchoCommand(String commandLine, ReadWriteStream& outputStream); - void procesDebugOnCommand(String commandLine, ReadWriteStream& outputStream); - void procesDebugOffCommand(String commandLine, ReadWriteStream& outputStream); + void processHelpCommand(String commandLine, ReadWriteStream& outputStream); + void processStatusCommand(String commandLine, ReadWriteStream& outputStream); + void processEchoCommand(String commandLine, ReadWriteStream& outputStream); + void processDebugOnCommand(String commandLine, ReadWriteStream& outputStream); + void processDebugOffCommand(String commandLine, ReadWriteStream& outputStream); void processCommandOptions(String commandLine, ReadWriteStream& outputStream); void processCommandLine(const String& cmdString); diff --git a/Sming/Components/CommandProcessing/src/CommandProcessing/Utils.cpp b/Sming/Libraries/CommandProcessing/src/CommandProcessing/Utils.cpp similarity index 100% rename from Sming/Components/CommandProcessing/src/CommandProcessing/Utils.cpp rename to Sming/Libraries/CommandProcessing/src/CommandProcessing/Utils.cpp diff --git a/Sming/Components/CommandProcessing/src/CommandProcessing/Utils.h b/Sming/Libraries/CommandProcessing/src/CommandProcessing/Utils.h similarity index 100% rename from Sming/Components/CommandProcessing/src/CommandProcessing/Utils.h rename to Sming/Libraries/CommandProcessing/src/CommandProcessing/Utils.h diff --git a/docs/source/information/command-handler.rst b/docs/source/information/command-handler.rst deleted file mode 100644 index 134e491e89..0000000000 --- a/docs/source/information/command-handler.rst +++ /dev/null @@ -1,90 +0,0 @@ -*************** -Command Handler -*************** - -Summary -======= - -The Sming ``CommandHandler`` implementation adds the possibility to -handle system and application commands over a variety of input media. - -The CLI provides a framework in which basic system commands can be -extended and application specific commands created. - -Currently implemented are: - Serial - Telnet Server - Websocket Server - -Implementation -============== - -The implementation makes use of a global object ``CommandHandler``, -which at the start is just a skeleton without any command active. - -System commands can be activated using: - -``Serial.commandProcessing(true)`` - -``telnetServer.enableCommand(true)`` - -``websockerServer.commandProcessing(true, "command")`` - -The additional parameter for websocket is used to allow multiple -connections on one websocket server instance, both with or without -command processing. - -When the websocket open request has query parameter ``"command=true"`` -command processing is enabled. - -Usage -===== - -The usage of ``CommandProcessor`` is the same for all implementations, -except for specific requirements. ``CommandProcesor`` options -implemented are: - Verbose -> sets display of welcome message - Prompt --> set prompt string - EOL -> set end of line character - welcome -message -> set the welcome message - -System Commands -=============== - -The base ``CommandHandler`` has the following sysytem commands -implemented: - ``help``, Displays all available commands - ``status``, -Displays System Information - ``echo``, Displays command entered - -``debugon``, Set Serial debug on - ``debugoff``, Set Serial debug off - -``command``, Usage verbose/silent/prompt for command options - -Application options -=================== - -Applications (and Sming Core functionality) can add commands to the -``CommandProcesor`` using a construct like - -.. code-block:: c++ - - registerCommand(CommandDelegate( - "status", - "Displays System Information", - "system", - commandFunctionDelegate(&CommandHandler::procesStatusCommand, this) - )); - -The added command will then be available over every opened command -channel. - -Example usage -============= - -The capabilities of ``CommandHandler`` are shown in the example -``CommandProcessing_Debug``. - -The example will create an application which shows ``CommandProcessing`` -for Telnet, Websocket and Serial. - -- to test Telnet, open telnet client and connect on port 23 to the ESP - ip -- to test the Websocket, open a web browser with the ESP -- to test Serial use your “Serial program” and make sure “local echo” - is activated - -For all implementations type “help” to show the available Commands - -The possibilities of extending commands with the application are shown -in: - the class ``ExampleCommand`` - the functions -``startExampleApplicationCommand()`` and -``processApplicationCommands()`` diff --git a/docs/source/information/index.rst b/docs/source/information/index.rst index 0b360e0604..1ac38ab5ba 100644 --- a/docs/source/information/index.rst +++ b/docs/source/information/index.rst @@ -15,5 +15,4 @@ Information tasks debugging rboot-ota - command-handler tips-n-tricks From fdbc1f05c4c3c785c62280140add6a4ae1faf00e Mon Sep 17 00:00:00 2001 From: Mike Date: Fri, 22 Mar 2024 07:48:48 +0000 Subject: [PATCH 031/128] Provide consistent serial console interface for samples (#2739) This PR provides a consistent mechanism for a basic serial console interface using the `LineBuffer` class. **Refactor LineBuffer** Implement LineBufferBase so we take non-trivial methods out of header. **Add `bool`, `String` operators, `printTo` method&** Simplify usage **Add `process`, `processKey` methods and update samples** Use LineBuffer for LiveDebug, Basic_Ota and Basic_FlashIP samples. These new methods are used to simplify application code. Note that the `CommandProcessing::Handler` class hasn't been updated here, mainly because it requires customisation of EOL handling (setCommandEOL, getCommandEOL). Presumably this is because of the variation in EOL sequences (CR, LF, CRLF or LFCR combinations). The LineBuffer `processKey` method doesn't care, it handles all of these. --- Sming/Core/Data/Buffer/LineBuffer.cpp | 107 +++++++++++ Sming/Core/Data/Buffer/LineBuffer.h | 104 +++++++---- .../src/CommandProcessing/Handler.cpp | 3 +- Sming/Libraries/FlashIP | 2 +- samples/Basic_Ota/app/application.cpp | 170 +++++++++++------- samples/LiveDebug/app/application.cpp | 44 ++--- 6 files changed, 299 insertions(+), 131 deletions(-) create mode 100644 Sming/Core/Data/Buffer/LineBuffer.cpp diff --git a/Sming/Core/Data/Buffer/LineBuffer.cpp b/Sming/Core/Data/Buffer/LineBuffer.cpp new file mode 100644 index 0000000000..2f4d14ab75 --- /dev/null +++ b/Sming/Core/Data/Buffer/LineBuffer.cpp @@ -0,0 +1,107 @@ +/**** + * Sming Framework Project - Open Source framework for high efficiency native ESP8266 development. + * Created 2015 by Skurydin Alexey + * http://github.com/SmingHub/Sming + * All files of the Sming Core are provided under the LGPL v3 license. + * + * LineBuffer.h - support for buffering/editing a line of text + * + * author mikee47 Feb 2019 + * + ****/ + +#include "LineBuffer.h" + +LineBufferBase::Action LineBufferBase::process(Stream& input, ReadWriteStream& output) +{ + int c; + while((c = input.read()) >= 0) { + auto action = processKey(c, &output); + if(action == Action::clear || action == Action::submit) { + return action; + } + } + return Action::none; +} + +LineBufferBase::Action LineBufferBase::processKey(char key, ReadWriteStream* output) +{ + auto prevKey = previousKey; + previousKey = key; + + switch(key) { + case '\x1b': // ESC -> delete current commandLine + clear(); + if(output) { + output->println(); + } + return Action::clear; + + case '\b': // delete (backspace) + case '\x7f': // xterm ctrl-? + if(!backspace()) { + return Action::none; + } + if(output) { + output->print("\b \b"); + } + return Action::backspace; + + case '\r': + case '\n': + // For "\r\n" or "\n\r" sequence ignore second key + if(prevKey != key && (prevKey == '\r' || prevKey == '\n')) { + previousKey = '\0'; + return Action::none; + } + if(output) { + output->println(); + } + return Action::submit; + + default: + if(!addChar(key)) { + return Action::none; + } + if(output) { + output->print(key); + } + return Action::echo; + } +} + +char LineBufferBase::addChar(char c) +{ + if(c == '\n' || c == '\r') { + return '\n'; + } + + if(c >= 0x20 && c < 0x7f && length < (size - 1)) { + buffer[length++] = c; + buffer[length] = '\0'; + return c; + } + + return '\0'; +} + +bool LineBufferBase::backspace() +{ + if(length == 0) { + return false; + } + --length; + buffer[length] = '\0'; + return true; +} + +bool LineBufferBase::startsWith(const char* text) const +{ + auto len = strlen(text); + return memcmp(buffer, text, len) == 0; +} + +bool LineBufferBase::contains(const char* text) const +{ + return strstr(buffer, text) != nullptr; +} diff --git a/Sming/Core/Data/Buffer/LineBuffer.h b/Sming/Core/Data/Buffer/LineBuffer.h index e23186b483..0295edc41f 100644 --- a/Sming/Core/Data/Buffer/LineBuffer.h +++ b/Sming/Core/Data/Buffer/LineBuffer.h @@ -14,14 +14,47 @@ #include #include +#include +#include /** * @brief Class to enable buffering of a single line of text, with simple editing * @note We define this as a template class for simplicity, no need for separate buffer memory management */ -template class LineBuffer +class LineBufferBase { public: + LineBufferBase(char* buffer, uint16_t size) : buffer(buffer), size(size) + { + } + + /** + * @brief Returned from `processKey` method directing caller + */ + enum class Action { + none, ///< Do nothing, ignore the key + clear, ///< Line is cleared: typically perform a carriage return + echo, ///< Key should be echoed + backspace, ///< Perform backspace edit, e.g. output "\b \b" + submit, ///< User hit return, process line and clear it + }; + + /** + * @brief Process all available data from `input` + * @param input Source of keystrokes + * @param output The output stream (e.g. Serial) for echoing + * @retval Action: none, clear or submit + */ + Action process(Stream& input, ReadWriteStream& output); + + /** + * @brief Process a keypress in a consistent manner for console editing + * @param key The keypress value + * @param output The output stream (e.g. Serial) for echoing, if required + * @retval Action + */ + Action processKey(char key, ReadWriteStream* output = nullptr); + /** * @brief Add a character to the buffer * @retval char Character added to buffer, '\0' if ignored, '\n' if line is complete @@ -36,6 +69,20 @@ template class LineBuffer length = 0; } + explicit operator bool() const + { + return length != 0; + } + + /** + * @brief Copy buffer contents into a String + * @retval String + */ + explicit operator String() const + { + return length ? String(buffer, length) : nullptr; + } + /** * @brief Get the text, nul-terminated */ @@ -72,44 +119,29 @@ template class LineBuffer */ bool backspace(); -private: - char buffer[BUFSIZE] = {'\0'}; ///< The text buffer - uint16_t length = 0; ///< Number of characters stored -}; - -template char LineBuffer::addChar(char c) -{ - if(c == '\n' || c == '\r') { - return '\n'; - } - - if(c >= 0x20 && c < 0x7f && length < (BUFSIZE - 1)) { - buffer[length++] = c; - buffer[length] = '\0'; - return c; + size_t printTo(Print& p) const + { + return p.write(buffer, length); } - return '\0'; -} +private: + char* buffer; + uint16_t size; + uint16_t length{0}; ///< Number of characters stored + char previousKey{'\0'}; ///< For processing CR/LF +}; -template bool LineBuffer::backspace() +/** + * @brief Class to enable buffering of a single line of text, with simple editing + * @note We define this as a template class for simplicity, no need for separate buffer memory management + */ +template class LineBuffer : public LineBufferBase { - if(length == 0) { - return false; - } else { - --length; - buffer[length] = '\0'; - return true; +public: + LineBuffer() : LineBufferBase(buffer, BUFSIZE) + { } -} -template bool LineBuffer::startsWith(const char* text) const -{ - auto len = strlen(text); - return memcmp(buffer, text, len) == 0; -} - -template bool LineBuffer::contains(const char* text) const -{ - return strstr(buffer, text) != nullptr; -} +private: + char buffer[BUFSIZE]{}; +}; diff --git a/Sming/Libraries/CommandProcessing/src/CommandProcessing/Handler.cpp b/Sming/Libraries/CommandProcessing/src/CommandProcessing/Handler.cpp index 03bec19a4e..60ec754bfd 100644 --- a/Sming/Libraries/CommandProcessing/src/CommandProcessing/Handler.cpp +++ b/Sming/Libraries/CommandProcessing/src/CommandProcessing/Handler.cpp @@ -29,9 +29,8 @@ size_t Handler::process(char recvChar) output.print(getCommandPrompt()); } } else if(recvChar == getCommandEOL()) { - String command(commandBuf.getBuffer(), commandBuf.getLength()); + processCommandLine(String(commandBuf)); commandBuf.clear(); - processCommandLine(command); } else if(recvChar == '\b' || recvChar == 0x7f) { if(commandBuf.backspace()) { output.print(_F("\b \b")); diff --git a/Sming/Libraries/FlashIP b/Sming/Libraries/FlashIP index 5f6d552d7d..820d08aab8 160000 --- a/Sming/Libraries/FlashIP +++ b/Sming/Libraries/FlashIP @@ -1 +1 @@ -Subproject commit 5f6d552d7d18f80bbbce7cd891fdce9026802953 +Subproject commit 820d08aab817dde262d5d5f9fb453dca88b374e9 diff --git a/samples/Basic_Ota/app/application.cpp b/samples/Basic_Ota/app/application.cpp index f9aa5f7153..51770d421c 100644 --- a/samples/Basic_Ota/app/application.cpp +++ b/samples/Basic_Ota/app/application.cpp @@ -3,6 +3,7 @@ #include #include #include +#include // If you want, you can define WiFi settings globally in Eclipse Environment Variables #ifndef WIFI_SSID @@ -10,9 +11,12 @@ #define WIFI_PWD "PleaseEnterPass" #endif +namespace +{ std::unique_ptr otaUpdater; Storage::Partition spiffsPartition; OtaUpgrader ota; +LineBuffer<16> commandBuffer; Storage::Partition findSpiffsPartition(Storage::Partition appPart) { @@ -122,76 +126,114 @@ void showInfo() << " @ 0x" << String(after.address(), HEX) << endl; } -void serialCallBack(Stream& stream, char arrivedChar, unsigned short availableCharsCount) +void showPrompt() +{ + Serial << _F("OTA> ") << commandBuffer; +} + +void handleCommand(const String& str) { - int pos = stream.indexOf('\n'); - if(pos > -1) { - char str[pos + 1]; - for(int i = 0; i < pos + 1; i++) { - str[i] = stream.read(); - if(str[i] == '\r' || str[i] == '\n') { - str[i] = '\0'; + if(F("connect") == str) { + Serial << _F("Connecting to '") << WIFI_SSID << "'..." << endl; + WifiStation.config(WIFI_SSID, WIFI_PWD); + WifiStation.enable(true); + WifiStation.connect(); + return; + } + + if(F("ip") == str) { + Serial << "ip: " << WifiStation.getIP() << ", mac: " << WifiStation.getMacAddress() << endl; + return; + } + + if(F("ota") == str) { + doUpgrade(); + return; + } + + if(F("switch") == str) { + doSwitch(); + return; + } + + if(F("restart") == str) { + System.restart(); + return; + } + + if(F("ls") == str) { + Directory dir; + if(dir.open()) { + while(dir.next()) { + Serial << " " << dir.stat().name << endl; } } + Serial << _F("filecount ") << dir.count() << endl; + return; + } - if(F("connect") == str) { - // connect to wifi - WifiStation.config(WIFI_SSID, WIFI_PWD); - WifiStation.enable(true); - WifiStation.connect(); - } else if(F("ip") == str) { - Serial << "ip: " << WifiStation.getIP() << ", mac: " << WifiStation.getMacAddress() << endl; - } else if(F("ota") == str) { - doUpgrade(); - } else if(F("switch") == str) { - doSwitch(); - } else if(F("restart") == str) { - System.restart(); - } else if(F("ls") == str) { - Directory dir; - if(dir.open()) { - while(dir.next()) { - Serial << " " << dir.stat().name << endl; - } - } - Serial << _F("filecount ") << dir.count() << endl; - } else if(F("cat") == str) { - Directory dir; - if(dir.open() && dir.next()) { - auto filename = dir.stat().name.c_str(); - Serial << "dumping file " << filename << ": " << endl; - // We don't know how big the is, so streaming it is safest - FileStream fs; - fs.open(filename); - Serial.copyFrom(&fs); - Serial.println(); - } else { - Serial.println(F("Empty spiffs!")); - } - } else if(F("info") == str) { - showInfo(); - } else if(F("help") == str) { - Serial.print(_F("\r\n" - "available commands:\r\n" - " help - display this message\r\n" - " ip - show current ip address\r\n" - " connect - connect to wifi\r\n" - " restart - restart the device\r\n" - " switch - switch to the other rom and reboot\r\n" - " ota - perform ota update, switch rom and reboot\r\n" - " info - show device info\r\n")); - - if(spiffsPartition) { - Serial.print(_F(" ls - list files in spiffs\r\n" - " cat - show first file in spiffs\r\n")); - } + if(F("cat") == str) { + Directory dir; + if(dir.open() && dir.next()) { + auto filename = dir.stat().name.c_str(); + Serial << "dumping file " << filename << ": " << endl; + // We don't know how big the is, so streaming it is safest + FileStream fs; + fs.open(filename); + Serial.copyFrom(&fs); Serial.println(); } else { - Serial.println("unknown command"); + Serial.println(F("Empty spiffs!")); + } + return; + } + + if(F("info") == str) { + showInfo(); + return; + } + + if(F("help") == str) { + Serial.print(_F("\r\n" + "available commands:\r\n" + " help - display this message\r\n" + " ip - show current ip address\r\n" + " connect - connect to wifi\r\n" + " restart - restart the device\r\n" + " switch - switch to the other rom and reboot\r\n" + " ota - perform ota update, switch rom and reboot\r\n" + " info - show device info\r\n")); + + if(spiffsPartition) { + Serial.print(_F(" ls - list files in spiffs\r\n" + " cat - show first file in spiffs\r\n")); } + Serial.println(); + return; } + + Serial << _F("unknown command: ") << str << endl; } +void serialCallBack(Stream& stream, char arrivedChar, unsigned short availableCharsCount) +{ + switch(commandBuffer.process(stream, Serial)) { + case commandBuffer.Action::submit: + if(commandBuffer) { + handleCommand(String(commandBuffer)); + commandBuffer.clear(); + } + showPrompt(); + break; + case commandBuffer.Action::clear: + showPrompt(); + break; + default:; + } +} + +} // namespace + void init() { Serial.begin(SERIAL_BAUD_RATE); // 115200 by default @@ -207,10 +249,14 @@ void init() } WifiAccessPoint.enable(false); + WifiEvents.onStationGotIP([](IpAddress ip, IpAddress netmask, IpAddress gateway) { showPrompt(); }); - Serial << _F("\r\nCurrently running ") << partition.name() << " @ 0x" << String(partition.address(), HEX) << '.' + Serial << endl + << _F("Currently running ") << partition.name() << " @ 0x" << String(partition.address(), HEX) << '.' << endl + << _F("Type 'help' and press enter for instructions.") << endl << endl; - Serial << _F("Type 'help' and press enter for instructions.") << endl << endl; + + showPrompt(); Serial.onDataReceived(serialCallBack); } diff --git a/samples/LiveDebug/app/application.cpp b/samples/LiveDebug/app/application.cpp index 6b7a5bd780..0adca0af28 100644 --- a/samples/LiveDebug/app/application.cpp +++ b/samples/LiveDebug/app/application.cpp @@ -96,9 +96,7 @@ void showPrompt() void onDataReceived(Stream& source, char arrivedChar, unsigned short availableCharsCount) { - static unsigned commandLength; - const unsigned MAX_COMMAND_LENGTH = 16; - static char commandBuffer[MAX_COMMAND_LENGTH + 1]; + static LineBuffer commandBuffer; // Error detection unsigned status = Serial.getStatus(); @@ -118,38 +116,24 @@ void onDataReceived(Stream& source, char arrivedChar, unsigned short availableCh } // Discard what is likely to be garbage Serial.clear(SERIAL_RX_ONLY); - commandLength = 0; + commandBuffer.clear(); showPrompt(); return; } - int c; - while((c = Serial.read()) >= 0) { - switch(c) { - case '\b': // delete (backspace) - case 0x7f: // xterm ctrl-? - if(commandLength > 0) { - --commandLength; - Serial.print(_F("\b \b")); - } - break; - case '\r': - case '\n': - if(commandLength > 0) { - Serial.println(); - String cmd(commandBuffer, commandLength); - commandLength = 0; - Serial.clear(SERIAL_RX_ONLY); - handleCommand(cmd); - } - showPrompt(); - break; - default: - if(c >= 0x20 && c <= 0x7f && commandLength < MAX_COMMAND_LENGTH) { - commandBuffer[commandLength++] = c; - Serial.print(char(c)); - } + switch(commandBuffer.process(source, Serial)) { + case commandBuffer.Action::clear: + showPrompt(); + break; + case commandBuffer.Action::submit: { + if(commandBuffer) { + handleCommand(String(commandBuffer)); + commandBuffer.clear(); } + showPrompt(); + } + default: + break; } } From dc7d3fa77a521040634d68969015bc5cb7ed557f Mon Sep 17 00:00:00 2001 From: Mike Date: Fri, 22 Mar 2024 10:22:04 +0000 Subject: [PATCH 032/128] Bugfix: stale HTTP connections may get missed in `cleanInactive` method (#2741) This PR fixes the `cleanInactive` method. By code inspection if a stale connection is deleted from the list then the loop index is increment, skipping over the next item. --- Sming/Components/Network/src/Network/HttpClient.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Sming/Components/Network/src/Network/HttpClient.cpp b/Sming/Components/Network/src/Network/HttpClient.cpp index 0d54aeff5f..7c97be2d6e 100644 --- a/Sming/Components/Network/src/Network/HttpClient.cpp +++ b/Sming/Components/Network/src/Network/HttpClient.cpp @@ -68,13 +68,16 @@ void HttpClient::cleanInactive() { debug_d("Total connections: %d", httpConnectionPool.count()); - for(size_t i = 0; i < httpConnectionPool.count(); i++) { + size_t i = 0; + while(i < httpConnectionPool.count()) { auto connection = httpConnectionPool.valueAt(i); if(connection->getConnectionState() > eTCS_Connecting && !connection->isActive()) { debug_d("Removing stale connection: State: %d, Active: %d, Finished: %d", connection->getConnectionState(), connection->isActive(), connection->isFinished()); httpConnectionPool.removeAt(i); + } else { + ++i; } } } From 629678adc69b1ae0e38c6a4c11f07ed2ef9d38a5 Mon Sep 17 00:00:00 2001 From: Mike Date: Mon, 25 Mar 2024 14:55:09 +0000 Subject: [PATCH 033/128] Address CI build warnings (#2742) This PR addresses warnings which have crept into the CI build logs. Worst case is probably esp32 with IDF 5.2 (GCC 13.2) producing 350 warnings, now 65. **General fixes** - Remove deprecated calls to command processing library from samples - Fix initialisation order and type names: still some instances of e.g. `uint8` which should be `uint8_t`. - Discovered component samples aren't built for Host in CI! Fixed this - note build time extended accordingly. - Simplify nested conditionals https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#f56-avoid-unnecessary-condition-nesting **Fix compiler warnings/errors** Replaced a few more instances of printf with streaming operators, simpler and type-safe. Fixed `Overloaded virtual` warnings. **Note**: See remaining warnings for NimBLE (submodule) which need attention. **Graphics library fixes** - Fix control rendering: Should use their own bounds for positioning, set button font - Fix const parameters - use unique_ptr for getGlyph calls - Use `std::make_unique` and assignment instead of reset() - Avoid printf - Demote `reinterpret_cast` to `static_cast` where appropriate - Apply coding style to samples - Fix compiler warnings **Disable esp32 warning about RWX segments** Check introduced with linker (LD, binutils 2.39) from IDF 5.2 onwards This specifically refers to IRAM, which we do actually need to be writeable and executable. (IDF link code disables this warning also.) --- Sming/Arch/Esp32/app.mk | 5 ++++ .../Components/esp8266/include/ets_sys.h | 4 +-- .../Components/esp8266/include/sdk/mem.h | 6 ++-- .../Esp8266/Components/spi_flash/flashmem.c | 4 +-- Sming/Arch/Esp8266/Core/Digital.cpp | 4 +-- Sming/Arch/Esp8266/Core/Interrupts.cpp | 6 ++-- Sming/Arch/Esp8266/Platform/RTC.cpp | 4 +-- Sming/Arch/Host/Components/esp_hal/clk.c | 4 +-- .../Host/Components/esp_hal/include/esp_clk.h | 2 +- .../Components/esp_hal/include/esp_sleep.h | 2 +- .../Components/esp_hal/include/esp_system.h | 2 +- Sming/Arch/Host/Components/esp_hal/sleep.c | 2 +- Sming/Arch/Host/Components/esp_hal/system.cpp | 2 +- .../Host/Components/spi_flash/flashmem.cpp | 2 +- Sming/Arch/Host/Core/Digital.cpp | 2 +- Sming/Arch/Host/Tools/ci/build.run.sh | 1 + .../Arch/Esp8266/Platform/AccessPointImpl.cpp | 2 +- .../Arch/Esp8266/Platform/StationImpl.cpp | 2 +- .../Arch/Host/Platform/StationImpl.cpp | 30 ++++++++++--------- .../src/Network/Http/HttpServerConnection.h | 7 ++++- .../samples/Arducam/app/ArduCamCommand.cpp | 4 +-- .../samples/Arducam/include/ArduCamCommand.h | 7 +++-- Sming/Libraries/Graphics | 2 +- Sming/Libraries/IOControl | 2 +- Sming/Libraries/LittleFS | 2 +- Sming/Libraries/MDNS | 2 +- Sming/Libraries/MFRC522/MFRC522.cpp | 3 +- Sming/Libraries/MFRC522/MFRC522.h | 2 +- Sming/Libraries/OneWire/OneWire.h | 2 +- .../OtaUpgrade/OtaUpgrade/BasicStream.cpp | 2 -- .../samples/Upgrade/app/application.cpp | 8 ++--- Sming/Libraries/Servo/Servo.h | 14 ++++----- Sming/Libraries/UPnP | 2 +- Sming/Libraries/UPnP-Schema | 2 +- Sming/System/include/gdb/gdb_hooks.h | 4 +++ Sming/Wiring/FakePgmSpace.h | 2 +- samples/Basic_ProgMem/app/TestProgmem.cpp | 10 +++++-- samples/Basic_Servo/app/application.cpp | 4 +-- samples/Basic_Storage/app/application.cpp | 18 ++++++----- .../Basic_WebSkeletonApp/app/application.cpp | 1 - .../app/application.cpp | 1 - samples/Network_Ping/app/application.cpp | 2 +- samples/SDCard/app/application.cpp | 14 ++++----- tests/HostTests/modules/String.cpp | 2 +- 44 files changed, 113 insertions(+), 92 deletions(-) diff --git a/Sming/Arch/Esp32/app.mk b/Sming/Arch/Esp32/app.mk index 32f75955d9..6cc8658584 100644 --- a/Sming/Arch/Esp32/app.mk +++ b/Sming/Arch/Esp32/app.mk @@ -9,6 +9,11 @@ LDFLAGS += \ -nostdlib \ -Wl,-static +ifeq ($(IDF_VERSION),v5.2) +LDFLAGS += \ + -Wl,--no-warn-rwx-segments +endif + ifdef IDF_TARGET_ARCH_RISCV LDFLAGS += \ -nostartfiles \ diff --git a/Sming/Arch/Esp8266/Components/esp8266/include/ets_sys.h b/Sming/Arch/Esp8266/Components/esp8266/include/ets_sys.h index 4d9eb1109c..c2673186f8 100644 --- a/Sming/Arch/Esp8266/Components/esp8266/include/ets_sys.h +++ b/Sming/Arch/Esp8266/Components/esp8266/include/ets_sys.h @@ -73,8 +73,8 @@ typedef void (*ets_isr_t)(void*); void ets_intr_lock(void); void ets_intr_unlock(void); void ets_isr_attach(int i, ets_isr_t func, void* arg); -void ets_isr_mask(uint32 mask); -void ets_isr_unmask(uint32 unmask); +void ets_isr_mask(uint32_t mask); +void ets_isr_unmask(uint32_t unmask); void NmiTimSetFunc(void (*func)(void)); diff --git a/Sming/Arch/Esp8266/Components/esp8266/include/sdk/mem.h b/Sming/Arch/Esp8266/Components/esp8266/include/sdk/mem.h index df1950df4a..6d3cb37d3d 100644 --- a/Sming/Arch/Esp8266/Components/esp8266/include/sdk/mem.h +++ b/Sming/Arch/Esp8266/Components/esp8266/include/sdk/mem.h @@ -6,10 +6,10 @@ extern "C" { #endif -void* pvPortMalloc(size_t xWantedSize, const char* file, uint32 line); +void* pvPortMalloc(size_t xWantedSize, const char* file, uint32_t line); void* pvPortCalloc(size_t count, size_t size, const char*, unsigned); -void* pvPortZalloc(size_t xWantedSize, const char* file, uint32 line); -void vPortFree(void* ptr, const char* file, uint32 line); +void* pvPortZalloc(size_t xWantedSize, const char* file, uint32_t line); +void vPortFree(void* ptr, const char* file, uint32_t line); void* vPortMalloc(size_t xWantedSize); void pvPortFree(void* ptr); diff --git a/Sming/Arch/Esp8266/Components/spi_flash/flashmem.c b/Sming/Arch/Esp8266/Components/spi_flash/flashmem.c index 75f33ec622..4c434270f6 100644 --- a/Sming/Arch/Esp8266/Components/spi_flash/flashmem.c +++ b/Sming/Arch/Esp8266/Components/spi_flash/flashmem.c @@ -267,7 +267,7 @@ uint32_t flashmem_write_internal( const void *from, uint32_t toaddr, uint32_t si { assert(IS_ALIGNED(from) && IS_ALIGNED(toaddr) && IS_ALIGNED(size)); - SpiFlashOpResult r = spi_flash_write(toaddr, (uint32*)from, size); + SpiFlashOpResult r = spi_flash_write(toaddr, (uint32_t*)from, size); if(SPI_FLASH_RESULT_OK == r) return size; else{ @@ -280,7 +280,7 @@ uint32_t flashmem_read_internal( void *to, uint32_t fromaddr, uint32_t size ) { assert(IS_ALIGNED(to) && IS_ALIGNED(fromaddr) && IS_ALIGNED(size)); - SpiFlashOpResult r = spi_flash_read(fromaddr, (uint32*)to, size); + SpiFlashOpResult r = spi_flash_read(fromaddr, (uint32_t*)to, size); if(SPI_FLASH_RESULT_OK == r) return size; else{ diff --git a/Sming/Arch/Esp8266/Core/Digital.cpp b/Sming/Arch/Esp8266/Core/Digital.cpp index 434b69589f..d8bb1e6021 100644 --- a/Sming/Arch/Esp8266/Core/Digital.cpp +++ b/Sming/Arch/Esp8266/Core/Digital.cpp @@ -104,7 +104,7 @@ void digitalWrite(uint16_t pin, uint8_t val) if(pin != 16) GPIO_REG_WRITE((((val != LOW) ? GPIO_OUT_W1TS_ADDRESS : GPIO_OUT_W1TC_ADDRESS)), (1 << pin)); else - WRITE_PERI_REG(RTC_GPIO_OUT, (READ_PERI_REG(RTC_GPIO_OUT) & (uint32)0xfffffffe) | (uint32)(val & 1)); + WRITE_PERI_REG(RTC_GPIO_OUT, (READ_PERI_REG(RTC_GPIO_OUT) & (uint32_t)0xfffffffe) | (uint32_t)(val & 1)); //GPIO_OUTPUT_SET(pin, (val ? 0xFF : 00)); } @@ -115,7 +115,7 @@ uint8_t digitalRead(uint16_t pin) if(pin != 16) return ((GPIO_REG_READ(GPIO_IN_ADDRESS) >> pin) & 1); else - return (uint8)(READ_PERI_REG(RTC_GPIO_IN_DATA) & 1); + return (uint8_t)(READ_PERI_REG(RTC_GPIO_IN_DATA) & 1); //return GPIO_INPUT_GET(pin); } diff --git a/Sming/Arch/Esp8266/Core/Interrupts.cpp b/Sming/Arch/Esp8266/Core/Interrupts.cpp index 7996b9e58f..daafce7582 100644 --- a/Sming/Arch/Esp8266/Core/Interrupts.cpp +++ b/Sming/Arch/Esp8266/Core/Interrupts.cpp @@ -31,14 +31,14 @@ static void interruptDelegateCallback(uint32_t interruptNumber) * @param intr_mask Interrupt mask * @param arg pointer to array of arguments */ -static void IRAM_ATTR interruptHandler(uint32 intr_mask, void* arg) +static void IRAM_ATTR interruptHandler(uint32_t intr_mask, void* arg) { bool processed; do { - uint32 gpioStatus = GPIO_REG_READ(GPIO_STATUS_ADDRESS); + uint32_t gpioStatus = GPIO_REG_READ(GPIO_STATUS_ADDRESS); processed = false; - for(uint8 i = 0; i < MAX_INTERRUPTS; i++) { + for(uint8_t i = 0; i < MAX_INTERRUPTS; i++) { if(!bitRead(gpioStatus, i)) { continue; } diff --git a/Sming/Arch/Esp8266/Platform/RTC.cpp b/Sming/Arch/Esp8266/Platform/RTC.cpp index b06db69d65..a90f06b9a2 100644 --- a/Sming/Arch/Esp8266/Platform/RTC.cpp +++ b/Sming/Arch/Esp8266/Platform/RTC.cpp @@ -67,8 +67,8 @@ bool RtcClass::setRtcSeconds(uint32_t seconds) void updateTime(RtcData& data) { - uint32 rtc_cycles; - uint32 cal, cal1, cal2; + uint32_t rtc_cycles; + uint32_t cal, cal1, cal2; cal1 = system_rtc_clock_cali_proc(); __asm__ __volatile__("memw" : : : "memory"); // Just for fun cal2 = system_rtc_clock_cali_proc(); diff --git a/Sming/Arch/Host/Components/esp_hal/clk.c b/Sming/Arch/Host/Components/esp_hal/clk.c index 8eedfb078f..6021a3e3b5 100644 --- a/Sming/Arch/Host/Components/esp_hal/clk.c +++ b/Sming/Arch/Host/Components/esp_hal/clk.c @@ -23,7 +23,7 @@ static uint32_t get_ccount(uint64_t nanos) return ccount; } -bool system_update_cpu_freq(uint8 freq) +bool system_update_cpu_freq(uint8_t freq) { if(freq == cpu_frequency) { return true; @@ -46,7 +46,7 @@ uint8_t ets_get_cpu_frequency(void) return cpu_frequency; } -uint8 system_get_cpu_freq(void) +uint8_t system_get_cpu_freq(void) { return ets_get_cpu_frequency(); } diff --git a/Sming/Arch/Host/Components/esp_hal/include/esp_clk.h b/Sming/Arch/Host/Components/esp_hal/include/esp_clk.h index da77d2650a..eb510b1dce 100644 --- a/Sming/Arch/Host/Components/esp_hal/include/esp_clk.h +++ b/Sming/Arch/Host/Components/esp_hal/include/esp_clk.h @@ -11,7 +11,7 @@ extern "C" { #define SYS_CPU_80MHZ 80 #define SYS_CPU_160MHZ 160 -bool system_update_cpu_freq(uint8 freq); +bool system_update_cpu_freq(uint8_t freq); uint8_t ets_get_cpu_frequency(void); uint8_t system_get_cpu_freq(void); diff --git a/Sming/Arch/Host/Components/esp_hal/include/esp_sleep.h b/Sming/Arch/Host/Components/esp_hal/include/esp_sleep.h index 82b4dfc028..9a6525d167 100644 --- a/Sming/Arch/Host/Components/esp_hal/include/esp_sleep.h +++ b/Sming/Arch/Host/Components/esp_hal/include/esp_sleep.h @@ -31,7 +31,7 @@ void wifi_fpm_auto_sleep_set_in_null_mode(uint8_t req); /* GPIO */ -void wifi_enable_gpio_wakeup(uint32 i, GPIO_INT_TYPE intr_status); +void wifi_enable_gpio_wakeup(uint32_t i, GPIO_INT_TYPE intr_status); void wifi_disable_gpio_wakeup(void); /* These aren't defined in the RTOS SDK */ diff --git a/Sming/Arch/Host/Components/esp_hal/include/esp_system.h b/Sming/Arch/Host/Components/esp_hal/include/esp_system.h index a436fad478..a1d4e0a42e 100644 --- a/Sming/Arch/Host/Components/esp_hal/include/esp_system.h +++ b/Sming/Arch/Host/Components/esp_hal/include/esp_system.h @@ -57,7 +57,7 @@ void os_delay_us(uint32_t us); const char* system_get_sdk_version(void); -uint32 system_get_chip_id(void); +uint32_t system_get_chip_id(void); bool system_rtc_mem_read(uint8_t src_addr, void* des_addr, uint16_t load_size); bool system_rtc_mem_write(uint8_t des_addr, const void* src_addr, uint16_t save_size); diff --git a/Sming/Arch/Host/Components/esp_hal/sleep.c b/Sming/Arch/Host/Components/esp_hal/sleep.c index d3aacca753..ae2d234973 100644 --- a/Sming/Arch/Host/Components/esp_hal/sleep.c +++ b/Sming/Arch/Host/Components/esp_hal/sleep.c @@ -46,7 +46,7 @@ void wifi_fpm_auto_sleep_set_in_null_mode(uint8_t req) /* GPIO */ -void wifi_enable_gpio_wakeup(uint32 i, GPIO_INT_TYPE intr_status) +void wifi_enable_gpio_wakeup(uint32_t i, GPIO_INT_TYPE intr_status) { } diff --git a/Sming/Arch/Host/Components/esp_hal/system.cpp b/Sming/Arch/Host/Components/esp_hal/system.cpp index 114cc751a0..02cec4e44a 100644 --- a/Sming/Arch/Host/Components/esp_hal/system.cpp +++ b/Sming/Arch/Host/Components/esp_hal/system.cpp @@ -96,7 +96,7 @@ const char* system_get_sdk_version(void) return version_string; } -uint32 system_get_chip_id(void) +uint32_t system_get_chip_id(void) { return 0xC001BEAF; } diff --git a/Sming/Arch/Host/Components/spi_flash/flashmem.cpp b/Sming/Arch/Host/Components/spi_flash/flashmem.cpp index 18a01c2b6e..9e65b39212 100644 --- a/Sming/Arch/Host/Components/spi_flash/flashmem.cpp +++ b/Sming/Arch/Host/Components/spi_flash/flashmem.cpp @@ -141,7 +141,7 @@ uint8_t flashmem_get_size_type() return flashmem_get_info().size; } -uint32 spi_flash_get_id(void) +uint32_t spi_flash_get_id(void) { return 0xFA1E0008; } diff --git a/Sming/Arch/Host/Core/Digital.cpp b/Sming/Arch/Host/Core/Digital.cpp index cb1f221e16..c266677a70 100644 --- a/Sming/Arch/Host/Core/Digital.cpp +++ b/Sming/Arch/Host/Core/Digital.cpp @@ -13,7 +13,7 @@ // Wemos D1 mini has pin 16 #define PIN_MAX 16 -static uint8 pinModes[PIN_MAX + 1]; +static uint8_t pinModes[PIN_MAX + 1]; DigitalHooks defaultHooks; static DigitalHooks* activeHooks = &defaultHooks; diff --git a/Sming/Arch/Host/Tools/ci/build.run.sh b/Sming/Arch/Host/Tools/ci/build.run.sh index 708f349153..d4aad47b37 100755 --- a/Sming/Arch/Host/Tools/ci/build.run.sh +++ b/Sming/Arch/Host/Tools/ci/build.run.sh @@ -14,6 +14,7 @@ if [[ $CHECK_SCA -eq 1 ]]; then "$DIR/coverity-scan.sh" else $MAKE_PARALLEL samples DEBUG_VERBOSE_LEVEL=3 + $MAKE_PARALLEL component-samples DEBUG_VERBOSE_LEVEL=3 fi # Build and run tests diff --git a/Sming/Components/Network/Arch/Esp8266/Platform/AccessPointImpl.cpp b/Sming/Components/Network/Arch/Esp8266/Platform/AccessPointImpl.cpp index d83ef86dd3..19037ecd1a 100644 --- a/Sming/Components/Network/Arch/Esp8266/Platform/AccessPointImpl.cpp +++ b/Sming/Components/Network/Arch/Esp8266/Platform/AccessPointImpl.cpp @@ -18,7 +18,7 @@ AccessPointClass& WifiAccessPoint = accessPoint; void AccessPointImpl::enable(bool enabled, bool save) { - uint8 mode; + uint8_t mode; if(save) { mode = wifi_get_opmode_default() & ~SOFTAP_MODE; } else { diff --git a/Sming/Components/Network/Arch/Esp8266/Platform/StationImpl.cpp b/Sming/Components/Network/Arch/Esp8266/Platform/StationImpl.cpp index 72fab1319f..63930c51ac 100644 --- a/Sming/Components/Network/Arch/Esp8266/Platform/StationImpl.cpp +++ b/Sming/Components/Network/Arch/Esp8266/Platform/StationImpl.cpp @@ -30,7 +30,7 @@ class BssInfoImpl : public BssInfo void StationImpl::enable(bool enabled, bool save) { - uint8 mode; + uint8_t mode; if(save) { mode = wifi_get_opmode_default() & ~STATION_MODE; } else { diff --git a/Sming/Components/Network/Arch/Host/Platform/StationImpl.cpp b/Sming/Components/Network/Arch/Host/Platform/StationImpl.cpp index a981dc7ae6..25a71a9021 100644 --- a/Sming/Components/Network/Arch/Host/Platform/StationImpl.cpp +++ b/Sming/Components/Network/Arch/Host/Platform/StationImpl.cpp @@ -161,24 +161,26 @@ bool StationImpl::isEnabled() const bool StationImpl::config(const Config& cfg) { for(auto& ap : apInfoList) { - if(cfg.ssid == ap.ssid) { - if(ap.authMode != AUTH_OPEN) { - if(cfg.password != ap.pwd) { - debug_w("Bad password for '%s'", cfg.ssid.c_str()); - return false; - } - } + if(cfg.ssid != ap.ssid) { + continue; + } - currentAp = ≈ - if(cfg.save) { - savedAp = ≈ + if(ap.authMode != AUTH_OPEN) { + if(cfg.password != ap.pwd) { + debug_w("Bad password for '%s'", cfg.ssid.c_str()); + return false; } + } - debug_i("Connected to SSID '%s'", cfg.ssid.c_str()); - - autoConnect = cfg.autoConnectOnStartup; - return true; + currentAp = ≈ + if(cfg.save) { + savedAp = ≈ } + + debug_i("Connected to SSID '%s'", cfg.ssid.c_str()); + + autoConnect = cfg.autoConnectOnStartup; + return true; } debug_w("SSID '%s' not found", cfg.ssid.c_str()); diff --git a/Sming/Components/Network/src/Network/Http/HttpServerConnection.h b/Sming/Components/Network/src/Network/Http/HttpServerConnection.h index 82a2fae9d5..0bb7958231 100644 --- a/Sming/Components/Network/src/Network/Http/HttpServerConnection.h +++ b/Sming/Components/Network/src/Network/Http/HttpServerConnection.h @@ -68,7 +68,7 @@ class HttpServerConnection : public HttpConnection } } - using TcpClient::send; + using HttpConnection::send; void setUpgradeCallback(HttpServerProtocolUpgradeCallback callback) { @@ -86,6 +86,11 @@ class HttpServerConnection : public HttpConnection } protected: + bool send(HttpRequest* request) override + { + return HttpConnection::send(request); + } + // HTTP parser methods int onMessageBegin(http_parser* parser) override; diff --git a/Sming/Libraries/CommandProcessing/samples/Arducam/app/ArduCamCommand.cpp b/Sming/Libraries/CommandProcessing/samples/Arducam/app/ArduCamCommand.cpp index c4bd3cddb3..2ddc3397f7 100644 --- a/Sming/Libraries/CommandProcessing/samples/Arducam/app/ArduCamCommand.cpp +++ b/Sming/Libraries/CommandProcessing/samples/Arducam/app/ArduCamCommand.cpp @@ -4,7 +4,7 @@ #include ArduCamCommand::ArduCamCommand(ArduCAM& CAM, CommandProcessing::Handler& commandHandler) - : myCAM(CAM), commandHandler(&commandHandler), imgSize(OV2640_320x240), imgType(JPEG) + : myCAM(CAM), commandHandler(&commandHandler), imgType(JPEG), imgSize(OV2640_320x240) { debug_d("ArduCamCommand Instantiating"); } @@ -155,7 +155,7 @@ void ArduCamCommand::setType(const String& type) setFormat(type == "BMP" ? BMP : JPEG); } -void ArduCamCommand::setFormat(uint8 type) +void ArduCamCommand::setFormat(uint8_t type) { if(type == BMP) { myCAM.set_format(BMP); diff --git a/Sming/Libraries/CommandProcessing/samples/Arducam/include/ArduCamCommand.h b/Sming/Libraries/CommandProcessing/samples/Arducam/include/ArduCamCommand.h index e3f3ffc1c9..ec07fc3690 100644 --- a/Sming/Libraries/CommandProcessing/samples/Arducam/include/ArduCamCommand.h +++ b/Sming/Libraries/CommandProcessing/samples/Arducam/include/ArduCamCommand.h @@ -23,11 +23,12 @@ class ArduCamCommand bool status = true; ArduCAM myCAM; CommandProcessing::Handler* commandHandler{nullptr}; - uint8 imgType; - uint8 imgSize; + uint8_t imgType; + uint8_t imgSize; + void processSetCommands(String commandLine, ReadWriteStream& commandOutput); - void setFormat(uint8 type); + void setFormat(uint8_t type); void showSettings(ReadWriteStream& commandOutput); const char* getImageType(); diff --git a/Sming/Libraries/Graphics b/Sming/Libraries/Graphics index 45ffbabc5e..7d2b503082 160000 --- a/Sming/Libraries/Graphics +++ b/Sming/Libraries/Graphics @@ -1 +1 @@ -Subproject commit 45ffbabc5e1ace51979ee84cf75d5a0ea635d296 +Subproject commit 7d2b503082c4219aaba0090f9ed4707b25740afa diff --git a/Sming/Libraries/IOControl b/Sming/Libraries/IOControl index b84671a34d..cd42d86b26 160000 --- a/Sming/Libraries/IOControl +++ b/Sming/Libraries/IOControl @@ -1 +1 @@ -Subproject commit b84671a34d452c49a0743b33070942c4bae3a474 +Subproject commit cd42d86b26901af9fedddb04a933807c5206ee5d diff --git a/Sming/Libraries/LittleFS b/Sming/Libraries/LittleFS index 21578a4816..237f1b73f0 160000 --- a/Sming/Libraries/LittleFS +++ b/Sming/Libraries/LittleFS @@ -1 +1 @@ -Subproject commit 21578a4816c5a8e17fa8aa091d149a53b9182631 +Subproject commit 237f1b73f0ba9bb8a49c0b6f5b209c56dd981550 diff --git a/Sming/Libraries/MDNS b/Sming/Libraries/MDNS index cb2d299958..bb7f00d3b9 160000 --- a/Sming/Libraries/MDNS +++ b/Sming/Libraries/MDNS @@ -1 +1 @@ -Subproject commit cb2d299958ee08d7bcdc3b8431c313e1057dda77 +Subproject commit bb7f00d3b91e5c4b2aa728057dbe5b1eac13a91a diff --git a/Sming/Libraries/MFRC522/MFRC522.cpp b/Sming/Libraries/MFRC522/MFRC522.cpp index 5b79dff3d5..f7411bb7de 100644 --- a/Sming/Libraries/MFRC522/MFRC522.cpp +++ b/Sming/Libraries/MFRC522/MFRC522.cpp @@ -30,8 +30,7 @@ void MFRC522::setSPIConfig() { } // End setSPIConfig() -void ICACHE_FLASH_ATTR - MFRC522::setControlPins(byte csPin,byte pdPin) { +void MFRC522::setControlPins(byte csPin,byte pdPin) { _chipSelectPin = csPin; _resetPowerDownPin = pdPin; diff --git a/Sming/Libraries/MFRC522/MFRC522.h b/Sming/Libraries/MFRC522/MFRC522.h index 927ee3f4df..5c849d1cb7 100644 --- a/Sming/Libraries/MFRC522/MFRC522.h +++ b/Sming/Libraries/MFRC522/MFRC522.h @@ -337,7 +337,7 @@ class MFRC522 { // Functions for manipulating the MFRC522 ///////////////////////////////////////////////////////////////////////////////////// void PCD_Init(); - void ICACHE_FLASH_ATTR setControlPins(byte csPin,byte pdPin); + void setControlPins(byte csPin,byte pdPin); void PCD_Reset(); void PCD_AntennaOn(); void PCD_AntennaOff(); diff --git a/Sming/Libraries/OneWire/OneWire.h b/Sming/Libraries/OneWire/OneWire.h index 80ca49e840..0a4c5a20be 100644 --- a/Sming/Libraries/OneWire/OneWire.h +++ b/Sming/Libraries/OneWire/OneWire.h @@ -137,7 +137,7 @@ class OneWire OneWire(uint8_t pin); void begin(); - void begin(uint8 pinOneWire); + void begin(uint8_t pinOneWire); // Perform a 1-Wire reset cycle. Returns 1 if a device responds // with a presence pulse. Returns 0 if there is no device or the diff --git a/Sming/Libraries/OtaUpgrade/OtaUpgrade/BasicStream.cpp b/Sming/Libraries/OtaUpgrade/OtaUpgrade/BasicStream.cpp index 094b0f3c00..c4575e2a5a 100644 --- a/Sming/Libraries/OtaUpgrade/OtaUpgrade/BasicStream.cpp +++ b/Sming/Libraries/OtaUpgrade/OtaUpgrade/BasicStream.cpp @@ -14,8 +14,6 @@ #include #include -extern "C" uint32 user_rf_cal_sector_set(void); - namespace OtaUpgrade { #ifndef ENABLE_OTA_DOWNGRADE diff --git a/Sming/Libraries/OtaUpgradeMqtt/samples/Upgrade/app/application.cpp b/Sming/Libraries/OtaUpgradeMqtt/samples/Upgrade/app/application.cpp index 009c1ed25a..042bae2d0d 100644 --- a/Sming/Libraries/OtaUpgradeMqtt/samples/Upgrade/app/application.cpp +++ b/Sming/Libraries/OtaUpgradeMqtt/samples/Upgrade/app/application.cpp @@ -92,10 +92,10 @@ void otaUpdate() void showInfo() { - Serial.printf(_F("\r\nSDK: v%s\r\n"), system_get_sdk_version()); - Serial.printf(_F("Free Heap: %d\r\n"), system_get_free_heap_size()); - Serial.printf(_F("CPU Frequency: %d MHz\r\n"), system_get_cpu_freq()); - Serial.printf(_F("System Chip ID: %x\r\n"), system_get_chip_id()); + Serial << endl << _F("SDK: v") << system_get_sdk_version() << endl; + Serial << _F("Free Heap: ") << system_get_free_heap_size() << endl; + Serial << _F("CPU Frequency: ") << system_get_cpu_freq() << _F(" MHz") << endl; + Serial << _F("System Chip ID: ") << String(system_get_chip_id(), HEX, 8) << endl; int total = 0; for(auto part : OtaManager.getBootPartitions()) { diff --git a/Sming/Libraries/Servo/Servo.h b/Sming/Libraries/Servo/Servo.h index 6d07753c56..1b13f6e1bb 100644 --- a/Sming/Libraries/Servo/Servo.h +++ b/Sming/Libraries/Servo/Servo.h @@ -78,13 +78,13 @@ class Servo private: ServoChannel* channels[maxChannels] = {0}; - unsigned channelCount = 0; ///< Number of active channels - HardwareTimer hardwareTimer; ///< Handles generation of output signals - Frame frames[2]; ///< Contains the active and next frames - uint8 activeFrameIndex = 0; ///< Frame being used by ISR - uint8 activeSlot = 0; ///< Slot being output by ISR - uint8_t nextFrameIndex = 0; ///< Frame to use when active frame has completed - SimpleTimer updateTimer; ///< If necessary, updates are pended + unsigned channelCount = 0; ///< Number of active channels + HardwareTimer hardwareTimer; ///< Handles generation of output signals + Frame frames[2]; ///< Contains the active and next frames + uint8_t activeFrameIndex = 0; ///< Frame being used by ISR + uint8_t activeSlot = 0; ///< Slot being output by ISR + uint8_t nextFrameIndex = 0; ///< Frame to use when active frame has completed + SimpleTimer updateTimer; ///< If necessary, updates are pended }; extern Servo servo; ///< global instance of the servo object diff --git a/Sming/Libraries/UPnP b/Sming/Libraries/UPnP index 28348c3d3e..6b73de1097 160000 --- a/Sming/Libraries/UPnP +++ b/Sming/Libraries/UPnP @@ -1 +1 @@ -Subproject commit 28348c3d3e60d7e3b38e53ba853493f0549b894c +Subproject commit 6b73de10973c513201acac11d10ae38d13580c6d diff --git a/Sming/Libraries/UPnP-Schema b/Sming/Libraries/UPnP-Schema index 4a0945dddc..0bb97e2287 160000 --- a/Sming/Libraries/UPnP-Schema +++ b/Sming/Libraries/UPnP-Schema @@ -1 +1 @@ -Subproject commit 4a0945dddc69fe138efb7b79aabca8eb04f9230e +Subproject commit 0bb97e228763fb7253dc20a221fb34771e7b6ff3 diff --git a/Sming/System/include/gdb/gdb_hooks.h b/Sming/System/include/gdb/gdb_hooks.h index 9821c29a10..e89f213b6c 100644 --- a/Sming/System/include/gdb/gdb_hooks.h +++ b/Sming/System/include/gdb/gdb_hooks.h @@ -58,7 +58,11 @@ void gdb_enable(bool state); #elif defined(ARCH_ESP8266) #define gdb_do_break() __asm__("break 0,0") #elif defined(ARCH_ESP32) +#if ESP_IDF_VERSION_MAJOR < 5 #define gdb_do_break() cpu_hal_break() +#else +#define gdb_do_break() esp_cpu_dbgr_break() +#endif #elif defined(ARCH_RP2040) #define gdb_do_break() __asm__("bkpt #0") #endif diff --git a/Sming/Wiring/FakePgmSpace.h b/Sming/Wiring/FakePgmSpace.h index 65b2a231a8..fae56002c7 100644 --- a/Sming/Wiring/FakePgmSpace.h +++ b/Sming/Wiring/FakePgmSpace.h @@ -137,7 +137,7 @@ int memcmp_aligned(const void* ptr1, const void* ptr2, unsigned len); * @brief Declare a global reference to a PSTR instance * @param name */ -#define DECLARE_PSTR(name) extern const char name[] PROGMEM; +#define DECLARE_PSTR(name) extern const char name[]; /** * @brief Create a local (stack) buffer called `name` and load it with flash data. diff --git a/samples/Basic_ProgMem/app/TestProgmem.cpp b/samples/Basic_ProgMem/app/TestProgmem.cpp index aac4f916b1..1815dc374d 100644 --- a/samples/Basic_ProgMem/app/TestProgmem.cpp +++ b/samples/Basic_ProgMem/app/TestProgmem.cpp @@ -49,7 +49,10 @@ void testPSTR(Print& out) // Note that characters after first nul won't be shown ... out.print("> demoPSTR1 (print char*): "); - out << '"' << _FLOAD(demoPSTR1) << '"' << endl; + { + LOAD_PSTR(s, demoPSTR1) + out << '"' << s << '"' << endl; + } // ... now they will: note buf will be aligned up to next dword boundary though out.print("> demoPSTR1 (write): "); @@ -131,7 +134,10 @@ void testFSTR(Print& out) // Test equality operators #define TEST(_test) out.printf(_F("%s: %s\n"), (_test) ? _F("PASS") : _F("FAIL"), _F(#_test)); TEST(demoFSTR1 == demoFSTR2) - TEST(demoFSTR1 != _FLOAD(demoPSTR1)) + { + LOAD_PSTR(s, demoPSTR1); + TEST(demoFSTR1 != s) + } TEST(String(demoFSTR1) == demoFSTR2) TEST(demoFSTR1 == String(demoFSTR2)) #undef TEST diff --git a/samples/Basic_Servo/app/application.cpp b/samples/Basic_Servo/app/application.cpp index 9fb66c4390..970d6fbb1c 100644 --- a/samples/Basic_Servo/app/application.cpp +++ b/samples/Basic_Servo/app/application.cpp @@ -31,8 +31,8 @@ class MyServoChannel : public ServoChannel private: Timer timer; - uint16 centerdelay = 0; - uint32 value = 0; + uint16_t centerdelay = 0; + uint32_t value = 0; int degree = 0; }; diff --git a/samples/Basic_Storage/app/application.cpp b/samples/Basic_Storage/app/application.cpp index e9fa62b362..04a260862a 100644 --- a/samples/Basic_Storage/app/application.cpp +++ b/samples/Basic_Storage/app/application.cpp @@ -12,16 +12,18 @@ void listSpiffsPartitions() Serial << _F(">> Mounting '") << part.name() << "' ..." << endl; bool ok = spiffs_mount(part); Serial.println(ok ? "OK, listing files:" : "Mount failed!"); - if(ok) { - Directory dir; - if(dir.open()) { - while(dir.next()) { - Serial.print(" "); - Serial.println(dir.stat().name); - } + if(!ok) { + continue; + } + + Directory dir; + if(dir.open()) { + while(dir.next()) { + Serial.print(" "); + Serial.println(dir.stat().name); } - Serial << dir.count() << _F(" files found") << endl << endl; } + Serial << dir.count() << _F(" files found") << endl << endl; } } diff --git a/samples/Basic_WebSkeletonApp/app/application.cpp b/samples/Basic_WebSkeletonApp/app/application.cpp index dcdbc84d42..ace2e6c8e6 100644 --- a/samples/Basic_WebSkeletonApp/app/application.cpp +++ b/samples/Basic_WebSkeletonApp/app/application.cpp @@ -36,7 +36,6 @@ void init() { Serial.begin(SERIAL_BAUD_RATE); // 115200 by default Serial.systemDebugOutput(true); - Serial.commandProcessing(false); #ifndef ENABLE_FLASHSTRING_MAP spiffs_mount(); // Mount file system, in order to work with files diff --git a/samples/Basic_WebSkeletonApp_LTS/app/application.cpp b/samples/Basic_WebSkeletonApp_LTS/app/application.cpp index 2d3d341db2..96b54664e4 100644 --- a/samples/Basic_WebSkeletonApp_LTS/app/application.cpp +++ b/samples/Basic_WebSkeletonApp_LTS/app/application.cpp @@ -12,7 +12,6 @@ void init() spiffs_mount(); // Mount file system, in order to work with files Serial.begin(SERIAL_BAUD_RATE); // 115200 by default Serial.systemDebugOutput(false); - Serial.commandProcessing(false); // Set higher CPU freq & disable wifi sleep System.setCpuFrequency(CpuCycleClockFast::cpuFrequency()); diff --git a/samples/Network_Ping/app/application.cpp b/samples/Network_Ping/app/application.cpp index 1c7521fb95..12c0aa5a42 100644 --- a/samples/Network_Ping/app/application.cpp +++ b/samples/Network_Ping/app/application.cpp @@ -20,7 +20,7 @@ constexpr uint8_t RESTART_DELAY_SECONDS = 2; Timer procTimer; -void ping(uint32 ip); +void ping(uint32_t ip); void pingTask() { diff --git a/samples/SDCard/app/application.cpp b/samples/SDCard/app/application.cpp index 7a777efb60..7d2eee5be9 100644 --- a/samples/SDCard/app/application.cpp +++ b/samples/SDCard/app/application.cpp @@ -108,7 +108,7 @@ void stat_file(char* fname) break; default: - Serial.printf(_F("Error(%d)\r\n"), fr); + Serial << _F("Error ") << fr << endl; } } @@ -162,11 +162,11 @@ void readWriteTest() f_printf(&file, _F(" has %d letters\r\n"), actual); if(actual != 5) { - Serial.printf(_F("Only written %u bytes\r\n"), actual); + Serial << _F("Only written ") << actual << _F(" bytes") << endl; } f_close(&file); } else { - Serial.printf(_F("fopen FAIL: %u\r\n"), (unsigned int)fRes); + Serial << _F("fopen FAIL: ") << fRes << endl; } } @@ -185,11 +185,11 @@ void readTest() f_read(&file, buffer, sizeof(buffer), &actual); buffer[actual] = 0; - Serial.printf(_F("Read: %s\r\n"), buffer); + Serial << _F("Read: ") << buffer << endl; f_close(&file); } else { - Serial.printf(_F("fopen FAIL: %u\r\n"), fRes); + Serial << _F("fopen FAIL: ") << fRes << endl; } } @@ -200,7 +200,7 @@ bool speedTest(unsigned num) uint32_t bytesPerRound; }; - static Test tests[] PROGMEM{ + static const Test tests[] PROGMEM{ {1024, 1}, {1024, 64}, {1024, 128}, {1024, 512}, {1024, 1024}, {4096, 1024}, {8192, 512}, {8192, 1024}, {8192, 8192}, }; @@ -209,7 +209,7 @@ bool speedTest(unsigned num) return false; } - Serial.printf(_F("4.%u: Write speed benchmark\r\n"), num + 1); + Serial << "4." << num + 1 << _F(": Write speed benchmark") << endl; auto& test = tests[num]; String filename; diff --git a/tests/HostTests/modules/String.cpp b/tests/HostTests/modules/String.cpp index ab0e0d87fa..8c48e8e5e5 100644 --- a/tests/HostTests/modules/String.cpp +++ b/tests/HostTests/modules/String.cpp @@ -156,7 +156,7 @@ class StringTest : public TestGroup void testMakeHexString() { - uint8 hwaddr[] = {0xaa, 0xbb, 0xcc, 0xdd, 0x12, 0x55, 0x00}; + uint8_t hwaddr[] = {0xaa, 0xbb, 0xcc, 0xdd, 0x12, 0x55, 0x00}; REQUIRE(makeHexString(nullptr, 6) == String::empty); REQUIRE(makeHexString(hwaddr, 0) == String::empty); REQUIRE(makeHexString(hwaddr, 6) == F("aabbccdd1255")); From 7af1fac17092dd34af166edf1f8259ea51a06713 Mon Sep 17 00:00:00 2001 From: Mike Date: Mon, 25 Mar 2024 14:57:21 +0000 Subject: [PATCH 034/128] Update CI action versions, readthedocs (#2743) This PR updates github action versions as some are now deprecated and cause build errors. Everything now builds against ubuntu-22.04. Sphinx has been updated from 4.2.0 to current 7.2.6 release. Noted that with Sming 5.1 seqdiag is broken (on https://sming.readthedocs.io/en/latest/information/tasks.html) because of pillow; downgrading to 9.5.0 fixes this. Also included are some minor documentation fixes. --- .github/workflows/codeql-analysis.yml | 8 +++---- .github/workflows/coverity-scan.yml | 4 ++-- .github/workflows/release.yml | 2 +- .github/workflows/spelling-check.yml | 2 +- .readthedocs.yml | 4 ++-- Sming/Arch/Esp32/Components/driver/pwm.rst | 21 ------------------- .../Arch/Host/Components/esp_wifi/README.rst | 2 +- Sming/Arch/Host/Components/hostlib/README.rst | 2 +- Sming/Components/Network/telnet.rst | 8 ++----- docs/requirements.txt | 13 ++++++------ docs/source/information/develop/ci.rst | 2 +- docs/source/upgrading/index.rst | 1 + 12 files changed, 22 insertions(+), 47 deletions(-) delete mode 100644 Sming/Arch/Esp32/Components/driver/pwm.rst diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index da50404adc..3b56ecb4f0 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -17,7 +17,7 @@ on: jobs: analyze: name: Analyze - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest strategy: fail-fast: false @@ -30,7 +30,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: # We must fetch at least the immediate parents so that if this is # a pull request then we can checkout the head. @@ -38,7 +38,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -67,4 +67,4 @@ jobs: make -j3 SMING_ARCH=Host - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/coverity-scan.yml b/.github/workflows/coverity-scan.yml index 10287b0e53..21b710e246 100644 --- a/.github/workflows/coverity-scan.yml +++ b/.github/workflows/coverity-scan.yml @@ -18,11 +18,11 @@ jobs: # group: ${{ github.head_ref || github.run_id }} # cancel-in-progress: true - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Check if we are allowed to scan env: BRANCH: ${{github.ref_name}} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 06cf230995..6eb7520fce 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,7 +12,7 @@ jobs: release: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: trstringer/manual-approval@v1 if: ${{ github.ref_type == 'tag' }} with: diff --git a/.github/workflows/spelling-check.yml b/.github/workflows/spelling-check.yml index 4e8d27e7be..9d3a76dba8 100644 --- a/.github/workflows/spelling-check.yml +++ b/.github/workflows/spelling-check.yml @@ -10,7 +10,7 @@ jobs: name: Check spelling runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: false - name: Get submodules diff --git a/.readthedocs.yml b/.readthedocs.yml index fa3b0a80e9..13b0003085 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -7,9 +7,9 @@ version: 2 # Required build: - os: ubuntu-20.04 + os: ubuntu-22.04 tools: - python: "3.8" + python: "3.12" # Optionally build your docs in additional formats such as PDF and ePub formats: diff --git a/Sming/Arch/Esp32/Components/driver/pwm.rst b/Sming/Arch/Esp32/Components/driver/pwm.rst deleted file mode 100644 index 47df1eb558..0000000000 --- a/Sming/Arch/Esp32/Components/driver/pwm.rst +++ /dev/null @@ -1,21 +0,0 @@ -PWM: Pulse-Width Modulation -=========================== - -The driver interface is defined in the ESP8266 SDK. - -Build variables ---------------- - -.. envvar:: ENABLE_CUSTOM_PWM - - undefined - use the Espressif PWM driver - - 1 (default) - Use the *New PWM* driver, a drop-in replacement for the version provided in the Espressif SDK. - -API Documentation ------------------ - -.. doxygengroup:: pwm_driver - :content-only: diff --git a/Sming/Arch/Host/Components/esp_wifi/README.rst b/Sming/Arch/Host/Components/esp_wifi/README.rst index 18270f816d..f13ef4ccde 100644 --- a/Sming/Arch/Host/Components/esp_wifi/README.rst +++ b/Sming/Arch/Host/Components/esp_wifi/README.rst @@ -1,4 +1,4 @@ Host WiFi ========= -Provides WiFi / network functions. The actual implementations are provided in :component-host:`lwip` +Provides WiFi / network functions. The actual implementations are provided in :component:`lwip` diff --git a/Sming/Arch/Host/Components/hostlib/README.rst b/Sming/Arch/Host/Components/hostlib/README.rst index 55716d30db..3e6f37db68 100644 --- a/Sming/Arch/Host/Components/hostlib/README.rst +++ b/Sming/Arch/Host/Components/hostlib/README.rst @@ -17,7 +17,7 @@ the :cpp:class:`CommandLine`. Startup ------- -Initialises :component-host:`spi_flash`, Uart server (in :component-host:`driver`) and :component-host:`lwip` +Initialises :component-host:`spi_flash`, Uart server (in :component-host:`driver`) and :component:`lwip` networking, then enters the main task loop. This loop services LWIP plus the task and timer queues (implemented in :component-host:`esp_hal`). The ``Ctrl+C`` keypress is trapped to provide an orderly exit. If the system has become stuck in a loop or is otherwise diff --git a/Sming/Components/Network/telnet.rst b/Sming/Components/Network/telnet.rst index 4ab02098fe..108342a335 100644 --- a/Sming/Components/Network/telnet.rst +++ b/Sming/Components/Network/telnet.rst @@ -3,9 +3,5 @@ Telnet https://en.m.wikipedia.org/wiki/Telnet -Server API ----------- - -.. doxygengroup:: telnetserver - :content-only: - :members: +This is a very simple protocol which can be implemented using a :cpp:class:`TcpServer` class. +See :sample:`TelnetServer` for an example application. diff --git a/docs/requirements.txt b/docs/requirements.txt index f731a1c431..0c9339c546 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,13 +1,12 @@ # Requirements file for pip # list of Python packages used in documentation build -sphinx==4.2.0 -sphinx-rtd-theme==1.0.0 -m2r2==0.3.2 -# mistune==2.0.3 # Version 2 not compatible with m2r2 -breathe==4.31.0 +sphinx==7.2.6 +sphinx-rtd-theme==2.0.0 +m2r2==0.3.3.post2 +breathe==4.35.0 sphinxcontrib-wavedrom sphinx-copybutton -sphinxcontrib-seqdiag==2.0.0 +sphinxcontrib-seqdiag jinja2>=3.0.3 setuptools>=57.5.0 -funcparserlib==1.0.0a0 +pillow==9.5.0 # V10 breaks seqdiag diff --git a/docs/source/information/develop/ci.rst b/docs/source/information/develop/ci.rst index 30062b52d0..4c55e692d2 100644 --- a/docs/source/information/develop/ci.rst +++ b/docs/source/information/develop/ci.rst @@ -120,7 +120,7 @@ Set ``Project URL slug`` For example, testing the ``Sming-jerryscript`` library requires this value to be set to ``jerryscript`` to match the Sming library name. - Build logs should then report a warning ``Multiple matches found for Component 'jerryscript'. + Build logs should then report a warning ``Multiple matches found for Component 'jerryscript'``. Set sming fork/branch By default builds use the main Sming ``develop`` branch. diff --git a/docs/source/upgrading/index.rst b/docs/source/upgrading/index.rst index 9677ddf287..a0e3ace2a7 100644 --- a/docs/source/upgrading/index.rst +++ b/docs/source/upgrading/index.rst @@ -7,6 +7,7 @@ For newer versions we have dedicated pages. .. toctree:: :maxdepth: 1 + 5.1-5.2 4.7-5.1 4.6-4.7 4.5-4.6 From 2db98d5606c1f7612f948ae93b71fadbc612e25c Mon Sep 17 00:00:00 2001 From: Mike Date: Tue, 26 Mar 2024 08:49:47 +0000 Subject: [PATCH 035/128] Improve `Vector` and `HttpHeaders` iteration (#2745) This PR improves use of C++ iterator pattern instead of traditional `for(i=0; i %d", commands.keyAt(i).c_str(), commands.valueAt(i)); + for(auto cmd : commands) { + host_debug_i("\t%s => %d", cmd.key().c_str(), cmd.value()); } host_debug_i("Connected. Starting application"); diff --git a/Sming/Components/Network/src/Network/Http/HttpClientConnection.cpp b/Sming/Components/Network/src/Network/Http/HttpClientConnection.cpp index cac3f32ce5..d939bfb915 100644 --- a/Sming/Components/Network/src/Network/Http/HttpClientConnection.cpp +++ b/Sming/Components/Network/src/Network/Http/HttpClientConnection.cpp @@ -367,9 +367,9 @@ void HttpClientConnection::sendRequestHeaders(HttpRequest* request) request->headers[HTTP_HEADER_TRANSFER_ENCODING] = _F("chunked"); } - for(unsigned i = 0; i < request->headers.count(); i++) { + for(auto hdr : request->headers) { // TODO: add name and/or value escaping (implement in HttpHeaders) - sendString(request->headers[i]); + sendString(hdr); } sendString("\r\n"); } diff --git a/Sming/Components/Network/src/Network/Http/HttpHeaders.cpp b/Sming/Components/Network/src/Network/Http/HttpHeaders.cpp index 9f98985693..1dfc8acd9f 100644 --- a/Sming/Components/Network/src/Network/Http/HttpHeaders.cpp +++ b/Sming/Components/Network/src/Network/Http/HttpHeaders.cpp @@ -11,6 +11,25 @@ #include "HttpHeaders.h" #include +String HttpHeaders::HeaderConst::getFieldName() const +{ + return fields.toString(key()); +} + +size_t HttpHeaders::HeaderConst::printTo(Print& p) const +{ + size_t n{0}; + n += p.print(getFieldName()); + n += p.print(" = "); + n += p.print(value()); + return n; +} + +HttpHeaders::HeaderConst::operator String() const +{ + return fields.toString(key(), value()); +} + const String& HttpHeaders::operator[](const String& name) const { auto field = fromString(name); @@ -40,9 +59,7 @@ bool HttpHeaders::append(const HttpHeaderFieldName& name, const String& value) void HttpHeaders::setMultiple(const HttpHeaders& headers) { - for(unsigned i = 0; i < headers.count(); i++) { - HttpHeaderFieldName fieldName = headers.keyAt(i); - auto fieldNameString = headers.toString(fieldName); - operator[](fieldNameString) = headers.valueAt(i); + for(auto hdr : headers) { + operator[](hdr.getFieldName()) = hdr.value(); } } diff --git a/Sming/Components/Network/src/Network/Http/HttpHeaders.h b/Sming/Components/Network/src/Network/Http/HttpHeaders.h index 8d181c6eae..af94df076e 100644 --- a/Sming/Components/Network/src/Network/Http/HttpHeaders.h +++ b/Sming/Components/Network/src/Network/Http/HttpHeaders.h @@ -34,6 +34,44 @@ class HttpHeaders : public HttpHeaderFields, private HashMap { public: + class HeaderConst : public BaseElement + { + public: + HeaderConst(const HttpHeaderFields& fields, const HttpHeaderFieldName& key, const String& value) + : BaseElement(key, value), fields(fields) + { + } + + String getFieldName() const; + operator String() const; + size_t printTo(Print& p) const; + + private: + const HttpHeaderFields& fields; + }; + + class Iterator : public HashMap::Iterator + { + public: + Iterator(const HttpHeaders& headers, unsigned index) + : HashMap::Iterator::Iterator(headers, index), fields(headers) + { + } + + HeaderConst operator*() + { + return HeaderConst(fields, map.keyAt(index), map.valueAt(index)); + } + + HeaderConst operator*() const + { + return HeaderConst(fields, map.keyAt(index), map.valueAt(index)); + } + + private: + const HttpHeaderFields& fields; + }; + HttpHeaders() = default; HttpHeaders(const HttpHeaders& headers) @@ -41,6 +79,16 @@ class HttpHeaders : public HttpHeaderFields, private HashMap String operator[](const BaseElement& elem) const + { + return toString(elem.key(), elem.value()); + } + using HashMap::contains; /** diff --git a/Sming/Components/Network/src/Network/Http/HttpRequest.cpp b/Sming/Components/Network/src/Network/Http/HttpRequest.cpp index cf330c6867..5f598f8c70 100644 --- a/Sming/Components/Network/src/Network/Http/HttpRequest.cpp +++ b/Sming/Components/Network/src/Network/Http/HttpRequest.cpp @@ -77,8 +77,8 @@ String HttpRequest::toString() const if(!headers.contains(HTTP_HEADER_HOST)) { content += headers.toString(HTTP_HEADER_HOST, uri.getHostWithPort()); } - for(unsigned i = 0; i < headers.count(); i++) { - content += headers[i]; + for(auto hdr : headers) { + content += hdr; } if(!headers.contains(HTTP_HEADER_CONTENT_LENGTH)) { diff --git a/Sming/Components/Network/src/Network/Http/HttpResponse.cpp b/Sming/Components/Network/src/Network/Http/HttpResponse.cpp index dadcd817a1..559cec1b27 100644 --- a/Sming/Components/Network/src/Network/Http/HttpResponse.cpp +++ b/Sming/Components/Network/src/Network/Http/HttpResponse.cpp @@ -137,8 +137,8 @@ String HttpResponse::toString() const content += ' '; content += ::toString(code); content += " \r\n"; - for(unsigned i = 0; i < headers.count(); i++) { - content += headers[i]; + for(auto hdr : headers) { + content += hdr; } content += "\r\n"; diff --git a/Sming/Components/Network/src/Network/Http/HttpServerConnection.cpp b/Sming/Components/Network/src/Network/Http/HttpServerConnection.cpp index 2dacf1a12a..37699a5238 100644 --- a/Sming/Components/Network/src/Network/Http/HttpServerConnection.cpp +++ b/Sming/Components/Network/src/Network/Http/HttpServerConnection.cpp @@ -312,8 +312,8 @@ void HttpServerConnection::sendResponseHeaders(HttpResponse* response) response->headers[HTTP_HEADER_DATE] = DateTime(SystemClock.now(eTZ_UTC)).toHTTPDate(); } - for(unsigned i = 0; i < response->headers.count(); i++) { - sendString(response->headers[i]); + for(auto hdr : response->headers) { + sendString(hdr); } sendString("\r\n"); } diff --git a/Sming/Components/Network/src/Network/Http/Websocket/WebsocketConnection.cpp b/Sming/Components/Network/src/Network/Http/Websocket/WebsocketConnection.cpp index d161532d58..81d1c46cb4 100644 --- a/Sming/Components/Network/src/Network/Http/Websocket/WebsocketConnection.cpp +++ b/Sming/Components/Network/src/Network/Http/Websocket/WebsocketConnection.cpp @@ -292,9 +292,9 @@ void WebsocketConnection::broadcast(const char* message, size_t length, ws_frame memcpy(copy, message, length); std::shared_ptr data(copy, [](const char* ptr) { delete[] ptr; }); - for(unsigned i = 0; i < websocketList.count(); i++) { + for(auto skt : websocketList) { auto stream = new SharedMemoryStream(data, length); - websocketList[i]->send(stream, type); + skt->send(stream, type); } } diff --git a/Sming/Components/Network/src/Network/SmtpClient.cpp b/Sming/Components/Network/src/Network/SmtpClient.cpp index 5f60c5fc27..5f44450ba2 100644 --- a/Sming/Components/Network/src/Network/SmtpClient.cpp +++ b/Sming/Components/Network/src/Network/SmtpClient.cpp @@ -306,8 +306,8 @@ void SmtpClient::sendMailHeaders(MailMessage* mail) mail->stream = mStream; } - for(unsigned i = 0; i < mail->headers.count(); i++) { - sendString(mail->headers[i]); + for(auto hdr : mail->headers) { + sendString(hdr); } sendString("\r\n"); } diff --git a/Sming/Components/Network/src/Network/TcpServer.cpp b/Sming/Components/Network/src/Network/TcpServer.cpp index 926a750c0e..e64a223a4d 100644 --- a/Sming/Components/Network/src/Network/TcpServer.cpp +++ b/Sming/Components/Network/src/Network/TcpServer.cpp @@ -164,13 +164,10 @@ void TcpServer::shutdown() return; } - for(unsigned i = 0; i < connections.count(); i++) { - TcpConnection* connection = connections[i]; - if(connection == nullptr) { - continue; + for(auto& connection : connections) { + if(connection != nullptr) { + connection->setTimeOut(1); } - - connection->setTimeOut(1); } } diff --git a/Sming/Core/Data/ObjectMap.h b/Sming/Core/Data/ObjectMap.h index af75a16870..19c2acec90 100644 --- a/Sming/Core/Data/ObjectMap.h +++ b/Sming/Core/Data/ObjectMap.h @@ -208,7 +208,7 @@ template class ObjectMap */ void set(const K& key, V* value) { - int i = indexOf(key); + int i = entries.indexOf(key); if(i >= 0) { entries[i].value.reset(value); } else { @@ -224,7 +224,7 @@ template class ObjectMap */ V* find(const K& key) const { - int index = indexOf(key); + int index = entries.indexOf(key); return (index < 0) ? nullptr : entries[index].value.get(); } @@ -235,12 +235,7 @@ template class ObjectMap */ int indexOf(const K& key) const { - for(unsigned i = 0; i < entries.count(); i++) { - if(entries[i].key == key) { - return i; - } - } - return -1; + return entries.indexOf(key); } /** @@ -250,7 +245,7 @@ template class ObjectMap */ bool contains(const K& key) const { - return indexOf(key) >= 0; + return entries.contains(key); } /** @@ -272,10 +267,9 @@ template class ObjectMap int index = indexOf(key); if(index < 0) { return false; - } else { - removeAt(index); - return true; } + removeAt(index); + return true; } /** @@ -322,6 +316,11 @@ template class ObjectMap K key; std::unique_ptr value; + bool operator==(const K& keyToFind) const + { + return key == keyToFind; + } + Entry(const K& key, V* value) : key(key) { this->value.reset(value); diff --git a/Sming/Core/Data/ObjectQueue.h b/Sming/Core/Data/ObjectQueue.h index 9108764b70..3470dc4219 100644 --- a/Sming/Core/Data/ObjectQueue.h +++ b/Sming/Core/Data/ObjectQueue.h @@ -31,11 +31,11 @@ template class ObjectQueue : public FIFO T* peek() const { - return FIFO::count() ? FIFO::peek() : nullptr; + return this->count() ? FIFO::peek() : nullptr; } T* dequeue() { - return FIFO::count() ? FIFO::dequeue() : nullptr; + return this->count() ? FIFO::dequeue() : nullptr; } }; diff --git a/Sming/Libraries/DiskStorage b/Sming/Libraries/DiskStorage index 3c649c03d4..6bfcd80140 160000 --- a/Sming/Libraries/DiskStorage +++ b/Sming/Libraries/DiskStorage @@ -1 +1 @@ -Subproject commit 3c649c03d4a2e7b21c3648184ad8ebe9e0723c96 +Subproject commit 6bfcd80140f1436f6da90d6ab6a438903f8fa752 diff --git a/Sming/Libraries/Yeelight/YeelightBulb.cpp b/Sming/Libraries/Yeelight/YeelightBulb.cpp index 132ec579c7..5cb8b8f71a 100644 --- a/Sming/Libraries/Yeelight/YeelightBulb.cpp +++ b/Sming/Libraries/Yeelight/YeelightBulb.cpp @@ -49,11 +49,11 @@ void YeelightBulb::sendCommand(const String& method, const Vector& param doc["id"] = requestId++; doc["method"] = method; auto arr = doc.createNestedArray("params"); - for(unsigned i = 0; i < params.count(); i++) { - if(isNumeric(params[i])) { - arr.add(params[i].toInt()); + for(auto& param : params) { + if(isNumeric(param)) { + arr.add(param.toInt()); } else { - arr.add(params[i]); + arr.add(param); } } String request = Json::serialize(doc); diff --git a/Sming/Wiring/WHashMap.h b/Sming/Wiring/WHashMap.h index 18dbd4bbb2..923593d8cc 100644 --- a/Sming/Wiring/WHashMap.h +++ b/Sming/Wiring/WHashMap.h @@ -165,7 +165,7 @@ template class HashMap return ElementConst{map.keyAt(index), map.valueAt(index)}; } - private: + protected: Map& map; unsigned index{0}; }; diff --git a/Sming/Wiring/WVector.h b/Sming/Wiring/WVector.h index 6d6856326c..eb2f97a493 100644 --- a/Sming/Wiring/WVector.h +++ b/Sming/Wiring/WVector.h @@ -113,7 +113,7 @@ template class Vector : public Countable return _data.size; } - bool contains(const Element& elem) const + template bool contains(const T& elem) const { return indexOf(elem) >= 0; } @@ -127,7 +127,7 @@ template class Vector : public Countable return _data[0]; } - int indexOf(const Element& elem) const; + template int indexOf(const T& elem) const; bool isEmpty() const { @@ -143,7 +143,7 @@ template class Vector : public Countable return _data[_size - 1]; } - int lastIndexOf(const Element& elem) const; + template int lastIndexOf(const T& elem) const; unsigned int count() const override { @@ -178,9 +178,9 @@ template class Vector : public Countable _size = 0; } - bool removeElement(const Element& obj) + template bool removeElement(const T& elem) { - return removeElementAt(indexOf(obj)); + return removeElementAt(indexOf(elem)); } /** @@ -316,7 +316,7 @@ template void Vector::copyInto(Element* array) const } } -template int Vector::indexOf(const Element& elem) const +template template int Vector::indexOf(const T& elem) const { for(unsigned int i = 0; i < _size; i++) { if(_data[i] == elem) { @@ -327,7 +327,7 @@ template int Vector::indexOf(const Element& elem) const return -1; } -template int Vector::lastIndexOf(const Element& elem) const +template template int Vector::lastIndexOf(const T& elem) const { // check for empty vector if(_size == 0) { diff --git a/samples/Basic_Ssl/app/application.cpp b/samples/Basic_Ssl/app/application.cpp index 6c89546433..e0b9fae5eb 100644 --- a/samples/Basic_Ssl/app/application.cpp +++ b/samples/Basic_Ssl/app/application.cpp @@ -40,8 +40,8 @@ int onDownload(HttpConnection& connection, bool success) Serial << _F(", received ") << stream->available() << _F(" bytes") << endl; auto& headers = connection.getResponse()->headers; - for(unsigned i = 0; i < headers.count(); ++i) { - Serial.print(headers[i]); + for(auto hdr : headers) { + Serial.print(hdr); } auto ssl = connection.getSsl(); diff --git a/samples/Basic_WiFi/app/application.cpp b/samples/Basic_WiFi/app/application.cpp index a9eed2e2b9..8b1f26c94f 100644 --- a/samples/Basic_WiFi/app/application.cpp +++ b/samples/Basic_WiFi/app/application.cpp @@ -14,9 +14,9 @@ void listNetworks(bool succeeded, BssList& list) return; } - for(unsigned i = 0; i < list.count(); i++) { - Serial << _F("\tWiFi: ") << list[i].ssid << ", " << list[i].getAuthorizationMethodName(); - if(list[i].hidden) { + for(auto& bss : list) { + Serial << _F("\tWiFi: ") << bss.ssid << ", " << bss.getAuthorizationMethodName(); + if(bss.hidden) { Serial << _F(" (hidden)"); } Serial.println(); diff --git a/samples/HttpClient_Instapush/app/application.cpp b/samples/HttpClient_Instapush/app/application.cpp index f7c2d0333c..5575d65e95 100644 --- a/samples/HttpClient_Instapush/app/application.cpp +++ b/samples/HttpClient_Instapush/app/application.cpp @@ -50,9 +50,9 @@ class InstapushApplication : protected HttpClient DynamicJsonDocument root(1024); root["event"] = event; JsonObject trackers = root.createNestedObject("trackers"); - for(unsigned i = 0; i < trackersInfo.count(); i++) { - debugf("%s: %s", trackersInfo.keyAt(i).c_str(), trackersInfo.valueAt(i).c_str()); - trackers[trackersInfo.keyAt(i)] = trackersInfo[trackersInfo.keyAt(i)]; + for(auto info : trackersInfo) { + debugf("%s: %s", info.key().c_str(), info.value().c_str()); + trackers[info.key()] = info.value(); } auto stream = new MemoryDataStream; diff --git a/samples/HttpServer_WebSockets/app/CUserData.cpp b/samples/HttpServer_WebSockets/app/CUserData.cpp index 801ea01470..f12e893988 100644 --- a/samples/HttpServer_WebSockets/app/CUserData.cpp +++ b/samples/HttpServer_WebSockets/app/CUserData.cpp @@ -10,14 +10,14 @@ void CUserData::addSession(WebsocketConnection& connection) void CUserData::removeSession(WebsocketConnection& connection) { - for(unsigned i = 0; i < activeWebSockets.count(); i++) { - if(connection == *(activeWebSockets[i])) { - activeWebSockets[i]->setUserData(nullptr); - activeWebSockets.remove(i); - Serial.println(F("Removed user session")); - return; - } + int i = activeWebSockets.indexOf(&connection); + if(i < 0) { + return; } + + activeWebSockets[i]->setUserData(nullptr); + activeWebSockets.remove(i); + Serial.println(F("Removed user session")); } void CUserData::printMessage(WebsocketConnection& connection, const String& msg) @@ -36,8 +36,8 @@ void CUserData::printMessage(WebsocketConnection& connection, const String& msg) void CUserData::logOut() { - for(unsigned i = 0; i < activeWebSockets.count(); i++) { - activeWebSockets[i]->setUserData(nullptr); + for(auto skt : activeWebSockets) { + skt->setUserData(nullptr); } activeWebSockets.removeAllElements(); diff --git a/samples/Wifi_Sniffer/app/application.cpp b/samples/Wifi_Sniffer/app/application.cpp index 37343ca383..14d6076349 100644 --- a/samples/Wifi_Sniffer/app/application.cpp +++ b/samples/Wifi_Sniffer/app/application.cpp @@ -60,11 +60,11 @@ static void printSummary() { Serial.println("\r\n" "-------------------------------------------------------------------------------------\r\n"); - for(unsigned u = 0; u < knownClients.count(); u++) { - printClient(knownClients[u]); + for(auto& client : knownClients) { + printClient(client); } - for(unsigned u = 0; u < knownAPs.count(); u++) { - printBeacon(knownAPs[u]); + for(auto& ap : knownAPs) { + printBeacon(ap); } Serial.println("\r\n" "-------------------------------------------------------------------------------------\r\n"); diff --git a/tests/HostTests/modules/Network/Http.cpp b/tests/HostTests/modules/Network/Http.cpp index ff4e329620..132e42da08 100644 --- a/tests/HostTests/modules/Network/Http.cpp +++ b/tests/HostTests/modules/Network/Http.cpp @@ -49,8 +49,8 @@ class HttpTest : public TestGroup { #if DEBUG_VERBOSE_LEVEL == DBG Serial << _F(" count: ") << headers.count() << endl; - for(unsigned i = 0; i < headers.count(); ++i) { - String s = headers[i]; + for(auto hdr : headers) { + String s = hdr; m_printHex(" ", s.c_str(), s.length(), 0, 32); } #endif From 5de5625e9bc9d9f02c7d581d6c7adc9ec048ebbd Mon Sep 17 00:00:00 2001 From: Mike Date: Wed, 27 Mar 2024 14:33:54 +0000 Subject: [PATCH 036/128] Fix rBoot implicit `system_rtc_mem_read` warning (#2747) Co-authored-by: mikee47 --- Sming/Components/rboot/rboot | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sming/Components/rboot/rboot b/Sming/Components/rboot/rboot index 2b0fb65c7f..cd98af3e92 160000 --- a/Sming/Components/rboot/rboot +++ b/Sming/Components/rboot/rboot @@ -1 +1 @@ -Subproject commit 2b0fb65c7f2ce430865496c22e6a2801174811ba +Subproject commit cd98af3e920c86e5db9e8aa1a72901b55ce1bfb9 From 99b2ccd240e50999e4ecc756ce36202d825651f8 Mon Sep 17 00:00:00 2001 From: Mike Date: Wed, 27 Mar 2024 15:08:17 +0000 Subject: [PATCH 037/128] Improve CommandProcessing library (#2748) Here's a PR with a few suggestions to improve the CommandProcessing library. **Handling line endings is inconsistent** Running the `CommandLine` sample on Host we get this: ``` Sming>statusSming Framework Version : 5.1.0 ESP SDK version : Host-SDK-v1.0 Time = 26/03/2024 08:59:47 System Start Reason : 0 Sming> ``` On Esp8266 it looks different: ``` Sming> statusSming Framework Version : 5.1.0 ESP SDK version : 3.0.3(8427744) Time = 01/01/1970 06:42:14 System Start Reason : 6 Sming> ``` This is what it should be: ``` Sming>status Sming Framework Version : 5.1.0 ESP SDK version : 3.0.3(8427744) Time = 01/01/1970 06:42:14 System Start Reason : 6 Sming> ``` **The `currentEOL` and associated methods seem redundant** The LineBuffer handles "\r", "\n", "\n\r" and "\r\n" the same way, so we can just use that. **command name is stored twice** Using a `HashMap` means the command name is stored both as a key, and in the `Command` object. Let's just use a `Vector` instead. **localEcho** Private member variable set to true and never changed. Assume intention was `isVerbose`. **Consuming RAM for string storage** Since `name`, `help` and `group` strings never change, we should keep them in flash and save some RAM. Best way I've found is storing them all in a single flash block, which requires only 4 bytes in the `Command` object. This requires use of a `CMDP_STRINGS` macro when calling `registerCommand`. To minimise disruption for existing code this can be enabled using `CMDPROC_FLASHSTRINGS=1`. When using regular Strings, there's also a small saving to be had by storing these together in a CStringArray. For comparison, here's what `system_get_free_heap_size()` reports for the `CommandLine` sample application. The call is made at the end of `init()`. | Commit | free RAM | | ----------------------------- | --------- | | Current version using hashmap | 51704 | | After initial tidy | 51752 | | Use Vector | 52104 | | Use CStringArray | 52160 | | Use FlashString | 52664 | **Welcome message not used** This was used by original TelnetServer sample application. Have restored display of welcome message to connecting clients. Also getting junk on display because of TELNET escape sequences, so filtered those out too. --- Sming/Libraries/CommandProcessing/README.rst | 17 +- .../Libraries/CommandProcessing/component.mk | 8 +- .../samples/Arducam/app/ArduCamCommand.cpp | 11 +- .../samples/Arducam/include/ArduCamCommand.h | 4 +- .../samples/CommandLine/app/application.cpp | 3 +- .../samples/TelnetServer/app/application.cpp | 37 ++++- .../src/CommandProcessing/Command.h | 113 +++++++++++-- .../src/CommandProcessing/Handler.cpp | 148 ++++++++++-------- .../src/CommandProcessing/Handler.h | 92 ++++++----- .../HttpServer_WebSockets/app/application.cpp | 2 +- 10 files changed, 285 insertions(+), 150 deletions(-) diff --git a/Sming/Libraries/CommandProcessing/README.rst b/Sming/Libraries/CommandProcessing/README.rst index bdaad7ee9c..a8571f1fd8 100644 --- a/Sming/Libraries/CommandProcessing/README.rst +++ b/Sming/Libraries/CommandProcessing/README.rst @@ -44,15 +44,28 @@ Using void processShutdownCommand(String commandLine, ReadWriteStream& commandOutput) { - // ... + // ... } void init() { commandHandler.registerSystemCommands(); - commandHandler.registerCommand(CommandProcessing::Command("shutdown", "Shutdown Server Command", "Application", processShutdownCommand)); + commandHandler.registerCommand({CMDP_STRINGS("shutdown", "Shutdown Server Command", "Application"), processShutdownCommand}); } +.. envvar:: CMDPROC_FLASHSTRINGS + + default: undefined (RAM strings) + 1: Store strings in flash memory + + Command ``name``, ``help`` and ``group`` strings default to RAM. + This maintains backward compatibility with existing applications which may define commands like this:: + + commandHandler.registerCommand({"shutdown", "Shutdown Server Command", "Application", processShutdownCommand}); + + To save RAM, update your code to use the ``CMDP_STRINGS`` macro and build with ``CMDPROC_FLASHSTRINGS=1``. + + API Documentation ----------------- diff --git a/Sming/Libraries/CommandProcessing/component.mk b/Sming/Libraries/CommandProcessing/component.mk index 71c9f17265..dc354b834a 100644 --- a/Sming/Libraries/CommandProcessing/component.mk +++ b/Sming/Libraries/CommandProcessing/component.mk @@ -7,4 +7,10 @@ COMPONENT_INCDIRS := $(COMPONENT_SRCDIRS) COMPONENT_DOXYGEN_INPUT := src COMPONENT_DOCFILES := \ - docs/* \ No newline at end of file + docs/* + + +COMPONENT_VARS := CMDPROC_FLASHSTRINGS +ifeq ($(CMDPROC_FLASHSTRINGS),1) +GLOBAL_CFLAGS += -DCMDPROC_FLASHSTRINGS +endif diff --git a/Sming/Libraries/CommandProcessing/samples/Arducam/app/ArduCamCommand.cpp b/Sming/Libraries/CommandProcessing/samples/Arducam/app/ArduCamCommand.cpp index 2ddc3397f7..a81e6c5b42 100644 --- a/Sming/Libraries/CommandProcessing/samples/Arducam/app/ArduCamCommand.cpp +++ b/Sming/Libraries/CommandProcessing/samples/Arducam/app/ArduCamCommand.cpp @@ -4,20 +4,15 @@ #include ArduCamCommand::ArduCamCommand(ArduCAM& CAM, CommandProcessing::Handler& commandHandler) - : myCAM(CAM), commandHandler(&commandHandler), imgType(JPEG), imgSize(OV2640_320x240) + : myCAM(CAM), commandHandler(commandHandler), imgType(JPEG), imgSize(OV2640_320x240) { debug_d("ArduCamCommand Instantiating"); } -ArduCamCommand::~ArduCamCommand() -{ -} - void ArduCamCommand::initCommand() { - commandHandler->registerCommand( - CommandProcessing::Command("set", "ArduCAM config commands", "Application", - CommandProcessing::Command::Callback(&ArduCamCommand::processSetCommands, this))); + commandHandler.registerCommand( + {CMDP_STRINGS("set", "ArduCAM config commands", "Application"), {&ArduCamCommand::processSetCommands, this}}); } void ArduCamCommand::showSettings(ReadWriteStream& commandOutput) diff --git a/Sming/Libraries/CommandProcessing/samples/Arducam/include/ArduCamCommand.h b/Sming/Libraries/CommandProcessing/samples/Arducam/include/ArduCamCommand.h index ec07fc3690..73b304c33d 100644 --- a/Sming/Libraries/CommandProcessing/samples/Arducam/include/ArduCamCommand.h +++ b/Sming/Libraries/CommandProcessing/samples/Arducam/include/ArduCamCommand.h @@ -13,16 +13,14 @@ class ArduCamCommand { public: ArduCamCommand(ArduCAM& CAM, CommandProcessing::Handler& commandHandler); - virtual ~ArduCamCommand(); void initCommand(); const char* getContentType(); void setSize(const String& size); void setType(const String& type); private: - bool status = true; ArduCAM myCAM; - CommandProcessing::Handler* commandHandler{nullptr}; + CommandProcessing::Handler& commandHandler; uint8_t imgType; uint8_t imgSize; diff --git a/Sming/Libraries/CommandProcessing/samples/CommandLine/app/application.cpp b/Sming/Libraries/CommandProcessing/samples/CommandLine/app/application.cpp index c91c116de1..17d91cf721 100644 --- a/Sming/Libraries/CommandProcessing/samples/CommandLine/app/application.cpp +++ b/Sming/Libraries/CommandProcessing/samples/CommandLine/app/application.cpp @@ -47,6 +47,5 @@ void init() CommandProcessing::enable(commandHandler, Serial); commandHandler.registerSystemCommands(); - commandHandler.registerCommand( - CommandProcessing::Command("example", "Example Command", "Application", processExampleCommand)); + commandHandler.registerCommand({CMDP_STRINGS("example", "Example Command", "Application"), processExampleCommand}); } diff --git a/Sming/Libraries/CommandProcessing/samples/TelnetServer/app/application.cpp b/Sming/Libraries/CommandProcessing/samples/TelnetServer/app/application.cpp index 2d688cf245..8dc8e9be27 100644 --- a/Sming/Libraries/CommandProcessing/samples/TelnetServer/app/application.cpp +++ b/Sming/Libraries/CommandProcessing/samples/TelnetServer/app/application.cpp @@ -11,9 +11,39 @@ namespace { CommandProcessing::Handler commandHandler; +void clientConnected(TcpClient* client) +{ + Serial << _F("Client ") << client->getRemoteIp() << _F(" connected") << endl; + if(commandHandler.isVerbose()) { + client->sendString(commandHandler.getCommandWelcomeMessage()); + client->sendString(commandHandler.getCommandPrompt()); + } +} + +void clientComplete(TcpClient& client, bool successful) +{ + Serial << _F("Client disconnected") << endl; +} + bool processTelnetInput(TcpClient& client, char* data, int size) { - return client.sendString(commandHandler.processNow(data, size)); + // Ignore TELNET escape sequences + constexpr char TC_ESC{'\xff'}; + uint8_t skip{}; + while(size-- > 0) { + char c = *data++; + if(skip) { + --skip; + } else if(c == '\xff') { + skip = 2; + } else { + commandHandler.process(c); + } + } + + String s; + commandHandler.getOutputStream().moveString(s); + return s ? client.sendString(s) : true; } void processExampleCommand(String commandLine, ReadWriteStream& commandOutput) @@ -32,11 +62,10 @@ void processExampleCommand(String commandLine, ReadWriteStream& commandOutput) void initCommands() { commandHandler.registerSystemCommands(); - commandHandler.registerCommand( - CommandProcessing::Command("example", "Example Command", "Application", processExampleCommand)); + commandHandler.registerCommand({CMDP_STRINGS("example", "Example Command", "Application"), processExampleCommand}); } -TcpServer telnetServer(processTelnetInput); +TcpServer telnetServer(clientConnected, processTelnetInput, clientComplete); // Will be called when station is fully operational void gotIP(IpAddress ip, IpAddress netmask, IpAddress gateway) diff --git a/Sming/Libraries/CommandProcessing/src/CommandProcessing/Command.h b/Sming/Libraries/CommandProcessing/src/CommandProcessing/Command.h index 9c40fee95f..684aeccf2d 100644 --- a/Sming/Libraries/CommandProcessing/src/CommandProcessing/Command.h +++ b/Sming/Libraries/CommandProcessing/src/CommandProcessing/Command.h @@ -12,13 +12,33 @@ #include #include +#include + +#ifdef CMDPROC_FLASHSTRINGS +/** + * @brief Command strings stored in single flash block for space-efficiency + * @tparam name Command name - the text a user types to invoke the command + * @tparam help Help message shown by CLI "help" command + * @tparam group The command group to which this command belongs + */ +#define CMDP_STRINGS(name, help, group) FS(name "\0" help "\0" group "\0") +#else +#define CMDP_STRINGS(name, help, group) F(name), F(help), F(group) +#endif namespace CommandProcessing { -/** @brief Command delegate class */ -class Command -{ -public: +// Order matches CMDP_STRINGS +enum class StringIndex { + name, + help, + group, +}; + +/** + * @brief Command definition stored by handler + */ +struct CommandDef { /** @brief Command delegate function * @param commandLine Command line entered by user at CLI, including command and parameters * @param commandOutput Pointer to the CLI print stream @@ -27,25 +47,84 @@ class Command */ using Callback = Delegate; - /** Instantiate a command delegate - * @param reqName Command name - the text a user types to invoke the command - * @param reqHelp Help message shown by CLI "help" command - * @param reqGroup The command group to which this command belongs - * @param reqFunction Delegate that should be invoked (triggered) when the command is entered by a user - */ - Command(String reqName, String reqHelp, String reqGroup, Callback reqFunction) - : name(reqName), description(reqHelp), group(reqGroup), callback(reqFunction) + operator bool() const { + return strings; } - Command() + bool operator==(const String& name) const { + return name == get(StringIndex::name); } - String name; ///< Command name - String description; ///< Command help - String group; ///< Command group - Callback callback; ///< Command Delegate (function that is called when command is invoked) + String get(StringIndex index) const + { +#ifdef CMDPROC_FLASHSTRINGS + return strings ? CStringArray(*strings)[unsigned(index)] : nullptr; +#else + return strings[unsigned(index)]; +#endif + } + +#ifdef CMDPROC_FLASHSTRINGS + const FlashString* strings{}; +#else + CStringArray strings; +#endif + + Callback callback; +}; + +/** @brief Command delegate class */ +class Command : private CommandDef +{ +public: + friend class Handler; + +#ifdef CMDPROC_FLASHSTRINGS + /** Instantiate a command delegate using block of flash strings + * @param strings Block of strings produced by `CMDP_STRINGS` macro + * @param callback Delegate that should be invoked (triggered) when the command is entered by a user + */ + Command(const FlashString& strings, Command::Callback callback) : Command({&strings, callback}) + { + } +#else + /** Instantiate a command delegate using set of wiring Strings + * @param name Command name - the text a user types to invoke the command + * @param help Help message shown by CLI "help" command + * @param group The command group to which this command belongs + * @param callback Delegate that should be invoked (triggered) when the command is entered by a user + */ + Command(const String& name, const String& help, const String& group, Callback callback) + : Command({nullptr, callback}) + { + strings.reserve(name.length() + help.length() + group.length() + 3); + strings += name; + strings += help; + strings += group; + } +#endif + + Command(const CommandDef& def) : CommandDef(def), name{*this}, help{*this}, group{*this} + { + } + + /** + * @brief Helper class for accessing individual strings + */ + template struct StringAccessor { + operator String() const + { + return def.get(index); + } + + const CommandDef& def; + }; + + const StringAccessor name; + const StringAccessor help; + const StringAccessor group; }; } // namespace CommandProcessing diff --git a/Sming/Libraries/CommandProcessing/src/CommandProcessing/Handler.cpp b/Sming/Libraries/CommandProcessing/src/CommandProcessing/Handler.cpp index 60ec754bfd..288e365f4e 100644 --- a/Sming/Libraries/CommandProcessing/src/CommandProcessing/Handler.cpp +++ b/Sming/Libraries/CommandProcessing/src/CommandProcessing/Handler.cpp @@ -13,32 +13,50 @@ namespace CommandProcessing { -Handler::Handler() : currentPrompt(F("Sming>")), currentWelcomeMessage(F("Welcome to the Sming CommandProcessing\r\n")) +String Handler::getCommandPrompt() const { + return prompt ?: F("Sming>"); +} + +String Handler::getCommandWelcomeMessage() const +{ + return welcomeMessage ?: F("Welcome to Sming Command Processing\r\n"); } size_t Handler::process(char recvChar) { auto& output = getOutputStream(); - if(recvChar == 27) // ESC -> delete current commandLine - { - commandBuf.clear(); + using Action = LineBufferBase::Action; + switch(commandBuf.processKey(recvChar)) { + case Action::clear: if(isVerbose()) { output.println(); output.print(getCommandPrompt()); } - } else if(recvChar == getCommandEOL()) { + break; + case Action::submit: + if(isVerbose()) { + output.println(); + } processCommandLine(String(commandBuf)); commandBuf.clear(); - } else if(recvChar == '\b' || recvChar == 0x7f) { - if(commandBuf.backspace()) { - output.print(_F("\b \b")); + if(isVerbose()) { + outputStream->print(getCommandPrompt()); } - } else { - if(commandBuf.addChar(recvChar) && localEcho) { + break; + case Action::backspace: + if(isVerbose()) { + output.print("\b \b"); + } + break; + case Action::echo: + if(isVerbose()) { output.print(recvChar); } + break; + case Action::none: + break; } return 1; } @@ -63,91 +81,90 @@ String Handler::processNow(const char* buffer, size_t size) void Handler::processCommandLine(const String& cmdString) { if(cmdString.length() == 0) { - outputStream->println(); - } else { - debug_d("Received full Command line, size = %u,cmd = %s", cmdString.length(), cmdString.c_str()); - String cmdCommand; - int cmdLen = cmdString.indexOf(' '); - if(cmdLen < 0) { - cmdCommand = cmdString; - } else { - cmdCommand = cmdString.substring(0, cmdLen); - } - - debug_d("CommandExecutor : executing command %s", cmdCommand.c_str()); - - Command cmdDelegate = getCommandDelegate(cmdCommand); + return; + } - if(!cmdDelegate.callback) { - outputStream->print(_F("Command not found, cmd = '")); - outputStream->print(cmdCommand); - outputStream->println('\''); - } else { - cmdDelegate.callback(cmdString, *outputStream); - } + debug_d("Received full Command line, size = %u, cmd = '%s'", cmdString.length(), cmdString.c_str()); + String name; + int cmdLen = cmdString.indexOf(' '); + if(cmdLen < 0) { + name = cmdString; + } else { + name = cmdString.substring(0, cmdLen); } - if(isVerbose()) { - outputStream->print(getCommandPrompt()); + debug_d("CommandExecutor : executing command '%s'", name.c_str()); + + Command cmd = getCommand(name); + if(!cmd) { + *outputStream << _F("Command '") << name << _F("' not found.") << endl; + } else if(cmd.callback) { + cmd.callback(cmdString, *outputStream); + } else { + *outputStream << _F("Command '") << name << _F("' has no callback.") << endl; } } void Handler::registerSystemCommands() { - String system = F("system"); - registerCommand({F("status"), F("Displays System Information"), system, {&Handler::processStatusCommand, this}}); - registerCommand({F("echo"), F("Displays command entered"), system, {&Handler::processEchoCommand, this}}); - registerCommand({F("help"), F("Displays all available commands"), system, {&Handler::processHelpCommand, this}}); - registerCommand({F("debugon"), F("Set Serial debug on"), system, {&Handler::processDebugOnCommand, this}}); - registerCommand({F("debugoff"), F("Set Serial debug off"), system, {&Handler::processDebugOffCommand, this}}); - registerCommand({F("command"), - F("Use verbose/silent/prompt as command options"), - system, + registerCommand( + {CMDP_STRINGS("status", "Displays System Information", "system"), {&Handler::processStatusCommand, this}}); + registerCommand({CMDP_STRINGS("echo", "Displays command entered", "system"), {&Handler::processEchoCommand, this}}); + registerCommand( + {CMDP_STRINGS("help", "Displays all available commands", "system"), {&Handler::processHelpCommand, this}}); + registerCommand( + {CMDP_STRINGS("debugon", "Set Serial debug on", "system"), {&Handler::processDebugOnCommand, this}}); + registerCommand( + {CMDP_STRINGS("debugoff", "Set Serial debug off", "system"), {&Handler::processDebugOffCommand, this}}); + registerCommand({CMDP_STRINGS("command", "Use verbose/silent/prompt as command options", "system"), {&Handler::processCommandOptions, this}}); } -Command Handler::getCommandDelegate(const String& commandString) +Command Handler::getCommand(const String& name) const { - if(registeredCommands.contains(commandString)) { - debug_d("Returning Delegate for %s \r\n", commandString.c_str()); - return registeredCommands[commandString]; - } else { - debug_d("Command %s not recognized, returning NULL\r\n", commandString.c_str()); - return Command("", "", "", nullptr); + int i = registeredCommands.indexOf(name); + if(i >= 0) { + debug_d("[CH] Returning Delegate for '%s'", name.c_str()); + return registeredCommands[i]; } + + debug_d("[CH] Command %s not recognized", name.c_str()); + return CommandDef{}; } -bool Handler::registerCommand(Command reqDelegate) +bool Handler::registerCommand(const Command& command) { - if(registeredCommands.contains(reqDelegate.name)) { + String name = command.name; + int i = registeredCommands.indexOf(name); + if(i >= 0) { // Command already registered, don't allow duplicates - debug_d("Commandhandler duplicate command %s", reqDelegate.name.c_str()); + debug_d("[CH] Duplicate command %s", name.c_str()); return false; - } else { - registeredCommands[reqDelegate.name] = reqDelegate; - debug_d("Commandhandlercommand %s registered", reqDelegate.name.c_str()); - return true; } + + registeredCommands.add(command); + debug_d("[CH] Command '%s' registered", name.c_str()); + return true; } -bool Handler::unregisterCommand(Command reqDelegate) +bool Handler::unregisterCommand(const Command& command) { - if(!registeredCommands.contains(reqDelegate.name)) { + int i = registeredCommands.indexOf(command.name); + if(i < 0) { // Command not registered, cannot remove return false; - } else { - registeredCommands.remove(reqDelegate.name); - // (*registeredCommands)[reqDelegate.commandName] = reqDelegate; - return true; } + + registeredCommands.remove(i); + return true; } void Handler::processHelpCommand(String commandLine, ReadWriteStream& outputStream) { debug_d("HelpCommand entered"); outputStream.println(_F("Commands available are :")); - for(auto cmd : registeredCommands) { - outputStream << cmd->name << " | " << cmd->group << " | " << cmd->description << endl; + for(Command cmd : registeredCommands) { + outputStream << cmd.name << " | " << cmd.group << " | " << cmd.help << endl; } } @@ -186,6 +203,9 @@ void Handler::processCommandOptions(String commandLine, ReadWriteStream& outputS bool printUsage = false; switch(numToken) { + case 1: + printUsage = true; + break; case 2: if(commandToken[1] == _F("help")) { printUsage = true; diff --git a/Sming/Libraries/CommandProcessing/src/CommandProcessing/Handler.h b/Sming/Libraries/CommandProcessing/src/CommandProcessing/Handler.h index d75e2f6b55..f8742d89ed 100644 --- a/Sming/Libraries/CommandProcessing/src/CommandProcessing/Handler.h +++ b/Sming/Libraries/CommandProcessing/src/CommandProcessing/Handler.h @@ -13,20 +13,16 @@ #pragma once -#include +#include #include #include #include -#include #include "Command.h" namespace CommandProcessing { constexpr size_t MAX_COMMANDSIZE = 64; -/** @brief Verbose mode -*/ - /** @brief Command handler class */ class Handler { @@ -34,7 +30,9 @@ class Handler /** * @brief Instantiate a CommandHandler */ - Handler(); + Handler() + { + } Handler(ReadWriteStream* stream, bool owned = true) : outputStream(stream), ownedStream(owned) { @@ -95,22 +93,29 @@ class Handler return retval; } + /** + * @brief Process command input and return response text + * @param buffer Command input + * @param size Number of characters to process + * @retval String Response text + * @note Do not use this method if `setOutputStream` has been called + */ String processNow(const char* buffer, size_t size); // Command registration/de-registration methods /** @brief Add a new command to the command handler - * @param reqDelegate Command delegate to register + * @param command Command to register * @retval bool True on success * @note If command already exists, it will not be replaced and function will fail. Call unregisterCommand first if you want to replace a command. */ - bool registerCommand(Command reqDelegate); + bool registerCommand(const Command& command); /** @brief Remove a command from the command handler - * @brief reqDelegate Delegate to remove from command handler + * @param command Item to remove from command handler */ - bool unregisterCommand(Command reqDelegate); + bool unregisterCommand(const Command& command); /** @brief Register default system commands * @note Adds the following system commands to the command handler @@ -123,14 +128,14 @@ class Handler */ void registerSystemCommands(); - /** @brief Get the command delegate for a command - * @param commandString Command to query - * @retval Command The command delegate matching the command + /** @brief Find command object + * @param name Command to query + * @retval Command The command object matching the command */ - Command getCommandDelegate(const String& commandString); + Command getCommand(const String& name) const; /** @brief Get the verbose mode - * @retval VerboseMode Verbose mode + * @retval bool Verbose mode */ bool isVerbose() const { @@ -138,7 +143,7 @@ class Handler } /** @brief Set the verbose mode - * @param reqVerboseMode Verbose mode to set + * @param mode Verbose mode to set */ void setVerbose(bool mode) { @@ -150,72 +155,63 @@ class Handler * @note This is what is shown on the command line before user input * Default is Sming> */ - const String& getCommandPrompt() const - { - return currentPrompt; - } + String getCommandPrompt() const; /** @brief Set the command line prompt - * @param reqPrompt The command line prompt + * @param prompt The command line prompt, nullptr to reset to default * @note This is what is shown on the command line before user input * Default is Sming> */ - void setCommandPrompt(const String& reqPrompt) + void setCommandPrompt(const String& prompt) { - currentPrompt = reqPrompt; + this->prompt = prompt; } /** @brief Get the end of line character * @retval char The EOL character * @note Only supports one EOL, unlike Windows + * @deprecated Not required */ - char getCommandEOL() const + char getCommandEOL() const SMING_DEPRECATED { - return currentEOL; + return '\n'; } /** @brief Set the end of line character - * @param reqEOL The EOL character + * @param eol The EOL character * @note Only supports one EOL, unlike Windows + * @deprecated Not required */ - void setCommandEOL(char reqEOL) + void setCommandEOL(char eol) SMING_DEPRECATED { - currentEOL = reqEOL; + (void)eol; } /** @brief Get the welcome message - * @retval String The welcome message that is shown when clients connect - * @note Only if verbose mode is enabled + * @retval String The welcome message that should be shown when clients connect + * @note For use by application */ - const String& getCommandWelcomeMessage() const - { - return currentWelcomeMessage; - } + String getCommandWelcomeMessage() const; /** @brief Set the welcome message - * @param reqWelcomeMessage The welcome message that is shown when clients connect - * @note Only if verbose mode is enabled + * @param message The welcome message that should be shown when clients connect + * @note For use by application + * Pass nullptr to revert to default message. */ - void setCommandWelcomeMessage(const String& reqWelcomeMessage) + void setCommandWelcomeMessage(const String& message) { - currentWelcomeMessage = reqWelcomeMessage; + welcomeMessage = message; } private: - HashMap registeredCommands; - String currentPrompt; -#ifdef ARCH_HOST - char currentEOL{'\n'}; -#else - char currentEOL{'\r'}; -#endif + Vector registeredCommands; + String prompt; bool verboseMode{false}; - bool localEcho{true}; - String currentWelcomeMessage; + String welcomeMessage; ReadWriteStream* outputStream{nullptr}; bool ownedStream = true; - LineBuffer commandBuf; + LineBuffer commandBuf; void processHelpCommand(String commandLine, ReadWriteStream& outputStream); void processStatusCommand(String commandLine, ReadWriteStream& outputStream); diff --git a/samples/HttpServer_WebSockets/app/application.cpp b/samples/HttpServer_WebSockets/app/application.cpp index b0cc2d3500..cd98bd93fc 100644 --- a/samples/HttpServer_WebSockets/app/application.cpp +++ b/samples/HttpServer_WebSockets/app/application.cpp @@ -166,7 +166,7 @@ void init() #if ENABLE_CMD_HANDLER commandHandler.registerSystemCommands(); commandHandler.registerCommand( - CommandProcessing::Command("shutdown", "Shutdown Server Command", "Application", processShutdownCommand)); + {CMDP_STRINGS("shutdown", "Shutdown Server Command", "Application"), processShutdownCommand}); #endif Serial.begin(SERIAL_BAUD_RATE); // 115200 by default From 0e4689164a7a85bfceb024f6b07133a1c50e051a Mon Sep 17 00:00:00 2001 From: Mike Date: Fri, 29 Mar 2024 06:30:27 +0000 Subject: [PATCH 038/128] Wrong version of linux-modules-extra being installed (#2750) --- Tools/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tools/install.sh b/Tools/install.sh index 555edbc7e2..4eb9635d28 100755 --- a/Tools/install.sh +++ b/Tools/install.sh @@ -104,7 +104,7 @@ if [ -n "$APPVEYOR" ] || [ -n "$GITHUB_ACTION" ]; then g++-multilib \ python3-setuptools \ ninja-build \ - linux-modules-extra-azure \ + linux-modules-extra-$(uname -r) \ exfatprogs \ $EXTRA_PACKAGES From 7f3eba5899f7cb43f356155f16aa7fbf2370d142 Mon Sep 17 00:00:00 2001 From: Mike Date: Wed, 3 Apr 2024 06:03:49 +0100 Subject: [PATCH 039/128] Free some esp8266 RAM (#2752) This PR frees some esp8266 RAM consumed by the `.rodata` segment. This can be examined with `objdump -s -j .rodata out/Esp8266/debug/build/app_0.out` **Store `http-parser` method strings in flash** Also remove unused `http_status_str()` function, consumes RAM even though it's never called. Saves 1100 bytes RAM. **More http-parser strings into flash** No need for these to be in RAM. **Update `http-parser` lib to final version** **Put WS parser error strings in flash** **Remove `EspDigitalPin`** Only MUX addresses are required, and that only needs 1 byte. Saves up to 196 byte RAM. **Optimise rapidxml lookup tables** Saves 2020 bytes RAM **Add python script to scan/compare CI build logs** Savings: ``` samples/Basic_Blink log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ---- ---- ---------- -------------- -------- --------- log-0e46891-develop.txt 58 712 1336 7265 55932 2106 79814 25521 log-9c72d313-flashstrings.txt 58 536 1336 7269 55932 1930 79990 25517 Difference -176 +4 -176 +176 -4 samples/Basic_Capsense log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ----- ----- ---------- -------------- -------- --------- log-0e46891-develop.txt 1316 916 25416 25570 209856 27648 54272 7216 log-9c72d313-flashstrings.txt 1316 740 25416 25574 209856 27472 54448 7212 Difference -176 +4 -176 +176 -4 samples/Basic_HwPWM log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ----- ----- ---------- -------------- -------- --------- log-0e46891-develop.txt 1329 924 25760 26122 219520 28013 53907 6664 log-9c72d313-flashstrings.txt 1329 764 25760 26122 219636 27853 54067 6664 Difference -160 +116 -160 +160 samples/Basic_IFS log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ----- ----- ---------- -------------- -------- --------- log-0e46891-develop.txt 1952 2616 25984 26453 399202 30552 51368 6333 log-9c72d313-flashstrings.txt 1952 1248 25976 26453 399834 29176 52744 6333 Difference -1368 -8 +632 -1376 +1376 samples/Basic_Interrupts log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ---- ---- ---------- -------------- -------- --------- log-0e46891-develop.txt 64 720 1712 7977 62256 2496 79424 24809 log-9c72d313-flashstrings.txt 64 544 1712 7981 62256 2320 79600 24805 Difference -176 +4 -176 +176 -4 samples/Basic_NFC log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ---- ---- ---------- -------------- -------- --------- log-0e46891-develop.txt 66 728 1536 8077 66232 2330 79590 24709 log-9c72d313-flashstrings.txt 66 552 1536 8081 66232 2154 79766 24705 Difference -176 +4 -176 +176 -4 samples/Basic_Neopixel log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ----- ----- ---------- -------------- -------- --------- log-0e46891-develop.txt 1320 960 25696 26838 222488 27976 53944 5948 log-9c72d313-flashstrings.txt 1320 784 25696 26842 222488 27800 54120 5944 Difference -176 +4 -176 +176 -4 samples/Basic_Ota log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ----- ----- ---------- -------------- -------- --------- log-0e46891-develop.txt 1608 2408 25584 26110 314125 29600 52320 6676 log-9c72d313-flashstrings.txt 1608 1016 25584 26110 314817 28208 53712 6676 Difference -1392 +692 -1392 +1392 samples/Basic_Servo log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ---- ---- ---------- -------------- -------- --------- log-0e46891-develop.txt 64 812 1888 8457 65064 2764 79156 24329 log-9c72d313-flashstrings.txt 64 636 1888 8461 65064 2588 79332 24325 Difference -176 +4 -176 +176 -4 samples/Basic_Ssl log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ----- ----- ---------- -------------- -------- --------- log-0e46891-develop.txt 1948 3020 25856 26122 332594 30824 51096 6664 log-9c72d313-flashstrings.txt 1948 1632 25856 26122 333286 29436 52484 6664 Difference -1388 +692 -1388 +1388 samples/Basic_WebSkeletonApp log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ----- ----- ---------- -------------- -------- --------- log-0e46891-develop.txt 1644 2468 25816 26106 331133 29928 51992 6680 log-9c72d313-flashstrings.txt 1644 1084 25808 26106 331765 28536 53384 6680 Difference -1384 -8 +632 -1392 +1392 samples/Basic_WebSkeletonApp_LTS log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ----- ----- ---------- -------------- -------- --------- log-0e46891-develop.txt 1644 2456 25856 26106 326425 29956 51964 6680 log-9c72d313-flashstrings.txt 1644 1072 25848 26106 327049 28564 53356 6680 Difference -1384 -8 +624 -1392 +1392 samples/CanBus log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ----- ----- ---------- -------------- -------- --------- log-0e46891-develop.txt 1336 1000 25488 26286 217156 27824 54096 6500 log-9c72d313-flashstrings.txt 1336 824 25488 26290 217156 27648 54272 6496 Difference -176 +4 -176 +176 -4 samples/DFPlayerMini log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ---- ---- ---------- -------------- -------- --------- log-0e46891-develop.txt 65 732 1544 8353 62536 2341 79579 24433 log-9c72d313-flashstrings.txt 65 556 1544 8357 62536 2165 79755 24429 Difference -176 +4 -176 +176 -4 samples/Display_TM1637 log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ---- ---- ---------- -------------- -------- --------- log-0e46891-develop.txt 57 748 1288 7089 56900 2093 79827 25697 log-9c72d313-flashstrings.txt 57 572 1288 7093 56900 1917 80003 25693 Difference -176 +4 -176 +176 -4 samples/DnsCaptivePortal log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ----- ----- ---------- -------------- -------- --------- log-0e46891-develop.txt 1572 2232 25664 25966 271856 29468 52452 6820 log-9c72d313-flashstrings.txt 1572 848 25656 25966 272488 28076 53844 6820 Difference -1384 -8 +632 -1392 +1392 samples/HttpClient log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ----- ----- ---------- -------------- -------- --------- log-0e46891-develop.txt 1584 2564 25536 26039 407804 29684 52236 6747 log-9c72d313-flashstrings.txt 1584 1172 25536 26039 408504 28292 53628 6747 Difference -1392 +700 -1392 +1392 samples/HttpClient_Instapush log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ----- ----- ---------- -------------- -------- --------- log-0e46891-develop.txt 1644 2388 25632 26014 268429 29664 52256 6772 log-9c72d313-flashstrings.txt 1644 996 25632 26014 269121 28272 53648 6772 Difference -1392 +692 -1392 +1392 samples/HttpClient_ThingSpeak log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ----- ----- ---------- -------------- -------- --------- log-0e46891-develop.txt 1576 2268 25592 26014 265477 29436 52484 6772 log-9c72d313-flashstrings.txt 1576 876 25592 26014 266169 28044 53876 6772 Difference -1392 +692 -1392 +1392 samples/HttpServer_AJAX log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ----- ----- ---------- -------------- -------- --------- log-0e46891-develop.txt 1652 2444 25920 25986 332858 30016 51904 6800 log-9c72d313-flashstrings.txt 1652 1060 25928 25986 333490 28640 53280 6800 Difference -1384 +8 +632 -1376 +1376 samples/HttpServer_Bootstrap log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ----- ----- ---------- -------------- -------- --------- log-0e46891-develop.txt 1576 2620 25824 26334 327765 30020 51900 6452 log-9c72d313-flashstrings.txt 1576 1060 25832 26338 328457 28468 53452 6448 Difference -1560 +8 +4 +692 -1552 +1552 -4 samples/HttpServer_ConfigNetwork log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ----- ----- ---------- -------------- -------- --------- log-0e46891-develop.txt 1644 2664 26016 26106 341734 30324 51596 6680 log-9c72d313-flashstrings.txt 1644 1288 26016 26106 342366 28948 52972 6680 Difference -1376 +632 -1376 +1376 samples/HttpServer_FirmwareUpload log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ----- ----- ---------- -------------- -------- --------- log-0e46891-develop.txt 1600 2288 25592 26106 348349 29480 52440 6680 log-9c72d313-flashstrings.txt 1600 920 25600 26106 348973 28120 53800 6680 Difference -1368 +8 +624 -1360 +1360 samples/HttpServer_Plugins log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ----- ----- ---------- -------------- -------- --------- log-0e46891-develop.txt 1572 2392 25504 25938 274500 29468 52452 6848 log-9c72d313-flashstrings.txt 1572 1008 25496 25938 275132 28076 53844 6848 Difference -1384 -8 +632 -1392 +1392 samples/HttpServer_WebSockets log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ----- ----- ---------- -------------- -------- --------- log-0e46891-develop.txt 1572 2504 25920 26222 330085 29996 51924 6564 log-9c72d313-flashstrings.txt 1572 988 25912 26222 330861 28472 53448 6564 Difference -1516 -8 +776 -1524 +1524 samples/Humidity_AM2321 log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ---- ---- ---------- -------------- -------- --------- log-0e46891-develop.txt 64 732 1552 7949 65937 2348 79572 24837 log-9c72d313-flashstrings.txt 64 556 1552 7953 65937 2172 79748 24833 Difference -176 +4 -176 +176 -4 samples/Humidity_BME280 log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ---- ---- ---------- -------------- -------- --------- log-0e46891-develop.txt 308 796 1624 8073 74220 2728 79192 24713 log-9c72d313-flashstrings.txt 308 620 1624 8077 74220 2552 79368 24709 Difference -176 +4 -176 +176 -4 samples/Humidity_DHT22 log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ---- ---- ---------- -------------- -------- --------- log-0e46891-develop.txt 308 792 1512 7997 74528 2612 79308 24789 log-9c72d313-flashstrings.txt 308 616 1512 8001 74528 2436 79484 24785 Difference -176 +4 -176 +176 -4 samples/IR_lib log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ---- ---- ---------- -------------- -------- --------- log-0e46891-develop.txt 64 2552 1824 8951 107664 4440 77480 23835 log-9c72d313-flashstrings.txt 64 2376 1824 8955 107664 4264 77656 23831 Difference -176 +4 -176 +176 -4 samples/LED_WS2812 log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ---- ---- ---------- -------------- -------- --------- log-0e46891-develop.txt 68 724 1272 7041 55876 2064 79856 25745 log-9c72d313-flashstrings.txt 68 548 1272 7045 55876 1888 80032 25741 Difference -176 +4 -176 +176 -4 samples/LiveDebug log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ---- ----- ---------- -------------- -------- --------- log-0e46891-develop.txt 316 2672 3000 10101 97068 5988 75932 22685 log-9c72d313-flashstrings.txt 316 2496 3000 10105 97068 5812 76108 22681 Difference -176 +4 -176 +176 -4 samples/MeteoControl log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ----- ----- ---------- -------------- -------- --------- log-0e46891-develop.txt 2080 3280 26248 27049 361489 31608 50312 5737 log-9c72d313-flashstrings.txt 2080 1716 26256 27053 362189 30052 51868 5733 Difference -1564 +8 +4 +700 -1556 +1556 -4 samples/Nextion_Button log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ---- ---- ---------- -------------- -------- --------- log-0e46891-develop.txt 76 796 1504 7949 62336 2376 79544 24837 log-9c72d313-flashstrings.txt 76 620 1504 7953 62336 2200 79720 24833 Difference -176 +4 -176 +176 -4 samples/Radio_RCSwitch log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ---- ---- ---------- -------------- -------- --------- log-0e46891-develop.txt 68 812 2184 8913 63160 3064 78856 23873 log-9c72d313-flashstrings.txt 68 636 2184 8917 63160 2888 79032 23869 Difference -176 +4 -176 +176 -4 samples/Radio_nRF24L01 log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ---- ---- ---------- -------------- -------- --------- log-0e46891-develop.txt 64 732 1488 7949 67900 2284 79636 24837 log-9c72d313-flashstrings.txt 64 556 1488 7953 67900 2108 79812 24833 Difference -176 +4 -176 +176 -4 samples/Radio_si4432 log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ---- ---- ---------- -------------- -------- --------- log-0e46891-develop.txt 100 760 1448 7997 67468 2308 79612 24789 log-9c72d313-flashstrings.txt 100 584 1448 8001 67468 2132 79788 24785 Difference -176 +4 -176 +176 -4 samples/SDCard log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ---- ---- ---------- -------------- -------- --------- log-0e46891-develop.txt 65 792 1424 7725 76596 2281 79639 25061 log-9c72d313-flashstrings.txt 65 616 1424 7729 76596 2105 79815 25057 Difference -176 +4 -176 +176 -4 samples/ScreenLCD_5110 log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ---- ---- ---------- -------------- -------- --------- log-0e46891-develop.txt 568 732 1432 7725 67512 2732 79188 25061 log-9c72d313-flashstrings.txt 568 556 1432 7729 67512 2556 79364 25057 Difference -176 +4 -176 +176 -4 samples/ScreenOLED_SSD1306 log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ---- ---- ---------- -------------- -------- --------- log-0e46891-develop.txt 64 736 1640 8277 74684 2440 79480 24509 log-9c72d313-flashstrings.txt 64 560 1640 8281 74684 2264 79656 24505 Difference -176 +4 -176 +176 -4 samples/ScreenTFT_ILI9163C log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ---- ---- ---------- -------------- -------- --------- log-0e46891-develop.txt 64 732 1472 7725 69540 2268 79652 25061 log-9c72d313-flashstrings.txt 64 556 1472 7729 69540 2092 79828 25057 Difference -176 +4 -176 +176 -4 samples/ScreenTFT_ILI9340-ILI9341 log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ---- ---- ---------- -------------- -------- --------- log-0e46891-develop.txt 88 788 1640 8077 112385 2516 79404 24709 log-9c72d313-flashstrings.txt 88 612 1640 8081 112385 2340 79580 24705 Difference -176 +4 -176 +176 -4 samples/ScreenTFT_ST7735 log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ---- ---- ---------- -------------- -------- --------- log-0e46891-develop.txt 68 1200 1568 8293 120205 2836 79084 24493 log-9c72d313-flashstrings.txt 68 1024 1568 8297 120205 2660 79260 24489 Difference -176 +4 -176 +176 -4 samples/Temperature_DS1820 log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ---- ---- ---------- -------------- -------- --------- log-0e46891-develop.txt 64 736 1632 8025 67700 2432 79488 24761 log-9c72d313-flashstrings.txt 64 560 1632 8029 67700 2256 79664 24757 Difference -176 +4 -176 +176 -4 samples/UdpServer_mDNS log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ----- ----- ---------- -------------- -------- --------- log-0e46891-develop.txt 1580 2332 25824 25938 326909 29736 52184 6848 log-9c72d313-flashstrings.txt 1580 952 25832 25938 327533 28364 53556 6848 Difference -1380 +8 +624 -1372 +1372 samples/Ultrasonic_HCSR04 log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ---- ---- ---------- -------------- -------- --------- log-0e46891-develop.txt 64 720 1472 7949 62208 2256 79664 24837 log-9c72d313-flashstrings.txt 64 544 1472 7953 62208 2080 79840 24833 Difference -176 +4 -176 +176 -4 samples/WebcamServer log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ----- ----- ---------- -------------- -------- --------- log-0e46891-develop.txt 1936 2324 25616 25966 316965 29876 52044 6820 log-9c72d313-flashstrings.txt 1936 940 25608 25966 317589 28484 53436 6820 Difference -1384 -8 +624 -1392 +1392 samples/Websocket_Client log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ----- ----- ---------- -------------- -------- --------- log-0e46891-develop.txt 1572 2448 25912 26126 273761 29932 51988 6660 log-9c72d313-flashstrings.txt 1572 924 25912 26126 274597 28408 53512 6660 Difference -1524 +836 -1524 +1524 samples/Basic_Blink log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ---- ---- ---------- -------------- -------- --------- log-0e46891-develop.txt 58 596 1232 7685 55964 1886 80034 25101 log-9c72d313-flashstrings.txt 58 420 1232 7689 55964 1710 80210 25097 Difference -176 +4 -176 +176 -4 samples/HttpServer_ConfigNetwork log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ----- ----- ---------- -------------- -------- --------- log-0e46891-develop.txt 1640 4044 26360 27558 345208 32044 49876 5228 log-9c72d313-flashstrings.txt 1640 2672 26360 27558 345840 30672 51248 5228 Difference -1372 +632 -1372 +1372 Components/Hosted/samples/serial log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ---- ---- ---------- -------------- -------- --------- log-0e46891-develop.txt 64 728 1480 7773 80500 2272 79648 25013 log-9c72d313-flashstrings.txt 64 552 1480 7777 80500 2096 79824 25009 Difference -176 +4 -176 +176 -4 Components/Hosted/samples/tcp log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ----- ----- ---------- -------------- -------- --------- log-0e46891-develop.txt 1324 1052 25344 26146 232324 27720 54200 6640 log-9c72d313-flashstrings.txt 1324 876 25344 26150 232324 27544 54376 6636 Difference -176 +4 -176 +176 -4 Libraries/AnimatedGIF/samples/Basic_AnimatedGIF log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ---- ----- ---------- -------------- -------- --------- log-0e46891-develop.txt 64 936 2096 11069 106647 3096 78824 21717 log-9c72d313-flashstrings.txt 64 760 2096 11069 106647 2920 79000 21717 Difference -176 -176 +176 Libraries/CS5460/samples/generic log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ----- ----- ---------- -------------- -------- --------- log-0e46891-develop.txt 1320 1020 25448 26238 214396 27788 54132 6548 log-9c72d313-flashstrings.txt 1320 844 25448 26242 214396 27612 54308 6544 Difference -176 +4 -176 +176 -4 Libraries/CommandProcessing/samples/Arducam log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ----- ----- ---------- -------------- -------- --------- log-0e46891-develop.txt 1572 3104 26160 26194 338417 30836 51084 6592 log-9c72d313-flashstrings.txt 1572 1548 26160 26198 339049 29280 52640 6588 Difference -1556 +4 +632 -1556 +1556 -4 Libraries/DIAL/samples/DIAL_Client log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ----- ----- ---------- -------------- -------- --------- log-0e46891-develop.txt 1576 4760 25824 26138 320971 32160 49760 6648 log-9c72d313-flashstrings.txt 1576 1072 25816 26138 322119 28464 53456 6648 Difference -3688 -8 +1148 -3696 +3696 Libraries/Graphics/samples/Advanced_Animation log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ---- ----- ---------- -------------- -------- --------- log-0e46891-develop.txt 64 936 2936 11125 93719 3936 77984 21661 log-9c72d313-flashstrings.txt 64 760 2936 11125 93719 3760 78160 21661 Difference -176 -176 +176 Libraries/Graphics/samples/Basic_Animation log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ---- ----- ---------- -------------- -------- --------- log-0e46891-develop.txt 64 1056 2984 11185 105707 4104 77816 21601 log-9c72d313-flashstrings.txt 64 880 2984 11185 105707 3928 77992 21601 Difference -176 -176 +176 Libraries/Graphics/samples/Basic_Graphics log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ---- ----- ---------- -------------- -------- --------- log-0e46891-develop.txt 1556 4400 2464 11461 301788 8420 73500 21325 log-9c72d313-flashstrings.txt 1556 1920 2464 11461 302248 5940 75980 21325 Difference -2480 +460 -2480 +2480 Libraries/Graphics/samples/Basic_Touch log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ---- ----- ---------- -------------- -------- --------- log-0e46891-develop.txt 72 1168 2304 11185 141239 3544 78376 21601 log-9c72d313-flashstrings.txt 72 992 2304 11185 141239 3368 78552 21601 Difference -176 -176 +176 Libraries/Graphics/samples/Bresenham log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ---- ----- ---------- -------------- -------- --------- log-0e46891-develop.txt 308 1668 2232 11193 176901 4208 77712 21593 log-9c72d313-flashstrings.txt 308 1492 2232 11193 176901 4032 77888 21593 Difference -176 -176 +176 Libraries/Graphics/samples/Color_Test log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ---- ----- ---------- -------------- -------- --------- log-0e46891-develop.txt 64 1080 2160 11185 104647 3304 78616 21601 log-9c72d313-flashstrings.txt 64 904 2160 11185 104647 3128 78792 21601 Difference -176 -176 +176 Libraries/Graphics/samples/Custom_Object log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ---- ----- ---------- -------------- -------- --------- log-0e46891-develop.txt 68 1156 2240 11241 108263 3464 78456 21545 log-9c72d313-flashstrings.txt 68 980 2240 11241 108263 3288 78632 21545 Difference -176 -176 +176 Libraries/HueEmulator/samples/Basic_Alexa log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ----- ----- ---------- -------------- -------- --------- log-0e46891-develop.txt 1644 2820 26208 26574 333055 30672 51248 6212 log-9c72d313-flashstrings.txt 1644 1256 26208 26578 333855 29108 52812 6208 Difference -1564 +4 +800 -1564 +1564 -4 Libraries/IOControl/samples/Basic_RS485 log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ---- ---- ---------- -------------- -------- --------- log-0e46891-develop.txt 152 924 1648 8217 100833 2724 79196 24569 log-9c72d313-flashstrings.txt 152 748 1648 8221 100833 2548 79372 24565 Difference -176 +4 -176 +176 -4 Libraries/ModbusMaster/samples/generic log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ----- ----- ---------- -------------- -------- --------- log-0e46891-develop.txt 1320 1012 25840 26238 214772 28172 53748 6548 log-9c72d313-flashstrings.txt 1320 836 25840 26242 214772 27996 53924 6544 Difference -176 +4 -176 +176 -4 Libraries/MultipartParser/samples/Conditional_Upload log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ----- ----- ---------- -------------- -------- --------- log-0e46891-develop.txt 1600 2348 25600 25966 314529 29548 52372 6820 log-9c72d313-flashstrings.txt 1600 972 25600 25966 315153 28172 53748 6820 Difference -1376 +624 -1376 +1376 Libraries/RingTone/samples/RingTonePlayer log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ----- ----- ---------- -------------- -------- --------- log-0e46891-develop.txt 2016 2872 26200 27361 355532 31088 50832 5425 log-9c72d313-flashstrings.txt 2016 1356 26192 27361 356308 29564 52356 5425 Difference -1516 -8 +776 -1524 +1524 Libraries/UPnP/samples/Basic_ControlPoint log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ----- ----- ---------- -------------- -------- --------- log-0e46891-develop.txt 1576 4852 25864 26194 409155 32292 49628 6592 log-9c72d313-flashstrings.txt 1576 1164 25856 26194 410323 28596 53324 6592 Difference -3688 -8 +1168 -3696 +3696 Libraries/UPnP/samples/Basic_UPnP log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ----- ----- ---------- -------------- -------- --------- log-0e46891-develop.txt 1940 5260 26192 26430 361963 33392 48528 6356 log-9c72d313-flashstrings.txt 1940 1396 26200 26434 363127 29536 52384 6352 Difference -3864 +8 +4 +1164 -3856 +3856 -4 Libraries/jerryscript/samples/Advanced_Jsvm log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ----- ----- ---------- -------------- -------- --------- log-0e46891-develop.txt 1964 5168 26848 26155 382821 33980 47940 6631 log-9c72d313-flashstrings.txt 1964 3800 26856 26155 383453 32620 49300 6631 Difference -1368 +8 +632 -1360 +1360 Libraries/modbusino/samples/generic log data rodata bss text irom0_text Total Used RAM Free RAM Free IRam ----------------------------- ---- ------ ----- ----- ---------- -------------- -------- --------- log-0e46891-develop.txt 1320 1020 25736 26238 214080 28076 53844 6548 log-9c72d313-flashstrings.txt 1320 844 25736 26242 214080 27900 54020 6544 Difference -176 +4 -176 +176 -4 ``` --- Sming/Arch/Esp8266/Core/Digital.cpp | 63 ++++-- Sming/Arch/Esp8266/Core/ESP8266EX.cpp | 48 ---- Sming/Arch/Esp8266/Core/ESP8266EX.h | 38 ---- Sming/Arch/Esp8266/Core/HardwarePWM.cpp | 24 +- Sming/Components/.patches/http-parser.patch | 207 +++++++++++++++--- Sming/Components/.patches/ws_parser.patch | 18 +- .../Network/src/Network/Http/HttpCommon.h | 3 +- Sming/Components/http-parser | 2 +- Sming/Libraries/RapidXML | 2 +- Tools/ci/scanlog.py | 193 ++++++++++++++++ 10 files changed, 449 insertions(+), 149 deletions(-) delete mode 100644 Sming/Arch/Esp8266/Core/ESP8266EX.cpp delete mode 100644 Sming/Arch/Esp8266/Core/ESP8266EX.h create mode 100644 Tools/ci/scanlog.py diff --git a/Sming/Arch/Esp8266/Core/Digital.cpp b/Sming/Arch/Esp8266/Core/Digital.cpp index d8bb1e6021..7a7af26d1e 100644 --- a/Sming/Arch/Esp8266/Core/Digital.cpp +++ b/Sming/Arch/Esp8266/Core/Digital.cpp @@ -9,19 +9,42 @@ ****/ #include -#include "ESP8266EX.h" #include #include #include #include +#define TOTAL_PINS 16 + +#define PINMUX_OFFSET(addr) uint8_t((addr)-PERIPHS_IO_MUX) + +// Used for pullup/noPullup +extern const uint8_t esp8266_pinmuxOffset[] = { + PINMUX_OFFSET(PERIPHS_IO_MUX_GPIO0_U), // 0 FLASH + PINMUX_OFFSET(PERIPHS_IO_MUX_U0TXD_U), // 1 TXD0 + PINMUX_OFFSET(PERIPHS_IO_MUX_GPIO2_U), // 2 TXD1 + PINMUX_OFFSET(PERIPHS_IO_MUX_U0RXD_U), // 3 RXD0 + PINMUX_OFFSET(PERIPHS_IO_MUX_GPIO4_U), // 4 + PINMUX_OFFSET(PERIPHS_IO_MUX_GPIO5_U), // 5 + PINMUX_OFFSET(PERIPHS_IO_MUX_SD_CLK_U), // 6 SD_CLK_U + PINMUX_OFFSET(PERIPHS_IO_MUX_SD_DATA0_U), // 7 SD_DATA0_U + PINMUX_OFFSET(PERIPHS_IO_MUX_SD_DATA1_U), // 8 SD_DATA1_U + PINMUX_OFFSET(PERIPHS_IO_MUX_SD_DATA2_U), // 9 + PINMUX_OFFSET(PERIPHS_IO_MUX_SD_DATA3_U), // 10 + PINMUX_OFFSET(PERIPHS_IO_MUX_SD_CMD_U), // 11 SD_CMD_U + PINMUX_OFFSET(PERIPHS_IO_MUX_MTDI_U), // 12 HSPIQ + PINMUX_OFFSET(PERIPHS_IO_MUX_MTCK_U), // 13 HSPID + PINMUX_OFFSET(PERIPHS_IO_MUX_MTMS_U), // 14 HSPICLK + PINMUX_OFFSET(PERIPHS_IO_MUX_MTDO_U), // 15 HSPICS +}; + // Prototype declared in esp8266-peri.h -const uint8_t esp8266_gpioToFn[16] = {0x34, 0x18, 0x38, 0x14, 0x3C, 0x40, 0x1C, 0x20, - 0x24, 0x28, 0x2C, 0x30, 0x04, 0x08, 0x0C, 0x10}; +const uint8_t esp8266_gpioToFn[TOTAL_PINS] = {0x34, 0x18, 0x38, 0x14, 0x3C, 0x40, 0x1C, 0x20, + 0x24, 0x28, 0x2C, 0x30, 0x04, 0x08, 0x0C, 0x10}; void pinMode(uint16_t pin, uint8_t mode) { - if(pin < 16) { + if(pin < TOTAL_PINS) { if(mode == SPECIAL) { GPC(pin) = (GPC(pin) & (0xF << GPCI)); //SOURCE(GPIO) | DRIVER(NORMAL) | INT_TYPE(UNCHANGED) | WAKEUP_ENABLE(DISABLED) @@ -100,38 +123,32 @@ void digitalWrite(uint16_t pin, uint8_t val) pullup(pin); else noPullup(pin); - } else { - if(pin != 16) - GPIO_REG_WRITE((((val != LOW) ? GPIO_OUT_W1TS_ADDRESS : GPIO_OUT_W1TC_ADDRESS)), (1 << pin)); - else - WRITE_PERI_REG(RTC_GPIO_OUT, (READ_PERI_REG(RTC_GPIO_OUT) & (uint32_t)0xfffffffe) | (uint32_t)(val & 1)); - - //GPIO_OUTPUT_SET(pin, (val ? 0xFF : 00)); - } + } else if(pin != 16) + GPIO_REG_WRITE((((val != LOW) ? GPIO_OUT_W1TS_ADDRESS : GPIO_OUT_W1TC_ADDRESS)), (1 << pin)); + else + WRITE_PERI_REG(RTC_GPIO_OUT, (READ_PERI_REG(RTC_GPIO_OUT) & (uint32_t)0xfffffffe) | (uint32_t)(val & 1)); } uint8_t digitalRead(uint16_t pin) { - if(pin != 16) + if(pin != 16) { return ((GPIO_REG_READ(GPIO_IN_ADDRESS) >> pin) & 1); - else - return (uint8_t)(READ_PERI_REG(RTC_GPIO_IN_DATA) & 1); - - //return GPIO_INPUT_GET(pin); + } + return (uint8_t)(READ_PERI_REG(RTC_GPIO_IN_DATA) & 1); } void pullup(uint16_t pin) { - if(pin >= 16) - return; - PIN_PULLUP_EN((EspDigitalPins[pin].mux)); + if(pin < TOTAL_PINS) { + PIN_PULLUP_EN(PERIPHS_IO_MUX + esp8266_pinmuxOffset[pin]); + } } void noPullup(uint16_t pin) { - if(pin >= 16) - return; - PIN_PULLUP_DIS((EspDigitalPins[pin].mux)); + if(pin < TOTAL_PINS) { + PIN_PULLUP_DIS(PERIPHS_IO_MUX + esp8266_pinmuxOffset[pin]); + } } /* Measures the length (in microseconds) of a pulse on the pin; state is HIGH diff --git a/Sming/Arch/Esp8266/Core/ESP8266EX.cpp b/Sming/Arch/Esp8266/Core/ESP8266EX.cpp deleted file mode 100644 index 59e8bd4250..0000000000 --- a/Sming/Arch/Esp8266/Core/ESP8266EX.cpp +++ /dev/null @@ -1,48 +0,0 @@ -/**** - * Sming Framework Project - Open Source framework for high efficiency native ESP8266 development. - * Created 2015 by Skurydin Alexey - * http://github.com/SmingHub/Sming - * All files of the Sming Core are provided under the LGPL v3 license. - * - * ESP8266EX.cpp - * - ****/ - -#include "Digital.h" -#include "ESP8266EX.h" -#include "Digital.h" -#include - -const EspDigitalPin EspDigitalPins[] = { - {0, PERIPHS_IO_MUX_GPIO0_U, FUNC_GPIO0}, // FLASH - {1, PERIPHS_IO_MUX_U0TXD_U, FUNC_GPIO1}, // TXD0 - {2, PERIPHS_IO_MUX_GPIO2_U, FUNC_GPIO2}, // TXD1 - {3, PERIPHS_IO_MUX_U0RXD_U, FUNC_GPIO3}, // RXD0 - {4, PERIPHS_IO_MUX_GPIO4_U, FUNC_GPIO4}, - {5, PERIPHS_IO_MUX_GPIO5_U, FUNC_GPIO5}, - {6, 0, 0}, // SD_CLK_U - {7, 0, 0}, // SD_DATA0_U - {8, 0, 0}, // SD_DATA1_U - {9, PERIPHS_IO_MUX_SD_DATA2_U, FUNC_GPIO9}, - {10, PERIPHS_IO_MUX_SD_DATA3_U, FUNC_GPIO10}, - {11, 0, 0}, // SD_CMD_U - {12, PERIPHS_IO_MUX_MTDI_U, FUNC_GPIO12}, // HSPIQ - {13, PERIPHS_IO_MUX_MTCK_U, FUNC_GPIO13}, // HSPID - {14, PERIPHS_IO_MUX_MTMS_U, FUNC_GPIO14}, // HSPICLK - {15, PERIPHS_IO_MUX_MTDO_U, FUNC_GPIO15}, // HSPICS -}; - -void EspDigitalPin::mode(uint8_t mode) const -{ - pinMode(id, mode); -} - -void EspDigitalPin::write(uint8_t val) const -{ - digitalWrite(id, val); -} - -uint8_t EspDigitalPin::read() const -{ - return digitalRead(id); -} diff --git a/Sming/Arch/Esp8266/Core/ESP8266EX.h b/Sming/Arch/Esp8266/Core/ESP8266EX.h deleted file mode 100644 index 6d83620d37..0000000000 --- a/Sming/Arch/Esp8266/Core/ESP8266EX.h +++ /dev/null @@ -1,38 +0,0 @@ -/**** - * Sming Framework Project - Open Source framework for high efficiency native ESP8266 development. - * Created 2015 by Skurydin Alexey - * http://github.com/SmingHub/Sming - * All files of the Sming Core are provided under the LGPL v3 license. - * - * ESP8266EX.h - * - ****/ - -#pragma once - -#include - -#define TOTAL_PINS 16 -#define NUM_DIGITAL_PINS TOTAL_PINS - -/** @brief ESP GPIO pin configuration - * @ingroup constants - */ -struct EspDigitalPin { - uint8_t id; - uint32_t mux; - uint8_t gpioFunc; - - operator const int() const - { - return id; - } - void mode(uint8_t mode) const; - void write(uint8_t val) const; - uint8_t read() const; -}; - -/** @brief ESP GPIO pin configuration - * @ingroup gpio - */ -extern const EspDigitalPin EspDigitalPins[]; diff --git a/Sming/Arch/Esp8266/Core/HardwarePWM.cpp b/Sming/Arch/Esp8266/Core/HardwarePWM.cpp index 585f5b813e..13aa43764a 100644 --- a/Sming/Arch/Esp8266/Core/HardwarePWM.cpp +++ b/Sming/Arch/Esp8266/Core/HardwarePWM.cpp @@ -25,11 +25,14 @@ #include #include -#include "ESP8266EX.h" #include +#include "pins_arduino.h" +#include #define PERIOD_TO_MAX_DUTY(x) (x * 25) +extern const uint8_t esp8266_pinmuxOffset[]; + HardwarePWM::HardwarePWM(uint8_t* pins, uint8_t noOfPins) : channel_count(noOfPins) { if(noOfPins == 0) { @@ -38,15 +41,22 @@ HardwarePWM::HardwarePWM(uint8_t* pins, uint8_t noOfPins) : channel_count(noOfPi uint32_t ioInfo[PWM_CHANNEL_NUM_MAX][3]; // pin information uint32_t pwmDutyInit[PWM_CHANNEL_NUM_MAX]; // pwm duty + unsigned pinCount = 0; for(uint8_t i = 0; i < noOfPins; i++) { - ioInfo[i][0] = EspDigitalPins[pins[i]].mux; - ioInfo[i][1] = EspDigitalPins[pins[i]].gpioFunc; - ioInfo[i][2] = EspDigitalPins[pins[i]].id; - pwmDutyInit[i] = 0; // Start with zero output - channels[i] = pins[i]; + auto pin = pins[i]; + assert(pin < 16); + if(pin >= 16) { + continue; + } + ioInfo[pinCount][0] = PERIPHS_IO_MUX + esp8266_pinmuxOffset[pin]; + ioInfo[pinCount][1] = esp8266_gpioToFn[pin]; + ioInfo[pinCount][2] = pin; + pwmDutyInit[pinCount] = 0; // Start with zero output + channels[pinCount] = pin; + ++pinCount; } const int initialPeriod = 1000; - pwm_init(initialPeriod, pwmDutyInit, noOfPins, ioInfo); + pwm_init(initialPeriod, pwmDutyInit, pinCount, ioInfo); update(); maxduty = PERIOD_TO_MAX_DUTY(initialPeriod); // for period of 1000 } diff --git a/Sming/Components/.patches/http-parser.patch b/Sming/Components/.patches/http-parser.patch index ce1eee4d48..8469a2dc19 100644 --- a/Sming/Components/.patches/http-parser.patch +++ b/Sming/Components/.patches/http-parser.patch @@ -1,8 +1,8 @@ diff --git a/http_parser.h b/http_parser.h -index df88252..5935bcf 100644 +index 3772b39..a1f4d38 100644 --- a/http_parser.h +++ b/http_parser.h -@@ -153,7 +153,7 @@ typedef int (*http_cb) (http_parser*); +@@ -155,7 +155,7 @@ typedef int (*http_cb) (http_parser*); enum http_status { @@ -11,7 +11,7 @@ index df88252..5935bcf 100644 HTTP_STATUS_MAP(XX) #undef XX }; -@@ -206,7 +206,7 @@ enum http_status +@@ -208,7 +208,7 @@ enum http_status enum http_method { @@ -20,7 +20,7 @@ index df88252..5935bcf 100644 HTTP_METHOD_MAP(XX) #undef XX }; -@@ -282,17 +282,13 @@ enum flags +@@ -283,17 +283,13 @@ enum flags /* Define HPE_* values for each errno value above */ @@ -39,8 +39,24 @@ index df88252..5935bcf 100644 struct http_parser { /** PRIVATE **/ unsigned int type : 2; /* enum http_parser_type */ +@@ -417,15 +413,6 @@ int http_should_keep_alive(const http_parser *parser); + /* Returns a string version of the HTTP method. */ + const char *http_method_str(enum http_method m); + +-/* Returns a string version of the HTTP status code. */ +-const char *http_status_str(enum http_status s); +- +-/* Return a string name of the given error */ +-const char *http_errno_name(enum http_errno err); +- +-/* Return a string description of the given error */ +-const char *http_errno_description(enum http_errno err); +- + /* Initialize all http_parser_url members to 0 */ + void http_parser_url_init(struct http_parser_url *u); + diff --git a/http_parser.c b/http_parser.c -index 95ff42f..beaf1f7 100644 +index 9be003e..e6b0051 100644 --- a/http_parser.c +++ b/http_parser.c @@ -20,11 +20,19 @@ @@ -65,7 +81,43 @@ index 95ff42f..beaf1f7 100644 static uint32_t max_header_size = HTTP_MAX_HEADER_SIZE; #ifndef ULLONG_MAX -@@ -186,7 +194,7 @@ static const char *method_strings[] = +@@ -160,7 +168,6 @@ do { \ + } \ + } while (0) + +- + #define PROXY_CONNECTION "proxy-connection" + #define CONNECTION "connection" + #define CONTENT_LENGTH "content-length" +@@ -170,10 +177,25 @@ do { \ + #define KEEP_ALIVE "keep-alive" + #define CLOSE "close" + ++static const char STR_PROXY_CONNECTION[] PROGMEM_PSTR = "proxy-connection"; ++static const char STR_CONNECTION[] PROGMEM_PSTR = "connection"; ++static const char STR_CONTENT_LENGTH[] PROGMEM_PSTR = "content-length"; ++static const char STR_TRANSFER_ENCODING[] PROGMEM_PSTR = "transfer-encoding"; ++static const char STR_UPGRADE[] PROGMEM_PSTR = "upgrade"; ++static const char STR_CHUNKED[] PROGMEM_PSTR = "chunked"; ++static const char STR_KEEP_ALIVE[] PROGMEM_PSTR = "keep-alive"; ++static const char STR_CLOSE[] PROGMEM_PSTR = "close"; + +-static const char *method_strings[] = ++#define get_pstr_char(name, index) (char)pgm_read_byte(&STR_##name[index]) ++ ++ ++#define XX(num, name, string) static const char method_string_##name[] PROGMEM_PSTR = #string; ++ HTTP_METHOD_MAP(XX) ++#undef XX ++ ++static const char *method_strings[] PROGMEM_PSTR = + { +-#define XX(num, name, string) #string, ++#define XX(num, name, string) method_string_##name, + HTTP_METHOD_MAP(XX) + #undef XX + }; +@@ -186,7 +208,7 @@ static const char *method_strings[] = * | "/" | "[" | "]" | "?" | "=" * | "{" | "}" | SP | HT */ @@ -74,7 +126,7 @@ index 95ff42f..beaf1f7 100644 /* 0 nul 1 soh 2 stx 3 etx 4 eot 5 enq 6 ack 7 bel */ 0, 0, 0, 0, 0, 0, 0, 0, /* 8 bs 9 ht 10 nl 11 vt 12 np 13 cr 14 so 15 si */ -@@ -220,19 +228,10 @@ static const char tokens[256] = { +@@ -220,19 +242,10 @@ static const char tokens[256] = { /* 120 x 121 y 122 z 123 { 124 | 125 } 126 ~ 127 del */ 'x', 'y', 'z', 0, '|', 0, '~', 0 }; @@ -98,7 +150,7 @@ index 95ff42f..beaf1f7 100644 #if HTTP_PARSER_STRICT # define T(v) 0 #else -@@ -428,14 +427,14 @@ enum http_host_state +@@ -428,14 +441,14 @@ enum http_host_state (c) == ';' || (c) == ':' || (c) == '&' || (c) == '=' || (c) == '+' || \ (c) == '$' || (c) == ',') @@ -115,7 +167,7 @@ index 95ff42f..beaf1f7 100644 #define IS_URL_CHAR(c) \ (BIT_AT(normal_url_char, (unsigned char)c) || ((c) & 0x80)) #define IS_HOST_CHAR(c) \ -@@ -467,16 +466,6 @@ do { \ +@@ -467,16 +480,6 @@ do { \ #endif @@ -132,7 +184,7 @@ index 95ff42f..beaf1f7 100644 int http_message_needs_eof(const http_parser *parser); /* Our URL parser. -@@ -758,7 +747,7 @@ reexecute: +@@ -760,7 +763,7 @@ reexecute: } parser->type = HTTP_REQUEST; @@ -141,7 +193,7 @@ index 95ff42f..beaf1f7 100644 parser->index = 2; UPDATE_STATE(s_req_method); } -@@ -938,23 +927,23 @@ reexecute: +@@ -940,23 +943,23 @@ reexecute: parser->method = (enum http_method) 0; parser->index = 1; switch (ch) { @@ -180,7 +232,18 @@ index 95ff42f..beaf1f7 100644 default: SET_ERRNO(HPE_INVALID_METHOD); goto error; -@@ -983,8 +972,8 @@ reexecute: +@@ -977,16 +980,17 @@ reexecute: + } + + matcher = method_strings[parser->method]; +- if (ch == ' ' && matcher[parser->index] == '\0') { ++ char matcher_char = pgm_read_byte(&matcher[parser->index]); ++ if (ch == ' ' && matcher_char == '\0') { + UPDATE_STATE(s_req_spaces_before_url); +- } else if (ch == matcher[parser->index]) { ++ } else if (ch == matcher_char) { + ; /* nada */ + } else if ((ch >= 'A' && ch <= 'Z') || ch == '-') { switch (parser->method << 16 | parser->index << 8 | ch) { #define XX(meth, pos, ch, new_meth) \ @@ -191,7 +254,7 @@ index 95ff42f..beaf1f7 100644 XX(POST, 1, 'U', PUT) XX(POST, 1, 'A', PATCH) -@@ -1024,7 +1013,7 @@ reexecute: +@@ -1026,7 +1030,7 @@ reexecute: if (ch == ' ') break; MARK(url); @@ -200,7 +263,7 @@ index 95ff42f..beaf1f7 100644 UPDATE_STATE(s_req_server_start); } -@@ -1100,7 +1089,7 @@ reexecute: +@@ -1102,7 +1106,7 @@ reexecute: UPDATE_STATE(s_req_http_H); break; case 'I': @@ -209,7 +272,88 @@ index 95ff42f..beaf1f7 100644 UPDATE_STATE(s_req_http_I); break; } -@@ -1826,7 +1815,7 @@ reexecute: +@@ -1303,7 +1307,7 @@ reexecute: + case h_matching_connection: + parser->index++; + if (parser->index > sizeof(CONNECTION)-1 +- || c != CONNECTION[parser->index]) { ++ || c != get_pstr_char(CONNECTION, parser->index)) { + parser->header_state = h_general; + } else if (parser->index == sizeof(CONNECTION)-2) { + parser->header_state = h_connection; +@@ -1315,7 +1319,7 @@ reexecute: + case h_matching_proxy_connection: + parser->index++; + if (parser->index > sizeof(PROXY_CONNECTION)-1 +- || c != PROXY_CONNECTION[parser->index]) { ++ || c != get_pstr_char(PROXY_CONNECTION, parser->index)) { + parser->header_state = h_general; + } else if (parser->index == sizeof(PROXY_CONNECTION)-2) { + parser->header_state = h_connection; +@@ -1327,7 +1331,7 @@ reexecute: + case h_matching_content_length: + parser->index++; + if (parser->index > sizeof(CONTENT_LENGTH)-1 +- || c != CONTENT_LENGTH[parser->index]) { ++ || c != get_pstr_char(CONTENT_LENGTH, parser->index)) { + parser->header_state = h_general; + } else if (parser->index == sizeof(CONTENT_LENGTH)-2) { + parser->header_state = h_content_length; +@@ -1339,7 +1343,7 @@ reexecute: + case h_matching_transfer_encoding: + parser->index++; + if (parser->index > sizeof(TRANSFER_ENCODING)-1 +- || c != TRANSFER_ENCODING[parser->index]) { ++ || c != get_pstr_char(TRANSFER_ENCODING, parser->index)) { + parser->header_state = h_general; + } else if (parser->index == sizeof(TRANSFER_ENCODING)-2) { + parser->header_state = h_transfer_encoding; +@@ -1352,7 +1356,7 @@ reexecute: + case h_matching_upgrade: + parser->index++; + if (parser->index > sizeof(UPGRADE)-1 +- || c != UPGRADE[parser->index]) { ++ || c != get_pstr_char(UPGRADE, parser->index)) { + parser->header_state = h_general; + } else if (parser->index == sizeof(UPGRADE)-2) { + parser->header_state = h_upgrade; +@@ -1597,7 +1601,7 @@ reexecute: + case h_matching_transfer_encoding_chunked: + parser->index++; + if (parser->index > sizeof(CHUNKED)-1 +- || c != CHUNKED[parser->index]) { ++ || c != get_pstr_char(CHUNKED, parser->index)) { + h_state = h_matching_transfer_encoding_token; + } else if (parser->index == sizeof(CHUNKED)-2) { + h_state = h_transfer_encoding_chunked; +@@ -1633,7 +1637,7 @@ reexecute: + case h_matching_connection_keep_alive: + parser->index++; + if (parser->index > sizeof(KEEP_ALIVE)-1 +- || c != KEEP_ALIVE[parser->index]) { ++ || c != get_pstr_char(KEEP_ALIVE, parser->index)) { + h_state = h_matching_connection_token; + } else if (parser->index == sizeof(KEEP_ALIVE)-2) { + h_state = h_connection_keep_alive; +@@ -1643,7 +1647,7 @@ reexecute: + /* looking for 'Connection: close' */ + case h_matching_connection_close: + parser->index++; +- if (parser->index > sizeof(CLOSE)-1 || c != CLOSE[parser->index]) { ++ if (parser->index > sizeof(CLOSE)-1 || c != get_pstr_char(CLOSE, parser->index)) { + h_state = h_matching_connection_token; + } else if (parser->index == sizeof(CLOSE)-2) { + h_state = h_connection_close; +@@ -1654,7 +1658,7 @@ reexecute: + case h_matching_connection_upgrade: + parser->index++; + if (parser->index > sizeof(UPGRADE) - 1 || +- c != UPGRADE[parser->index]) { ++ c != get_pstr_char(UPGRADE, parser->index)) { + h_state = h_matching_connection_token; + } else if (parser->index == sizeof(UPGRADE)-2) { + h_state = h_connection_upgrade; +@@ -1833,7 +1837,7 @@ reexecute: parser->upgrade = (parser->type == HTTP_REQUEST || parser->status_code == 101); } else { @@ -218,7 +362,7 @@ index 95ff42f..beaf1f7 100644 } /* Here we call the headers_complete callback. This is somewhat -@@ -1874,7 +1863,7 @@ reexecute: +@@ -1881,7 +1885,7 @@ reexecute: hasBody = parser->flags & F_CHUNKED || (parser->content_length > 0 && parser->content_length != ULLONG_MAX); @@ -227,7 +371,7 @@ index 95ff42f..beaf1f7 100644 (parser->flags & F_SKIPBODY) || !hasBody)) { /* Exit, the rest of the message is in a different protocol. */ UPDATE_STATE(NEW_MESSAGE()); -@@ -1991,7 +1980,7 @@ reexecute: +@@ -1998,7 +2002,7 @@ reexecute: assert(nread == 1); assert(parser->flags & F_CHUNKED); @@ -236,7 +380,7 @@ index 95ff42f..beaf1f7 100644 if (UNLIKELY(unhex_val == -1)) { SET_ERRNO(HPE_INVALID_CHUNK_SIZE); goto error; -@@ -2013,7 +2002,7 @@ reexecute: +@@ -2020,7 +2024,7 @@ reexecute: break; } @@ -245,16 +389,27 @@ index 95ff42f..beaf1f7 100644 if (unhex_val == -1) { if (ch == ';' || ch == ' ') { -@@ -2207,7 +2196,7 @@ const char * - http_status_str (enum http_status s) +@@ -2207,18 +2211,7 @@ http_should_keep_alive (const http_parser *parser) + const char * + http_method_str (enum http_method m) { - switch (s) { +- return ELEM_AT(method_strings, m, ""); +-} +- +-const char * +-http_status_str (enum http_status s) +-{ +- switch (s) { -#define XX(num, name, string) case HTTP_STATUS_##name: return #string; -+#define XX(num, name, string) case HTTP_STATUS_CODE_##name: return #string; - HTTP_STATUS_MAP(XX) - #undef XX - default: return ""; -@@ -2231,18 +2220,6 @@ http_parser_settings_init(http_parser_settings *settings) +- HTTP_STATUS_MAP(XX) +-#undef XX +- default: return ""; +- } ++ return ELEM_AT(method_strings, m, PSTR("")); + } + + void +@@ -2238,18 +2231,6 @@ http_parser_settings_init(http_parser_settings *settings) memset(settings, 0, sizeof(*settings)); } diff --git a/Sming/Components/.patches/ws_parser.patch b/Sming/Components/.patches/ws_parser.patch index b1ff2037ef..c1231824dc 100644 --- a/Sming/Components/.patches/ws_parser.patch +++ b/Sming/Components/.patches/ws_parser.patch @@ -1,16 +1,17 @@ diff --git a/ws_parser.c b/ws_parser.c -index d43adbe..beeb611 100644 +index d43adbe..17040e3 100644 --- a/ws_parser.c +++ b/ws_parser.c -@@ -3,6 +3,7 @@ +@@ -3,6 +3,8 @@ #endif #include "ws_parser.h" +#include ++#include enum { S_OPCODE = 0, -@@ -27,6 +28,7 @@ enum { +@@ -27,6 +29,7 @@ enum { void ws_parser_init(ws_parser_t* parser) { @@ -18,7 +19,7 @@ index d43adbe..beeb611 100644 parser->state = S_OPCODE; parser->fragment = 0; } -@@ -247,7 +249,8 @@ ws_parser_execute( +@@ -247,7 +250,8 @@ ws_parser_execute( } if(parser->mask_flag) { @@ -28,3 +29,12 @@ index d43adbe..beeb611 100644 buff[i] ^= parser->mask[parser->mask_pos++]; } } +@@ -302,7 +306,7 @@ ws_parser_execute( + const char* + ws_parser_error(int rc) + { +- #define XX(name, code) if(rc == code) return #name; ++ #define XX(name, code) if(rc == code) return PSTR(#name); + WS_PARSER_ERROR_CODES(XX) + #undef XX + diff --git a/Sming/Components/Network/src/Network/Http/HttpCommon.h b/Sming/Components/Network/src/Network/Http/HttpCommon.h index 6ee5181b36..57c0785044 100644 --- a/Sming/Components/Network/src/Network/Http/HttpCommon.h +++ b/Sming/Components/Network/src/Network/Http/HttpCommon.h @@ -121,7 +121,8 @@ inline String httpGetStatusText(unsigned code) */ inline String toString(HttpMethod method) { - return http_method_str(http_method(method)); + auto fstr = reinterpret_cast(http_method_str(http_method(method))); + return String(fstr); } /** @} */ diff --git a/Sming/Components/http-parser b/Sming/Components/http-parser index 2343fd6b52..ec8b5ee63f 160000 --- a/Sming/Components/http-parser +++ b/Sming/Components/http-parser @@ -1 +1 @@ -Subproject commit 2343fd6b5214b2ded2cdcf76de2bf60903bb90cd +Subproject commit ec8b5ee63f0e51191ea43bb0c6eac7bfbff3141d diff --git a/Sming/Libraries/RapidXML b/Sming/Libraries/RapidXML index 4a1fe56b85..1e9f276c65 160000 --- a/Sming/Libraries/RapidXML +++ b/Sming/Libraries/RapidXML @@ -1 +1 @@ -Subproject commit 4a1fe56b851fef63b99a1433c743e5ca0043b08d +Subproject commit 1e9f276c656c3319a09ac956d3acf53e74988e85 diff --git a/Tools/ci/scanlog.py b/Tools/ci/scanlog.py new file mode 100644 index 0000000000..80f1d7a12f --- /dev/null +++ b/Tools/ci/scanlog.py @@ -0,0 +1,193 @@ +#!/usr/bin/env python +# +# Python application to scan CI build logs and extract useful information +# +# Github action logs must be in raw (text) format for processing. +# +# Use cases: +# +# Generate table of esp8266 memory usage +# Take two log files (cut & paste usually) +# Run scanlog on both +# Use kdiff3 to compare +# + +import argparse +import os +from enum import Enum + + +class State(Enum): + searching = 1 + building = 2 + linking = 3 + + +class Table: + COL_SEP = ' ' + + def __init__(self, name: str): + self.name = name + self.headings = [] + self.rows = [] + self.col_widths = [] + + def append(self, row_data: dict): + for k in row_data.keys(): + if k in self.headings: + continue + self.headings.append(k) + self.col_widths.append(len(k)) + row = ['' for x in self.headings] + for k, v in row_data.items(): + i = self.headings.index(k) + row[i] = v + self.col_widths[i] = max(self.col_widths[i], len(v)) + self.rows.append(row) + + def _format_values(self, values: list): + return self.COL_SEP.join(str(v).ljust(self.col_widths[i]) for i, v in enumerate(values)) + + def format_headings(self): + return self._format_values(self.headings) + + def format_separator(self): + return self.COL_SEP.join(''.ljust(w, '-') for w in self.col_widths) + + def format_row(self, row: int | list): + if isinstance(row, int): + row = self.rows[row] + return self._format_values(row) + + def find_row(self, name: str): + return next((i for i, row in enumerate(self.rows) if row[0] == name), -1) + + def __iter__(self): + return TableFormatter(self) + + +class TableFormatter: + def __init__(self, table: Table): + self.table = table + self.row_index = -2 + + def __next__(self): + table = self.table + idx = self.row_index + if idx == -2: + self.row_index = -1 + return table.format_headings() + if idx == -1: + self.row_index = 0 + return table.format_separator() + if idx < len(table.rows): + self.row_index = idx + 1 + return table.format_row(idx) + raise StopIteration + + +def scan_log(filename: str): + state = State.searching + table = Table(os.path.basename(filename)) + target = None + row = None + log = open(filename, 'rb') + for line in log: + line = line.decode('utf-8-sig') + dtstr, _, line = line.strip().partition(' ') + if not dtstr: + continue + _, sep, c = line.partition('** Building ') + if sep: + target, _, _ = c.partition(' ') + target = target.removeprefix('/home/runner/projects/') + row = { + 'target': target + } + state = State.building + continue + if state == State.building: + if line.startswith(f'{os.path.basename(target)}: Linking'): + state = State.linking + continue + if state == State.linking: + if 'Section' in line: + continue + if line.startswith('----'): + continue + if '|' in line: + cols = line.split('|') + k, v = cols[0], cols[4] + elif ' : ' in line: + k, v = line.split(':') + else: + table.append(row) + row = None + state = State.searching + continue + k, v = k.strip(), v.strip() + row[k] = v + continue + + return table + + +def print_table(table: Table): + print(f'{table.name}') + for line in table: + print(' ', line) + print() + + +def main(): + parser = argparse.ArgumentParser(description='Sming CI log parser') + parser.add_argument('filename', help='Log filename') + parser.add_argument('-c', '--compare', help='Second log to compare') + + args = parser.parse_args() + + table1 = scan_log(args.filename) + if args.compare is None: + print_table(table1) + return + + table2 = scan_log(args.compare) + + for row1 in table1.rows: + target = row1[0] + i = table2.find_row(target) + if i < 0: + print(f'** {target} NOT found in {table2.name}') + continue + row2 = table2.rows.pop(i) + if row2 == row1: + continue + + diff_table = Table(target) + + data = {'log': table1.name} + for k, v in zip(table1.headings[1:], row1[1:]): + data[k] = v + diff_table.append(data) + data = {'log': table2.name} + for k, v in zip(table2.headings[1:], row2[1:]): + data[k] = v + diff_table.append(data) + + data = {'log': 'Difference'} + for name, v1, v2 in zip(table1.headings[1:], row1[1:], row2[1:]): + if v1 == v2: + continue + v1, v2 = int(v1, 0), int(v2, 0) + data[name] = f'{v2-v1:+}' + diff_table.append(data) + + print_table(diff_table) + + if table2.rows: + print(f'** Targets not in {table1.name}') + print_table(table2) + + +if __name__ == "__main__": + main() From c487f910a077f3d420e36ce96d9369071af85ad1 Mon Sep 17 00:00:00 2001 From: Mike Date: Wed, 3 Apr 2024 06:13:28 +0100 Subject: [PATCH 040/128] Fix client Websockets (broken) (#2749) This PR addresses several issues discovered in the client websocket stack. A simple local websocket echo server has been added for testing, which can be run using `make wsserver`. **Memory leaks** Running `HttpServer_Websockets` sample with valgrind shows a memory leak (details below, fig 1). I can see from the logic of `WebsocketConnection::send` that there are multiple reasons the call could fail, but the `source` stream is only destroyed in one of them. **Failed connection** Testing with the local server failed with `websockets.exceptions.InvalidHeaderValue: invalid Sec-WebSocket-Key header`. The key was 17 bytes instead of 16. **utf-8 decoding errors** Turns out message was getting corrupted because mask value passed to XorStream is on stack, which then gets overwritten before message has been sent out. Fixed by taking a copy of the value. **CLOSE message not being sent** Tested with `Websocket_Client` sample (running local echo server) to confirm correct behaviour, noticed a `Streams without known size are not supported` message when closing the connection. This blocked sending 'CLOSE' notifications which have no payload. **Issues during CLOSE** The TCP socket was being closed too soon, causing additional errors. Increasing timeout to 2 seconds fixes this. Also included a status code in the CLOSE message indicating normal closure; this is optional, but seems like a good thing to do. RFC 6455 states: *The application MUST NOT send any more data frames after sending a Close frame. If an endpoint receives a Close frame and did not previously send a Close frame, the endpoint MUST send a Close frame in response.* Therefore, the `close()` logic has been changed so that a CLOSE message is *always* sent, either in response to a previous incoming request (in which case the received status is echoed back) or as notification that we're closing (status 1000 - normal closure). Checked server operation with `HttpServer_Websockets` sample **Simplify packet generation** It's not necessary to pre-calculate the packet length as it's never more than 14 bytes in length. **WebsocketConnection not getting destroyed** HttpConnection objects are not 'auto-released' so leaks memory every time a WebsocketConnection is destroyed (512+ bytes). Simplest fix for this is to add a `setAutoSelfDestruct` method to `TcpConnection` so this can be changed. **Messages not being received** Connection is activated OK, but `HttpClientConnection` then calls `init` in its `onConnected` handler which resets the new `receive` delegate. This causes incoming websocket frames to be passed to the http parser, instead of the WS parser, hence the `HTTP parser error: HPE_INVALID_CONSTANT` message. ==== Fig 1: Initial memory leak reported by valgrind ``` ==1291918== ==1291918== HEAP SUMMARY: ==1291918== in use at exit: 4,133 bytes in 16 blocks ==1291918== total heap usage: 573 allocs, 557 frees, 71,139 bytes allocated ==1291918== ==1291918== 64 bytes in 2 blocks are definitely lost in loss record 10 of 13 ==1291918== at 0x4041D7D: operator new(unsigned int) (vg_replace_malloc.c:476) ==1291918== by 0x8075BC5: WebsocketConnection::send(char const*, unsigned int, ws_frame_type_t) (WebsocketConnection.cpp:180) ==1291918== by 0x804EB65: send (WebsocketConnection.h:107) ==1291918== by 0x804EB65: sendString (WebsocketConnection.h:145) ==1291918== by 0x804EB65: wsCommandReceived(WebsocketConnection&, String const&) (application.cpp:88) ==1291918== by 0x807575D: operator() (std_function.h:591) ==1291918== by 0x807575D: WebsocketConnection::staticOnDataPayload(void*, char const*, unsigned int) (WebsocketConnection.cpp:128) ==1291918== by 0x8081E5C: ws_parser_execute (ws_parser.c:263) ==1291918== by 0x80756C6: WebsocketConnection::processFrame(TcpClient&, char*, int) (WebsocketConnection.cpp:103) ==1291918== by 0x8079305: operator() (std_function.h:591) ==1291918== by 0x8079305: TcpClient::onReceive(pbuf*) (TcpClient.cpp:150) ==1291918== by 0x8078A8E: TcpConnection::internalOnReceive(pbuf*, signed char) (TcpConnection.cpp:484) ==1291918== by 0x8058560: tcp_input (in /stripe/sandboxes/sming-dev/samples/HttpServer_WebSockets/out/Host/debug/firmware/app) ==1291918== by 0x80627F3: ip4_input (in /stripe/sandboxes/sming-dev/samples/HttpServer_WebSockets/out/Host/debug/firmware/app) ==1291918== by 0x8063C89: ethernet_input (in /stripe/sandboxes/sming-dev/samples/HttpServer_WebSockets/out/Host/debug/firmware/app) ==1291918== by 0x8064245: tapif_select (in /stripe/sandboxes/sming-dev/samples/HttpServer_WebSockets/out/Host/debug/firmware/app) ==1291918== ==1291918== LEAK SUMMARY: ==1291918== definitely lost: 64 bytes in 2 blocks ==1291918== indirectly lost: 0 bytes in 0 blocks ==1291918== possibly lost: 0 bytes in 0 blocks ==1291918== still reachable: 4,069 bytes in 14 blocks ==1291918== suppressed: 0 bytes in 0 blocks ==1291918== Reachable blocks (those to which a pointer was found) are not shown. ==1291918== To see them, rerun with: --leak-check=full --show-leak-kinds=all ==1291918== ==1291918== For lists of detected and suppressed errors, rerun with: -s ==1291918== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0) ``` --- Sming/Components/Network/component.mk | 11 + .../Http/Websocket/WebsocketConnection.cpp | 189 ++++++++++-------- .../Http/Websocket/WebsocketConnection.h | 24 +-- .../Network/src/Network/TcpConnection.h | 6 +- .../Network/src/Network/WebsocketClient.cpp | 31 ++- .../Network/src/Network/WebsocketClient.h | 6 +- Sming/Components/Network/tools/wsserver.py | 20 ++ Sming/Core/Data/Stream/XorOutputStream.h | 5 +- samples/Websocket_Client/README.rst | 14 +- samples/Websocket_Client/app/application.cpp | 73 ++++--- samples/Websocket_Client/component.mk | 7 + samples/Websocket_Client/test.py | 30 +++ 12 files changed, 253 insertions(+), 163 deletions(-) create mode 100644 Sming/Components/Network/tools/wsserver.py create mode 100644 samples/Websocket_Client/test.py diff --git a/Sming/Components/Network/component.mk b/Sming/Components/Network/component.mk index 1bcda900fb..5763cdca7c 100644 --- a/Sming/Components/Network/component.mk +++ b/Sming/Components/Network/component.mk @@ -93,3 +93,14 @@ COMPONENT_INCDIRS += \ endif endif + +##@Testing + +# Websocket Server +CACHE_VARS += WSSERVER_PORT +WSSERVER_PORT ?= 9999 +.PHONY: wsserver +wsserver: ##Launch a simple python Websocket echo server for testing client applications + $(info Starting Websocket server for TESTING) + $(Q) $(PYTHON) $(CMP_Network_PATH)/tools/wsserver.py $(WSSERVER_PORT) + diff --git a/Sming/Components/Network/src/Network/Http/Websocket/WebsocketConnection.cpp b/Sming/Components/Network/src/Network/Http/Websocket/WebsocketConnection.cpp index 81d1c46cb4..ec4d5253a2 100644 --- a/Sming/Components/Network/src/Network/Http/Websocket/WebsocketConnection.cpp +++ b/Sming/Components/Network/src/Network/Http/Websocket/WebsocketConnection.cpp @@ -9,13 +9,11 @@ ****/ #include "WebsocketConnection.h" -#include #include #include #include #include #include -#include DEFINE_FSTR(WSSTR_CONNECTION, "connection") DEFINE_FSTR(WSSTR_UPGRADE, "upgrade") @@ -32,12 +30,14 @@ WebsocketList WebsocketConnection::websocketList; /** @brief ws_parser function table * @note stored in flash memory; as it is word-aligned it can be accessed directly */ -const ws_parser_callbacks_t WebsocketConnection::parserSettings PROGMEM = {.on_data_begin = staticOnDataBegin, - .on_data_payload = staticOnDataPayload, - .on_data_end = staticOnDataEnd, - .on_control_begin = staticOnControlBegin, - .on_control_payload = staticOnControlPayload, - .on_control_end = staticOnControlEnd}; +const ws_parser_callbacks_t WebsocketConnection::parserSettings PROGMEM{ + .on_data_begin = staticOnDataBegin, + .on_data_payload = staticOnDataPayload, + .on_data_end = staticOnDataEnd, + .on_control_begin = staticOnControlBegin, + .on_control_payload = staticOnControlPayload, + .on_control_end = staticOnControlEnd, +}; /** @brief Boilerplate code for ws_parser callbacks * @note Obtain connection object and check it @@ -55,6 +55,14 @@ WebsocketConnection::WebsocketConnection(HttpConnection* connection, bool isClie ws_parser_init(&parser); } +void WebsocketConnection::setConnection(HttpConnection* connection, bool isClientConnection) +{ + assert(this->connection == nullptr); + this->connection = connection; + this->isClientConnection = isClientConnection; + this->state = connection ? eWSCS_Ready : eWSCS_Closed; +} + bool WebsocketConnection::bind(HttpRequest& request, HttpResponse& response) { String version = request.headers[HTTP_HEADER_SEC_WEBSOCKET_VERSION]; @@ -124,10 +132,21 @@ int WebsocketConnection::staticOnDataPayload(void* userData, const char* at, siz { GET_CONNECTION(); - if(connection->frameType == WS_FRAME_TEXT && connection->wsMessage) { - connection->wsMessage(*connection, String(at, length)); - } else if(connection->frameType == WS_FRAME_BINARY && connection->wsBinary) { - connection->wsBinary(*connection, reinterpret_cast(const_cast(at)), length); + switch(connection->frameType) { + case WS_FRAME_TEXT: + if(connection->wsMessage) { + connection->wsMessage(*connection, String(at, length)); + } + break; + case WS_FRAME_BINARY: + if(connection->wsBinary) { + connection->wsBinary(*connection, reinterpret_cast(const_cast(at)), length); + } + break; + case WS_FRAME_CLOSE: + case WS_FRAME_PING: + case WS_FRAME_PONG: + break; } return WS_OK; @@ -142,11 +161,7 @@ int WebsocketConnection::staticOnControlBegin(void* userData, ws_frame_type_t ty { GET_CONNECTION(); - connection->controlFrame = WsFrameInfo(type, nullptr, 0); - - if(type == WS_FRAME_CLOSE) { - connection->close(); - } + connection->controlFrame = WsFrameInfo{type}; return WS_OK; } @@ -165,13 +180,26 @@ int WebsocketConnection::staticOnControlEnd(void* userData) { GET_CONNECTION(); - if(connection->controlFrame.type == WS_FRAME_PING) { + switch(connection->controlFrame.type) { + case WS_FRAME_PING: connection->send(connection->controlFrame.payload, connection->controlFrame.payloadLength, WS_FRAME_PONG); - } + break; + case WS_FRAME_PONG: + if(connection->wsPong) { + connection->wsPong(*connection); + } + break; + + case WS_FRAME_CLOSE: + debug_hex(DBG, "WS: CLOSE", connection->controlFrame.payload, connection->controlFrame.payloadLength); + connection->close(); + break; - if(connection->controlFrame.type == WS_FRAME_PONG && connection->wsPong) { - connection->wsPong(*connection); + case WS_FRAME_TEXT: + case WS_FRAME_BINARY: + break; } + return WS_OK; } @@ -186,6 +214,7 @@ bool WebsocketConnection::send(const char* message, size_t length, ws_frame_type size_t written = stream->write(message, length); if(written != length) { debug_e("Unable to store data in memory buffer"); + delete stream; return false; } @@ -194,96 +223,78 @@ bool WebsocketConnection::send(const char* message, size_t length, ws_frame_type bool WebsocketConnection::send(IDataSourceStream* source, ws_frame_type_t type, bool useMask, bool isFin) { + // Ensure source gets destroyed if we return prematurely + std::unique_ptr sourceRef(source); + if(source == nullptr) { + debug_w("WS: No source"); return false; } if(connection == nullptr) { + debug_w("WS: No connection"); return false; } if(!activated) { - debug_e("WS Connection is not activated yet!"); + debug_e("WS: Not activated"); return false; } int available = source->available(); - if(available < 1) { - debug_e("Streams without known size are not supported"); + if(available < 0) { + debug_e("WS: Unknown stream size"); return false; } - debug_d("Sending: %d bytes, Type: %d\n", available, type); - - size_t packetLength = 2; - uint16_t lengthValue = available; + debug_d("WS: Sending %d bytes, type %d", available, type); - // calculate message length .... - if(available <= 125) { - lengthValue = available; - } else if(available < 65536) { - lengthValue = 126; - packetLength += 2; - } else { - lengthValue = 127; - packetLength += 8; - } - - if(useMask) { - packetLength += 4; // we use mask with size 4 bytes - } - - uint8_t packet[packetLength]; - memset(packet, 0, packetLength); - - int i = 0; - // byte 0 + // Construct packet + uint8_t packet[16]{}; + unsigned len = 0; if(isFin) { - packet[i] |= bit(7); // set Fin + packet[len] |= _BV(7); // set Fin } - packet[i++] |= (uint8_t)type; // set opcode - // byte 1 + packet[len++] |= type; // set opcode if(useMask) { - packet[i] |= bit(7); // set mask + packet[len] |= _BV(7); // set mask } - // length - if(lengthValue < 126) { - packet[i++] |= lengthValue; - } else if(lengthValue == 126) { - packet[i++] |= 126; - packet[i++] = (available >> 8) & 0xFF; - packet[i++] = available & 0xFF; - } else if(lengthValue == 127) { - packet[i++] |= 127; - packet[i++] = 0; - packet[i++] = 0; - packet[i++] = 0; - packet[i++] = 0; - packet[i++] = (available >> 24) & 0xFF; - packet[i++] = (available >> 16) & 0xFF; - packet[i++] = (available >> 8) & 0xFF; - packet[i++] = (available)&0xFF; + if(available <= 125) { + packet[len++] |= available; + } else if(available <= 0xffff) { + packet[len++] |= 126; + packet[len++] = available >> 8; + packet[len++] = available; + } else { + packet[len++] |= 127; + len += 4; // All 0 + packet[len++] = available >> 24; + packet[len++] = available >> 16; + packet[len++] = available >> 8; + packet[len++] = available; } - if(useMask) { - uint8_t maskKey[4] = {0x00, 0x00, 0x00, 0x00}; - for(uint8_t x = 0; x < sizeof(maskKey); x++) { - maskKey[x] = (char)os_random(); - packet[i++] = maskKey[x]; - } + uint8_t maskKey[4]; + os_get_random(maskKey, sizeof(maskKey)); + memcpy(&packet[len], maskKey, sizeof(maskKey)); + len += sizeof(maskKey); auto xorStream = new XorOutputStream(source, maskKey, sizeof(maskKey)); - source = xorStream; + if(xorStream == nullptr) { + return false; + } + sourceRef.release(); + sourceRef.reset(xorStream); } // send the header - if(!connection->send(reinterpret_cast(packet), packetLength)) { - delete source; + if(!connection->send(reinterpret_cast(packet), len)) { return false; } - return connection->send(source); + // Pass stream to connection + return connection->send(sourceRef.release()); } void WebsocketConnection::broadcast(const char* message, size_t length, ws_frame_type_t type) @@ -300,12 +311,19 @@ void WebsocketConnection::broadcast(const char* message, size_t length, ws_frame void WebsocketConnection::close() { - debug_d("Terminating Websocket connection."); + if(connection == nullptr) { + return; + } + + debug_d("WS: Terminating connection %p, state %u", connection, state); websocketList.removeElement(this); if(state != eWSCS_Closed) { state = eWSCS_Closed; - if(isClientConnection) { - send(nullptr, 0, WS_FRAME_CLOSE); + if(controlFrame.type == WS_FRAME_CLOSE) { + send(controlFrame.payload, controlFrame.payloadLength, WS_FRAME_CLOSE); + } else { + uint16_t status = htons(1000); + send(reinterpret_cast(&status), sizeof(status), WS_FRAME_CLOSE); } activated = false; if(wsDisconnect) { @@ -313,10 +331,9 @@ void WebsocketConnection::close() } } - if(connection) { - connection->setTimeOut(1); - connection = nullptr; - } + connection->setTimeOut(2); + connection->setAutoSelfDestruct(true); + connection = nullptr; } void WebsocketConnection::reset() diff --git a/Sming/Components/Network/src/Network/Http/Websocket/WebsocketConnection.h b/Sming/Components/Network/src/Network/Http/Websocket/WebsocketConnection.h index fc5b32350f..95f2f12620 100644 --- a/Sming/Components/Network/src/Network/Http/Websocket/WebsocketConnection.h +++ b/Sming/Components/Network/src/Network/Http/Websocket/WebsocketConnection.h @@ -56,13 +56,6 @@ struct WsFrameInfo { ws_frame_type_t type = WS_FRAME_TEXT; char* payload = nullptr; size_t payloadLength = 0; - - WsFrameInfo() = default; - - WsFrameInfo(ws_frame_type_t type, char* payload, size_t payloadLength) - : type(type), payload(payload), payloadLength(payloadLength) - { - } }; class WebsocketConnection @@ -73,7 +66,7 @@ class WebsocketConnection * @param connection the transport connection * @param isClientConnection true when the passed connection is an http client connection */ - WebsocketConnection(HttpConnection* connection, bool isClientConnection = true); + WebsocketConnection(HttpConnection* connection = nullptr, bool isClientConnection = true); virtual ~WebsocketConnection() { @@ -109,14 +102,14 @@ class WebsocketConnection /** * @brief Sends websocket message from a stream - * @param stream + * @param source The stream to send - we get ownership of the stream * @param type * @param useMask MUST be true for client connections * @param isFin true if this is the final frame * * @retval bool true on success */ - bool send(IDataSourceStream* stream, ws_frame_type_t type = WS_FRAME_TEXT, bool useMask = false, bool isFin = true); + bool send(IDataSourceStream* source, ws_frame_type_t type = WS_FRAME_TEXT, bool useMask = false, bool isFin = true); /** * @brief Broadcasts a message to all active websocket connections @@ -271,11 +264,7 @@ class WebsocketConnection * @param connection the transport connection * @param isClientConnection true when the passed connection is an http client connection */ - void setConnection(HttpConnection* connection, bool isClientConnection = true) - { - this->connection = connection; - this->isClientConnection = isClientConnection; - } + void setConnection(HttpConnection* connection, bool isClientConnection = true); /** @brief Gets the state of the websocket connection * @retval WsConnectionState @@ -311,7 +300,7 @@ class WebsocketConnection void* userData = nullptr; - WsConnectionState state = eWSCS_Ready; + WsConnectionState state; private: ws_frame_type_t frameType = WS_FRAME_TEXT; @@ -322,9 +311,8 @@ class WebsocketConnection static WebsocketList websocketList; - bool isClientConnection = true; - HttpConnection* connection = nullptr; + bool isClientConnection; bool activated = false; }; diff --git a/Sming/Components/Network/src/Network/TcpConnection.h b/Sming/Components/Network/src/Network/TcpConnection.h index d2f023df2f..3ea5367418 100644 --- a/Sming/Components/Network/src/Network/TcpConnection.h +++ b/Sming/Components/Network/src/Network/TcpConnection.h @@ -50,11 +50,15 @@ class TcpConnection : public IpConnection virtual ~TcpConnection(); -public: virtual bool connect(const String& server, int port, bool useSsl = false); virtual bool connect(IpAddress addr, uint16_t port, bool useSsl = false); virtual void close(); + void setAutoSelfDestruct(bool state) + { + autoSelfDestruct = state; + } + /** @brief Writes string data directly to the TCP buffer * @param data null terminated string * @param apiflags TCP_WRITE_FLAG_COPY, TCP_WRITE_FLAG_MORE diff --git a/Sming/Components/Network/src/Network/WebsocketClient.cpp b/Sming/Components/Network/src/Network/WebsocketClient.cpp index 8e5fe09537..d3643c0df8 100644 --- a/Sming/Components/Network/src/Network/WebsocketClient.cpp +++ b/Sming/Components/Network/src/Network/WebsocketClient.cpp @@ -17,11 +17,25 @@ #include "Http/HttpHeaders.h" #include +class WebsocketClientConnection : public HttpClientConnection +{ +protected: + // Prevent HttpClientConnection from resetting our receive delegate set by activate() call + err_t onConnected(err_t err) override + { + if(err == ERR_OK) { + state = eHCS_Ready; + } + + return HttpConnection::onConnected(err); + } +}; + HttpConnection* WebsocketClient::getHttpConnection() { auto connection = WebsocketConnection::getConnection(); if(connection == nullptr && state == eWSCS_Closed) { - connection = new HttpClientConnection(); + connection = new WebsocketClientConnection(); setConnection(connection); } @@ -48,14 +62,9 @@ bool WebsocketClient::connect(const Url& url) state = eWSCS_Ready; // Generate the key - unsigned char keyStart[17] = {0}; - char b64Key[25]; - memset(b64Key, 0, sizeof(b64Key)); - - for(int i = 0; i < 16; ++i) { - keyStart[i] = 1 + os_random() % 255; - } - key = base64_encode(keyStart, sizeof(keyStart)); + unsigned char keyData[16]; + os_get_random(keyData, sizeof(keyData)); + key = base64_encode(keyData, sizeof(keyData)); HttpRequest* request = new HttpRequest(uri); request->headers[HTTP_HEADER_UPGRADE] = WSSTR_WEBSOCKET; @@ -87,9 +96,9 @@ int WebsocketClient::verifyKey(HttpConnection& connection, HttpResponse& respons auto hash = Crypto::Sha1().calculate(keyToHash); String base64hash = base64_encode(hash.data(), hash.size()); if(base64hash != serverHashedKey) { - debug_e("wscli key mismatch: %s | %s", serverHashedKey.c_str(), base64hash.c_str()); + debug_e("WS: key mismatch: %s | %s", serverHashedKey.c_str(), base64hash.c_str()); state = eWSCS_Closed; - WebsocketConnection::getConnection()->setTimeOut(1); + connection.setTimeOut(1); return -3; } diff --git a/Sming/Components/Network/src/Network/WebsocketClient.h b/Sming/Components/Network/src/Network/WebsocketClient.h index 3952942e0f..db39285fd0 100644 --- a/Sming/Components/Network/src/Network/WebsocketClient.h +++ b/Sming/Components/Network/src/Network/WebsocketClient.h @@ -31,11 +31,7 @@ class WebsocketClient : protected WebsocketConnection { public: - WebsocketClient() : WebsocketConnection(new HttpClientConnection) - { - } - - WebsocketClient(HttpConnection* connection) : WebsocketConnection(connection) + WebsocketClient(HttpConnection* connection = nullptr) : WebsocketConnection(connection) { } diff --git a/Sming/Components/Network/tools/wsserver.py b/Sming/Components/Network/tools/wsserver.py new file mode 100644 index 0000000000..f0f5aee8bd --- /dev/null +++ b/Sming/Components/Network/tools/wsserver.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python + +import asyncio +from websockets.server import serve +import logging + +async def echo(websocket): + async for message in websocket: + await websocket.send(message) + +async def main(): + logging.basicConfig( + format="%(asctime)s %(message)s", + level=logging.DEBUG, + ) + async with serve(echo, None, 8000): + await asyncio.Future() # run forever + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/Sming/Core/Data/Stream/XorOutputStream.h b/Sming/Core/Data/Stream/XorOutputStream.h index 831a1df2a6..9ff7e1489e 100644 --- a/Sming/Core/Data/Stream/XorOutputStream.h +++ b/Sming/Core/Data/Stream/XorOutputStream.h @@ -28,8 +28,9 @@ class XorOutputStream : public IDataSourceStream * @param maskLenth */ XorOutputStream(IDataSourceStream* stream, uint8_t* mask, size_t maskLength) - : stream(stream), mask(mask), maskLength(maskLength) + : stream(stream), mask(new uint8_t[maskLength]), maskLength(maskLength) { + memcpy(this->mask.get(), mask, maskLength); } StreamType getStreamType() const override @@ -72,7 +73,7 @@ class XorOutputStream : public IDataSourceStream private: std::unique_ptr stream; - uint8_t* mask; + std::unique_ptr mask; size_t maskLength; size_t maskPos = 0; }; diff --git a/samples/Websocket_Client/README.rst b/samples/Websocket_Client/README.rst index 141fe2fa58..86335d0938 100644 --- a/samples/Websocket_Client/README.rst +++ b/samples/Websocket_Client/README.rst @@ -2,9 +2,17 @@ Websocket Client ================ This is a simple demo of the WebsocketClient class. +It shows connection, closing and reconnection methods of WebsocketClient. -The client tries to connect to *echo.websocket.org*. +The client tries to connect to a websocket echo server. It sents 10 messages then client connection is closed. -It reconnects and sends 25 messages and continues doing same. +This sequence repeats after 20 seconds. -This demo shows connection, closing and reconnection methods of WebsocketClient. +The sample was originally written to communicate with *echo.websocket.org* +but that service no longer exists. +Instead, run ``make wsserver`` to run a local test server. +This has the advantage of showing detailed diagnostic information which may be helpful. + +Build with ``WS_URL`` set to the server address. For example: + + make WS_URL=ws://192.168.1.10:8000 diff --git a/samples/Websocket_Client/app/application.cpp b/samples/Websocket_Client/app/application.cpp index 7b80fb4974..d2f075b278 100644 --- a/samples/Websocket_Client/app/application.cpp +++ b/samples/Websocket_Client/app/application.cpp @@ -20,12 +20,13 @@ //Uncomment next line to enable websocket binary transfer test //#define WS_BINARY +namespace +{ WebsocketClient wsClient; Timer msgTimer; -Timer restartTimer; // Number of messages to send -const unsigned MESSAGES_TO_SEND = 10; +const unsigned MESSAGES_TO_SEND = 5; // Interval (in seconds) between sending of messages const unsigned MESSAGE_INTERVAL = 1; @@ -33,21 +34,18 @@ const unsigned MESSAGE_INTERVAL = 1; // Time (in seconds) to wait before restarting client and sending another group of messages const unsigned RESTART_PERIOD = 20; -unsigned msg_cnt = 0; +unsigned messageCount; +bool sendBinary; -#ifdef ENABLE_SSL -DEFINE_FSTR_LOCAL(ws_Url, "wss://echo.websocket.org"); -#else -DEFINE_FSTR_LOCAL(ws_Url, "ws://echo.websocket.org"); -#endif /* ENABLE_SSL */ +DEFINE_FSTR_LOCAL(ws_Url, WS_URL); -void wsMessageSent(); +void sendNewMessage(); void wsConnected(WebsocketConnection& wsConnection) { Serial << _F("Start sending messages every ") << MESSAGE_INTERVAL << _F(" second(s)...") << endl; - msgTimer.initializeMs(MESSAGE_INTERVAL * 1000, wsMessageSent); - msgTimer.start(); + msgTimer.initializeMs(MESSAGE_INTERVAL * 1000, sendNewMessage); + msgTimer.startOnce(); } void wsMessageReceived(WebsocketConnection& wsConnection, const String& message) @@ -58,11 +56,7 @@ void wsMessageReceived(WebsocketConnection& wsConnection, const String& message) void wsBinReceived(WebsocketConnection& wsConnection, uint8_t* data, size_t size) { - Serial.println(_F("WebSocket BINARY received")); - for(uint8_t i = 0; i < size; i++) { - Serial << "wsBin[" << i << "] = 0x" << String(data[i], HEX, 2) << endl; - } - + m_printHex(_F("WebSocket BINARY received"), data, size); Serial << _F("Free Heap: ") << system_get_free_heap_size() << endl; } @@ -70,47 +64,50 @@ void restart() { Serial.println(_F("restart...")); - msg_cnt = 0; + messageCount = 0; + sendBinary = false; wsClient.connect(String(ws_Url)); } void wsDisconnected(WebsocketConnection& wsConnection) { Serial << _F("Restarting websocket client after ") << RESTART_PERIOD << _F(" seconds") << endl; - msgTimer.setCallback(restart); - msgTimer.setIntervalMs(RESTART_PERIOD * 1000); + msgTimer.initializeMs(RESTART_PERIOD * 1000, restart); msgTimer.startOnce(); } -void wsMessageSent() +void sendNewMessage() { if(!WifiStation.isConnected()) { // Check if Esp8266 is connected to router return; } - if(msg_cnt > MESSAGES_TO_SEND) { - Serial.println(_F("End Websocket client session")); - msgTimer.stop(); - wsClient.close(); // clean disconnect. + if(messageCount >= MESSAGES_TO_SEND) { + if(sendBinary) { + Serial.println(_F("End Websocket client session")); + wsClient.close(); // clean disconnect. + return; + } - return; + messageCount = 0; + sendBinary = true; } -#ifndef WS_BINARY - String message = F("Hello ") + String(msg_cnt++); - Serial << _F("Sending websocket message: ") << message << endl; - wsClient.sendString(message); -#else - uint8_t buf[] = {0xF0, 0x00, 0xF0}; - buf[1] = msg_cnt++; - Serial.println(_F("Sending websocket binary buffer")); - for(uint8_t i = 0; i < 3; i++) { - Serial << "wsBin[" << i << "] = 0x" << String(buf[i], HEX, 2) << endl; + if(sendBinary) { + uint8_t buf[10]; + os_get_random(buf, sizeof(buf)); + buf[1] = messageCount; + m_printHex(_F("Sending websocket binary"), buf, sizeof(buf)); + wsClient.sendBinary(buf, sizeof(buf)); + } else { + String message = F("Hello ") + String(messageCount); + Serial << _F("Sending websocket message: ") << message << endl; + wsClient.sendString(message); } - wsClient.sendBinary(buf, 3); -#endif + ++messageCount; + msgTimer.startOnce(); } void STAGotIP(IpAddress ip, IpAddress mask, IpAddress gateway) @@ -133,6 +130,8 @@ void STADisconnect(const String& ssid, MacAddress bssid, WifiDisconnectReason re << endl; } +} // namespace + void init() { Serial.begin(COM_SPEED_SERIAL); diff --git a/samples/Websocket_Client/component.mk b/samples/Websocket_Client/component.mk index 27468497da..619b65de9b 100644 --- a/samples/Websocket_Client/component.mk +++ b/samples/Websocket_Client/component.mk @@ -1,2 +1,9 @@ # Uncomment the option below if you want SSL support #ENABLE_SSL=1 + +CONFIG_VARS += WS_URL + +# Use wss: for secure connection +WS_URL ?= ws://127.0.0.1:8000 + +COMPONENT_CXXFLAGS += -DWS_URL=\"$(WS_URL)\" diff --git a/samples/Websocket_Client/test.py b/samples/Websocket_Client/test.py new file mode 100644 index 0000000000..429b47bcd4 --- /dev/null +++ b/samples/Websocket_Client/test.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python +# +# Test python application to send test message to server +# +# Shows connection detail so we can compare it to Sming code +# + +import argparse +import logging +import asyncio +from websockets.sync.client import connect + +def main(): + parser = argparse.ArgumentParser(description='Simple websocket client test') + parser.add_argument('URL', help='Connection URL', default='ws://192.168.13.10/ws') + + args = parser.parse_args() + + logging.basicConfig( + format="%(asctime)s %(message)s", + level=logging.DEBUG, + ) + print(f'Connecting to {args.URL}...') + with connect(args.URL) as websocket: + websocket.send("Hello world!") + websocket.recv() + websocket.recv() + +if __name__ == "__main__": + main() From 8843f26a5d4b1a8e0d4447a0808d9f182d7cdccd Mon Sep 17 00:00:00 2001 From: Mike Date: Fri, 5 Apr 2024 12:34:30 +0100 Subject: [PATCH 041/128] Improve sample applications (#2756) This PR updates all the main Sming samples to try and demonstrate best practice when coding Sming applications. Specifically: - Initialise timers using templated methods where possible as this provides static range check on values - Using `SimpleTimer` is sufficient in most cases. This is both more efficient and avoids complication where compiler cannot distinguish between delegates and callback functions. - Use timer `startOnce()` method rather than `start(false)` - Use C++ iterators in preference to C-style loops `for(int i=0; i + * + * This file is part of the Sming Framework Project + * + * This library is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, version 3 or later. + * + * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with SHEM. + * If not, see . + * + ****/ + +#pragma once + +#include + +namespace Host +{ +class OutputStream : public Print +{ +public: + OutputStream(int fileno) : fileno(fileno) + { + } + + virtual size_t write(uint8_t c) override + { + return write(&c, 1); + } + + size_t write(const uint8_t* buffer, size_t size) override + { + return ::write(fileno, buffer, size); + } + +private: + int fileno; +}; + +OutputStream standardOutput(STDOUT_FILENO); +OutputStream standardError(STDERR_FILENO); + +}; // namespace Host diff --git a/Sming/Components/Storage/src/include/Storage/SysMem.h b/Sming/Components/Storage/src/include/Storage/SysMem.h index d2d333f1d2..7b6f20361d 100644 --- a/Sming/Components/Storage/src/include/Storage/SysMem.h +++ b/Sming/Components/Storage/src/include/Storage/SysMem.h @@ -73,6 +73,8 @@ class SysMem : public Device class SysMemPartitionTable : public PartitionTable { public: + using PartitionTable::add; + /** * @brief Add partition entry for FlashString data access */ diff --git a/Sming/Libraries/BLEGamepad/samples/Bluetooth_Gamepad/app/application.cpp b/Sming/Libraries/BLEGamepad/samples/Bluetooth_Gamepad/app/application.cpp index 6fa4659ec5..84dc8a3719 100644 --- a/Sming/Libraries/BLEGamepad/samples/Bluetooth_Gamepad/app/application.cpp +++ b/Sming/Libraries/BLEGamepad/samples/Bluetooth_Gamepad/app/application.cpp @@ -40,5 +40,5 @@ void init() // Auto reporting is enabled by default. // Use bleGamepad.setAutoReport(false); to disable auto reporting, and then use bleGamepad.sendReport(); as needed - procTimer.initializeMs(500, loop).start(); + procTimer.initializeMs<500>(loop).start(); } diff --git a/Sming/Libraries/BLEKeyboard/samples/Bluetooth_Keyboard/app/application.cpp b/Sming/Libraries/BLEKeyboard/samples/Bluetooth_Keyboard/app/application.cpp index bc6d959e34..5ecbd571ba 100644 --- a/Sming/Libraries/BLEKeyboard/samples/Bluetooth_Keyboard/app/application.cpp +++ b/Sming/Libraries/BLEKeyboard/samples/Bluetooth_Keyboard/app/application.cpp @@ -44,5 +44,5 @@ void init() Serial.println("Starting BLE Keyboard sample!"); bleKeyboard.begin(); - procTimer.initializeMs(1000, loop).start(); + procTimer.initializeMs<1000>(loop).start(); } diff --git a/Sming/Libraries/CS5460/samples/generic/app/application.cpp b/Sming/Libraries/CS5460/samples/generic/app/application.cpp index ea0bec20a1..31e0983b93 100644 --- a/Sming/Libraries/CS5460/samples/generic/app/application.cpp +++ b/Sming/Libraries/CS5460/samples/generic/app/application.cpp @@ -1,30 +1,31 @@ #include #include +namespace +{ CS5460 powerMeter(PIN_NDEFINED, PIN_NDEFINED, PIN_NDEFINED, PIN_NDEFINED); - -Timer printVoltageTimer; +SimpleTimer printVoltageTimer; void printVoltage() { - debugf("Measured RMS voltage is: %f", powerMeter.getRMSVoltage()); + Serial << _F("Measured RMS voltage is: ") << powerMeter.getRMSVoltage() << endl; } +} // namespace + void init() { - Serial.begin(SERIAL_BAUD_RATE, SERIAL_8N1, - SERIAL_FULL); // 115200 by default, GPIO1,GPIO3, see Serial.swap(), HardwareSerial + Serial.begin(SERIAL_BAUD_RATE); Serial.systemDebugOutput(true); powerMeter.init(); - powerMeter.setCurrentGain(190.84); //0.25 / shunt (0.00131) - powerMeter.setVoltageGain(500); //0.25V (Veff max) * dividerGain - uint32_t conf = 0; - conf = powerMeter.readRegister(CONFIG_REGISTER); - conf |= ENABLE_VOLTAGE_HPF; - conf |= ENABLE_CURRENT_HPF; + powerMeter.setCurrentGain(190.84); // 0.25 / shunt (0.00131) + powerMeter.setVoltageGain(500); // 0.25V (Veff max) * dividerGain + + uint32_t conf = powerMeter.readRegister(CONFIG_REGISTER); + conf |= ENABLE_VOLTAGE_HPF | ENABLE_CURRENT_HPF; powerMeter.writeRegister(CONFIG_REGISTER, conf); powerMeter.startMultiConvert(); - printVoltageTimer.initializeMs(1000, printVoltage).start(); + printVoltageTimer.initializeMs<1000>(printVoltage).start(); } diff --git a/Sming/Libraries/CommandProcessing/samples/TelnetServer/app/application.cpp b/Sming/Libraries/CommandProcessing/samples/TelnetServer/app/application.cpp index 8dc8e9be27..6d2862b5e4 100644 --- a/Sming/Libraries/CommandProcessing/samples/TelnetServer/app/application.cpp +++ b/Sming/Libraries/CommandProcessing/samples/TelnetServer/app/application.cpp @@ -34,7 +34,7 @@ bool processTelnetInput(TcpClient& client, char* data, int size) char c = *data++; if(skip) { --skip; - } else if(c == '\xff') { + } else if(c == TC_ESC) { skip = 2; } else { commandHandler.process(c); diff --git a/Sming/Libraries/DS18S20/ds18s20.cpp b/Sming/Libraries/DS18S20/ds18s20.cpp index 6c9d7d3b6e..f0d5ab4dbd 100644 --- a/Sming/Libraries/DS18S20/ds18s20.cpp +++ b/Sming/Libraries/DS18S20/ds18s20.cpp @@ -51,7 +51,7 @@ void DS18S20::StartMeasure() InProgress=true; ds->begin(); ds->reset_search(); - DelaysTimer.initializeMs(150, TimerDelegate(&DS18S20::DoSearch, this)).start(false); + DelaysTimer.initializeMs<150>(TimerDelegate(&DS18S20::DoSearch, this)).start(false); } } @@ -146,7 +146,7 @@ void DS18S20::StartReadNext() ds->select(addr); ds->write(STARTCONVO, 1); // start conversion, with parasite power on at the end - DelaysTimer.initializeMs(900, TimerDelegate(&DS18S20::DoMeasure, this)).start(false); + DelaysTimer.initializeMs<900>(TimerDelegate(&DS18S20::DoMeasure, this)).start(false); } else { @@ -210,7 +210,7 @@ void DS18S20::DoMeasure() debugx(" DBG: Temperature = %f Celsius, %f Fahrenheit",celsius[numberOfread],fahrenheit[numberOfread]); numberOfread++; - DelaysTimer.initializeMs(100, TimerDelegate(&DS18S20::StartReadNext, this)).start(false); + DelaysTimer.initializeMs<100>(TimerDelegate(&DS18S20::StartReadNext, this)).start(false); } diff --git a/Sming/Libraries/ModbusMaster/samples/generic/app/application.cpp b/Sming/Libraries/ModbusMaster/samples/generic/app/application.cpp index ddefd7a32b..2a3800a694 100644 --- a/Sming/Libraries/ModbusMaster/samples/generic/app/application.cpp +++ b/Sming/Libraries/ModbusMaster/samples/generic/app/application.cpp @@ -6,26 +6,28 @@ #define MB_SLAVE_ADDR 1 #define SLAVE_REG_ADDR 1 -Timer mbLoopTimer; - +namespace +{ +SimpleTimer mbLoopTimer; ModbusMaster mbMaster; HardwareSerial modbusComPort(UART_ID_0); HardwareSerial debugComPort(UART_ID_1); -uint16_t globalSeconds = 0; +uint16_t globalSeconds; void mbLoop() { globalSeconds++; - uint8_t numberOfRegistersToWrite = 1; - uint8_t bufferPosition = 0; + const uint8_t numberOfRegistersToWrite = 1; + const uint8_t bufferPosition = 0; mbMaster.begin(MB_SLAVE_ADDR, modbusComPort); mbMaster.setTransmitBuffer(bufferPosition, globalSeconds); mbMaster.writeMultipleRegisters(SLAVE_REG_ADDR, numberOfRegistersToWrite); - uint8_t nrOfRegistersToRead = 1; - uint8_t mbResult = mbMaster.readHoldingRegisters(SLAVE_REG_ADDR, nrOfRegistersToRead); //see also readInputRegisters + // see also readInputRegisters + const uint8_t nrOfRegistersToRead = 1; + uint8_t mbResult = mbMaster.readHoldingRegisters(SLAVE_REG_ADDR, nrOfRegistersToRead); if(mbResult == mbMaster.ku8MBSuccess) { /* @@ -34,9 +36,9 @@ void mbLoop() debugComPort.printf("Reg %d: %d\r\n", i, buffer[i]); } */ - debugf("Data from slave: %d", mbMaster.getResponseBuffer(0)); + debug_i("Data from slave: %d", mbMaster.getResponseBuffer(0)); } else { - debugf("Res err: %d", mbResult); + debug_i("Res err: %d", mbResult); } mbMaster.clearResponseBuffer(); @@ -57,37 +59,37 @@ void mbLogReceive(const uint8_t* adu, size_t aduSize, uint8_t status) if(status != mbMaster.ku8MBSuccess) { switch(status) { case mbMaster.ku8MBIllegalFunction: - debugf("MB Illegal Function"); + debug_i("MB Illegal Function"); break; case mbMaster.ku8MBIllegalDataAddress: - debugf("MB Illegal Address"); + debug_i("MB Illegal Address"); break; case mbMaster.ku8MBIllegalDataValue: - debugf("MB Illegal Data Value"); + debug_i("MB Illegal Data Value"); break; case mbMaster.ku8MBSlaveDeviceFailure: - debugf("MB Slave Device Failure"); + debug_i("MB Slave Device Failure"); break; case mbMaster.ku8MBInvalidSlaveID: - debugf("MB Invalid Slave ID"); + debug_i("MB Invalid Slave ID"); break; case mbMaster.ku8MBInvalidFunction: - debugf("MB Invalid function"); + debug_i("MB Invalid function"); break; case mbMaster.ku8MBResponseTimedOut: - debugf("MB Response Timeout"); + debug_i("MB Response Timeout"); break; case mbMaster.ku8MBInvalidCRC: - debugf("MB Invalid CRC"); + debug_i("MB Invalid CRC"); break; case mbMaster.ku8MBResponseTooLarge: - debugf("MB Response too large"); + debug_i("MB Response too large"); break; } - debugf("ADU Size: %d, status: %d ", aduSize, status); + debug_i("ADU Size: %d, status: %d ", aduSize, status); debug_hex(INFO, "RX ADU", adu, aduSize); } - debugf("\r\n"); + debug_i("\r\n"); } void mbLogTransmit(const uint8_t* adu, size_t aduSize) @@ -95,13 +97,14 @@ void mbLogTransmit(const uint8_t* adu, size_t aduSize) debug_hex(INFO, "TX ADU", adu, aduSize); } +} // namespace + void init() { pinMode(RS485_RE_PIN, OUTPUT); digitalWrite(RS485_RE_PIN, LOW); modbusComPort.begin(MODBUS_COM_SPEED, SERIAL_8N1, SERIAL_FULL); - debugComPort.begin(SERIAL_BAUD_RATE, SERIAL_8N1, - SERIAL_TX_ONLY); // 115200 by default, GPIO1,GPIO3, see Serial.swap(), HardwareSerial + debugComPort.begin(SERIAL_BAUD_RATE, SERIAL_8N1, SERIAL_TX_ONLY); debugComPort.systemDebugOutput(true); mbMaster.preTransmission(preTransmission); @@ -109,5 +112,5 @@ void init() mbMaster.logReceive(mbLogReceive); mbMaster.logTransmit(mbLogTransmit); - mbLoopTimer.initializeMs(1000, mbLoop).start(); + mbLoopTimer.initializeMs<1000>(mbLoop).start(); } diff --git a/Sming/Libraries/SwitchJoycon/samples/Bluetooth_Joycon/app/application.cpp b/Sming/Libraries/SwitchJoycon/samples/Bluetooth_Joycon/app/application.cpp index caa9ef8fe4..c9c38a6bf5 100644 --- a/Sming/Libraries/SwitchJoycon/samples/Bluetooth_Joycon/app/application.cpp +++ b/Sming/Libraries/SwitchJoycon/samples/Bluetooth_Joycon/app/application.cpp @@ -31,7 +31,7 @@ void onConnect(NimBLEServer& server) { Serial.println("Connected :) !"); - procTimer.initializeMs(500, loop).start(); + procTimer.initializeMs<500>(loop).start(); } void onDisconnect(NimBLEServer& server) diff --git a/Sming/Libraries/jerryscript b/Sming/Libraries/jerryscript index ac5f4ed3cf..c8f24895a5 160000 --- a/Sming/Libraries/jerryscript +++ b/Sming/Libraries/jerryscript @@ -1 +1 @@ -Subproject commit ac5f4ed3cf68b4f5877db890fa02929a128498c3 +Subproject commit c8f24895a59ff64e9bba212946dcf2dad504b27a diff --git a/Sming/Libraries/si4432/component.mk b/Sming/Libraries/si4432/component.mk deleted file mode 100644 index e243360e9c..0000000000 --- a/Sming/Libraries/si4432/component.mk +++ /dev/null @@ -1 +0,0 @@ -COMPONENT_SOC := esp8266 diff --git a/Sming/Libraries/si4432/si4432.cpp b/Sming/Libraries/si4432/si4432.cpp index 69499dfbad..27ea4c2118 100644 --- a/Sming/Libraries/si4432/si4432.cpp +++ b/Sming/Libraries/si4432/si4432.cpp @@ -354,16 +354,16 @@ void Si4432::BurstWrite(Registers startReg, const byte value[], uint8_t length) // _spi->enable(); _spi->beginTransaction(_spi->SPIDefaultSettings); delayMicroseconds(1); -// _spi->send(®Val, 1); - _spi->transfer(®Val, 1); + _spi->transfer(regVal); #if DEBUG_VERBOSE_SI4432 debugf("Writing: %x | %x ... %x (%d bytes)", (regVal != 0xFF ? (regVal) & 0x7F : 0x7F), value[0], value[length-1], length); #endif -// _spi->send(value, length); - _spi->transfer((uint8 *)value, length); + uint8_t buffer[length]; + memcpy(buffer, value, length); + _spi->transfer(buffer, length); // _spi->disable(); _spi->endTransaction(); @@ -376,13 +376,11 @@ void Si4432::BurstRead(Registers startReg, byte value[], uint8_t length) { // _spi->enable(); _spi->beginTransaction(_spi->SPIDefaultSettings); delayMicroseconds(1); -// _spi->send(®Val, 1); - _spi->transfer(®Val, 1); + _spi->transfer(regVal); // _spi->setMOSI(HIGH); /* Send 0xFF */ - _spi->transfer((uint8 *)0xFF, 1); -// _spi->recv(value, length); - _spi->transfer((uint8 *)value, length); + _spi->transfer(0xff); + _spi->transfer(value, length); #if DEBUG_VERBOSE_SI4432 debugf("Reading: %x | %x..%x (%d bytes)", (regVal != 0x7F ? (regVal) & 0x7F : 0x7F), diff --git a/samples/Accel_Gyro_MPU6050/app/application.cpp b/samples/Accel_Gyro_MPU6050/app/application.cpp index 0190f99c03..f060d104f2 100644 --- a/samples/Accel_Gyro_MPU6050/app/application.cpp +++ b/samples/Accel_Gyro_MPU6050/app/application.cpp @@ -18,7 +18,8 @@ void init() Wire.begin(DEFAULT_SDA_PIN, DEFAULT_SCL_PIN); mpu.initialize(); - Serial.println(mpu.testConnection() ? "MPU6050 connection successful" : "MPU6050 connection failed"); + bool success = mpu.testConnection(); + Serial << _F("MPU6050 connection ") << (success ? _F("successful") : _F("failed")) << endl; mainLoopTimer.initializeMs(mainLoop).start(); } diff --git a/samples/Accelerometer_MMA7455/app/application.cpp b/samples/Accelerometer_MMA7455/app/application.cpp index edff93d77a..2a02f7b75d 100644 --- a/samples/Accelerometer_MMA7455/app/application.cpp +++ b/samples/Accelerometer_MMA7455/app/application.cpp @@ -3,11 +3,11 @@ // For more information read: https://code.google.com/p/mma-7455-arduino-library/ MMA_7455 accel; -Timer procTimer; +SimpleTimer procTimer; void readSensor() { - Serial.println("Reading.."); + Serial.println(_F("Reading..")); int8_t x = accel.readAxis('x'); int8_t y = accel.readAxis('y'); @@ -29,5 +29,5 @@ void init() accel.initSensitivity(MMA_7455_2G_MODE); // Start reading loop - procTimer.initializeMs(300, readSensor).start(); + procTimer.initializeMs<300>(readSensor).start(); } diff --git a/samples/Basic_APA102/app/application.cpp b/samples/Basic_APA102/app/application.cpp index 9297b41639..0558482bc3 100644 --- a/samples/Basic_APA102/app/application.cpp +++ b/samples/Basic_APA102/app/application.cpp @@ -23,7 +23,9 @@ #define SPI_CS 2 -Timer procTimer; +namespace +{ +SimpleTimer procTimer; // in this demo, the same ports for HW and SW SPI are used #ifdef _USE_SOFTSPI @@ -34,12 +36,12 @@ APA102 LED(NUM_LED); // APA102 constructor, call with number of LEDs //APA102 LED(NUM_LED, SPI); #endif -static SPISettings SPI_1MHZ = SPISettings(1000000, MSBFIRST, SPI_MODE3); -static SPISettings SPI_2MHZ = SPISettings(2000000, MSBFIRST, SPI_MODE3); +SPISettings SPI_1MHZ{1000000, MSBFIRST, SPI_MODE3}; +SPISettings SPI_2MHZ{2000000, MSBFIRST, SPI_MODE3}; /* color wheel function: * (simple) three 120° shifted colors -> color transitions r-g-b-r */ -static col_t colorWheel(uint16_t step, uint16_t numStep) +col_t colorWheel(uint16_t step, uint16_t numStep) { col_t col = {0}; col.br = 10; @@ -63,10 +65,10 @@ static col_t colorWheel(uint16_t step, uint16_t numStep) return col; } -static void updateLED() +void updateLED() { - static unsigned state = 0; - static unsigned cnt = 0; + static unsigned state; + static unsigned cnt; switch(state++) { case 0: @@ -113,6 +115,8 @@ static void updateLED() } } +} // namespace + void init() { Serial.begin(SERIAL_BAUD_RATE); diff --git a/samples/Basic_AWS/app/application.cpp b/samples/Basic_AWS/app/application.cpp index 03c637a0ab..7f2edde2f2 100644 --- a/samples/Basic_AWS/app/application.cpp +++ b/samples/Basic_AWS/app/application.cpp @@ -26,11 +26,11 @@ void startMqttClient() Url url; url.Scheme = URI_SCHEME_MQTT_SECURE; url.Host = awsEndpoint; - mqtt.connect(url, "Basic_AWS"); + mqtt.connect(url, F("Basic_AWS")); // Assign a disconnect callback function // mqtt.setCompleteDelegate(checkMQTTDisconnect); - mqtt.subscribe("thing/fish/test"); + mqtt.subscribe(F("thing/fish/test")); } // Publish our message @@ -41,8 +41,8 @@ void publishMessage() startMqttClient(); } - Serial.println("publish message"); - mqtt.publish("version", "ver.1.2"); + Serial.println(_F("publish message")); + mqtt.publish(F("version"), F("ver.1.2")); } // Callback for messages, arrived from MQTT server diff --git a/samples/Basic_Audio/app/application.cpp b/samples/Basic_Audio/app/application.cpp index 30c549a672..2d623b36a7 100644 --- a/samples/Basic_Audio/app/application.cpp +++ b/samples/Basic_Audio/app/application.cpp @@ -19,6 +19,8 @@ // If using an external DAC, set this to 0 - you'll probably need to change other settings as well #define ENABLE_DELTA_SIGMA +namespace +{ // Set this to 0 to output fixed values for easier checking on a scope or signal analyzer //#define GENERATE_FIXED_VALUES constexpr i2s_sample_t fixedSampleValue = {0xF55F0AA0}; @@ -30,17 +32,17 @@ constexpr unsigned targetSampleRate = 44100; constexpr float sineWaveFrequency = 440; // Measure time taken to fill the I2S DMA buffers -static Profiling::MicroTimes fillTime("Fill Time"); +Profiling::MicroTimes fillTime("Fill Time"); // Measure time taken between I2S callback (interrupt) and our task callback getting executed -static Profiling::MicroTimes callbackLatency("Callback latency"); +Profiling::MicroTimes callbackLatency("Callback latency"); // Report status periodically -static SimpleTimer statusTimer; -static constexpr unsigned statusIntervalMs = 5000; +SimpleTimer statusTimer; +constexpr unsigned statusIntervalMs = 5000; // One full sine-wave cycle -static struct { +struct SineWaveTable { std::unique_ptr samples; unsigned sampleCount = 0; unsigned readPos = 0; @@ -81,7 +83,24 @@ static struct { } return value; } -} sineWaveTable; +}; + +SineWaveTable sineWaveTable; + +#ifdef GENERATE_FIXED_VALUES + +void writeFixedValues() +{ + i2s_buffer_info_t info; + while(i2s_dma_write(&info, UINT_MAX)) { + memset(info.samples, 0, info.size); + // for(unsigned i = 0; i < info.size / sizeof(i2s_sample_t); ++i) { + // info.samples[i] = fixedSampleValue; + // } + } +} + +#else /* * Outputs a 172.266Hz sine wave (256 samples at 44100 samples/sec) @@ -105,16 +124,7 @@ void writeSine() } } -void writeFixedValues() -{ - i2s_buffer_info_t info; - while(i2s_dma_write(&info, UINT_MAX)) { - memset(info.samples, 0, info.size); - // for(unsigned i = 0; i < info.size / sizeof(i2s_sample_t); ++i) { - // info.samples[i] = fixedSampleValue; - // } - } -} +#endif // GENERATE_FIXED_VALUES void fillBuffers() { @@ -128,7 +138,7 @@ void fillBuffers() fillTime.update(); } -static void checkReceive() +void checkReceive() { unsigned total = 0; i2s_buffer_info_t info; @@ -154,7 +164,7 @@ void IRAM_ATTR i2sCallback(void* param, i2s_event_type_t event) } } -static void initialiseI2S() +void initialiseI2S() { i2s_config_t config; memset(&config, 0, sizeof(config)); @@ -231,6 +241,8 @@ static void initialiseI2S() i2s_start(); } +} // namespace + void init() { /* diff --git a/samples/Basic_Blink/app/application.cpp b/samples/Basic_Blink/app/application.cpp index 65422d0701..1856c6e5c0 100644 --- a/samples/Basic_Blink/app/application.cpp +++ b/samples/Basic_Blink/app/application.cpp @@ -6,7 +6,7 @@ #define LED_PIN 2 // GPIO2 #endif -Timer procTimer; +SimpleTimer procTimer; bool state = true; void blink() @@ -18,5 +18,5 @@ void blink() void init() { pinMode(LED_PIN, OUTPUT); - procTimer.initializeMs(1000, blink).start(); + procTimer.initializeMs<1000>(blink).start(); } diff --git a/samples/Basic_Capsense/app/application.cpp b/samples/Basic_Capsense/app/application.cpp index 8fcab1a935..8d34b8a55a 100644 --- a/samples/Basic_Capsense/app/application.cpp +++ b/samples/Basic_Capsense/app/application.cpp @@ -7,17 +7,24 @@ // clock speed on the ESP means we need a higher charge current than arduino ?? // Further investigation required. -CapacitiveSensor cs_0_2 = CapacitiveSensor(0, 2); //Send pin 0, Receive Pin 2. +#define PIN_SEND 0 +#define PIN_RECEIVE 2 +#define SAMPLES_TO_READ 30 -Timer procTimer; +namespace +{ +CapacitiveSensor cs_0_2(PIN_SEND, PIN_RECEIVE); +SimpleTimer procTimer; void capsense() { - long total = cs_0_2.capacitiveSensor(30); //Read sensor with 30 samples - Serial << _F("Sense Value: ") << total << endl; // print sensor output + long total = cs_0_2.capacitiveSensor(SAMPLES_TO_READ); + Serial << _F("Sense Value: ") << total << endl; } +} // namespace + void init() { - procTimer.initializeMs(100, capsense).start(); + procTimer.initializeMs<100>(capsense).start(); } diff --git a/samples/Basic_DateTime/app/application.cpp b/samples/Basic_DateTime/app/application.cpp index c941d5354e..2de8e3968d 100644 --- a/samples/Basic_DateTime/app/application.cpp +++ b/samples/Basic_DateTime/app/application.cpp @@ -5,107 +5,117 @@ Prints each type of DateTime::format option */ #include +#include -static time_t timestamp = 0; -static size_t tsLength = 0; - +namespace +{ DEFINE_FSTR_LOCAL(commandPrompt, "Enter Unix timestamp: "); -void showTime(time_t timestamp) +void showTime(DateTime dt) { - DateTime dt(timestamp); - //Non-time - Serial.println(dt.format("%%%% Percent sign: %%")); - Serial.println(dt.format("%%n New-line character: %n")); - Serial.println(dt.format("%%t Horizontal-tab character: >|%t|<")); - //Year - Serial.println(dt.format("%%Y Full year (YYYY): %Y")); - Serial.println(dt.format("%%C Year, first two digits (00-99)%: %C")); - Serial.println(dt.format("%%y Year, last two digits (00-99): %y")); - //Month - Serial.println(dt.format("%%B Full month name (e.g. June): %B")); - Serial.println(dt.format("%%b Abbreviated month name (e.g. Jun): %b")); - Serial.println(dt.format("%%h Abbreviated month name (e.g. Jun): %h")); - Serial.println(dt.format("%%m Month as a decimal number (01-12): %m")); - //Week - Serial.println(dt.format("%%U Week number with the first Sunday as the first day of week one (00-53): %U")); //NYI - Serial.println(dt.format("%%W Week number with the first Monday as the first day of week one (00-53): %W")); //NYI - Serial.println(dt.format("%%V ISO 8601 week number (01-53): %V")); //NYI - //Day - Serial.println(dt.format("%%j Day of the year (001-366): %j")); - Serial.println(dt.format("%%d Day of the month, zero-padded (01-31)%: %d")); - Serial.println(dt.format("%%e Day of the month, space-padded ( 1-31): %e")); - Serial.println(dt.format("%%A Full weekday name (e.g. Monday): %A")); - Serial.println(dt.format("%%a Abbreviated weekday name (e.g. Mon): %a")); - Serial.println(dt.format("%%w Weekday as a decimal number with Sunday as 0 (0-6): %w")); - Serial.println(dt.format("%%u ISO 8601 weekday as number with Monday as 1 (1-7): %u")); - //Hour - Serial.println(dt.format("%%p Meridiem (AM|PM): %p")); - Serial.println(dt.format("%%H Hour in 24h format (00-23): %H")); - Serial.println(dt.format("%%h Hour in 12h format (01-12): %I")); - //Minute - Serial.println(dt.format("%%M Minute (00-59): %M")); - //Second - Serial.println(dt.format("%%S Second (00-61): %S")); - //Formatted strings - Serial.println(dt.format("%%R 24-hour time (HH:MM): %R")); - Serial.println(dt.format("%%r 12-hour time (hh:MM:SS AM): %r")); - Serial.println(dt.format("%%c Locale date and time: %c")); - Serial.println(dt.format("%%D US short date (MM/DD/YY): %D")); - Serial.println(dt.format("%%F ISO 8601 date (YYYY-MM-DD): %F")); - Serial.println(dt.format("%%T ISO 8601 time (HH:MM:SS): %T")); - Serial.println(dt.format("%%x Locale date: %x")); - Serial.println(dt.format("%%X Locale time: %X")); - //HTTP date - Serial << "toHTTPDate: " << dt.toHTTPDate() << endl; + auto printFormat = [&dt](const String& fmt, const String& msg) -> void { + Serial << fmt << ' ' << msg << ": " << dt.format(fmt) << endl; + }; + + // Non-time + printFormat("%%", F("Percent sign")); + printFormat("%n", F("New-line character")); + printFormat("|<", F("Horizontal-tab character: >|")); + // Year + printFormat("%Y", F("Full year (YYYY)")); + printFormat("%C", F("Year, first two digits (00-99)")); + printFormat("%y", F("Year, last two digits (00-99)")); + // Month + printFormat("%B", F("Full month name (e.g. June)")); + printFormat("%b", F("Abbreviated month name (e.g. Jun)")); + printFormat("%h", F("Abbreviated month name (e.g. Jun)")); + printFormat("%m", F("Month as a decimal number (01-12)")); + // Week + printFormat("%U", F("Week number with the first Sunday as the first day of week one (00-53)")); + printFormat("%W", F("Week number with the first Monday as the first day of week one (00-53)")); + printFormat("%V", F("ISO 8601 week number (01-53)")); + // Day + printFormat("%j", F("Day of the year (001-366)")); + printFormat("%d", F("Day of the month, zero-padded (01-31)")); + printFormat("%e", F("Day of the month, space-padded ( 1-31)")); + printFormat("%A", F("Full weekday name (e.g. Monday)")); + printFormat("%a", F("Abbreviated weekday name (e.g. Mon)")); + printFormat("%w", F("Weekday as a decimal number with Sunday as 0 (0-6)")); + printFormat("%u", F("ISO 8601 weekday as number with Monday as 1 (1-7)")); + // Hour + printFormat("%p", F("Meridiem (AM|PM)")); + printFormat("%H", F("Hour in 24h format (00-23)")); + printFormat("%I", F("Hour in 12h format (01-12)")); + // Minute + printFormat("%M", F("Minute (00-59)")); + // Second + printFormat("%S", F("Second (00-61)")); + // Formatted strings + printFormat("%R", F("24-hour time (HH:MM)")); + printFormat("%r", F("12-hour time (hh:MM:SS AM)")); + printFormat("%c", F("Locale date and time")); + printFormat("%D", F("US short date (MM/DD/YY)")); + printFormat("%F", F("ISO 8601 date (YYYY-MM-DD)")); + printFormat("%T", F("ISO 8601 time (HH:MM:SS)")); + printFormat("%x", F("Locale date")); + printFormat("%X", F("Locale time")); + + auto print = [](const String& tag, const String& value) { Serial << tag << ": " << value << endl; }; + + // HTTP date + print(F("toHTTPDate"), dt.toHTTPDate()); DateTime dt2; dt2.fromHttpDate(dt.toHTTPDate()); - Serial << "fromHTTPDate: " << dt2.toHTTPDate() << endl; - Serial << "toFullDateTimeString: " << dt.toFullDateTimeString() << endl; - Serial << "toISO8601: " << dt.toISO8601() << endl; - Serial << "toShortDateString: " << dt.toShortDateString() << endl; - Serial << "toShortTimeString: " << dt.toShortTimeString() << endl; + print(F("fromHTTPDate"), dt2.toHTTPDate()); + print(F("toFullDateTimeString"), dt.toFullDateTimeString()); + print(F("toISO8601"), dt.toISO8601()); + print(F("toShortDateString"), dt.toShortDateString()); + print(F("toShortTimeString"), dt.toShortTimeString()); } void onRx(Stream& source, char arrivedChar, unsigned short availableCharsCount) { - switch(arrivedChar) { - case '\n': - Serial.println(); - Serial.println(); - Serial << _F("****Showing DateTime formatting options for Unix timestamp: ") << timestamp << endl; - showTime(timestamp); - Serial.print(commandPrompt); - timestamp = 0; - tsLength = 0; - break; - case '0' ... '9': - timestamp *= 10; - timestamp += arrivedChar - '0'; - ++tsLength; - Serial.print(arrivedChar); - break; - case '\b': - if(tsLength) { - Serial.print('\b'); - Serial.print(" "); - Serial.print('\b'); - --tsLength; - timestamp /= 10; + static LineBuffer<32> buffer; + + switch(buffer.process(source, Serial)) { + case buffer.Action::submit: { + if(!buffer) { + break; + } + String s(buffer); + buffer.clear(); + char* p; + time_t timestamp = strtoul(s.c_str(), &p, 0); + if(p == s.end()) { + Serial << endl + << _F("****Showing DateTime formatting options for Unix timestamp: ") << uint32_t(timestamp) << endl; + showTime(timestamp); + break; } + + DateTime dt; + if(dt.fromHttpDate(s)) { + Serial << endl << _F("****Showing DateTime formatting options for Http time: ") << s << endl; + showTime(dt); + break; + } + + Serial << endl << _F("Please enter a valid numeric timestamp, or HTTP time string!") << endl; break; - case 27: - timestamp = 0; - tsLength = 0; - Serial.print('\r'); - Serial.print(_F(" ")); - Serial.print('\r'); - Serial.print(commandPrompt); + } + + case buffer.Action::clear: break; + + default:; + return; } - m_puts("\r\n"); + + Serial.print(commandPrompt); } +} // namespace + void init() { Serial.begin(COM_SPEED_SERIAL); diff --git a/samples/Basic_DateTime/component.mk b/samples/Basic_DateTime/component.mk index 250bb9a77d..5c4d4e7499 100644 --- a/samples/Basic_DateTime/component.mk +++ b/samples/Basic_DateTime/component.mk @@ -1,3 +1 @@ -# Emulate UART 0 -ENABLE_HOST_UARTID := 0 DISABLE_NETWORK := 1 diff --git a/samples/Basic_HwPWM/app/application.cpp b/samples/Basic_HwPWM/app/application.cpp index e04e7d050a..b1c29c4784 100644 --- a/samples/Basic_HwPWM/app/application.cpp +++ b/samples/Basic_HwPWM/app/application.cpp @@ -18,34 +18,46 @@ #include #include -uint8_t pins[8] = {4, 5, 0, 2, 15, 13, 12, 14}; // List of pins that you want to connect to pwm -HardwarePWM HW_pwm(pins, 8); +#define LED_PIN 2 -Timer procTimer; -int32 i = 0; -bool countUp = true; +namespace +{ +// List of pins that you want to connect to pwm +uint8_t pins[]{ + LED_PIN, 4, 5, 0, 15, 13, 12, 14, +}; +HardwarePWM HW_pwm(pins, ARRAY_SIZE(pins)); + +SimpleTimer procTimer; -int maxDuty = HW_pwm.getMaxDuty(); -int32 inc = maxDuty / 50; +const int maxDuty = HW_pwm.getMaxDuty(); void doPWM() { - if(countUp == true) { - i += inc; - if(i >= maxDuty) { - i = maxDuty; + static bool countUp = true; + static int duty; + + const int increment = maxDuty / 50; + + if(countUp) { + duty += increment; + if(duty >= maxDuty) { + duty = maxDuty; countUp = false; } } else { - i -= inc; - if(i <= 0) { - i = 0; + duty -= increment; + if(duty <= 0) { + duty = 0; countUp = true; } } - HW_pwm.analogWrite(2, i); + + HW_pwm.analogWrite(LED_PIN, duty); } +} // namespace + void init() { Serial.begin(SERIAL_BAUD_RATE); // 115200 by default @@ -67,7 +79,7 @@ void init() HW_pwm.analogWrite(12, 2 * maxDuty / 3); HW_pwm.analogWrite(14, maxDuty); - debugf("PWM output set on all 8 Pins. Kindly check..."); - debugf("Now Pin 2 will go from 0 to VCC to 0 in cycles."); - procTimer.initializeMs(100, doPWM).start(); + Serial.println(_F("PWM output set on all 8 Pins. Kindly check...\r\n" + "Now LED_PIN will go from 0 to VCC to 0 in cycles.")); + procTimer.initializeMs<100>(doPWM).start(); } diff --git a/samples/Basic_HwPWM/component.mk b/samples/Basic_HwPWM/component.mk index ec78f78162..be6ff9c63a 100644 --- a/samples/Basic_HwPWM/component.mk +++ b/samples/Basic_HwPWM/component.mk @@ -1,4 +1,5 @@ COMPONENT_SOC := esp8266 +DISABLE_NETWORK := 1 # Uncomment the line below if you want to use Espressif's PWM library. #ENABLE_CUSTOM_PWM=0 diff --git a/samples/Basic_IFS/app/application.cpp b/samples/Basic_IFS/app/application.cpp index 5bdfc2787a..c3bd209ed3 100644 --- a/samples/Basic_IFS/app/application.cpp +++ b/samples/Basic_IFS/app/application.cpp @@ -54,6 +54,8 @@ IMPORT_FSTR(listing_json, PROJECT_DIR "/resource/listing.json") HttpServer server; FtpServer ftp; int requestCount; +Profiling::TaskStat taskStat(Serial); +SimpleTimer statTimer; /* * Handle any custom fields here @@ -465,9 +467,6 @@ void fstest() listAttributes(); } -Profiling::TaskStat taskStat(Serial); -Timer statTimer; - } // namespace void init() @@ -489,6 +488,6 @@ void init() WifiEvents.onStationGotIP(gotIP); - statTimer.initializeMs<2000>(InterruptCallback([]() { taskStat.update(); })); + statTimer.initializeMs<2000>([]() { taskStat.update(); }); statTimer.start(); } diff --git a/samples/Basic_NFC/app/application.cpp b/samples/Basic_NFC/app/application.cpp index 9d532ec4bd..1ecaba08bc 100644 --- a/samples/Basic_NFC/app/application.cpp +++ b/samples/Basic_NFC/app/application.cpp @@ -1,69 +1,56 @@ #include #include -Timer procTimer; -static Timer nfcScanTimer; -int helloCounter = 0; +#define SS_PIN 4 // D2 + +namespace +{ +SimpleTimer procTimer; -void scanNfc(byte scanner); +MFRC522 mfrc522(SS_PIN, SS_PIN); -#define SS_PIN 4 // D2 +// List of pins where NFC devices may be connected +const uint8_t ss_pin[]{4, 15}; -MFRC522 mfrc522(SS_PIN, SS_PIN); // Create MFRC522 instance -byte ss_pin[] = {4, 15}; +void scanNfc(uint8_t scanner); void sayHello() { - for(int pinNdx = 0; pinNdx < 2; pinNdx++) { - byte pin = ss_pin[pinNdx]; + for(auto pin : ss_pin) { mfrc522.setControlPins(pin, pin); - mfrc522.PCD_Init(); // Init MFRC522 + mfrc522.PCD_Init(); scanNfc(pin); } } -//--------------------------------- -static void dump_byte_array(byte* buffer, byte bufferSize) -{ - String hexOut; - for(byte i = 0; i < bufferSize; i++) { - hexOut += String(buffer[i], HEX); - } - debugf("%s", hexOut.c_str()); -} -//--------------------------------- -void scanNfc(byte scanner) + +void scanNfc(uint8_t scannerPin) { if(!mfrc522.PICC_IsNewCardPresent()) { - debugf("Scanning nfc Scanner:%d \r\n", scanner); + Serial << _F("Scanning NFC pin #") << scannerPin << _F(" not present") << endl; return; } - if(!mfrc522.PICC_ReadCardSerial()) { // Select one of the cards - debugf("Selecting card failed..."); + // Select one of the cards + if(!mfrc522.PICC_ReadCardSerial()) { + Serial << _F("Selecting card #") << scannerPin << _F(" failed...") << endl; } else { // Show some details of the PICC (that is: the tag/card) - debugf("Card UID on scanner:%d:", scanner); - dump_byte_array(mfrc522.uid.uidByte, mfrc522.uid.size); - debugf(); + Serial << _F("Card UID on scanner: ") << scannerPin << endl; + m_printHex("UID", mfrc522.uid.uidByte, mfrc522.uid.size); } + mfrc522.PICC_HaltA(); + // Stop encryption on PCD mfrc522.PCD_StopCrypto1(); - mfrc522.PCD_Init(); // Init MFRC522 - - //nfcScanTimer.restart(); + mfrc522.PCD_Init(); } +} // namespace + void init() { - Serial.begin(SERIAL_BAUD_RATE); // 115200 by default - - procTimer.initializeMs(2000, sayHello).start(); - - //----- NFC - MFRC522 mfrc522(SS_PIN, SS_PIN); - SPI.begin(); - mfrc522.PCD_Init(); // Init MFRC522 + Serial.begin(SERIAL_BAUD_RATE); - //nfcScanTimer.initializeMs(50, scanNfc).startOnce(); + procTimer.initializeMs<2000>(sayHello).start(); } diff --git a/samples/Basic_Neopixel/app/application.cpp b/samples/Basic_Neopixel/app/application.cpp index 82a6a11f3f..86f5a1b6b8 100644 --- a/samples/Basic_Neopixel/app/application.cpp +++ b/samples/Basic_Neopixel/app/application.cpp @@ -13,71 +13,96 @@ // How many NeoPixels are attached to the Esp8266? #define NUMPIXELS 16 -Adafruit_NeoPixel strip = Adafruit_NeoPixel(NUMPIXELS, PIN, NEO_GRB + NEO_KHZ800); - -void StartDemo(void); - -Timer StripDemoTimer; -Timer ColorWipeTimer; -Timer TheaterChaseTimer; - -int StripDemoType = 0; -int StripColor = 0; -int StripNo = 0; -int ChaseCycle = 0; -int TheaterChaseQ = 0; +namespace +{ +Adafruit_NeoPixel strip(NUMPIXELS, PIN, NEO_GRB + NEO_KHZ800); + +void startDemo(); + +SimpleTimer timer; +uint32_t stripColor; +uint8_t stripDemoType; // Defined by demoData below +uint8_t chaseCycle; + +enum ColorMap { + CLR_BLACK = 0x000000, + CLR_RED = 0xff0000, + CLR_GREEN = 0x00ff00, + CLR_BLUE = 0x0000ff, + CLR_DIM_WHITE = 0x7f7f7f, + CLR_DIM_RED = 0x7f0000, + CLR_DIM_BLUE = 0x00007f, +}; + +struct DemoData { + uint32_t color; + uint16_t stepMilliseconds; + uint8_t chase; +}; + +const DemoData demoData[]{ + {CLR_BLACK, 50, 0}, // 0 + {CLR_RED, 50, 0}, // 1 + {CLR_GREEN, 100, 0}, // 2 + {CLR_BLUE, 150, 0}, // 3 + {CLR_BLACK, 50, 0}, // 4 + {CLR_DIM_WHITE, 50, 10}, // 5 + {CLR_DIM_RED, 60, 15}, // 6 + {CLR_DIM_BLUE, 70, 20}, // 7 +}; // // Fill the dots one after the other with a color +// Timer callback // - -void ColorWipe() +void colorWipe() { - if(StripNo < strip.numPixels()) { - strip.setPixelColor(StripNo, StripColor); - strip.show(); - StripNo++; - } else { - StripDemoType++; // next demo type - ColorWipeTimer.stop(); // stop this demo timer - StripDemoTimer.initializeMs(2000, StartDemo).start(true); // start next demo after 2 seconds + static uint16_t stripNumber; + + if(stripNumber >= strip.numPixels()) { + stripNumber = 0; + // start next demo after 2 seconds + stripDemoType++; + timer.initializeMs<2000>(startDemo).startOnce(); + return; } + + strip.setPixelColor(stripNumber, stripColor); + strip.show(); + ++stripNumber; } // -//Theatre-style crawling lights. -//Timer callback - -void TheaterChase() +// Theatre-style crawling lights +// Timer callback +// +void theatreChase() { - int i, b = 0; - if(ChaseCycle > 0) { - if(TheaterChaseQ == 0) - b = 3; - if(TheaterChaseQ == 1) - b = 0; - if(TheaterChaseQ == 2) - b = 1; - if(TheaterChaseQ == 3) - b = 2; - - for(i = 0; i < strip.numPixels(); i = i + 4) - strip.setPixelColor(i + b, 0); //turn prev every third pixel off - - for(i = 0; i < strip.numPixels(); i = i + 4) - strip.setPixelColor(i + TheaterChaseQ, StripColor); //turn every third pixel on - - strip.show(); - TheaterChaseQ++; - if(TheaterChaseQ > 3) { - TheaterChaseQ = 0; - ChaseCycle--; - } - } else { - // finish this demo - StripDemoType++; // next demo type - TheaterChaseTimer.stop(); // stop this demo dimer - StripDemoTimer.initializeMs(2000, StartDemo).start(true); // start another demo after 2 seconds + static uint8_t theatreChaseQ; + + if(chaseCycle <= 0) { + assert(theatreChaseQ == 0); + // start another demo after 2 seconds + stripDemoType++; + timer.initializeMs<2000>(startDemo).startOnce(); + return; + } + + const uint8_t offsetList[]{3, 0, 1, 2}; + int b = offsetList[theatreChaseQ]; + + // Turn every third pixel off + for(unsigned i = 0; i < strip.numPixels(); i += 4) { + strip.setPixelColor(i + b, 0); + strip.setPixelColor(i + theatreChaseQ, stripColor); + } + + strip.show(); + + ++theatreChaseQ; + if(theatreChaseQ > 3) { + theatreChaseQ = 0; + --chaseCycle; } } @@ -85,71 +110,38 @@ void TheaterChase() // Demo timer callback // -void StartDemo() +void startDemo() { - Serial << _F("NeoPixel Demo type: ") << StripDemoType << endl; - - StripDemoTimer.stop(); // next demo wait until this demo ends - - StripNo = 0; //start from led index 0 - TheaterChaseQ = 0; //another counter - - switch(StripDemoType) { // select demo type - case 0: - StripColor = strip.Color(0, 0, 0); // black - ColorWipeTimer.initializeMs(50, ColorWipe).start(true); // 50 ms step - break; - case 1: - StripColor = strip.Color(255, 0, 0); // Red - ColorWipeTimer.initializeMs(50, ColorWipe).start(true); // 50 ms step - break; - case 2: - StripColor = strip.Color(0, 255, 0); // Green - ColorWipeTimer.initializeMs(100, ColorWipe).start(true); // 100 ms step - break; - case 3: - StripColor = strip.Color(0, 0, 255); // Blue - ColorWipeTimer.initializeMs(150, ColorWipe).start(true); // 150 ms step - break; - case 4: - StripColor = strip.Color(0, 0, 0); // black - ColorWipeTimer.initializeMs(50, ColorWipe).start(true); // 50 ms step - break; - - case 5: - ChaseCycle = 10; //do 10 cycles of chasing - StripColor = strip.Color(127, 127, 127); // White - TheaterChaseTimer.initializeMs(50, TheaterChase).start(true); // 50 ms step - break; - case 6: - ChaseCycle = 15; //do 15 cycles of chasing - StripColor = strip.Color(127, 0, 0); // Red - TheaterChaseTimer.initializeMs(60, TheaterChase).start(true); // 60 ms step - break; - case 7: - ChaseCycle = 20; //do 20 cycles of chasing - StripColor = strip.Color(0, 0, 127); // Blue - TheaterChaseTimer.initializeMs(70, TheaterChase).start(true); // 70 ms step - break; - - default: - StripDemoType = 0; - StripDemoTimer.initializeMs(1000, StartDemo).start(true); //demo loop restart - break; + Serial << _F("NeoPixel Demo type: ") << stripDemoType << endl; + + if(stripDemoType >= ARRAY_SIZE(demoData)) { + // Demo loop restart + stripDemoType = 0; + timer.initializeMs<1000>(startDemo).start(); + return; } + + auto& data = demoData[stripDemoType]; + chaseCycle = data.chase; + stripColor = data.color; + timer.initializeMs(data.stepMilliseconds, chaseCycle ? theatreChase : colorWipe).start(); } -void got_IP(IpAddress ip, IpAddress netmask, IpAddress gateway) +#ifndef DISABLE_WIFI +void gotIp(IpAddress ip, IpAddress netmask, IpAddress gateway) { Serial << "IP: " << ip << endl; // You can put here other job like web,tcp etc. } // Will be called when WiFi station loses connection -void connect_Fail(const String& ssid, MacAddress bssid, WifiDisconnectReason reason) +void connectFail(const String& ssid, MacAddress bssid, WifiDisconnectReason reason) { Serial.println(_F("I'm NOT CONNECTED!")); } +#endif + +} // namespace void init() { @@ -165,13 +157,12 @@ void init() WifiStation.config(WIFI_SSID, WIFI_PWD); WifiStation.enable(true); WifiAccessPoint.enable(false); - WifiEvents.onStationDisconnect(connect_Fail); - WifiEvents.onStationGotIP(got_IP); + WifiEvents.onStationDisconnect(connectFail); + WifiEvents.onStationGotIP(gotIp); #endif - StripDemoType = 0; //demo index to be displayed - - strip.begin(); //init port + stripDemoType = 0; + strip.begin(); - StripDemoTimer.initializeMs(1000, StartDemo).start(); //start demo + timer.initializeMs<1000>(startDemo).start(); } diff --git a/samples/Basic_ScannerI2C/app/application.cpp b/samples/Basic_ScannerI2C/app/application.cpp index 81ecd50d4f..c155894ac7 100644 --- a/samples/Basic_ScannerI2C/app/application.cpp +++ b/samples/Basic_ScannerI2C/app/application.cpp @@ -29,22 +29,22 @@ #include -Timer procTimer; +namespace +{ +SimpleTimer procTimer; +} void scanBus() { - byte error, address; - int nDevices; - Serial.println("Scanning..."); - nDevices = 0; - for(address = 1; address < 127; address++) { + unsigned nDevices{0}; + for(uint8_t address = 1; address < 127; address++) { // The i2c_scanner uses the return value of // the Write.endTransmisstion to see if // a device did acknowledge to the address. Wire.beginTransmission(address); - error = Wire.endTransmission(); + auto error = Wire.endTransmission(); WDT.alive(); // Second option: notify Watch Dog what you are alive (feed it) @@ -55,11 +55,8 @@ void scanBus() Serial << _F("Unknown error at address 0x") << String(address, HEX, 2) << endl; } } - if(nDevices == 0) { - Serial.println("No I2C devices found\n"); - } else { - Serial.println("done\n"); - } + + Serial << nDevices << _F(" I2C devices found.") << endl << endl; } void init() @@ -75,5 +72,5 @@ void init() //Wire.pins(14, 12); // SDA, SCL Wire.begin(); - procTimer.initializeMs(3000, scanBus).start(); + procTimer.initializeMs<3000>(scanBus).start(); } diff --git a/samples/Basic_Serial/app/SerialReadingDelegateDemo.cpp b/samples/Basic_Serial/app/SerialReadingDelegateDemo.cpp index 4e9b346b9e..3e73508fac 100644 --- a/samples/Basic_Serial/app/SerialReadingDelegateDemo.cpp +++ b/samples/Basic_Serial/app/SerialReadingDelegateDemo.cpp @@ -50,6 +50,11 @@ void SerialReadingDelegateDemo::onData(Stream& stream, char arrivedChar, unsigne numCallback++; + /* + * A more functional serial command-line interface can be implemented using the `LineBuffer` class. + * See `Basic_DateTime` sample application for a demonstration. + */ + if(arrivedChar == '\n') // Lets show data! { serial->println(_F("")); diff --git a/samples/Basic_Serial/app/application.cpp b/samples/Basic_Serial/app/application.cpp index 0faceff2a4..a33a47bf68 100644 --- a/samples/Basic_Serial/app/application.cpp +++ b/samples/Basic_Serial/app/application.cpp @@ -5,6 +5,8 @@ #include "SerialReadingDelegateDemo.h" #include "SerialTransmitDemo.h" +namespace +{ struct SerialPins { uint8_t tx; uint8_t rx; @@ -128,9 +130,9 @@ DEFINE_FSTR(testFlashData, "The following are the graphical (non-control) charac "\r\n" "END OF TEXT\r\n"); -Timer procTimer; +SimpleTimer procTimer; SerialReadingDelegateDemo delegateDemoClass; -int helloCounter = 0; +int helloCounter; /** * Serial1 uses UART1, TX pin is GPIO2. @@ -229,6 +231,8 @@ void handleCommand(const String& command) } } +} // namespace + void init() { /* @@ -312,7 +316,7 @@ void init() debugf("(DEBUG) message printed on UART1"); // You should see the debug message in UART1 only. - procTimer.initializeMs(2000, sayHello).start(); + procTimer.initializeMs<2000>(sayHello).start(); testPrintf(); diff --git a/samples/Basic_Servo/app/application.cpp b/samples/Basic_Servo/app/application.cpp index 970d6fbb1c..d3d18c1afa 100644 --- a/samples/Basic_Servo/app/application.cpp +++ b/samples/Basic_Servo/app/application.cpp @@ -7,6 +7,8 @@ #include #include +namespace +{ // Un-comment to use raw time values instead of degrees for servo positioning //#define UPDATE_RAW @@ -36,8 +38,6 @@ class MyServoChannel : public ServoChannel int degree = 0; }; -static MyServoChannel channels[4]; - void MyServoChannel::calcValue() { const char indent[] = " "; @@ -63,10 +63,10 @@ void MyServoChannel::calcValue() value = 0; // overflow and restart linear ramp } - Serial << " value = " << value; + Serial << _F(" value = ") << value; if(!setValue(value)) { - Serial << ": setValue(" << value << ") failed!"; + Serial << _F(": setValue(") << value << _F(") failed!"); } #else @@ -77,10 +77,10 @@ void MyServoChannel::calcValue() ++degree; } - Serial << " degree = " << degree; + Serial << _F(" degree = ") << degree; if(!setDegree(degree)) { - Serial << ": setDegree(" << degree << ") failed!"; + Serial << _F(": setDegree(") << degree << _F(") failed!"); } #endif @@ -102,6 +102,10 @@ void MyServoChannel::calcValue() Serial.println(); } +MyServoChannel channels[4]; + +} // namespace + void init() { Serial.begin(SERIAL_BAUD_RATE); @@ -109,7 +113,7 @@ void init() Serial.setTxWait(false); Serial.setTxBufferSize(1024); - Serial.println("Init Basic Servo Sample"); + Serial.println(_F("Init Basic Servo Sample")); System.setCpuFrequency(CpuCycleClockNormal::cpuFrequency()); #ifdef ARCH_HOST diff --git a/samples/Basic_SmartConfig/app/application.cpp b/samples/Basic_SmartConfig/app/application.cpp index 05f9eec4ca..7e0909b529 100644 --- a/samples/Basic_SmartConfig/app/application.cpp +++ b/samples/Basic_SmartConfig/app/application.cpp @@ -1,24 +1,30 @@ #include +namespace +{ bool smartConfigCallback(SmartConfigEvent event, const SmartConfigEventInfo& info) { switch(event) { case SCE_Wait: - debugf("SCE_Wait\n"); + Serial.println(_F("SCE_Wait")); break; + case SCE_FindChannel: - debugf("SCE_FindChannel\n"); + Serial.println(_F("SCE_FindChannel")); break; + case SCE_GettingSsid: - debugf("SCE_GettingSsid, type = %d\n", info.type); + Serial << _F("SCE_GettingSsid, type = ") << info.type << endl; break; + case SCE_Link: - debugf("SCE_Link\n"); + Serial.println(_F("SCE_Link")); WifiStation.config(info.ssid, info.password); WifiStation.connect(); break; + case SCE_LinkOver: - debugf("SCE_LinkOver\n"); + Serial.println(_F("SCE_LinkOver")); WifiStation.smartConfigStop(); break; } @@ -29,9 +35,11 @@ bool smartConfigCallback(SmartConfigEvent event, const SmartConfigEventInfo& inf void gotIP(IpAddress ip, IpAddress netmask, IpAddress gateway) { - Serial << "Connected: " << ip << endl; + Serial << _F("Connected: ") << ip << endl; } +} // namespace + void init() { Serial.begin(SERIAL_BAUD_RATE); // 115200 by default @@ -40,8 +48,9 @@ void init() WifiEvents.onStationGotIP(gotIP); WifiAccessPoint.enable(false); WifiStation.enable(true); + // automatic (acts as the sample callback above) - //WifiStation.smartConfigStart(SCT_EspTouch); + // WifiStation.smartConfigStart(SCT_EspTouch); // manual, use callback above WifiStation.smartConfigStart(SCT_EspTouch, smartConfigCallback); } diff --git a/samples/Basic_Ssl/app/application.cpp b/samples/Basic_Ssl/app/application.cpp index e0b9fae5eb..d6c16c1b51 100644 --- a/samples/Basic_Ssl/app/application.cpp +++ b/samples/Basic_Ssl/app/application.cpp @@ -11,13 +11,35 @@ #define WIFI_PWD "PleaseEnterPass" #endif -Timer procTimer; +namespace +{ +// Use the Gibson Research fingerprints web page as an example. Unlike Google, these fingerprints change very infrequently. +DEFINE_FSTR(REQUEST_URL, "https://www.grc.com/fingerprints.htm") + +const Ssl::Fingerprint::Cert::Sha1 sha1Fingerprint PROGMEM = { + 0xA6, 0x8F, 0x8C, 0x47, 0x6B, 0xD0, 0xDE, 0x9E, 0x1D, 0x18, + 0x4A, 0x0A, 0x51, 0x4D, 0x90, 0x11, 0x31, 0x93, 0x40, 0x6D, + +}; + +const Ssl::Fingerprint::Cert::Sha256 certSha256Fingerprint PROGMEM = { + 0xDA, 0x2C, 0xF7, 0x62, 0x09, 0x2C, 0x0A, 0x7B, 0x88, 0xA0, 0xDA, 0x65, 0xF9, 0x29, 0x45, 0x97, + 0xA6, 0xDB, 0xAA, 0x2C, 0x80, 0xFD, 0x75, 0x0A, 0xD9, 0xA0, 0x75, 0xEE, 0x64, 0xEE, 0x06, 0x68, +}; + +const Ssl::Fingerprint::Pki::Sha256 publicKeyFingerprint PROGMEM = { + // Out of date + 0xad, 0xcc, 0x21, 0x92, 0x8e, 0x65, 0xc7, 0x54, 0xac, 0xac, 0xb8, 0x2f, 0x12, 0x95, 0x2e, 0x19, + 0x7d, 0x15, 0x7e, 0x32, 0xbe, 0x90, 0x27, 0x43, 0xab, 0xfe, 0xf1, 0xf2, 0xf2, 0xe1, 0x9c, 0x35, +}; + HttpClient downloadClient; OneShotFastMs connectTimer; +SimpleTimer heapTimer; void printHeap() { - Serial.println(_F("Heap statistics")); + Serial << _F("Heap statistics") << endl; Serial << _F(" Free bytes: ") << system_get_free_heap_size() << endl; #ifdef ENABLE_MALLOC_COUNT Serial << _F(" Used: ") << MallocCount::getCurrent() << endl; @@ -61,22 +83,6 @@ void grcSslInit(Ssl::Session& session, HttpRequest& request) { debug_i("Initialising SSL session for GRC"); - // Use the Gibson Research fingerprints web page as an example. Unlike Google, these fingerprints change very infrequently. - static const Ssl::Fingerprint::Cert::Sha1 sha1Fingerprint PROGMEM = { - 0xC3, 0xFB, 0x91, 0x85, 0xCC, 0x6B, 0x4C, 0x7D, 0xE7, 0x18, - 0xED, 0xD8, 0x00, 0xD2, 0x84, 0xE7, 0x6E, 0x97, 0x06, 0x07, - }; - - static const Ssl::Fingerprint::Cert::Sha256 certSha256Fingerprint PROGMEM = { - 0xC3, 0xFB, 0x91, 0x85, 0xCC, 0x6B, 0x4C, 0x7D, 0xE7, 0x18, - 0xED, 0xD8, 0x00, 0xD2, 0x84, 0xE7, 0x6E, 0x97, 0x06, 0x07, - }; - - static const Ssl::Fingerprint::Pki::Sha256 publicKeyFingerprint PROGMEM = { - 0xad, 0xcc, 0x21, 0x92, 0x8e, 0x65, 0xc7, 0x54, 0xac, 0xac, 0xb8, 0x2f, 0x12, 0x95, 0x2e, 0x19, - 0x7d, 0x15, 0x7e, 0x32, 0xbe, 0x90, 0x27, 0x43, 0xab, 0xfe, 0xf1, 0xf2, 0xf2, 0xe1, 0x9c, 0x35, - }; - // Trust certificate only if it matches the SHA1 fingerprint... session.validators.pin(sha1Fingerprint); @@ -95,12 +101,11 @@ void grcSslInit(Ssl::Session& session, HttpRequest& request) void gotIP(IpAddress ip, IpAddress netmask, IpAddress gateway) { - Serial.print(F("Connected. Got IP: ")); - Serial.println(ip); + Serial << _F("Connected. Got IP: ") << ip << endl; connectTimer.start(); - auto request = new HttpRequest(F("https://www.grc.com/fingerprints.htm")); + auto request = new HttpRequest(String(REQUEST_URL)); request->onSslInit(grcSslInit); request->onRequestComplete(onDownload); request->setResponseStream(new CounterStream); @@ -112,6 +117,8 @@ void connectFail(const String& ssid, MacAddress bssid, WifiDisconnectReason reas Serial.println(F("I'm NOT CONNECTED!")); } +} // namespace + void init() { Serial.begin(SERIAL_BAUD_RATE); @@ -122,8 +129,7 @@ void init() MallocCount::enableLogging(true); #endif - auto heapTimer = new Timer; - heapTimer->initializeMs<5000>(printHeap).start(); + heapTimer.initializeMs<5000>(printHeap).start(); // Setup the WIFI connection WifiStation.enable(true); diff --git a/samples/Basic_Ssl/component.mk b/samples/Basic_Ssl/component.mk index b994d8280f..59fa1e1767 100644 --- a/samples/Basic_Ssl/component.mk +++ b/samples/Basic_Ssl/component.mk @@ -4,4 +4,4 @@ COMPONENT_DEPENDS += malloc_count COMPONENT_CXXFLAGS += -DENABLE_MALLOC_COUNT=1 endif -ENABLE_SSL = 1 +ENABLE_SSL = Bearssl diff --git a/samples/Basic_Storage/app/application.cpp b/samples/Basic_Storage/app/application.cpp index 04a260862a..bb0e8e7920 100644 --- a/samples/Basic_Storage/app/application.cpp +++ b/samples/Basic_Storage/app/application.cpp @@ -3,6 +3,9 @@ #include #include +namespace +{ +// Use this source file as contents of our test partitions IMPORT_FSTR(FS_app, PROJECT_DIR "/app/application.cpp") void listSpiffsPartitions() @@ -19,8 +22,7 @@ void listSpiffsPartitions() Directory dir; if(dir.open()) { while(dir.next()) { - Serial.print(" "); - Serial.println(dir.stat().name); + Serial << " " << dir.stat().name << endl; } } Serial << dir.count() << _F(" files found") << endl << endl; @@ -58,6 +60,8 @@ void printPart(const String& partitionName) } } +} // namespace + void init() { Serial.begin(SERIAL_BAUD_RATE); @@ -88,7 +92,11 @@ void init() printPart(part); Serial.println(_F("** Reading SysMem device (RAM)")); - part = Storage::sysMem.editablePartitions().add(F("fs_app RAM"), FS_app, {Storage::Partition::Type::data, 100}); + const size_t bufferSize{4096}; + auto buffer = std::make_unique(bufferSize); + FS_app.readFlash(0, buffer.get(), bufferSize); + part = Storage::sysMem.editablePartitions().add(F("fs_app RAM"), {Storage::Partition::Type::data, 100}, + storage_size_t(buffer.get()), bufferSize); printPart(part); printPart(part); printPart(part); diff --git a/samples/Basic_Utility/app/utility.cpp b/samples/Basic_Utility/app/utility.cpp index 58fac25f67..3ef86868de 100644 --- a/samples/Basic_Utility/app/utility.cpp +++ b/samples/Basic_Utility/app/utility.cpp @@ -1,5 +1,8 @@ #include #include +#include + +auto& output = Host::standardOutput; namespace { @@ -14,21 +17,20 @@ DEFINE_FSTR(testWebConstants, "testWebConstants") void testWebConstants() { - m_printf("%s\n", __FUNCTION__); + output << __FUNCTION__ << endl; const char* extensions[] = { "htm", "html", "txt", "js", "css", "xml", "json", "jpg", "gif", "png", "svg", "ico", "gzip", "zip", "git", "sh", "bin", "exe", }; - for(unsigned i = 0; i < ARRAY_SIZE(extensions); ++i) { - auto ext = extensions[i]; + for(auto ext : extensions) { String contentType = ContentType::fromFileExtension(ext); if(!contentType) { - contentType = "(NOT FOUND)"; + contentType = F("(NOT FOUND)"); } MimeType mimeType = ContentType::fromString(contentType); - m_printf(" %u %s: %s (#%u)\n", i, ext, contentType.c_str(), unsigned(mimeType)); + output << " " << ext << ": " << contentType << " (#" << unsigned(mimeType) << ')' << endl; } } @@ -36,44 +38,38 @@ void testWebConstants() void init() { - // Hook up debug output - Serial.begin(COM_SPEED_SERIAL); - Serial.systemDebugOutput(true); - /* * In a real utility we'd probably need to parse command-line arguments. * See Arch/Host/Components/hostlib/options.h for one way to do this. */ - m_printf("\n** Basic Host utility sample **\n"); + output << endl << "** Basic Host utility sample **" << endl; auto parameters = commandLine.getParameters(); if(parameters.count() == 0) { - m_printf("No command line parameters\n" - "Try doing this:\n" - " make run HOST_PARAMETERS='command=%s'\n", - String(Command::testWebConstants).c_str()); + output << "No command line parameters" << endl + << "Try doing this:" << endl + << " make run HOST_PARAMETERS='command=" << String(Command::testWebConstants) << "'" << endl; } else { - m_printf("Command-line parameters: %u\n", parameters.count()); - for(unsigned i = 0; i < parameters.count(); ++i) { - auto param = parameters[i]; - m_printf(" %u: text = '%s'\n", i, param.text); - m_printf(" name = '%s'\n", param.getName().c_str()); - m_printf(" value = '%s'\n", param.getValue().c_str()); + output << "Command-line parameters: " << parameters.count() << endl; + for(auto& param : parameters) { + output << param.text << endl; + output << " name = '" << param.getName() << "'" << endl; + output << " value = '" << param.getValue() << "'" << endl; } - m_putc('\n'); + output.println(); auto param = parameters.findIgnoreCase("command"); if(param) { if(Command::testWebConstants.equalsIgnoreCase(param.getValue())) { testWebConstants(); } else { - m_printf("Command '%s' not recognised. Try '%s'.\n", param.getValue().c_str(), - String(Command::testWebConstants).c_str()); + output << "Command '" << param.getValue() << "' not recognised. Try '" + << String(Command::testWebConstants) << "'." << endl; } } } - m_putc('\n'); + output.println(); System.restart(); } diff --git a/samples/Basic_WebSkeletonApp/app/application.cpp b/samples/Basic_WebSkeletonApp/app/application.cpp index ace2e6c8e6..9175976292 100644 --- a/samples/Basic_WebSkeletonApp/app/application.cpp +++ b/samples/Basic_WebSkeletonApp/app/application.cpp @@ -1,29 +1,35 @@ -#include +#include +#include +#include -static Timer counterTimer; -unsigned long counter = 0; +// Global +unsigned long counter; -static void counterLoop() +namespace +{ +SimpleTimer counterTimer; + +void counterCallback() { counter++; } -static void STADisconnect(const String& ssid, MacAddress bssid, WifiDisconnectReason reason) +void STADisconnect(const String& ssid, MacAddress bssid, WifiDisconnectReason reason) { - debugf("DISCONNECT - SSID: %s, REASON: %s\n", ssid.c_str(), WifiEvents.getDisconnectReasonDesc(reason).c_str()); + Serial << _F("DISCONNECT - SSID: ") << ssid << _F(", REASON: ") << WifiEvents.getDisconnectReasonDesc(reason) + << endl; if(!WifiAccessPoint.isEnabled()) { - debugf("Starting OWN AP"); + Serial << _F("Starting OWN AP"); WifiStation.disconnect(); WifiAccessPoint.enable(true); WifiStation.connect(); } } -static void STAGotIP(IpAddress ip, IpAddress mask, IpAddress gateway) +void STAGotIP(IpAddress ip, IpAddress mask, IpAddress gateway) { - debugf("GOTIP - IP: %s, MASK: %s, GW: %s\n", ip.toString().c_str(), mask.toString().c_str(), - gateway.toString().c_str()); + Serial << _F("GOTIP - IP: ") << ip << _F(", MASK: ") << mask << _F(", GW: ") << gateway << endl; if(WifiAccessPoint.isEnabled()) { debugf("Shutdown OWN AP"); @@ -32,6 +38,8 @@ static void STAGotIP(IpAddress ip, IpAddress mask, IpAddress gateway) // Add commands to be executed after successfully connecting to AP and got IP from it } +} // namespace + void init() { Serial.begin(SERIAL_BAUD_RATE); // 115200 by default @@ -57,5 +65,5 @@ void init() System.onReady(startWebServer); - counterTimer.initializeMs(1000, counterLoop).start(); + counterTimer.initializeMs<1000>(counterCallback).start(); } diff --git a/samples/Basic_WebSkeletonApp/app/configuration.cpp b/samples/Basic_WebSkeletonApp/app/configuration.cpp index a97aa3227a..6795908354 100644 --- a/samples/Basic_WebSkeletonApp/app/configuration.cpp +++ b/samples/Basic_WebSkeletonApp/app/configuration.cpp @@ -7,6 +7,14 @@ #define WIFI_PWD "PleaseEnterPass" #endif +namespace +{ +DEFINE_FSTR(KEY_NETWORK, "network") +DEFINE_FSTR(KEY_SSID, "StaSSID") +DEFINE_FSTR(KEY_PASS, "StaPassword") +DEFINE_FSTR(KEY_ENABLE, "StaEnable") +} // namespace + ThermConfig activeConfig; ThermConfig loadConfig() @@ -14,14 +22,14 @@ ThermConfig loadConfig() StaticJsonDocument doc; ThermConfig cfg; if(Json::loadFromFile(doc, THERM_CONFIG_FILE)) { - JsonObject network = doc["network"]; - cfg.StaSSID = network["StaSSID"].as(); - cfg.StaPassword = network["StaPassword"].as(); - cfg.StaEnable = network["StaEnable"]; + JsonObject network = doc[KEY_NETWORK]; + cfg.StaSSID = network[KEY_SSID].as(); + cfg.StaPassword = network[KEY_PASS].as(); + cfg.StaEnable = network[KEY_ENABLE]; } else { // Factory defaults if no config file present, or could not access it - cfg.StaSSID = WIFI_SSID; - cfg.StaPassword = WIFI_PWD; + cfg.StaSSID = F(WIFI_SSID); + cfg.StaPassword = F(WIFI_PWD); } return cfg; } @@ -30,10 +38,10 @@ void saveConfig(ThermConfig& cfg) { StaticJsonDocument doc; - JsonObject network = doc.createNestedObject("network"); - network["StaSSID"] = cfg.StaSSID; - network["StaPassword"] = cfg.StaPassword; - network["StaEnable"] = cfg.StaEnable; + JsonObject network = doc.createNestedObject(KEY_NETWORK); + network[KEY_SSID] = cfg.StaSSID; + network[KEY_PASS] = cfg.StaPassword; + network[KEY_ENABLE] = cfg.StaEnable; Json::saveToFile(doc, THERM_CONFIG_FILE, Json::Pretty); } diff --git a/samples/Basic_WebSkeletonApp/app/webserver.cpp b/samples/Basic_WebSkeletonApp/app/webserver.cpp index 5e19982205..228c45d4cb 100644 --- a/samples/Basic_WebSkeletonApp/app/webserver.cpp +++ b/samples/Basic_WebSkeletonApp/app/webserver.cpp @@ -1,8 +1,10 @@ -#include #include #include #include #include "DelayStream.h" +#include + +extern unsigned long counter; // Kind of heartbeat counter namespace { @@ -40,20 +42,20 @@ void sendFile(const String& fileName, HttpServerConnection& connection) auto response = connection.getResponse(); String compressed = fileName + ".gz"; - auto v = fileMap[compressed]; - if(v) { + auto content = fileMap[compressed]; + if(content) { response->headers[HTTP_HEADER_CONTENT_ENCODING] = _F("gzip"); } else { - v = fileMap[fileName]; - if(!v) { + content = fileMap[fileName]; + if(!content) { debug_w("File '%s' not found", fileName.c_str()); response->code = HTTP_STATUS_NOT_FOUND; return; } } - debug_i("found %s in fileMap", String(v.key()).c_str()); - auto stream = new FSTR::Stream(v.content()); + debug_i("found %s in fileMap", String(content.key()).c_str()); + auto stream = new FSTR::Stream(content.content()); response->sendDataStream(stream, ContentType::fromFullFileName(fileName)); // Use client caching for better performance. diff --git a/samples/Basic_WebSkeletonApp/include/configuration.h b/samples/Basic_WebSkeletonApp/include/configuration.h index 555e69c3a3..25de6c43d1 100644 --- a/samples/Basic_WebSkeletonApp/include/configuration.h +++ b/samples/Basic_WebSkeletonApp/include/configuration.h @@ -1,6 +1,6 @@ #pragma once -#include +#include const char THERM_CONFIG_FILE[] = ".therm.conf"; // leading point for security reasons :) @@ -8,16 +8,9 @@ const char THERM_CONFIG_FILE[] = ".therm.conf"; // leading point for security re const uint8_t ConfigJsonBufferSize = 200; struct ThermConfig { - ThermConfig() - { - StaEnable = 1; //Enable WIFI Client - } - String StaSSID; String StaPassword; - uint8_t StaEnable; - - // ThermControl settings + bool StaEnable{true}; // Enable WIFI Client }; ThermConfig loadConfig(); diff --git a/samples/Basic_WebSkeletonApp/include/tytherm.h b/samples/Basic_WebSkeletonApp/include/tytherm.h deleted file mode 100644 index 5239e3df42..0000000000 --- a/samples/Basic_WebSkeletonApp/include/tytherm.h +++ /dev/null @@ -1,8 +0,0 @@ -#pragma once - -#include "configuration.h" - -extern unsigned long counter; // Kind of heartbeat counter - -// Webserver -void startWebServer(); diff --git a/samples/Basic_WebSkeletonApp/include/webserver.h b/samples/Basic_WebSkeletonApp/include/webserver.h new file mode 100644 index 0000000000..0ea88af3fd --- /dev/null +++ b/samples/Basic_WebSkeletonApp/include/webserver.h @@ -0,0 +1,3 @@ +#pragma once + +void startWebServer(); diff --git a/samples/Basic_WebSkeletonApp_LTS/app/application.cpp b/samples/Basic_WebSkeletonApp_LTS/app/application.cpp index 96b54664e4..6224c65df9 100644 --- a/samples/Basic_WebSkeletonApp_LTS/app/application.cpp +++ b/samples/Basic_WebSkeletonApp_LTS/app/application.cpp @@ -25,7 +25,7 @@ void init() System.onReady(startWebServer); - counterTimer.initializeMs(1000, counterLoop).start(); + counterTimer.initializeMs<1000>(counterLoop).start(); } void counterLoop() diff --git a/samples/Basic_WiFi/app/application.cpp b/samples/Basic_WiFi/app/application.cpp index 8b1f26c94f..7de448f2ee 100644 --- a/samples/Basic_WiFi/app/application.cpp +++ b/samples/Basic_WiFi/app/application.cpp @@ -6,6 +6,8 @@ #define WIFI_PWD "PleaseEnterPass" #endif +namespace +{ // Will be called when WiFi station network scan was completed void listNetworks(bool succeeded, BssList& list) { @@ -60,6 +62,8 @@ void ready() } } +} // namespace + void init() { Serial.begin(SERIAL_BAUD_RATE); diff --git a/samples/CanBus/app/application.cpp b/samples/CanBus/app/application.cpp index b616aae340..e36dbe58fd 100644 --- a/samples/CanBus/app/application.cpp +++ b/samples/CanBus/app/application.cpp @@ -3,9 +3,11 @@ #include #include -unsigned long rxId = 0; -uint8_t len = 0; -uint8_t rxBuf[8] = {0}; +namespace +{ +unsigned long rxId; +uint8_t len; +uint8_t rxBuf[8]; uint8_t txBuf0[] = {0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55}; uint8_t txBuf1[] = {0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA}; @@ -13,7 +15,7 @@ uint8_t txBuf1[] = {0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA}; MCP_CAN canBus0(10); // CAN0 interface using CS on digital pin 10 MCP_CAN canBus1(9); // CAN1 interface using CS on digital pin 9 -Timer loopTimer; +SimpleTimer loopTimer; SPISettings spiSettings{8000000, MSBFIRST, SPI_MODE3}; @@ -29,6 +31,8 @@ void loop() } } +} // namespace + void init() { Serial.begin(SERIAL_BAUD_RATE); @@ -55,5 +59,5 @@ void init() canBus0.sendMsgBuf(0x1000000, 1, 8, txBuf0); canBus1.sendMsgBuf(0x1000001, 1, 8, txBuf1); - loopTimer.initializeMs(1000, loop).start(); + loopTimer.initializeMs<1000>(loop).start(); } diff --git a/samples/Compass_HMC5883L/app/application.cpp b/samples/Compass_HMC5883L/app/application.cpp index 471dfebccf..ee674bb2d3 100644 --- a/samples/Compass_HMC5883L/app/application.cpp +++ b/samples/Compass_HMC5883L/app/application.cpp @@ -1,15 +1,34 @@ #include #include +namespace +{ // class default I2C address is 0x1E // specific I2C addresses may be passed as a parameter here // this device only supports one I2C address (0x1E) HMC5883L mag; int16_t mx, my, mz; -Timer procTimer; +SimpleTimer procTimer; + +void readCompass() +{ + // read raw heading measurements from device + mag.getHeading(&mx, &my, &mz); + + // To calculate heading in degrees. 0 degree indicates North + float heading = atan2(my, mx); + if(heading < 0) { + heading += 2 * PI; + } else if(heading > 2 * PI) { + heading -= 2 * PI; + } + + // display tab-separated gyro x/y/z values and heading + Serial << "mag:\t" << mx << '\t' << my << '\t' << mz << "\theading:\t" << heading * RAD_TO_DEG << endl; +} -void readCompass(); +} // namespace void init() { @@ -28,22 +47,5 @@ void init() } // Start reading loop - procTimer.initializeMs(100, readCompass).start(); -} - -void readCompass() -{ - // read raw heading measurements from device - mag.getHeading(&mx, &my, &mz); - - // To calculate heading in degrees. 0 degree indicates North - float heading = atan2(my, mx); - if(heading < 0) { - heading += 2 * PI; - } else if(heading > 2 * PI) { - heading -= 2 * PI; - } - - // display tab-separated gyro x/y/z values and heading - Serial << "mag:\t" << mx << '\t' << my << '\t' << mz << "\theading:\t" << heading * RAD_TO_DEG << endl; + procTimer.initializeMs<100>(readCompass).start(); } diff --git a/samples/DFPlayerMini/app/application.cpp b/samples/DFPlayerMini/app/application.cpp index 4dcdea9666..29580239b4 100644 --- a/samples/DFPlayerMini/app/application.cpp +++ b/samples/DFPlayerMini/app/application.cpp @@ -3,37 +3,40 @@ #define GPIO_LED 2 -Timer timerDFPlayer; -Timer timerLedBlink; -DFRobotDFPlayerMini player; -bool ledState = true; - -void blink() +namespace { - digitalWrite(GPIO_LED, ledState); - ledState = !ledState; -} +SimpleTimer timer; +DFRobotDFPlayerMini player; void nextSong() { player.next(); } -void init() +void checkReady() { - Serial.begin(9600); + static bool ledState; - pinMode(GPIO_LED, OUTPUT); - timerLedBlink.initializeMs(100, blink).start(); + ledState = !ledState; + digitalWrite(GPIO_LED, ledState); - while(!player.begin(Serial)) { - delay(500); + if(!player.begin(Serial)) { + return; } - timerLedBlink.stop(); digitalWrite(GPIO_LED, 0); player.volume(15); - timerDFPlayer.initializeMs(10000, nextSong).start(); + timer.initializeMs<10000>(nextSong).start(); +} + +} // namespace + +void init() +{ + Serial.begin(9600); + + pinMode(GPIO_LED, OUTPUT); + timer.initializeMs<250>(checkReady).start(); } diff --git a/samples/DS3232RTC_NTP_Setter/app/application.cpp b/samples/DS3232RTC_NTP_Setter/app/application.cpp index 802812dbfe..fba95fbf86 100644 --- a/samples/DS3232RTC_NTP_Setter/app/application.cpp +++ b/samples/DS3232RTC_NTP_Setter/app/application.cpp @@ -1,5 +1,4 @@ #include - #include #include @@ -9,39 +8,44 @@ #define WIFI_PWD "ENTER_YOUR_PASSWORD" #endif +namespace +{ // Change as required static constexpr uint8_t SDA_PIN{4}; static constexpr uint8_t SCL_PIN{5}; void onNtpReceive(NtpClient& client, time_t timestamp); -Timer printTimer; - NtpClient ntpClient(onNtpReceive); +SimpleTimer printTimer; void onPrintSystemTime() { DateTime rtcNow = DSRTC.get(); Serial.println(_F("Current time")); - Serial << _F(" System(LOCAL TZ): ") << SystemClock.getSystemTimeString() << endl; - Serial << _F(" UTC(UTC TZ): ") << SystemClock.getSystemTimeString(eTZ_UTC) << endl; - Serial << _F(" DSRTC(UTC TZ): ") << rtcNow.toFullDateTimeString() << endl; + Serial << _F(" System (LOCAL TZ): ") << SystemClock.getSystemTimeString() << endl; + Serial << _F(" UTC (UTC TZ): ") << SystemClock.getSystemTimeString(eTZ_UTC) << endl; + Serial << _F(" DSRTC (UTC TZ): ") << rtcNow.toFullDateTimeString() << endl; } void onNtpReceive(NtpClient& client, time_t timestamp) { - SystemClock.setTime(timestamp, eTZ_UTC); //System timezone is LOCAL so to set it from UTC we specify TZ - DSRTC.set(timestamp); //DSRTC timezone is UTC so we need TZ-correct DSRTC.get() + // Both System and DSRTC timezones are UTC + SystemClock.setTime(timestamp, eTZ_UTC); + DSRTC.set(timestamp); + // Display LOCAL time Serial << _F("Time synchronized: ") << SystemClock.getSystemTimeString() << endl; } void gotIP(IpAddress ip, IpAddress netmask, IpAddress gateway) { - debug_i("Got IP %s", ip.toString().c_str()); + Serial << _F("Got IP ") << ip << endl; ntpClient.requestTime(); } +} // namespace + void init() { Serial.begin(SERIAL_BAUD_RATE); diff --git a/samples/Display_TM1637/app/application.cpp b/samples/Display_TM1637/app/application.cpp index 011a5fd917..14ae2294c8 100644 --- a/samples/Display_TM1637/app/application.cpp +++ b/samples/Display_TM1637/app/application.cpp @@ -8,105 +8,164 @@ // The amount of time (in milliseconds) between tests #define TEST_DELAY 500 -const uint8_t SEG_DONE[] = { +namespace +{ +const uint8_t SEG_DONE[4]{ SEG_B | SEG_C | SEG_D | SEG_E | SEG_G, // d SEG_A | SEG_B | SEG_C | SEG_D | SEG_E | SEG_F, // O SEG_C | SEG_E | SEG_G, // n SEG_A | SEG_D | SEG_E | SEG_F | SEG_G // E }; +const uint8_t SEG_ALL_OFF[4]{}; +const uint8_t SEG_ALL_ON[4]{0xff, 0xff, 0xff, 0xff}; TM1637Display display(CLK, DIO); +SimpleTimer stateTimer; -void testDisplay() +bool testDisplay() { - int k; - uint8_t data[] = {0xff, 0xff, 0xff, 0xff}; - display.setBrightness(0x0f); - - // All segments on - display.setSegments(data); - delay(TEST_DELAY); - - // Selectively set different digits - data[0] = 0b01001001; - data[1] = display.encodeDigit(1); - data[2] = display.encodeDigit(2); - data[3] = display.encodeDigit(3); - - for(k = 3; k >= 0; k--) { - display.setSegments(data, 1, k); - delay(TEST_DELAY); + static uint8_t state; + static uint8_t subState; + +#define STATE(fmt, ...) debug_i("#%u.%u " fmt, state, subState, ##__VA_ARGS__); + + switch(state) { + case 0: { + // All segments on + STATE("All segments on"); + display.setBrightness(7); + display.setSegments(SEG_ALL_ON); + break; } - - display.setSegments(data + 2, 2, 2); - delay(TEST_DELAY); - - display.setSegments(data + 2, 2, 1); - delay(TEST_DELAY); - - display.setSegments(data + 1, 3, 1); - delay(TEST_DELAY); - - // Show decimal numbers with/without leading zeros - bool lz = false; - for(uint8_t z = 0; z < 2; z++) { - for(k = 0; k < 10000; k += k * 4 + 7) { - display.showNumberDec(k, lz); - delay(TEST_DELAY); + case 1: { + // Selectively set different digits + uint8_t pos = 3 - subState; + STATE("setSegments(data, 1, %u)", pos); + const uint8_t seg = 0b01001001; + display.setSegments(&seg, 1, pos); + if(subState++ < 3) { + return false; } - lz = true; + break; } - - // Show decimal number whose length is smaller than 4 - for(k = 0; k < 4; k++) { - data[k] = 0; + case 2: { + uint8_t data[]{ + display.encodeDigit(1), + display.encodeDigit(2), + display.encodeDigit(3), + }; + uint8_t length = (subState == 2) ? 3 : 2; + uint8_t pos = (subState == 0) ? 2 : 1; + STATE("setSegments([0x%02x, 0x%02x, 0x%02x], %u, %u)", data[0], data[1], data[2], length, pos); + display.setSegments(data, length, pos); + if(subState++ < 3) { + return false; + } + break; } - display.setSegments(data); - - // Run through all the dots - for(k = 0; k <= 4; k++) { - display.showNumberDecEx(0, (0x80 >> k), true); - delay(TEST_DELAY); + case 3: + case 4: { + // Show decimal numbers with/without leading zeros + static unsigned k; + if(subState == 0) { + k = 0; + ++subState; + } + bool showLeadingZeroes = (state == 4); + STATE("showNumberDec(%u, %u)", k, showLeadingZeroes); + display.showNumberDec(k, showLeadingZeroes); + k += k * 4 + 7; + if(k < 10000) { + return false; + } + break; } - - display.showNumberDec(153, false, 3, 1); - delay(TEST_DELAY); - display.showNumberDec(22, false, 2, 2); - delay(TEST_DELAY); - display.showNumberDec(0, true, 1, 3); - delay(TEST_DELAY); - display.showNumberDec(0, true, 1, 2); - delay(TEST_DELAY); - display.showNumberDec(0, true, 1, 1); - delay(TEST_DELAY); - display.showNumberDec(0, true, 1, 0); - delay(TEST_DELAY); - - // Brightness Test - for(k = 0; k < 4; k++) { - data[k] = 0xff; + case 5: { + // Run through all the dots + display.setSegments(SEG_ALL_OFF); + uint8_t dots = 0x80 >> subState; + STATE("showNumberDec(0, 0x%02x, true)", dots); + display.showNumberDecEx(0, dots, true); + if(subState++ < 4) { + return false; + } + break; } - for(k = 0; k < 7; k++) { - display.setBrightness(k); - display.setSegments(data); - delay(TEST_DELAY); + case 6: { + // Show decimal number whose length is smaller than 4 + struct NumberArgs { + uint8_t num; + bool leadingZero; + uint8_t length; + uint8_t pos; + }; + const NumberArgs arglist[]{ + {153, false, 3, 1}, // + {22, false, 2, 2}, // + {0, true, 1, 3}, // + {0, true, 1, 2}, // + {0, true, 1, 1}, // + {0, true, 1, 0}, // + }; + auto& args = arglist[subState]; + STATE("showNumberDec(%u, %u, %u, %u)", args.num, args.leadingZero, args.length, args.pos); + display.showNumberDec(args.num, args.leadingZero, args.length, args.pos); + if(subState++ < ARRAY_SIZE(arglist)) { + return false; + } + break; } - - // On/Off test - for(k = 0; k < 4; k++) { - display.setBrightness(7, false); // Turn off - display.setSegments(data); - delay(TEST_DELAY); - display.setBrightness(7, true); // Turn on - display.setSegments(data); - delay(TEST_DELAY); + case 7: { + // Brightness Test + uint8_t brightness = subState; + STATE("setBrightness(%u)", brightness); + display.setBrightness(brightness); + display.setSegments(SEG_ALL_ON); + if(++subState < 7) { + return false; + } + break; + } + case 8: { + // On/Off test + bool onOff = subState & 0x01; + debug_i("setBrightness(7, %u)", onOff); + display.setBrightness(7, onOff); + display.setSegments(SEG_ALL_ON); + if(++subState < 8) { + return false; + } + break; + } + case 9: + // Done! + debug_i("Done!"); + display.setSegments(SEG_DONE); + return true; } - // Done! - display.setSegments(SEG_DONE); + ++state; + subState = 0; + return false; } +} // namespace + void init() { - testDisplay(); + Serial.begin(SERIAL_BAUD_RATE); + Serial.systemDebugOutput(true); + + Serial << _F("Sample TM1637 application") << endl; + +#ifdef ARCH_HOST + setDigitalHooks(nullptr); +#endif + + stateTimer.initializeMs([]() { + if(!testDisplay()) { + stateTimer.startOnce(); + } + }); + stateTimer.startOnce(); } diff --git a/samples/Distance_Vl53l0x/app/application.cpp b/samples/Distance_Vl53l0x/app/application.cpp index 3a5e97a96b..2a5e2251c0 100644 --- a/samples/Distance_Vl53l0x/app/application.cpp +++ b/samples/Distance_Vl53l0x/app/application.cpp @@ -1,15 +1,16 @@ #include #include -Adafruit_VL53L0X lox; - // GPIO - NodeMCU pins #define SDA 4 // D2 #define SCL 5 // D1 #define XSHUT 14 // D5 #define INT 12 // D6 -Timer loopTimer; +namespace +{ +Adafruit_VL53L0X lox; +SimpleTimer loopTimer; void loop() { @@ -25,6 +26,8 @@ void loop() } } +} // namespace + void init() { Serial.begin(SERIAL_BAUD_RATE); // 115200 by default diff --git a/samples/DnsCaptivePortal/app/application.cpp b/samples/DnsCaptivePortal/app/application.cpp index c06c947f1f..5e599afcc2 100644 --- a/samples/DnsCaptivePortal/app/application.cpp +++ b/samples/DnsCaptivePortal/app/application.cpp @@ -1,14 +1,16 @@ #include +#define DNS_PORT 53 + +namespace +{ DnsServer dnsServer; HttpServer server; -#define DNS_PORT 53 - void onIndex(HttpRequest& request, HttpResponse& response) { response.setContentType(MIME_HTML); - response.sendString("SMING captive portal"); + response.sendString(F("SMING captive portal")); } void startWebServer() @@ -25,6 +27,8 @@ void startServers() startWebServer(); } +} // namespace + void init() { Serial.begin(SERIAL_BAUD_RATE); // 115200 by default @@ -33,7 +37,7 @@ void init() // Start AP WifiStation.enable(false); WifiAccessPoint.enable(true); - WifiAccessPoint.config("DNSCaptive Portal", "", AUTH_OPEN); + WifiAccessPoint.config(F("DNSCaptive Portal"), nullptr, AUTH_OPEN); System.onReady(startServers); } diff --git a/samples/Echo_Ssl/app/application.cpp b/samples/Echo_Ssl/app/application.cpp index 348cede4e6..87f582a348 100644 --- a/samples/Echo_Ssl/app/application.cpp +++ b/samples/Echo_Ssl/app/application.cpp @@ -19,7 +19,9 @@ #define SERVER_IP "127.0.0.1" #endif -Timer procTimer; +namespace +{ +SimpleTimer procTimer; TcpClient* client; bool showMeta = true; @@ -53,12 +55,14 @@ void connectFail(const String& ssid, MacAddress bssid, WifiDisconnectReason reas void gotIP(IpAddress ip, IpAddress netmask, IpAddress gateway) { - debugf("IP: %s", ip.toString().c_str()); + Serial << _F("Got IP ") << ip << endl; client = new TcpClient(TcpClientDataDelegate(onReceive)); client->setSslInitHandler([](Ssl::Session& session) { session.options.verifyLater = true; }); client->connect(IpAddress(SERVER_IP), 4444, true); } +} // namespace + void init() { Serial.begin(SERIAL_BAUD_RATE); @@ -66,7 +70,7 @@ void init() // Setup the WIFI connection WifiStation.enable(true); - WifiStation.config(WIFI_SSID, WIFI_PWD); // Put your SSID and password here + WifiStation.config(WIFI_SSID, WIFI_PWD); // Run our method when station was connected to AP (or not connected) WifiEvents.onStationDisconnect(connectFail); diff --git a/samples/FtpServer_Files/app/application.cpp b/samples/FtpServer_Files/app/application.cpp index 0b659e560b..1071126279 100644 --- a/samples/FtpServer_Files/app/application.cpp +++ b/samples/FtpServer_Files/app/application.cpp @@ -6,11 +6,13 @@ #define WIFI_PWD "PleaseEnterPass" #endif +namespace +{ FtpServer ftp; void gotIP(IpAddress ip, IpAddress netmask, IpAddress gateway) { - Serial << "IP: " << ip << endl; + Serial << _F("Got IP ") << ip << endl; // Start FTP server ftp.listen(21); // Add user accounts @@ -26,6 +28,8 @@ void connectFail(const String& ssid, MacAddress bssid, WifiDisconnectReason reas Serial.println(_F("I'm NOT CONNECTED. Need help!!! :(")); } +} // namespace + void init() { Serial.begin(SERIAL_BAUD_RATE); // 115200 by default diff --git a/samples/Gesture_APDS-9960/app/application.cpp b/samples/Gesture_APDS-9960/app/application.cpp index 3018d2b7c0..6dbdd3ce12 100644 --- a/samples/Gesture_APDS-9960/app/application.cpp +++ b/samples/Gesture_APDS-9960/app/application.cpp @@ -2,48 +2,58 @@ #include -SparkFun_APDS9960 apds = SparkFun_APDS9960(); +namespace +{ +SparkFun_APDS9960 apds; // For I2C // Default I2C pins 0 and 2. Pin 4 - interrupt pin // -#define APDS9960_INT 4 // Needs to be an interrupt pin +#define APDS9960_INT 4 + +String dirToString(unsigned dir) +{ + switch(dir) { + case DIR_NONE: + return F("NONE"); + case DIR_LEFT: + return F("LEFT"); + case DIR_RIGHT: + return F("RIGHT"); + case DIR_UP: + return F("UP"); + case DIR_DOWN: + return F("DOWN"); + case DIR_NEAR: + return F("NEAR"); + case DIR_FAR: + return F("FAR"); + case DIR_ALL: + return F("ALL"); + default: + return nullptr; + } +} void handleGesture() { - if(apds.isGestureAvailable()) { - switch(apds.readGesture()) { - case DIR_UP: - Serial.println(_F("UP")); - break; - case DIR_DOWN: - Serial.println(_F("DOWN")); - break; - case DIR_LEFT: - Serial.println(_F("LEFT")); - break; - case DIR_RIGHT: - Serial.println(_F("RIGHT")); - break; - case DIR_NEAR: - Serial.println(_F("NEAR")); - break; - case DIR_FAR: - Serial.println(_F("FAR")); - break; - default: - Serial.println(_F("NONE")); - } + if(!apds.isGestureAvailable()) { + return; } + + auto dir = apds.readGesture(); + Serial.println(dirToString(dir)); } -void interruptRoutine() +void interruptDelegate() { detachInterrupt(APDS9960_INT); handleGesture(); - attachInterrupt(APDS9960_INT, InterruptDelegate(interruptRoutine), FALLING); + attachInterrupt(APDS9960_INT, InterruptDelegate(interruptDelegate), FALLING); } +} // namespace + void init() { Serial.begin(SERIAL_BAUD_RATE); // 115200 by default @@ -61,20 +71,22 @@ void init() "--------------------------------\r\n")); // Initialize APDS-9960 (configure I2C and initial values) - if(apds.init()) { - Serial.println(_F("APDS-9960 initialization complete")); - } else { + if(!apds.init()) { Serial.println(_F("Something went wrong during APDS-9960 init!")); + return; } + Serial.println(_F("APDS-9960 initialization complete")); + // Start running the APDS-9960 gesture sensor engine - if(apds.enableGestureSensor(true)) { - Serial.println(_F("Gesture sensor is now running")); - } else { + if(!apds.enableGestureSensor(true)) { Serial.println(_F("Something went wrong during gesture sensor init!")); + return; } + Serial.println(_F("Gesture sensor is now running")); + // Initialize interrupt service routine - pinMode(APDS9960_INT, (GPIO_INT_TYPE)GPIO_PIN_INTR_ANYEDGE); - attachInterrupt(APDS9960_INT, InterruptDelegate(interruptRoutine), FALLING); + pinMode(APDS9960_INT, GPIO_PIN_INTR_ANYEDGE); + attachInterrupt(APDS9960_INT, InterruptDelegate(interruptDelegate), FALLING); } diff --git a/samples/HttpClient/app/application.cpp b/samples/HttpClient/app/application.cpp index ed50d78ad8..ed64810044 100644 --- a/samples/HttpClient/app/application.cpp +++ b/samples/HttpClient/app/application.cpp @@ -7,19 +7,25 @@ #define WIFI_PWD "PleaseEnterPass" #endif +namespace +{ HttpClient httpClient; int onDownload(HttpConnection& connection, bool success) { - debugf("\n=========[ URL: %s ]============", connection.getRequest()->uri.toString().c_str()); - debugf("RemoteIP: %s", connection.getRemoteIp().toString().c_str()); - debugf("Got response code: %d", connection.getResponse()->code); - debugf("Success: %d", success); - if(connection.getRequest()->method != HTTP_HEAD) { + auto& request = *connection.getRequest(); + auto& response = *connection.getResponse(); + + Serial << endl << _F("=========[ URL: ") << request.uri << _F(" ]============") << endl; + Serial << _F("RemoteIP: ") << connection.getRemoteIp() << endl; + Serial << _F("Got response code: ") << response.code << " " << toString(response.code) << endl; + Serial << _F("Success: ") << success << " " << (success ? "OK" : "FAIL") << endl; + + if(request.method != HTTP_HEAD) { Serial.print(_F("Got content: ")); - auto stream = connection.getResponse()->stream; + auto stream = response.stream; if(stream == nullptr || stream->available() == 0) { - Serial.println("EMPTY!"); + Serial.println(_F("EMPTY!")); } else { Serial.copyFrom(stream); Serial.println(); @@ -146,6 +152,8 @@ void connectOk(IpAddress ip, IpAddress mask, IpAddress gateway) httpClient.send(putRequest); } +} // namespace + void init() { Serial.begin(SERIAL_BAUD_RATE); diff --git a/samples/HttpClient_Instapush/app/application.cpp b/samples/HttpClient_Instapush/app/application.cpp index 5575d65e95..12e1f22de0 100644 --- a/samples/HttpClient_Instapush/app/application.cpp +++ b/samples/HttpClient_Instapush/app/application.cpp @@ -1,6 +1,5 @@ #include -#include -#include +#include /*** Direct PUSH notifications on your mobile phone! * @@ -14,29 +13,27 @@ * 3. Update Application ID and Application Secret below: */ -#define APP_ID "55719abba4c48a802c881205" -#define APP_SECRET "5300adbe3f906938950fc0cdbc301986" - // If you want, you can define WiFi settings globally in Eclipse Environment Variables #ifndef WIFI_SSID #define WIFI_SSID "PleaseEnterSSID" // Put your SSID and password here #define WIFI_PWD "PleaseEnterPass" #endif -class InstapushTrackers : public HashMap +namespace { -}; +DEFINE_FSTR(APP_ID, "55719abba4c48a802c881205") +DEFINE_FSTR(APP_SECRET, "5300adbe3f906938950fc0cdbc301986") + +using InstapushTrackers = HashMap; class InstapushApplication : protected HttpClient { public: - InstapushApplication(String appId, String appSecret) + InstapushApplication(const String& appId, const String& appSecret) : app(appId), secret(appSecret) { - app = appId; - secret = appSecret; } - void notify(String event, InstapushTrackers& trackersInfo) + void notify(const String& event, InstapushTrackers& trackersInfo) { debugf("preparing request"); @@ -47,7 +44,8 @@ class InstapushApplication : protected HttpClient requestHeaders[F("x-instapush-appid")] = app; requestHeaders[F("x-instapush-appsecret")] = secret; - DynamicJsonDocument root(1024); + auto stream = new JsonObjectStream(1024); + auto root = stream->getRoot(); root["event"] = event; JsonObject trackers = root.createNestedObject("trackers"); for(auto info : trackersInfo) { @@ -55,8 +53,6 @@ class InstapushApplication : protected HttpClient trackers[info.key()] = info.value(); } - auto stream = new MemoryDataStream; - Json::serialize(root, stream); request->setBody(stream); request->onRequestComplete(RequestCompletedDelegate(&InstapushApplication::processed, this)); @@ -73,10 +69,10 @@ class InstapushApplication : protected HttpClient private: String app; String secret; - const char* url = "http://api.instapush.im/v1/post"; + DEFINE_FSTR_LOCAL(url, "http://api.instapush.im/v1/post"); }; -Timer procTimer; +SimpleTimer procTimer; InstapushApplication pusher(APP_ID, APP_SECRET); // Publish our message @@ -98,6 +94,8 @@ void gotIP(IpAddress ip, IpAddress netmask, IpAddress gateway) procTimer.initializeMs<10 * 1000>(publishMessage).start(); } +} // namespace + void init() { Serial.begin(SERIAL_BAUD_RATE); // 115200 by default diff --git a/samples/HttpClient_ThingSpeak/app/application.cpp b/samples/HttpClient_ThingSpeak/app/application.cpp index ee6424f537..e11ca1f162 100644 --- a/samples/HttpClient_ThingSpeak/app/application.cpp +++ b/samples/HttpClient_ThingSpeak/app/application.cpp @@ -6,8 +6,10 @@ #define WIFI_PWD "PleaseEnterPass" #endif -Timer procTimer; -int sensorValue = 0; +namespace +{ +SimpleTimer procTimer; +int sensorValue; HttpClient thingSpeak; int onDataSent(HttpConnection& client, bool successful) @@ -40,8 +42,8 @@ void sendData() */ Url url; url.Scheme = URI_SCHEME_HTTP; - url.Host = "api.thingspeak.com"; - url.Path = "/update"; + url.Host = F("api.thingspeak.com"); + url.Path = F("/update"); url.Query["key"] = F("7XXUJWCWYTMXKN3L"); url.Query["field1"] = String(sensorValue); thingSpeak.downloadString(url, onDataSent); @@ -62,9 +64,11 @@ void connectFail(const String& ssid, MacAddress bssid, WifiDisconnectReason reas void gotIP(IpAddress ip, IpAddress netmask, IpAddress gateway) { // Start send data loop - procTimer.initializeMs(25 * 1000, sendData).start(); // every 25 seconds + procTimer.initializeMs<25 * 1000>(sendData).start(); // every 25 seconds } +} // namespace + void init() { Serial.begin(SERIAL_BAUD_RATE); // 115200 by default diff --git a/samples/HttpServer_AJAX/app/application.cpp b/samples/HttpServer_AJAX/app/application.cpp index ccd3595381..205c72e578 100644 --- a/samples/HttpServer_AJAX/app/application.cpp +++ b/samples/HttpServer_AJAX/app/application.cpp @@ -7,18 +7,26 @@ #define WIFI_PWD "PleaseEnterPass" #endif +namespace +{ HttpServer server; FtpServer ftp; -int inputs[] = {0, 2}; // Set input GPIO pins here -Vector namesInput; -const int countInputs = sizeof(inputs) / sizeof(inputs[0]); +// Set input GPIO pins here +const uint8_t inputs[] = {5, 2}; void onIndex(HttpRequest& request, HttpResponse& response) { TemplateFileStream* tmpl = new TemplateFileStream("index.html"); - //auto& vars = tmpl->variables(); - //vars["counter"] = String(counter); + auto& vars = tmpl->variables(); + String gpioList; + for(unsigned i = 0; i < ARRAY_SIZE(inputs); ++i) { + String s = F("GPIO{gpio} "); + s.replace("{id}", String(i)); + s.replace("{gpio}", String(inputs[i])); + gpioList += s; + } + vars["gpio_list"] = gpioList; response.sendNamedStream(tmpl); // this template object will be deleted automatically } @@ -40,8 +48,8 @@ void onAjaxInput(HttpRequest& request, HttpResponse& response) JsonObject json = stream->getRoot(); json["status"] = (bool)true; - String stringKey = "StringKey"; - String stringValue = "StringValue"; + String stringKey = F("StringKey"); + String stringValue = F("StringValue"); json[stringKey] = stringValue; @@ -51,8 +59,9 @@ void onAjaxInput(HttpRequest& request, HttpResponse& response) } JsonObject gpio = json.createNestedObject("gpio"); - for(int i = 0; i < countInputs; i++) - gpio[namesInput[i].c_str()] = digitalRead(inputs[i]); + for(unsigned i = 0; i < ARRAY_SIZE(inputs); ++i) { + gpio[String(i)] = digitalRead(inputs[i]); + } response.sendDataStream(stream, MIME_JSON); } @@ -64,7 +73,7 @@ void onAjaxFrequency(HttpRequest& request, HttpResponse& response) JsonObjectStream* stream = new JsonObjectStream(); JsonObject json = stream->getRoot(); - json["status"] = (bool)true; + json["status"] = true; json["value"] = (int)System.getCpuFrequency(); response.sendDataStream(stream, MIME_JSON); @@ -78,10 +87,11 @@ void startWebServer() server.paths.set("/ajax/frequency", onAjaxFrequency); server.paths.setDefault(onFile); - Serial.println(_F("\r\n" - "=== WEB SERVER STARTED ===")); - Serial.println(WifiStation.getIP()); - Serial.println(_F("==========================\r\n")); + Serial << endl + << _F("=== WEB SERVER STARTED ===") << endl + << WifiStation.getIP() << endl + << _F("==========================") << endl + << endl; } void startFTP() @@ -101,6 +111,8 @@ void gotIP(IpAddress ip, IpAddress netmask, IpAddress gateway) startWebServer(); } +} // namespace + void init() { spiffs_mount(); // Mount file system, in order to work with files @@ -112,9 +124,8 @@ void init() WifiStation.config(WIFI_SSID, WIFI_PWD); WifiAccessPoint.enable(false); - for(int i = 0; i < countInputs; i++) { - namesInput.add(String(inputs[i])); - pinMode(inputs[i], INPUT); + for(auto pin : inputs) { + pinMode(pin, INPUT); } // Run our method when station was connected to AP diff --git a/samples/HttpServer_AJAX/web/build/index.html b/samples/HttpServer_AJAX/web/build/index.html index 1fab1be6ed..cb23240030 100644 --- a/samples/HttpServer_AJAX/web/build/index.html +++ b/samples/HttpServer_AJAX/web/build/index.html @@ -71,8 +71,7 @@

Sming AJAX Example

Reading Input value

-

GPIO0 -

GPIO2 + {gpio_list}

diff --git a/samples/HttpServer_AJAX/web/dev/index.html b/samples/HttpServer_AJAX/web/dev/index.html index 1fab1be6ed..cb23240030 100644 --- a/samples/HttpServer_AJAX/web/dev/index.html +++ b/samples/HttpServer_AJAX/web/dev/index.html @@ -71,8 +71,7 @@

Sming AJAX Example

Reading Input value

-

GPIO0 -

GPIO2 + {gpio_list}

diff --git a/samples/HttpServer_Bootstrap/app/application.cpp b/samples/HttpServer_Bootstrap/app/application.cpp index bf75ae9fba..cdc1fdbb99 100644 --- a/samples/HttpServer_Bootstrap/app/application.cpp +++ b/samples/HttpServer_Bootstrap/app/application.cpp @@ -8,8 +8,11 @@ #define LED_PIN 0 // GPIO number +namespace +{ HttpServer server; int counter = 0; +HttpClient downloadClient; void onIndex(HttpRequest& request, HttpResponse& response) { @@ -56,26 +59,24 @@ void startWebServer() server.paths.set("/hello", onHello); server.paths.setDefault(onFile); - Serial.println(_F("\r\n" - "=== WEB SERVER STARTED ===")); - Serial.println(WifiStation.getIP()); - Serial.println(_F("==========================\r\n")); + Serial << endl + << _F("=== WEB SERVER STARTED ===") << endl + << WifiStation.getIP() << endl + << _F("==========================") << endl + << endl; } -Timer downloadTimer; -HttpClient downloadClient; -int dowfid = 0; void downloadContentFiles() { downloadClient.downloadFile(F("http://simple.anakod.ru/templates/index.html")); downloadClient.downloadFile(F("http://simple.anakod.ru/templates/bootstrap.css.gz")); downloadClient.downloadFile(F("http://simple.anakod.ru/templates/jquery.js.gz"), - (RequestCompletedDelegate)([](HttpConnection& connection, bool success) -> int { + [](HttpConnection& connection, bool success) -> int { if(success) { startWebServer(); } return 0; - })); + }); } void gotIP(IpAddress ip, IpAddress netmask, IpAddress gateway) @@ -88,15 +89,17 @@ void gotIP(IpAddress ip, IpAddress netmask, IpAddress gateway) } } +} // namespace + void init() { + Serial.begin(SERIAL_BAUD_RATE); // 115200 by default + Serial.systemDebugOutput(true); // Enable debug output to serial + spiffs_mount(); // Mount file system, in order to work with files pinMode(LED_PIN, OUTPUT); - Serial.begin(SERIAL_BAUD_RATE); // 115200 by default - Serial.systemDebugOutput(true); // Enable debug output to serial - WifiStation.enable(true); WifiStation.config(WIFI_SSID, WIFI_PWD); WifiAccessPoint.enable(false); diff --git a/samples/HttpServer_ConfigNetwork/app/application.cpp b/samples/HttpServer_ConfigNetwork/app/application.cpp index b9bf939fce..2301a5257e 100644 --- a/samples/HttpServer_ConfigNetwork/app/application.cpp +++ b/samples/HttpServer_ConfigNetwork/app/application.cpp @@ -5,18 +5,25 @@ namespace { +ApplicationSettingsStorage AppSettings; + HttpServer server; FtpServer ftp; HashMap networks; -String network, password; -Timer connectionTimer; +String network; +String password; +SimpleTimer connectionTimer; String lastModified; SimpleTimer scanTimer; +DEFINE_FSTR(DEFAULT_IP, "192.168.1.77") +DEFINE_FSTR(DEFAULT_NETMASK, "255.255.255.0") +DEFINE_FSTR(DEFAULT_GATEWAY, "192.168.1.1") + // Instead of using a SPIFFS file, here we demonstrate usage of imported Flash Strings IMPORT_FSTR_LOCAL(flashSettings, PROJECT_DIR "/web/build/settings.html") @@ -31,7 +38,7 @@ void onIndex(HttpRequest& request, HttpResponse& response) int onIpConfig(HttpServerConnection& connection, HttpRequest& request, HttpResponse& response) { if(request.method == HTTP_POST) { - debugf("Request coming from IP: %s", connection.getRemoteIp().toString().c_str()); + Serial << _F("Request coming from IP: ") << connection.getRemoteIp() << endl; // If desired you can also limit the access based on remote IP. Example below: // if(IpAddress("192.168.4.23") != connection.getRemoteIp()) { // return 1; // error @@ -41,7 +48,7 @@ int onIpConfig(HttpServerConnection& connection, HttpRequest& request, HttpRespo AppSettings.ip = request.getPostParameter("ip"); AppSettings.netmask = request.getPostParameter("netmask"); AppSettings.gateway = request.getPostParameter("gateway"); - debugf("Updating IP settings: %d", AppSettings.ip.isNull()); + Serial << _F("Updating IP settings: ") << AppSettings.ip.isNull() << endl; AppSettings.save(); } @@ -54,18 +61,13 @@ int onIpConfig(HttpServerConnection& connection, HttpRequest& request, HttpRespo auto& vars = tmpl->variables(); bool dhcp = WifiStation.isEnabledDHCP(); - vars["dhcpon"] = dhcp ? "checked='checked'" : ""; - vars["dhcpoff"] = !dhcp ? "checked='checked'" : ""; + vars["dhcpon"] = dhcp ? _F("checked='checked'") : ""; + vars["dhcpoff"] = !dhcp ? _F("checked='checked'") : ""; - if(!WifiStation.getIP().isNull()) { - vars["ip"] = WifiStation.getIP().toString(); - vars["netmask"] = WifiStation.getNetworkMask().toString(); - vars["gateway"] = WifiStation.getNetworkGateway().toString(); - } else { - vars["ip"] = "192.168.1.77"; - vars["netmask"] = "255.255.255.0"; - vars["gateway"] = "192.168.1.1"; - } + auto set = !WifiStation.getIP().isNull(); + vars["ip"] = set ? WifiStation.getIP().toString() : DEFAULT_IP; + vars["netmask"] = set ? WifiStation.getNetworkMask().toString() : DEFAULT_NETMASK; + vars["gateway"] = set ? WifiStation.getNetworkGateway().toString() : DEFAULT_GATEWAY; response.sendNamedStream(tmpl); // will be automatically deleted @@ -160,7 +162,7 @@ void onAjaxConnect(HttpRequest& request, HttpResponse& response) password = curPass; debugf("CONNECT TO: %s %s", network.c_str(), password.c_str()); json["connected"] = false; - connectionTimer.initializeMs(1200, makeConnection).startOnce(); + connectionTimer.initializeMs<1200>(makeConnection).startOnce(); } else { json["connected"] = WifiStation.isConnected(); debugf("Network already selected. Current status: %s", WifiStation.getConnectionStatusName().c_str()); @@ -186,9 +188,9 @@ void startWebServer() server.listen(80); #endif server.paths.set("/", onIndex); - server.paths.set("/ipconfig", onIpConfig); - server.paths.set("/ajax/get-networks", onAjaxNetworkList); - server.paths.set("/ajax/connect", onAjaxConnect); + server.paths.set(F("/ipconfig"), onIpConfig); + server.paths.set(F("/ajax/get-networks"), onAjaxNetworkList); + server.paths.set(F("/ajax/connect"), onAjaxConnect); server.paths.setDefault(onFile); } @@ -196,7 +198,7 @@ void startFTP() { if(!fileExist("index.html")) fileSetContent("index.html", - "

Please connect to FTP and upload files from folder 'web/build' (details in code)

"); + F("

Please connect to FTP and upload files from folder 'web/build' (details in code)

")); // Start FTP server ftp.listen(21); diff --git a/samples/HttpServer_ConfigNetwork/include/AppSettings.h b/samples/HttpServer_ConfigNetwork/include/AppSettings.h index 66a32fcb89..0c97b586ef 100644 --- a/samples/HttpServer_ConfigNetwork/include/AppSettings.h +++ b/samples/HttpServer_ConfigNetwork/include/AppSettings.h @@ -5,11 +5,10 @@ * Author: Anakod */ -#include -#include +#pragma once -#ifndef INCLUDE_APPSETTINGS_H_ -#define INCLUDE_APPSETTINGS_H_ +#include +#include #define APP_SETTINGS_FILE ".settings.conf" // leading point for security reasons :) @@ -61,7 +60,3 @@ struct ApplicationSettingsStorage { return fileExist(APP_SETTINGS_FILE); } }; - -static ApplicationSettingsStorage AppSettings; - -#endif /* INCLUDE_APPSETTINGS_H_ */ diff --git a/samples/HttpServer_FirmwareUpload/app/application.cpp b/samples/HttpServer_FirmwareUpload/app/application.cpp index 2dbfe83325..586f3251e8 100644 --- a/samples/HttpServer_FirmwareUpload/app/application.cpp +++ b/samples/HttpServer_FirmwareUpload/app/application.cpp @@ -2,8 +2,15 @@ #include #include #include -#include +// If you want, you can define WiFi settings globally in Eclipse Environment Variables +#ifndef WIFI_SSID +#define WIFI_SSID "PleaseEnterSSID" // Put your SSID and password here +#define WIFI_PWD "PleaseEnterPass" +#endif + +namespace +{ HttpServer server; void onFile(HttpRequest& request, HttpResponse& response) @@ -83,6 +90,8 @@ void startWebServer() server.paths.setDefault(onFile); } +} // namespace + void init() { Serial.begin(SERIAL_BAUD_RATE); // 115200 by default @@ -91,8 +100,8 @@ void init() spiffs_mount(); // Mount file system, in order to work with files WifiStation.enable(true); - //WifiStation.config(WIFI_SSID, WIFI_PWD); - //WifiStation.enableDHCP(true); + WifiStation.config(WIFI_SSID, WIFI_PWD); + // WifiStation.enableDHCP(true); // Run WEB server on system ready System.onReady(startWebServer); diff --git a/samples/HttpServer_Plugins/app/application.cpp b/samples/HttpServer_Plugins/app/application.cpp index 04af70bb83..cc1689288d 100644 --- a/samples/HttpServer_Plugins/app/application.cpp +++ b/samples/HttpServer_Plugins/app/application.cpp @@ -55,8 +55,8 @@ void startWebServer() * * make sure to replace the IP address with the IP address of your HttpServer */ - server->paths.set("/ip-n-auth", echoContentBody, - new ResourceIpAuth(IpAddress("192.168.13.0"), IpAddress("255.255.255.0")), pluginBasicAuth); + server->paths.set("/ip-n-auth", echoContentBody, new ResourceIpAuth(F("192.168.13.0"), F("255.255.255.0")), + pluginBasicAuth); /* * This content coming to this resource is modified on the fly @@ -70,16 +70,18 @@ void startWebServer() */ server->paths.set("/test", echoContentBody, new ContentDecoder()); - Serial.println(F("\r\n=== WEB SERVER STARTED ===")); - Serial.println(WifiStation.getIP()); - Serial.println(F("==============================\r\n")); + Serial << endl + << _F("=== WEB SERVER STARTED ===") << endl + << WifiStation.getIP() << endl + << _F("==============================") << endl + << endl; } // Will be called when WiFi station becomes fully operational void gotIP(IpAddress ip, IpAddress netmask, IpAddress gateway) { startWebServer(); - debug_i("free heap = %u", system_get_free_heap_size()); + Serial << _F("Free heap = ") << system_get_free_heap_size() << endl; } } // namespace diff --git a/samples/HttpServer_WebSockets/app/CUserData.cpp b/samples/HttpServer_WebSockets/app/CUserData.cpp index f12e893988..ea949da3be 100644 --- a/samples/HttpServer_WebSockets/app/CUserData.cpp +++ b/samples/HttpServer_WebSockets/app/CUserData.cpp @@ -1,7 +1,5 @@ #include "CUserData.h" -//Simplified container modelling a user session - void CUserData::addSession(WebsocketConnection& connection) { activeWebSockets.addElement(&connection); @@ -10,27 +8,17 @@ void CUserData::addSession(WebsocketConnection& connection) void CUserData::removeSession(WebsocketConnection& connection) { - int i = activeWebSockets.indexOf(&connection); - if(i < 0) { - return; + if(activeWebSockets.removeElement(&connection)) { + connection.setUserData(nullptr); + Serial.println(F("Removed user session")); } - - activeWebSockets[i]->setUserData(nullptr); - activeWebSockets.remove(i); - Serial.println(F("Removed user session")); } void CUserData::printMessage(WebsocketConnection& connection, const String& msg) { - unsigned i = 0; - for(; i < activeWebSockets.count(); i++) { - if(connection == *(activeWebSockets[i])) { - break; - } - } - - if(i < activeWebSockets.count()) { - Serial << _F("Received msg on connection ") << i << ": " << msg; + int i = activeWebSockets.indexOf(&connection); + if(i >= 0) { + Serial << _F("Received msg on connection ") << i << ": " << msg << endl; } } diff --git a/samples/HttpServer_WebSockets/app/application.cpp b/samples/HttpServer_WebSockets/app/application.cpp index cd98bd93fc..2d0a059c4c 100644 --- a/samples/HttpServer_WebSockets/app/application.cpp +++ b/samples/HttpServer_WebSockets/app/application.cpp @@ -2,7 +2,7 @@ #include #include "CUserData.h" -#if ENABLE_CMD_HANDLER +#ifdef ENABLE_CMD_HANDLER #include CommandProcessing::Handler commandHandler; #endif @@ -13,10 +13,12 @@ CommandProcessing::Handler commandHandler; #define WIFI_PWD "PleaseEnterPass" #endif +namespace +{ HttpServer server; -unsigned totalActiveSockets = 0; +unsigned totalActiveSockets; -CUserData userGeorge("George", "I like SMING"); +CUserData userGeorge; void onIndex(HttpRequest& request, HttpResponse& response) { @@ -38,6 +40,14 @@ void onFile(HttpRequest& request, HttpResponse& response) } } +void shutdownServer() +{ + // Don't shutdown immediately, wait a bit to allow messages to propagate + auto timer = new AutoDeleteTimer; + timer->initializeMs<1000>([&]() { server.shutdown(); }); + timer->startOnce(); +} + void wsConnected(WebsocketConnection& socket) { totalActiveSockets++; @@ -58,37 +68,29 @@ void wsMessageReceived(WebsocketConnection& socket, const String& message) if(message == _F("shutdown")) { String message(F("The server is shutting down...")); socket.broadcast(message); - - // Don't shutdown immediately, wait a bit to allow messages to propagate - auto timer = new SimpleTimer; - timer->initializeMs<1000>( - [](void* timer) { - delete static_cast(timer); - server.shutdown(); - }, - timer); - timer->startOnce(); + shutdownServer(); return; } String response = F("Echo: ") + message; socket.sendString(response); - //Normally you would use dynamic cast but just be careful not to convert to wrong object type! - auto user = reinterpret_cast(socket.getUserData()); + // Normally you would use dynamic cast but just be careful not to convert to wrong object type! + auto user = static_cast(socket.getUserData()); if(user != nullptr) { user->printMessage(socket, message); } } -#if ENABLE_CMD_HANDLER +#ifdef ENABLE_CMD_HANDLER void wsCommandReceived(WebsocketConnection& socket, const String& message) { + debug_i("%s(%s)", __FUNCTION__, message.c_str()); String response = commandHandler.processNow(message.c_str(), message.length()); socket.sendString(response); - //Normally you would use dynamic cast but just be careful not to convert to wrong object type! - auto user = reinterpret_cast(socket.getUserData()); + // Normally you would use dynamic cast but just be careful not to convert to wrong object type! + auto user = static_cast(socket.getUserData()); if(user != nullptr) { user->printMessage(socket, message); } @@ -96,15 +98,7 @@ void wsCommandReceived(WebsocketConnection& socket, const String& message) void processShutdownCommand(String commandLine, ReadWriteStream& commandOutput) { - // Don't shutdown immediately, wait a bit to allow messages to propagate - auto timer = new SimpleTimer; - timer->initializeMs<1000>( - [](void* timer) { - delete static_cast(timer); - server.shutdown(); - }, - timer); - timer->startOnce(); + shutdownServer(); } #endif @@ -117,8 +111,8 @@ void wsDisconnected(WebsocketConnection& socket) { totalActiveSockets--; - //Normally you would use dynamic cast but just be careful not to convert to wrong object type! - auto user = reinterpret_cast(socket.getUserData()); + // Normally you would use dynamic cast but just be careful not to convert to wrong object type! + auto user = static_cast(socket.getUserData()); if(user != nullptr) { user->removeSession(socket); } @@ -138,7 +132,7 @@ void startWebServer() auto wsResource = new WebsocketResource(); wsResource->setConnectionHandler(wsConnected); wsResource->setMessageHandler(wsMessageReceived); -#if ENABLE_CMD_HANDLER +#ifdef ENABLE_CMD_HANDLER wsResource->setMessageHandler(wsCommandReceived); #endif @@ -159,21 +153,25 @@ void gotIP(IpAddress ip, IpAddress netmask, IpAddress gateway) startWebServer(); } +} // namespace + void init() { spiffs_mount(); // Mount file system, in order to work with files -#if ENABLE_CMD_HANDLER +#ifdef ENABLE_CMD_HANDLER commandHandler.registerSystemCommands(); commandHandler.registerCommand( {CMDP_STRINGS("shutdown", "Shutdown Server Command", "Application"), processShutdownCommand}); #endif + userGeorge = CUserData{F("George"), F("I like SMING")}; + Serial.begin(SERIAL_BAUD_RATE); // 115200 by default Serial.systemDebugOutput(true); // Enable debug output to serial WifiStation.enable(true); - WifiStation.config(WIFI_SSID, WIFI_PWD); + WifiStation.config(F(WIFI_SSID), F(WIFI_PWD)); WifiAccessPoint.enable(false); // Run our method when station was connected to AP diff --git a/samples/HttpServer_WebSockets/component.mk b/samples/HttpServer_WebSockets/component.mk index 25bbd74f26..4c7fcfebf6 100644 --- a/samples/HttpServer_WebSockets/component.mk +++ b/samples/HttpServer_WebSockets/component.mk @@ -3,8 +3,7 @@ HWCONFIG := spiffs-2m COMPONENT_VARS += ENABLE_CMD_HANDLER ENABLE_CMD_HANDLER ?= 1 -ifeq ($(ENABLE_CMD_HANDLER), 1) - COMPONENT_DEPENDS += CommandProcessing +ifeq ($(ENABLE_CMD_HANDLER),1) +COMPONENT_DEPENDS += CommandProcessing +APP_CFLAGS += -DENABLE_CMD_HANDLER=1 endif - -APP_CFLAGS += -DENABLE_CMD_HANDLER=$(ENABLE_CMD_HANDLER) \ No newline at end of file diff --git a/samples/HttpServer_WebSockets/include/CUserData.h b/samples/HttpServer_WebSockets/include/CUserData.h index 74e5d7024a..bf93d68c2e 100644 --- a/samples/HttpServer_WebSockets/include/CUserData.h +++ b/samples/HttpServer_WebSockets/include/CUserData.h @@ -1,13 +1,16 @@ -#ifndef C_USER_DATA_H_SAMPLE -#define C_USER_DATA_H_SAMPLE +#pragma once #include -//Simplified container modelling a user session +// Simplified container modelling a user session class CUserData { public: - CUserData(const char* uName, const char* uData) : userName(uName), userData(uData) + CUserData() + { + } + + CUserData(const String& uName, const String& uData) : userName(uName), userData(uData) { } @@ -26,5 +29,3 @@ class CUserData String userData; Vector activeWebSockets; }; - -#endif /*C_USER_DATA_H_SAMPLE*/ diff --git a/samples/Humidity_AM2321/app/application.cpp b/samples/Humidity_AM2321/app/application.cpp index 0890214c5e..5b01a7f96b 100644 --- a/samples/Humidity_AM2321/app/application.cpp +++ b/samples/Humidity_AM2321/app/application.cpp @@ -1,9 +1,11 @@ #include #include "Libraries/AM2321/AM2321.h" +namespace +{ AM2321 am2321; -Timer procTimer; +SimpleTimer procTimer; bool state = true; // You can change I2C pins here: @@ -15,6 +17,8 @@ void read() Serial << am2321.read() << ',' << am2321.temperature / 10.0 << ',' << am2321.humidity / 10.0 << endl; } +} // namespace + void init() { Serial.begin(SERIAL_BAUD_RATE); // 115200 by default @@ -32,5 +36,5 @@ void init() am2321.begin(); // REQUIRED. Call it after choosing I2C pins. Serial.println(am2321.uid()); - procTimer.initializeMs(3000, read).start(); + procTimer.initializeMs<3000>(read).start(); } diff --git a/samples/Humidity_BME280/app/application.cpp b/samples/Humidity_BME280/app/application.cpp index 395d5a62a0..0f8b38eb41 100644 --- a/samples/Humidity_BME280/app/application.cpp +++ b/samples/Humidity_BME280/app/application.cpp @@ -24,8 +24,10 @@ #define SEALEVELPRESSURE_HPA (1013.25) +namespace +{ Adafruit_BME280 bme; -Timer procTimer; +SimpleTimer procTimer; // Will use default pins for selected architecture. You can override values here // #define SDA 4 @@ -40,6 +42,8 @@ void printValues() Serial.println(); } +} // namespace + void init() { Serial.begin(SERIAL_BAUD_RATE); // 115200 by default diff --git a/samples/Humidity_DHT22/app/application.cpp b/samples/Humidity_DHT22/app/application.cpp index 4f5f8e09ba..8849ad545c 100644 --- a/samples/Humidity_DHT22/app/application.cpp +++ b/samples/Humidity_DHT22/app/application.cpp @@ -4,24 +4,10 @@ //#define WORK_PIN 14 // GPIO14 #define WORK_PIN 2 -DHTesp dht; - -Timer readTemperatureProcTimer; -void onTimer_readTemperatures(); - -void init() +namespace { - Serial.begin(SERIAL_BAUD_RATE); // 115200 by default - Serial.systemDebugOutput(true); // Allow debug output to serial - - dht.setup(WORK_PIN, DHTesp::DHT22); - readTemperatureProcTimer.initializeMs(5 * 1000, onTimer_readTemperatures).start(); // every so often. - - Serial.println(_F("\r\n" - "DHT improved lib")); - Serial << _F("TickCount=") << RTC.getRtcNanoseconds() / 1000000 - << _F("; Need to wait 1 second for the sensor to boot up") << endl; -} +DHTesp dht; +SimpleTimer readTemperatureProcTimer; void onTimer_readTemperatures() { @@ -107,10 +93,25 @@ void onTimer_readTemperatures() Serial.print(_F("Cold And Dry")); break; default: - Serial.print(_F("Unknown:")); - Serial.print(cf); + Serial << _F("Unknown:") << cf; break; } Serial.println(')'); } + +} // namespace + +void init() +{ + Serial.begin(SERIAL_BAUD_RATE); // 115200 by default + Serial.systemDebugOutput(true); // Allow debug output to serial + + dht.setup(WORK_PIN, DHTesp::DHT22); + readTemperatureProcTimer.initializeMs<5 * 1000>(onTimer_readTemperatures).start(); // every so often. + + Serial << endl + << _F("DHT improved lib") << endl + << _F("TickCount=") << RTC.getRtcNanoseconds() / 1000000 + << _F("; Need to wait 1 second for the sensor to boot up") << endl; +} diff --git a/samples/Humidity_SI7021/app/application.cpp b/samples/Humidity_SI7021/app/application.cpp index 8861e78543..fe189dfdd4 100644 --- a/samples/Humidity_SI7021/app/application.cpp +++ b/samples/Humidity_SI7021/app/application.cpp @@ -2,13 +2,15 @@ #include #include -SI7021 hydrometer; -Timer procTimer_ht; -Timer procTimer_olt; - #define I2C_SCL 5 // SCL #define I2C_SDA 4 // SCA +namespace +{ +SI7021 hydrometer; +SimpleTimer procTimer_ht; +SimpleTimer procTimer_olt; + //LN10 - 2.30258509299404568402 double getDewPoint(unsigned int humidity, int temperature) { @@ -21,7 +23,9 @@ void si_read_ht() { if(!hydrometer.begin()) { Serial.println(_F("Could not connect to SI7021.")); + return; } + Serial.println(_F("Start reading Humidity and Temperature")); si7021_env env_data = hydrometer.getHumidityAndTemperature(); @@ -39,7 +43,9 @@ void si_read_olt() { if(!hydrometer.begin()) { Serial.println(_F("Could not connect to SI7021.")); + return; } + Serial.println(_F("Start reading Temperature")); si7021_olt olt_data = hydrometer.getTemperatureOlt(); @@ -51,13 +57,17 @@ void si_read_olt() } } +} // namespace + void init() { Serial.begin(SERIAL_BAUD_RATE); // 115200 by default Serial.systemDebugOutput(true); // Allow debug output to serial - Serial.print(_F("Start I2c")); + + Serial.println(_F("Start I2c")); Wire.pins(I2C_SDA, I2C_SCL); // SDA, SCL Wire.begin(); - procTimer_ht.initializeMs(10000, si_read_ht).start(); - procTimer_olt.initializeMs(15000, si_read_olt).start(); + + procTimer_ht.initializeMs<10000>(si_read_ht).start(); + procTimer_olt.initializeMs<15000>(si_read_olt).start(); } diff --git a/samples/IR_lib/app/application.cpp b/samples/IR_lib/app/application.cpp index fa97eb7c7d..1f8ba9c3f5 100644 --- a/samples/IR_lib/app/application.cpp +++ b/samples/IR_lib/app/application.cpp @@ -10,13 +10,14 @@ #define IR_RECV_PIN 12 // GPIO12 #define IR_SEND_PIN 5 // GPIO5 -Timer irTimer; +namespace +{ +SimpleTimer irTimer; IRrecv irrecv(IR_RECV_PIN); IRsend irsend(IR_SEND_PIN); void receiveIR() { - irTimer.stop(); decode_results dresults; dresults.decode_type = UNUSED; if(irrecv.decode(&dresults)) { @@ -28,14 +29,19 @@ void receiveIR() Serial.println("Send IR Code"); irsend.send(dresults.decode_type, dresults.value, dresults.bits); } - irTimer.start(); + irTimer.startOnce(); } +} // namespace + void init() { Serial.begin(SERIAL_BAUD_RATE); // 115200 by default + Serial.println("Setting up..."); + irrecv.enableIRIn(); // Start the receiver - irTimer.initializeMs(200, receiveIR).start(); + irTimer.initializeMs<200>(receiveIR).startOnce(); + Serial.println("Ready..."); } diff --git a/samples/LED_WS2812/app/application.cpp b/samples/LED_WS2812/app/application.cpp index 7e39358bad..bff8a7e489 100644 --- a/samples/LED_WS2812/app/application.cpp +++ b/samples/LED_WS2812/app/application.cpp @@ -2,18 +2,26 @@ #define LED_PIN 2 // GPIO2 -void init() +namespace { - while(true) { - char buffer1[] = "\x40\x00\x00\x00\x40\x00\x00\x00\x40"; - ws2812_writergb(LED_PIN, buffer1, sizeof(buffer1)); - os_delay_us(500000); - - //We need to feed WDT. - WDT.alive(); +SimpleTimer procTimer; +bool state; +void update() +{ + if(state) { char buffer2[] = "\x00\x40\x40\x40\x00\x40\x40\x40\x00"; ws2812_writergb(LED_PIN, buffer2, sizeof(buffer2)); - os_delay_us(500000); + } else { + char buffer1[] = "\x40\x00\x00\x00\x40\x00\x00\x00\x40"; + ws2812_writergb(LED_PIN, buffer1, sizeof(buffer1)); } + state = !state; +} + +} // namespace + +void init() +{ + procTimer.initializeMs<500>(update).start(); } diff --git a/samples/LED_YeelightBulb/app/application.cpp b/samples/LED_YeelightBulb/app/application.cpp index 734e681cfa..384d0e1d12 100644 --- a/samples/LED_YeelightBulb/app/application.cpp +++ b/samples/LED_YeelightBulb/app/application.cpp @@ -7,11 +7,13 @@ #define WIFI_PWD "PleaseEnterPass" #endif +namespace +{ // Enter your bulb IP here: YeelightBulb bulb(IpAddress("192.168.1.100")); -Timer procTimer; -bool state = false; +SimpleTimer procTimer; +bool state; void blink() { @@ -27,13 +29,15 @@ void blink() // Will be called when WiFi station was connected to AP void gotIP(IpAddress ip, IpAddress netmask, IpAddress gateway) { - debugf("I'm CONNECTED"); + Serial << _F("I'm CONNECTED, IP ") << ip << endl; // Connection to Yeelight Bulb will be established on any first action: bulb.updateState(); // Read actual bulb state - procTimer.initializeMs(5000, blink).start(); + procTimer.initializeMs<5000>(blink).start(); } +} // namespace + void init() { Serial.begin(SERIAL_BAUD_RATE); // 115200 by default diff --git a/samples/Light_BH1750/app/application.cpp b/samples/Light_BH1750/app/application.cpp index d55733ceb3..790e8de670 100644 --- a/samples/Light_BH1750/app/application.cpp +++ b/samples/Light_BH1750/app/application.cpp @@ -1,6 +1,8 @@ #include #include +namespace +{ /* Set the Address pin state to change I2C address: BH1750FVI_ADDRESS_LOW "0x23" - usually by default @@ -8,7 +10,7 @@ BH1750FVI_ADDRESS_HIGH "0x5C" */ BH1750FVI LightSensor(BH1750FVI_ADDRESS_LOW); -Timer procTimer; +SimpleTimer procTimer; void readLight() { @@ -19,15 +21,19 @@ void readLight() Serial.println(" lux"); } +} // namespace + void init() { Serial.begin(SERIAL_BAUD_RATE); // 115200 by default Serial.systemDebugOutput(false); // Disable debug output to serial - if(LightSensor.begin() == 0) - Serial.println(_F("LightSensor initialized")); - else + if(LightSensor.begin() != 0) { Serial.println(_F("LightSensor not available. May be wrong I2C address?")); + return; + } + + Serial.println(_F("LightSensor initialized")); /* Set the Working Mode for this sensor @@ -36,5 +42,5 @@ void init() LightSensor.setMode(BH1750_Continuous_H_resolution_Mode); // Start reading loop - procTimer.initializeMs(300, readLight).start(); + procTimer.initializeMs<300>(readLight).start(); } diff --git a/samples/LiquidCrystal_44780/app/application.cpp b/samples/LiquidCrystal_44780/app/application.cpp index 7bf4477424..5462634d21 100644 --- a/samples/LiquidCrystal_44780/app/application.cpp +++ b/samples/LiquidCrystal_44780/app/application.cpp @@ -9,8 +9,54 @@ // For more information visit useful wiki page: http://arduino-info.wikispaces.com/LCD-Blue-I2C #define I2C_LCD_ADDR 0x27 + LiquidCrystal_I2C lcd(I2C_LCD_ADDR, 2, 1, 0, 4, 5, 6, 7, 3, POSITIVE); +SimpleTimer flashTimer; + +//-------- Write characters on the display ------------------ +void writeDisplay() +{ + debug_d("%s", __FUNCTION__); + + // NOTE: Cursor Position: (CHAR, LINE) start at 0 + lcd.setCursor(0, 0); + lcd.print(_F("SMING: Let's do")); + lcd.setCursor(0, 1); + lcd.print(_F("smart things!")); +} + +// ------- Quick 3 blinks of backlight ------------- +void flashBacklight() +{ + static unsigned state; + + if(state == 6) { + debug_d("%s DONE", __FUNCTION__); + lcd.backlight(); // finish with backlight on + writeDisplay(); + return; + } + + if(state == 0) { + debug_d("%s INIT", __FUNCTION__); + flashTimer.setCallback(flashBacklight); + } + + if(state & 0x01) { + debug_d("%s OFF", __FUNCTION__); + lcd.noBacklight(); + flashTimer.setIntervalMs<250>(); + } else { + debug_d("%s ON", __FUNCTION__); + lcd.backlight(); + flashTimer.setIntervalMs<150>(); + } + + ++state; + flashTimer.startOnce(); +} + void init() { Serial.begin(SERIAL_BAUD_RATE); // 115200 by default @@ -20,19 +66,5 @@ void init() lcd.begin(16, 2); // initialize the lcd for 16 chars 2 lines, turn on backlight - // ------- Quick 3 blinks of backlight ------------- - for(int i = 0; i < 3; i++) { - lcd.backlight(); - delay(150); - lcd.noBacklight(); - delay(250); - } - lcd.backlight(); // finish with backlight on - - //-------- Write characters on the display ------------------ - // NOTE: Cursor Position: (CHAR, LINE) start at 0 - lcd.setCursor(0, 0); - lcd.print(_F("SMING: Let's do")); - lcd.setCursor(0, 1); - lcd.print(_F("smart things!")); + flashBacklight(); } diff --git a/samples/LiveDebug/app/application.cpp b/samples/LiveDebug/app/application.cpp index 0adca0af28..6606f63742 100644 --- a/samples/LiveDebug/app/application.cpp +++ b/samples/LiveDebug/app/application.cpp @@ -8,6 +8,8 @@ #define LED_PIN 2 // Note: LED is attached to UART1 TX output +namespace +{ // Max length of debug command const unsigned MAX_COMMAND_LENGTH = 64; @@ -49,18 +51,15 @@ Timer procTimer; #define CALLBACK_ATTR GDB_IRAM_ATTR #endif -// See blink() -bool ledState = true; - // A simple log file stored on the host -static GdbFileStream logFile; +GdbFileStream logFile; #define LOG_FILENAME "testlog.txt" // Handles messages from SDK -static OsMessageInterceptor osMessageInterceptor; +OsMessageInterceptor osMessageInterceptor; // Supports `consoleOff` command to prevent re-enabling when debugger is attached -bool consoleOffRequested = false; +bool consoleOffRequested; // IFS::Gdb::FileSystem gdbfs; @@ -75,8 +74,10 @@ void readConsole(); */ void CALLBACK_ATTR blink() { - digitalWrite(LED_PIN, ledState); + static bool ledState; + ledState = !ledState; + digitalWrite(LED_PIN, ledState); } void showPrompt() @@ -418,7 +419,7 @@ COMMAND_HANDLER(write0) * @param msg * @retval bool true if we want to report this */ -static bool __noinline parseOsMessage(OsMessage& msg) +bool __noinline parseOsMessage(OsMessage& msg) { m_printf(_F("[OS] %s\r\n"), msg.getBuffer()); if(msg.startsWith(_F("E:M "))) { @@ -436,7 +437,7 @@ static bool __noinline parseOsMessage(OsMessage& msg) * @brief Called when the OS outputs a debug message using os_printf, etc. * @param msg The message */ -static void onOsMessage(OsMessage& msg) +void onOsMessage(OsMessage& msg) { // Note: We do the check in a separate function to avoid messing up the stack pointer if(parseOsMessage(msg)) { @@ -622,6 +623,14 @@ void readConsole() })); } +void printTimerDetails() +{ + Serial << procTimer << ", maxTicks = " << procTimer.maxTicks() + << ", maxTime = " << procTimer.micros().ticksToTime(procTimer.maxTicks()).value() << endl; +} + +} // namespace + extern "C" void gdb_on_attach(bool attached) { debug_i("GdbAttach(%d)", attached); @@ -646,12 +655,6 @@ extern "C" void gdb_on_attach(bool attached) } } -static void printTimerDetails() -{ - Serial << procTimer << ", maxTicks = " << procTimer.maxTicks() - << ", maxTime = " << procTimer.micros().ticksToTime(procTimer.maxTicks()).value() << endl; -} - void GDB_IRAM_ATTR init() { Serial.begin(SERIAL_BAUD_RATE); diff --git a/samples/MeteoControl/app/application.cpp b/samples/MeteoControl/app/application.cpp index b320285b4d..9509a4a92a 100644 --- a/samples/MeteoControl/app/application.cpp +++ b/samples/MeteoControl/app/application.cpp @@ -10,6 +10,11 @@ #include "special_chars.h" #include "webserver.h" +// Sensors string values +String StrT, StrRH, StrTime; + +namespace +{ DHTesp dht; // For more information visit useful wiki page: http://arduino-info.wikispaces.com/LCD-Blue-I2C @@ -17,53 +22,9 @@ DHTesp dht; #define I2C_LCD_ADDR 0x27 LiquidCrystal_I2C lcd(I2C_LCD_ADDR, 2, 1, 0, 4, 5, 6, 7, 3, POSITIVE); -Timer procTimer; -Timer displayTimer; +SimpleTimer procTimer; +SimpleTimer displayTimer; bool state = true; -// Sensors string values -String StrT, StrRH, StrTime; - -void process(); -void connectOk(const String& SSID, MacAddress bssid, uint8_t channel); -void connectFail(const String& ssid, MacAddress bssid, WifiDisconnectReason reason); -void gotIP(IpAddress ip, IpAddress netmask, IpAddress gateway); - -void init() -{ - spiffs_mount(); // Mount file system, in order to work with files - - Serial.begin(SERIAL_BAUD_RATE); // 115200 by default - Serial.systemDebugOutput(false); // Debug output to serial - - ActiveConfig = loadConfig(); - - // Select control line - pinMode(CONTROL_PIN, OUTPUT); - - // DHT sensor start - dht.setup(DHT_PIN, DHTesp::DHT11); - - lcd.begin(16, 2); - lcd.backlight(); - lcd.createChar(1, icon_termometer); - lcd.createChar(2, icon_water); - lcd.createChar(3, celsius); - lcd.createChar(4, icon_retarrow); - lcd.createChar(5, icon_clock); - lcd.createChar(6, icon_cross); - lcd.createChar(7, icon_check); - - WifiStation.config(ActiveConfig.NetworkSSID, ActiveConfig.NetworkPassword); - WifiStation.enable(true); - WifiAccessPoint.enable(false); - - WifiEvents.onStationConnect(connectOk); - WifiEvents.onStationDisconnect(connectFail); - WifiEvents.onStationGotIP(gotIP); - - procTimer.initializeMs(5000, process).start(); - process(); -} void showValues() { @@ -93,17 +54,19 @@ void process() float t = dht.getTemperature() + ActiveConfig.AddT; float h = dht.getHumidity() + ActiveConfig.AddRH; - if(ActiveConfig.Trigger == eTT_Temperature) + if(ActiveConfig.Trigger == eTT_Temperature) { state = t < ActiveConfig.RangeMin || t > ActiveConfig.RangeMax; - else if(ActiveConfig.Trigger == eTT_Humidity) + } else if(ActiveConfig.Trigger == eTT_Humidity) { state = h < ActiveConfig.RangeMin || h > ActiveConfig.RangeMax; + } digitalWrite(CONTROL_PIN, state); StrT = String(t, 0); StrRH = String(h, 0); - if(!displayTimer.isStarted()) - displayTimer.initializeMs(1000, showValues).start(); + if(!displayTimer.isStarted()) { + displayTimer.initializeMs<1000>(showValues).start(); + } } void connectOk(const String& SSID, MacAddress bssid, uint8_t channel) @@ -115,8 +78,7 @@ void connectOk(const String& SSID, MacAddress bssid, uint8_t channel) void gotIP(IpAddress ip, IpAddress netmask, IpAddress gateway) { lcd.clear(); - lcd.print("\7 "); - lcd.print(ip); + lcd << "\7 " << ip; // Restart main screen output procTimer.restart(); displayTimer.stop(); @@ -124,10 +86,11 @@ void gotIP(IpAddress ip, IpAddress netmask, IpAddress gateway) startWebClock(); // At first run we will download web server content if(!fileExist("index.html") || !fileExist("config.html") || !fileExist("api.html") || - !fileExist("bootstrap.css.gz") || !fileExist("jquery.js.gz")) + !fileExist("bootstrap.css.gz") || !fileExist("jquery.js.gz")) { downloadContentFiles(); - else + } else { startWebServer(); + } } void connectFail(const String& ssid, MacAddress bssid, WifiDisconnectReason reason) @@ -152,26 +115,31 @@ void connectFail(const String& ssid, MacAddress bssid, WifiDisconnectReason reas } ////// WEB Clock ////// -Timer clockRefresher; + +SimpleTimer clockRefresher; HttpClient clockWebClient; -uint32_t lastClockUpdate = 0; +uint32_t lastClockUpdate; DateTime clockValue; const int clockUpdateIntervalMs = 10 * 60 * 1000; // Update web clock every 10 minutes int onClockUpdating(HttpConnection& client, bool successful) { + auto& response = *client.getResponse(); + if(!successful) { - debugf("CLOCK UPDATE FAILED %d (code: %d)", successful, client.getResponse()->code); + Serial << _F("CLOCK UPDATE FAILED, ") << response.code << " " << toString(response.code) << endl; lastClockUpdate = 0; return -1; } // Extract date header from response - clockValue = client.getResponse()->headers.getServerDate(); - if(clockValue.isNull()) - clockValue = client.getResponse()->headers.getLastModifiedDate(); - if(!clockValue.isNull()) + clockValue = response.headers.getServerDate(); + if(clockValue.isNull()) { + clockValue = response.headers.getLastModifiedDate(); + } + if(!clockValue.isNull()) { clockValue.addMilliseconds(ActiveConfig.AddTZ * 1000 * 60 * 60); + } return 0; } @@ -179,27 +147,65 @@ int onClockUpdating(HttpConnection& client, bool successful) void refreshClockTime() { uint32_t nowClock = millis(); - if(nowClock < lastClockUpdate) + if(nowClock < lastClockUpdate) { lastClockUpdate = 0; // Prevent overflow, restart + } if((lastClockUpdate == 0 || nowClock - lastClockUpdate > clockUpdateIntervalMs)) { clockWebClient.downloadString("google.com", onClockUpdating); lastClockUpdate = nowClock; - } else if(!clockValue.isNull()) + } else if(!clockValue.isNull()) { clockValue.addMilliseconds(clockRefresher.getIntervalMs()); + } - if(!clockValue.isNull()) { - StrTime = clockValue.toShortDateString() + " " + clockValue.toShortTimeString(false); - - if((millis() % 2000) > 1000) - StrTime.setCharAt(13, ' '); - else - StrTime.setCharAt(13, ':'); + if(clockValue.isNull()) { + return; } + + StrTime = clockValue.toShortDateString() + " " + clockValue.toShortTimeString(false); + StrTime.setCharAt(13, ((nowClock % 2000) > 1000) ? ' ' : ':'); } +} // namespace + void startWebClock() { lastClockUpdate = 0; - clockRefresher.stop(); - clockRefresher.initializeMs(500, refreshClockTime).start(); + clockRefresher.initializeMs<500>(refreshClockTime).start(); +} + +void init() +{ + Serial.begin(SERIAL_BAUD_RATE); // 115200 by default + Serial.systemDebugOutput(false); // Debug output to serial + + spiffs_mount(); // Mount file system, in order to work with files + + ActiveConfig = loadConfig(); + + // Select control line + pinMode(CONTROL_PIN, OUTPUT); + + // DHT sensor start + dht.setup(DHT_PIN, DHTesp::DHT11); + + lcd.begin(16, 2); + lcd.backlight(); + lcd.createChar(1, icon_termometer); + lcd.createChar(2, icon_water); + lcd.createChar(3, celsius); + lcd.createChar(4, icon_retarrow); + lcd.createChar(5, icon_clock); + lcd.createChar(6, icon_cross); + lcd.createChar(7, icon_check); + + WifiStation.config(ActiveConfig.NetworkSSID, ActiveConfig.NetworkPassword); + WifiStation.enable(true); + WifiAccessPoint.enable(false); + + WifiEvents.onStationConnect(connectOk); + WifiEvents.onStationDisconnect(connectFail); + WifiEvents.onStationGotIP(gotIP); + + procTimer.initializeMs<5000>(process).start(); + process(); } diff --git a/samples/MeteoControl/app/configuration.cpp b/samples/MeteoControl/app/configuration.cpp index 603130b664..fb844d0977 100644 --- a/samples/MeteoControl/app/configuration.cpp +++ b/samples/MeteoControl/app/configuration.cpp @@ -1,7 +1,5 @@ #include "configuration.h" -#include - MeteoConfig ActiveConfig; MeteoConfig loadConfig() @@ -23,8 +21,8 @@ MeteoConfig loadConfig() cfg.RangeMin = trigger["min"]; cfg.RangeMax = trigger["max"]; } else { - cfg.NetworkSSID = WIFI_SSID; - cfg.NetworkPassword = WIFI_PWD; + cfg.NetworkSSID = F(WIFI_SSID); + cfg.NetworkPassword = F(WIFI_PWD); } return cfg; } diff --git a/samples/MeteoControl/app/webserver.cpp b/samples/MeteoControl/app/webserver.cpp index bc1cc12adb..530dda51da 100644 --- a/samples/MeteoControl/app/webserver.cpp +++ b/samples/MeteoControl/app/webserver.cpp @@ -1,11 +1,16 @@ #include - #include "configuration.h" +#include "webserver.h" -bool serverStarted = false; -HttpServer server; extern String StrT, StrRH; // Sensors string values +namespace +{ +HttpServer server; +HttpClient downloadClient; +bool serverStarted = false; +int dowfid = 0; + void onIndex(HttpRequest& request, HttpResponse& response) { TemplateFileStream* tmpl = new TemplateFileStream("index.html"); @@ -92,23 +97,44 @@ void onApiSensors(HttpRequest& request, HttpResponse& response) void onApiOutput(HttpRequest& request, HttpResponse& response) { int val = request.getQueryParameter("control", "-1").toInt(); - if(val == 0 || val == 1) + if(val == 0 || val == 1) { digitalWrite(CONTROL_PIN, val == 1); - else + } else { val = -1; + } JsonObjectStream* stream = new JsonObjectStream(); JsonObject json = stream->getRoot(); - json["status"] = val != -1; - if(val == -1) + json["status"] = val >= 0; + if(val < 0) json["error"] = "Wrong control parameter value, please use: ?control=0|1"; response.sendDataStream(stream, MIME_JSON); } +} // namespace + +void downloadContentFiles() +{ + debugf("DownloadContentFiles"); + + downloadClient.downloadFile(F("http://simple.anakod.ru/templates/MeteoControl/MeteoControl.html"), "index.html"); + downloadClient.downloadFile(F("http://simple.anakod.ru/templates/MeteoControl/MeteoConfig.html"), "config.html"); + downloadClient.downloadFile(F("http://simple.anakod.ru/templates/MeteoControl/MeteoAPI.html"), "api.html"); + downloadClient.downloadFile(F("http://simple.anakod.ru/templates/bootstrap.css.gz")); + downloadClient.downloadFile(F("http://simple.anakod.ru/templates/jquery.js.gz"), + [](HttpConnection& connection, bool success) -> int { + if(success) { + startWebServer(); + } + return 0; + }); +} + void startWebServer() { - if(serverStarted) + if(serverStarted) { return; + } server.listen(80); server.paths.set("/", onIndex); @@ -124,24 +150,3 @@ void startWebServer() if(WifiAccessPoint.isEnabled()) debugf("AP: %s", WifiAccessPoint.getIP().toString().c_str()); } - -/// FileSystem Initialization /// - -HttpClient downloadClient; -int dowfid = 0; -void downloadContentFiles() -{ - debugf("DownloadContentFiles"); - - downloadClient.downloadFile("http://simple.anakod.ru/templates/MeteoControl/MeteoControl.html", "index.html"); - downloadClient.downloadFile("http://simple.anakod.ru/templates/MeteoControl/MeteoConfig.html", "config.html"); - downloadClient.downloadFile("http://simple.anakod.ru/templates/MeteoControl/MeteoAPI.html", "api.html"); - downloadClient.downloadFile("http://simple.anakod.ru/templates/bootstrap.css.gz"); - downloadClient.downloadFile("http://simple.anakod.ru/templates/jquery.js.gz", - (RequestCompletedDelegate)([](HttpConnection& connection, bool success) -> int { - if(success) { - startWebServer(); - } - return 0; - })); -} diff --git a/samples/MeteoControl/include/configuration.h b/samples/MeteoControl/include/configuration.h index d39a298f39..24b490c888 100644 --- a/samples/MeteoControl/include/configuration.h +++ b/samples/MeteoControl/include/configuration.h @@ -1,5 +1,4 @@ -#ifndef INCLUDE_CONFIGURATION_H_ -#define INCLUDE_CONFIGURATION_H_ +#pragma once #include #include @@ -20,7 +19,11 @@ #define METEO_CONFIG_FILE ".meteo.conf" // leading point for security reasons :) -enum TriggerType { eTT_None = 0, eTT_Temperature, eTT_Humidity }; +enum TriggerType { + eTT_None = 0, + eTT_Temperature, + eTT_Humidity, +}; struct MeteoConfig { MeteoConfig() @@ -50,5 +53,3 @@ void saveConfig(MeteoConfig& cfg); extern void startWebClock(); extern MeteoConfig ActiveConfig; - -#endif /* INCLUDE_CONFIGURATION_H_ */ diff --git a/samples/MeteoControl/include/webserver.h b/samples/MeteoControl/include/webserver.h index 9ce09aa046..54f6987fc9 100644 --- a/samples/MeteoControl/include/webserver.h +++ b/samples/MeteoControl/include/webserver.h @@ -1,7 +1,4 @@ -#ifndef INCLUDE_WEBSERVER_H_ -#define INCLUDE_WEBSERVER_H_ +#pragma once void startWebServer(); void downloadContentFiles(); - -#endif /* INCLUDE_WEBSERVER_H_ */ diff --git a/samples/MeteoControl_mqtt/app/application.cpp b/samples/MeteoControl_mqtt/app/application.cpp index 9fb95ca902..5851177202 100644 --- a/samples/MeteoControl_mqtt/app/application.cpp +++ b/samples/MeteoControl_mqtt/app/application.cpp @@ -1,13 +1,14 @@ #include #include - #include "configuration.h" // application configuration -#include "bmp180.cpp" // bmp180 configuration -#include "si7021.cpp" // htu21d configuration +extern void BMPinit(); +extern void SIinit(); -Timer publishTimer; +MqttClient mqtt; +namespace +{ // Publish our message void publishMessage() // uncomment timer in connectOk() if need publishMessage() loop { @@ -26,7 +27,15 @@ int onMessageReceived(MqttClient& client, mqtt_message_t* message) return 0; } -// Run MQTT client +void gotIP(IpAddress ip, IpAddress netmask, IpAddress gateway) +{ + Serial << _F("Connected: ") << ip << endl; + startMqttClient(); + publishMessage(); // run once publishMessage +} + +} // namespace + void startMqttClient() { Url url(URI_SCHEME_MQTT, F(LOG), F(PASS), F(MQTT_SERVER), MQTT_PORT); @@ -35,13 +44,6 @@ void startMqttClient() mqtt.subscribe(SUB_TOPIC); } -void gotIP(IpAddress ip, IpAddress netmask, IpAddress gateway) -{ - Serial << _F("Connected: ") << ip << endl; - startMqttClient(); - publishMessage(); // run once publishMessage -} - void init() { Serial.begin(SERIAL_BAUD_RATE); // 115200 by default diff --git a/samples/MeteoControl_mqtt/app/bmp180.cpp b/samples/MeteoControl_mqtt/app/bmp180.cpp index de51ac26c7..024b8c1696 100644 --- a/samples/MeteoControl_mqtt/app/bmp180.cpp +++ b/samples/MeteoControl_mqtt/app/bmp180.cpp @@ -1,6 +1,3 @@ -#ifndef INCLUDE_BMP180_H_ -#define INCLUDE_BMP180_H_ - #include #include @@ -9,9 +6,10 @@ // GPIO0 SCL // GPIO2 SDA +namespace +{ BMP180 barometer; - -Timer publishBMPTimer; +SimpleTimer publishBMPTimer; void publishBMP() { @@ -22,27 +20,29 @@ void publishBMP() Serial.println(_F("Start reading BMP180 sensor")); if(!barometer.EnsureConnected()) { Serial.println(_F("Could not connect to BMP180")); - } else { - // Retrieve the current pressure in Pascals - long currentPressure = barometer.GetPressure(); - // convert pressure to mmHg - float BMPPress = currentPressure / 133.322; - - // Print out the Pressure - Serial << _F("Pressure: ") << BMPPress << _F(" mmHg") << endl; - mqtt.publish(BMP_P, String(BMPPress)); - - // Retrieve the current temperature in degrees celsius - float BMPTemp = barometer.GetTemperature(); - - // Print out the Temperature - Serial << _F("Temperature: ") << BMPTemp << " °C" << endl; - mqtt.publish(BMP_T, String(BMPTemp)); - Serial.println(_F("BMP180 sensor read and transmitted to server\r\n" - "********************************************")); + return; } + // Retrieve the current pressure in Pascals + long currentPressure = barometer.GetPressure(); + // convert pressure to mmHg + float BMPPress = currentPressure / 133.322; + + // Print out the Pressure + Serial << _F("Pressure: ") << BMPPress << _F(" mmHg") << endl; + mqtt.publish(BMP_P, String(BMPPress)); + + // Retrieve the current temperature in degrees celsius + float BMPTemp = barometer.GetTemperature(); + + // Print out the Temperature + Serial << _F("Temperature: ") << BMPTemp << " °C" << endl; + mqtt.publish(BMP_T, String(BMPTemp)); + Serial.println(_F("BMP180 sensor read and transmitted to server\r\n" + "********************************************")); } +} // namespace + void BMPinit() { // When we have connected, we reset the device to ensure a clean start. @@ -52,7 +52,5 @@ void BMPinit() barometer.Initialize(); barometer.PrintCalibrationData(); - publishBMPTimer.initializeMs(TIMER * 3000, publishBMP).start(); // start publish BMP180 sensor data + publishBMPTimer.initializeMs(publishBMP).start(); // start publish BMP180 sensor data } - -#endif /* INCLUDE_BMP180_H_ */ diff --git a/samples/MeteoControl_mqtt/app/si7021.cpp b/samples/MeteoControl_mqtt/app/si7021.cpp index 8fb2c61fe6..7eb1000f20 100644 --- a/samples/MeteoControl_mqtt/app/si7021.cpp +++ b/samples/MeteoControl_mqtt/app/si7021.cpp @@ -1,14 +1,12 @@ -#ifndef INCLUDE_SI7021_H_ -#define INCLUDE_SI7021_H_ - #include #include #include "configuration.h" +namespace +{ SI7021 hydrometer; - -Timer publishSITimer; +SimpleTimer publishSITimer; void publishSI() { @@ -21,28 +19,30 @@ void publishSI() if(!hydrometer.begin()) { Serial.println(_F("Could not connect to SI7021")); - } else { - si7021_env data = hydrometer.getHumidityAndTemperature(); - if(data.error_crc == 1) { - Serial.println(_F("\tCRC ERROR")); - } else { - float SIhum = data.humidityPercent; - // Print out the humidity - Serial << _F("Humidity: ") << SIhum << '%' << endl; - mqtt.publish(SI_H, String(SIhum)); - float SITemp = data.temperature; - // Print out the Temperature - Serial << _F("Temperature: ") << SITemp / 100 << " °C" << endl; - mqtt.publish(SI_T, String(SITemp / 100)); - Serial.println(_F("SI sensor read and transmitted to server\r\n" - "*********************************************")); - } + return; + } + + si7021_env data = hydrometer.getHumidityAndTemperature(); + if(data.error_crc == 1) { + Serial.println(_F("\tCRC ERROR")); + return; } + + float SIhum = data.humidityPercent; + // Print out the humidity + Serial << _F("Humidity: ") << SIhum << '%' << endl; + mqtt.publish(SI_H, String(SIhum)); + float SITemp = data.temperature; + // Print out the Temperature + Serial << _F("Temperature: ") << SITemp / 100 << " °C" << endl; + mqtt.publish(SI_T, String(SITemp / 100)); + Serial.println(_F("SI sensor read and transmitted to server\r\n" + "*********************************************")); } +} // namespace + void SIinit() { - publishSITimer.initializeMs(TIMER * 1000, publishSI).start(); // start publish SI sensor data + publishSITimer.initializeMs(publishSI).start(); // start publish SI sensor data } - -#endif /* INCLUDE_SI7021_H_ */ diff --git a/samples/MeteoControl_mqtt/include/configuration.h b/samples/MeteoControl_mqtt/include/configuration.h index ba02ccdb83..a1d3cbf9ea 100644 --- a/samples/MeteoControl_mqtt/include/configuration.h +++ b/samples/MeteoControl_mqtt/include/configuration.h @@ -1,7 +1,6 @@ -#ifndef INCLUDE_CONFIGURATION_H_ -#define INCLUDE_CONFIGURATION_H_ +#pragma once -#include +#include //////////////////////////// Wi-Fi config /////////////////////////////////////// #ifndef WIFI_SSID @@ -31,13 +30,14 @@ #define SI_H "testing/status/SI7021/Humidity" #define VER_TOPIC "testing/firmware/version" -int TIMER = 20; // every N* seconds send to mqtt server +constexpr int TIMER = 20; // every N* seconds send to mqtt server -// Forward declarations -void startMqttClient(); - -MqttClient mqtt; +enum TriggerType { + eTT_None = 0, + eTT_Temperature, + eTT_Humidity, +}; -enum TriggerType { eTT_None = 0, eTT_Temperature, eTT_Humidity }; +void startMqttClient(); -#endif /* INCLUDE_CONFIGURATION_H_ */ +extern MqttClient mqtt; diff --git a/samples/MqttClient_Hello/app/application.cpp b/samples/MqttClient_Hello/app/application.cpp index d2f8772610..dc51bb8980 100644 --- a/samples/MqttClient_Hello/app/application.cpp +++ b/samples/MqttClient_Hello/app/application.cpp @@ -7,10 +7,12 @@ #define WIFI_PWD "PleaseEnterPass" #endif +namespace +{ // For testing purposes, try a few different URL formats -#define MQTT_URL1 "mqtt://test.mosquitto.org:1883" -#define MQTT_URL2 "mqtts://test.mosquitto.org:8883" // (Need ENABLE_SSL) -#define MQTT_URL3 "mqtt://frank:fiddle@192.168.100.107:1883" +DEFINE_FSTR(MQTT_URL1, "mqtt://test.mosquitto.org:1883") +DEFINE_FSTR(MQTT_URL2, "mqtts://test.mosquitto.org:8883") // (Need ENABLE_SSL) +DEFINE_FSTR(MQTT_URL3, "mqtt://frank:fiddle@192.168.100.107:1883") #ifdef ENABLE_SSL #include @@ -24,8 +26,7 @@ void startMqttClient(); MqttClient mqtt; - -Timer procTimer; +SimpleTimer procTimer; // Check for MQTT Disconnection void checkMQTTDisconnect(TcpClient& client, bool flag) @@ -37,7 +38,7 @@ void checkMQTTDisconnect(TcpClient& client, bool flag) } // Restart connection attempt after few seconds - procTimer.initializeMs(2 * 1000, startMqttClient).start(); // every 2 seconds + procTimer.initializeMs<2 * 1000>(startMqttClient).start(); // every 2 seconds } int onMessageDelivered(MqttClient& client, mqtt_message_t* message) @@ -88,7 +89,7 @@ void startMqttClient() // Start publishing message now publishMessage(); // and schedule a timer to send messages every 5 seconds - procTimer.initializeMs(5 * 1000, publishMessage).start(); + procTimer.initializeMs<5 * 1000>(publishMessage).start(); return 0; }); @@ -118,6 +119,8 @@ void onConnected(IpAddress ip, IpAddress netmask, IpAddress gateway) startMqttClient(); } +} // namespace + void init() { Serial.begin(SERIAL_BAUD_RATE); // 115200 by default diff --git a/samples/Network_Ping/app/application.cpp b/samples/Network_Ping/app/application.cpp index 12c0aa5a42..360291aeee 100644 --- a/samples/Network_Ping/app/application.cpp +++ b/samples/Network_Ping/app/application.cpp @@ -18,7 +18,7 @@ constexpr uint8_t MAX_FAILED_ATTEMTPS = 5; constexpr uint8_t PING_INTERVAL_SECONDS = 10; constexpr uint8_t RESTART_DELAY_SECONDS = 2; -Timer procTimer; +SimpleTimer procTimer; void ping(uint32_t ip); @@ -45,7 +45,7 @@ void onSent(void* arg, void* pdata) } debug_d("Scheduling another ping in %d seconds", PING_INTERVAL_SECONDS); - procTimer.initializeMs(PING_INTERVAL_SECONDS * 1000, pingTask).startOnce(); + procTimer.initializeMs(pingTask).startOnce(); } void onReceived(void* arg, void* pdata) @@ -79,7 +79,7 @@ void ping(uint32_t ip) void connectOk(IpAddress ip, IpAddress mask, IpAddress gateway) { debug_d("Scheduling initial ping in 1 second."); - procTimer.initializeMs(1000, pingTask).startOnce(); + procTimer.initializeMs<1000>(pingTask).startOnce(); } } // namespace diff --git a/samples/Nextion_Button/app/application.cpp b/samples/Nextion_Button/app/application.cpp index 6bddd18ef9..75662f56cd 100644 --- a/samples/Nextion_Button/app/application.cpp +++ b/samples/Nextion_Button/app/application.cpp @@ -1,9 +1,6 @@ #include #include -#include -#include - #define GPIO_LED 2 // See this example in action: https://youtu.be/lHk6fqDBHyI @@ -22,22 +19,22 @@ // Note: I always unplugged the ESP8266 from USB (connecting with computer) // while fiddling with the connections between ESP8266 and Nextion display. -NexButton b0 = NexButton(0, 1, "b0"); -NexText t0 = NexText(0, 2, "g0"); - +namespace +{ +NexButton b0(0, 1, "b0"); +NexText t0(0, 2, "g0"); NexTouch* nex_listen_list[] = {&b0, NULL}; - -Timer timerNextion; +SimpleTimer timerNextion; void loopNextion() { nexLoop(nex_listen_list); } -bool ledState = true; - void b0PopCallback(void* ptr) { + static bool ledState = true; + digitalWrite(GPIO_LED, ledState); // state == false => on // state == true => off @@ -49,10 +46,12 @@ void b0PopCallback(void* ptr) ledState = !ledState; } +} // namespace + void init() { pinMode(GPIO_LED, OUTPUT); nexInit(); b0.attachPop(b0PopCallback, &b0); - timerNextion.initializeMs(100, loopNextion).start(); + timerNextion.initializeMs<100>(loopNextion).start(); } diff --git a/samples/PortExpander_MCP23017/app/application.cpp b/samples/PortExpander_MCP23017/app/application.cpp index 8b00bc081e..0e8953d236 100644 --- a/samples/PortExpander_MCP23017/app/application.cpp +++ b/samples/PortExpander_MCP23017/app/application.cpp @@ -1,19 +1,24 @@ #include #include +namespace +{ MCP23017 mcp; -volatile bool awakenByInterrupt = false; +volatile bool awakenByInterrupt; byte mcpPinA = 0; byte interruptPin = 15; -void interruptCallback() +void interruptDelegate() { awakenByInterrupt = true; - Serial.println("Interrupt Called"); - while(!(mcp.digitalRead(mcpPinA))) - ; + Serial.println(_F("Interrupt Called")); + while(!mcp.digitalRead(mcpPinA)) { + // + } } +} // namespace + void init() { Serial.begin(COM_SPEED_SERIAL); @@ -36,5 +41,5 @@ void init() mcp.pinMode(mcpPinA, INPUT); mcp.pullUp(mcpPinA, HIGH); mcp.setupInterruptPin(mcpPinA, FALLING); - attachInterrupt(interruptPin, InterruptDelegate(interruptCallback), FALLING); + attachInterrupt(interruptPin, InterruptDelegate(interruptDelegate), FALLING); } diff --git a/samples/PortExpander_MCP23S17/app/application.cpp b/samples/PortExpander_MCP23S17/app/application.cpp index 4095b02ea8..9126294024 100644 --- a/samples/PortExpander_MCP23S17/app/application.cpp +++ b/samples/PortExpander_MCP23S17/app/application.cpp @@ -1,21 +1,30 @@ #include -#include -#include #include +namespace +{ // // Instantiate an object called "inputchip" on an MCP23S17 device at address 1 = 0b00000001 and CS pin = GPIO16 MCP inputchip(1, 16); // Instantiate an object called "outputchip" on an MCP23S17 device at address 0 = 0b00000010 and CS pin = GPIO16 MCP outputchip(0, 16); -void loop(); +SimpleTimer procTimer; + +void loop() +{ + int value; // declare an integer to hold the value temporarily. + value = inputchip.digitalRead(); // read the input chip in word-mode, storing the result in "value" + outputchip.digitalWrite(value); // write the output chip in word-mode, using our variable "value" as the argument + // outputchip.digitalWrite(inputchip.digitalRead()); // this one line replaces the three above, and is more efficient +} -Timer procTimer; +} // namespace void init() { Serial.begin(SERIAL_BAUD_RATE); // 115200 by default Serial.systemDebugOutput(false); // Allow debug output to serial + Serial.println(_F("<-= Sming start =->")); // Set higher CPU freq & disable wifi sleep @@ -35,13 +44,5 @@ void init() inputchip.inputInvert(0x0000); // Use word-write mode to invert the inputs so that logic 0 is read as HIGH outputchip.pinMode(0x0000); // Use word-write mode to Set all of the pins on outputchip to be outputs - procTimer.initializeMs(200, loop).start(); -} - -void loop() -{ - int value; // declare an integer to hold the value temporarily. - value = inputchip.digitalRead(); // read the input chip in word-mode, storing the result in "value" - outputchip.digitalWrite(value); // write the output chip in word-mode, using our variable "value" as the argument - // outputchip.digitalWrite(inputchip.digitalRead()); // this one line replaces the three above, and is more efficient + procTimer.initializeMs<200>(loop).start(); } diff --git a/samples/Pressure_BMP180/app/application.cpp b/samples/Pressure_BMP180/app/application.cpp index db9d44a384..b097b74ec0 100644 --- a/samples/Pressure_BMP180/app/application.cpp +++ b/samples/Pressure_BMP180/app/application.cpp @@ -2,6 +2,7 @@ #include BMP180 barometer; + void init() { Serial.begin(SERIAL_BAUD_RATE); // 115200 by default diff --git a/samples/Radio_RCSwitch/app/application.cpp b/samples/Radio_RCSwitch/app/application.cpp index 7ce74fc87c..0a2e997680 100644 --- a/samples/Radio_RCSwitch/app/application.cpp +++ b/samples/Radio_RCSwitch/app/application.cpp @@ -3,10 +3,11 @@ #define LED_PIN 2 // GPIO2 -Timer sendTimer; -Timer receiveTimer; - -RCSwitch mySwitch = RCSwitch(); +namespace +{ +SimpleTimer sendTimer; +SimpleTimer receiveTimer; +RCSwitch mySwitch; void sendRF() { @@ -16,18 +17,22 @@ void sendRF() void receiveRF() { - if(mySwitch.available()) { - if(mySwitch.getReceivedValue() == 0) { - Serial.print(_F("Unknown encoding")); - } else { - Serial << _F("Received ") << mySwitch.getReceivedValue() << " / " << mySwitch.getReceivedBitlength() - << " bit, Protocol: " << mySwitch.getReceivedProtocol() << endl; - } - - mySwitch.resetAvailable(); + if(mySwitch.available() == 0) { + return; + } + + if(mySwitch.getReceivedValue() == 0) { + Serial.println(_F("Unknown encoding")); + } else { + Serial << _F("Received ") << mySwitch.getReceivedValue() << " / " << mySwitch.getReceivedBitlength() + << " bit, Protocol: " << mySwitch.getReceivedProtocol() << endl; } + + mySwitch.resetAvailable(); } +} // namespace + void init() { Serial.begin(SERIAL_BAUD_RATE); // 115200 by default @@ -41,6 +46,6 @@ void init() // Optional set protocol (default is 1, will work for most outlets) // mySwitch.setProtocol(2); - sendTimer.initializeMs(1000, sendRF).start(); - receiveTimer.initializeMs(20, receiveRF).start(); + sendTimer.initializeMs<1000>(sendRF).start(); + receiveTimer.initializeMs<20>(receiveRF).start(); } diff --git a/samples/Radio_nRF24L01/app/application.cpp b/samples/Radio_nRF24L01/app/application.cpp index 167154e8eb..246e768bff 100644 --- a/samples/Radio_nRF24L01/app/application.cpp +++ b/samples/Radio_nRF24L01/app/application.cpp @@ -5,6 +5,8 @@ // Use this server with standard RF24 client example "pingpair" // https://github.com/maniacbug/RF24/tree/master/examples/pingpair +namespace +{ /* **** NRF24L01 pins connection: **** * VCC 3.3v @@ -27,7 +29,7 @@ RF24 radio(RF24_CE_PIN, RF24_CSN_PIN); // Radio pipe addresses for the 2 nodes to communicate. const uint64_t pipes[2] = {0xF0F0F0F0E1LL, 0xF0F0F0F0D2LL}; -Timer procTimer; +SimpleTimer procTimer; void loopListen() { @@ -65,6 +67,8 @@ void loopListen() } } +} // namespace + void init() { Serial.begin(SERIAL_BAUD_RATE); // 115200 by default @@ -107,6 +111,6 @@ void init() radio.printDetails(); Serial.println(_F("Initialization completed.")); - procTimer.initializeMs(10, loopListen).start(); + procTimer.initializeMs<10>(loopListen).start(); Serial.println(_F("Listening...")); } diff --git a/samples/Radio_si4432/app/application.cpp b/samples/Radio_si4432/app/application.cpp index 7a947f63eb..32cb28d277 100644 --- a/samples/Radio_si4432/app/application.cpp +++ b/samples/Radio_si4432/app/application.cpp @@ -18,27 +18,29 @@ Link: http://www.electrodragon.com/w/SI4432_433M-Wireless_Transceiver_Module_%28 #define PIN_RADIO_CK 15 /* Serial Clock */ #define PIN_RADIO_SS 13 /* Slave Select */ -Timer procTimer; -Si4432* radio = nullptr; -SPISoft* pRadioSPI = nullptr; +namespace +{ +SimpleTimer procTimer; +SPISoft radioSPI(PIN_RADIO_DO, PIN_RADIO_DI, PIN_RADIO_CK, PIN_RADIO_SS); +Si4432 radio(&radioSPI); #define PING_PERIOD_MS 2000 #define PING_WAIT_PONG_MS 100 -unsigned long lastPingTime; + +PeriodicFastMs pingTimer; void loopListen() { - const byte* ack = (const byte*)"OK"; //{ 0x01, 0x3, 0x11, 0x13 }; - const byte* ping = (const byte*)"PING"; + const char* ack = "OK"; //{ 0x01, 0x3, 0x11, 0x13 }; + const char* ping = "PING"; byte payLoad[64] = {0}; byte len = 0; //1. Ping from time to time, and wait for incoming response - if(millis() - lastPingTime > PING_PERIOD_MS) { - lastPingTime = millis(); - + if(pingTimer.expired()) { Serial.print(_F("Ping -> ")); - if(!radio->sendPacket(strlen((const char*)ping), ping, true, PING_WAIT_PONG_MS, &len, payLoad)) { + if(!radio.sendPacket(strlen(ping), reinterpret_cast(ping), true, PING_WAIT_PONG_MS, &len, + payLoad)) { Serial.println(" ERR!"); } else { Serial.println(_F(" SENT!")); @@ -49,62 +51,55 @@ void loopListen() } //2. Listen for any other incoming packet - bool pkg = radio->isPacketReceived(); + bool pkg = radio.isPacketReceived(); if(pkg) { - radio->getPacketReceived(&len, payLoad); + radio.getPacketReceived(&len, payLoad); Serial << _F("ASYNC RX (") << len << "): "; Serial.write(payLoad, len); Serial.println(); Serial.print(_F("Response -> ")); - if(!radio->sendPacket(strlen((const char*)ack), ack)) { + if(!radio.sendPacket(strlen(ack), reinterpret_cast(ack))) { Serial.println(_F("ERR!")); } else { Serial.println(_F("SENT!")); } - radio->startListening(); // restart the listening. + radio.startListening(); // restart the listening. } } +} // namespace + void init() { +#ifdef ARCH_HOST + setDigitalHooks(nullptr); +#endif + Serial.begin(SERIAL_BAUD_RATE); // 115200 by default Serial.systemDebugOutput(true); //Allow debug output to serial - Serial.println(_F("\nRadio si4432 example - !!! see code for HW setup !!! \n")); - - pRadioSPI = new SPISoft(PIN_RADIO_DO, PIN_RADIO_DI, PIN_RADIO_CK, PIN_RADIO_SS); - - if(pRadioSPI != nullptr) { - radio = new Si4432(pRadioSPI); - } - - if(radio == nullptr) { - Serial.println(_F("Error: Not enough heap.")); - return; - } - - delay(100); + Serial.println(_F("\r\nRadio si4432 example - !!! see code for HW setup !!!\r\n")); - //initialise radio with default settings - radio->init(); + // initialise radio with default settings + radio.init(); - //explicitly set baudrate and channel - radio->setBaudRateFast(eBaud_38k4); - radio->setChannel(0); + // explicitly set baudrate and channel + radio.setBaudRateFast(eBaud_38k4); + radio.setChannel(0); - //dump the register configuration to console - radio->readAll(); + // dump the register configuration to console + radio.readAll(); - //start listening for incoming packets + // start listening for incoming packets Serial.println("Listening..."); - radio->startListening(); + radio.startListening(); - lastPingTime = millis(); + pingTimer.reset(PING_PERIOD_MS); - //start listen loop - procTimer.initializeMs(10, loopListen).start(); + // start listen loop + procTimer.initializeMs<10>(loopListen).start(); } diff --git a/samples/SDCard/app/application.cpp b/samples/SDCard/app/application.cpp index 7d2eee5be9..a7979af387 100644 --- a/samples/SDCard/app/application.cpp +++ b/samples/SDCard/app/application.cpp @@ -26,6 +26,8 @@ Descr: SDCard/FAT file usage and write benchmark. /* Sets the max frequency of SPI (init is done at a lower speed than the main communication) */ #define SPI_FREQ_LIMIT 2000000 +namespace +{ void writeToFile(const String& filename, uint32_t totalBytes, uint32_t bytesPerRound) { char* buf = new char[totalBytes]; @@ -201,8 +203,16 @@ bool speedTest(unsigned num) }; static const Test tests[] PROGMEM{ - {1024, 1}, {1024, 64}, {1024, 128}, {1024, 512}, {1024, 1024}, - {4096, 1024}, {8192, 512}, {8192, 1024}, {8192, 8192}, + {1024, 1}, // + {1024, 64}, // + {1024, 128}, // + {1024, 512}, // + {1024, 1024}, // + {4096, 1024}, // + {8192, 512}, // + {8192, 1024}, // + {8192, 8192}, // + }; if(num >= ARRAY_SIZE(tests)) { @@ -263,6 +273,8 @@ void runTest(uint32_t state = 0) System.queueCallback(runTest, state + 1); } +} // namespace + void init() { Serial.begin(SERIAL_BAUD_RATE); // 115200 by default diff --git a/samples/ScreenLCD_5110/app/application.cpp b/samples/ScreenLCD_5110/app/application.cpp index 2fffa50e1d..945dea1448 100644 --- a/samples/ScreenLCD_5110/app/application.cpp +++ b/samples/ScreenLCD_5110/app/application.cpp @@ -1,32 +1,48 @@ #include #include +namespace +{ // GPIO13/D7 - Serial clock out (SCLK) // GPIO12/D6 - Serial data out (DIN) // GPIO14/D5 - Data/Command select (D/C) // GPIO5/D1 - LCD chip select (CS) // GPIO4/D2 - LCD reset (RST) -Adafruit_PCD8544 display = Adafruit_PCD8544(13, 12, 14, 5, 4); +Adafruit_PCD8544 display(13, 12, 14, 5, 4); +SimpleTimer timer; void displayTest() { display.begin(); display.setContrast(10); display.display(); // show splashscreen - delay(2000); - display.clearDisplay(); // no changes will be visible until display() is called - display.setRotation(4); // rotate 90 degrees counter clockwise, can also use values of 2 and 3 to go further. - display.setTextSize(1); - display.setTextColor(BLACK); - display.setCursor(0, 0); - display.println("Sming"); - display.setTextSize(2); - display.println("Example"); - display.display(); + debug_d("Show splash..."); + + timer.initializeMs<2000>([]() { + debug_d("Update screen."); + display.clearDisplay(); // no changes will be visible until display() is called + display.setRotation(4); // rotate 90 degrees counter clockwise, can also use values of 2 and 3 to go further. + display.setTextSize(1); + display.setTextColor(BLACK); + display.setCursor(0, 0); + display.println("Sming"); + display.setTextSize(2); + display.println("Example"); + display.display(); + }); + timer.startOnce(); } +} // namespace + void init() { +#ifdef ARCH_HOST + setDigitalHooks(nullptr); +#endif + Serial.begin(SERIAL_BAUD_RATE); + Serial.systemDebugOutput(true); + displayTest(); } diff --git a/samples/ScreenOLED_SSD1306/app/application.cpp b/samples/ScreenOLED_SSD1306/app/application.cpp index d0b8d3b5f6..df67dadd1b 100644 --- a/samples/ScreenOLED_SSD1306/app/application.cpp +++ b/samples/ScreenOLED_SSD1306/app/application.cpp @@ -18,6 +18,8 @@ #define SH1106_128_64 // larger one */ +namespace +{ // For spi oled module // Adafruit_SSD1306 display(0, 16, 2); @@ -25,7 +27,7 @@ // Default I2C pins 0 (SCL) and 2 (SDA). Pin 4 - optional reset Adafruit_SSD1306 display(-1); // reset Pin required but later ignored if set to False -Timer DemoTimer; +SimpleTimer demoTimer; void Demo2() { @@ -45,7 +47,6 @@ void Demo2() display.setTextSize(3); display.print("IoT"); display.display(); - DemoTimer.stop(); // Finish demo } void Demo1() @@ -56,10 +57,13 @@ void Demo1() // draw a circle, 10 pixel radius display.fillCircle(display.width() / 2, display.height() / 2, 10, WHITE); display.display(); - DemoTimer.stop(); - DemoTimer.initializeMs(2000, Demo2).start(); + + demoTimer.setCallback(Demo2); + demoTimer.startOnce(); } +} // namespace + void init() { Serial.begin(SERIAL_BAUD_RATE); // 115200 by default @@ -70,5 +74,6 @@ void init() // bool:reset set to TRUE or FALSE depending on your display display.begin(SSD1306_SWITCHCAPVCC, 0x3c, false); display.display(); - DemoTimer.initializeMs(2000, Demo1).start(); + + demoTimer.initializeMs<2000>(Demo1).startOnce(); } diff --git a/samples/ScreenTFT_ILI9163C/app/application.cpp b/samples/ScreenTFT_ILI9163C/app/application.cpp index 8994109c9c..3d1611a989 100644 --- a/samples/ScreenTFT_ILI9163C/app/application.cpp +++ b/samples/ScreenTFT_ILI9163C/app/application.cpp @@ -1,6 +1,8 @@ #include #include +namespace +{ /* * LED (BACKLIGHT) 3.3v * SCK (SCLK) GPIO14 @@ -12,33 +14,41 @@ * VCC (VCC) 3.3v */ TFT_ILI9163C tft(2, 0); +SimpleTimer timer; -void init() +void demo() { - Serial.begin(SERIAL_BAUD_RATE); // 115200 by default - Serial.systemDebugOutput(true); // Allow debug output to serial - Serial.println(_F("Display start")); tft.begin(); tft.setRotation(2); // try yourself tft.fillScreen(); tft.fillRect(10, 20, 100, 120, YELLOW); - delay(1000); - // text display tests - tft.fillScreen(); - tft.setTextSize(1); - tft.setTextColor(GREEN); - tft.setCursor(0, 0); - tft.println(_F("Sming Framework")); - tft.setTextColor(BLACK, WHITE); // 'inverted' text - tft.setCursor(104, 7); - tft.println("v1.0"); - tft.setTextColor(WHITE); - tft.println(_F("Let's do smart things")); - tft.setTextSize(3); - tft.setTextColor(BLUE); - tft.print("IoT"); + timer.initializeMs<1000>([]() { + // text display tests + tft.fillScreen(); + tft.setTextSize(1); + tft.setTextColor(GREEN); + tft.setCursor(0, 0); + tft.println(_F("Sming Framework")); + tft.setTextColor(BLACK, WHITE); // 'inverted' text + tft.setCursor(104, 7); + tft.println("v1.0"); + tft.setTextColor(WHITE); + tft.println(_F("Let's do smart things")); + tft.setTextSize(3); + tft.setTextColor(BLUE); + tft.print("IoT"); + }); + timer.startOnce(); +} + +} // namespace + +void init() +{ + Serial.begin(SERIAL_BAUD_RATE); // 115200 by default + Serial.systemDebugOutput(true); // Allow debug output to serial - delay(2000); + demo(); } diff --git a/samples/ScreenTFT_ILI9340-ILI9341/app/application.cpp b/samples/ScreenTFT_ILI9340-ILI9341/app/application.cpp index 2b0ec9b1d3..5aba476570 100644 --- a/samples/ScreenTFT_ILI9340-ILI9341/app/application.cpp +++ b/samples/ScreenTFT_ILI9340-ILI9341/app/application.cpp @@ -8,36 +8,41 @@ #define WIFI_PWD "PleaseEnterPass" #endif +namespace +{ // See library for pinout Adafruit_ILI9341 tft; -Timer guiTimer; - -int r = 0; - -int ara = 4, yerara = 15; -int u1 = 100; -int u2 = 320 - (u1 + ara); -int s1 = 0; -int s2 = (u1 + ara); -int p1 = 50; - -int g = 28; -int y = 90; -int satir = 6; - -String lists[] = {"a", "b", "c", "d", "e", "f"}; +SimpleTimer guiTimer; void basicBMP() { + debug_d("%s", __FUNCTION__); + tft.fillScreen(ILI9341_BLACK); // Clear display tft.setRotation(tft.getRotation() + 1); // Inc rotation 90 degrees - for(uint8_t i = 0; i < 4; i++) // Draw 4 parrots - bmpDraw(tft, "sming.bmp", tft.width() / 4 * i, tft.height() / 4 * i); + for(uint8_t i = 0; i < 4; i++) { // Draw 4 parrots + bmpDraw(tft, "sming.bmp", i * tft.width() / 4, i * tft.height() / 4); + } } void basicGui() { + debug_d("%s", __FUNCTION__); + + static int r; + + const int ara = 4; + const int yerara = 15; + const int u1 = 100; + const int u2 = 320 - (u1 + ara); + const int s1 = 0; + const int s2 = (u1 + ara); + + const int g = 28; + + int p1 = 50; + tft.setTextSize(1); tft.setRotation(1); @@ -48,12 +53,13 @@ void basicGui() tft.println("Sming"); tft.setTextSize(2); tft.fillRect((u1 * 2) + ara, 0, 318 - (u1 * 2), 48, ILI9341_RED); - for(int a = 0; a < satir; a++) { + for(auto a : {'a', 'b', 'c', 'd', 'e', 'f'}) { + debug_d("%c: %u", a, r); tft.setTextColor(ILI9341_GREEN); tft.fillRect(s1, p1, u1, g, ILI9341_DARKCYAN); tft.setCursor(s1 + yerara, p1 + 6); tft.setTextColor(ILI9341_WHITE); - tft.println(lists[a]); + tft.println(a); tft.fillRect(s2, p1, u2, g, ILI9341_DARKCYAN); tft.setCursor(s2 + yerara, p1 + 6); tft.println(r); @@ -61,24 +67,16 @@ void basicGui() } p1 = 50; r++; - guiTimer.initializeMs<1000>(basicBMP).start(false); -} -void init() -{ - Serial.begin(SERIAL_BAUD_RATE); // 115200 by default - Serial.systemDebugOutput(true); // Allow debug output to serial - -#ifndef DISABLE_WIFI - //WifiStation.config(WIFI_SSID, WIFI_PWD); - WifiStation.enable(false); - WifiAccessPoint.enable(false); -#endif + if(r >= 5) { + guiTimer.setCallback(basicBMP); + } - spiffs_mount(); - Serial.println(_F("FileSystem mounted.")); + guiTimer.startOnce(); +} - // delay(2000); +void demo() +{ Serial.println(_F("Display start")); // text display tests @@ -99,8 +97,33 @@ void init() tft.println(_F("ili9340-40C-41 ")); tft.setCursor(60, 125); tft.println(_F("M.Bozkurt")); - delay(2000); - tft.fillScreen(0); - guiTimer.initializeMs<1000>(basicGui).start(false); - //runTest(); + + guiTimer.initializeMs<2000>([]() { + tft.fillScreen(0); + guiTimer.initializeMs<1000>(basicGui).startOnce(); + }); + guiTimer.startOnce(); +} + +} // namespace + +void init() +{ +#ifdef ARCH_HOST + setDigitalHooks(nullptr); +#endif + + Serial.begin(SERIAL_BAUD_RATE); // 115200 by default + Serial.systemDebugOutput(true); // Allow debug output to serial + +#ifndef DISABLE_WIFI + //WifiStation.config(WIFI_SSID, WIFI_PWD); + WifiStation.enable(false); + WifiAccessPoint.enable(false); +#endif + + spiffs_mount(); + Serial.println(_F("FileSystem mounted.")); + + demo(); } diff --git a/samples/ScreenTFT_ST7735/app/application.cpp b/samples/ScreenTFT_ST7735/app/application.cpp index b4a2ed3701..b4a3b7bbe7 100644 --- a/samples/ScreenTFT_ST7735/app/application.cpp +++ b/samples/ScreenTFT_ST7735/app/application.cpp @@ -18,14 +18,12 @@ #define TFT_DC 0 #define TFT_CS 2 -//Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_MOSI, TFT_SCLK, TFT_RST); -Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_RST); - -Timer DemoScreenTimer; -float p = 3.1415926; -uint32_t startTime; +namespace +{ +//Adafruit_ST7735 tft(TFT_CS, TFT_DC, TFT_MOSI, TFT_SCLK, TFT_RST); +Adafruit_ST7735 tft(TFT_CS, TFT_DC, TFT_RST); -void testdrawtext(const char text[], uint16_t color) +void testdrawtext(const String& text, uint16_t color) { tft.setCursor(0, 0); tft.setTextColor(color); @@ -61,7 +59,7 @@ void tftPrintTest2() tft.println("Hello Sming!"); tft.setTextSize(1); tft.setTextColor(ST7735_GREEN); - tft.print(p, 6); + tft.print(PI, 6); tft.println(" Want pi?"); tft.println(" "); tft.print(8675309, HEX); // print 8,675,309 out in HEX! @@ -226,122 +224,155 @@ void mediabuttons() void screen13() { - startTime = millis(); - debugf("screen13: bmpDraw rotaton %d ms", millis() - startTime); tft.fillScreen(ST7735_BLACK); // Clear display tft.setRotation(tft.getRotation() + 1); // Inc rotation 90 degrees - for(uint8_t i = 0; i < 4; i++) // Draw 4 parrots + for(uint8_t i = 0; i < 4; i++) { // Draw 4 parrots bmpDraw(tft, "sming.bmp", tft.width() / 4 * i, tft.height() / 4 * i); + } } void screen12() { - startTime = millis(); bmpDraw(tft, "sming.bmp", 0, 0); - debugf("screen12: bmpDraw %d ms", millis() - startTime); - // DemoScreenTimer.initializeMs(2000, screen13).start(false); } void screen11() { - startTime = millis(); mediabuttons(); - debugf("screen11: mediabuttons %d ms", millis() - startTime); - DemoScreenTimer.initializeMs(1000, screen12).start(false); } void screen10() { - startTime = millis(); testtriangles(); - debugf("screen10: testtriangles %d ms", millis() - startTime); - DemoScreenTimer.initializeMs(1000, screen11).start(false); } void screen9() { - startTime = millis(); testroundrects(); - debugf("screen9: testroundrects %d ms", millis() - startTime); - DemoScreenTimer.initializeMs(1000, screen10).start(false); } void screen8() { - startTime = millis(); tft.fillScreen(ST7735_BLACK); testfillcircles(10, ST7735_BLUE); testdrawcircles(10, ST7735_WHITE); - debugf("screen8: testfillcircles %d ms", millis() - startTime); - DemoScreenTimer.initializeMs(1000, screen9).start(false); } void screen7() { - startTime = millis(); testfillrects(ST7735_YELLOW, ST7735_MAGENTA); - debugf("screen7: testfillrects %d ms", millis() - startTime); - DemoScreenTimer.initializeMs(1000, screen8).start(false); } void screen6() { - startTime = millis(); testdrawrects(ST7735_GREEN); - debugf("screen6: testdrawrects %d ms", millis() - startTime); - DemoScreenTimer.initializeMs(1000, screen7).start(false); } void screen5() { - startTime = millis(); // optimized lines testfastlines(ST7735_RED, ST7735_BLUE); - debugf("screen5: testfastlines %d ms", millis() - startTime); - DemoScreenTimer.initializeMs(1000, screen6).start(false); } void screen4() { - startTime = millis(); // line draw test testlines(ST7735_YELLOW); - debugf("screen4: testlines %d ms", millis() - startTime); - DemoScreenTimer.initializeMs(1000, screen5).start(false); } void screen3() { - startTime = millis(); tftPrintTest2(); - debugf("screen3: tftPrintTest2 %d ms", millis() - startTime); - DemoScreenTimer.initializeMs(1000, screen4).start(false); } void screen2() { - startTime = millis(); tftPrintTest1(); - debugf("screen2: tftPrintTest1 %d ms", millis() - startTime); - DemoScreenTimer.initializeMs(1000, screen3).start(false); } void screen1() { - startTime = millis(); // large block of text tft.fillScreen(ST7735_BLACK); - testdrawtext("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur adipiscing ante sed nibh " - "tincidunt feugiat. Maecenas enim massa, fringilla sed malesuada et, malesuada sit amet turpis. Sed " - "porttitor neque ut ante pretium vitae malesuada nunc bibendum. Nullam aliquet ultrices massa eu " - "hendrerit. Ut sed nisi lorem. In vestibulum purus a tortor imperdiet posuere. ", - ST7735_WHITE); - debugf("screen1: testdrawtext %d ms", millis() - startTime); - DemoScreenTimer.initializeMs(1000, screen2).start(false); + DEFINE_FSTR_LOCAL( + largeTextBlock, + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur adipiscing ante sed nibh " + "tincidunt feugiat. Maecenas enim massa, fringilla sed malesuada et, malesuada sit amet turpis. Sed " + "porttitor neque ut ante pretium vitae malesuada nunc bibendum. Nullam aliquet ultrices massa eu " + "hendrerit. Ut sed nisi lorem. In vestibulum purus a tortor imperdiet posuere. ") + testdrawtext(largeTextBlock, ST7735_WHITE); } +void initialise() +{ + // Use this initializer if you're using a 1.8" TFT + // tft.initR(INITR_BLACKTAB); // initialize a ST7735S chip, black tab + + // Use this initializer (uncomment) if you're using a 1.44" TFT + tft.initR(INITR_144GREENTAB); // initialize a ST7735S chip, black tab + + tft.fillScreen(ST7735_BLACK); +} + +#define SCREEN_FUNCTION_MAP(XX) \ + XX(initialise, "Initialise") \ + XX(screen1, "testdrawtext") \ + XX(screen2, "tftPrintTest1") \ + XX(screen3, "tftPrintTest2") \ + XX(screen4, "testlines") \ + XX(screen5, "testfastlines") \ + XX(screen6, "testdrawrects") \ + XX(screen7, "testfillrects") \ + XX(screen8, "testfillcircles") \ + XX(screen9, "testroundrects") \ + XX(screen10, "testtriangles") \ + XX(screen11, "mediabuttons") \ + XX(screen12, "bmpDraw") \ + XX(screen13, "bmpDraw rotation") + +#define XX(function, title) DEFINE_FSTR(function##_title, #function ": " title) +SCREEN_FUNCTION_MAP(XX) +#undef XX + +struct DemoScreen { + InterruptCallback function; + const FlashString& title; +}; + +const DemoScreen screenList[] PROGMEM{ +#define XX(name, title) {name, name##_title}, + SCREEN_FUNCTION_MAP(XX) +#undef XX +}; + +SimpleTimer demoScreenTimer; +auto screen = std::begin(screenList); + +void nextScreen() +{ + OneShotFastMs elapseTimer; + screen->function(); + auto elapsed = elapseTimer.elapsedTime(); + + Serial << screen->title << " in " << elapsed << "ms" << endl; + + ++screen; + if(screen != std::end(screenList)) { + demoScreenTimer.startOnce(); + return; + } + + Serial.println(_F("All screens displayed")); +} + +} // namespace + void init() { +#ifdef ARCH_HOST + setDigitalHooks(nullptr); +#endif + spiffs_mount(); // Mount file system, in order to work with files Serial.begin(SERIAL_BAUD_RATE); // 115200 by default @@ -353,19 +384,7 @@ void init() WifiAccessPoint.enable(false); #endif - debugf("Display start"); - startTime = millis(); - - // Use this initializer if you're using a 1.8" TFT - // tft.initR(INITR_BLACKTAB); // initialize a ST7735S chip, black tab - - // Use this initializer (uncomment) if you're using a 1.44" TFT - tft.initR(INITR_144GREENTAB); // initialize a ST7735S chip, black tab - - tft.fillScreen(ST7735_BLACK); - startTime = millis() - startTime; - - debugf("Initialized in %d ms\n", startTime); - - DemoScreenTimer.initializeMs(500, screen1).start(false); + Serial.println(_F("Initialise display")); + demoScreenTimer.initializeMs<500>(nextScreen); + nextScreen(); } diff --git a/samples/ScreenTFT_ST7735/component.mk b/samples/ScreenTFT_ST7735/component.mk index 17c7646fff..41fb0dc894 100644 --- a/samples/ScreenTFT_ST7735/component.mk +++ b/samples/ScreenTFT_ST7735/component.mk @@ -1,4 +1,3 @@ -COMPONENT_SOC := esp* HWCONFIG := spiffs-2m ARDUINO_LIBRARIES := Adafruit_ST7735 DISABLE_NETWORK := 1 diff --git a/samples/SmtpClient/app/application.cpp b/samples/SmtpClient/app/application.cpp index 9890635d3a..1b6083cc6c 100644 --- a/samples/SmtpClient/app/application.cpp +++ b/samples/SmtpClient/app/application.cpp @@ -7,12 +7,14 @@ #define WIFI_PWD "PleaseEnterPass" #endif +namespace +{ // Make sure to change those to your desired values -#define MAIL_FROM "admin@sming.com" -#define MAIL_TO "slav@attachix.com" -#define SMTP_USERNAME nullptr -#define SMTP_PASSWORD nullptr -#define SMTP_HOST "attachix.com" +DEFINE_FSTR(MAIL_FROM, "admin@sming.com") +DEFINE_FSTR(MAIL_TO, "slav@attachix.com") +DEFINE_FSTR(SMTP_USERNAME, "") +DEFINE_FSTR(SMTP_PASSWORD, "") +DEFINE_FSTR(SMTP_HOST, "attachix.com") #define SMTP_PORT 0 // Use default port #define SMTP_USE_SSL false @@ -31,11 +33,11 @@ int onMailSent(SmtpClient& client, int code, char* status) MailMessage* mail = client.getCurrentMessage(); // TODO: The status line contains the unique ID that was given to this email - debugf("Mail sent to '%s'. Status: %s", mail->to.c_str(), status); + Serial << _F("Mail sent to '") << mail->to << _F("'. Status: ") << status << endl; // And if there are no more pending emails then you can disconnect from the server if(client.countPending() == 0) { - debugf("No more mails to send. Quitting..."); + Serial.println(_F("No more mails to send. Quitting...")); client.quit(); } @@ -50,7 +52,7 @@ void onConnected(IpAddress ip, IpAddress mask, IpAddress gateway) Url dsn(SMTP_USE_SSL ? URI_SCHEME_SMTP_SECURE : URI_SCHEME_SMTP, SMTP_USERNAME, SMTP_PASSWORD, SMTP_HOST, SMTP_PORT); - debugf("Connecting to SMTP server using: %s", String(dsn).c_str()); + Serial << _F("Connecting to SMTP server using: ") << dsn << endl; client.connect(dsn); @@ -58,8 +60,9 @@ void onConnected(IpAddress ip, IpAddress mask, IpAddress gateway) mail->from = MAIL_FROM; mail->to = MAIL_TO; mail->subject = "Greetings from Sming"; - String body = F("Hello.\r\n.\r\n" - "This is test email from Sming " + String body = F("Hello.\r\n." + "\r\n" + "This is test email from Sming \r\n" "It contains attachment, Ümlauts, кирилица + etc"); // Note: Body can be quite large so use move semantics to avoid additional heap allocation mail->setBody(std::move(body)); @@ -71,11 +74,13 @@ void onConnected(IpAddress ip, IpAddress mask, IpAddress gateway) client.send(mail); } +} // namespace + void init() { Serial.begin(SERIAL_BAUD_RATE); Serial.systemDebugOutput(true); - Serial.println("Sming: SmtpClient example!"); + Serial.println(_F("Sming: SmtpClient example!")); spiffs_mount(); diff --git a/samples/SystemClock_NTP/app/application.cpp b/samples/SystemClock_NTP/app/application.cpp index 5129bb6f22..ecd3121802 100644 --- a/samples/SystemClock_NTP/app/application.cpp +++ b/samples/SystemClock_NTP/app/application.cpp @@ -7,9 +7,12 @@ #define WIFI_PWD "PleaseEnterPass" #endif +namespace +{ +#pragma GCC diagnostic ignored "-Wunused-function" void onNtpReceive(NtpClient& client, time_t timestamp); -Timer printTimer; +SimpleTimer printTimer; // Option 1 // Use this option if you want to have full control of NtpTime client @@ -62,7 +65,7 @@ void onNtpReceive(NtpClient& client, time_t timestamp) // Will be called when WiFi station timeout was reached void connectFail(const String& ssid, MacAddress bssid, WifiDisconnectReason reason) { - debugf("I'm NOT CONNECTED!"); + Serial.println(_F("I'm NOT CONNECTED!")); } void gotIP(IpAddress ip, IpAddress netmask, IpAddress gateway) @@ -88,6 +91,8 @@ void gotIP(IpAddress ip, IpAddress netmask, IpAddress gateway) } } +} // namespace + // Will be called when WiFi hardware and software initialization was finished // And system initialization was completed diff --git a/samples/TcpClient_NarodMon/app/application.cpp b/samples/TcpClient_NarodMon/app/application.cpp index 063bdd03a6..cdc8c21c47 100644 --- a/samples/TcpClient_NarodMon/app/application.cpp +++ b/samples/TcpClient_NarodMon/app/application.cpp @@ -13,7 +13,9 @@ #define WIFI_PWD "PleaseEnterPass" #endif -#define NARODM_HOST "narodmon.ru" +namespace +{ +DEFINE_FSTR(NARODM_HOST, "narodmon.ru") #define NARODM_PORT 8283 // Time between command packets, in seconds @@ -21,7 +23,7 @@ const unsigned SENDDATA_INTERVAL = 6 * 60; // Таймер для периодического вызова отправки данных // Timer for a periodic call to send data -Timer procTimer; +SimpleTimer procTimer; // Переменная для хранения mac-адреса // Variable for storing MAC address @@ -103,33 +105,33 @@ void sendData() // Когда удачно подключились к роутеру void connectOk(const String& SSID, MacAddress bssid, uint8_t channel) { - // debug msg - debugf("I'm CONNECTED to WiFi"); - // Get the MAC address of our ESP // получаем MAC-адрес нашей ESP и помещаем в переменную mac mac = WifiStation.getMacAddress(); - debugf("mac: %s", mac.toString().c_str()); + + Serial << _F("I'm CONNECTED to WiFi, MAC: ") << mac << endl; } void connectFail(const String& ssid, MacAddress bssid, WifiDisconnectReason reason) { // Display a message on failed connection // Если подключение к роутеру не удалось, выводим сообщение - debugf("I'm NOT CONNECTED!"); + Serial.println(_F("I'm NOT CONNECTED!")); } void gotIP(IpAddress ip, IpAddress netmask, IpAddress gateway) { // Call the sendData function by timer, every 6 minutes // вызываем по таймеру функцию sendData - procTimer.initializeMs(SENDDATA_INTERVAL * 1000, sendData).start(); // каждые 6 минут + procTimer.initializeMs(sendData).start(); // каждые 6 минут // Send immediately on startup // ну и заодно сразу после запуска вызываем, чтобы не ждать 6 минут первый раз sendData(); } +} // namespace + void init() { // Configure and enable output in UART for debug diff --git a/samples/Temperature_DS1820/app/application.cpp b/samples/Temperature_DS1820/app/application.cpp index 75d695c349..3c4277df35 100644 --- a/samples/Temperature_DS1820/app/application.cpp +++ b/samples/Temperature_DS1820/app/application.cpp @@ -3,8 +3,10 @@ #define I2C_PIN 4 +namespace +{ DS18S20 ReadTemp; -Timer procTimer; +SimpleTimer procTimer; //********************************************************** // DS18S20 example, reading @@ -22,31 +24,35 @@ Timer procTimer; //*********************************************************** void readData() { - if(!ReadTemp.MeasureStatus()) // the last measurement completed - { - auto sensorCount = ReadTemp.GetSensorsCount(); - if(sensorCount > 0) { - Serial.println(_F("******************************************")); - } - Serial.println(_F(" Reading temperature DEMO")); - // prints for all sensors - for(unsigned a = 0; a < sensorCount; a++) { - Serial << " T" << a + 1 << " = "; - if(ReadTemp.IsValidTemperature(a)) { - Serial << ReadTemp.GetCelsius(a) << _F(" Celsius, (") << ReadTemp.GetFahrenheit(a) << _F(" Fahrenheit)") - << endl; - } else { - Serial.println(_F("Temperature not valid")); - } - - Serial << _F(" ' << endl; - } - Serial.println(_F("******************************************")); - ReadTemp.StartMeasure(); // next measure, result after 1.2 seconds * number of sensors - } else + if(ReadTemp.MeasureStatus()) { Serial.println(_F("No valid Measure so far! wait please")); + return; + } + + // the last measurement completed + auto sensorCount = ReadTemp.GetSensorsCount(); + if(sensorCount > 0) { + Serial.println(_F("******************************************")); + } + Serial.println(_F(" Reading temperature DEMO")); + // prints for all sensors + for(unsigned a = 0; a < sensorCount; a++) { + Serial << " T" << a + 1 << " = "; + if(ReadTemp.IsValidTemperature(a)) { + Serial << ReadTemp.GetCelsius(a) << _F(" Celsius, (") << ReadTemp.GetFahrenheit(a) << _F(" Fahrenheit)") + << endl; + } else { + Serial.println(_F("Temperature not valid")); + } + + Serial << _F(" ' << endl; + } + Serial.println(_F("******************************************")); + ReadTemp.StartMeasure(); // next measure, result after 1.2 seconds * number of sensors } +} // namespace + void init() { Serial.begin(SERIAL_BAUD_RATE); diff --git a/samples/UdpServer_Echo/app/application.cpp b/samples/UdpServer_Echo/app/application.cpp index 52cf2503ec..ab90275c8f 100644 --- a/samples/UdpServer_Echo/app/application.cpp +++ b/samples/UdpServer_Echo/app/application.cpp @@ -6,7 +6,9 @@ #define WIFI_PWD "PleaseEnterPass" #endif -void onReceive(UdpConnection& connection, char* data, int size, IpAddress remoteIP, uint16_t remotePort); // Declaration +namespace +{ +void onReceive(UdpConnection& connection, char* data, int size, IpAddress remoteIP, uint16_t remotePort); // UDP server const uint16_t EchoPort = 1234; @@ -20,7 +22,7 @@ void onReceive(UdpConnection& connection, char* data, int size, IpAddress remote Serial << ">\t" << data; // Send echo to remote sender - String text = String("echo: ") + data; + String text = F("echo: ") + data; udp.sendStringTo(remoteIP, remotePort, text); } @@ -34,6 +36,8 @@ void gotIP(IpAddress ip, IpAddress gateway, IpAddress netmask) Serial.println(_F("==========================\r\n")); } +} // namespace + void init() { Serial.begin(SERIAL_BAUD_RATE); // 115200 by default diff --git a/samples/UdpServer_mDNS/app/application.cpp b/samples/UdpServer_mDNS/app/application.cpp index f9497b0c2c..83c60187cb 100644 --- a/samples/UdpServer_mDNS/app/application.cpp +++ b/samples/UdpServer_mDNS/app/application.cpp @@ -11,8 +11,8 @@ // Our device will be reachable via "sming.local" DEFINE_FSTR_LOCAL(hostName, "sming"); -HttpServer server; - +namespace +{ class MyHttpService : public mDNS::Service { public: @@ -28,37 +28,9 @@ class MyHttpService : public mDNS::Service } }; -static mDNS::Responder responder; -static MyHttpService myHttpService; - -void speedTest(mDNS::Question* question) -{ - using namespace mDNS; - - OneShotFastUs timer; - unsigned count{0}; - for(unsigned i = 0; i < 10000; ++i) { - if(question->getName() == fstrServicesLocal) { - ++count; - } - if(question->getName().equalsIgnoreCase("Haggis basher")) { - ++count; - } - } - debug_i("(question == fstrServicesLocal): %u, %s", count, timer.elapsedTime().toString().c_str()); - - timer.start(); - count = 0; - for(unsigned i = 0; i < 10000; ++i) { - if(String(question->getName()).equalsIgnoreCase(fstrServicesLocal)) { - ++count; - } - if(String(question->getName()).equalsIgnoreCase("Haggis basher")) { - ++count; - } - } - debug_i("(question == fstrServicesLocal): %u, %s", count, timer.elapsedTime().toString().c_str()); -} +HttpServer server; +mDNS::Responder responder; +MyHttpService myHttpService; void test() { @@ -97,8 +69,6 @@ void test() Query query; auto question = query.addQuestion(fstrServicesLocal, ResourceType::PTR); - // speedTest(question); - checkLike(question, _F("_services._dns-sd._udp.local"), true); checkLike(question, _F("_dns-sd._udp.local"), true); checkLike(question, _F("_udp.local"), true); @@ -141,18 +111,6 @@ void onIndex(HttpRequest& request, HttpResponse& response) response.sendFile("index.html"); } -void onFile(HttpRequest& request, HttpResponse& response) -{ - String file = request.uri.getRelativePath(); - - if(file[0] == '.') { - response.code = HTTP_STATUS_FORBIDDEN; - } else { - response.setCache(86400, true); // It's important to use cache for better performance. - response.sendFile(file); - } -} - void startWebServer() { server.listen(80); @@ -180,6 +138,8 @@ void gotIP(IpAddress ip, IpAddress netmask, IpAddress gateway) startmDNS(); // Start mDNS "Advertise" of your hostname "test.local" for this example } +} // namespace + void init() { Serial.begin(SERIAL_BAUD_RATE); // 115200 by default diff --git a/samples/Ultrasonic_HCSR04/app/application.cpp b/samples/Ultrasonic_HCSR04/app/application.cpp index 756e9e3c76..bd2ee80f43 100644 --- a/samples/Ultrasonic_HCSR04/app/application.cpp +++ b/samples/Ultrasonic_HCSR04/app/application.cpp @@ -14,8 +14,10 @@ #define TRIG_PIN 2 #define ECHO_PIN 5 -Timer procTimer; -Ultrasonic ultrasonic = Ultrasonic(); +namespace +{ +SimpleTimer procTimer; +Ultrasonic ultrasonic; void measure() { @@ -26,9 +28,11 @@ void measure() Serial.println(dist); } +} // namespace + void init() { Serial.begin(SERIAL_BAUD_RATE); ultrasonic.begin(TRIG_PIN, ECHO_PIN); - procTimer.initializeMs(500, measure).start(); + procTimer.initializeMs<500>(measure).start(); } diff --git a/samples/WebcamServer/app/application.cpp b/samples/WebcamServer/app/application.cpp index 84a1320747..320d808fdb 100644 --- a/samples/WebcamServer/app/application.cpp +++ b/samples/WebcamServer/app/application.cpp @@ -10,8 +10,10 @@ #define WIFI_PWD "PleaseEnterPass" #endif +namespace +{ HttpServer server; -FakeCamera* camera = nullptr; +FakeCamera* camera; /* * default http handler to check if server is up and running @@ -82,6 +84,8 @@ void startWebServer() server.paths.setDefault(onFile); } +} // namespace + void init() { Serial.begin(SERIAL_BAUD_RATE); // Enable serial diff --git a/samples/Websocket_Client/app/application.cpp b/samples/Websocket_Client/app/application.cpp index d2f075b278..6e6b591b8a 100644 --- a/samples/Websocket_Client/app/application.cpp +++ b/samples/Websocket_Client/app/application.cpp @@ -23,7 +23,7 @@ namespace { WebsocketClient wsClient; -Timer msgTimer; +SimpleTimer msgTimer; // Number of messages to send const unsigned MESSAGES_TO_SEND = 5; @@ -44,7 +44,7 @@ void sendNewMessage(); void wsConnected(WebsocketConnection& wsConnection) { Serial << _F("Start sending messages every ") << MESSAGE_INTERVAL << _F(" second(s)...") << endl; - msgTimer.initializeMs(MESSAGE_INTERVAL * 1000, sendNewMessage); + msgTimer.initializeMs(sendNewMessage); msgTimer.startOnce(); } @@ -72,7 +72,7 @@ void restart() void wsDisconnected(WebsocketConnection& wsConnection) { Serial << _F("Restarting websocket client after ") << RESTART_PERIOD << _F(" seconds") << endl; - msgTimer.initializeMs(RESTART_PERIOD * 1000, restart); + msgTimer.initializeMs(restart); msgTimer.startOnce(); } diff --git a/samples/Wifi_Sniffer/app/application.cpp b/samples/Wifi_Sniffer/app/application.cpp index 14d6076349..d5c71d2fd0 100644 --- a/samples/Wifi_Sniffer/app/application.cpp +++ b/samples/Wifi_Sniffer/app/application.cpp @@ -3,8 +3,10 @@ #include "Platform/WifiSniffer.h" #include "Data/HexString.h" -static BeaconInfoList knownAPs; ///< List of known APs -static ClientInfoList knownClients; ///< List of known CLIENTs +namespace +{ +BeaconInfoList knownAPs; ///< List of known APs +ClientInfoList knownClients; ///< List of known CLIENTs const unsigned scanTimeoutMs = 2000; ///< End scan on channel if no new devices found within this time @@ -16,7 +18,7 @@ SimpleTimer timer; * Replace these with ?. * Return a String of exactly 32 characters. */ -static String makeSsidString(const uint8_t* ssid, size_t len) +String makeSsidString(const uint8_t* ssid, size_t len) { String s; s.pad(32); @@ -25,7 +27,7 @@ static String makeSsidString(const uint8_t* ssid, size_t len) return s; } -static void printBeacon(const BeaconInfo& beacon) +void printBeacon(const BeaconInfo& beacon) { if(beacon.err != 0) { Serial << _F("BEACON ERR: (") << beacon.err << ')' << endl; @@ -37,58 +39,68 @@ static void printBeacon(const BeaconInfo& beacon) Serial << makeHexString(beacon.ssid, 32) << " " << beacon.ssid_len << endl; } -static void printClient(const ClientInfo& client) +void printClient(const ClientInfo& client) { if(client.err != 0) { Serial << _F("CLIENT ERR: (") << client.err << ')' << endl; - } else { - Serial << _F("DEVICE: ") << client.station << _F(" ==> "); - - int i = knownAPs.indexOf(client.bssid); - if(i < 0) { - Serial << _F("Unknown/Malformed packet, BSSID = ") << client.bssid << endl; - } else { - auto& ap = knownAPs[i]; - String ssid = makeSsidString(ap.ssid, ap.ssid_len); - Serial << '[' << ssid << ']' << " " << client.ap << " " << String(ap.channel).padLeft(3) << " " - << String(client.rssi).padLeft(4) << endl; - } + return; } + + Serial << _F("DEVICE: ") << client.station << _F(" ==> "); + + int i = knownAPs.indexOf(client.bssid); + if(i < 0) { + Serial << _F("Unknown/Malformed packet, BSSID = ") << client.bssid << endl; + return; + } + + auto& ap = knownAPs[i]; + String ssid = makeSsidString(ap.ssid, ap.ssid_len); + Serial << '[' << ssid << ']' << " " << client.ap << " " << String(ap.channel).padLeft(3) << " " + << String(client.rssi).padLeft(4) << endl; } -static void printSummary() +void printSummary() { - Serial.println("\r\n" - "-------------------------------------------------------------------------------------\r\n"); + DEFINE_FSTR_LOCAL(SEPARATOR, + "\r\n" + "-------------------------------------------------------------------------------------\r\n") + + Serial.println(SEPARATOR); for(auto& client : knownClients) { printClient(client); } for(auto& ap : knownAPs) { printBeacon(ap); } - Serial.println("\r\n" - "-------------------------------------------------------------------------------------\r\n"); + Serial.println(SEPARATOR); } -static void onBeacon(const BeaconInfo& beacon) +void onBeacon(const BeaconInfo& beacon) { - if(knownAPs.indexOf(beacon.bssid) < 0) { - knownAPs.add(beacon); - printBeacon(beacon); - timer.restart(); + if(knownAPs.indexOf(beacon.bssid) >= 0) { + // Already listed + return; } + + knownAPs.add(beacon); + printBeacon(beacon); + timer.restart(); } -static void onClient(const ClientInfo& client) +void onClient(const ClientInfo& client) { - if(knownClients.indexOf(client.station) < 0) { - knownClients.add(client); - printClient(client); - timer.restart(); + if(knownClients.indexOf(client.station) >= 0) { + // Allready listed + return; } + + knownClients.add(client); + printClient(client); + timer.restart(); } -static void scanChannel(void* param) +void scanChannel(void* param) { auto channel = reinterpret_cast(param); if(channel <= 15) { @@ -97,21 +109,22 @@ static void scanChannel(void* param) sniffer.setChannel(channel); timer.initializeMs(scanChannel, reinterpret_cast(channel + 1)); timer.startOnce(); - } else { - // Stop sniffing and display final scan results - sniffer.end(); - printSummary(); + return; } + + // Stop sniffing and display final scan results + sniffer.end(); + printSummary(); } +} // namespace + void init() { Serial.begin(SERIAL_BAUD_RATE); // 115200 by default Serial.systemDebugOutput(true); // Debug output to serial - Serial << _F("\r\n\r\n" - "SDK version:") - << system_get_sdk_version() << endl; + Serial << endl << endl << _F("SDK version:") << system_get_sdk_version() << endl; Serial.println(_F("ESP8266 mini-sniff by Ray Burnette http://www.hackster.io/rayburne/projects")); Serial.println( _F("Type: /---------MAC---------/-----WiFi Access Point SSID-----/ /------MAC------/ Chnl RSSI")); From 1bcb31dbf165468177b9197cd35d95a867fb3989 Mon Sep 17 00:00:00 2001 From: Mike Date: Mon, 8 Apr 2024 10:11:53 +0100 Subject: [PATCH 042/128] Fix potential memory leaks in `TcpClient::send` (#2753) This PR addresses a concern raised by Coverity. The `TcpClient::send(const char*, ...` method is tricky because it can either use the existing stream or create a new one. Further leaks are possible in `TcpClient::send(IDataSourceStream*, ...` when inspecting the failure paths. The code has been refactored using `std::unique_ptr` guards. [scan:coverity] --- .../Network/src/Network/TcpClient.cpp | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/Sming/Components/Network/src/Network/TcpClient.cpp b/Sming/Components/Network/src/Network/TcpClient.cpp index 56b151352d..0a0a58fb60 100644 --- a/Sming/Components/Network/src/Network/TcpClient.cpp +++ b/Sming/Components/Network/src/Network/TcpClient.cpp @@ -44,9 +44,14 @@ bool TcpClient::send(const char* data, uint16_t len, bool forceCloseAfterSent) return false; } + std::unique_ptr newStream; auto memoryStream = static_cast(stream); if(memoryStream == nullptr || memoryStream->getStreamType() != eSST_MemoryWritable) { - memoryStream = new MemoryDataStream(); + newStream = std::make_unique(); + if(!newStream) { + return false; + } + memoryStream = newStream.get(); } if(!memoryStream->ensureCapacity(memoryStream->getSize() + len)) { @@ -56,11 +61,17 @@ bool TcpClient::send(const char* data, uint16_t len, bool forceCloseAfterSent) memoryStream->write(data, len); + (void)newStream.release(); return send(memoryStream, forceCloseAfterSent); } bool TcpClient::send(IDataSourceStream* source, bool forceCloseAfterSent) { + std::unique_ptr sourceRef; + if(stream != source) { + sourceRef.reset(source); + } + if(state != eTCS_Connecting && state != eTCS_Connected) { return false; } @@ -76,36 +87,32 @@ bool TcpClient::send(IDataSourceStream* source, bool forceCloseAfterSent) auto chainStream = static_cast(stream); if(!chainStream->attachStream(source)) { debug_w("Unable to attach source to existing stream chain!"); - delete source; return false; } } else { debug_d("Creating stream chain ..."); - auto chainStream = new StreamChain(); - if(chainStream == nullptr) { - delete source; + auto chainStream = std::make_unique(); + if(!chainStream) { debug_w("Unable to create stream chain!"); return false; } if(!chainStream->attachStream(stream)) { - delete source; - delete chainStream; debug_w("Unable to attach stream to new chain!"); return false; } if(!chainStream->attachStream(source)) { - delete source; - delete chainStream; debug_w("Unable to attach source to new chain!"); return false; } - stream = chainStream; + stream = chainStream.release(); } } + (void)sourceRef.release(); + int length = source->available(); if(length > 0) { totalSentBytes += length; From 39f0cf69d4d62d12010813fadbad34e8f2fb03fc Mon Sep 17 00:00:00 2001 From: Mike Date: Wed, 10 Apr 2024 12:25:08 +0100 Subject: [PATCH 043/128] PYTHON not set correctly (#2759) The `PYTHON` environment variable isn't set correctly for CI builds, or for other installations where non-standard locations may be used. This can result in unexpected behaviour where multiple python3 versions are installed. Update to #2735. --- Tools/export.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tools/export.sh b/Tools/export.sh index 473c449568..763f395023 100755 --- a/Tools/export.sh +++ b/Tools/export.sh @@ -28,7 +28,7 @@ if [ -z "$SMING_HOME" ]; then fi # Common -export PYTHON=${PYTHON:=/usr/bin/python3} +export PYTHON=${PYTHON:=$(which python3)} # Esp8266 export ESP_HOME=${ESP_HOME:=/opt/esp-quick-toolchain} From 38a85ff53891d7ad1e7d7bf5bb77cc1896f25b4b Mon Sep 17 00:00:00 2001 From: Mike Date: Wed, 10 Apr 2024 13:23:17 +0100 Subject: [PATCH 044/128] Allow pillow upgrade to latest (#2757) This PR removes the sphinx `seqdiag` support as is broken with recent versions of pillow. There is only one affected diagram in the documentation so this has been replaced with a `.png` image. The restriction on `pillow` version has been removed to satisfy #2755. Sequence diagrams are only used here https://smingdev.readthedocs.io/en/latest/information/tasks.html. --- docs/requirements.txt | 3 +-- docs/source/conf.py | 1 - docs/source/information/tasks.png | Bin 0 -> 8747 bytes docs/source/information/tasks.rst | 26 +------------------------- 4 files changed, 2 insertions(+), 28 deletions(-) create mode 100644 docs/source/information/tasks.png diff --git a/docs/requirements.txt b/docs/requirements.txt index 0c9339c546..bbb9040a49 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -6,7 +6,6 @@ m2r2==0.3.3.post2 breathe==4.35.0 sphinxcontrib-wavedrom sphinx-copybutton -sphinxcontrib-seqdiag jinja2>=3.0.3 setuptools>=57.5.0 -pillow==9.5.0 # V10 breaks seqdiag +pillow diff --git a/docs/source/conf.py b/docs/source/conf.py index 11d98d2e10..ed62d3a349 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -48,7 +48,6 @@ extensions = [ 'm2r2', 'breathe', - 'sphinxcontrib.seqdiag', 'link-roles', 'sphinxcontrib.wavedrom', 'sphinx_copybutton', diff --git a/docs/source/information/tasks.png b/docs/source/information/tasks.png new file mode 100644 index 0000000000000000000000000000000000000000..86baa87419e8231af979dc61778d2df6e9b8667c GIT binary patch literal 8747 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 literal 0 HcmV?d00001 diff --git a/docs/source/information/tasks.rst b/docs/source/information/tasks.rst index 339406cd69..42aaf6679c 100644 --- a/docs/source/information/tasks.rst +++ b/docs/source/information/tasks.rst @@ -17,31 +17,7 @@ The BasicTask class This class uses the task queue plus a timer to provide an easy way to write a background task. All you need to is define a *loop()* function which does the work. The task has three states: -.. seqdiag:: - :caption: Task states - :align: center - - seqdiag task-states { - activation = none; - node_width = 80; - node_height = 60; - edge_length = 160; - span_height = 5; - default_shape = roundedbox; - default_fontsize = 12; - - SUSPENDED [label = "suspended"]; - RUNNING [label = "running"]; - SLEEPING [label = "sleeping"]; - - SUSPENDED -> RUNNING [label = "resume()"]; - SUSPENDED -> SLEEPING [label = "sleep()"]; - RUNNING -> SLEEPING [label = "sleep()"]; - RUNNING -> SUSPENDED [label = "suspend()"]; - SLEEPING -> RUNNING [label = "resume()"]; - SLEEPING -> RUNNING [label = "timer expired"]; - SLEEPING -> SUSPENDED [label = "suspend()"]; - } +.. image:: tasks.png To see this in operation, have a look at the :sample:`Basic_Tasks` sample. From 20cc413f76d21875cafa2fd22f719be6b37983cd Mon Sep 17 00:00:00 2001 From: slaff Date: Wed, 10 Apr 2024 15:10:44 +0200 Subject: [PATCH 045/128] Fix typos reported by codespell. (#2760) --- Sming/Libraries/APA102/apa102.h | 2 +- Sming/Libraries/AtClient/README.rst | 4 ++-- .../LiquidCrystal/examples/i2cLCDextraIO/i2cLCDextraIO.pde | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sming/Libraries/APA102/apa102.h b/Sming/Libraries/APA102/apa102.h index 57397c0deb..f2a05788c6 100644 --- a/Sming/Libraries/APA102/apa102.h +++ b/Sming/Libraries/APA102/apa102.h @@ -29,7 +29,7 @@ class APA102 APA102(uint16_t n); /** - * @brief Iniitialise for given number of LEDs on specific SPI device + * @brief Initialise for given number of LEDs on specific SPI device */ APA102(uint16_t n, SPIBase& spiRef); diff --git a/Sming/Libraries/AtClient/README.rst b/Sming/Libraries/AtClient/README.rst index 1b37edde1b..561e910df9 100644 --- a/Sming/Libraries/AtClient/README.rst +++ b/Sming/Libraries/AtClient/README.rst @@ -17,7 +17,7 @@ This library simplifies the communication with such devices. Usage ----- -1. Add ``COMPONENT_DEPENDS += AtClient`` to your application componenent.mk file. +1. Add ``COMPONENT_DEPENDS += AtClient`` to your application component.mk file. 2. Add these lines to your application:: #include @@ -38,4 +38,4 @@ Usage atClient = new AtClient(Serial); atClient->send("ATE0\r"); // ... - } \ No newline at end of file + } diff --git a/Sming/Libraries/LiquidCrystal/examples/i2cLCDextraIO/i2cLCDextraIO.pde b/Sming/Libraries/LiquidCrystal/examples/i2cLCDextraIO/i2cLCDextraIO.pde index bbeefe01fe..10aa2ce121 100644 --- a/Sming/Libraries/LiquidCrystal/examples/i2cLCDextraIO/i2cLCDextraIO.pde +++ b/Sming/Libraries/LiquidCrystal/examples/i2cLCDextraIO/i2cLCDextraIO.pde @@ -36,7 +36,7 @@ /*! @defined CHAR_WIDTH - @abstract Character witdth of the display, expressed in pixeles per character. + @abstract Character width of the display, expressed in pixeles per character. */ #define CHAR_WIDTH 5 From 040d6bd168178b09bcd0b05c1dc36b35432f7389 Mon Sep 17 00:00:00 2001 From: Mike Date: Fri, 12 Apr 2024 11:51:47 +0100 Subject: [PATCH 046/128] Optimise FlashString iteration and indexing (#2761) This PR improves performance of `FlashString` library object iteration and indexing. --- .../libc/src/include/sys/pgmspace.h | 20 +++++++++---------- .../Components/libc/include/sys/pgmspace.h | 20 +++++++++---------- .../Components/libc/include/sys/pgmspace.h | 20 +++++++++---------- .../libc/src/include/sys/pgmspace.h | 20 +++++++++---------- Sming/Components/FlashString | 2 +- 5 files changed, 41 insertions(+), 41 deletions(-) diff --git a/Sming/Arch/Esp32/Components/libc/src/include/sys/pgmspace.h b/Sming/Arch/Esp32/Components/libc/src/include/sys/pgmspace.h index a2c3294ebe..3a103d4a86 100644 --- a/Sming/Arch/Esp32/Components/libc/src/include/sys/pgmspace.h +++ b/Sming/Arch/Esp32/Components/libc/src/include/sys/pgmspace.h @@ -63,17 +63,17 @@ extern "C" { #define pgm_read_dword_far(addr) pgm_read_dword(addr) #define pgm_read_float_far(addr) pgm_read_float(addr) -#define memcpy_P(dest, src, num) memcpy(dest, src, num) -#define memcmp_P(a1, b1, len) memcmp(a1, b1, len) -#define strlen_P(a) strlen(a) -#define strcpy_P(dest, src) strcpy(dest, src) -#define strncpy_P(dest, src, size) strncpy(dest, src, size) -#define strcmp_P(a, b) strcmp(a, b) +#define memcpy_P(dest, src_P, num) memcpy(dest, src_P, num) +#define memcmp_P(buf1, buf2_P, len) memcmp(buf1, buf2_P, len) +#define strlen_P(str_P) strlen(str_P) +#define strcpy_P(dest, src_P) strcpy(dest, src_P) +#define strncpy_P(dest, src_P, size) strncpy(dest, src_P, size) +#define strcmp_P(str1, str2_P) strcmp(str1, str2_P) #define strncmp_P(str1, str2_P, size) strncmp(str1, str2_P, size) -#define strcasecmp_P(a, b) strcasecmp(a, b) -#define strcat_P(dest, src) strcat(dest, src) -#define strstr_P(a, b) strstr(a, b) -#define sprintf_P(s, f, ...) m_snprintf(s, 1024, f, ##__VA_ARGS__) +#define strcasecmp_P(str1, str2_P) strcasecmp(str1, str2_P) +#define strcat_P(dest, src_P) strcat(dest, src_P) +#define strstr_P(haystack, needle_P) strstr(haystack, needle_P) +#define sprintf_P(str, format_P, ...) m_snprintf(str, 1024, format_P, ##__VA_ARGS__) #ifdef __cplusplus } diff --git a/Sming/Arch/Esp8266/Components/libc/include/sys/pgmspace.h b/Sming/Arch/Esp8266/Components/libc/include/sys/pgmspace.h index 42a23c0b1b..0d348681b5 100644 --- a/Sming/Arch/Esp8266/Components/libc/include/sys/pgmspace.h +++ b/Sming/Arch/Esp8266/Components/libc/include/sys/pgmspace.h @@ -153,16 +153,16 @@ static inline uint16_t pgm_read_word_inlined(const void* addr) * @{ */ -void* memcpy_P(void* dest, const void* src_P, size_t length); -int memcmp_P(const void* a1, const void* b1, size_t len); -size_t strlen_P(const char* src_P); -char* strcpy_P(char* dest, const char* src_P); -char* strncpy_P(char* dest, const char* src_P, size_t size); -int strcmp_P(const char* str1, const char* str2_P); -int strncmp_P(const char* str1, const char* str2_P, const size_t size); -int strcasecmp_P(const char* str1, const char* str2_P); -char* strcat_P(char* dest, const char* src_P); -char* strstr_P(char* haystack, const char* needle_P); +void* memcpy_P(void* dest, PGM_VOID_P src_P, size_t length); +int memcmp_P(const void* buf1, PGM_VOID_P buf2_P, size_t len); +size_t strlen_P(PGM_VOID_P src_P); +char* strcpy_P(char* dest, PGM_P src_P); +char* strncpy_P(char* dest, PGM_P src_P, size_t size); +int strcmp_P(const char* str1, PGM_P str2_P); +int strncmp_P(const char* str1, PGM_P str2_P, const size_t size); +int strcasecmp_P(const char* str1, PGM_P str2_P); +char* strcat_P(char* dest, PGM_P src_P); +char* strstr_P(char* haystack, PGM_P needle_P); #define sprintf_P(s, f_P, ...) \ (__extension__({ \ diff --git a/Sming/Arch/Host/Components/libc/include/sys/pgmspace.h b/Sming/Arch/Host/Components/libc/include/sys/pgmspace.h index e97595d139..3d1917db5c 100644 --- a/Sming/Arch/Host/Components/libc/include/sys/pgmspace.h +++ b/Sming/Arch/Host/Components/libc/include/sys/pgmspace.h @@ -39,17 +39,17 @@ bool isFlashPtr(const void* ptr); #define pgm_read_dword_far(addr) pgm_read_dword(addr) #define pgm_read_float_far(addr) pgm_read_float(addr) -#define memcpy_P(dest, src, num) memcpy(dest, src, num) -#define memcmp_P(a1, b1, len) memcmp(a1, b1, len) -#define strlen_P(a) strlen(a) -#define strcpy_P(dest, src) strcpy(dest, src) -#define strncpy_P(dest, src, size) strncpy(dest, src, size) -#define strcmp_P(a, b) strcmp(a, b) +#define memcpy_P(dest, src_P, num) memcpy(dest, src_P, num) +#define memcmp_P(buf1, buf2_P, len) memcmp(buf1, buf2_P, len) +#define strlen_P(str_P) strlen(str_P) +#define strcpy_P(dest, src_P) strcpy(dest, src_P) +#define strncpy_P(dest, src_P, size) strncpy(dest, src_P, size) +#define strcmp_P(str1, str2_P) strcmp(str1, str2_P) #define strncmp_P(str1, str2_P, size) strncmp(str1, str2_P, size) -#define strcasecmp_P(a, b) strcasecmp(a, b) -#define strcat_P(dest, src) strcat(dest, src) -#define strstr_P(a, b) strstr(a, b) -#define sprintf_P(s, f, ...) m_snprintf(s, 1024, f, ##__VA_ARGS__) +#define strcasecmp_P(str1, str2_P) strcasecmp(str1, str2_P) +#define strcat_P(dest, src_P) strcat(dest, src_P) +#define strstr_P(haystack, needle_P) strstr(haystack, needle_P) +#define sprintf_P(str, format_P, ...) m_snprintf(str, 1024, format_P, ##__VA_ARGS__) #ifdef __cplusplus } diff --git a/Sming/Arch/Rp2040/Components/libc/src/include/sys/pgmspace.h b/Sming/Arch/Rp2040/Components/libc/src/include/sys/pgmspace.h index 676e70fec6..a04deaa0c9 100644 --- a/Sming/Arch/Rp2040/Components/libc/src/include/sys/pgmspace.h +++ b/Sming/Arch/Rp2040/Components/libc/src/include/sys/pgmspace.h @@ -63,17 +63,17 @@ extern "C" { #define pgm_read_dword_far(addr) pgm_read_dword(addr) #define pgm_read_float_far(addr) pgm_read_float(addr) -#define memcpy_P(dest, src, num) memcpy(dest, src, num) -#define memcmp_P(a1, b1, len) memcmp(a1, b1, len) -#define strlen_P(a) strlen(a) -#define strcpy_P(dest, src) strcpy(dest, src) -#define strncpy_P(dest, src, size) strncpy(dest, src, size) -#define strcmp_P(a, b) strcmp(a, b) +#define memcpy_P(dest, src_P, num) memcpy(dest, src_P, num) +#define memcmp_P(buf1, buf2_P, len) memcmp(buf1, buf2_P, len) +#define strlen_P(str_P) strlen(str_P) +#define strcpy_P(dest, src_P) strcpy(dest, src_P) +#define strncpy_P(dest, src_P, size) strncpy(dest, src_P, size) +#define strcmp_P(str1, str2_P) strcmp(str1, str2_P) #define strncmp_P(str1, str2_P, size) strncmp(str1, str2_P, size) -#define strcasecmp_P(a, b) strcasecmp(a, b) -#define strcat_P(dest, src) strcat(dest, src) -#define strstr_P(a, b) strstr(a, b) -#define sprintf_P(s, f, ...) m_snprintf(s, 1024, f, ##__VA_ARGS__) +#define strcasecmp_P(str1, str2_P) strcasecmp(str1, str2_P) +#define strcat_P(dest, src_P) strcat(dest, src_P) +#define strstr_P(haystack, needle_P) strstr(haystack, needle_P) +#define sprintf_P(str, format_P, ...) m_snprintf(str, 1024, format_P, ##__VA_ARGS__) #ifdef __cplusplus } diff --git a/Sming/Components/FlashString b/Sming/Components/FlashString index d9a0a50e4d..d7a129d6a6 160000 --- a/Sming/Components/FlashString +++ b/Sming/Components/FlashString @@ -1 +1 @@ -Subproject commit d9a0a50e4d91f0d7ba68166cae6e0d52ac474453 +Subproject commit d7a129d6a6b2f3e8e9b90b143556fdcb8587e77f From 3c39c29412a4b46b345fa6aafa9d5b33866e8cf1 Mon Sep 17 00:00:00 2001 From: Mike Date: Fri, 12 Apr 2024 11:54:51 +0100 Subject: [PATCH 047/128] Fix and expand DateTime class, address 64-bit time_t issues (#2762) This PR improves test coverage for the `DateTime` class and adds a number of necessary fixes. It also adds some obvious omissions. **Compare day/month names without case-sensitivity** Allows more use cases when converting strings. **Ensure host builds (linux) use 64-bit `time_t`** Defaults to 32-bit if not specified by `_TIME_BITS`. Now consistent with embedded toolchains which use fixed size (all but IDF 4.x are 64-bit). See issue #2758. **Document and add static check for time_t range** CI will fail build if incorrect. For legacy toolchains (Windows host, IDF 4.x) will fail if it ever gets fixed as a reminder. **`mktime` broken by axtls. Correct implementation available in newlib.** Calls esp8266 ROM function which doesn't support 64-bit time_t. **Fix `isLeapYear` implementation** Doesn't account for century. **Add enumeration for Month value** For backward compatibility methods use standard types, but makes code easier to read and avoids off-by-one errors in application code. **Fix `toUnixTime` to accommodate negative values** `time_t` can represent dates before 1 Jan 1970. **Fix `toUnixTime` parameter handling outside normal range** Comment reads: "Seconds, minutes, hours and days may be any value, e.g. to calculate the value for 300 days since 1970 (epoch), set day=300" This requires a more appropriate parameter type (int) and casting so calculations are 64-bit. **Apply `const` to DateTime methods** As appropriate. **Add `fromISO8601` method** Complements `toISO8601`. Update Basic_DateTime sample so strings can be interpreted. **Expand HTTP string conversion** Various RFC versions suggest more relaxed interpretation of strings. Accept e.g. "Sun" or "Sunday". Also "Sunasdlfkj" but do we care? Make leading day of week optional, since the value gets discarded anyway Fold whitespace **Fix `setTime`, `fromUnixTime` methods** Results are not always correct. e.g. Out by a whole day for `0x66152e16`. Code in `gmtime_r` markedly different, and not bloaty, so just use that. **Provide `gmtime_r` implementation for Windows host builds** Windows does have `gmtime` but doesn't behave like glibc/newlib does. Safest to just copy the code from newlib. https://sourceware.org/git/?p=newlib-cygwin.git;a=blob;f=newlib/libc/time/gmtime_r.c **Use DateTime to produce default IFS TimeStamp string** Just output UTC, localising will likely be wrong. **Include milliseconds with '%T' format if non-zero** Consistent with ISO time strings. This is different from libC `strftime` behaviour, but then `struct tm` doesn't have a fractional seconds field. **Update tests** Generate test data using python script Check dates before 1970 Include 64-bit-only tests Verify `setTime` calls with out-of-range offsets **Pull out some utility methods. Some internal functions could be useful so add those as static methods:** bool isLeapYear(uint16_t year); uint8_t getMonthDays(uint8_t month, uint16_t year); String getLocaleDayName(uint8_t day); String getLocaleMonthName(uint8_t month); String getIsoDayName(uint8_t day); String getIsoMonthName(uint8_t month); uint16_t getDaysInYear(uint16_t year); **Replace helper macros with inline functions** --- Sming/Arch/Host/Components/libc/gmtime_r.c | 122 +++++ .../Arch/Host/Components/libc/include/time.h | 17 + Sming/Arch/Host/build.mk | 3 +- Sming/Components/IFS | 2 +- Sming/Components/axtls-8266/axtls-8266.patch | 21 +- Sming/Core/DateTime.cpp | 420 ++++++++++++------ Sming/Core/DateTime.h | 180 ++++++-- docs/source/upgrading/5.1-5.2.rst | 32 +- samples/Basic_DateTime/app/application.cpp | 15 +- tests/HostTests/README.rst | 13 + tests/HostTests/include/DateTimeData.h | 76 ++++ tests/HostTests/modules/DateTime.cpp | 176 ++++++-- tests/HostTests/tools/datetime-test.py | 211 +++++++++ 13 files changed, 1069 insertions(+), 219 deletions(-) create mode 100644 Sming/Arch/Host/Components/libc/gmtime_r.c create mode 100644 Sming/Arch/Host/Components/libc/include/time.h create mode 100644 tests/HostTests/README.rst create mode 100644 tests/HostTests/include/DateTimeData.h create mode 100644 tests/HostTests/tools/datetime-test.py diff --git a/Sming/Arch/Host/Components/libc/gmtime_r.c b/Sming/Arch/Host/Components/libc/gmtime_r.c new file mode 100644 index 0000000000..b9af8fc798 --- /dev/null +++ b/Sming/Arch/Host/Components/libc/gmtime_r.c @@ -0,0 +1,122 @@ +/* + * gmtime_r.c + * Original Author: Adapted from tzcode maintained by Arthur David Olson. + * Modifications: + * - Changed to mktm_r and added __tzcalc_limits - 04/10/02, Jeff Johnston + * - Fixed bug in mday computations - 08/12/04, Alex Mogilnikov + * - Fixed bug in __tzcalc_limits - 08/12/04, Alex Mogilnikov + * - Move code from _mktm_r() to gmtime_r() - 05/09/14, Freddie Chopin + * - Fixed bug in calculations for dates after year 2069 or before year 1901. Ideas for + * solution taken from musl's __secs_to_tm() - 07/12/2014, Freddie Chopin + * + * - Use faster algorithm from civil_from_days() by Howard Hinnant - 12/06/2014, + * Freddie Chopin + * + * Converts the calendar time pointed to by tim_p into a broken-down time + * expressed as local time. Returns a pointer to a structure containing the + * broken-down time. + */ + +#ifdef __WIN32 + +#include + +#define SECSPERMIN 60L +#define MINSPERHOUR 60L +#define HOURSPERDAY 24L +#define SECSPERHOUR (SECSPERMIN * MINSPERHOUR) +#define SECSPERDAY (SECSPERHOUR * HOURSPERDAY) +#define DAYSPERWEEK 7 +#define MONSPERYEAR 12 + +#define YEAR_BASE 1900 +#define EPOCH_YEAR 1970 +#define EPOCH_WDAY 4 +#define EPOCH_YEARS_SINCE_LEAP 2 +#define EPOCH_YEARS_SINCE_CENTURY 70 +#define EPOCH_YEARS_SINCE_LEAP_CENTURY 370 + +#define isleap(y) ((((y) % 4) == 0 && ((y) % 100) != 0) || ((y) % 400) == 0) + +/* Move epoch from 01.01.1970 to 01.03.0000 (yes, Year 0) - this is the first + * day of a 400-year long "era", right after additional day of leap year. + * This adjustment is required only for date calculation, so instead of + * modifying time_t value (which would require 64-bit operations to work + * correctly) it's enough to adjust the calculated number of days since epoch. + */ +#define EPOCH_ADJUSTMENT_DAYS 719468L +/* year to which the adjustment was made */ +#define ADJUSTED_EPOCH_YEAR 0 +/* 1st March of year 0 is Wednesday */ +#define ADJUSTED_EPOCH_WDAY 3 +/* there are 97 leap years in 400-year periods. ((400 - 97) * 365 + 97 * 366) */ +#define DAYS_PER_ERA 146097L +/* there are 24 leap years in 100-year periods. ((100 - 24) * 365 + 24 * 366) */ +#define DAYS_PER_CENTURY 36524L +/* there is one leap year every 4 years */ +#define DAYS_PER_4_YEARS (3 * 365 + 366) +/* number of days in a non-leap year */ +#define DAYS_PER_YEAR 365 +/* number of days in January */ +#define DAYS_IN_JANUARY 31 +/* number of days in non-leap February */ +#define DAYS_IN_FEBRUARY 28 +/* number of years per era */ +#define YEARS_PER_ERA 400 + +struct tm * +gmtime_r (const time_t *__restrict tim_p, + struct tm *__restrict res) +{ + long days, rem; + const time_t lcltime = *tim_p; + int era, weekday, year; + unsigned erayear, yearday, month, day; + unsigned long eraday; + + days = lcltime / SECSPERDAY + EPOCH_ADJUSTMENT_DAYS; + rem = lcltime % SECSPERDAY; + if (rem < 0) + { + rem += SECSPERDAY; + --days; + } + + /* compute hour, min, and sec */ + res->tm_hour = (int) (rem / SECSPERHOUR); + rem %= SECSPERHOUR; + res->tm_min = (int) (rem / SECSPERMIN); + res->tm_sec = (int) (rem % SECSPERMIN); + + /* compute day of week */ + if ((weekday = ((ADJUSTED_EPOCH_WDAY + days) % DAYSPERWEEK)) < 0) + weekday += DAYSPERWEEK; + res->tm_wday = weekday; + + /* compute year, month, day & day of year */ + /* for description of this algorithm see + * http://howardhinnant.github.io/date_algorithms.html#civil_from_days */ + era = (days >= 0 ? days : days - (DAYS_PER_ERA - 1)) / DAYS_PER_ERA; + eraday = days - era * DAYS_PER_ERA; /* [0, 146096] */ + erayear = (eraday - eraday / (DAYS_PER_4_YEARS - 1) + eraday / DAYS_PER_CENTURY - + eraday / (DAYS_PER_ERA - 1)) / 365; /* [0, 399] */ + yearday = eraday - (DAYS_PER_YEAR * erayear + erayear / 4 - erayear / 100); /* [0, 365] */ + month = (5 * yearday + 2) / 153; /* [0, 11] */ + day = yearday - (153 * month + 2) / 5 + 1; /* [1, 31] */ + month += month < 10 ? 2 : -10; + year = ADJUSTED_EPOCH_YEAR + erayear + era * YEARS_PER_ERA + (month <= 1); + + res->tm_yday = yearday >= DAYS_PER_YEAR - DAYS_IN_JANUARY - DAYS_IN_FEBRUARY ? + yearday - (DAYS_PER_YEAR - DAYS_IN_JANUARY - DAYS_IN_FEBRUARY) : + yearday + DAYS_IN_JANUARY + DAYS_IN_FEBRUARY + isleap(erayear); + res->tm_year = year - YEAR_BASE; + res->tm_mon = month; + res->tm_mday = day; + + res->tm_isdst = 0; + + return (res); +} + +#endif // __WIN32 + diff --git a/Sming/Arch/Host/Components/libc/include/time.h b/Sming/Arch/Host/Components/libc/include/time.h new file mode 100644 index 0000000000..4b54658620 --- /dev/null +++ b/Sming/Arch/Host/Components/libc/include/time.h @@ -0,0 +1,17 @@ +#pragma once + +#include_next + +#ifdef __WIN32 + +#ifdef __cplusplus +extern "C" { +#endif + +struct tm* gmtime_r(const time_t*, struct tm*); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/Sming/Arch/Host/build.mk b/Sming/Arch/Host/build.mk index 171b724b81..d50633bef1 100644 --- a/Sming/Arch/Host/build.mk +++ b/Sming/Arch/Host/build.mk @@ -23,7 +23,8 @@ GCC_UPGRADE_URL := https://sming.readthedocs.io/en/latest/arch/host/host-emulato CPPFLAGS += \ -m32 \ -Wno-deprecated-declarations \ - -D_FILE_OFFSET_BITS=64 + -D_FILE_OFFSET_BITS=64 \ + -D_TIME_BITS=64 # => Tools MEMANALYZER = size diff --git a/Sming/Components/IFS b/Sming/Components/IFS index ec9a902121..c72b265e53 160000 --- a/Sming/Components/IFS +++ b/Sming/Components/IFS @@ -1 +1 @@ -Subproject commit ec9a902121afaebd882b7627907e8fffa78c7b5a +Subproject commit c72b265e53ac215a979eff907d216eab068987df diff --git a/Sming/Components/axtls-8266/axtls-8266.patch b/Sming/Components/axtls-8266/axtls-8266.patch index dd20ab83dd..16eff5af6a 100644 --- a/Sming/Components/axtls-8266/axtls-8266.patch +++ b/Sming/Components/axtls-8266/axtls-8266.patch @@ -220,7 +220,7 @@ index 53509d0..25c568d 100644 #endif diff --git a/replacements/time.c b/replacements/time.c -index 4972119..3e7e407 100644 +index 4972119..d39481c 100644 --- a/replacements/time.c +++ b/replacements/time.c @@ -16,29 +16,25 @@ @@ -286,7 +286,7 @@ index 4972119..3e7e407 100644 { sntp_stop(); -@@ -93,22 +89,24 @@ void configTime(int timezone, int daylightOffset_sec, const char* server1, const +@@ -93,22 +89,16 @@ void configTime(int timezone, int daylightOffset_sec, const char* server1, const sntp_init(); } @@ -302,21 +302,20 @@ index 4972119..3e7e407 100644 return 0; } - // seconds since 1970 +-// seconds since 1970 -time_t mktime(struct tm *t) -+time_t WEAK_ATTR mktime(struct tm *t) - { - // system_mktime expects month in range 1..12 - #define START_MONTH 1 - return DIFF1900TO1970 + system_mktime(t->tm_year, t->tm_mon + START_MONTH, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec); - } - +-{ +- // system_mktime expects month in range 1..12 +- #define START_MONTH 1 +- return DIFF1900TO1970 + system_mktime(t->tm_year, t->tm_mon + START_MONTH, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec); +-} +- -time_t time(time_t * t) +time_t WEAK_ATTR time(time_t * t) { time_t seconds = sntp_get_current_timestamp(); if (t) -@@ -118,30 +116,32 @@ time_t time(time_t * t) +@@ -118,30 +108,32 @@ time_t time(time_t * t) return seconds; } diff --git a/Sming/Core/DateTime.cpp b/Sming/Core/DateTime.cpp index 67367c13f7..bd76f59d47 100644 --- a/Sming/Core/DateTime.cpp +++ b/Sming/Core/DateTime.cpp @@ -9,58 +9,111 @@ #include "DateTime.h" #include "Data/CStringArray.h" -#include -#define LEAP_YEAR(year) ((year % 4) == 0) +#ifdef ARCH_ESP32 +#include +#endif -DEFINE_FSTR_LOCAL(flashMonthNames, LOCALE_MONTH_NAMES); -DEFINE_FSTR_LOCAL(flashDayNames, LOCALE_DAY_NAMES); +#if defined(__WIN32) || (defined(ARCH_ESP32) && ESP_IDF_VERSION_MAJOR < 5) +static_assert(sizeof(time_t) != 8, "Great! Now supports 64-bit - please update code"); +#warning "**Y2038** time_t is only 32-bits in this build configuration" +#else +static_assert(sizeof(time_t) == 8, "Expecting 64-bit time_t"); +#endif + +namespace +{ +DEFINE_FSTR(localeMonthNames, LOCALE_MONTH_NAMES); +DEFINE_FSTR(localeDayNames, LOCALE_DAY_NAMES); /* We can more efficiently compare text of 4 character length by comparing as words */ union FourDigitName { char c[4]; uint32_t value; + // Compare without case-sensitivity bool operator==(const FourDigitName& name) const { - return value == name.value; + constexpr uint32_t caseMask{~0x20202020U}; + return ((value ^ name.value) & caseMask) == 0; } -}; -static const FourDigitName isoDayNames[7] PROGMEM = { - {'S', 'u', 'n', '\0'}, {'M', 'o', 'n', '\0'}, {'T', 'u', 'e', '\0'}, {'W', 'e', 'd', '\0'}, - {'T', 'h', 'u', '\0'}, {'F', 'r', 'i', '\0'}, {'S', 'a', 't', '\0'}, + explicit operator String() const + { + return String(c, 4); + } }; -static const FourDigitName isoMonthNames[12] PROGMEM = { - {'J', 'a', 'n', '\0'}, {'F', 'e', 'b', '\0'}, {'M', 'a', 'r', '\0'}, {'A', 'p', 'r', '\0'}, - {'M', 'a', 'y', '\0'}, {'J', 'u', 'n', '\0'}, {'J', 'u', 'l', '\0'}, {'A', 'u', 'g', '\0'}, - {'S', 'e', 'p', '\0'}, {'O', 'c', 't', '\0'}, {'N', 'o', 'v', '\0'}, {'D', 'e', 'c', '\0'}, -}; +const FourDigitName isoDayNames[7] PROGMEM = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; + +const FourDigitName isoMonthNames[12] PROGMEM = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; + +/* + * @brief Match a day or month name against a list of values and set the required value + * @param ptr Current string position + * @param value On success, contains index of matched string + * @isoNames Array of values + * @nameCount Number of names in array + * @retval bool false on failure + * + * Names are compared without case-sensitivity + */ +bool matchName(const char*& ptr, uint8_t& value, const FourDigitName isoNames[], unsigned nameCount) +{ + FourDigitName name{ptr[0], ptr[1], ptr[2]}; + for(unsigned i = 0; i < nameCount; ++i) { + if(isoNames[i] == name) { + value = i; + ptr += 3; + return true; + } + } + return false; +} + +bool isLeapCentury(uint16_t year) +{ + return year % 100 != 0 || year % 400 == 0; +} + +} // namespace + +bool DateTime::isLeapYear(uint16_t year) +{ + return year % 4 == 0 && isLeapCentury(year); +} /** @brief Get the number of days in a month, taking leap years into account * @param month 0=jan * @param year * @retval uint8_t number of days in the month */ -static uint8_t getMonthDays(uint8_t month, uint8_t year) +uint8_t DateTime::getMonthDays(uint8_t month, uint16_t year) { - static const uint8_t monthDays[] PROGMEM = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; - return (month == 1 && LEAP_YEAR(year)) ? 29 : pgm_read_byte(&monthDays[month]); + if(month == dtFebruary) { + return isLeapYear(year) ? 29 : 28; + } + const uint32_t monthDays = 0b101011010101; + return 30 + ((monthDays >> month) & 0x01); } -//****************************************************************************** -//* DateTime Public Methods -//****************************************************************************** - void DateTime::setTime(time_t time) { - fromUnixTime(time, &Second, &Minute, &Hour, &Day, &DayofWeek, &Month, &Year); + struct tm t; + gmtime_r(&time, &t); + Year = t.tm_year + 1900; + Month = t.tm_mon; + Day = t.tm_mday; + DayofWeek = t.tm_wday; + Hour = t.tm_hour; + Minute = t.tm_min; + Second = t.tm_sec; Milliseconds = 0; calcDayOfYear(); } -bool DateTime::isNull() +bool DateTime::isNull() const { return Second == 0 && Minute == 0 && Hour == 0 && Day == 0 && Month == 0 && Year == 0 && DayofWeek == 0 && Milliseconds == 0; @@ -68,102 +121,197 @@ bool DateTime::isNull() bool DateTime::fromHttpDate(const String& httpDate) { - int first = httpDate.indexOf(','); - if(first < 0 || httpDate.length() - first < 20) { - return false; - } - first += 2; // Skip ", " auto ptr = httpDate.c_str(); // Parse and return a decimal number and update ptr to the first non-numeric character after it auto parseNumber = [&ptr]() { return strtol(ptr, const_cast(&ptr), 10); }; - // Match a day or month name against a list of values and set the required value; return false on failure - auto matchName = [&ptr](uint8_t& value, const FourDigitName isoNames[], unsigned nameCount) { - FourDigitName name = {ptr[0], ptr[1], ptr[2], '\0'}; - for(unsigned i = 0; i < nameCount; ++i) { - if(isoNames[i] == name) { - value = i; - return true; - } + auto skipWhitespace = [&ptr]() -> void { + while(isspace(*ptr)) { + ++ptr; } - return false; }; - if(!matchName(DayofWeek, isoDayNames, 7)) { - return false; // Invalid day of week + skipWhitespace(); + if(!isdigit(*ptr)) { + if(!matchName(ptr, DayofWeek, isoDayNames, 7)) { + return false; // Invalid day of week + } + if(ptr[0] == ',') { + // Accept "Sun,", etc. + ptr += 2; + } else if(strncmp(ptr, "day,", 4) == 0) { + // Accept "Sunday, ", etc + ptr += 5; + } else { + return false; + } } - ptr += first; - + skipWhitespace(); Day = parseNumber(); - if(*ptr == '\0') { + if(*ptr != '-' && !isspace(*ptr)) { return false; } - ptr++; + ++ptr; - // Decode the month name - if(!matchName(Month, isoMonthNames, 12)) { + // Should we check DayOfWeek against calculation from date? + // We could just ignore the DOW... + skipWhitespace(); + if(!matchName(ptr, Month, isoMonthNames, 12)) { return false; // Invalid month } - ptr += 4; // Skip space as well as month + if(*ptr != '-') { + // Skip over any other characters: assume that the full month name has been provided + while(*ptr != '\0' && !isspace(*ptr)) { + ++ptr; + } + if(*ptr == '\0') { + return false; + } + } + ++ptr; + skipWhitespace(); Year = parseNumber(); - if(*ptr == '\0') { + if(*ptr++ != ' ') { return false; } - if(Year < 70) { Year += 2000; } else if(Year < 100) { Year += 1900; } + skipWhitespace(); Hour = parseNumber(); - if(*ptr != ':') { + if(*ptr++ != ':') { return false; } - - ptr++; Minute = parseNumber(); - if(*ptr != ':') { + if(*ptr++ != ':') { return false; } - - ptr++; Second = parseNumber(); + + if(*ptr != '\0' && strcmp(ptr, " GMT") != 0) { + return false; + } + Milliseconds = 0; calcDayOfYear(); return true; } -time_t DateTime::toUnixTime() +bool DateTime::fromISO8601(const String& datetime) +{ + auto ptr = datetime.c_str(); + bool notDigit{false}; + + // Parse and return a decimal number of the given length and update ptr to the first non-numeric character after it + auto parseNumber = [&](unsigned digitCount) -> unsigned { + unsigned value{0}; + while(digitCount--) { + char c = *ptr; + if(!isdigit(c)) { + notDigit = true; + break; + } + value = (value * 10) + c - '0'; + ++ptr; + } + return value; + }; + + auto skip = [&](char c) -> bool { + if(*ptr != c) { + return false; + } + + ++ptr; + return true; + }; + + bool haveTime = skip('T') || ptr[2] == ':'; + + if(haveTime) { + Year = 1970; + Month = dtJanuary; + Day = 1; + } else { + Year = parseNumber(4); + skip('-'); + Month = parseNumber(2) - 1; + skip('-'); + if(*ptr == '\0' || isspace(*ptr)) { + Day = 1; + } else { + Day = parseNumber(2); + } + if(notDigit) { + return false; + } + haveTime = skip('T'); + } + + Hour = parseNumber(2); + skip(':'); + Minute = parseNumber(2); + skip(':'); + Milliseconds = 0; + if(*ptr == '\0') { + Second = 0; + } else { + Second = parseNumber(2); + if(*ptr == '.') { + ++ptr; + Milliseconds = parseNumber(3); + // Discard any microsecond digits + while(isdigit(*ptr)) { + ++ptr; + } + } + } + if(haveTime && notDigit) { + return false; + } + + if(*ptr != '\0') { + return false; + } + + calcDayOfYear(); + + return true; +} + +time_t DateTime::toUnixTime() const { return toUnixTime(Second + (Milliseconds / 1000), Minute, Hour, Day, Month, Year); } -String DateTime::toShortDateString() +String DateTime::toShortDateString() const { return format(_F("%d.%m.%Y")); } -String DateTime::toShortTimeString(bool includeSeconds) +String DateTime::toShortTimeString(bool includeSeconds) const { return format(includeSeconds ? _F("%T") : _F("%r")); } -String DateTime::toFullDateTimeString() +String DateTime::toFullDateTimeString() const { return format(_F("%x %T")); } -String DateTime::toISO8601() +String DateTime::toISO8601() const { return format(_F("%FT%TZ")); } -String DateTime::toHTTPDate() +String DateTime::toHTTPDate() const { return format(_F("%a, %d %b %Y %T GMT")); } @@ -182,88 +330,67 @@ void DateTime::addMilliseconds(long add) void DateTime::fromUnixTime(time_t timep, uint8_t* psec, uint8_t* pmin, uint8_t* phour, uint8_t* pday, uint8_t* pwday, uint8_t* pmonth, uint16_t* pyear) { - // convert the given time_t to time components - // this is a more compact version of the C library localtime function + struct tm t; + gmtime_r(&timep, &t); - unsigned long epoch = timep; - if(psec != nullptr) { - *psec = epoch % 60; + if(psec) { + *psec = t.tm_sec; } - epoch /= 60; // now it is minutes - if(pmin != nullptr) { - *pmin = epoch % 60; + if(pmin) { + *pmin = t.tm_min; } - epoch /= 60; // now it is hours - if(phour != nullptr) { - *phour = epoch % 24; + if(phour) { + *phour = t.tm_hour; } - epoch /= 24; // now it is days - if(pwday != nullptr) { - *pwday = (epoch + 4) % 7; + if(pwday) { + *pwday = t.tm_wday; } - - unsigned year = 70; - unsigned long days = 0; - while((days += (LEAP_YEAR(year) ? 366 : 365)) <= epoch) { - year++; + if(pyear) { + *pyear = t.tm_year + 1900; } - if(pyear != nullptr) { - *pyear = year + 1900; // *pyear is returned as years from 1900 + if(pmonth) { + *pmonth = t.tm_mon; } - - days -= LEAP_YEAR(year) ? 366 : 365; - epoch -= days; // now it is days in this year, starting at 0 - // *pdayofyear=epoch; // days since jan 1 this year - - uint8_t month; - for(month = 0; month < 12; month++) { - uint8_t monthDays = getMonthDays(month, year); - if(epoch >= monthDays) { - epoch -= monthDays; - } else { - break; - } - } - - if(pmonth != nullptr) { - *pmonth = month; // jan is month 0 - } - if(pday != nullptr) { - *pday = epoch + 1; // day of month + if(pday) { + *pday = t.tm_mday; } } -time_t DateTime::toUnixTime(uint8_t sec, uint8_t min, uint8_t hour, uint8_t day, uint8_t month, uint16_t year) +time_t DateTime::toUnixTime(int sec, int min, int hour, int day, uint8_t month, uint16_t year) { - // converts time components to time_t - // note year argument is full four digit year (or digits since 2000), i.e.1975, (year 8 is 2008) - if(year < 69) { year += 2000; } + // seconds from 1970 till 1 jan 00:00:00 this year - time_t seconds = (year - 1970) * (SECS_PER_DAY * 365); + auto seconds = time_t(year - 1970) * (SECS_PER_DAY * 365); // add extra days for leap years - for(unsigned i = 1970; i < year; i++) { - if(LEAP_YEAR(i)) { + for(unsigned y = 1972; y < year; y += 4) { + if(isLeapCentury(y)) { seconds += SECS_PER_DAY; } } + for(unsigned y = 1968; y >= year; y -= 4) { + if(isLeapCentury(y)) { + seconds -= SECS_PER_DAY; + } + } + // add days for this year - for(unsigned i = 0; i < month; i++) { - seconds += SECS_PER_DAY * getMonthDays(i, year); + for(unsigned m = dtJanuary; m < month; ++m) { + seconds += SECS_PER_DAY * getMonthDays(m, year); } - seconds += (day - 1) * SECS_PER_DAY; - seconds += hour * SECS_PER_HOUR; - seconds += min * SECS_PER_MIN; + seconds += time_t(day - 1) * SECS_PER_DAY; + seconds += time_t(hour) * SECS_PER_HOUR; + seconds += time_t(min) * SECS_PER_MIN; seconds += sec; return seconds; } -String DateTime::format(const char* sFormat) +String DateTime::format(const char* sFormat) const { if(sFormat == nullptr) { return nullptr; @@ -299,10 +426,10 @@ String DateTime::format(const char* sFormat) // Month (not implemented: Om) case 'b': // Abbreviated month name, e.g. Oct (always English) case 'h': // Synonym of b - sReturn.concat(CStringArray(flashMonthNames)[Month], 3); + sReturn.concat(CStringArray(localeMonthNames)[Month], 3); break; case 'B': // Full month name, e.g. October (always English) - sReturn += CStringArray(flashMonthNames)[Month]; + sReturn += CStringArray(localeMonthNames)[Month]; break; case 'm': // Month as a decimal number [01..12] sReturn.concat(Month + 1, DEC, 2); @@ -339,10 +466,10 @@ String DateTime::format(const char* sFormat) sReturn += char('0' + DayofWeek); break; case 'a': // Abbreviated weekday name, e.g. Fri - sReturn.concat(CStringArray(flashDayNames)[DayofWeek], 3); + sReturn.concat(CStringArray(localeDayNames)[DayofWeek], 3); break; case 'A': // Full weekday name, e.g. Friday - sReturn += CStringArray(flashDayNames)[DayofWeek]; + sReturn += CStringArray(localeDayNames)[DayofWeek]; break; case 'u': // Weekday as a decimal number, where Monday is 1 (ISO 8601 format) [1..7] sReturn += (DayofWeek == 0) ? '7' : char('0' + DayofWeek); @@ -376,8 +503,12 @@ String DateTime::format(const char* sFormat) case 'R': // Short time (HH:MM) sReturn += format(_F("%H:%M")); break; - case 'T': // ISO 8601 time format (HH:MM:SS) + case 'T': // ISO 8601 time format (HH:MM:SS{.mmm}) sReturn += format(_F("%H:%M:%S")); + if(Milliseconds != 0) { + sReturn += '.'; + sReturn.concat(Milliseconds, DEC, 3); + } break; case 'p': // Meridiem [AM,PM] sReturn += (Hour < 12) ? "AM" : "PM"; @@ -401,16 +532,16 @@ String DateTime::format(const char* sFormat) void DateTime::calcDayOfYear() { DayofYear = 0; - for(auto i = 0; i < Month; ++i) { - switch(i) { - case 8: // Sep - case 3: // Apr - case 5: // Jun - case 10: // Nov + for(unsigned m = dtJanuary; m < Month; ++m) { + switch(m) { + case dtSeptember: + case dtApril: + case dtJune: + case dtNovember: DayofYear += 30; break; - case 1: // Feb - DayofYear += LEAP_YEAR(Year) ? 29 : 28; + case dtFebruary: + DayofYear += isLeapYear(Year) ? 29 : 28; break; default: DayofYear += 31; @@ -419,7 +550,7 @@ void DateTime::calcDayOfYear() DayofYear += Day; } -uint8_t DateTime::calcWeek(uint8_t firstDay) +uint8_t DateTime::calcWeek(uint8_t firstDay) const { int16_t startOfWeek = DayofYear - DayofWeek + firstDay; int8_t firstDayofWeek = startOfWeek % 7; @@ -428,3 +559,36 @@ uint8_t DateTime::calcWeek(uint8_t firstDay) } return (startOfWeek + 7 - firstDayofWeek) / 7; } + +String DateTime::getLocaleDayName(uint8_t day) +{ + return CStringArray(localeDayNames)[day]; +} + +String DateTime::getLocaleMonthName(uint8_t month) +{ + return CStringArray(localeMonthNames)[month]; +} + +String DateTime::getIsoDayName(uint8_t day) +{ + if(day > dtSaturday) { + return nullptr; + } + auto name = isoDayNames[day]; + return String(name); +} + +String DateTime::getIsoMonthName(uint8_t month) +{ + if(month > dtDecember) { + return nullptr; + } + auto name = isoMonthNames[month]; + return String(name); +} + +uint16_t DateTime::getDaysInYear(uint16_t year) +{ + return isLeapYear(year) ? 366 : 365; +} diff --git a/Sming/Core/DateTime.h b/Sming/Core/DateTime.h index d07047f1dc..e267bff0cc 100644 --- a/Sming/Core/DateTime.h +++ b/Sming/Core/DateTime.h @@ -21,7 +21,6 @@ #include "SmingLocale.h" #include -/*==============================================================================*/ /* Useful Constants */ #define SECS_PER_MIN (60UL) #define SECS_PER_HOUR (3600UL) @@ -31,40 +30,90 @@ #define SECS_PER_YEAR (SECS_PER_WEEK * 52L) #define SECS_YR_2000 (946681200UL) -/* Useful Macros for getting elapsed time */ +/** @brief Days of week +*/ +enum dtDays_t { + dtSunday, + dtMonday, + dtTuesday, + dtWednesday, + dtThursday, + dtFriday, + dtSaturday, +}; + +/** @brief Months +*/ +enum dtMonth_t { + dtJanuary, + dtFebruary, + dtMarch, + dtApril, + dtMay, + dtJune, + dtJuly, + dtAugust, + dtSeptember, + dtOctober, + dtNovember, + dtDecember, +}; + +/* Useful functions for getting elapsed time */ + /** Get just seconds part of given Unix time */ -#define numberOfSeconds(_time_) (_time_ % SECS_PER_MIN) +inline constexpr uint8_t numberOfSeconds(time_t time) +{ + return time % SECS_PER_MIN; +} + /** Get just minutes part of given Unix time */ -#define numberOfMinutes(_time_) ((_time_ / SECS_PER_MIN) % SECS_PER_MIN) +inline constexpr uint8_t numberOfMinutes(time_t time) +{ + return (time / SECS_PER_MIN) % SECS_PER_MIN; +} + /** Get just hours part of given Unix time */ -#define numberOfHours(_time_) ((_time_ % SECS_PER_DAY) / SECS_PER_HOUR) +inline constexpr uint8_t numberOfHours(time_t time) +{ + return (time % SECS_PER_DAY) / SECS_PER_HOUR; +} + /** Get day of week from given Unix time */ -#define dayOfWeek(_time_) ((_time_ / SECS_PER_DAY + 4) % DAYS_PER_WEEK) // 0 = Sunday +inline constexpr dtDays_t dayOfWeek(time_t time) +{ + return dtDays_t((time / SECS_PER_DAY + 4) % DAYS_PER_WEEK); +} + /** Get elapsed days since 1970-01-01 from given Unix time */ -#define elapsedDays(_time_) (_time_ / SECS_PER_DAY) // this is number of days since Jan 1 1970 +inline constexpr uint8_t elapsedDays(time_t time) +{ + return time / SECS_PER_DAY; +} + /** Get quantity of seconds since midnight from given Unix time */ -#define elapsedSecsToday(_time_) (_time_ % SECS_PER_DAY) // the number of seconds since last midnight +inline constexpr unsigned elapsedSecsToday(time_t time) +{ + return time % SECS_PER_DAY; +} + /** Get Unix time of midnight at start of day from given Unix time */ -#define previousMidnight(_time_) ((_time_ / SECS_PER_DAY) * SECS_PER_DAY) // time at the start of the given day -/** Get Unix time of midnight at end of day from given just Unix time */ -#define nextMidnight(_time_) (previousMidnight(_time_) + SECS_PER_DAY) // time at the end of the given day -/** Get quantity of seconds since midnight at start of previous Sunday from given Unix time */ -#define elapsedSecsThisWeek(_time_) (elapsedSecsToday(_time_) + (dayOfWeek(_time_) * SECS_PER_DAY)) +inline constexpr time_t previousMidnight(time_t time) +{ + return (time / SECS_PER_DAY) * SECS_PER_DAY; +} -// todo add date math macros -/*============================================================================*/ +/** Get Unix time of midnight at end of day from given just Unix time */ +inline constexpr time_t nextMidnight(time_t time) +{ + return previousMidnight(time) + SECS_PER_DAY; +} -/** @brief Days of week -*/ -enum dtDays_t { - dtSunday, ///< Sunday - dtMonday, ///< Monday - dtTuesday, ///< Tuesday - dtWednesday, ///< Wednesday - dtThursday, ///< Thursday - dtFriday, ///< Friday - dtSaturday ///< Saturday -}; +/** Get quantity of seconds since midnight at start of previous Sunday from given Unix time */ +inline constexpr unsigned elapsedSecsThisWeek(time_t time) +{ + return elapsedSecsToday(time) + dayOfWeek(time) * SECS_PER_DAY; +} /** @brief Date and time class * @@ -73,8 +122,15 @@ enum dtDays_t { * This means that timespan calculation and free-running clocks may be inaccurate if they span leap seconds. * To facilitate leap seconds, reference must be made to leap second table. * This will not be done within the Sming framework and must be handled by application code if required. + * @see https://www.eecis.udel.edu/~mills/leap.html + * + * @note time_t is a signed 64-bit value for all architectures **except** Windows Host and esp32 ESP-IDF 4.x. + * + * 32-bit signed values support a range of +/-68 years; the Unix epoch is midnight 1 Jan 1970, so overflows at about 3am on 19 Jan 2038. + * The value is signed which allows dates prior to 1970 to be represented. * - * @note Sming uses 32-bit signed integer for its time_t data type which supports a range of +/-68 years. This means Sming is susceptible to Year 2038 problem. + * An unsigned 32-bit value can be used to store dates provided they are after 1970. + * These are good until February 2106. */ class DateTime { @@ -134,42 +190,65 @@ class DateTime */ bool fromHttpDate(const String& httpDate); + /** @brief Parse an ISO8601 date/time string + * @param datetime Date and optional time in ISO8601 format, e.g. "1994-11-06", "1994-11-06T08:49:37". Separators are optional. + * @retval bool True on success + * @see See https://en.wikipedia.org/wiki/ISO_8601 + * + * `Basic format` doesn't include separators, whereas `Extended format` does. + * + * Acceptable date formats: + * + * YYYY-MM-DD or YYYYMMDD + * YYYY-MM (but not YYYYMM) + * + * Acceptable time formats: + * + * Thh:mm:ss.sss or Thhmmss.sss + * Thh:mm:ss or Thhmmss + * Thh:mm.mmm or Thhmm.mmm + * Thh:mm or Thhmm + * Thh.hhh + * Thh + */ + bool fromISO8601(const String& datetime); + /** @brief Check if time date object is initialised * @retval True if object has no value. False if initialised. */ - bool isNull(); + bool isNull() const; /** @brief Get Unix time * @retval time_t Unix time, quantity of seconds since 00:00:00 1970-01-01 - * @note Unix time does not account for leap seconds. To convert Unix time to UTC requires reference to a leap second table. + * @note Unix time does not account for leap seconds. */ - time_t toUnixTime(); + time_t toUnixTime() const; /** @brief Get human readable date * @retval String Date in requested format, e.g. DD.MM.YYYY */ - String toShortDateString(); + String toShortDateString() const; /** @brief Get human readable time * @param includeSeconds True to include seconds (Default: false) * @retval String Time in format hh:mm or hh:mm:ss */ - String toShortTimeString(bool includeSeconds = false); + String toShortTimeString(bool includeSeconds = false) const; /** @brief Get human readable date and time * @retval String Date and time in format DD.MM.YYYY hh:mm:ss */ - String toFullDateTimeString(); + String toFullDateTimeString() const; /** @brief Get human readable date and time * @retval String Date and time in format YYYY-MM-DDThh:mm:ssZ */ - String toISO8601(); + String toISO8601() const; /** @brief Get human readable date and time * @retval String Date and time in format DDD, DD MMM YYYY hh:mm:ss GMT */ - String toHTTPDate(); + String toHTTPDate() const; /** @brief Add time to date time object * @param add Quantity of milliseconds to add to object @@ -190,7 +269,7 @@ class DateTime * @note Pass the Unix timedate value and pointers to existing integers. The integers are updated with the converted values * @note This static function may be used without instantiating a DateTime object, e.g. DateTime::convertFromUnixTime(...); * @note 32-bit Unix time has year 2036 issue. - * @note Unix time does not account for leap seconds. To convert Unix time to UTC requires reference to a leap second table. + * @note Unix time does not account for leap seconds. * @note All of the return values are optional, specify nullptr if not required */ static void fromUnixTime(time_t timep, uint8_t* psec, uint8_t* pmin, uint8_t* phour, uint8_t* pday, uint8_t* pwday, @@ -203,14 +282,14 @@ class DateTime * @param min Minutes * @param hour Hours * @param day Days - * @param month Month (0-11, Jan=0, Feb=1, ...Dec=11) - * @param year Year (1901-2036), either full 4 digit year or 2 digits for 1970-2036 + * @param month Month (0-11, Jan=0, Feb=1, ...Dec=11), or enum (dtJanuary, ...) + * @param year Year, either full 4 digit year or 2 digits for 2000-2068 + * @retval time_t Number of seconds since unix epoch (Midnight, Jan 1 1970) * @note Seconds, minutes, hours and days may be any value, e.g. to calculate the value for 300 days since 1970 (epoch), set day=300 - * @note This static function may be used without instantiating a DateTime object, e.g. time_t unixTime = DateTime::convertToUnixTime(...); - * @note 32-bit Unix time is valid between 1901-12-13 and 03:14:07 2038-01-19 - * @note Unix time does not account for leap seconds. To convert Unix time to UTC requires reference to a leap second table. + * @note This static function may be used without instantiating a DateTime object, e.g. `time_t unixTime = DateTime::toUnixTime(...);` + * @note Unix time does not account for leap seconds. */ - static time_t toUnixTime(uint8_t sec, uint8_t min, uint8_t hour, uint8_t day, uint8_t month, uint16_t year); + static time_t toUnixTime(int sec, int min, int hour, int day, uint8_t month, uint16_t year); /** @brief Create string formatted with time and date placeholders * @param formatString String including date and time formatting @@ -255,21 +334,30 @@ class DateTime * | %%Y | Year as a decimal number (range 1970 to ...) | | * | %% | Percent sign | | */ - String format(const char* formatString); + String format(const char* formatString) const; /** @brief Create string formatted with time and date placeholders * @param formatString String including date and time formatting * @retval String Formatted string * @note see format(const char*) for parameter details */ - String format(const String& formatString) + String format(const String& formatString) const { return format(formatString.c_str()); } + static bool isLeapYear(uint16_t year); + static uint8_t getMonthDays(uint8_t month, uint16_t year); + static String getLocaleDayName(uint8_t day); + static String getLocaleMonthName(uint8_t month); + static String getIsoDayName(uint8_t day); + static String getIsoMonthName(uint8_t month); + static uint16_t getDaysInYear(uint16_t year); + private: - void calcDayOfYear(); // Helper function calculates day of year - uint8_t calcWeek(uint8_t firstDay); //Helper function calculates week number based on firstDay of week + // Helper methods + void calcDayOfYear(); // Calculate day of year + uint8_t calcWeek(uint8_t firstDay) const; // Calculate week number based on firstDay of week public: uint8_t Hour = 0; ///< Hour (0-23) diff --git a/docs/source/upgrading/5.1-5.2.rst b/docs/source/upgrading/5.1-5.2.rst index 0dcbfa40f3..e06142934a 100644 --- a/docs/source/upgrading/5.1-5.2.rst +++ b/docs/source/upgrading/5.1-5.2.rst @@ -3,7 +3,7 @@ From v5.1 to v5.2 .. highlight:: c++ -**Breaking change** +**PartitionStream: Breaking change** The :cpp:class:`Storage::PartitionStream` constructors with ``blockErase`` parameter have been deprecated. The intended default behaviour is read-only, however previously this also allowed writing without block erase. @@ -11,3 +11,33 @@ This can result in corrupted flash contents where the flash has not been explici The new constructors instead use a :cpp:enum:`Storage::Mode` so behaviour is more explicit. The default is read-only and writes will now be failed. + + +**64-bit time_t** + +There is some variability in whether `time_t` is 32 or 64 bits. See issue #2758. + +This is dependent on the toolchain and accompanying C library. + +32-bits: + +- Esp32 IDF 4.x +- Windows Host (using mingw) +- Linux host builds prior to Sming v5.2 + +Range of time_t: + + -0x80000000: 1901-12-13 20:45:52 + 0x00000000: 1970-01-01 00:00:00 + 0x7fffffff: 2038-01-19 03:14:07 + +All others use 64-bit values. + +For reference, C library source code can be found here https://sourceware.org/git/?p=newlib-cygwin.git;a=blob;f=newlib/libc/ + +Rp2040 builds with standard ARM toolkit so probably accommodated by the standard repo. + +Espressif toolchains use forks: + +esp8266: https://github.com/earlephilhower/newlib-xtensa/blob/xtensa-4_0_0-lock-arduino/newlib/libc/ +esp32: https://github.com/espressif/newlib-esp32/blob/esp-4.3.0/newlib/libc/ diff --git a/samples/Basic_DateTime/app/application.cpp b/samples/Basic_DateTime/app/application.cpp index 2de8e3968d..a36ff1360f 100644 --- a/samples/Basic_DateTime/app/application.cpp +++ b/samples/Basic_DateTime/app/application.cpp @@ -11,7 +11,7 @@ namespace { DEFINE_FSTR_LOCAL(commandPrompt, "Enter Unix timestamp: "); -void showTime(DateTime dt) +void showTime(const DateTime& dt) { auto printFormat = [&dt](const String& fmt, const String& msg) -> void { Serial << fmt << ' ' << msg << ": " << dt.format(fmt) << endl; @@ -85,10 +85,9 @@ void onRx(Stream& source, char arrivedChar, unsigned short availableCharsCount) String s(buffer); buffer.clear(); char* p; - time_t timestamp = strtoul(s.c_str(), &p, 0); + time_t timestamp = strtoll(s.c_str(), &p, 0); if(p == s.end()) { - Serial << endl - << _F("****Showing DateTime formatting options for Unix timestamp: ") << uint32_t(timestamp) << endl; + Serial << endl << _F("****Showing DateTime formatting options for Unix timestamp: ") << timestamp << endl; showTime(timestamp); break; } @@ -100,7 +99,13 @@ void onRx(Stream& source, char arrivedChar, unsigned short availableCharsCount) break; } - Serial << endl << _F("Please enter a valid numeric timestamp, or HTTP time string!") << endl; + if(dt.fromISO8601(s)) { + Serial << endl << _F("****Showing DateTime formatting options for ISO8601 date/time: ") << s << endl; + showTime(dt); + break; + } + + Serial << endl << _F("Please enter a valid numeric timestamp, HTTP or ISO8601 date/time string!") << endl; break; } diff --git a/tests/HostTests/README.rst b/tests/HostTests/README.rst new file mode 100644 index 0000000000..e8f0799bc0 --- /dev/null +++ b/tests/HostTests/README.rst @@ -0,0 +1,13 @@ +HostTests +========= + +Modular tests which must build and run on all architectures. + +DateTime +-------- + +Test data in ``include/DateTimeData.h`` is generated using python script ``tools/datetime-test.py``. + +If this file is changed, re-generate by running:: + + tools/datetime-test.py include/DateTimeData.h diff --git a/tests/HostTests/include/DateTimeData.h b/tests/HostTests/include/DateTimeData.h new file mode 100644 index 0000000000..7f1a2d03ce --- /dev/null +++ b/tests/HostTests/include/DateTimeData.h @@ -0,0 +1,76 @@ + +/* + * DateTime test data + * + * File generated Thu Apr 11 14:30:36 2024 + * + */ + +// clang-format off + +namespace +{ +struct TestDate { + const FlashString* stringToParse; + const FlashString* expectedString; + int64_t unixTimestamp; + uint16_t milliseconds; +}; + +#define VALID_HTTP_DATE_MAP(XX) \ + XX(DT_1, "Sun, 06 Nov 1994 08:49:37 GMT", "Sun, 06 Nov 1994 08:49:37 GMT", 0x2ebc98a1LL, 0) \ + XX(DT_2, "Sunday, 06 Nov 1994 08:49:37 GMT", "Sun, 06 Nov 1994 08:49:37 GMT", 0x2ebc98a1LL, 0) \ + XX(DT_3, "Sunday, 06-Nov-94 08:49:37 GMT", "Sun, 06 Nov 1994 08:49:37 GMT", 0x2ebc98a1LL, 0) \ + XX(DT_4, "Mon, 07 November 1994 00:00:00", "Mon, 07 Nov 1994 00:00:00 GMT", 0x2ebd6e00LL, 0) \ + XX(DT_5, " 1 JAN\t \r\n \t2000 23:59:59", "Sat, 01 Jan 2000 23:59:59 GMT", 0x386e94ffLL, 0) + +#define VALID_ISO_DATETIME_MAP(XX) \ + XX(DT_6, "2024-01-31", "2024-01-31T00:00:00Z", 0x65b98d80LL, 0) \ + XX(DT_7, "20240131", "2024-01-31T00:00:00Z", 0x65b98d80LL, 0) \ + XX(DT_8, "2024-01", "2024-01-01T00:00:00Z", 0x65920080LL, 0) \ + XX(DT_9, "1901-12-13T20:45:53", "1901-12-13T20:45:53Z", -0x7fffffffLL, 0) \ + XX(DT_10, "1901-12-13T20:45:52", "1901-12-13T20:45:52Z", -0x80000000LL, 0) \ + XX(DT_11, "1970-01-01T00:00:00", "1970-01-01T00:00:00Z", 0x0LL, 0) \ + XX(DT_12, "2038-01-19T03:14:07", "2038-01-19T03:14:07Z", 0x7fffffffLL, 0) \ + XX(DT_13, "1950-01-01", "1950-01-01T00:00:00Z", -0x259e9d80LL, 0) \ + XX(DT_14, "2024-04-08T12:33:17", "2024-04-08T12:33:17Z", 0x6613e40dLL, 0) \ + XX(DT_15, "2024-04-09T12:33:16", "2024-04-09T12:33:16Z", 0x6615358cLL, 0) + +#define VALID_ISO_DATETIME64_MAP(XX) \ + XX(DT_16, "2038-01-19T03:14:08", "2038-01-19T03:14:08Z", 0x80000000LL, 0) \ + XX(DT_17, "2099-12-31T23:59:59", "2099-12-31T23:59:59Z", 0xf48656ffLL, 0) \ + XX(DT_18, "2105-12-31T23:59:59", "2105-12-31T23:59:59Z", 0xffcedd7fLL, 0) \ + XX(DT_19, "2106-02-07T06:28:15", "2106-02-07T06:28:15Z", 0xffffffffLL, 0) \ + XX(DT_20, "2106-02-07T06:28:15", "2106-02-07T06:28:15Z", 0xffffffffLL, 0) \ + XX(DT_21, "2106-02-07T06:28:16", "2106-02-07T06:28:16Z", 0x100000000LL, 0) \ + XX(DT_22, "1901-12-13T20:45:52", "1901-12-13T20:45:52Z", -0x80000000LL, 0) + +#define VALID_ISO_TIME_MAP(XX) \ + XX(DT_23, "T18:47:12.123", "18:47:12.123", 0x10830LL, 123) \ + XX(DT_24, "T18:47:12.123456", "18:47:12.123", 0x10830LL, 123) \ + XX(DT_25, "T12:34:12", "12:34:12", 0xb0c4LL, 0) \ + XX(DT_26, "T23:59:59", "23:59:59", 0x1517fLL, 0) \ + XX(DT_27, "12:34", "12:34:00", 0xb0b8LL, 0) \ + XX(DT_28, "T2359", "23:59:00", 0x15144LL, 0) + + +#define XX(tag, str, str2, ...) \ + DEFINE_FSTR_LOCAL(STR1_##tag, str) \ + DEFINE_FSTR_LOCAL(STR2_##tag, str2) + VALID_HTTP_DATE_MAP(XX) + VALID_ISO_DATETIME_MAP(XX) + VALID_ISO_DATETIME64_MAP(XX) + VALID_ISO_TIME_MAP(XX) + +#undef XX + +#define XX(tag, str, str2, ...) {&STR1_##tag, &STR2_##tag, ##__VA_ARGS__}, +DEFINE_FSTR_ARRAY(VALID_HTTP_DATE, TestDate, VALID_HTTP_DATE_MAP(XX)) +DEFINE_FSTR_ARRAY(VALID_ISO_DATETIME, TestDate, VALID_ISO_DATETIME_MAP(XX)) +DEFINE_FSTR_ARRAY(VALID_ISO_DATETIME64, TestDate, VALID_ISO_DATETIME64_MAP(XX)) +DEFINE_FSTR_ARRAY(VALID_ISO_TIME, TestDate, VALID_ISO_TIME_MAP(XX)) + +#undef XX + +} // namespace + diff --git a/tests/HostTests/modules/DateTime.cpp b/tests/HostTests/modules/DateTime.cpp index e34c3b0af1..738adf564a 100644 --- a/tests/HostTests/modules/DateTime.cpp +++ b/tests/HostTests/modules/DateTime.cpp @@ -1,6 +1,8 @@ #include - #include +#include + +#include "DateTimeData.h" class DateTimeTest : public TestGroup { @@ -11,46 +13,168 @@ class DateTimeTest : public TestGroup void execute() override { - DEFINE_FSTR_LOCAL(testDateString, "Sun, 06 Nov 1994 08:49:37 GMT"); - constexpr time_t testDate = 784111777; - DateTime dt; + Serial << _F("time_t is ") << sizeof(time_t) * 8 << _F(" bits") << endl; TEST_CASE("fromHttpDate()") { - REQUIRE(dt.fromHttpDate(testDateString) == true); + checkHttpDates(VALID_HTTP_DATE); + } + + TEST_CASE("fromISO8601 (32-bit)") + { + checkIsoTimes(VALID_ISO_DATETIME, false); } - TEST_CASE("toUnixTime()") + TEST_CASE("fromISO8601 (time only)") { - debug_i("parseHttpDate(\"%s\") produced \"%s\"", String(testDateString).c_str(), - dt.toFullDateTimeString().c_str()); - time_t unixTime = dt.toUnixTime(); - REQUIRE(unixTime == testDate); + checkIsoTimes(VALID_ISO_TIME, true); } - dt = testDate; - debug_d("\"%s\"", dt.toFullDateTimeString().c_str()); + if(sizeof(time_t) == 8) { + TEST_CASE("fromISO8601 (64-bit)") + { + checkIsoTimes(VALID_ISO_DATETIME64, false); + } + } + + // dt = testDate; + // debug_d("\"%s\"", dt.toFullDateTimeString().c_str()); TEST_CASE("setTime") { - checkSetTime(59, 59, 23, 14, 2, 2019); - checkSetTime(13, 1, 1, 1, 1, 1970); + checkSetTime(VALID_HTTP_DATE); + checkSetTime(VALID_ISO_DATETIME); + if(sizeof(time_t) == 8) { + checkSetTime(VALID_ISO_DATETIME64); + } + } + + TEST_CASE("setTime speed check") + { + OneShotFastUs timer; + unsigned count = checkSetTime(VALID_HTTP_DATE); + count += checkSetTime(VALID_ISO_DATETIME); + auto elapsed = timer.elapsedTime(); + Serial << "Checked " << count << " dates in " << elapsed.toString() << ", " << elapsed / count + << " per date" << endl; + } + + TEST_CASE("getMonthDays") + { + for(auto year : {1980, 1981}) { + unsigned yearDays{0}; + for(unsigned month = dtJanuary; month <= dtDecember; ++month) { + auto days = DateTime::getMonthDays(month, year); + yearDays += days; + Serial << DateTime::getIsoMonthName(month) << ' ' << year << " : " << days << endl; + } + REQUIRE_EQ(yearDays, DateTime::getDaysInYear(year)); + } + } + + TEST_CASE("getDayName") + { + for(unsigned day = dtSunday; day <= dtSaturday; ++day) { + Serial << day << ": " << DateTime::getIsoDayName(day) << ", " << DateTime::getLocaleDayName(day) + << endl; + } + } + + TEST_CASE("getMonthName") + { + for(unsigned month = dtJanuary; month <= dtDecember; ++month) { + Serial << month << ": " << DateTime::getIsoMonthName(month) << ", " + << DateTime::getLocaleMonthName(month) << endl; + } } } - void checkSetTime(uint8_t sec, uint8_t min, uint8_t hour, uint8_t day, uint8_t month, uint16_t year) + void checkHttpDates(const FSTR::Array& dates) { - DateTime dt; - dt.setTime(sec, min, hour, day, month, year); - debug_i("Checking '%s'", dt.toFullDateTimeString().c_str()); - time_t unixTime = dt.toUnixTime(); - dt.setTime(unixTime); - REQUIRE(dt.Second == sec); - REQUIRE(dt.Minute == min); - REQUIRE(dt.Hour == hour); - REQUIRE(dt.Day == day); - REQUIRE(dt.Month == month); - REQUIRE(dt.Year == year); + for(auto date : VALID_HTTP_DATE) { + DateTime dt; + String s(*date.stringToParse); + Serial << s << endl; + REQUIRE(dt.fromHttpDate(s)); + REQUIRE_EQ(date.unixTimestamp, dt.toUnixTime()); + + dt.setTime(date.unixTimestamp); + REQUIRE_EQ(date.unixTimestamp, dt.toUnixTime()); + + REQUIRE_EQ(String(*date.expectedString), dt.toHTTPDate()); + Serial.println(); + } + } + + void checkIsoTimes(const FSTR::Array& dates, bool timeOnly) + { + for(auto date : dates) { + DateTime dt; + String s(*date.stringToParse); + Serial << s << ", " << String(time_t(date.unixTimestamp), HEX) << endl; + REQUIRE(dt.fromISO8601(s)); + Serial << dt.toISO8601() << endl; + REQUIRE_EQ(date.unixTimestamp, dt.toUnixTime()); + REQUIRE_EQ(date.milliseconds, dt.Milliseconds); + + if(timeOnly) { + REQUIRE_EQ(String(*date.expectedString), dt.format("%T")); + } else { + REQUIRE_EQ(String(*date.expectedString), dt.toISO8601()); + } + + dt.setTime(date.unixTimestamp); + REQUIRE_EQ(date.unixTimestamp, dt.toUnixTime()); + + Serial.println(); + } + } + + // Return number of dates checked + unsigned checkSetTime(const FSTR::Array& dates, bool silent = false) + { + unsigned checkCount{0}; + + for(auto date : dates) { + const DateTime refDate(date.unixTimestamp); + + if(!silent) { + Serial << "RefDate " << refDate.toFullDateTimeString() << endl; + } + + auto check = [&](int secOffset, int minOffset, int hourOffset, int dayOffset) { + ++checkCount; + const int sec = secOffset + refDate.Second; + const int min = minOffset + refDate.Minute; + const int hour = hourOffset + refDate.Hour; + const int day = dayOffset + refDate.Day; + DateTime dt = DateTime ::toUnixTime(sec, min, hour, day, refDate.Month, refDate.Year); + time_t refTime = DateTime::toUnixTime(0, 0, 0, 1, refDate.Month, refDate.Year); + refTime += sec; + refTime += int64_t(min) * 60; + refTime += int64_t(hour) * 60 * 60; + refTime += int64_t(day - 1) * 24 * 60 * 60; + time_t calcTime = dt.toUnixTime(); + if(calcTime == refTime) { + return; + } + + char buf[100]; + m_snprintf(buf, sizeof(buf), " (%ds, %dm %dh, %dd)", secOffset, minOffset, hourOffset, dayOffset); + Serial << _F("Check ") << DateTime(refTime).toFullDateTimeString() << buf << endl; + Serial << _F("Got ") << dt.toFullDateTimeString() << ", diff " << refTime - calcTime << endl; + REQUIRE_EQ(refTime, calcTime); + }; + + for(int offset = -10000; offset < 10000; offset += 555) { + check(offset, 0, 0, 0); + check(0, offset, 0, 0); + check(0, 0, offset, 0); + check(0, 0, 0, offset); + } + }; + + return checkCount; } }; diff --git a/tests/HostTests/tools/datetime-test.py b/tests/HostTests/tools/datetime-test.py new file mode 100644 index 0000000000..77c0af69c3 --- /dev/null +++ b/tests/HostTests/tools/datetime-test.py @@ -0,0 +1,211 @@ +#!/usr/bin/python3 +# +# Generate test data for DateTime module: +# +# python datetime-test.py include/DateTimeData.h +# +# + +import os +import sys +from dataclasses import dataclass +import datetime +import email.utils + +""" +ISO datetimes can be strings in short form (i.e. no separators) or extended form (with separators) +For testing unix timestamp values give that and we'll translate it accordingly. + +Python doesn't accept all formats specified by standard +In these cases, specify a tuple (str, python_str) + +There are some gotchas with Python version: + +- short-form unsupported before 3.11 +- Windows craps itself when given negative timestamps + +""" + +def check_compatibility(): + version = 100 * sys.version_info.major + sys.version_info.minor + if version < 311: + raise DeprecationWarning("Require at least python 3.11") + if sys.platform == 'win32': + raise DeprecationWarning("Windows doesn't support negative unix timestamps!") + + +@dataclass +class Record: + string: str + string2: str + timestamp: int + milliseconds: int + + +def parse_http_date(value: str) -> Record: + dt = email.utils.parsedate_to_datetime(value) + if dt.tzinfo is None: + dt = dt.replace(tzinfo=datetime.timezone.utc) + return Record(value, email.utils.format_datetime(dt, usegmt=True), dt.timestamp(), 0) + + +def parse_iso_datetime(value: str | tuple | int) -> Record: + if isinstance(value, int): + dt = datetime.datetime.fromtimestamp(value, tz=datetime.timezone.utc) + s = dt.isoformat().removesuffix('+00:00') + elif isinstance(value, tuple): + s = value[0] + dt = datetime.datetime.fromisoformat(value[1]) + else: + s = value + dt = datetime.datetime.fromisoformat(value) + if dt.tzinfo is None: + dt = dt.replace(tzinfo=datetime.timezone.utc) + iso_string = dt.isoformat().removesuffix('+00:00') + 'Z' + return Record(s, iso_string, dt.timestamp(), dt.microsecond // 1000) + + +def parse_iso_time(value: str) -> Record: + t = datetime.time.fromisoformat(value) + o = t.second + (t.minute * 60) + (t.hour * 60 * 60) + if t.microsecond: + iso_string = t.isoformat(timespec='milliseconds') + else: + iso_string = t.isoformat() + return Record(value, iso_string, o, t.microsecond // 1000) + + +""" +Consider these as appropriate for HTTP and ISO date strings. + +valid dates must include: + all days + min/max day numbers + all month names + min/max month numbers + selection of years from min to max + +invalid dates must include: + bad day numbers (month-specific) + leap years + bad month numbers + bad month names + bad day names + +times + 24-hour only + hours:mins + hours:mins:secs + hours:mins:secs.milliseconds + +After basic validation can use DateTime to generate strings and then verify they can be decoded back again. +""" + +MAP_LIST = { + 'VALID_HTTP_DATE': ( + parse_http_date, + [ + "Sun, 06 Nov 1994 08:49:37 GMT", + "Sunday, 06 Nov 1994 08:49:37 GMT", + "Sunday, 06-Nov-94 08:49:37 GMT", + "Mon, 07 November 1994 00:00:00", + " 1 JAN\t \r\n 2000 23:59:59", + ]), + 'VALID_ISO_DATETIME': ( + parse_iso_datetime, + [ + "2024-01-31", + "20240131", + ("2024-01", "2024-01-01"), + -0x7fffffff, + -0x80000000, + 0, + 0x7fffffff, + "1950-01-01", + 0x6613e40d, + 0x6615358c, + ]), + 'VALID_ISO_DATETIME64': ( + parse_iso_datetime, + [ + 0x80000000, + "2099-12-31T23:59:59", + "2105-12-31T23:59:59", + "2106-02-07T06:28:15", + "2106-02-07T06:28:15", + 0x100000000, + -0x80000000, + ]), + 'VALID_ISO_TIME': ( + parse_iso_time, + [ + "T18:47:12.123", + "T18:47:12.123456", + "T12:34:12", + "T23:59:59", + "12:34", + "T2359", + ]), +} + + +def main(): + check_compatibility() + + output = f""" +/* + * DateTime test data + * + * File generated {datetime.datetime.now().ctime()} + * + */ + +// clang-format off + +namespace +{{ +struct TestDate {{ + const FlashString* stringToParse; + const FlashString* expectedString; + int64_t unixTimestamp; + uint16_t milliseconds; +}}; + +""" + + index = 1 + for name, (parse, data) in MAP_LIST.items(): + records = [f'#define {name}_MAP(XX)'] + for value in data: + r = parse(value) + s = r.string.encode('unicode_escape').decode() + records += [f'XX(DT_{index}, "{s}", "{r.string2}", {hex(int(r.timestamp))}LL, {r.milliseconds})'] + index += 1 + output += " \\\n ".join(records) + output += "\n\n" + + output += f""" +#define XX(tag, str, str2, ...) \\ + DEFINE_FSTR_LOCAL(STR1_##tag, str) \\ + DEFINE_FSTR_LOCAL(STR2_##tag, str2) +{"".join(f' {name}_MAP(XX)\n' for name in MAP_LIST)} +#undef XX + +#define XX(tag, str, str2, ...) {{&STR1_##tag, &STR2_##tag, ##__VA_ARGS__}}, +{"".join(f'DEFINE_FSTR_ARRAY({name}, TestDate, {name}_MAP(XX))\n' for name in MAP_LIST)} +#undef XX + +}} // namespace +""" + + if len(sys.argv) > 1: + filename = sys.argv[1] + print('Creating file', repr(filename)) + with open(filename, 'w') as f: + print(output, file=f) + else: + print(output) + + +if __name__ == '__main__': + main() From 895535ea3bf60af38781ac2890a24371dd0b76a8 Mon Sep 17 00:00:00 2001 From: Mike Date: Mon, 15 Apr 2024 08:41:35 +0100 Subject: [PATCH 048/128] esp8266: NMI not disabled when timer1 ISR cleared (#2764) SDK doesn't actually disable NMI interrupts when interrupt callback cleared. This blocks regular (FRC) timer1 interrupts if subsequently enabled. The HardwareTimer (timer1) appears to stop working if applications switch from using NMI interrupts to regular timer interrupts. A more subtle manifestation of this bug is that the original NMI callback continues to be called when a different FRC handler is set. --- .../Esp8266/Components/driver/hw_timer.cpp | 30 +++++++++++++++++++ .../driver/include/driver/hw_timer.h | 7 +---- .../esp8266/include/espinc/eagle_soc.h | 1 + tests/HostTests/modules/Timers.cpp | 15 ---------- 4 files changed, 32 insertions(+), 21 deletions(-) diff --git a/Sming/Arch/Esp8266/Components/driver/hw_timer.cpp b/Sming/Arch/Esp8266/Components/driver/hw_timer.cpp index b7a8641980..3d15c4525a 100644 --- a/Sming/Arch/Esp8266/Components/driver/hw_timer.cpp +++ b/Sming/Arch/Esp8266/Components/driver/hw_timer.cpp @@ -36,6 +36,28 @@ static void IRAM_ATTR nmi_handler() nmi_callback.func(nmi_callback.arg); } +/* + * The `ETS_FRC_TIMER1_NMI_INTR_ATTACH` macro calls SDK `NmiTimSetFunc` which + * doesn't actually disable NMI (a known bug). + * If we subsequently enable FRC interrupts, the timer won't work so we need to properly + * disable NMI manually. + * + * The NmiTimSetFunc code looks like this: + * + * uint32_t value = REG_READ(NMI_INT_ENABLE_REG); + * value &= ~0x1f; + * value |= 0x0f; + * REG_WRITE(NMI_INT_ENABLE_REG, value); + * + * Note that there is no published documentation for this register. + * Clearing it to zero appears to work but may have unintended side-effects. + */ +static void IRAM_ATTR hw_timer1_disable_nmi() +{ + auto value = REG_READ(NMI_INT_ENABLE_REG); + REG_WRITE(NMI_INT_ENABLE_REG, value & ~0x1f); +} + void hw_timer1_attach_interrupt(hw_timer_source_type_t source_type, hw_timer_callback_t callback, void* arg) { if(source_type == TIMER_NMI_SOURCE) { @@ -47,10 +69,18 @@ void hw_timer1_attach_interrupt(hw_timer_source_type_t source_type, hw_timer_cal ETS_FRC_TIMER1_NMI_INTR_ATTACH(nmi_handler); } } else { + hw_timer1_disable_nmi(); ETS_FRC_TIMER1_INTR_ATTACH(callback, arg); } } +void IRAM_ATTR hw_timer1_detach_interrupt(void) +{ + hw_timer1_disable(); + hw_timer1_disable_nmi(); + ETS_FRC_TIMER1_INTR_ATTACH(NULL, NULL); +} + void hw_timer_init(void) { constexpr uint32_t FRC2_ENABLE_TIMER = BIT7; diff --git a/Sming/Arch/Esp8266/Components/driver/include/driver/hw_timer.h b/Sming/Arch/Esp8266/Components/driver/include/driver/hw_timer.h index 1d7a7459f7..04395c83c7 100644 --- a/Sming/Arch/Esp8266/Components/driver/include/driver/hw_timer.h +++ b/Sming/Arch/Esp8266/Components/driver/include/driver/hw_timer.h @@ -123,12 +123,7 @@ __forceinline void IRAM_ATTR hw_timer1_disable(void) /** * @brief Detach interrupt from the timer */ -__forceinline void IRAM_ATTR hw_timer1_detach_interrupt(void) -{ - hw_timer1_disable(); - ETS_FRC_TIMER1_NMI_INTR_ATTACH(NULL); - ETS_FRC_TIMER1_INTR_ATTACH(NULL, NULL); -} +void hw_timer1_detach_interrupt(void); /** * @brief Get timer1 count diff --git a/Sming/Arch/Esp8266/Components/esp8266/include/espinc/eagle_soc.h b/Sming/Arch/Esp8266/Components/esp8266/include/espinc/eagle_soc.h index fcb72fcc97..99244138f1 100644 --- a/Sming/Arch/Esp8266/Components/esp8266/include/espinc/eagle_soc.h +++ b/Sming/Arch/Esp8266/Components/esp8266/include/espinc/eagle_soc.h @@ -126,6 +126,7 @@ //}} //Interrupt remap control registers define{{ +#define NMI_INT_ENABLE_REG (PERIPHS_DPORT_BASEADDR) #define EDGE_INT_ENABLE_REG (PERIPHS_DPORT_BASEADDR + 0x04) #define WDT_EDGE_INT_ENABLE() SET_PERI_REG_MASK(EDGE_INT_ENABLE_REG, BIT0) #define TM1_EDGE_INT_ENABLE() SET_PERI_REG_MASK(EDGE_INT_ENABLE_REG, BIT1) diff --git a/tests/HostTests/modules/Timers.cpp b/tests/HostTests/modules/Timers.cpp index 361627e98d..f03b3ef2a9 100644 --- a/tests/HostTests/modules/Timers.cpp +++ b/tests/HostTests/modules/Timers.cpp @@ -79,9 +79,6 @@ class CallbackTimerTest : public TestGroup statusTimer.stop(); Serial.print("statusTimer stopped: "); Serial.println(statusTimer); - - // Release any allocated delegate memory - timer64.setCallback(TimerDelegate(nullptr)); done = true; } @@ -113,17 +110,6 @@ class CallbackTimerTest : public TestGroup Serial.println(statusTimer); - { - auto tmp = new Timer; - tmp->initializeMs<1200>( - [](void* arg) { - auto self = static_cast(arg); - Serial << self->timer64 << _F(" fired") << endl; - }, - this); - tmp->startOnce(); - } - if(1) { longTimer.setCallback([this]() { --activeTimerCount; @@ -222,7 +208,6 @@ class CallbackTimerTest : public TestGroup private: Timer statusTimer; unsigned statusTimerCount = 0; - Timer timer64; HardwareTimerTest timer1; Timer longTimer; uint32_t longStartTicks = 0; From bb5074ab0a763a2da7f818650de9db6e8c80df18 Mon Sep 17 00:00:00 2001 From: Mike Date: Mon, 15 Apr 2024 08:42:31 +0100 Subject: [PATCH 049/128] Fix building `lwip2` with `ENABLE_LWIPDEBUG=1` (#2754) --- Sming/Arch/Esp8266/Components/lwip2/lwip2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sming/Arch/Esp8266/Components/lwip2/lwip2 b/Sming/Arch/Esp8266/Components/lwip2/lwip2 index cfe476dfa2..2e85a7ad86 160000 --- a/Sming/Arch/Esp8266/Components/lwip2/lwip2 +++ b/Sming/Arch/Esp8266/Components/lwip2/lwip2 @@ -1 +1 @@ -Subproject commit cfe476dfa2793025660b8698d993258f4a598f8d +Subproject commit 2e85a7ad86b1af23d4b092bd5484406a644a6bcf From cf1e532d31688107353067d95a990e133c71ab0b Mon Sep 17 00:00:00 2001 From: Mike Date: Mon, 15 Apr 2024 08:43:58 +0100 Subject: [PATCH 050/128] Fixes to hardware config editing/display (#2765) **If name not specified when loading config then 'save' raises exception** For example, `Basic_Ota` sample doesn't have its `name` field set in `ota.hw`: - Run `make hwconfig-edit` - Click 'save' So ensure we provide a default name based on filename. **Ensure all sample .hw files have names** For consistency. **Remove 'unused_before' and 'unused_after' fields from `make hwconfig` output** These are only used for map building so shouldn't be visible. Example: ``` "phy_init": { "device": "spiFlash", "address": "0x000fc000", "size": "4K", "type": "data", "subtype": "phy", "readonly": false, "encrypted": false, "filename": "$(FLASH_INIT_DATA)", "unused_before": 0, ///< "unused_after": 0 ///< Don't show }, ``` **Don't set name, comment fields from option data** For example, `Basic_Blink` sample uses default config. Running `make hwconfig` gives: ``` { "name": "Standard config with single ROM", "comment": "Should work with any Esp8266 variant", "arch": "Esp8266", "options": [], ``` If we run `make hwconfig HWCONFIG_OPTS=4m` we get this: ``` { "name": "", "comment": "", "arch": "Esp8266", "options": [ "4m" ], ``` The `name` and `comment` have been wiped out, should be as before. **Fix use of `not in` and `is not` operators** e.g. `x not in [...]` preferred to `not x in [...]` --- .../Storage/Tools/hwconfig/common.py | 3 +++ .../Storage/Tools/hwconfig/config.py | 9 ++++---- .../Storage/Tools/hwconfig/editor.py | 23 +++++++++++++------ .../Storage/Tools/hwconfig/partition.py | 4 ++-- samples/Basic_IFS/basic_ifs_Esp32.hw | 1 + samples/Basic_IFS/basic_ifs_Esp8266.hw | 1 + samples/Basic_IFS/basic_ifs_Host.hw | 1 + samples/Basic_IFS/basic_ifs_Rp2040.hw | 4 ++-- samples/Basic_Ota/ota.hw | 1 + samples/FtpServer_Files/ftpserver-esp32.hw | 2 +- samples/FtpServer_Files/ftpserver-esp8266.hw | 2 +- 11 files changed, 34 insertions(+), 17 deletions(-) diff --git a/Sming/Components/Storage/Tools/hwconfig/common.py b/Sming/Components/Storage/Tools/hwconfig/common.py index d0b30932d8..760a415aa5 100644 --- a/Sming/Components/Storage/Tools/hwconfig/common.py +++ b/Sming/Components/Storage/Tools/hwconfig/common.py @@ -109,6 +109,9 @@ def json_save(data, filename): def to_json(obj): return json.dumps(obj, indent=4) +def get_basename_no_ext(filename): + basename = os.path.basename(filename) + return os.path.splitext(basename)[0] def lookup_keyword(t, keywords): for k, v in keywords.items(): diff --git a/Sming/Components/Storage/Tools/hwconfig/config.py b/Sming/Components/Storage/Tools/hwconfig/config.py index fb5ebe9277..04e6df8d4d 100644 --- a/Sming/Components/Storage/Tools/hwconfig/config.py +++ b/Sming/Components/Storage/Tools/hwconfig/config.py @@ -87,6 +87,8 @@ def __init__(self): self.options = [] self.option_library = load_option_library() self.base_config = None + self.name = '' + self.comment = '' def __str__(self): return "'%s' for %s" % (self.name, self.arch) @@ -119,6 +121,8 @@ def load(self, name): self.depends.append(filename) data = json_load(filename) self.parse_dict(data) + self.name = data.get('name', '') + self.comment = data.get('comment', '') def parse_options(self, options): """Apply any specified options. @@ -164,10 +168,7 @@ def parse_dict(self, data): elif k != 'name' and k != 'comment': raise InputError("Unknown config key '%s'" % k) - self.name = data.get('name', '') - self.comment = data.get('comment', '') - - if not partitions is None: + if partitions: self.partitions.parse_dict(partitions, self.devices) def dict(self): diff --git a/Sming/Components/Storage/Tools/hwconfig/editor.py b/Sming/Components/Storage/Tools/hwconfig/editor.py index 89f0c1a1a5..6eb275b370 100644 --- a/Sming/Components/Storage/Tools/hwconfig/editor.py +++ b/Sming/Components/Storage/Tools/hwconfig/editor.py @@ -39,7 +39,7 @@ def read_property(obj, name): def get_dict_value(dict, key, default): """Read dictionary value, creating one if it doesn't exist.""" - if not key in dict: + if key not in dict: dict[key] = default return dict[key] @@ -370,7 +370,7 @@ def init(self): self.array = {} # dictionary for array element variables self.row = 0 keys = self.schema['properties'].keys() - if not 'name' in keys: + if 'name' not in keys: self.addControl('name') for k in keys: self.addControl(k) @@ -1150,16 +1150,23 @@ def fileOpen(): self.loadConfig(filename) def fileSave(): - filename = self.json['name'] + filename = self._filename + if not filename: + filename = self.json.get('name') + if not filename: + filename = get_basename_no_ext(filename) filename = filedialog.asksaveasfilename( title='Save profile to file', filetypes=hwFilter, initialfile=filename, initialdir=os.getcwd()) - if len(filename) != 0 and checkProfilePath(filename): + if filename and checkProfilePath(filename): ext = os.path.splitext(filename)[1] if ext != HW_EXT: filename += HW_EXT + if not self.json.get('name'): + self.json['name'] = get_basename_no_ext(filename) + self.reload() json_save(self.json, filename) # Toolbar @@ -1268,10 +1275,11 @@ def loadConfig(self, filename): self.json['base_config'] = config_name else: self.json = json_load(filename) + self._filename = os.path.basename(filename) options = get_dict_value(self.json, 'options', []) for opt in configVars.get('HWCONFIG_OPTS', '').replace(' ', '').split(): - if not opt in options: + if opt not in options: options.append(opt) self.reload() @@ -1280,7 +1288,7 @@ def loadConfig(self, filename): def updateWindowTitle(self): name = self.json.get('name', None) - if name is None: + if not name: name = '(new)' else: name = '"' + name + '"' @@ -1290,8 +1298,9 @@ def reset(self): self.tree.clear() self.map.clear() self.status.set('') + self._filename = '' self.json = OrderedDict() - self.json['name'] = 'New Profile' + self.json['name'] = '' self.json['base_config'] = 'standard' self.reload() self.updateWindowTitle() diff --git a/Sming/Components/Storage/Tools/hwconfig/partition.py b/Sming/Components/Storage/Tools/hwconfig/partition.py index 51549d4e0d..f7b5e312a0 100644 --- a/Sming/Components/Storage/Tools/hwconfig/partition.py +++ b/Sming/Components/Storage/Tools/hwconfig/partition.py @@ -414,7 +414,7 @@ def dict(self): res[k] = stringnum(self.type_str()) elif k == 'subtype': res[k] = stringnum(self.subtype_str()) - elif v is not None and k != 'name': + elif v is not None and k not in ['name', 'unused_before', 'unused_after']: res[k] = v return res @@ -583,7 +583,7 @@ def add_unused(table, device, address, last_end): # Devices with no defined partitions pdevs = set(p.device for p in partitions) for dev in config.devices: - if not dev in pdevs: + if dev not in pdevs: add_unused(partitions, dev, dev.size, -1) partitions.sort() diff --git a/samples/Basic_IFS/basic_ifs_Esp32.hw b/samples/Basic_IFS/basic_ifs_Esp32.hw index 3f3f6e9c17..60bec7563b 100644 --- a/samples/Basic_IFS/basic_ifs_Esp32.hw +++ b/samples/Basic_IFS/basic_ifs_Esp32.hw @@ -1,4 +1,5 @@ { + "name": "Basic IFS sample (ESP32)", "base_config": "basic_ifs", "arch": "Esp32", "partitions": { diff --git a/samples/Basic_IFS/basic_ifs_Esp8266.hw b/samples/Basic_IFS/basic_ifs_Esp8266.hw index 301edd0a72..ab5c5fb119 100644 --- a/samples/Basic_IFS/basic_ifs_Esp8266.hw +++ b/samples/Basic_IFS/basic_ifs_Esp8266.hw @@ -1,4 +1,5 @@ { + "name": "Basic IFS (ESP8266)", "base_config": "basic_ifs", "arch": "Esp8266", "partitions": { diff --git a/samples/Basic_IFS/basic_ifs_Host.hw b/samples/Basic_IFS/basic_ifs_Host.hw index 7b6468d01a..26fe7a3313 100644 --- a/samples/Basic_IFS/basic_ifs_Host.hw +++ b/samples/Basic_IFS/basic_ifs_Host.hw @@ -1,4 +1,5 @@ { + "name": "Basic IFS (HOST)", "base_config": "basic_ifs", "arch": "Host", "partitions": { diff --git a/samples/Basic_IFS/basic_ifs_Rp2040.hw b/samples/Basic_IFS/basic_ifs_Rp2040.hw index a63a50ea25..004bc45534 100644 --- a/samples/Basic_IFS/basic_ifs_Rp2040.hw +++ b/samples/Basic_IFS/basic_ifs_Rp2040.hw @@ -1,7 +1,7 @@ { - "name": "Rp2040 config", - "arch": "Rp2040", + "name": "Basic IFS sample (RP2040)", "base_config": "basic_ifs", + "arch": "Rp2040", "options": [ "2m", "cyw43_fw" diff --git a/samples/Basic_Ota/ota.hw b/samples/Basic_Ota/ota.hw index a6a64f6186..ed59f038c9 100644 --- a/samples/Basic_Ota/ota.hw +++ b/samples/Basic_Ota/ota.hw @@ -1,4 +1,5 @@ { + "name": "Basic OTA sample", "base_config": "spiffs-two-roms", "partitions": { "rom0": { diff --git a/samples/FtpServer_Files/ftpserver-esp32.hw b/samples/FtpServer_Files/ftpserver-esp32.hw index 3fd3cdb1e4..b655c3b0db 100644 --- a/samples/FtpServer_Files/ftpserver-esp32.hw +++ b/samples/FtpServer_Files/ftpserver-esp32.hw @@ -1,5 +1,5 @@ { - "name": "FTP Server sample", + "name": "FTP Server sample (ESP32)", "base_config": "ftpserver", "partitions": { "factory": { diff --git a/samples/FtpServer_Files/ftpserver-esp8266.hw b/samples/FtpServer_Files/ftpserver-esp8266.hw index c1aedd3b6b..8a8c73f151 100644 --- a/samples/FtpServer_Files/ftpserver-esp8266.hw +++ b/samples/FtpServer_Files/ftpserver-esp8266.hw @@ -1,5 +1,5 @@ { - "name": "FTP Server sample", + "name": "FTP Server sample (ESP8266)", "base_config": "ftpserver", "partitions": { "rom0": { From 2c3ed1be74d1425310508dbed27a2a43a6a4b7a9 Mon Sep 17 00:00:00 2001 From: Mike Date: Mon, 15 Apr 2024 08:46:45 +0100 Subject: [PATCH 051/128] Fix valgrind allocator mismatch warnings (#2766) This PR fixes warnings flagged by valgrind when running HostTests. **Fix mismatched new/delete issues with SharedMemoryStream** Use correct templated allocation, also simplifies code. **Fix mismatched allocators with `LimitedMemoryStream`** Use malloc/free` instead of new/delete, as for MemoryDataStream. Resolves issue with mismatch highlighted by valgrind using `moveString`. Also add null check in allocation. --- .../Http/Websocket/WebsocketConnection.cpp | 10 ++++++---- Sming/Core/Data/Stream/LimitedMemoryStream.cpp | 5 ++++- Sming/Core/Data/Stream/LimitedMemoryStream.h | 2 +- Sming/Core/Data/Stream/SharedMemoryStream.h | 4 ++-- tests/HostTests/modules/Stream.cpp | 18 +++++++++--------- 5 files changed, 22 insertions(+), 17 deletions(-) diff --git a/Sming/Components/Network/src/Network/Http/Websocket/WebsocketConnection.cpp b/Sming/Components/Network/src/Network/Http/Websocket/WebsocketConnection.cpp index ec4d5253a2..e38363e4c0 100644 --- a/Sming/Components/Network/src/Network/Http/Websocket/WebsocketConnection.cpp +++ b/Sming/Components/Network/src/Network/Http/Websocket/WebsocketConnection.cpp @@ -299,12 +299,14 @@ bool WebsocketConnection::send(IDataSourceStream* source, ws_frame_type_t type, void WebsocketConnection::broadcast(const char* message, size_t length, ws_frame_type_t type) { - char* copy = new char[length]; - memcpy(copy, message, length); - std::shared_ptr data(copy, [](const char* ptr) { delete[] ptr; }); + std::shared_ptr data(new char[length]); + if(!data) { + return; + } + memcpy(data.get(), message, length); for(auto skt : websocketList) { - auto stream = new SharedMemoryStream(data, length); + auto stream = new SharedMemoryStream(data, length); skt->send(stream, type); } } diff --git a/Sming/Core/Data/Stream/LimitedMemoryStream.cpp b/Sming/Core/Data/Stream/LimitedMemoryStream.cpp index 89112e547f..5fe7fadf8b 100644 --- a/Sming/Core/Data/Stream/LimitedMemoryStream.cpp +++ b/Sming/Core/Data/Stream/LimitedMemoryStream.cpp @@ -50,7 +50,10 @@ int LimitedMemoryStream::seekFrom(int offset, SeekOrigin origin) size_t LimitedMemoryStream::write(const uint8_t* data, size_t size) { if(buffer == nullptr) { - buffer = new char[capacity]; + buffer = static_cast(malloc(capacity)); + if(buffer == nullptr) { + return 0; + } owned = true; } diff --git a/Sming/Core/Data/Stream/LimitedMemoryStream.h b/Sming/Core/Data/Stream/LimitedMemoryStream.h index 9457482d91..fb3148eba1 100644 --- a/Sming/Core/Data/Stream/LimitedMemoryStream.h +++ b/Sming/Core/Data/Stream/LimitedMemoryStream.h @@ -44,7 +44,7 @@ class LimitedMemoryStream : public ReadWriteStream ~LimitedMemoryStream() { if(owned) { - delete[] buffer; + free(buffer); } } diff --git a/Sming/Core/Data/Stream/SharedMemoryStream.h b/Sming/Core/Data/Stream/SharedMemoryStream.h index 6b44b55f35..7650189004 100644 --- a/Sming/Core/Data/Stream/SharedMemoryStream.h +++ b/Sming/Core/Data/Stream/SharedMemoryStream.h @@ -26,9 +26,9 @@ template class SharedMemoryStream : public IDataSourceStream public: /** @brief Constructor for use with pre-existing buffer * @param buffer - * @param capacity Size of buffer in elements + * @param size Size of buffer in elements */ - SharedMemoryStream(std::shared_ptr(buffer), size_t size) : buffer(buffer), capacity(size * sizeof(T)) + SharedMemoryStream(std::shared_ptr(buffer), size_t size) : buffer(buffer), capacity(size * sizeof(buffer[0])) { } diff --git a/tests/HostTests/modules/Stream.cpp b/tests/HostTests/modules/Stream.cpp index d360be6d1d..20199bb51e 100644 --- a/tests/HostTests/modules/Stream.cpp +++ b/tests/HostTests/modules/Stream.cpp @@ -171,8 +171,8 @@ class StreamTest : public TestGroup { // STL may perform one-time memory allocation for mutexes, etc. - std::shared_ptr data(new char[18]); - SharedMemoryStream(data, 18); + std::shared_ptr data(new char[18]); + SharedMemoryStream stream(data, 18); } auto memStart = MallocCount::getCurrent(); @@ -180,21 +180,21 @@ class StreamTest : public TestGroup TEST_CASE("SharedMemoryStream") { - char* message = new char[18]; - memcpy(message, "Wonderful data...", 18); - std::shared_ptr data(message, [&message](const char* p) { delete[] p; }); + const char* message = "Wonderful data..."; + const size_t msglen = strlen(message); + std::shared_ptr data(new char[msglen]); + memcpy(data.get(), message, msglen); debug_d("RefCount: %d", data.use_count()); - Vector*> list; + Vector*> list; for(unsigned i = 0; i < 4; i++) { - list.addElement(new SharedMemoryStream(data, strlen(message))); + list.addElement(new SharedMemoryStream(data, msglen)); } - for(unsigned i = 0; i < list.count(); i++) { + for(auto element : list) { constexpr size_t bufferSize{5}; char buffer[bufferSize]{}; - auto element = list[i]; String output; while(!element->isFinished()) { From c8cffebcfb2b1a43aa098bc794f16bb3955c6fae Mon Sep 17 00:00:00 2001 From: Mike Date: Mon, 22 Apr 2024 08:11:57 +0100 Subject: [PATCH 052/128] A few DateTime tweaks (#2767) Some further minor updates to `DateTime`: - Reduce class size. Member variables misaligned so consumes 14 bytes instead of 12. - Simplify `calcDayOfYear()` --- Sming/Core/DateTime.cpp | 18 +++--------------- Sming/Core/DateTime.h | 10 +++++----- 2 files changed, 8 insertions(+), 20 deletions(-) diff --git a/Sming/Core/DateTime.cpp b/Sming/Core/DateTime.cpp index bd76f59d47..f4d696d81f 100644 --- a/Sming/Core/DateTime.cpp +++ b/Sming/Core/DateTime.cpp @@ -531,23 +531,11 @@ String DateTime::format(const char* sFormat) const void DateTime::calcDayOfYear() { - DayofYear = 0; + uint16_t prevMonthDays{0}; for(unsigned m = dtJanuary; m < Month; ++m) { - switch(m) { - case dtSeptember: - case dtApril: - case dtJune: - case dtNovember: - DayofYear += 30; - break; - case dtFebruary: - DayofYear += isLeapYear(Year) ? 29 : 28; - break; - default: - DayofYear += 31; - } + prevMonthDays += getMonthDays(m, Year); } - DayofYear += Day; + DayofYear = prevMonthDays + Day; } uint8_t DateTime::calcWeek(uint8_t firstDay) const diff --git a/Sming/Core/DateTime.h b/Sming/Core/DateTime.h index e267bff0cc..cceb7b28ca 100644 --- a/Sming/Core/DateTime.h +++ b/Sming/Core/DateTime.h @@ -360,15 +360,15 @@ class DateTime uint8_t calcWeek(uint8_t firstDay) const; // Calculate week number based on firstDay of week public: + uint16_t Year = 0; ///< Full Year number + uint16_t DayofYear = 0; ///< Day of year (0-365) + uint8_t DayofWeek = 0; ///< Day of week (0-6 Sunday is day 0) + uint8_t Month = 0; ///< Month (0-11 Jan is month 0) + uint8_t Day = 0; ///< Day of month (1-31) uint8_t Hour = 0; ///< Hour (0-23) uint8_t Minute = 0; ///< Minute (0-59) uint8_t Second = 0; ///< Second (0-59) uint16_t Milliseconds = 0; ///< Milliseconds (0-999) - uint8_t Day = 0; ///< Day of month (1-31) - uint8_t DayofWeek = 0; ///< Day of week (0-6 Sunday is day 0) - uint16_t DayofYear = 0; ///< Day of year (0-365) - uint8_t Month = 0; ///< Month (0-11 Jan is month 0) - uint16_t Year = 0; ///< Full Year number }; /** @} */ From ea00370bdb4ac917206b2ab5a5cd8b32d1b0a390 Mon Sep 17 00:00:00 2001 From: Mike Date: Mon, 22 Apr 2024 15:12:15 +0100 Subject: [PATCH 053/128] Improvements to HostEd (#2768) This PR proposes some changes to the Hosted classes **Update simpleRPC to master** With clang-tidy #2648 some failures occur in the `simpleRPC` library. Updating this to current master solves the issues. I've also put all the code into the `simpleRPC` namespace which sorts out the conflict with `Vector` but makes it available for use if required. **Tidy HostTests module** Also decode the anonymous 'packet' blob so we can see what's in it and compare with spec. should we wish to update it. **Use abstract base class for callbacks** Simplifies implementation since usually all callbacks are required. Compiler ensures all methods have implementations. `clang-tidy` gave potential memory leak indication because of Delegates. Not sure if that's real but this fixes it. --- .../Components/hostlib/include/hostlib/init.h | 30 ---- Sming/Arch/Host/Components/hostlib/init.cpp | 27 ---- .../Arch/Host/Components/hostlib/startup.cpp | 5 +- Sming/Arch/Host/app.mk | 4 +- Sming/Components/Hosted/README.rst | 8 +- Sming/Components/Hosted/component.mk | 16 +- .../Components/Hosted/include/Hosted/Client.h | 29 ++-- .../Components/Hosted/include/Hosted/Serial.h | 1 - .../include/Hosted/Transport/BaseTransport.h | 8 +- .../Hosted/Transport/SerialTransport.h | 8 +- .../Hosted/Transport/TcpClientStream.h | 12 +- .../Hosted/Transport/TcpClientTransport.h | 10 +- .../Hosted/Transport/TcpServerTransport.h | 12 +- .../include/Hosted/Transport/TcpTransport.h | 8 +- .../Hosted/init/serial/InitClient.cpp | 20 ++- .../Components/Hosted/init/tcp/InitClient.cpp | 23 ++- .../Hosted/samples/serial/app/application.cpp | 39 ++--- .../Hosted/samples/tcp/app/application.cpp | 2 +- .../Hosted/samples/tcp/component.mk | 2 +- Sming/Components/Hosted/src/Util.cpp | 2 +- .../simpleRPC/include/simpleRPC/parser.h | 28 ++-- Sming/Components/simpleRPC/simpleRPC | 2 +- Sming/Components/simpleRPC/simpleRPC.patch | 69 ++++----- Sming/Components/simpleRPC/src/parser.cpp | 50 +++--- .../modules/Network/Arch/Host/Hosted.cpp | 143 +++++++++--------- 25 files changed, 238 insertions(+), 320 deletions(-) delete mode 100644 Sming/Arch/Host/Components/hostlib/include/hostlib/init.h delete mode 100644 Sming/Arch/Host/Components/hostlib/init.cpp diff --git a/Sming/Arch/Host/Components/hostlib/include/hostlib/init.h b/Sming/Arch/Host/Components/hostlib/include/hostlib/init.h deleted file mode 100644 index 6b73b8afbc..0000000000 --- a/Sming/Arch/Host/Components/hostlib/include/hostlib/init.h +++ /dev/null @@ -1,30 +0,0 @@ -/**** - * hostlib.h - * - * Copyright 2019 mikee47 - * - * This file is part of the Sming Framework Project - * - * This library is free software: you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation, version 3 or later. - * - * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with SHEM. - * If not, see . - * - ****/ - -#pragma once - -#ifdef __cplusplus -extern "C" { -#endif - -void host_init(); - -#ifdef __cplusplus -} -#endif diff --git a/Sming/Arch/Host/Components/hostlib/init.cpp b/Sming/Arch/Host/Components/hostlib/init.cpp deleted file mode 100644 index 4246269810..0000000000 --- a/Sming/Arch/Host/Components/hostlib/init.cpp +++ /dev/null @@ -1,27 +0,0 @@ -/** - * init.cpp - Sming Host Emulator startup code - * - * Copyright 2019 mikee47 - * - * This file is part of the Sming Framework Project - * - * This library is free software: you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation, version 3 or later. - * - * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with SHEM. - * If not, see . - * - ****/ - -#include "include/hostlib/init.h" - -extern void init(); - -void host_init() -{ - init(); -} diff --git a/Sming/Arch/Host/Components/hostlib/startup.cpp b/Sming/Arch/Host/Components/hostlib/startup.cpp index 8bb37fe968..4286327055 100644 --- a/Sming/Arch/Host/Components/hostlib/startup.cpp +++ b/Sming/Arch/Host/Components/hostlib/startup.cpp @@ -32,7 +32,6 @@ #include #include #include -#include "include/hostlib/init.h" #include "include/hostlib/emu.h" #include "include/hostlib/hostlib.h" #include "include/hostlib/CommandLine.h" @@ -43,6 +42,8 @@ #include #endif +extern void init(); + namespace { static int exitCode; @@ -286,7 +287,7 @@ int main(int argc, char* argv[]) System.initialize(); - host_init(); + init(); while(!done) { int due = host_main_loop(); diff --git a/Sming/Arch/Host/app.mk b/Sming/Arch/Host/app.mk index 795589309a..a2a92ebb85 100644 --- a/Sming/Arch/Host/app.mk +++ b/Sming/Arch/Host/app.mk @@ -14,7 +14,9 @@ TARGET_OUT_0 := $(FW_BASE)/$(APP_NAME)$(TOOL_EXT) # Hosted Settings ifneq ($(ENABLE_HOSTED),) - COMPONENTS_AR := $(USER_LIBDIR)/$(CLIB_PREFIX)Hosted-Lib-$(CMP_Hosted-Lib_LIBHASH).a $(COMPONENTS_AR) +COMPONENTS_AR := \ + $(CMP_Hosted-Lib_TARGETS) \ + $(COMPONENTS_AR) endif # Target definitions diff --git a/Sming/Components/Hosted/README.rst b/Sming/Components/Hosted/README.rst index 6acc1b9f58..d167327cf9 100644 --- a/Sming/Components/Hosted/README.rst +++ b/Sming/Components/Hosted/README.rst @@ -6,8 +6,10 @@ HostEd The hosted component allows Sming's host emulator to run parts of the commands on an actual microcontroller. The communication is done via `simplePRC `_ and the microcontroller has to be flashed with a special application. + Overview -------- + Sming's host emulator allows easier debugging and development of embedded applications. This component named "Hosted" extends the host emulator and facilitates testing functionality that only a real microcontroller can provide as for example digital I/O operations or SPI operations. @@ -24,7 +26,7 @@ We need to compile and flash also a special application on the desired microcont This application will act as an RPC Server and will execute the commands from the host emulator on the microcontroller. In the ``samples`` directory you will find the sample applications that will turn your microcontroller into -an RCP server. +an RPC server. The compilation and flashing for ESP32, for example, can be done using the following commands:: @@ -33,7 +35,8 @@ The compilation and flashing for ESP32, for example, can be done using the follo make flash If you replace ``SMING_ARCH=Esp32`` with ``SMING_ARCH=Esp8266`` then the hosted application will be compiled and flashed on a ESP8266 microcontroller. -Make sure to replace the values of  WIFI_SSID and WIFI_PWD with the actual name and password for the Access Point (AP). +Make sure to replace the values of WIFI_SSID and WIFI_PWD with the actual name and password for the Access Point (AP). + Communication ------------- @@ -42,6 +45,7 @@ can be done using TCP or serial interface. The ``transport`` classes are located under ``include/Hosted/Transport``. + Configuration ------------- diff --git a/Sming/Components/Hosted/component.mk b/Sming/Components/Hosted/component.mk index 4d19f5a640..9916e6aedb 100644 --- a/Sming/Components/Hosted/component.mk +++ b/Sming/Components/Hosted/component.mk @@ -11,7 +11,7 @@ ENABLE_HOSTED ?= ifneq ($(ENABLE_HOSTED),) COMPONENT_SRCDIRS += init/$(ENABLE_HOSTED) - EXTRA_LDFLAGS := $(call Wrap,host_init) + EXTRA_LDFLAGS := $(call Wrap,_Z4initv) COMPONENT_DEPENDS += SerialLib ifeq ($(ENABLE_HOSTED),tcp) COMPONENT_DEPENDS += Network @@ -20,13 +20,17 @@ ifneq ($(ENABLE_HOSTED),) endif endif -COMPONENT_RELINK_VARS += HOSTED_SERVER_IP +COMPONENT_RELINK_VARS += \ + HOSTED_SERVER_IP \ + HOSTED_COM_PORT \ + HOSTED_COM_SPEED -COMPONENT_RELINK_VARS += HOSTED_COM_PORT HOSTED_COM_PORT ?= $(COM_PORT) - -COMPONENT_RELINK_VARS += HOSTED_COM_SPEED HOSTED_COM_SPEED ?= 115200 -COMPONENT_CFLAGS = -DHOSTED_SERVER_IP=$(HOSTED_SERVER_IP) -DHOSTED_COM_PORT="\"$(HOSTED_COM_PORT)"\" -DHOSTED_COM_SPEED=$(HOSTED_COM_SPEED) +COMPONENT_CFLAGS = \ + -DHOSTED_SERVER_IP=$(HOSTED_SERVER_IP) \ + -DHOSTED_COM_PORT="\"$(HOSTED_COM_PORT)"\" \ + -DHOSTED_COM_SPEED=$(HOSTED_COM_SPEED) + COMPONENT_CXXFLAGS := $(COMPONENT_CFLAGS) diff --git a/Sming/Components/Hosted/include/Hosted/Client.h b/Sming/Components/Hosted/include/Hosted/Client.h index 55c981c904..d7e76f91a7 100644 --- a/Sming/Components/Hosted/include/Hosted/Client.h +++ b/Sming/Components/Hosted/include/Hosted/Client.h @@ -26,13 +26,11 @@ #include #include "Util.h" -using namespace simpleRPC; - namespace Hosted { constexpr int COMMAND_NOT_FOUND = -1; -class Client +class Client : private simpleRPC::ParserCallbacks { public: using RemoteCommands = HashMap; @@ -64,7 +62,7 @@ class Client return false; } - rpcPrint(stream, uint8_t(functionId), args...); + simpleRPC::rpcPrint(stream, uint8_t(functionId), args...); stream.flush(); return true; @@ -120,17 +118,12 @@ class Client { host_debug_i("Getting remote RPC commands \033[5m...\033[0m"); + using namespace simpleRPC; + uint8_t head = 0xff; stream.write(&head, 1); char buffer[512]; - ParserSettings settings; - settings.startMethods = ParserSettings::SimpleMethod(&Client::startMethods, this); - settings.startMethod = ParserSettings::SimpleMethod(&Client::startMethod, this); - settings.methodSignature = ParserSettings::CharMethod(&Client::methodSignature, this); - settings.methodName = ParserSettings::CharMethod(&Client::methodName, this); - settings.endMethod = ParserSettings::SimpleMethod(&Client::endMethod, this); - settings.endMethods = ParserSettings::SimpleMethod(&Client::endMethods, this); - settings.state = ParserState::ready; + ParserSettings settings{*this}; do { stream.flush(); @@ -172,29 +165,29 @@ class Client String signature; char methodEndsWith; - void startMethods() + void startMethods() override { methodPosition = 0; commands.clear(); } - void startMethod() + void startMethod() override { name = ""; signature = ""; } - void methodSignature(char ch) + void methodSignature(char ch) override { signature += ch; } - void methodName(char ch) + void methodName(char ch) override { name += ch; } - void endMethod() + void endMethod() override { if(!commands.contains(name) || signature == ":") { commands[name] = methodPosition; @@ -202,7 +195,7 @@ class Client commands[name + "(" + signature + ")"] = methodPosition++; } - void endMethods() + void endMethods() override { fetchCommands = false; } diff --git a/Sming/Components/Hosted/include/Hosted/Serial.h b/Sming/Components/Hosted/include/Hosted/Serial.h index 3f65340e8b..f5d703580f 100644 --- a/Sming/Components/Hosted/include/Hosted/Serial.h +++ b/Sming/Components/Hosted/include/Hosted/Serial.h @@ -116,7 +116,6 @@ class Serial : public Stream private: String ttyDevice; - serialib transport; }; diff --git a/Sming/Components/Hosted/include/Hosted/Transport/BaseTransport.h b/Sming/Components/Hosted/include/Hosted/Transport/BaseTransport.h index 484a568c02..186db741bf 100644 --- a/Sming/Components/Hosted/include/Hosted/Transport/BaseTransport.h +++ b/Sming/Components/Hosted/include/Hosted/Transport/BaseTransport.h @@ -16,9 +16,7 @@ #include #include -namespace Hosted -{ -namespace Transport +namespace Hosted::Transport { class BaseTransport { @@ -38,6 +36,4 @@ class BaseTransport DataHandler handler; }; -} // namespace Transport - -} // namespace Hosted +} // namespace Hosted::Transport diff --git a/Sming/Components/Hosted/include/Hosted/Transport/SerialTransport.h b/Sming/Components/Hosted/include/Hosted/Transport/SerialTransport.h index 6dca2b1510..e044ca9057 100644 --- a/Sming/Components/Hosted/include/Hosted/Transport/SerialTransport.h +++ b/Sming/Components/Hosted/include/Hosted/Transport/SerialTransport.h @@ -16,9 +16,7 @@ #include #include "BaseTransport.h" -namespace Hosted -{ -namespace Transport +namespace Hosted::Transport { class SerialTransport : public BaseTransport { @@ -35,6 +33,4 @@ class SerialTransport : public BaseTransport } }; -} // namespace Transport - -} // namespace Hosted +} // namespace Hosted::Transport diff --git a/Sming/Components/Hosted/include/Hosted/Transport/TcpClientStream.h b/Sming/Components/Hosted/include/Hosted/Transport/TcpClientStream.h index 1d43fcc872..d1809f4078 100644 --- a/Sming/Components/Hosted/include/Hosted/Transport/TcpClientStream.h +++ b/Sming/Components/Hosted/include/Hosted/Transport/TcpClientStream.h @@ -16,15 +16,13 @@ #include #include -namespace Hosted -{ -namespace Transport +namespace Hosted::Transport { class TcpClientStream : public Stream { public: TcpClientStream(TcpClient& client, size_t cbufferSize = 1024, size_t threshold = 400) - : cBuffer(cbufferSize), client(client), pendingBytes(0), threshold(threshold) + : cBuffer(cbufferSize), client(client), threshold(threshold) { client.setReceiveDelegate(TcpClientDataDelegate(&TcpClientStream::store, this)); } @@ -87,7 +85,7 @@ class TcpClientStream : public Stream private: CircularBuffer cBuffer; TcpClient& client; - size_t pendingBytes; + size_t pendingBytes{0}; size_t threshold; bool store(TcpClient& client, char* data, int size) @@ -96,6 +94,4 @@ class TcpClientStream : public Stream } }; -} // namespace Transport - -} // namespace Hosted +} // namespace Hosted::Transport diff --git a/Sming/Components/Hosted/include/Hosted/Transport/TcpClientTransport.h b/Sming/Components/Hosted/include/Hosted/Transport/TcpClientTransport.h index 0971b0c496..d9c7b53409 100644 --- a/Sming/Components/Hosted/include/Hosted/Transport/TcpClientTransport.h +++ b/Sming/Components/Hosted/include/Hosted/Transport/TcpClientTransport.h @@ -18,14 +18,12 @@ #include "TcpClientStream.h" #include -namespace Hosted -{ -namespace Transport +namespace Hosted::Transport { class TcpClientTransport : public TcpTransport { public: - TcpClientTransport(TcpClient& client) : stream(new TcpClientStream(client)) + TcpClientTransport(TcpClient& client) : stream(std::make_unique(client)) { client.setReceiveDelegate(TcpClientDataDelegate(&TcpClientTransport::process, this)); } @@ -44,6 +42,4 @@ class TcpClientTransport : public TcpTransport std::unique_ptr stream; }; -} // namespace Transport - -} // namespace Hosted +} // namespace Hosted::Transport diff --git a/Sming/Components/Hosted/include/Hosted/Transport/TcpServerTransport.h b/Sming/Components/Hosted/include/Hosted/Transport/TcpServerTransport.h index 6e85cafc43..bfd387ea8b 100644 --- a/Sming/Components/Hosted/include/Hosted/Transport/TcpServerTransport.h +++ b/Sming/Components/Hosted/include/Hosted/Transport/TcpServerTransport.h @@ -18,15 +18,11 @@ #include "TcpTransport.h" #include "TcpClientStream.h" -namespace Hosted -{ -namespace Transport +namespace Hosted::Transport { class TcpServerTransport : public TcpTransport { public: - using ClientMap = ObjectMap; - TcpServerTransport(TcpServer& server) { server.setClientReceiveHandler(TcpClientDataDelegate(&TcpServerTransport::process, this)); @@ -52,9 +48,9 @@ class TcpServerTransport : public TcpTransport } private: + using ClientMap = ObjectMap; + ClientMap map; }; -} // namespace Transport - -} // namespace Hosted +} // namespace Hosted::Transport diff --git a/Sming/Components/Hosted/include/Hosted/Transport/TcpTransport.h b/Sming/Components/Hosted/include/Hosted/Transport/TcpTransport.h index 9553430592..3099167f75 100644 --- a/Sming/Components/Hosted/include/Hosted/Transport/TcpTransport.h +++ b/Sming/Components/Hosted/include/Hosted/Transport/TcpTransport.h @@ -16,9 +16,7 @@ #include #include "BaseTransport.h" -namespace Hosted -{ -namespace Transport +namespace Hosted::Transport { class TcpTransport : public BaseTransport { @@ -26,6 +24,4 @@ class TcpTransport : public BaseTransport virtual bool process(TcpClient& client, char* data, int size) = 0; }; -} // namespace Transport - -} // namespace Hosted +} // namespace Hosted::Transport diff --git a/Sming/Components/Hosted/init/serial/InitClient.cpp b/Sming/Components/Hosted/init/serial/InitClient.cpp index f1ecbed542..91eb3a7177 100644 --- a/Sming/Components/Hosted/init/serial/InitClient.cpp +++ b/Sming/Components/Hosted/init/serial/InitClient.cpp @@ -22,27 +22,25 @@ #define HOSTED_COM_SPEED 115200 #endif -Hosted::Client* hostedClient{nullptr}; +Hosted::Client* hostedClient; -extern void init(); - -extern "C" { -void __real_host_init(); -void __wrap_host_init(); -} +extern "C" void __real__Z4initv(); +namespace +{ Hosted::Serial hostedSerial(HOSTED_COM_PORT); +} -void __wrap_host_init() +extern "C" void __wrap__Z4initv() { host_printf("Connecting to: %s ...\r\n", HOSTED_COM_PORT); - bool serialReady = false; - while(!(serialReady = hostedSerial.begin(HOSTED_COM_SPEED))) { + while(!hostedSerial.begin(HOSTED_COM_SPEED)) { msleep(50); } hostedClient = new Hosted::Client(hostedSerial, '>'); hostedClient->getRemoteCommands(); - init(); + + __real__Z4initv(); } diff --git a/Sming/Components/Hosted/init/tcp/InitClient.cpp b/Sming/Components/Hosted/init/tcp/InitClient.cpp index 3e4dce3bc7..dce5a33439 100644 --- a/Sming/Components/Hosted/init/tcp/InitClient.cpp +++ b/Sming/Components/Hosted/init/tcp/InitClient.cpp @@ -11,11 +11,12 @@ * ****/ -#include +#include +#include #include #include -Hosted::Client* hostedClient{nullptr}; +Hosted::Client* hostedClient; #ifndef WIFI_SSID #define WIFI_SSID "PleaseEnterSSID" // Put your SSID and password here @@ -30,19 +31,14 @@ Hosted::Client* hostedClient{nullptr}; #define REMOTE_IP STRINGIFY(HOSTED_SERVER_IP) #endif -extern "C" { -void __real_host_init(); -void __wrap_host_init(); -} - -extern void init(); +extern "C" void __real__Z4initv(); namespace { -TcpClient* tcpClient = nullptr; -Hosted::Transport::TcpClientStream* stream = nullptr; +TcpClient* tcpClient; +Hosted::Transport::TcpClientStream* stream; -static void ready(IpAddress ip, IpAddress mask, IpAddress gateway) +void ready(IpAddress ip, IpAddress mask, IpAddress gateway) { if(hostedClient != nullptr) { return; @@ -59,12 +55,13 @@ static void ready(IpAddress ip, IpAddress mask, IpAddress gateway) hostedClient = new Hosted::Client(*stream, '>'); hostedClient->getRemoteCommands(); - init(); + + __real__Z4initv(); } } // namespace -void __wrap_host_init() +extern "C" void __wrap__Z4initv() { WifiEvents.onStationGotIP(ready); WifiStation.enable(true); diff --git a/Sming/Components/Hosted/samples/serial/app/application.cpp b/Sming/Components/Hosted/samples/serial/app/application.cpp index 5f5c0288d4..734f88221f 100644 --- a/Sming/Components/Hosted/samples/serial/app/application.cpp +++ b/Sming/Components/Hosted/samples/serial/app/application.cpp @@ -8,14 +8,15 @@ using namespace Hosted::Transport; -SerialTransport* transport = nullptr; +SerialTransport transport(Serial); void init() { Serial.begin(SERIAL_BAUD_RATE); - transport = new SerialTransport(Serial); - transport->onData([](Stream& stream) { + using namespace simpleRPC; + + transport.onData([](Stream& stream) { // clang-format off interface(stream, /* @@ -28,25 +29,25 @@ void init() digitalWrite, F("digitalWrite> Write to a digital pin. @pin: Pin number. @value: Pin value."), pulseIn, F("pulseIn> Measure duration of pulse on pin. @pin: Pin number. @state: State of pulse to measure. @timeout: Maximum duration of pulse. @return: Pulse duration in microseconds)"), // void TwoWire::begin(uint8_t sda, uint8_t scl) - pack(&Wire, (void(TwoWire::*)(uint8_t,uint8_t))&TwoWire::begin), F("TwoWire::begin> Starts two-wire communication. @sda: Data pin. @scl: Clock pin."), + makeTuple(&Wire, static_cast(&TwoWire::begin)), F("TwoWire::begin> Starts two-wire communication. @sda: Data pin. @scl: Clock pin."), // void TwoWire::begin() - pack(&Wire, (void(TwoWire::*)(void))&TwoWire::begin), F("TwoWire::begin> Starts two-wire communication."), - pack(&Wire, &TwoWire::pins), F("TwoWire::pins> Starts two-wire communication. @sda: Data pin. @scl: Clock pin."), - pack(&Wire, &TwoWire::status), F("TwoWire::status> Get status."), - pack(&Wire, &TwoWire::end), F("TwoWire::end> Ends two-wire communication."), - pack(&Wire, &TwoWire::setClock), F("TwoWire::setClock> Sets clock frequency. @freq: clock frequency."), - pack(&Wire, &TwoWire::setClockStretchLimit), F("TwoWire::setClockStretchLimit> Sts clock stretch limit. @limit: stretch limit."), - pack(&Wire, &TwoWire::requestFrom), F("TwoWire::requestFrom> Request from. @address: Address. @size: Size. @sendStop flag. @return: uint8_t."), - pack(&Wire, &TwoWire::beginTransmission), F("TwoWire::beginTransmission> Begin transmission. @address: Address."), - pack(&Wire, &TwoWire::endTransmission), F("TwoWire::endTransmission> End transmission. @sendStop: flag. @return: error code"), + makeTuple(&Wire, static_cast(&TwoWire::begin)), F("TwoWire::begin> Starts two-wire communication."), + makeTuple(&Wire, &TwoWire::pins), F("TwoWire::pins> Starts two-wire communication. @sda: Data pin. @scl: Clock pin."), + makeTuple(&Wire, &TwoWire::status), F("TwoWire::status> Get status."), + makeTuple(&Wire, &TwoWire::end), F("TwoWire::end> Ends two-wire communication."), + makeTuple(&Wire, &TwoWire::setClock), F("TwoWire::setClock> Sets clock frequency. @freq: clock frequency."), + makeTuple(&Wire, &TwoWire::setClockStretchLimit), F("TwoWire::setClockStretchLimit> Sts clock stretch limit. @limit: stretch limit."), + makeTuple(&Wire, &TwoWire::requestFrom), F("TwoWire::requestFrom> Request from. @address: Address. @size: Size. @sendStop flag. @return: uint8_t."), + makeTuple(&Wire, &TwoWire::beginTransmission), F("TwoWire::beginTransmission> Begin transmission. @address: Address."), + makeTuple(&Wire, &TwoWire::endTransmission), F("TwoWire::endTransmission> End transmission. @sendStop: flag. @return: error code"), // size_t TwoWire::write(uint8_t data) - pack(&Wire, (size_t(TwoWire::*)(uint8_t))&TwoWire::write), F("TwoWire::write> Write byte. @data: byte. @return: written bytes"), + makeTuple(&Wire, static_cast(&TwoWire::write)), F("TwoWire::write> Write byte. @data: byte. @return: written bytes"), // size_t TwoWire::write(const uint8_t* data, size_t quantity) - pack(&Wire, (size_t(TwoWire::*)(const uint8_t*, size_t))&TwoWire::write), F("TwoWire::write> Write bytes. @data: data pointer. @quantity: data size. @return: written bytes"), - pack(&Wire, &TwoWire::available), F("TwoWire::available> Available bytes. @return: count"), - pack(&Wire, &TwoWire::read), F("TwoWire::read> Read a byte. @return: byte"), - pack(&Wire, &TwoWire::peek), F("TwoWire::peek> Peek. @return: byte without advancing the internal pointer."), - pack(&Wire, &TwoWire::flush), F("TwoWire::flush> Flush.") + makeTuple(&Wire, static_cast(&TwoWire::write)), F("TwoWire::write> Write bytes. @data: data pointer. @quantity: data size. @return: written bytes"), + makeTuple(&Wire, &TwoWire::available), F("TwoWire::available> Available bytes. @return: count"), + makeTuple(&Wire, &TwoWire::read), F("TwoWire::read> Read a byte. @return: byte"), + makeTuple(&Wire, &TwoWire::peek), F("TwoWire::peek> Peek. @return: byte without advancing the internal pointer."), + makeTuple(&Wire, &TwoWire::flush), F("TwoWire::flush> Flush.") ); // clang-format on diff --git a/Sming/Components/Hosted/samples/tcp/app/application.cpp b/Sming/Components/Hosted/samples/tcp/app/application.cpp index 54ceb15e6e..a7e264cd19 100644 --- a/Sming/Components/Hosted/samples/tcp/app/application.cpp +++ b/Sming/Components/Hosted/samples/tcp/app/application.cpp @@ -38,7 +38,7 @@ void connectOk(IpAddress ip, IpAddress mask, IpAddress gateway) transport = new TcpServerTransport(*server); transport->onData([](Stream& stream) { // clang-format off - interface(stream, + simpleRPC::interface(stream, /* * Below we are exporting the following remote commands: * - pinMode diff --git a/Sming/Components/Hosted/samples/tcp/component.mk b/Sming/Components/Hosted/samples/tcp/component.mk index 8883932d4c..26c10562da 100644 --- a/Sming/Components/Hosted/samples/tcp/component.mk +++ b/Sming/Components/Hosted/samples/tcp/component.mk @@ -4,7 +4,7 @@ ENABLE_HOSTED := # If set the application should connect to a WIFI access point # otherwise it will set its own access point -COMPONENT_RELINK_VARS := CONNECT_TO_WIFI +CONFIG_VARS := CONNECT_TO_WIFI CONNECT_TO_WIFI ?= 0 APP_CFLAGS = -DCONNECT_TO_WIFI=$(CONNECT_TO_WIFI) diff --git a/Sming/Components/Hosted/src/Util.cpp b/Sming/Components/Hosted/src/Util.cpp index dde76ac755..11d9c6ecb5 100644 --- a/Sming/Components/Hosted/src/Util.cpp +++ b/Sming/Components/Hosted/src/Util.cpp @@ -36,7 +36,7 @@ char convertType(const String& type) // TODO: ... add all types... if(type != "void") { - debug_w("Unknown type: %s", type); + debug_w("Unknown type: %s", type.c_str()); } // void and unknown diff --git a/Sming/Components/simpleRPC/include/simpleRPC/parser.h b/Sming/Components/simpleRPC/include/simpleRPC/parser.h index d797cbc51f..2c43bfd890 100644 --- a/Sming/Components/simpleRPC/include/simpleRPC/parser.h +++ b/Sming/Components/simpleRPC/include/simpleRPC/parser.h @@ -14,6 +14,7 @@ #pragma once #include +#include namespace simpleRPC { @@ -47,19 +48,28 @@ enum class ParserState { finished }; -struct ParserSettings { - using SimpleMethod = Delegate; - using CharMethod = Delegate; +class ParserCallbacks +{ +public: + virtual ~ParserCallbacks() + { + } + + virtual void startMethods() = 0; + virtual void startMethod() = 0; + virtual void methodSignature(char c) = 0; + virtual void methodName(char c) = 0; + virtual void endMethod() = 0; + virtual void endMethods() = 0; +}; - SimpleMethod startMethods; - SimpleMethod startMethod; - CharMethod methodSignature; - CharMethod methodName; - SimpleMethod endMethod; - SimpleMethod endMethods; +struct ParserSettings { + ParserCallbacks& callbacks; ParserState state = ParserState::ready; }; ParserResult parse(ParserSettings& settings, const char* buffer, size_t length, char nameEndsWith = ':'); +String toString(ParserResult result); + } // namespace simpleRPC diff --git a/Sming/Components/simpleRPC/simpleRPC b/Sming/Components/simpleRPC/simpleRPC index 609e739742..12d58ac910 160000 --- a/Sming/Components/simpleRPC/simpleRPC +++ b/Sming/Components/simpleRPC/simpleRPC @@ -1 +1 @@ -Subproject commit 609e7397428a973e83a97b2d5cf75c4321b8d26f +Subproject commit 12d58ac9100ea4121a2a3bd34a02828f79516eae diff --git a/Sming/Components/simpleRPC/simpleRPC.patch b/Sming/Components/simpleRPC/simpleRPC.patch index f19e3c2aa3..004a144383 100644 --- a/Sming/Components/simpleRPC/simpleRPC.patch +++ b/Sming/Components/simpleRPC/simpleRPC.patch @@ -1,48 +1,41 @@ diff --git a/src/defs.h b/src/defs.h -index c5b9ccf..9b2367d 100644 +index 54347e9..b977b19 100644 --- a/src/defs.h +++ b/src/defs.h -@@ -1,6 +1,6 @@ +@@ -1,6 +1,8 @@ #pragma once -#include +#include ++#include ++using std::min; + + char const PROTOCOL_[] {"simpleRPC"}; + char const VERSION_[] {"\x03\x00\x00"}; +diff --git a/src/simpleRPC.h b/src/simpleRPC.h +index dea278c..da3394e 100644 +--- a/src/simpleRPC.h ++++ b/src/simpleRPC.h +@@ -1,6 +1,10 @@ + #pragma once - #define _PROTOCOL "simpleRPC" - #define _VERSION "\3\0\0" -diff --git a/src/read.tcc b/src/read.tcc -index b32699e..3f03cde 100644 ---- a/src/read.tcc -+++ b/src/read.tcc -@@ -2,7 +2,6 @@ - - #include "defs.h" - #include "tuple.tcc" --#include "vector.tcc" - - //! \defgroup read - -diff --git a/src/types.tcc b/src/types.tcc -index b0de8d9..aca5f07 100644 ---- a/src/types.tcc -+++ b/src/types.tcc -@@ -2,7 +2,6 @@ - - #include "print.tcc" - #include "tuple.tcc" --#include "vector.tcc" - - //! \defgroup types - -diff --git a/src/write.tcc b/src/write.tcc -index 1f018c4..727f81f 100644 ---- a/src/write.tcc -+++ b/src/write.tcc -@@ -2,7 +2,6 @@ - - #include "print.tcc" - #include "tuple.tcc" --#include "vector.tcc" ++namespace simpleRPC ++{ + #include "interface.tcc" + + // I/O plugins. + #include "plugins/half_duplex/stream.h" ++ ++} // namespace simpleRPC +diff --git a/src/vector.tcc b/src/vector.tcc +index 49e1d27..a2d6352 100644 +--- a/src/vector.tcc ++++ b/src/vector.tcc +@@ -1,7 +1,5 @@ + #pragma once - //! \defgroup write +-#include +- + template + void swap_(T& a, T& b) noexcept { diff --git a/Sming/Components/simpleRPC/src/parser.cpp b/Sming/Components/simpleRPC/src/parser.cpp index adfce7d5a4..b7a8daebe4 100644 --- a/Sming/Components/simpleRPC/src/parser.cpp +++ b/Sming/Components/simpleRPC/src/parser.cpp @@ -36,9 +36,11 @@ namespace simpleRPC ParserResult parse(ParserSettings& settings, const char* buffer, size_t length, char nameEndsWith) { auto& state = settings.state; + auto& callbacks = settings.callbacks; + /* - * See: https://simplerpc.readthedocs.io/en/latest/protocol.html# -00000000 ff . + * See: https://simplerpc.readthedocs.io/en/latest/protocol.html# + 00000000 ff . 00000000 73 s 00000001 69 i 00000002 6d m @@ -73,7 +75,7 @@ ParserResult parse(ParserSettings& settings, const char* buffer, size_t length, 000000E1 65 72 2e 20 40 76 61 6c 75 65 3a 20 50 69 6e 20 er. @val ue: Pin 000000F1 76 61 6c 75 65 2e 00 value.. 00 . -*/ + */ bool hasError = false; for(const char* p = buffer; p != buffer + length; p++) { char ch = *p; @@ -129,9 +131,7 @@ ParserResult parse(ParserSettings& settings, const char* buffer, size_t length, goto ERROR; } - if(settings.startMethods) { - settings.startMethods(); - } + callbacks.startMethods(); state = ParserState::extract_method_start; break; } @@ -141,9 +141,7 @@ ParserResult parse(ParserSettings& settings, const char* buffer, size_t length, goto REENTER; } - if(settings.startMethod) { - settings.startMethod(); - } + callbacks.startMethod(); state = ParserState::extract_method_signature; /* fall-through */ } @@ -153,9 +151,7 @@ ParserResult parse(ParserSettings& settings, const char* buffer, size_t length, break; } - if(settings.methodSignature) { - settings.methodSignature(ch); - } + callbacks.methodSignature(ch); break; } case ParserState::extract_method_name: { @@ -164,23 +160,17 @@ ParserResult parse(ParserSettings& settings, const char* buffer, size_t length, break; } - if(settings.methodName) { - settings.methodName(ch); - } + callbacks.methodName(ch); break; } case ParserState::extract_method_end: { SKIP_UNTIL('\0', ParserState::extract_method_start); - if(settings.endMethod) { - settings.endMethod(); - } + callbacks.endMethod(); break; } case ParserState::end_methods: { - if(settings.endMethods) { - settings.endMethods(); - } + callbacks.endMethods(); state = ParserState::finished; /* fall through */ } @@ -195,11 +185,21 @@ ParserResult parse(ParserSettings& settings, const char* buffer, size_t length, } // end for ERROR: - if(hasError) { - return ParserResult::error; - } + return hasError ? ParserResult::error : ParserResult::more; +} - return ParserResult::more; +String toString(ParserResult result) +{ + using namespace simpleRPC; + switch(result) { + case ParserResult::finished: + return F("finished"); + case ParserResult::more: + return F("more"); + case ParserResult::error: + return F("error"); + } + return nullptr; } } // namespace simpleRPC diff --git a/tests/HostTests/modules/Network/Arch/Host/Hosted.cpp b/tests/HostTests/modules/Network/Arch/Host/Hosted.cpp index fa01c930e9..9f194841c5 100644 --- a/tests/HostTests/modules/Network/Arch/Host/Hosted.cpp +++ b/tests/HostTests/modules/Network/Arch/Host/Hosted.cpp @@ -8,6 +8,8 @@ #include #include +namespace +{ using namespace simpleRPC; static uint32_t plusCommand(uint8_t a, uint16_t b) @@ -40,48 +42,72 @@ class TheWire TheWire theWire; -class HostedTest : public TestGroup +class TestParseHandler : public ParserCallbacks { public: using RemoteCommands = HashMap; - HostedTest() : TestGroup(_F("Hosted")) + RemoteCommands commands; + uint8_t methodPosition = 0; + String parsedCommand; + + void startMethods() override { + methodPosition = 0; + commands.clear(); } - void execute() override + void startMethod() override + { + parsedCommand = ""; + } + + void methodSignature(char c) override + { + } + + void methodName(char ch) override + { + parsedCommand += ch; + } + + void endMethod() override + { + commands[parsedCommand] = methodPosition++; + } + + void endMethods() override + { + } +}; + +class HostedTest : public TestGroup +{ +public: + HostedTest() : TestGroup("Hosted") { - char packet[] = { - 0x73, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x50, 0x43, 0x00, 0x03, 0x00, 0x00, 0x3c, 0x49, 0x00, 0x3a, 0x20, - 0x48, 0x20, 0x42, 0x3b, 0x70, 0x69, 0x6e, 0x4d, 0x6f, 0x64, 0x65, 0x3a, 0x20, 0x53, 0x65, 0x74, 0x73, 0x20, - 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x64, 0x69, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x20, 0x70, 0x69, - 0x6e, 0x2e, 0x20, 0x40, 0x70, 0x69, 0x6e, 0x3a, 0x20, 0x50, 0x69, 0x6e, 0x20, 0x6e, 0x75, 0x6d, 0x62, 0x65, - 0x72, 0x2c, 0x20, 0x40, 0x6d, 0x6f, 0x64, 0x65, 0x3a, 0x20, 0x4d, 0x6f, 0x64, 0x65, 0x20, 0x74, 0x79, 0x70, - 0x65, 0x2e, 0x00, 0x42, 0x3a, 0x20, 0x48, 0x3b, 0x64, 0x69, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x52, 0x65, 0x61, - 0x64, 0x3a, 0x20, 0x52, 0x65, 0x61, 0x64, 0x20, 0x64, 0x69, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x20, 0x70, 0x69, - 0x6e, 0x2e, 0x20, 0x40, 0x70, 0x69, 0x6e, 0x3a, 0x20, 0x50, 0x69, 0x6e, 0x20, 0x6e, 0x75, 0x6d, 0x62, 0x65, - 0x72, 0x2e, 0x20, 0x40, 0x72, 0x65, 0x74, 0x75, 0x72, 0x6e, 0x3a, 0x20, 0x50, 0x69, 0x6e, 0x20, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x2e, 0x00, 0x3a, 0x20, 0x48, 0x20, 0x42, 0x3b, 0x64, 0x69, 0x67, 0x69, 0x74, 0x61, 0x6c, - 0x57, 0x72, 0x69, 0x74, 0x65, 0x3a, 0x20, 0x57, 0x72, 0x69, 0x74, 0x65, 0x20, 0x74, 0x6f, 0x20, 0x61, 0x20, - 0x64, 0x69, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x20, 0x70, 0x69, 0x6e, 0x2e, 0x20, 0x40, 0x70, 0x69, 0x6e, 0x3a, - 0x20, 0x50, 0x69, 0x6e, 0x20, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x2e, 0x20, 0x40, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x3a, 0x20, 0x50, 0x69, 0x6e, 0x20, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x2e, 0x00, 0x00}; - - ParserSettings settings; - settings.startMethods = ParserSettings::SimpleMethod(&HostedTest::startMethods, this); - settings.startMethod = ParserSettings::SimpleMethod(&HostedTest::startMethod, this); - settings.methodName = ParserSettings::CharMethod(&HostedTest::methodName, this); - settings.endMethod = ParserSettings::SimpleMethod(&HostedTest::endMethod, this); - settings.endMethods = ParserSettings::SimpleMethod(&HostedTest::endMethods, this); - settings.state = ParserState::ready; + } + void execute() override + { TEST_CASE("simpleRPC::parse()") { - REQUIRE(parse(settings, packet, sizeof(packet)) == ParserResult::finished); - REQUIRE(commands.count() == 3); - REQUIRE(commands["digitalWrite"] == 2); - REQUIRE(commands["pinMode"] != 2); - REQUIRE(commands["pinMode"] == 0); + char packet[]{"simpleRPC\0" // protocol identifier + "\3\0\0" // version + "listen(4031); server->setTimeOut(USHRT_MAX); // disable connection timeout server->setKeepAlive(USHRT_MAX); // disable connection timeout @@ -108,10 +134,10 @@ class HostedTest : public TestGroup plusCommand, "plusCommand> Sum two numbers. @a: number one. @b: number two.", /* class methods */ // uint8_t TwoWire::begin(uint8_t sda, uint8_t scl) - pack(&theWire, (uint8_t(TheWire::*)(uint8_t,uint8_t))&TheWire::begin), "TheWire::begin> Starts two-wire communication. @sda: Data pin. @scl: Clock pin.", + makeTuple(&theWire, static_cast(&TheWire::begin)), "TheWire::begin> Starts two-wire communication. @sda: Data pin. @scl: Clock pin.", // void TheWire::begin() - pack(&theWire, (void(TheWire::*)())&TheWire::begin), "TheWire::begin> Starts two-wire communication.", - pack(&theWire, (uint8_t(TheWire::*)())&TheWire::getCalled), "TheWire::getCalled> Gets times called. @return: Result." + makeTuple(&theWire, static_cast(&TheWire::begin)), "TheWire::begin> Starts two-wire communication.", + makeTuple(&theWire, &TheWire::getCalled), "TheWire::getCalled> Gets times called. @return: Result." ); // clang-format on @@ -120,6 +146,7 @@ class HostedTest : public TestGroup // RPC Client + TcpClient client{false}; client.connect(WifiStation.getIP(), 4031); Hosted::Transport::TcpClientStream stream(client, 1024); @@ -128,8 +155,8 @@ class HostedTest : public TestGroup TEST_CASE("Client::getRemoteCommands()") { REQUIRE(hostedClient.getRemoteCommands() == true); - REQUIRE(hostedClient.getFunctionId("plusCommand") == 2); - REQUIRE(hostedClient.getFunctionId("uint8_t TheWire::begin(uint8_t, uint8_t)") == 3); + REQUIRE_EQ(hostedClient.getFunctionId("plusCommand"), 2); + REQUIRE_EQ(hostedClient.getFunctionId("uint8_t TheWire::begin(uint8_t, uint8_t)"), 3); } TEST_CASE("Client::send and wait()") @@ -137,7 +164,7 @@ class HostedTest : public TestGroup ElapseTimer timer; REQUIRE(hostedClient.send("plusCommand", uint8_t(3), uint16_t(2)) == true); - REQUIRE(hostedClient.wait() == 5); + REQUIRE_EQ(hostedClient.wait(), 5); debug_i("PlusCommand Roundtrip Time: %s", timer.elapsedTime().toString().c_str()); } @@ -145,48 +172,18 @@ class HostedTest : public TestGroup TEST_CASE("Client::send and check class method") { REQUIRE(hostedClient.send("uint8_t TheWire::begin(uint8_t, uint8_t)", uint8_t(3), uint8_t(3)) == true); - REQUIRE(hostedClient.wait() == 6); + REQUIRE_EQ(hostedClient.wait(), 6); REQUIRE(hostedClient.send("uint8_t TheWire::getCalled()") == true); - REQUIRE(hostedClient.wait() == 6); + REQUIRE_EQ(hostedClient.wait(), 6); } - } - -private: - RemoteCommands commands; - uint8_t methodPosition = 0; - String parsedCommand; - - TcpServer* server{nullptr}; - TcpClient client{false}; - Hosted::Transport::TcpClientStream* stream{nullptr}; - - void startMethods() - { - methodPosition = 0; - commands.clear(); - } - - void startMethod() - { - parsedCommand = ""; - } - void methodName(char ch) - { - parsedCommand += ch; - } - - void endMethod() - { - commands[parsedCommand] = methodPosition++; - } - - void endMethods() - { + server->shutdown(); } }; +} // namespace + void REGISTER_TEST(Hosted) { registerGroup(); From 75a748192cd3883030012f680a0b1bd5cba67598 Mon Sep 17 00:00:00 2001 From: Mike Date: Tue, 23 Apr 2024 09:57:35 +0100 Subject: [PATCH 054/128] Add `clang-tidy` support for static code analysis (#2648) This PR allows static code analysis using clang-tidy, suggested by #2616. Clang has more limited `constexpr` support than GCC so cannot parse some of the FlashString and NanoTime code without modification. However, these changes are only made when `__clang__` is defined and do not affect regular builds with GCC. This PR also includes some basic code fixes which clang identifies. There are a lot more to look at. To try it out, build a project with: ``` make CLANG_TIDY=clang-tidy ``` Add additional parameters like this: ``` make CLANG_TIDY="clang-tidy --fix --fix-errors" ``` Notes: - Don't use `-j` option as clang-tidy output and fixes don't get serialised correctly. - Settings are in the main `.clang-tidy` file. A custom version can be used and passed on the command line. - I've been using clang 17.0.6 https://releases.llvm.org/17.0.1/tools/clang/tools/extra/docs/clang-tidy/index.html - Only source files which haven't been built are inspected. So, to restrict which code gets processed built the entire application then 'clean' the relevant modules before proceeding with clang-tidy. - No object code is generated by clang. --- .clang-tidy | 70 +++++++++----- Sming/Arch/Esp32/Core/Interrupts.cpp | 2 +- Sming/Arch/Esp8266/Core/Interrupts.cpp | 2 +- .../Arch/Host/Components/driver/hw_timer.cpp | 2 - Sming/Arch/Host/Components/driver/uart.cpp | 5 - Sming/Arch/Host/Components/esp_hal/clk.c | 7 +- Sming/Arch/Host/Components/esp_hal/system.cpp | 4 +- Sming/Arch/Host/Components/esp_hal/tasks.cpp | 9 +- Sming/Arch/Host/Components/hostlib/hostlib.c | 10 +- Sming/Arch/Host/Components/hostlib/hostmsg.c | 2 +- Sming/Arch/Host/Components/hostlib/keyb.cpp | 41 ++++---- .../Arch/Host/Components/hostlib/sockets.cpp | 8 +- Sming/Arch/Host/Components/hostlib/sockets.h | 17 ++-- .../Arch/Host/Components/hostlib/startup.cpp | 17 ++-- .../Arch/Host/Components/hostlib/threads.cpp | 3 +- Sming/Arch/Host/Components/hostlib/threads.h | 2 +- Sming/Arch/Host/Core/HardwarePWM.cpp | 2 +- Sming/Arch/Host/Platform/RTC.cpp | 6 +- Sming/Arch/Host/app.mk | 4 + Sming/Arch/Rp2040/Core/Interrupts.cpp | 4 +- Sming/Components/FlashString | 2 +- .../Hosted/Transport/TcpServerTransport.h | 1 - Sming/Components/IFS | 2 +- .../src/Data/Stream/Base64OutputStream.cpp | 8 +- Sming/Components/Network/src/IpAddress.h | 6 +- .../src/Network/Http/HttpConnection.cpp | 3 +- .../Network/src/Network/NtpClient.cpp | 2 +- .../Network/src/Network/SmtpClient.cpp | 7 +- .../Network/src/Network/WebsocketClient.cpp | 1 + .../Storage/src/PartitionStream.cpp | 2 +- .../Components/Storage/src/PartitionTable.cpp | 7 +- .../Storage/src/include/Storage/Device.h | 5 + .../Storage/src/include/Storage/Partition.h | 9 ++ .../src/include/Storage/PartitionTable.h | 2 +- .../lwip/src/Arch/Host/Linux/lwip_arch.cpp | 8 +- .../Components/malloc_count/malloc_count.cpp | 6 +- Sming/Core/Data/CString.h | 6 ++ Sming/Core/Data/LinkedObjectList.h | 6 +- Sming/Core/Data/Stream/DataSourceStream.h | 2 +- .../Data/Stream/IFS/DirectoryTemplate.cpp | 10 +- Sming/Core/Data/Stream/ReadWriteStream.h | 2 +- Sming/Core/Data/Stream/SectionTemplate.cpp | 1 - Sming/Core/Data/Stream/SectionTemplate.h | 2 +- Sming/Core/Data/StreamTransformer.h | 2 +- Sming/Core/Data/Uuid.cpp | 4 +- Sming/Core/DateTime.cpp | 8 +- Sming/Core/HardwarePWM.h | 2 +- Sming/Core/Interrupts.h | 2 +- Sming/Core/NanoTime.h | 19 +++- Sming/Core/PolledTimer.h | 6 +- Sming/Core/Timer.h | 2 +- Sming/Libraries/Graphics | 2 +- .../OtaUpgrade/OtaUpgrade/BasicStream.h | 4 +- Sming/Libraries/SPI/src/SPISoft.cpp | 4 + Sming/Libraries/Spiffs/src/FileMeta.cpp | 2 +- .../Spiffs/src/include/IFS/SPIFFS/FileMeta.h | 2 +- Sming/Platform/System.cpp | 2 +- Sming/Services/HexDump/HexDump.cpp | 10 -- Sming/Services/HexDump/HexDump.h | 5 +- Sming/Services/Profiling/MinMaxTimes.h | 8 +- Sming/Services/Profiling/TaskStat.h | 2 + Sming/System/include/stringutil.h | 37 ++++---- Sming/System/m_printf.cpp | 2 +- Sming/System/stringutil.cpp | 42 ++++++--- Sming/Wiring/Countable.h | 9 ++ Sming/Wiring/Print.h | 12 ++- Sming/Wiring/Printable.h | 9 ++ Sming/Wiring/WMath.cpp | 27 ++---- Sming/Wiring/WMath.h | 21 ++++- Sming/Wiring/WString.h | 35 +++---- Sming/Wiring/WVector.h | 24 +++-- Sming/Wiring/WiringList.h | 17 +++- Sming/build.mk | 6 ++ Sming/component-wrapper.mk | 21 +++++ .../information/develop/clang-tools.rst | 93 +++++++++++++++---- samples/Basic_DateTime/app/application.cpp | 5 +- tests/HostTests/modules/ArduinoString.cpp | 2 +- tests/HostTests/modules/DateTime.cpp | 7 +- tests/HostTests/modules/Files.cpp | 78 ++++++++-------- tests/HostTests/modules/Libc.cpp | 3 +- .../modules/Network/Arch/Host/HttpRequest.cpp | 3 +- tests/HostTests/modules/Stream.cpp | 2 +- tests/HostTests/modules/TemplateStream.cpp | 5 +- tests/HostTests/modules/Timers.cpp | 4 +- tests/HostTests/modules/Wiring.cpp | 4 +- 85 files changed, 540 insertions(+), 331 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index bff0592d9d..d33851630e 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -1,28 +1,48 @@ --- -Checks: 'clang-diagnostic-*,clang-analyzer-*,-clang-analyzer-alpha*' -HeaderFilterRegex: '' -AnalyzeTemporaryDtors: false -User: slavey +Checks: + -* + bugprone-* + -bugprone-macro-parentheses + -bugprone-reserved-identifier + -bugprone-easily-swappable-parameters + clang-analyzer-* + performance-* + portability-* + cppcoreguidelines-* + -cppcoreguidelines-avoid-c-arrays + -cppcoreguidelines-avoid-do-while + -cppcoreguidelines-pro-bounds-array-to-pointer-decay + -cppcoreguidelines-macro-usage + -cppcoreguidelines-pro-type-reinterpret-cast + -cppcoreguidelines-pro-type-static-cast-downcast + -cppcoreguidelines-pro-type-vararg + -cppcoreguidelines-pro-bounds-pointer-arithmetic + -cppcoreguidelines-pro-bounds-constant-array-index + -cppcoreguidelines-pro-type-union-access + -cppcoreguidelines-avoid-const-or-ref-data-members + -cppcoreguidelines-non-private-member-variables-in-classes +HeaderFilterRegex: '.*' CheckOptions: - - key: google-readability-braces-around-statements.ShortStatementLines - value: '1' - - key: google-readability-function-size.StatementThreshold - value: '800' - - key: google-readability-namespace-comments.ShortNamespaceLines - value: '10' - - key: google-readability-namespace-comments.SpacesBeforeComments - value: '2' - - key: modernize-loop-convert.MaxCopySize - value: '16' - - key: modernize-loop-convert.MinConfidence - value: reasonable - - key: modernize-loop-convert.NamingStyle - value: CamelCase - - key: modernize-pass-by-value.IncludeStyle - value: llvm - - key: modernize-replace-auto-ptr.IncludeStyle - value: llvm - - key: modernize-use-nullptr.NullMacros - value: 'NULL' + - key: google-readability-braces-around-statements.ShortStatementLines + value: '1' + - key: google-readability-function-size.StatementThreshold + value: '800' + - key: google-readability-namespace-comments.ShortNamespaceLines + value: '10' + - key: google-readability-namespace-comments.SpacesBeforeComments + value: '2' + - key: modernize-loop-convert.MaxCopySize + value: '16' + - key: modernize-loop-convert.MinConfidence + value: reasonable + - key: modernize-loop-convert.NamingStyle + value: CamelCase + - key: modernize-pass-by-value.IncludeStyle + value: llvm + - key: modernize-replace-auto-ptr.IncludeStyle + value: llvm + - key: cppcoreguidelines-pro-type-static-cast-downcast.StrictMode + value: false + - key: cppcoreguidelines-explicit-virtual-functions.IgnoreDestructors + value: true ... - diff --git a/Sming/Arch/Esp32/Core/Interrupts.cpp b/Sming/Arch/Esp32/Core/Interrupts.cpp index 280addb4bf..889a39219f 100644 --- a/Sming/Arch/Esp32/Core/Interrupts.cpp +++ b/Sming/Arch/Esp32/Core/Interrupts.cpp @@ -89,7 +89,7 @@ void attachInterrupt(uint8_t pin, InterruptDelegate delegateFunction, GPIO_INT_T return; // WTF o_O } gpioInterruptsList[pin] = nullptr; - delegateFunctionList[pin] = delegateFunction; + delegateFunctionList[pin] = std::move(delegateFunction); attachInterruptHandler(pin, type); } diff --git a/Sming/Arch/Esp8266/Core/Interrupts.cpp b/Sming/Arch/Esp8266/Core/Interrupts.cpp index daafce7582..d8dfebc3ad 100644 --- a/Sming/Arch/Esp8266/Core/Interrupts.cpp +++ b/Sming/Arch/Esp8266/Core/Interrupts.cpp @@ -73,7 +73,7 @@ void attachInterrupt(uint8_t pin, InterruptDelegate delegateFunction, GPIO_INT_T return; // WTF o_O } gpioInterruptsList[pin] = nullptr; - delegateFunctionList[pin] = delegateFunction; + delegateFunctionList[pin] = std::move(delegateFunction); attachInterruptHandler(pin, type); } diff --git a/Sming/Arch/Host/Components/driver/hw_timer.cpp b/Sming/Arch/Host/Components/driver/hw_timer.cpp index 0ceacae8fa..ac52f34a50 100644 --- a/Sming/Arch/Host/Components/driver/hw_timer.cpp +++ b/Sming/Arch/Host/Components/driver/hw_timer.cpp @@ -120,7 +120,6 @@ class CTimerThread : public CThread private: typedef std::ratio base_ticks_per_us; uint32_t divisor = 1; - uint32_t frequency = HW_TIMER_BASE_CLK; uint64_t start_time = 0; uint64_t interval = 0; // In microseconds CSemaphore sem; // Signals state change @@ -130,7 +129,6 @@ class CTimerThread : public CThread State state = stopped; hw_timer_source_type_t source_type = TIMER_FRC1_SOURCE; - unsigned irq_level = 1; struct { hw_timer_callback_t func = nullptr; void* arg = nullptr; diff --git a/Sming/Arch/Host/Components/driver/uart.cpp b/Sming/Arch/Host/Components/driver/uart.cpp index d67e386020..9294a03355 100644 --- a/Sming/Arch/Host/Components/driver/uart.cpp +++ b/Sming/Arch/Host/Components/driver/uart.cpp @@ -73,11 +73,6 @@ void notify(smg_uart_t* uart, smg_uart_notify_code_t code) } } -__forceinline bool smg_uart_isr_enabled(uint8_t nr) -{ - return bitRead(isrMask, nr); -} - } // namespace smg_uart_t* smg_uart_get_uart(uint8_t uart_nr) diff --git a/Sming/Arch/Host/Components/esp_hal/clk.c b/Sming/Arch/Host/Components/esp_hal/clk.c index 6021a3e3b5..fa9231f623 100644 --- a/Sming/Arch/Host/Components/esp_hal/clk.c +++ b/Sming/Arch/Host/Components/esp_hal/clk.c @@ -11,16 +11,13 @@ static uint32_t base_ccount; */ static uint32_t get_ccount(uint64_t nanos) { - uint32_t ccount; if(base_nanos == 0) { base_nanos = nanos; base_ccount = nanos / cpu_frequency; - ccount = base_ccount; - } else { - ccount = base_ccount + cpu_frequency * ((nanos - base_nanos) / 1000); + return base_ccount; } - return ccount; + return base_ccount + cpu_frequency * ((nanos - base_nanos) / 1000); } bool system_update_cpu_freq(uint8_t freq) diff --git a/Sming/Arch/Host/Components/esp_hal/system.cpp b/Sming/Arch/Host/Components/esp_hal/system.cpp index 02cec4e44a..ed4159fb10 100644 --- a/Sming/Arch/Host/Components/esp_hal/system.cpp +++ b/Sming/Arch/Host/Components/esp_hal/system.cpp @@ -27,7 +27,7 @@ static uint64_t initTime() timeref.startTicks = os_get_nanoseconds(); #endif - timeval tv; + timeval tv{}; gettimeofday(&tv, nullptr); return (1000000ULL * tv.tv_sec) + tv.tv_usec; } @@ -41,7 +41,7 @@ uint64_t os_get_nanoseconds() QueryPerformanceCounter(&count); return timeref.countsPerNanosecond * uint64_t(count.QuadPart - timeref.startCount.QuadPart); #else - timespec ts; + timespec ts{}; clock_gettime(CLOCK_MONOTONIC, &ts); return (1000000000ULL * ts.tv_sec) + ts.tv_nsec - timeref.startTicks; #endif diff --git a/Sming/Arch/Host/Components/esp_hal/tasks.cpp b/Sming/Arch/Host/Components/esp_hal/tasks.cpp index 437d9a6b08..bd242df133 100644 --- a/Sming/Arch/Host/Components/esp_hal/tasks.cpp +++ b/Sming/Arch/Host/Components/esp_hal/tasks.cpp @@ -9,11 +9,8 @@ class TaskQueue { public: TaskQueue(os_task_t callback, os_event_t* events, uint8_t length) + : callback(callback), events(events), length(length) { - this->callback = callback; - this->events = events; - this->length = length; - read = count = 0; } bool post(os_signal_t sig, os_param_t par) @@ -45,8 +42,8 @@ class TaskQueue static CMutex mutex; os_task_t callback; os_event_t* events; - uint8_t read; - uint8_t count; + uint8_t read{0}; + uint8_t count{0}; uint8_t length; }; diff --git a/Sming/Arch/Host/Components/hostlib/hostlib.c b/Sming/Arch/Host/Components/hostlib/hostlib.c index 93d6d00941..7e43603df9 100644 --- a/Sming/Arch/Host/Components/hostlib/hostlib.c +++ b/Sming/Arch/Host/Components/hostlib/hostlib.c @@ -35,14 +35,12 @@ size_t getHostAppDir(char* path, size_t bufSize) return 0; } - size_t len; - char sep; #ifdef __WIN32 - len = GetModuleFileName(NULL, path, bufSize); - sep = '\\'; + size_t len = GetModuleFileName(NULL, path, bufSize); + char sep = '\\'; #else - len = readlink("/proc/self/exe", path, bufSize - 1); - sep = '/'; + size_t len = readlink("/proc/self/exe", path, bufSize - 1); + char sep = '/'; #endif path[len] = '\0'; char* p = strrchr(path, sep); diff --git a/Sming/Arch/Host/Components/hostlib/hostmsg.c b/Sming/Arch/Host/Components/hostlib/hostmsg.c index 0c561b0fd4..14b8311cbc 100644 --- a/Sming/Arch/Host/Components/hostlib/hostmsg.c +++ b/Sming/Arch/Host/Components/hostlib/hostmsg.c @@ -56,7 +56,7 @@ void host_printf(const char* fmt, ...) void host_printfp(const char* fmt, const char* pretty_function, ...) { - size_t len; + size_t len = 0; const char* name = get_method_name(pretty_function, &len); va_list args; diff --git a/Sming/Arch/Host/Components/hostlib/keyb.cpp b/Sming/Arch/Host/Components/hostlib/keyb.cpp index ed1b418072..579f48e4df 100644 --- a/Sming/Arch/Host/Components/hostlib/keyb.cpp +++ b/Sming/Arch/Host/Components/hostlib/keyb.cpp @@ -34,9 +34,14 @@ #include #include -static bool g_orig_values_saved, g_values_changed; -static struct termios g_orig_attr; -static int g_orig_flags; +namespace +{ +bool g_orig_values_saved; +bool g_values_changed; +struct termios g_orig_attr; +int g_orig_flags; + +} // namespace #endif @@ -62,9 +67,9 @@ class CKeycode int add(int c); private: - char m_esc; - char m_buffer[32]; - unsigned m_count; + char m_esc{}; + char m_buffer[32]{}; + unsigned m_count{}; void push(char c) { @@ -228,14 +233,16 @@ int CKeycode::add(int c) void keyb_restore() { #ifndef __WIN32 - if(g_values_changed) { - static struct termios attr = g_orig_attr; - attr.c_lflag |= ICANON | ECHO; - tcsetattr(STDIN_FILENO, TCSANOW, &attr); - - (void)fcntl(0, F_SETFL, g_orig_flags); - g_values_changed = false; + if(!g_values_changed) { + return; } + + static struct termios attr = g_orig_attr; + attr.c_lflag |= ICANON | ECHO; + tcsetattr(STDIN_FILENO, TCSANOW, &attr); + + (void)fcntl(0, F_SETFL, g_orig_flags); + g_values_changed = false; #endif } @@ -271,14 +278,16 @@ int getch() int getkey() { static CKeycode kc; - int c; + int c = 0; for(;;) { c = getch(); - if(c == KEY_NONE) + if(c == KEY_NONE) { break; + } c = kc.add(c); - if(c != KEY_NONE) + if(c != KEY_NONE) { break; + } } return c; } diff --git a/Sming/Arch/Host/Components/hostlib/sockets.cpp b/Sming/Arch/Host/Components/hostlib/sockets.cpp index 4dcc740289..670c6670a1 100644 --- a/Sming/Arch/Host/Components/hostlib/sockets.cpp +++ b/Sming/Arch/Host/Components/hostlib/sockets.cpp @@ -123,13 +123,12 @@ std::string socket_strerror() { char buf[256]; buf[0] = '\0'; - int ErrorCode; #ifdef __WIN32 - ErrorCode = WSAGetLastError(); + int ErrorCode = WSAGetLastError(); FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS | FORMAT_MESSAGE_ARGUMENT_ARRAY, nullptr, ErrorCode, 0, buf, sizeof(buf), nullptr); #else - ErrorCode = errno; + int ErrorCode = errno; char* res = strerror_r(ErrorCode, buf, sizeof(buf)); if(res == nullptr) { strcpy(buf, "Unknown"); @@ -438,7 +437,8 @@ CSocket* CServerSocket::try_connect() return nullptr; } - struct sockaddr sa; + struct sockaddr sa { + }; host_socklen_t len = sizeof(sa); int fd = ::accept(m_fd, &sa, &len); if(fd < 0) { diff --git a/Sming/Arch/Host/Components/hostlib/sockets.h b/Sming/Arch/Host/Components/hostlib/sockets.h index 83570d30ab..ffc3437a09 100644 --- a/Sming/Arch/Host/Components/hostlib/sockets.h +++ b/Sming/Arch/Host/Components/hostlib/sockets.h @@ -58,7 +58,7 @@ class CSockAddr struct sockaddr sa; struct sockaddr_in in4; // AF_INET struct sockaddr_in6 in6; // AF_INET6 - } m_addr; + } m_addr{}; public: CSockAddr() @@ -113,12 +113,12 @@ class CSocket assign(fd, addr); } - virtual ~CSocket() + ~CSocket() { close(); } - virtual void close(); + void close(); bool setblocking(bool block); bool bind(const CSockAddr& sa); @@ -213,11 +213,16 @@ class CSocketList : public std::vector class CServerSocket : public CSocket { public: - CServerSocket(int type = SOCK_STREAM) : CSocket(type), m_max_connections(1) + CServerSocket(int type = SOCK_STREAM) : CSocket(type) { } - void close() override + virtual ~CServerSocket() + { + close(); + } + + void close() { m_clients.closeall(); CSocket::close(); @@ -235,6 +240,6 @@ class CServerSocket : public CSocket } private: - unsigned m_max_connections; + unsigned m_max_connections{1}; CSocketList m_clients; }; diff --git a/Sming/Arch/Host/Components/hostlib/startup.cpp b/Sming/Arch/Host/Components/hostlib/startup.cpp index 4286327055..3fce4bec04 100644 --- a/Sming/Arch/Host/Components/hostlib/startup.cpp +++ b/Sming/Arch/Host/Components/hostlib/startup.cpp @@ -86,7 +86,7 @@ static size_t parse_flash_size(const char* str) if(str == nullptr) { return 0; } - char* tail; + char* tail = nullptr; long res = strtol(str, &tail, 0); if(res < 0) { return 0; @@ -133,21 +133,22 @@ int main(int argc, char* argv[]) struct Config { int pause{-1}; int exitpause{-1}; - int loopcount; - uint8_t cpulimit; - bool initonly; + int loopcount{}; + uint8_t cpulimit{}; + bool initonly{}; bool enable_network{true}; - UartServer::Config uart; - FlashmemConfig flash; + UartServer::Config uart{}; + FlashmemConfig flash{}; #ifndef DISABLE_NETWORK - struct lwip_param lwip; + struct lwip_param lwip { + }; #endif }; static Config config{}; int uart_num{-1}; option_tag_t opt; - const char* arg; + const char* arg = nullptr; while((opt = get_option(argc, argv, arg)) != opt_none) { switch(opt) { case opt_help: diff --git a/Sming/Arch/Host/Components/hostlib/threads.cpp b/Sming/Arch/Host/Components/hostlib/threads.cpp index 83aa68ca08..83d0da5e14 100644 --- a/Sming/Arch/Host/Components/hostlib/threads.cpp +++ b/Sming/Arch/Host/Components/hostlib/threads.cpp @@ -136,7 +136,8 @@ void CMutex::unlock() bool CSemaphore::timedwait(unsigned us) { - struct timespec ts; + struct timespec ts { + }; clock_gettime(CLOCK_REALTIME, &ts); uint64_t ns = ts.tv_nsec + uint64_t(us) * 1000; ts.tv_sec += ns / 1000000000; diff --git a/Sming/Arch/Host/Components/hostlib/threads.h b/Sming/Arch/Host/Components/hostlib/threads.h index b97e708f65..b4934c4940 100644 --- a/Sming/Arch/Host/Components/hostlib/threads.h +++ b/Sming/Arch/Host/Components/hostlib/threads.h @@ -133,7 +133,7 @@ class CSemaphore } private: - sem_t m_sem; + sem_t m_sem{}; }; /** diff --git a/Sming/Arch/Host/Core/HardwarePWM.cpp b/Sming/Arch/Host/Core/HardwarePWM.cpp index d2eaf908f7..38ff4b539c 100644 --- a/Sming/Arch/Host/Core/HardwarePWM.cpp +++ b/Sming/Arch/Host/Core/HardwarePWM.cpp @@ -25,7 +25,7 @@ #include -HardwarePWM::HardwarePWM(uint8_t* pins, uint8_t no_of_pins) : channel_count(no_of_pins), maxduty(0) +HardwarePWM::HardwarePWM(uint8_t* pins, uint8_t no_of_pins) : channel_count(no_of_pins) { } diff --git a/Sming/Arch/Host/Platform/RTC.cpp b/Sming/Arch/Host/Platform/RTC.cpp index 6d93b702be..b9f1b625f4 100644 --- a/Sming/Arch/Host/Platform/RTC.cpp +++ b/Sming/Arch/Host/Platform/RTC.cpp @@ -25,7 +25,8 @@ RtcClass::RtcClass() uint64_t RtcClass::getRtcNanoseconds() { - struct timeval tv; + struct timeval tv { + }; gettimeofday(&tv, nullptr); uint64_t usecs = (tv.tv_sec * 1000000ULL) + (uint32_t)tv.tv_usec; return usecs * 1000; @@ -33,7 +34,8 @@ uint64_t RtcClass::getRtcNanoseconds() uint32_t RtcClass::getRtcSeconds() { - struct timeval tv; + struct timeval tv { + }; gettimeofday(&tv, nullptr); return tv.tv_sec + timeDiff; } diff --git a/Sming/Arch/Host/app.mk b/Sming/Arch/Host/app.mk index a2a92ebb85..4af8f3fe53 100644 --- a/Sming/Arch/Host/app.mk +++ b/Sming/Arch/Host/app.mk @@ -25,11 +25,15 @@ endif application: $(TARGET_OUT_0) $(TARGET_OUT_0): $(COMPONENTS_AR) +ifdef CLANG_TIDY + $(info Skipping link step for clang-tidy) +else $(info $(notdir $(PROJECT_DIR)): Linking $@) $(Q) $(LD) $(addprefix -L,$(LIBDIRS)) $(LDFLAGS) -Wl,--start-group $(COMPONENTS_AR) $(addprefix -l,$(LIBS)) -Wl,--end-group -o $@ $(Q) $(call WriteFirmwareConfigFile,$@) $(Q) $(MEMANALYZER) $@ > $(FW_MEMINFO) $(Q) cat $(FW_MEMINFO) +endif ##@Tools diff --git a/Sming/Arch/Rp2040/Core/Interrupts.cpp b/Sming/Arch/Rp2040/Core/Interrupts.cpp index 133c22a939..2e839d9e86 100644 --- a/Sming/Arch/Rp2040/Core/Interrupts.cpp +++ b/Sming/Arch/Rp2040/Core/Interrupts.cpp @@ -47,7 +47,7 @@ struct Handler { void setDelegate(InterruptDelegate delegate) { reset(); - this->delegate = new InterruptDelegate(delegate); + this->delegate = new InterruptDelegate(std::move(delegate)); type = Type::delegate; } }; @@ -95,7 +95,7 @@ void attachInterrupt(uint8_t pin, InterruptDelegate delegateFunction, GPIO_INT_T CHECK_PIN(pin) auto& handler = handlers[pin]; - handler.setDelegate(delegateFunction); + handler.setDelegate(std::move(delegateFunction)); attachInterruptHandler(pin, type); } diff --git a/Sming/Components/FlashString b/Sming/Components/FlashString index d7a129d6a6..8f91c37d6b 160000 --- a/Sming/Components/FlashString +++ b/Sming/Components/FlashString @@ -1 +1 @@ -Subproject commit d7a129d6a6b2f3e8e9b90b143556fdcb8587e77f +Subproject commit 8f91c37d6bc87f1109084c55d70aac9695e33d30 diff --git a/Sming/Components/Hosted/include/Hosted/Transport/TcpServerTransport.h b/Sming/Components/Hosted/include/Hosted/Transport/TcpServerTransport.h index bfd387ea8b..ecd81d98a0 100644 --- a/Sming/Components/Hosted/include/Hosted/Transport/TcpServerTransport.h +++ b/Sming/Components/Hosted/include/Hosted/Transport/TcpServerTransport.h @@ -37,7 +37,6 @@ class TcpServerTransport : public TcpTransport if(stream == nullptr) { map[key] = stream = new TcpClientStream(client); client.setReceiveDelegate(TcpClientDataDelegate(&TcpServerTransport::process, this)); - stream = map[key]; } if(!stream->push(reinterpret_cast(data), size)) { diff --git a/Sming/Components/IFS b/Sming/Components/IFS index c72b265e53..1b4e2f04d4 160000 --- a/Sming/Components/IFS +++ b/Sming/Components/IFS @@ -1 +1 @@ -Subproject commit c72b265e53ac215a979eff907d216eab068987df +Subproject commit 1b4e2f04d4308f0dc966e4d59ee2d6fe5ffd4c2e diff --git a/Sming/Components/Network/src/Data/Stream/Base64OutputStream.cpp b/Sming/Components/Network/src/Data/Stream/Base64OutputStream.cpp index 5bc2df6e0c..8214d373b3 100644 --- a/Sming/Components/Network/src/Data/Stream/Base64OutputStream.cpp +++ b/Sming/Components/Network/src/Data/Stream/Base64OutputStream.cpp @@ -23,12 +23,10 @@ Base64OutputStream::Base64OutputStream(IDataSourceStream* stream, size_t resultS size_t Base64OutputStream::transform(const uint8_t* source, size_t sourceLength, uint8_t* target, size_t targetLength) { - size_t count; if(sourceLength == 0) { - count = base64_encode_blockend((char*)target, &state); - } else { - count = base64_encode_block((const char*)source, sourceLength, (char*)target, &state); + return base64_encode_blockend(reinterpret_cast(target), &state); } - return count; + return base64_encode_block(reinterpret_cast(source), sourceLength, reinterpret_cast(target), + &state); } diff --git a/Sming/Components/Network/src/IpAddress.h b/Sming/Components/Network/src/IpAddress.h index c092e8f356..f25ae90edb 100644 --- a/Sming/Components/Network/src/IpAddress.h +++ b/Sming/Components/Network/src/IpAddress.h @@ -64,14 +64,12 @@ class IpAddress ip_addr_set_ip4_u32(&this->address, address); } - IpAddress(ip_addr_t& addr) + IpAddress(ip_addr_t& addr) : address(addr) { - address = addr; } - IpAddress(const ip_addr_t& addr) + IpAddress(const ip_addr_t& addr) : address(addr) { - address = addr; } #if LWIP_VERSION_MAJOR == 2 && LWIP_IPV6 diff --git a/Sming/Components/Network/src/Network/Http/HttpConnection.cpp b/Sming/Components/Network/src/Network/Http/HttpConnection.cpp index a2cd6f9eb0..6224d63f1c 100644 --- a/Sming/Components/Network/src/Network/Http/HttpConnection.cpp +++ b/Sming/Components/Network/src/Network/Http/HttpConnection.cpp @@ -206,8 +206,7 @@ bool HttpConnection::isActive() return false; } - struct tcp_pcb* pcb; - for(pcb = tcp_active_pcbs; pcb != nullptr; pcb = pcb->next) { + for(auto pcb = tcp_active_pcbs; pcb != nullptr; pcb = pcb->next) { if(tcp == pcb) { return true; } diff --git a/Sming/Components/Network/src/Network/NtpClient.cpp b/Sming/Components/Network/src/Network/NtpClient.cpp index e6d7b7dc6f..8b977a1a93 100644 --- a/Sming/Components/Network/src/Network/NtpClient.cpp +++ b/Sming/Components/Network/src/Network/NtpClient.cpp @@ -14,12 +14,12 @@ #include NtpClient::NtpClient(const String& reqServer, unsigned reqIntervalSeconds, NtpTimeResultDelegate delegateFunction) + : delegateCompleted(delegateFunction) { // Setup timer, but don't start it timer.setCallback(TimerDelegate(&NtpClient::requestTime, this)); this->server = reqServer ?: NTP_DEFAULT_SERVER; - this->delegateCompleted = delegateFunction; if(!delegateFunction) { autoUpdateSystemClock = true; } diff --git a/Sming/Components/Network/src/Network/SmtpClient.cpp b/Sming/Components/Network/src/Network/SmtpClient.cpp index 5f44450ba2..a604133bd1 100644 --- a/Sming/Components/Network/src/Network/SmtpClient.cpp +++ b/Sming/Components/Network/src/Network/SmtpClient.cpp @@ -365,15 +365,16 @@ int SmtpClient::smtpParse(char* buffer, size_t len) code[codeLength++] = currentByte; ADVANCE; continue; - } else if(codeLength == 3) { + } + + if(codeLength == 3) { code[codeLength] = '\0'; if(currentByte != ' ' && currentByte != '-') { // the code must be followed by space or minus return 0; } - char* tmp; - codeValue = strtol(code, &tmp, 10); + codeValue = strtol(code, nullptr, 10); isLastLine = (currentByte == ' '); codeLength++; ADVANCE; diff --git a/Sming/Components/Network/src/Network/WebsocketClient.cpp b/Sming/Components/Network/src/Network/WebsocketClient.cpp index d3643c0df8..fd464252eb 100644 --- a/Sming/Components/Network/src/Network/WebsocketClient.cpp +++ b/Sming/Components/Network/src/Network/WebsocketClient.cpp @@ -27,6 +27,7 @@ class WebsocketClientConnection : public HttpClientConnection state = eHCS_Ready; } + // IMPORTANT: Skip HttpClientConnection implementation return HttpConnection::onConnected(err); } }; diff --git a/Sming/Components/Storage/src/PartitionStream.cpp b/Sming/Components/Storage/src/PartitionStream.cpp index 07980de01b..338aef21af 100644 --- a/Sming/Components/Storage/src/PartitionStream.cpp +++ b/Sming/Components/Storage/src/PartitionStream.cpp @@ -41,7 +41,7 @@ int PartitionStream::seekFrom(int offset, SeekOrigin origin) } readPos = newPos; - return readPos; + return int(readPos); } size_t PartitionStream::write(const uint8_t* data, size_t length) diff --git a/Sming/Components/Storage/src/PartitionTable.cpp b/Sming/Components/Storage/src/PartitionTable.cpp index e62612bf62..793c6acb65 100644 --- a/Sming/Components/Storage/src/PartitionTable.cpp +++ b/Sming/Components/Storage/src/PartitionTable.cpp @@ -17,13 +17,12 @@ namespace Storage void PartitionTable::load(const esp_partition_info_t* entry, unsigned count) { mEntries.clear(); - for(unsigned i = 0; i < count; ++i) { - auto& e = entry[i]; + for(; count != 0; --count, ++entry) { // name may not be zero-terminated char name[Partition::nameSize + 1]; - memcpy(name, e.name, Partition::nameSize); + memcpy(name, entry->name, Partition::nameSize); name[Partition::nameSize] = '\0'; - add(name, {e.type, e.subtype}, e.offset, e.size, e.flags); + add(name, {entry->type, entry->subtype}, entry->offset, entry->size, entry->flags); } } diff --git a/Sming/Components/Storage/src/include/Storage/Device.h b/Sming/Components/Storage/src/include/Storage/Device.h index b05017e069..e8d77162c2 100644 --- a/Sming/Components/Storage/src/include/Storage/Device.h +++ b/Sming/Components/Storage/src/include/Storage/Device.h @@ -50,6 +50,11 @@ class Device : public LinkedObjectTemplate { } + Device(const Device&) = delete; + Device(Device&&) = delete; + Device& operator=(const Device&) = delete; + Device& operator=(Device&&) = delete; + ~Device(); bool operator==(const String& name) const diff --git a/Sming/Components/Storage/src/include/Storage/Partition.h b/Sming/Components/Storage/src/include/Storage/Partition.h index 535b0fd4cf..1a2e4c0d0e 100644 --- a/Sming/Components/Storage/src/include/Storage/Partition.h +++ b/Sming/Components/Storage/src/include/Storage/Partition.h @@ -223,10 +223,19 @@ class Partition { } + Partition(Partition&& other) = default; + Partition(Device& device, const Info& info) : mDevice(&device), mPart(&info) { } + ~Partition() + { + } + + Partition& operator=(const Partition& other) = default; + Partition& operator=(Partition&& other) = default; + /** * @name Confirm partition is of the expected type * @{ diff --git a/Sming/Components/Storage/src/include/Storage/PartitionTable.h b/Sming/Components/Storage/src/include/Storage/PartitionTable.h index 2497178f76..8edf7a05da 100644 --- a/Sming/Components/Storage/src/include/Storage/PartitionTable.h +++ b/Sming/Components/Storage/src/include/Storage/PartitionTable.h @@ -70,7 +70,7 @@ class PartitionTable */ Partition find(uint32_t address) const { - return *std::find_if(begin(), end(), [address](Partition part) { return part.contains(address); }); + return *std::find_if(begin(), end(), [address](const Partition& part) { return part.contains(address); }); } /** diff --git a/Sming/Components/lwip/src/Arch/Host/Linux/lwip_arch.cpp b/Sming/Components/lwip/src/Arch/Host/Linux/lwip_arch.cpp index c8ae59f16c..a94d453f65 100644 --- a/Sming/Components/lwip/src/Arch/Host/Linux/lwip_arch.cpp +++ b/Sming/Components/lwip/src/Arch/Host/Linux/lwip_arch.cpp @@ -16,6 +16,11 @@ * If not, see . * ****/ + +// Use implementations defined in +#define lwip_htonl nthol +#define lwip_htons htons + #include "../lwip_arch.h" #include #include @@ -40,7 +45,8 @@ void getMacAddress(const char* ifname, uint8_t hwaddr[6]) return; } - struct ifreq ifr = {0}; + struct ifreq ifr { + }; ifr.ifr_addr.sa_family = AF_INET; strncpy(ifr.ifr_name, ifname, IFNAMSIZ); ifr.ifr_name[IFNAMSIZ - 1] = '\0'; diff --git a/Sming/Components/malloc_count/malloc_count.cpp b/Sming/Components/malloc_count/malloc_count.cpp index f094888c72..51ad1e638a 100644 --- a/Sming/Components/malloc_count/malloc_count.cpp +++ b/Sming/Components/malloc_count/malloc_count.cpp @@ -195,7 +195,7 @@ void setAllocLimit(size_t maxBytes) /* user function to supply a memory profile callback */ void setCallback(Callback callback) { - userCallback = callback; + userCallback = std::move(callback); } #ifdef ENABLE_MALLOC_COUNT @@ -367,7 +367,7 @@ void* operator new(size_t size) return mc_malloc(size); } -void* operator new(size_t size, const std::nothrow_t&) +void* operator new(size_t size, const std::nothrow_t&) noexcept { return mc_malloc(size); } @@ -377,7 +377,7 @@ void* operator new[](size_t size) return mc_malloc(size); } -void* operator new[](size_t size, const std::nothrow_t&) +void* operator new[](size_t size, const std::nothrow_t&) noexcept { return mc_malloc(size); } diff --git a/Sming/Core/Data/CString.h b/Sming/Core/Data/CString.h index 4335b95bc7..2c895683ff 100644 --- a/Sming/Core/Data/CString.h +++ b/Sming/Core/Data/CString.h @@ -33,6 +33,8 @@ class CString : public std::unique_ptr assign(src.get()); } + CString(CString&& other) = default; + CString(const String& src) { assign(src); @@ -43,6 +45,8 @@ class CString : public std::unique_ptr assign(src); } + ~CString() = default; + void assign(const String& src) { if(src) { @@ -74,6 +78,8 @@ class CString : public std::unique_ptr return *this; } + CString& operator=(CString&& src) = default; + CString& operator=(const String& src) { assign(src); diff --git a/Sming/Core/Data/LinkedObjectList.h b/Sming/Core/Data/LinkedObjectList.h index b194efa681..166b8de6ae 100644 --- a/Sming/Core/Data/LinkedObjectList.h +++ b/Sming/Core/Data/LinkedObjectList.h @@ -103,12 +103,12 @@ template class LinkedObjectListTemplate : public LinkedObj ObjectType* head() { - return reinterpret_cast(mHead); + return static_cast(mHead); } const ObjectType* head() const { - return reinterpret_cast(mHead); + return static_cast(mHead); } Iterator begin() @@ -153,7 +153,7 @@ template class LinkedObjectListTemplate : public LinkedObj ObjectType* pop() { - return reinterpret_cast(LinkedObjectList::pop()); + return static_cast(LinkedObjectList::pop()); } size_t count() const diff --git a/Sming/Core/Data/Stream/DataSourceStream.h b/Sming/Core/Data/Stream/DataSourceStream.h index a57e12400d..8fef7842ab 100644 --- a/Sming/Core/Data/Stream/DataSourceStream.h +++ b/Sming/Core/Data/Stream/DataSourceStream.h @@ -117,7 +117,7 @@ class IDataSourceStream : public Stream * @brief Return the total length of the stream * @retval int -1 is returned when the size cannot be determined */ - virtual int available() + int available() override { return -1; } diff --git a/Sming/Core/Data/Stream/IFS/DirectoryTemplate.cpp b/Sming/Core/Data/Stream/IFS/DirectoryTemplate.cpp index ca667ef846..15fe7e4cdc 100644 --- a/Sming/Core/Data/Stream/IFS/DirectoryTemplate.cpp +++ b/Sming/Core/Data/Stream/IFS/DirectoryTemplate.cpp @@ -51,14 +51,14 @@ String DirectoryTemplate::getValue(const char* name) return statValid ? String(s.id) : nullptr; case Field::name: - if(statValid) { - value.setString(s.name.buffer, s.name.length); - formatter().escape(value); - return value; - } else { + if(!statValid) { return nullptr; } + value.setString(s.name.buffer, s.name.length); + formatter().escape(value); + return value; + case Field::modified: return statValid ? DateTime(s.mtime).toISO8601() : nullptr; diff --git a/Sming/Core/Data/Stream/ReadWriteStream.h b/Sming/Core/Data/Stream/ReadWriteStream.h index 0cdef21026..f4fce5fd5a 100644 --- a/Sming/Core/Data/Stream/ReadWriteStream.h +++ b/Sming/Core/Data/Stream/ReadWriteStream.h @@ -34,7 +34,7 @@ class ReadWriteStream : public IDataSourceStream * uses this as the core output method so descendants are required * to implement it */ - virtual size_t write(const uint8_t* buffer, size_t size) = 0; + size_t write(const uint8_t* buffer, size_t size) override = 0; /** @brief Copy data from a source stream * @param source Stream to read data from diff --git a/Sming/Core/Data/Stream/SectionTemplate.cpp b/Sming/Core/Data/Stream/SectionTemplate.cpp index 8f1d179a67..827f7ab497 100644 --- a/Sming/Core/Data/Stream/SectionTemplate.cpp +++ b/Sming/Core/Data/Stream/SectionTemplate.cpp @@ -248,7 +248,6 @@ class ArgList SectionTemplate::SectionTemplate(IDataSourceStream* source, uint8_t maxSections) : TemplateStream(§ionStream, false), sectionStream(source, maxSections) { - setFormatter(Format::standard); sectionStream.onNextSection([this]() { seekFrom(0, SeekOrigin::Start); }); sectionStream.onNextRecord(SectionStream::NextRecord(&SectionTemplate::nextRecord, this)); // Level 0 is always enabled diff --git a/Sming/Core/Data/Stream/SectionTemplate.h b/Sming/Core/Data/Stream/SectionTemplate.h index 77d82b0612..1392a4c8de 100644 --- a/Sming/Core/Data/Stream/SectionTemplate.h +++ b/Sming/Core/Data/Stream/SectionTemplate.h @@ -217,7 +217,7 @@ class SectionTemplate : public TemplateStream String elseTag(); SectionStream sectionStream; - Formatter* activeFormatter; + Formatter* activeFormatter{&Format::standard}; GetValue getValueCallback; NextRecord nextRecordCallback; BitSet32 conditionalFlags; // Enable state for each level diff --git a/Sming/Core/Data/StreamTransformer.h b/Sming/Core/Data/StreamTransformer.h index 48050577ee..55beb3278d 100644 --- a/Sming/Core/Data/StreamTransformer.h +++ b/Sming/Core/Data/StreamTransformer.h @@ -43,7 +43,7 @@ class StreamTransformer : public IDataSourceStream return -1; } - bool isValid() const + bool isValid() const override { return sourceStream && sourceStream->isValid(); } diff --git a/Sming/Core/Data/Uuid.cpp b/Sming/Core/Data/Uuid.cpp index 4b4b157ad8..83f88c2f80 100644 --- a/Sming/Core/Data/Uuid.cpp +++ b/Sming/Core/Data/Uuid.cpp @@ -34,8 +34,8 @@ bool Uuid::operator==(const Uuid& other) const bool Uuid::generate(MacAddress mac) { - uint8_t version = 1; // DCE version - uint8_t variant = 2; // DCE variant + const uint8_t version = 1; // DCE version + const uint8_t variant = 2; // DCE variant uint16_t clock_seq = os_random(); uint32_t time; if(SystemClock.isSet()) { diff --git a/Sming/Core/DateTime.cpp b/Sming/Core/DateTime.cpp index f4d696d81f..b4636cc16a 100644 --- a/Sming/Core/DateTime.cpp +++ b/Sming/Core/DateTime.cpp @@ -44,10 +44,10 @@ union FourDigitName { } }; -const FourDigitName isoDayNames[7] PROGMEM = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; +const FourDigitName isoDayNames[7] PROGMEM{{"Sun"}, {"Mon"}, {"Tue"}, {"Wed"}, {"Thu"}, {"Fri"}, {"Sat"}}; -const FourDigitName isoMonthNames[12] PROGMEM = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", - "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; +const FourDigitName isoMonthNames[12] PROGMEM{{"Jan"}, {"Feb"}, {"Mar"}, {"Apr"}, {"May"}, {"Jun"}, + {"Jul"}, {"Aug"}, {"Sep"}, {"Oct"}, {"Nov"}, {"Dec"}}; /* * @brief Match a day or month name against a list of values and set the required value @@ -61,7 +61,7 @@ const FourDigitName isoMonthNames[12] PROGMEM = {"Jan", "Feb", "Mar", "Apr", "Ma */ bool matchName(const char*& ptr, uint8_t& value, const FourDigitName isoNames[], unsigned nameCount) { - FourDigitName name{ptr[0], ptr[1], ptr[2]}; + FourDigitName name{{ptr[0], ptr[1], ptr[2]}}; for(unsigned i = 0; i < nameCount; ++i) { if(isoNames[i] == name) { value = i; diff --git a/Sming/Core/HardwarePWM.h b/Sming/Core/HardwarePWM.h index 101f692899..fe1d6d501d 100644 --- a/Sming/Core/HardwarePWM.h +++ b/Sming/Core/HardwarePWM.h @@ -131,7 +131,7 @@ class HardwarePWM private: uint8_t channel_count; uint8_t channels[PWM_CHANNEL_NUM_MAX]; - uint32_t maxduty; + uint32_t maxduty{0}; }; /** @} */ diff --git a/Sming/Core/Interrupts.h b/Sming/Core/Interrupts.h index 446d8e2387..afed3265a7 100644 --- a/Sming/Core/Interrupts.h +++ b/Sming/Core/Interrupts.h @@ -85,7 +85,7 @@ void attachInterrupt(uint8_t pin, InterruptDelegate delegateFunction, GPIO_INT_T __forceinline void attachInterrupt(uint8_t pin, InterruptDelegate delegateFunction, uint8_t mode) { GPIO_INT_TYPE type = ConvertArduinoInterruptMode(mode); - attachInterrupt(pin, delegateFunction, type); + attachInterrupt(pin, std::move(delegateFunction), type); } /** @brief Enable interrupts on GPIO pin diff --git a/Sming/Core/NanoTime.h b/Sming/Core/NanoTime.h index c0e1d67744..00b8c2f284 100644 --- a/Sming/Core/NanoTime.h +++ b/Sming/Core/NanoTime.h @@ -38,6 +38,15 @@ namespace NanoTime { +constexpr uint64_t round_ce(double v) +{ +#ifdef __clang__ + return uint64_t(v + 0.5); +#else + return round(v); +#endif +} + /** * @brief Identify units for a scalar quantity of time * @note Ordered in increasing unit size, e.g. days > seconds @@ -242,7 +251,7 @@ template , UnitTickRatio>> constexpr uint64_t convert() { - return ({ round(double(time) * R::num / R::den); }); + return ({ round_ce(double(time) * R::num / R::den); }); } /** @@ -504,7 +513,7 @@ template struct TimeConst { */ static constexpr uint64_t ticks() { - return round(double(time_) * TicksPerUnit::num / TicksPerUnit::den); + return round_ce(double(time_) * TicksPerUnit::num / TicksPerUnit::den); } /** @@ -522,7 +531,7 @@ template struct TimeConst { */ static constexpr uint64_t clockTime() { - return round(double(ticks()) * TicksPerUnit::den / TicksPerUnit::num); + return round_ce(double(ticks()) * TicksPerUnit::den / TicksPerUnit::num); } /** @@ -583,8 +592,8 @@ template struct TicksConst { template using TimeConst = TimeConst::den / - Clock::template TicksPerUnit::num))>; + TimeType(round_ce(double(ticks_) * Clock::template TicksPerUnit::den / + Clock::template TicksPerUnit::num))>; /** * @brief Get the time for the tick count in a specific time unit diff --git a/Sming/Core/PolledTimer.h b/Sming/Core/PolledTimer.h index 2a85dc0cdb..8a5ce571d1 100644 --- a/Sming/Core/PolledTimer.h +++ b/Sming/Core/PolledTimer.h @@ -142,7 +142,7 @@ class Timer : public NanoTime::TimeSource */ __forceinline bool IRAM_ATTR reset(const TimeType& timeInterval) { - return resetTicks(this->template timeToTicks(timeInterval)); + return resetTicks(this->timeToTicks(timeInterval)); } /** @@ -185,7 +185,7 @@ class Timer : public NanoTime::TimeSource */ __forceinline NanoTime::Time elapsedTime() const { - return this->template ticksToTime(elapsedTicks()); + return this->ticksToTime(elapsedTicks()); } /** @@ -202,7 +202,7 @@ class Timer : public NanoTime::TimeSource */ __forceinline NanoTime::Time remainingTime() const { - return this->template ticksToTime(remainingTicks()); + return this->ticksToTime(remainingTicks()); } __forceinline bool canExpire() const diff --git a/Sming/Core/Timer.h b/Sming/Core/Timer.h index 192314dc84..0f16eb6781 100644 --- a/Sming/Core/Timer.h +++ b/Sming/Core/Timer.h @@ -138,7 +138,7 @@ template class OsTimer64Api : public CallbackTimerApi void OsTimer64Api::setInterval(TickType interval) { - constexpr auto maxTicks = osTimer.maxTicks(); + constexpr auto maxTicks = OsTimerApi::maxTicks(); if(interval > maxTicks) { // interval too large, calculate a good divider uint32_t div = (interval / (maxTicks + 1)) + 1; // integer division, intended diff --git a/Sming/Libraries/Graphics b/Sming/Libraries/Graphics index 7d2b503082..1809db3740 160000 --- a/Sming/Libraries/Graphics +++ b/Sming/Libraries/Graphics @@ -1 +1 @@ -Subproject commit 7d2b503082c4219aaba0090f9ed4707b25740afa +Subproject commit 1809db3740086700d895e1c0b3c5a7b632c4e210 diff --git a/Sming/Libraries/OtaUpgrade/OtaUpgrade/BasicStream.h b/Sming/Libraries/OtaUpgrade/OtaUpgrade/BasicStream.h index 880fad0653..208129144c 100644 --- a/Sming/Libraries/OtaUpgrade/OtaUpgrade/BasicStream.h +++ b/Sming/Libraries/OtaUpgrade/OtaUpgrade/BasicStream.h @@ -88,10 +88,12 @@ class BasicStream : public ReadWriteStream { return 0; } - virtual int available() override + + int available() override { return 0; } + bool isFinished() override { return true; diff --git a/Sming/Libraries/SPI/src/SPISoft.cpp b/Sming/Libraries/SPI/src/SPISoft.cpp index d20e3925f2..cfca4ef31d 100644 --- a/Sming/Libraries/SPI/src/SPISoft.cpp +++ b/Sming/Libraries/SPI/src/SPISoft.cpp @@ -24,7 +24,11 @@ #define SPISOFT_ARCH_DELAY_FIXED 0 #endif +#ifdef __clang__ +#define FUNC_OPT +#else #define FUNC_OPT __attribute__((optimize(3))) +#endif namespace spisoft { diff --git a/Sming/Libraries/Spiffs/src/FileMeta.cpp b/Sming/Libraries/Spiffs/src/FileMeta.cpp index f8af99324e..9cb8bbd960 100644 --- a/Sming/Libraries/Spiffs/src/FileMeta.cpp +++ b/Sming/Libraries/Spiffs/src/FileMeta.cpp @@ -44,7 +44,7 @@ void* FileMeta::getAttributePtr(AttributeTag tag) } } -int SpiffsMetaBuffer::enumxattr(AttributeEnumCallback callback, void* buffer, size_t bufsize) +int SpiffsMetaBuffer::enumxattr(const AttributeEnumCallback& callback, void* buffer, size_t bufsize) { size_t count{0}; AttributeEnum e{buffer, bufsize}; diff --git a/Sming/Libraries/Spiffs/src/include/IFS/SPIFFS/FileMeta.h b/Sming/Libraries/Spiffs/src/include/IFS/SPIFFS/FileMeta.h index 5a4f846fd1..aea05a5951 100644 --- a/Sming/Libraries/Spiffs/src/include/IFS/SPIFFS/FileMeta.h +++ b/Sming/Libraries/Spiffs/src/include/IFS/SPIFFS/FileMeta.h @@ -114,7 +114,7 @@ struct SpiffsMetaBuffer { } } - int enumxattr(AttributeEnumCallback callback, void* buffer, size_t bufsize); + int enumxattr(const AttributeEnumCallback& callback, void* buffer, size_t bufsize); int getxattr(AttributeTag tag, void* buffer, size_t size); int setxattr(AttributeTag tag, const void* data, size_t size); int getUserAttribute(unsigned userTag, void* buffer, size_t size); diff --git a/Sming/Platform/System.cpp b/Sming/Platform/System.cpp index 9d9aca0452..c490fc3018 100644 --- a/Sming/Platform/System.cpp +++ b/Sming/Platform/System.cpp @@ -100,7 +100,7 @@ bool SystemClass::queueCallback(TaskDelegate callback) // @todo consider failing immediately if called from interrupt context - auto delegate = new TaskDelegate(callback); + auto delegate = new TaskDelegate(std::move(callback)); if(delegate == nullptr) { return false; } diff --git a/Sming/Services/HexDump/HexDump.cpp b/Sming/Services/HexDump/HexDump.cpp index 4c6f43c994..bc5ceddeaf 100644 --- a/Sming/Services/HexDump/HexDump.cpp +++ b/Sming/Services/HexDump/HexDump.cpp @@ -7,16 +7,6 @@ #include "HexDump.h" -HexDump::HexDump() -{ - addr = 0; -} - -HexDump::~HexDump() -{ - // TODO Auto-generated destructor stub -} - void HexDump::print(unsigned char* data, int len) { Serial.printf("Data: (%d Bytes)\n", len); diff --git a/Sming/Services/HexDump/HexDump.h b/Sming/Services/HexDump/HexDump.h index a10e4430e6..8e36e9dfb8 100644 --- a/Sming/Services/HexDump/HexDump.h +++ b/Sming/Services/HexDump/HexDump.h @@ -12,8 +12,9 @@ class HexDump { public: - HexDump(); - virtual ~HexDump(); + virtual ~HexDump() + { + } void print(unsigned char* data, int len); void resetAddr(); diff --git a/Sming/Services/Profiling/MinMaxTimes.h b/Sming/Services/Profiling/MinMaxTimes.h index 74b12c1792..24016e92a2 100644 --- a/Sming/Services/Profiling/MinMaxTimes.h +++ b/Sming/Services/Profiling/MinMaxTimes.h @@ -19,22 +19,22 @@ template class MinMaxTimes : public MinMax32, public Timer NanoTime::Time getMinTime() const { - return this->template ticksToTime(getMin()); + return this->ticksToTime(getMin()); } NanoTime::Time getMaxTime() const { - return this->template ticksToTime(getMax()); + return this->ticksToTime(getMax()); } NanoTime::Time getAverageTime() const { - return this->template ticksToTime(getAverage()); + return this->ticksToTime(getAverage()); } NanoTime::Time getTotalTime() const { - return this->template ticksToTime(getTotal()); + return this->ticksToTime(getTotal()); } size_t printTo(Print& p) const diff --git a/Sming/Services/Profiling/TaskStat.h b/Sming/Services/Profiling/TaskStat.h index ca6c761338..141435bf27 100644 --- a/Sming/Services/Profiling/TaskStat.h +++ b/Sming/Services/Profiling/TaskStat.h @@ -52,8 +52,10 @@ class TaskStat static constexpr size_t maxTasks{32}; struct Info; std::unique_ptr taskInfo; +#ifdef ARCH_ESP32 uint8_t startIndex{0}; uint8_t endIndex{0}; +#endif }; } // namespace Profiling diff --git a/Sming/System/include/stringutil.h b/Sming/System/include/stringutil.h index a64950f7db..efdfac1180 100644 --- a/Sming/System/include/stringutil.h +++ b/Sming/System/include/stringutil.h @@ -44,33 +44,28 @@ int strcasecmp(const char* s1, const char* s2); */ void* memmem(const void* haystack, size_t haystacklen, const void* needle, size_t needlelen); -void *memrchr(const void *s, int c, size_t n); +void* memrchr(const void* s, int c, size_t n); #endif +/** + * @brief Compare block of memory without case sensitivity + */ int memicmp(const void* buf1, const void* buf2, size_t len); -static inline char hexchar(unsigned char c) -{ - if(c < 10) - return '0' + c; - else if(c <= 15) - return 'a' + c - 10; - else - return '\0'; -} +/** + * @brief Return hex character corresponding to given value + * @param c Value from 0-15, others are invalid + * @retval char Hex character '0'-'9', 'a'-'f' or '\0' if invalid + */ +char hexchar(unsigned char c); -static inline signed char unhex(char c) -{ - if(c >= '0' && c <= '9') - return c - '0'; - else if(c >= 'a' && c <= 'f') - return 10 + c - 'a'; - else if(c >= 'A' && c <= 'F') - return 10 + c - 'A'; - else - return -1; -} +/** + * @brief Return numeric value corresponding to given hex character + * @param c Character '0'-'9', 'a'-'f' or 'A'-'F' + * @retval int8_t Numeric value or -1 if character invalid + */ +signed char unhex(char c); #ifndef ARRAY_SIZE #define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0])) diff --git a/Sming/System/m_printf.cpp b/Sming/System/m_printf.cpp index 42482fef39..80fe56223c 100644 --- a/Sming/System/m_printf.cpp +++ b/Sming/System/m_printf.cpp @@ -51,7 +51,7 @@ static int skip_atoi(const char **s) nputs_callback_t m_setPuts(nputs_callback_t callback) { nputs_callback_t previousCallback = _puts_callback; - _puts_callback = callback; + _puts_callback = std::move(callback); return previousCallback; } diff --git a/Sming/System/stringutil.cpp b/Sming/System/stringutil.cpp index 9e877c6081..5266856c62 100644 --- a/Sming/System/stringutil.cpp +++ b/Sming/System/stringutil.cpp @@ -24,23 +24,16 @@ const char* strstri(const char* pString, const char* pToken) return NULL; int matchIndex = 0; - while(*pString) - { - if(tolower(*pString) == tolower(pToken[matchIndex])) - { + while(*pString) { + if(tolower(*pString) == tolower(pToken[matchIndex])) { //If we reached the end of pToken, return the match - if(pToken[matchIndex + 1] == 0) - { + if(pToken[matchIndex + 1] == 0) { return pString - matchIndex; - } - else - { + } else { ++matchIndex; } ++pString; - } - else - { + } else { //If we were in the middle of a matching process, // recheck current pString character with // the first pToken character else increase pString @@ -68,3 +61,28 @@ int memicmp(const void* buf1, const void* buf2, size_t len) return result; } + +char hexchar(unsigned char c) +{ + if(c < 10) { + return (char)('0' + c); + } + if(c <= 15) { + return (char)('a' + c - 10); + } + return '\0'; +} + +signed char unhex(char c) +{ + if(c >= '0' && c <= '9') { + return (char)(c - '0'); + } + if(c >= 'a' && c <= 'f') { + return (char)(10 + c - 'a'); + } + if(c >= 'A' && c <= 'F') { + return (char)(10 + c - 'A'); + } + return -1; +} diff --git a/Sming/Wiring/Countable.h b/Sming/Wiring/Countable.h index 18b2ea87f4..ddd98204ef 100644 --- a/Sming/Wiring/Countable.h +++ b/Sming/Wiring/Countable.h @@ -19,6 +19,15 @@ template class Countable { public: + Countable() + { + } + + Countable(const Countable&) = delete; + Countable(Countable&&) = delete; + Countable& operator=(const Countable&) = delete; + Countable& operator=(Countable&&) = delete; + virtual ~Countable() { } diff --git a/Sming/Wiring/Print.h b/Sming/Wiring/Print.h index f9ca622aee..14decaf59e 100644 --- a/Sming/Wiring/Print.h +++ b/Sming/Wiring/Print.h @@ -36,10 +36,20 @@ class Print { public: + Print() + { + } + + Print(const Print&) = delete; + Print(Print&&) = delete; + virtual ~Print() { } + Print& operator=(const Print&) = delete; + Print& operator=(Print&&) = delete; + /** @brief Gets last error @retval int Error number of last write error */ @@ -87,7 +97,7 @@ class Print */ size_t write(const char* buffer, size_t size) { - return write((const uint8_t*)buffer, size); + return write(reinterpret_cast(buffer), size); } /** @brief Prints a single character to output stream diff --git a/Sming/Wiring/Printable.h b/Sming/Wiring/Printable.h index f538398189..c9252d82db 100644 --- a/Sming/Wiring/Printable.h +++ b/Sming/Wiring/Printable.h @@ -42,6 +42,15 @@ class Print; class Printable { public: + Printable() + { + } + + Printable(const Printable&) = delete; + Printable(Printable&&) = delete; + Printable& operator=(const Printable&) = delete; + Printable& operator=(Printable&&) = delete; + virtual ~Printable() { } diff --git a/Sming/Wiring/WMath.cpp b/Sming/Wiring/WMath.cpp index d4007c9ea8..b8ae5dbb32 100644 --- a/Sming/Wiring/WMath.cpp +++ b/Sming/Wiring/WMath.cpp @@ -30,12 +30,10 @@ void srandom(unsigned int s) static int getRandom() { - unsigned int next = seed; - int result; - + auto next = seed; next *= 1103515245; next += 12345; - result = (unsigned int)(next / 65536) % 2048; + auto result = (unsigned int)(next / 65536) % 2048; next *= 1103515245; next += 12345; @@ -49,27 +47,30 @@ static int getRandom() seed = next; - return result; + return int(result); } void randomSeed(uint16_t seed) { - if(seed != 0) + if(seed != 0) { srandom(seed); + } } long random(long howbig) { - if(howbig == 0) + if(howbig == 0) { return 0; + } return getRandom() % howbig; } long random(long howsmall, long howbig) { - if(howsmall >= howbig) + if(howsmall >= howbig) { return howsmall; + } long diff = howbig - howsmall; return random(diff) + howsmall; @@ -83,13 +84,3 @@ long map(long x, long in_min, long in_max, long out_min, long out_max) } return ((x - in_min) * (out_max - out_min) / divisor) + out_min; } - -uint16_t makeWord(uint16_t w) -{ - return w; -} - -uint16_t makeWord(uint8_t highByte, uint8_t lowByte) -{ - return (highByte << 8) | lowByte; -} diff --git a/Sming/Wiring/WMath.h b/Sming/Wiring/WMath.h index 39ada0421f..b67ac0bb22 100644 --- a/Sming/Wiring/WMath.h +++ b/Sming/Wiring/WMath.h @@ -19,9 +19,20 @@ #include -long random(long); -long random(long, long); -long map(long, long, long, long, long); +long random(long howbig); + +long random(long howsmall, long howbig); + +long map(long x, long in_min, long in_max, long out_min, long out_max); + void randomSeed(uint16_t); -uint16_t makeWord(uint8_t, uint8_t); -uint16_t makeWord(uint16_t); + +static inline uint16_t makeWord(uint8_t highByte, uint8_t lowByte) +{ + return (highByte << 8) | lowByte; +} + +static inline uint16_t makeWord(uint16_t w) +{ + return w; +} diff --git a/Sming/Wiring/WString.h b/Sming/Wiring/WString.h index 054065b1bd..edcd00ae0f 100644 --- a/Sming/Wiring/WString.h +++ b/Sming/Wiring/WString.h @@ -135,6 +135,7 @@ using flash_string_t = const __FlashStringHelper*; */ class String { +protected: // use a function pointer to allow for "if (s)" without the // complications of an operator bool(). for more information, see: // http://www.artima.com/cppsource/safebool.html @@ -191,19 +192,19 @@ class String String(StringSumHelper&& rval) noexcept; #endif explicit String(char c); - explicit String(unsigned char, unsigned char base = 10, unsigned char width = 0, char pad = '0'); - explicit String(int num, unsigned char base = 10, unsigned char width = 0, char pad = '0') + explicit String(unsigned char, unsigned char base = DEC, unsigned char width = 0, char pad = '0'); + explicit String(int num, unsigned char base = DEC, unsigned char width = 0, char pad = '0') : String(long(num), base, width, pad) { } - explicit String(unsigned int num, unsigned char base = 10, unsigned char width = 0, char pad = '0') + explicit String(unsigned int num, unsigned char base = DEC, unsigned char width = 0, char pad = '0') : String((unsigned long)(num), base, width, pad) { } - explicit String(long, unsigned char base = 10, unsigned char width = 0, char pad = '0'); - explicit String(long long, unsigned char base = 10, unsigned char width = 0, char pad = '0'); - explicit String(unsigned long, unsigned char base = 10, unsigned char width = 0, char pad = '0'); - explicit String(unsigned long long, unsigned char base = 10, unsigned char width = 0, char pad = '0'); + explicit String(long, unsigned char base = DEC, unsigned char width = 0, char pad = '0'); + explicit String(long long, unsigned char base = DEC, unsigned char width = 0, char pad = '0'); + explicit String(unsigned long, unsigned char base = DEC, unsigned char width = 0, char pad = '0'); + explicit String(unsigned long long, unsigned char base = DEC, unsigned char width = 0, char pad = '0'); explicit String(float, unsigned char decimalPlaces = 2); explicit String(double, unsigned char decimalPlaces = 2); /** @} */ @@ -331,19 +332,19 @@ class String { return concat(&c, 1); } - bool concat(unsigned char num, unsigned char base = 10, unsigned char width = 0, char pad = '0'); - bool concat(int num, unsigned char base = 10, unsigned char width = 0, char pad = '0') + bool concat(unsigned char num, unsigned char base = DEC, unsigned char width = 0, char pad = '0'); + bool concat(int num, unsigned char base = DEC, unsigned char width = 0, char pad = '0') { return concat(long(num), base, width, pad); } - bool concat(unsigned int num, unsigned char base = 10, unsigned char width = 0, char pad = '0') + bool concat(unsigned int num, unsigned char base = DEC, unsigned char width = 0, char pad = '0') { return concat((unsigned long)(num), base, width, pad); } - bool concat(long num, unsigned char base = 10, unsigned char width = 0, char pad = '0'); - bool concat(long long num, unsigned char base = 10, unsigned char width = 0, char pad = '0'); - bool concat(unsigned long num, unsigned char base = 10, unsigned char width = 0, char pad = '0'); - bool concat(unsigned long long num, unsigned char base = 10, unsigned char width = 0, char pad = '0'); + bool concat(long num, unsigned char base = DEC, unsigned char width = 0, char pad = '0'); + bool concat(long long num, unsigned char base = DEC, unsigned char width = 0, char pad = '0'); + bool concat(unsigned long num, unsigned char base = DEC, unsigned char width = 0, char pad = '0'); + bool concat(unsigned long long num, unsigned char base = DEC, unsigned char width = 0, char pad = '0'); bool concat(float num); bool concat(double num); @@ -606,7 +607,7 @@ class String */ void toCharArray(char* buf, size_t bufsize, size_t index = 0) const { - getBytes((unsigned char*)buf, bufsize, index); + getBytes(reinterpret_cast(buf), bufsize, index); } /** @@ -787,7 +788,7 @@ class String */ String& padLeft(uint16_t minWidth, char c = ' ') { - return pad(-minWidth, c); + return pad(int16_t(-minWidth), c); } /** @@ -795,7 +796,7 @@ class String */ String& padRight(uint16_t minWidth, char c = ' ') { - return pad(minWidth, c); + return pad(int16_t(minWidth), c); } /** diff --git a/Sming/Wiring/WVector.h b/Sming/Wiring/WVector.h index eb2f97a493..425b951ea4 100644 --- a/Sming/Wiring/WVector.h +++ b/Sming/Wiring/WVector.h @@ -46,11 +46,18 @@ template class Vector : public Countable using E = typename std::conditional::type; Iterator(const Iterator&) = default; + Iterator(Iterator&&) = default; + Iterator& operator=(const Iterator&) = default; + Iterator& operator=(Iterator&&) = default; Iterator(V& vector, unsigned index) : vector(vector), index(index) { } + ~Iterator() + { + } + Iterator& operator++() { ++index; @@ -96,7 +103,6 @@ template class Vector : public Countable unsigned index{0}; }; - // constructors Vector(unsigned int initialCapacity = 10, unsigned int capacityIncrement = 10) : _increment(capacityIncrement) { _data.allocate(initialCapacity); @@ -107,6 +113,12 @@ template class Vector : public Countable copyFrom(rhv); } + Vector(Vector&&) = delete; + + ~Vector() + { + } + // methods unsigned int capacity() const { @@ -238,7 +250,7 @@ template class Vector : public Countable return _data[index]; } - const Vector& operator=(const Vector& rhv) + Vector& operator=(const Vector& rhv) { if(this != &rhv) { copyFrom(rhv); @@ -246,7 +258,7 @@ template class Vector : public Countable return *this; } - const Vector& operator=(Vector&& other) noexcept // move assignment + Vector& operator=(Vector&& other) noexcept // move assignment { clear(); _increment = 0; @@ -320,7 +332,7 @@ template template int Vector::indexOf(cons { for(unsigned int i = 0; i < _size; i++) { if(_data[i] == elem) { - return i; + return int(i); } } @@ -339,7 +351,7 @@ template template int Vector::lastIndexOf( do { i--; if(_data[i] == elem) { - return i; + return int(i); } } while(i != 0); @@ -436,7 +448,7 @@ template void Vector::sort(Comparer compareFunction) Element& keyRef = _data[j]; // Smaller values move up int i; - for(i = j - 1; (i >= 0) && compareFunction(_data[i], keyRef) > 0; i--) { + for(i = int(j) - 1; (i >= 0) && compareFunction(_data[i], keyRef) > 0; i--) { _data.values[i + 1] = _data.values[i]; } // Put key into its proper location diff --git a/Sming/Wiring/WiringList.h b/Sming/Wiring/WiringList.h index d799215cc4..757e3059d6 100644 --- a/Sming/Wiring/WiringList.h +++ b/Sming/Wiring/WiringList.h @@ -19,6 +19,15 @@ template struct ScalarList { T* values{nullptr}; size_t size{0}; + ScalarList() + { + } + + ScalarList(const ScalarList&) = delete; + ScalarList(ScalarList&&) = default; + ScalarList& operator=(const ScalarList&) = delete; + ScalarList& operator=(ScalarList&&) = default; + ~ScalarList() { clear(); @@ -67,7 +76,7 @@ template struct ScalarList { const T& operator[](unsigned index) const { - return const_cast(*this)[index]; + return values[index]; } }; @@ -99,6 +108,12 @@ template struct ObjectList : public ScalarList { } }; + ObjectList() = default; + ObjectList(const ObjectList&) = delete; + ObjectList(ObjectList&&) = default; + ObjectList& operator=(const ObjectList&) = delete; + ObjectList& operator=(ObjectList&&) = default; + ~ObjectList() { clear(); diff --git a/Sming/build.mk b/Sming/build.mk index a0b60ffc65..4717a21f2a 100644 --- a/Sming/build.mk +++ b/Sming/build.mk @@ -26,6 +26,12 @@ ifeq (,$(wildcard $(SMING_HOME)/Arch/$(SMING_ARCH)/build.mk)) $(error Arch '$(SMING_ARCH)' not found) endif +ifdef CLANG_TIDY +ifneq (Host,$(SMING_ARCH)) + $(error CLANG_TIDY supported only for Host architecture.) +endif +endif + export SMING_ARCH export SMING_SOC diff --git a/Sming/component-wrapper.mk b/Sming/component-wrapper.mk index f6da0f6a34..320399dda5 100644 --- a/Sming/component-wrapper.mk +++ b/Sming/component-wrapper.mk @@ -117,6 +117,13 @@ else OUTPUT_DEPS := -MF $$@ endif +# Additional flags to pass to clang +CLANG_FLAG_EXTRA ?= + +# Flags which clang doesn't recognise +CLANG_FLAG_UNKNOWN := \ + -fstrict-volatile-bitfields + # $1 -> absolute source directory, no trailing path separator # $2 -> relative output build directory, with trailing path separator define GenerateCompileTargets @@ -132,6 +139,11 @@ $2%.o: $1/%.S $(Q) $(AS) $(addprefix -I,$(INCDIR)) $(CPPFLAGS) $(CFLAGS) -c $$< -o $$@ endif ifneq (,$(filter $1/%.c,$(SOURCE_FILES))) +ifdef CLANG_TIDY +$2%.o: $1/%.c + $(vecho) "TIDY $$<" + $(Q) $(CLANG_TIDY) $$< -- $(addprefix -I,$(INCDIR)) $$(filter-out $(CLANG_FLAG_UNKNOWN),$(CPPFLAGS) $(CFLAGS)) $(CLANG_FLAG_EXTRA) +else $2%.o: $1/%.c $2%.c.d $(vecho) "CC $$<" $(Q) $(CC) $(addprefix -I,$(INCDIR)) $(CPPFLAGS) $(CFLAGS) -c $$< -o $$@ @@ -139,7 +151,13 @@ $2%.c.d: $1/%.c $(Q) $(CC) $(addprefix -I,$(INCDIR)) $(CPPFLAGS) $(CFLAGS) -MM -MT $2$$*.o $$< $(OUTPUT_DEPS) .PRECIOUS: $2%.c.d endif +endif ifneq (,$(filter $1/%.cpp,$(SOURCE_FILES))) +ifdef CLANG_TIDY +$2%.o: $1/%.cpp + $(vecho) "TIDY $$<" + $(Q) $(CLANG_TIDY) $$< -- $(addprefix -I,$(INCDIR)) $$(filter-out $(CLANG_FLAG_UNKNOWN),$(CPPFLAGS) $(CXXFLAGS)) $(CLANG_FLAG_EXTRA) +else $2%.o: $1/%.cpp $2%.cpp.d $(vecho) "C+ $$<" $(Q) $(CXX) $(addprefix -I,$(INCDIR)) $(CPPFLAGS) $(CXXFLAGS) -c $$< -o $$@ @@ -147,6 +165,7 @@ $2%.cpp.d: $1/%.cpp $(Q) $(CXX) $(addprefix -I,$(INCDIR)) $(CPPFLAGS) $(CXXFLAGS) -MM -MT $2$$*.o $$< $(OUTPUT_DEPS) .PRECIOUS: $2%.cpp.d endif +endif endef # Resolve a source path to the corresponding build output object file @@ -177,10 +196,12 @@ include $(wildcard $(ABS_BUILD_DIRS:=/*.cpp.d)) # Provide a target unless Component is custom built, in which case the component.mk will have defined this already $(COMPONENT_LIBPATH): build.ar +ifndef CLANG_TIDY $(vecho) "AR $@" $(Q) test ! -f $@ || rm $@ $(Q) $(AR) -M < build.ar $(Q) mv $(notdir $(COMPONENT_LIBPATH)) $(COMPONENT_LIBPATH) +endif define addmod @echo ADDMOD $2 >> $1 diff --git a/docs/source/information/develop/clang-tools.rst b/docs/source/information/develop/clang-tools.rst index aba5e45913..bc9ac01fc8 100644 --- a/docs/source/information/develop/clang-tools.rst +++ b/docs/source/information/develop/clang-tools.rst @@ -5,10 +5,10 @@ Clang Tools is a tool that implements automatic source code formatting. It can be used to automatically enforce the layout rules for Sming. -`clang-tidy `__ -is a C++ “linter” tool to assist with diagnosing and fixing typical programming errors -such as style violations, interface misuse, or bugs that can be deduced via static analysis. -It is provided as part of +`clang-tidy `__ +is a C++ “linter” tool to assist with diagnosing and fixing typical programming errors +including portability/readability issues, bug-prone code constructs, +interface misuse, or bugs that can be deduced via static analysis. You can find details for the current release at https://releases.llvm.org/download.html. Note that *clang-format* is part of the main **Clang** project, whilst *clang-tidy* can be @@ -31,15 +31,15 @@ systems. Different versions of clang-format can produce different results, despite using the same configuration file. - We are using version 8.0.1 of clang-format and clang-tidy on our + We are using version 8.0.1 of clang-format on our Continuous Integration (CI) System. - + You should install the same version on your development computer. -Configuration -------------- +clang-format +------------ Rules ~~~~~ @@ -57,10 +57,7 @@ IDE integration There are multiple existing integrations for IDEs. You can find details in the `ClangFormat documentation `__. -Eclipse IDE -^^^^^^^^^^^ - -For our Eclipse IDE, which is our preferred IDE, we recommend installing +For the Eclipse IDE we recommend installing the `CppStyle plugin `__. You can configure your IDE to auto-format the code on “Save” using the recommended coding style and/or format according to our coding style @@ -69,9 +66,6 @@ lines). Read `Configure CppStyle `__ for details. -Usage ------ - Command Line ~~~~~~~~~~~~ @@ -79,10 +73,10 @@ Single File If you want to directly apply the coding standards from the command line you can run the following command:: - + cd $SMING_HOME clang-format -style=file -i Core/ - + Where ``Core/`` should be replaced with the path to the file that you have modified. @@ -91,10 +85,10 @@ All files The following command will run again the coding standards formatter over all C, C++ and header files inside the ``Sming/Core``, ``samples`` and other key directories:: - + cd $SMING_HOME make cs - + The command needs time to finish. So be patient. It will go over all files and will try to fix any coding style issues. @@ -105,7 +99,7 @@ All files make cs from your project directory. - + Eclipse ~~~~~~~ @@ -116,3 +110,62 @@ Alternatively, you can manually apply the coding style rules by selecting the so C, C++ or header file or a selection in it and run the ``Format`` command (usually Ctrl-Shift-F). + +clang-tidy +---------- + +Configuration +~~~~~~~~~~~~~ + +No specific version is required but generally you should aim to use the most recent version +available in your distribution. Version 17.0.6 was used at time of writing these notes. + +The default tool configuration is defined in the +`.clang-tidy `__ +file, located in the root directory of the framework. + +.. note:: + + Unlike clang-format, clang-tidy has to be able to compile the target code in order to perform static analysis. + Code must build without errors for **Host** architecture. + + Clang has more limited `constexpr` support than GCC so cannot parse some of the FlashString + and NanoTime code without modification. + However, these changes are only made when `__clang__` is defined and do not affect regular builds with GCC. + + No object code is generated by clang-tidy. + +Usage +~~~~~ + +Only source files which haven't been built are inspected. +So, to restrict which code gets processed built the entire application normally, +then 'clean' the relevant modules before proceeding with clang-tidy. + +For example:: + + cd $SMING_HOME/../samples/Basic_Servo + make -j SMING_SOC=host + make clean Servo-clean + make CLANG_TIDY=clang-tidy + +If you want to fix a particular type of problem, it's usually best to be explicit:: + + make CLANG_TIDY="clang-tidy --checks='-*,modernize-use-equals-default' --fix" + +Remember to run ``make cs`` and check the output before committing! + +If you want to provide a custom configuration file:: + + make CLANG_TIDY="clang-tidy --config-file=myTidyConfig" + + +.. note:: + + clang-tidy can take a long time to do its work, so it's tempting to use the `-j` option + to speed things up. + You may see some corrupted output though as the output from multiple clang-tidy + instances aren't serialised correctly. + It's usually fine to get a rough 'first-pass' indication of any problems though. + + However, if attempting to apply fixes **DO NOT** use the -j option as this will result in corrupted output. diff --git a/samples/Basic_DateTime/app/application.cpp b/samples/Basic_DateTime/app/application.cpp index a36ff1360f..ccd8b42f2c 100644 --- a/samples/Basic_DateTime/app/application.cpp +++ b/samples/Basic_DateTime/app/application.cpp @@ -77,8 +77,9 @@ void onRx(Stream& source, char arrivedChar, unsigned short availableCharsCount) { static LineBuffer<32> buffer; + using Action = LineBufferBase::Action; switch(buffer.process(source, Serial)) { - case buffer.Action::submit: { + case Action::submit: { if(!buffer) { break; } @@ -109,7 +110,7 @@ void onRx(Stream& source, char arrivedChar, unsigned short availableCharsCount) break; } - case buffer.Action::clear: + case Action::clear: break; default:; diff --git a/tests/HostTests/modules/ArduinoString.cpp b/tests/HostTests/modules/ArduinoString.cpp index b38279c943..399ab29d88 100644 --- a/tests/HostTests/modules/ArduinoString.cpp +++ b/tests/HostTests/modules/ArduinoString.cpp @@ -24,7 +24,7 @@ class ArduinoStringTest : public TestGroup { } - void execute() + void execute() override { TEST_CASE("String::trim", "[core][String]") { diff --git a/tests/HostTests/modules/DateTime.cpp b/tests/HostTests/modules/DateTime.cpp index 738adf564a..5f262a8c0d 100644 --- a/tests/HostTests/modules/DateTime.cpp +++ b/tests/HostTests/modules/DateTime.cpp @@ -55,8 +55,11 @@ class DateTimeTest : public TestGroup unsigned count = checkSetTime(VALID_HTTP_DATE); count += checkSetTime(VALID_ISO_DATETIME); auto elapsed = timer.elapsedTime(); - Serial << "Checked " << count << " dates in " << elapsed.toString() << ", " << elapsed / count - << " per date" << endl; + Serial << "Checked " << count << " dates in " << elapsed.toString(); + if(count != 0) { + Serial << ", " << elapsed / count << " per date"; + } + Serial << endl; } TEST_CASE("getMonthDays") diff --git a/tests/HostTests/modules/Files.cpp b/tests/HostTests/modules/Files.cpp index ed25524c7f..7f5ad4d131 100644 --- a/tests/HostTests/modules/Files.cpp +++ b/tests/HostTests/modules/Files.cpp @@ -16,46 +16,48 @@ class FilesTest : public TestGroup DEFINE_FSTR_LOCAL(testFileName, "test.txt"); int res, pos, size; - FileHandle file{-1}; - TEST_CASE("Initial position and size") { - res = fileSetContent(testFileName, testContent); - debug_i("fileSetContent() returned %d", res); - REQUIRE(size_t(res) == testContent.length()); - file = fileOpen(testFileName, File::ReadWrite); - size = fileSeek(file, 0, SeekOrigin::End); - pos = fileSeek(file, 100, SeekOrigin::Start); - debug_i("pos = %d, size = %d", pos, size); - REQUIRE(pos == 100); - REQUIRE(size_t(size) == testContent.length()); - } - - TEST_CASE("Reduce file sizes") - { - res = fileTruncate(file, 555); - pos = fileTell(file); - size = fileSeek(file, 0, SeekOrigin::End); - debug_i("res = %d, pos = %d, size = %d", res, pos, size); - REQUIRE(res == 0); - REQUIRE(pos == 100); - REQUIRE(size == 555); - } - - TEST_CASE("Increase file size") - { - res = fileTruncate(file, 12345); - size = fileSeek(file, 0, SeekOrigin::End); - debug_i("res = %d, size = %d", res, size); - REQUIRE(res < 0); - REQUIRE(size == 555); - } - - TEST_CASE("Close file") - { - fileClose(file); - REQUIRE(fileTell(file) < 0); - file = -1; + FileHandle file{-1}; + + TEST_CASE("Initial position and size") + { + res = fileSetContent(testFileName, testContent); + debug_i("fileSetContent() returned %d", res); + REQUIRE(size_t(res) == testContent.length()); + file = fileOpen(testFileName, File::ReadWrite); + size = fileSeek(file, 0, SeekOrigin::End); + pos = fileSeek(file, 100, SeekOrigin::Start); + debug_i("pos = %d, size = %d", pos, size); + REQUIRE(pos == 100); + REQUIRE(size_t(size) == testContent.length()); + } + + TEST_CASE("Reduce file sizes") + { + res = fileTruncate(file, 555); + pos = fileTell(file); + size = fileSeek(file, 0, SeekOrigin::End); + debug_i("res = %d, pos = %d, size = %d", res, pos, size); + REQUIRE(res == 0); + REQUIRE(pos == 100); + REQUIRE(size == 555); + } + + TEST_CASE("Increase file size") + { + res = fileTruncate(file, 12345); + size = fileSeek(file, 0, SeekOrigin::End); + debug_i("res = %d, size = %d", res, size); + REQUIRE(res < 0); + REQUIRE(size == 555); + } + + TEST_CASE("Close file") + { + fileClose(file); + REQUIRE(fileTell(file) < 0); + } } TEST_CASE("fileGetSize") diff --git a/tests/HostTests/modules/Libc.cpp b/tests/HostTests/modules/Libc.cpp index 73f0183068..c5c1d55e61 100644 --- a/tests/HostTests/modules/Libc.cpp +++ b/tests/HostTests/modules/Libc.cpp @@ -3,9 +3,8 @@ static int num_instances; struct A { - A() + A() : order(++num_instances) { - order = ++num_instances; } int order; diff --git a/tests/HostTests/modules/Network/Arch/Host/HttpRequest.cpp b/tests/HostTests/modules/Network/Arch/Host/HttpRequest.cpp index 5782443abb..3941747b1b 100644 --- a/tests/HostTests/modules/Network/Arch/Host/HttpRequest.cpp +++ b/tests/HostTests/modules/Network/Arch/Host/HttpRequest.cpp @@ -31,9 +31,8 @@ TestFile testFiles[]{ class HttpRequestTest : public TestGroup { public: - HttpRequestTest() : TestGroup(_F("HTTP")) + HttpRequestTest() : TestGroup(_F("HTTP")), server(new HttpServer) { - server = new HttpServer; } void execute() override diff --git a/tests/HostTests/modules/Stream.cpp b/tests/HostTests/modules/Stream.cpp index 20199bb51e..9781e7f993 100644 --- a/tests/HostTests/modules/Stream.cpp +++ b/tests/HostTests/modules/Stream.cpp @@ -108,7 +108,7 @@ class StreamTest : public TestGroup TEST_CASE("MultipartStream / MultiStream") { unsigned itemIndex{0}; - constexpr const FlashString* items[]{ + const FlashString* items[]{ &template1, &template1_1, &template1_2, &template2, &template2_1, }; MultipartStream multi([&]() -> MultipartStream::BodyPart { diff --git a/tests/HostTests/modules/TemplateStream.cpp b/tests/HostTests/modules/TemplateStream.cpp index 3f325130e7..66f1a6c9bc 100644 --- a/tests/HostTests/modules/TemplateStream.cpp +++ b/tests/HostTests/modules/TemplateStream.cpp @@ -109,7 +109,7 @@ class TemplateStreamTest : public TestGroup SectionTemplate tmpl(new FlashMemoryStream(Resource::ut_template1_in_rst)); REQUIRE(tmpl.sectionCount() == 1); tmpl.setDoubleBraces(true); - tmpl.onGetValue([&tmpl](const char* name) -> String { + tmpl.onGetValue([](const char* name) -> String { debug_e("getValue(%s)", name); if(FS("emit_contents") == name) { return "1"; @@ -174,7 +174,6 @@ class TemplateStreamTest : public TestGroup } tmpl.seek(read); outlen += read; - ptr += read; } String expected; @@ -193,7 +192,7 @@ class TemplateStreamTest : public TestGroup TEST_CASE("CSV Reader") { - auto str = [](const CStringArray cs) { + auto str = [](const CStringArray& cs) { String s = reinterpret_cast(cs); s.replace('\0', ';'); return s; diff --git a/tests/HostTests/modules/Timers.cpp b/tests/HostTests/modules/Timers.cpp index f03b3ef2a9..c2819b78f3 100644 --- a/tests/HostTests/modules/Timers.cpp +++ b/tests/HostTests/modules/Timers.cpp @@ -228,7 +228,7 @@ template class CallbackTimerApiTest : public TestGroup { } - void execute() + void execute() override { api.setInterval(api.maxTicks()); @@ -292,7 +292,7 @@ template class CallbackTimerSpeedTest : public TestGroup { } - void execute() + void execute() override { Serial.println(timer); diff --git a/tests/HostTests/modules/Wiring.cpp b/tests/HostTests/modules/Wiring.cpp index 647eccc88a..31df928533 100644 --- a/tests/HostTests/modules/Wiring.cpp +++ b/tests/HostTests/modules/Wiring.cpp @@ -18,7 +18,7 @@ template Print& operator<<(Print& p, const std::pair void print(const T& list, const char* separator = "\r\n") { - for(auto e : list) { + for(const auto& e : list) { Serial << e << separator; } } @@ -93,7 +93,7 @@ class WiringTest : public TestGroup Serial.println(); using Func = Delegate; - auto time = [this](const String& description, Func function) { + auto time = [](const String& description, const Func& function) { Serial << description << " ..." << endl; TestMap map; fillMap(map); From 591b673f50fa5728d95f720e113f7fb6c8f57755 Mon Sep 17 00:00:00 2001 From: Mike Date: Tue, 23 Apr 2024 09:58:42 +0100 Subject: [PATCH 055/128] Make `time_t` size check message more helpful (#2769) Further to #2758 host builds should generally match code run on hardware. As mentioned this is not straightforward for Windows, but for Linux we should expect 64-bits as the default. GCC 10+ can make use of `_TIME_BITS` to ensure we get this behaviour. More specifically, this is a feature of `glibc` shipped with those versions. For earlier versions, even though the compiler is 64-bit we are building in 32-bit mode and `time_t` stays stuck in 32-bits. There are various discussions about this but the short answer is that prior to GCC 10 (libc 4) 32-bit applications get 32-bit `time_t`. Instead of generating a `static_assert` (which stops compilation) we get a compiler warning - this still requires building in STRICT mode. --- Sming/Core/DateTime.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sming/Core/DateTime.cpp b/Sming/Core/DateTime.cpp index b4636cc16a..4840e84841 100644 --- a/Sming/Core/DateTime.cpp +++ b/Sming/Core/DateTime.cpp @@ -17,8 +17,10 @@ #if defined(__WIN32) || (defined(ARCH_ESP32) && ESP_IDF_VERSION_MAJOR < 5) static_assert(sizeof(time_t) != 8, "Great! Now supports 64-bit - please update code"); #warning "**Y2038** time_t is only 32-bits in this build configuration" +#elif defined(ARCH_HOST) && !defined(__USE_TIME_BITS64) +#warning "**Y2038** Expecting 64-bit time_t - please use GCC 10 or later" #else -static_assert(sizeof(time_t) == 8, "Expecting 64-bit time_t"); +static_assert(sizeof(time_t) == 8, "Expecting 64-bit time_t - please use GCC 10 or later"); #endif namespace From ef3cc6b67e2653684fd561e3bfba616e90a06d87 Mon Sep 17 00:00:00 2001 From: Mike Date: Mon, 27 May 2024 08:04:32 +0100 Subject: [PATCH 056/128] Fix problem defining arrays in class declarations (#2776) - Fix `static_assert` failure using `int64_t` for map keys - Add integration tests to verify before/after behaviour. - Update documentation For example, this code produces `__fstr__myIntArray not defined` error: ``` struct MyClass { DEFINE_FSTR_ARRAY_LOCAL(myIntArray, int, 1, 2, 3, 4, 5) }; ``` The same issue applies to maps and vectors. --- Sming/Components/FlashString | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sming/Components/FlashString b/Sming/Components/FlashString index 8f91c37d6b..c7f2b606c1 160000 --- a/Sming/Components/FlashString +++ b/Sming/Components/FlashString @@ -1 +1 @@ -Subproject commit 8f91c37d6bc87f1109084c55d70aac9695e33d30 +Subproject commit c7f2b606c121d757a385b17faa50b7b7367d223a From 7d69a58c3e2682e06fb6cb96095536c36de1137a Mon Sep 17 00:00:00 2001 From: Mike Date: Mon, 27 May 2024 08:06:18 +0100 Subject: [PATCH 057/128] Fix IFS build inconsistencies (#2774) This PR addresses a couple of issues which have arisen in testing. **Globally define _POSIX_C_SOURCE** Value set by Sming to 200809L making available functionality from the 2008 edition of the POSIX standard (IEEE Standard 1003.1-2008). See https://www.gnu.org/software/libc/manual/html_node/Feature-Test-Macros.html. We should expect those features to be available generally. IFS library no longer needs to define this. **Fix IFS issues** - Fix README links - Build images using environment variables with regular Windows paths require 'fixing', e.g. "/C/dir/path" -> "C:/dir/path" - Remove `TimeStamp::String()` operator completely: it's ambiguous TimeStamp has implicit `time_t` cast, which is appropriate. Calling `String(timestamp)` could yield the numeric value or the string value. Instead, call `toString()` method. For example, when calling `Serial.println(timestamp);` GCC 10.2 calls `TimeStamp::String()`, whereas GCC 14.1 uses `String(time_t)`. --- Sming/Arch/Host/Components/hostlib/include/hostlib/hostlib.h | 4 ---- Sming/Components/IFS | 2 +- Sming/build.mk | 3 ++- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/Sming/Arch/Host/Components/hostlib/include/hostlib/hostlib.h b/Sming/Arch/Host/Components/hostlib/include/hostlib/hostlib.h index 8dc8f9d2b3..139e8a661f 100644 --- a/Sming/Arch/Host/Components/hostlib/include/hostlib/hostlib.h +++ b/Sming/Arch/Host/Components/hostlib/include/hostlib/hostlib.h @@ -19,10 +19,6 @@ #pragma once -// Required for sleep(), probably others -#undef _POSIX_C_SOURCE -#define _POSIX_C_SOURCE 200112L - #ifdef __WIN32 // Prevent early inclusion of winsock.h #include diff --git a/Sming/Components/IFS b/Sming/Components/IFS index 1b4e2f04d4..af9ddbc666 160000 --- a/Sming/Components/IFS +++ b/Sming/Components/IFS @@ -1 +1 @@ -Subproject commit 1b4e2f04d4308f0dc966e4d59ee2d6fe5ffd4c2e +Subproject commit af9ddbc666a29b582cd3f1f4ed40a9848a91fe0a diff --git a/Sming/build.mk b/Sming/build.mk index 4717a21f2a..5dab2d4903 100644 --- a/Sming/build.mk +++ b/Sming/build.mk @@ -158,7 +158,8 @@ CPPFLAGS = \ -Wl,-EL \ -finline-functions \ -fdata-sections \ - -ffunction-sections + -ffunction-sections \ + -D_POSIX_C_SOURCE=200809L # Required to access peripheral registers using structs # e.g. `uint32_t value: 8` sitting at a byte or word boundary will be 'optimised' to From 2cac9139286cb2f154e508e6e1472652b102905e Mon Sep 17 00:00:00 2001 From: Mike Date: Mon, 27 May 2024 08:07:32 +0100 Subject: [PATCH 058/128] Remove legacy compiler build options (#2775) Remove `__GXX_EXPERIMENTAL_CXX0X__`, it's for obsolete compiler versions. Enable deprecated warnings for Host builds. These were originally masked but there's currently no reason for it. --- Sming/Arch/Host/build.mk | 1 - Sming/Core/Data/CStringArray.h | 3 +-- Sming/Wiring/WString.cpp | 6 ------ Sming/Wiring/WString.h | 10 ---------- 4 files changed, 1 insertion(+), 19 deletions(-) diff --git a/Sming/Arch/Host/build.mk b/Sming/Arch/Host/build.mk index d50633bef1..ba2380bae1 100644 --- a/Sming/Arch/Host/build.mk +++ b/Sming/Arch/Host/build.mk @@ -22,7 +22,6 @@ GCC_UPGRADE_URL := https://sming.readthedocs.io/en/latest/arch/host/host-emulato CPPFLAGS += \ -m32 \ - -Wno-deprecated-declarations \ -D_FILE_OFFSET_BITS=64 \ -D_TIME_BITS=64 diff --git a/Sming/Core/Data/CStringArray.h b/Sming/Core/Data/CStringArray.h index c4b8c74ad0..27afb9a66c 100644 --- a/Sming/Core/Data/CStringArray.h +++ b/Sming/Core/Data/CStringArray.h @@ -71,16 +71,15 @@ class CStringArray : private String init(); } -#ifdef __GXX_EXPERIMENTAL_CXX0X__ CStringArray(String&& rval) noexcept : String(std::move(rval)) { init(); } + CStringArray(StringSumHelper&& rval) noexcept : String(std::move(rval)) { init(); } -#endif /** @} */ CStringArray& operator=(const char* cstr) diff --git a/Sming/Wiring/WString.cpp b/Sming/Wiring/WString.cpp index e6d6ac00a1..b2d3b83aac 100644 --- a/Sming/Wiring/WString.cpp +++ b/Sming/Wiring/WString.cpp @@ -35,12 +35,10 @@ String::String(const char* cstr) : String() copy(cstr, strlen(cstr)); } -#ifdef __GXX_EXPERIMENTAL_CXX0X__ String::String(StringSumHelper&& rval) noexcept : String() { move(rval); } -#endif String::String(char c) : String() { @@ -265,7 +263,6 @@ String& String::copy(flash_string_t pstr, size_t length) return *this; } -#ifdef __GXX_EXPERIMENTAL_CXX0X__ void String::move(String& rhs) { if(rhs.isNull()) { @@ -300,7 +297,6 @@ void String::move(String& rhs) rhs.ptr.capacity = 0; rhs.ptr.len = 0; } -#endif String& String::operator=(const String& rhs) { @@ -316,7 +312,6 @@ String& String::operator=(const String& rhs) return *this; } -#ifdef __GXX_EXPERIMENTAL_CXX0X__ String& String::operator=(StringSumHelper&& rval) noexcept { if(this != &rval) { @@ -324,7 +319,6 @@ String& String::operator=(StringSumHelper&& rval) noexcept } return *this; } -#endif String& String::operator=(const char* cstr) { diff --git a/Sming/Wiring/WString.h b/Sming/Wiring/WString.h index edcd00ae0f..f6c5a689ac 100644 --- a/Sming/Wiring/WString.h +++ b/Sming/Wiring/WString.h @@ -68,10 +68,6 @@ */ using FlashString = FSTR::String; -#ifndef __GXX_EXPERIMENTAL_CXX0X__ -#define __GXX_EXPERIMENTAL_CXX0X__ -#endif - // When compiling programs with this class, the following gcc parameters // dramatically increase performance and memory (RAM) efficiency, typically // with little or no increase in code size. @@ -184,13 +180,11 @@ class String setString(pstr); } -#ifdef __GXX_EXPERIMENTAL_CXX0X__ String(String&& rval) noexcept : String() { move(rval); } String(StringSumHelper&& rval) noexcept; -#endif explicit String(char c); explicit String(unsigned char, unsigned char base = DEC, unsigned char width = 0, char pad = '0'); explicit String(int num, unsigned char base = DEC, unsigned char width = 0, char pad = '0') @@ -300,7 +294,6 @@ class String * * @{ */ -#ifdef __GXX_EXPERIMENTAL_CXX0X__ String& operator=(String&& rval) noexcept { if(this != &rval) @@ -308,7 +301,6 @@ class String return *this; } String& operator=(StringSumHelper&& rval) noexcept; -#endif /** @} */ /** @@ -882,9 +874,7 @@ class String // copy and move String& copy(const char* cstr, size_t length); String& copy(flash_string_t pstr, size_t length); -#ifdef __GXX_EXPERIMENTAL_CXX0X__ void move(String& rhs); -#endif }; /** @} */ From 2d3d267e398a1db49b17f8fcfce62ea7e9efaee2 Mon Sep 17 00:00:00 2001 From: Mike Date: Mon, 27 May 2024 09:53:04 +0100 Subject: [PATCH 059/128] Update littlefs from 2.4.0 -> 2.9.3 (#2777) This PR updates the [Sming-LittleFS](https://github.com/mikee47/Sming-LittleFS) library, which implements IFS for the [littlefs](https://github.com/littlefs-project/littlefs) file system. The library still uses a fork of littlefs because it adds attribute enumeration support - see upstream [PR request](https://github.com/littlefs-project/littlefs/pull/636). This has now been rebased on version 2.9.3 - see [releases](https://github.com/littlefs-project/littlefs/releases) for details of all the changes. Here's a (non-exhaustive) summary: - Improved RAM usage - Improved performance - v2.5.0 removed all recursion - v2.6.0 bump the on-disk minor version of littlefs from lfs2.0 -> lfs2.1. "This change is backwards-compatible, but after the first write with the new version, the image on disk will no longer be mountable by older versions of littlefs." - v2.7.0 Add `lfs_fs_stat` as an analog for the POSIX stavfs. This isn't currently used by Sming-LittleFS. - v2.9.0 `rename` now returns LFS_ERR_NOTDIR if the source is a regular file and the destination is a directory. This better aligns with POSIX. There are no functional changes to the library, but the samples have been simplified. An additional sample has been added to assess wear levelling behaviour (as a result of discussion #2771). --- Sming/Libraries/LittleFS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sming/Libraries/LittleFS b/Sming/Libraries/LittleFS index 237f1b73f0..14d9c51afe 160000 --- a/Sming/Libraries/LittleFS +++ b/Sming/Libraries/LittleFS @@ -1 +1 @@ -Subproject commit 237f1b73f0ba9bb8a49c0b6f5b209c56dd981550 +Subproject commit 14d9c51afea4c89717fde894baf3ff02b4ced971 From 66216b1fa67bd68a32ce617ec87489612fd0cfe6 Mon Sep 17 00:00:00 2001 From: Mike Date: Thu, 30 May 2024 17:51:11 +0100 Subject: [PATCH 060/128] NPCAP gone down. Use SmingTools cache. (#2778) The npcap SDK is required by Host networking in Windows, and is downloaded on demand from `npcam.com`. This site has been down since yesterday and whilst it may come back up again it makes sense to keep a cached copy of this in SmingTools. --- Sming/Components/lwip/src/Arch/Host/arch.mk | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Sming/Components/lwip/src/Arch/Host/arch.mk b/Sming/Components/lwip/src/Arch/Host/arch.mk index 06334b16f2..0f6622c651 100644 --- a/Sming/Components/lwip/src/Arch/Host/arch.mk +++ b/Sming/Components/lwip/src/Arch/Host/arch.mk @@ -13,6 +13,9 @@ ifeq ($(UNAME),Windows) COMPONENT_PREREQUISITES += $(NPCAP_SRCDIR)/.ok PCAP_SRC := npcap-sdk-1.05.zip + # PCAP_URL := https://npcap.com/dist/ + PCAP_URL := https://github.com/SmingHub/SmingTools/releases/download/1.0/ + $(NPCAP_SRCDIR)/.ok: @echo Fetching npcap... $(Q) \ @@ -20,7 +23,7 @@ $(NPCAP_SRCDIR)/.ok: mkdir -p $(@D) && \ cd $(@D) && \ powershell -Command "Set-Variable ProgressPreference SilentlyContinue; \ - Invoke-WebRequest https://npcap.com/dist/$(PCAP_SRC) -OutFile $(PCAP_SRC); \ + Invoke-WebRequest $(PCAP_URL)$(PCAP_SRC) -OutFile $(PCAP_SRC); \ Expand-Archive $(PCAP_SRC) ." && \ $(call ApplyPatch,$(LWIP_ARCH_SRCDIR)/npcap.patch) && \ touch $@ From a90219605fd38824dbf742031814a6c6331dace4 Mon Sep 17 00:00:00 2001 From: Mike Date: Sat, 1 Jun 2024 07:55:26 +0100 Subject: [PATCH 061/128] Fix delayed sending of websocket messages (#2782) Fixes #2780 by kicking TCP when messages are queued. Without this messages are sent lazily by lwip stack. --- .../src/Network/Http/Websocket/WebsocketConnection.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Sming/Components/Network/src/Network/Http/Websocket/WebsocketConnection.cpp b/Sming/Components/Network/src/Network/Http/Websocket/WebsocketConnection.cpp index e38363e4c0..8e50ecd6d0 100644 --- a/Sming/Components/Network/src/Network/Http/Websocket/WebsocketConnection.cpp +++ b/Sming/Components/Network/src/Network/Http/Websocket/WebsocketConnection.cpp @@ -294,7 +294,11 @@ bool WebsocketConnection::send(IDataSourceStream* source, ws_frame_type_t type, } // Pass stream to connection - return connection->send(sourceRef.release()); + if(!connection->send(sourceRef.release())) { + return false; + } + connection->commit(); + return true; } void WebsocketConnection::broadcast(const char* message, size_t length, ws_frame_type_t type) From 4753ef43c8ba1e67c1fe58967edfca8292061965 Mon Sep 17 00:00:00 2001 From: Mike Date: Mon, 3 Jun 2024 09:21:45 +0100 Subject: [PATCH 062/128] Upgrade toolchains (#2787) This PR updates the embedded toolchains to more recent versions, setting the new default installed version for Esp32 to IDF 5.2. The library testing github actions script has been updated to include missing esp32c2 architecture and test against IDF 4.4 and 5.2. **Esp32 IDF SDK** The installation scripts now install IDF version 5.2 by default (see issue #2784 regarding docker build failure because of Y2038 warning with IDF < 5). Deprecation warnings printed for IDF 4.3 and 4.4 after final link step. See #2732 for discussion regarding old versions. Windows CI tests for IDF 4.3, 5.0 removed. **Extend library tests to include missing esp32 variants** CI testing when developing libraries is much more efficient than running the main Sming CI tests. Looks like these haven't been updated for a while so have added the `esp32s3` and `esp32c2` variants and testing against IDF 5.2 for all esp32 variants. Previously was only 4.4 (old default). **Esp8266 Toolchain** The installer has been updated to use the latest toolchain (Feb 23), gcc 10.3. **RP2040 Toolchain** The installer has been updated to use the latest toolchain (Oct 23), gcc 13.2. --- .github/workflows/ci-esp32.yml | 4 + .github/workflows/library.yml | 23 ++++- Sming/Arch/Esp32/README.rst | 4 +- Sming/Arch/Esp32/Tools/install.cmd | 2 +- Sming/Arch/Esp32/Tools/install.sh | 2 +- Sming/Arch/Esp32/app.mk | 8 ++ Sming/Arch/Esp8266/Tools/install.cmd | 11 ++- Sming/Arch/Esp8266/Tools/install.sh | 8 +- .../Arch/Rp2040/Components/libc/component.mk | 12 ++- .../Components/libc/src/libc_replacements.c | 86 +++++++++++++++++++ Sming/Arch/Rp2040/README.rst | 17 +++- Sming/Arch/Rp2040/Tools/install.cmd | 10 +-- Sming/Arch/Rp2040/Tools/install.sh | 15 ++-- Sming/Arch/Rp2040/app.mk | 1 + Sming/Arch/Rp2040/build.mk | 7 +- docs/source/upgrading/5.1-5.2.rst | 13 +++ 16 files changed, 184 insertions(+), 39 deletions(-) create mode 100644 Sming/Arch/Rp2040/Components/libc/src/libc_replacements.c diff --git a/.github/workflows/ci-esp32.yml b/.github/workflows/ci-esp32.yml index f849ea2033..37b7cf1db1 100644 --- a/.github/workflows/ci-esp32.yml +++ b/.github/workflows/ci-esp32.yml @@ -21,6 +21,10 @@ jobs: idf_version: "4.3" - variant: esp32c2 idf_version: "4.4" + - idf_version: "4.3" + os: windows-latest + - idf_version: "5.0" + os: windows-latest concurrency: group: ${{ github.workflow }}-${{ github.ref }}-${{ toJson(matrix) }} diff --git a/.github/workflows/library.yml b/.github/workflows/library.yml index 55c26fd07c..44c6f6c4f8 100644 --- a/.github/workflows/library.yml +++ b/.github/workflows/library.yml @@ -22,7 +22,8 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, windows-latest] - variant: [esp8266, host, esp32, esp32s2, esp32c3, rp2040] + variant: [esp8266, host, esp32, esp32s2, esp32c3, esp32s3, esp32c2, rp2040] + idf_version: ["4.4", ""] # "" denotes default, currently 5.2 include: - variant: esp8266 arch: Esp8266 @@ -34,8 +35,21 @@ jobs: arch: Esp32 - variant: esp32c3 arch: Esp32 + - variant: esp32s3 + arch: Esp32 + - variant: esp32c2 + arch: Esp32 - variant: rp2040 arch: Rp2040 + exclude: + - variant: esp32c2 + idf_version: "4.4" + - variant: esp8266 + idf_version: "4.4" + - variant: host + idf_version: "4.4" + - variant: rp2040 + idf_version: "4.4" concurrency: group: ${{ github.workflow }}-${{ github.ref }}-${{ toJson(matrix) }} @@ -43,6 +57,11 @@ jobs: runs-on: ${{ matrix.os }} + env: + SMING_ARCH: ${{ matrix.arch }} + SMING_SOC: ${{ matrix.variant }} + INSTALL_IDF_VER: ${{ matrix.idf_version }} + steps: - name: Fix autocrlf setting run: | @@ -72,8 +91,6 @@ jobs: "SMING_HOME=" + (Resolve-Path "../../sming/Sming").path >> $env:GITHUB_ENV "COMPONENT_SEARCH_DIRS=" + (Resolve-Path "..").path >> $env:GITHUB_ENV "CI_MAKEFILE=" + (Resolve-Path "../../sming/Tools/ci/library/Makefile") >> $env:GITHUB_ENV - "SMING_ARCH=${{ matrix.arch }}" >> $env:GITHUB_ENV - "SMING_SOC=${{ matrix.variant }}" >> $env:GITHUB_ENV - name: Install build tools for Ubuntu if: ${{ matrix.os == 'ubuntu-latest' }} diff --git a/Sming/Arch/Esp32/README.rst b/Sming/Arch/Esp32/README.rst index 9de7952ad8..431f15967e 100644 --- a/Sming/Arch/Esp32/README.rst +++ b/Sming/Arch/Esp32/README.rst @@ -108,9 +108,9 @@ IDF versions Sming currently supports IDF versions 4.3, 4.4, 5.0 and 5.2. -The default installed IDF version is 4.4. This can be changed as follows:: +The default installed IDF version is 5.2. This can be changed as follows:: - INSTALL_IDF_VER=5.2 $SMING_HOME/../Tools/install.sh esp32 + INSTALL_IDF_VER=4.4 $SMING_HOME/../Tools/install.sh esp32 The installation script creates a soft-link in ``/opt/esp-idf`` pointing to the last version installed. Use the `IDF_PATH` environment variable or change the soft-link to select which one to use. diff --git a/Sming/Arch/Esp32/Tools/install.cmd b/Sming/Arch/Esp32/Tools/install.cmd index cba5358357..8e758a62de 100644 --- a/Sming/Arch/Esp32/Tools/install.cmd +++ b/Sming/Arch/Esp32/Tools/install.cmd @@ -4,7 +4,7 @@ if "%IDF_PATH%"=="" goto :EOF if "%IDF_TOOLS_PATH%"=="" goto :EOF if "%IDF_REPO%"=="" set IDF_REPO="https://github.com/mikee47/esp-idf.git" -if "%INSTALL_IDF_VER%"=="" set INSTALL_IDF_VER=4.4 +if "%INSTALL_IDF_VER%"=="" set INSTALL_IDF_VER=5.2 set IDF_BRANCH="sming/release/v%INSTALL_IDF_VER%" git clone -b %IDF_BRANCH% %IDF_REPO% %IDF_PATH% diff --git a/Sming/Arch/Esp32/Tools/install.sh b/Sming/Arch/Esp32/Tools/install.sh index 0667e28704..a495ea2a73 100755 --- a/Sming/Arch/Esp32/Tools/install.sh +++ b/Sming/Arch/Esp32/Tools/install.sh @@ -36,7 +36,7 @@ if [ ! -L "$IDF_PATH" ] && [ -d "$IDF_PATH" ]; then mv "$IDF_PATH" "$IDF_PATH-old" fi -INSTALL_IDF_VER="${INSTALL_IDF_VER:=4.4}" +INSTALL_IDF_VER="${INSTALL_IDF_VER:=5.2}" IDF_CLONE_PATH="$(readlink -m "$IDF_PATH/..")/esp-idf-${INSTALL_IDF_VER}" IDF_REPO="${IDF_REPO:=https://github.com/mikee47/esp-idf.git}" IDF_BRANCH="sming/release/v${INSTALL_IDF_VER}" diff --git a/Sming/Arch/Esp32/app.mk b/Sming/Arch/Esp32/app.mk index 6cc8658584..f06d8165a6 100644 --- a/Sming/Arch/Esp32/app.mk +++ b/Sming/Arch/Esp32/app.mk @@ -21,8 +21,16 @@ ifdef IDF_TARGET_ARCH_RISCV --specs=nosys.specs endif + .PHONY: application application: $(TARGET_BIN) +ifeq ($(IDF_VERSION),v4.3) + @printf "\033[47;1;31mWARNING! ESP-IDF 4.3 reached 'End of Life' in December 2023.\033[0m Upgrade to v5.2 recommended.\n" +else ifeq ($(IDF_VERSION),v4.4) + @printf "\033[47;1;31mWARNING! ESP-IDF 4.4 support ends August 2024!\033[0m Upgrade to v5.2 recommended.\n" +else ifeq ($(IDF_VERSION),v5.0) + @printf "\033[47;1;34mNOTE! ESP-IDF 5.0 not recommended for new designs.\033[0m Please consider upgrading to v5.2.\n" +endif $(TARGET_OUT): $(COMPONENTS_AR) $(info $(notdir $(PROJECT_DIR)): Linking $@) diff --git a/Sming/Arch/Esp8266/Tools/install.cmd b/Sming/Arch/Esp8266/Tools/install.cmd index 33ec662a42..3c20108241 100644 --- a/Sming/Arch/Esp8266/Tools/install.cmd +++ b/Sming/Arch/Esp8266/Tools/install.cmd @@ -1,9 +1,8 @@ REM Esp8266 install.cmd -call :install "%ESP_HOME%" x86_64-w64-mingw32.xtensa-lx106-elf-e6a192b.201211.zip -goto :EOF +set EQT_REPO=https://github.com/earlephilhower/esp-quick-toolchain/releases/download/3.2.0-gcc10.3 +set EQT_TOOLCHAIN=x86_64-w64-mingw32.xtensa-lx106-elf-c791b74.230224.zip -:install -mkdir %1 -curl -Lo %DOWNLOADS%/%2 %SMINGTOOLS%/%2 -7z -o%1 x %DOWNLOADS%/%2 +mkdir %ESP_HOME% +curl -Lo %DOWNLOADS%/%EQT_TOOLCHAIN% %EQT_REPO%/%EQT_TOOLCHAIN% +7z -o%ESP_HOME% x %DOWNLOADS%/%EQT_TOOLCHAIN% diff --git a/Sming/Arch/Esp8266/Tools/install.sh b/Sming/Arch/Esp8266/Tools/install.sh index 29c1cb3fce..02ab45a130 100755 --- a/Sming/Arch/Esp8266/Tools/install.sh +++ b/Sming/Arch/Esp8266/Tools/install.sh @@ -2,13 +2,13 @@ # # Esp8266 install.sh -EQT_REPO=https://github.com/earlephilhower/esp-quick-toolchain/releases/download/3.0.0-newlib4.0.0-gnu20 +EQT_REPO="https://github.com/earlephilhower/esp-quick-toolchain/releases/download/3.2.0-gcc10.3" +EQT_TOOLCHAIN="$(uname -m)-linux-gnu.xtensa-lx106-elf-c791b74.230224.tar.gz" if [ -d "$ESP_HOME" ]; then printf "\n\n** Skipping Esp8266 tools installation: '$ESP_HOME' exists\n\n" else - TOOLCHAIN="$(uname -m)-linux-gnu.xtensa-lx106-elf-e6a192b.201211.tar.gz" - $WGET "$EQT_REPO/$TOOLCHAIN" -O "$DOWNLOADS/$TOOLCHAIN" + $WGET "$EQT_REPO/$EQT_TOOLCHAIN" -O "$DOWNLOADS/$EQT_TOOLCHAIN" mkdir -p "$ESP_HOME" - tar -zxf "$DOWNLOADS/$TOOLCHAIN" -C "$ESP_HOME" --totals + tar -zxf "$DOWNLOADS/$EQT_TOOLCHAIN" -C "$ESP_HOME" --totals fi diff --git a/Sming/Arch/Rp2040/Components/libc/component.mk b/Sming/Arch/Rp2040/Components/libc/component.mk index e316951715..8b3d1e35dc 100644 --- a/Sming/Arch/Rp2040/Components/libc/component.mk +++ b/Sming/Arch/Rp2040/Components/libc/component.mk @@ -8,4 +8,14 @@ LIBC_DEFSYMS := \ __wrap_vprintf=m_vprintf \ __wrap_printf=m_printf -EXTRA_LDFLAGS := $(call DefSym,$(LIBC_DEFSYMS)) +LIBC_UNDEFSYMS := \ + _close_r \ + _lseek_r \ + _read_r \ + _write_r \ + _isatty_r + + +EXTRA_LDFLAGS := \ + $(call DefSym,$(LIBC_DEFSYMS)) \ + $(call Undef,$(LIBC_UNDEFSYMS)) diff --git a/Sming/Arch/Rp2040/Components/libc/src/libc_replacements.c b/Sming/Arch/Rp2040/Components/libc/src/libc_replacements.c new file mode 100644 index 0000000000..8f33842417 --- /dev/null +++ b/Sming/Arch/Rp2040/Components/libc/src/libc_replacements.c @@ -0,0 +1,86 @@ +/* + libc_replacements.c - replaces libc functions with functions + from Espressif SDK + + Copyright (c) 2015 Ivan Grokhotkov. All rights reserved. + This file is part of the esp8266 core for Arduino environment. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + + Modified 03 April 2015 by Markus Sattler + + */ + +#include +#include +#include +#include +#include +#include + +int _open_r(struct _reent* unused, const char* ptr, int mode) +{ + (void)unused; + (void)ptr; + (void)mode; + return 0; +} + +int _close_r(struct _reent* unused, int file) +{ + (void)unused; + (void)file; + return 0; +} + +int _fstat_r(struct _reent* unused, int file, struct stat* st) +{ + (void)unused; + (void)file; + st->st_mode = S_IFCHR; + return 0; +} + +int _lseek_r(struct _reent* unused, int file, int ptr, int dir) +{ + (void)unused; + (void)file; + (void)ptr; + (void)dir; + return 0; +} + +int _read_r(struct _reent* unused, int file, char* ptr, int len) +{ + (void)unused; + (void)file; + (void)ptr; + (void)len; + return 0; +} + +int _write_r(struct _reent* r, int file, char* ptr, int len) +{ + (void)r; + if(file == STDOUT_FILENO) { + return m_nputs(ptr, len); + } + return 0; +} + +int _isatty_r(struct _reent* r, int fd) +{ + return 0; +} diff --git a/Sming/Arch/Rp2040/README.rst b/Sming/Arch/Rp2040/README.rst index e5eab5f052..7678906bd7 100644 --- a/Sming/Arch/Rp2040/README.rst +++ b/Sming/Arch/Rp2040/README.rst @@ -85,21 +85,30 @@ The following instructions should help. Compiler/linker The RP2040 contains two ARM Cortex-M0+ cores. Tools for all platforms can be downloaded from the - `ARM developer website `__. + `ARM developer website `__. Unzip the archive to a suitable location (e.g. ``/opt/rp2040`` or ``c:\tools\rp2040``) and set :envvar:`PICO_TOOLCHAIN_PATH` accordingly. .. note:: - At time of writing the Ubuntu repositories contain an older version of this toolchain. - It also does not contain GDB, but can be installed separately: + The Sming installer script can do this for you ``Tools/install.sh rp2040`` + You can alternatively use the toolchains provided in your GNU/Linux distribution. + + Ubuntu + :: sudo apt install gcc-arm-none-eabi gdb-multiarch - To use gdb-multiarch you'll need to do this: + To use gdb-multiarch you'll need to do this:: make gdb GDB=gdb-multiarch + Fedora + :: + sudo dnf install arm-none-eabi-gcc-cs-c++ arm-none-eabi-newlib + + The standard GDB appears to work OK. + Ninja This is used to build the RP2040 SDK code: diff --git a/Sming/Arch/Rp2040/Tools/install.cmd b/Sming/Arch/Rp2040/Tools/install.cmd index 7fd04c4f7e..cf77680df7 100644 --- a/Sming/Arch/Rp2040/Tools/install.cmd +++ b/Sming/Arch/Rp2040/Tools/install.cmd @@ -1,11 +1,11 @@ REM Rp2040 install.cmd if exist "%PICO_TOOLCHAIN_PATH%/arm-none-eabi" goto :already_got -set TOOLCHAIN_VERSION=10.3-2021.07 -set TOOLCHAIN_BASE_URL=https://developer.arm.com/-/media/Files/downloads/gnu-rm -set TOOLCHAIN_NAME=gcc-arm-none-eabi-%TOOLCHAIN_VERSION% -set TOOLCHAIN_FILE=%TOOLCHAIN_NAME%-win32.zip -curl -Lo tmp.zip %TOOLCHAIN_BASE_URL%/%TOOLCHAIN_VERSION%/%TOOLCHAIN_FILE% || goto :EOF +set TOOLCHAIN_VERSION=13.2.rel1 +set TOOLCHAIN_BASE_URL=https://developer.arm.com/-/media/Files/downloads/gnu +set TOOLCHAIN_NAME=arm-gnu-toolchain-%TOOLCHAIN_VERSION%-mingw-w64-i686-arm-none-eabi +set TOOLCHAIN_FILE=%TOOLCHAIN_NAME%.zip +curl -Lo tmp.zip %TOOLCHAIN_BASE_URL%/%TOOLCHAIN_VERSION%/binrel/%TOOLCHAIN_FILE% || goto :EOF 7z -o"%PICO_TOOLCHAIN_PATH%-tmp" x tmp.zip || goto :EOF del tmp.zip move "%PICO_TOOLCHAIN_PATH%-tmp/%TOOLCHAIN_NAME%" "%PICO_TOOLCHAIN_PATH%" diff --git a/Sming/Arch/Rp2040/Tools/install.sh b/Sming/Arch/Rp2040/Tools/install.sh index 03451153e5..00cc633dab 100755 --- a/Sming/Arch/Rp2040/Tools/install.sh +++ b/Sming/Arch/Rp2040/Tools/install.sh @@ -6,14 +6,13 @@ $PKG_INSTALL ninja-build if [ -d "$PICO_TOOLCHAIN_PATH/arm-none-eabi" ]; then printf "\n\n** Skipping Rp2040 tools installation: '$PICO_TOOLCHAIN_PATH' exists\n\n" -else - TOOLCHAIN_VERSION="10.3-2021.10" - TOOLCHAIN_BASE_URL="https://developer.arm.com/-/media/Files/downloads/gnu-rm" - TOOLCHAIN_NAME="gcc-arm-none-eabi-$TOOLCHAIN_VERSION" - TOOLCHAIN_FILE="$TOOLCHAIN_NAME-$(uname -m)-linux.tar.bz2" - TOOLCHAIN_URL="$TOOLCHAIN_BASE_URL/$TOOLCHAIN_VERSION/$TOOLCHAIN_FILE" +elif [ -n "$PICO_TOOLCHAIN_PATH" ]; then + TOOLCHAIN_VERSION="13.2.rel1" + TOOLCHAIN_BASE_URL="https://developer.arm.com/-/media/Files/downloads/gnu" + TOOLCHAIN_FILE="arm-gnu-toolchain-$TOOLCHAIN_VERSION-$(uname -m)-arm-none-eabi.tar.xz" + TOOLCHAIN_URL="$TOOLCHAIN_BASE_URL/$TOOLCHAIN_VERSION/binrel/$TOOLCHAIN_FILE" $WGET "$TOOLCHAIN_URL" -O "$DOWNLOADS/$TOOLCHAIN_FILE" mkdir -p "$PICO_TOOLCHAIN_PATH" - tar -jxf "$DOWNLOADS/$TOOLCHAIN_FILE" -C "$PICO_TOOLCHAIN_PATH" --totals --transform='s|^/*||' - mv "$PICO_TOOLCHAIN_PATH/$TOOLCHAIN_NAME/"* "$PICO_TOOLCHAIN_PATH" + tar -xf "$DOWNLOADS/$TOOLCHAIN_FILE" -C "$PICO_TOOLCHAIN_PATH" --totals --transform='s|^/*||' + mv "$PICO_TOOLCHAIN_PATH/"*/* "$PICO_TOOLCHAIN_PATH" fi diff --git a/Sming/Arch/Rp2040/app.mk b/Sming/Arch/Rp2040/app.mk index c8c0c32f0f..f96bc1d8f0 100644 --- a/Sming/Arch/Rp2040/app.mk +++ b/Sming/Arch/Rp2040/app.mk @@ -6,6 +6,7 @@ # linker flags used to generate the main object file LDFLAGS += \ + -Wl,--no-warn-rwx-segments \ -Wl,--build-id=none \ --specs=nosys.specs \ -mcpu=cortex-m0plus \ diff --git a/Sming/Arch/Rp2040/build.mk b/Sming/Arch/Rp2040/build.mk index 77cb67856f..810d3d9057 100644 --- a/Sming/Arch/Rp2040/build.mk +++ b/Sming/Arch/Rp2040/build.mk @@ -26,11 +26,10 @@ ifeq (,$(wildcard $(PICO_TOOLCHAIN_PATH)/$(CONFIG_TOOLPREFIX))) $(error PICO_TOOLCHAIN_PATH not set correctly: $(PICO_TOOLCHAIN_PATH)) endif else -PICO_TOOLCHAIN_PATH := $(shell which $(CONFIG_TOOLPREFIX)-gcc) -ifeq (,$(PICO_TOOLCHAIN_PATH)) +ifeq (,$(shell which $(CONFIG_TOOLPREFIX)-gcc)) $(error Toolchain not found, maybe set PICO_TOOLCHAIN_PATH) endif -TOOLSPEC := $(dir $(PICO_TOOLCHAIN_PATH))$(CONFIG_TOOLPREFIX)- +TOOLSPEC := $(CONFIG_TOOLPREFIX)- endif # select which tools to use as assembler, compiler, librarian and linker @@ -42,7 +41,7 @@ LD := $(TOOLSPEC)gcc NM := $(TOOLSPEC)nm OBJCOPY := $(TOOLSPEC)objcopy OBJDUMP := $(TOOLSPEC)objdump -GDB := $(TOOLSPEC)gdb +GDB := gdb CPPFLAGS += \ -nostdlib diff --git a/docs/source/upgrading/5.1-5.2.rst b/docs/source/upgrading/5.1-5.2.rst index e06142934a..9d3303d4e1 100644 --- a/docs/source/upgrading/5.1-5.2.rst +++ b/docs/source/upgrading/5.1-5.2.rst @@ -41,3 +41,16 @@ Espressif toolchains use forks: esp8266: https://github.com/earlephilhower/newlib-xtensa/blob/xtensa-4_0_0-lock-arduino/newlib/libc/ esp32: https://github.com/espressif/newlib-esp32/blob/esp-4.3.0/newlib/libc/ + + +**Toolchain versions updated** + +Esp8266 + The installer has been updated to use the latest toolchain (Feb 23), gcc 10.3. + +Esp32 IDF + The installation scripts now install IDF version 5.2 by default. + See :ref:`idf_versions` for further details. + +RP2040 + The installer has been updated to use the latest toolchain (Oct 23), gcc 13.2. From 90511f14152cbaa753d947fa9be70febbed3a57c Mon Sep 17 00:00:00 2001 From: Mike Date: Wed, 5 Jun 2024 09:17:51 +0100 Subject: [PATCH 063/128] Documentation/CI fixes (#2783) This PR contains a few minor fixes relating to CI testing and documentation. There are no functional changes to the framework. **Documentation fixes** - Noticed that `:issue:` links don't work! Just a typo in the URL... - The `Storage` component API documentation contained everything from the FatIFS and DiskStorage libraries too since they live in the same namespace. I've broken things out a bit to avoid that. - Doxygen API index page isn't appearing at https://sming.readthedocs.io/en/latest/api/, just the placeholder page. The pages are all there but Sphinx hasn't overwritten the placeholder index.html as it should do. Fixed by adding a manual copy at end of sphinx build. Tested on readthedocs and locally. - Document `STRICT` setting and add upgrade note about Y2038 warning. - Add tips to vscode documentation. - Upgrade sphinx to latest 7.3.7 **Test fixes** - Simplify HostTests partition layout - Fix uf2conf python regex warning --- .gitignore | 7 +-- Sming/Arch/Rp2040/Components/uf2/uf2conv.py | 2 +- Sming/Components/IFS | 2 +- Sming/Components/Storage/README.rst | 57 ++++++++++++++++++- .../src/include/Storage/StreamDevice.h | 9 +++ Sming/README.rst | 30 +++++++++- docs/.gitignore | 5 ++ docs/requirements.txt | 2 +- docs/source/conf.py | 24 ++++++-- docs/source/link-roles.py | 2 +- docs/source/tools/vscode.rst | 27 +++++++++ docs/source/upgrading/5.1-5.2.rst | 23 +++++--- tests/HostTests/host-tests-1m.hw | 6 +- tests/HostTests/host-tests.hw | 36 +----------- .../modules/Network/Arch/Host/HttpRequest.cpp | 5 +- 15 files changed, 169 insertions(+), 68 deletions(-) create mode 100644 docs/.gitignore diff --git a/.gitignore b/.gitignore index 70e63abe10..180c1a5cc0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,8 @@ # generated binaries out -# generated documentation -docs/api -docs/build -docs/source/_inc -*.pyc +# Python cache +__pycache__ # Eclipse .project diff --git a/Sming/Arch/Rp2040/Components/uf2/uf2conv.py b/Sming/Arch/Rp2040/Components/uf2/uf2conv.py index 06beeb27f2..d7fc2d95da 100644 --- a/Sming/Arch/Rp2040/Components/uf2/uf2conv.py +++ b/Sming/Arch/Rp2040/Components/uf2/uf2conv.py @@ -176,7 +176,7 @@ def get_drives(): "get", "DeviceID,", "VolumeName,", "FileSystem,", "DriveType"]) for line in r.decode().split('\n'): - words = re.split('\s+', line) + words = re.split(r'\s+', line) if len(words) >= 3 and words[1] == "2" and words[2] == "FAT": drives.append(words[0]) else: diff --git a/Sming/Components/IFS b/Sming/Components/IFS index af9ddbc666..4db9a92b7e 160000 --- a/Sming/Components/IFS +++ b/Sming/Components/IFS @@ -1 +1 @@ -Subproject commit af9ddbc666a29b582cd3f1f4ed40a9848a91fe0a +Subproject commit 4db9a92b7e893a0406f1c29ca30072adb676e753 diff --git a/Sming/Components/Storage/README.rst b/Sming/Components/Storage/README.rst index f2818c0f13..9a08f5e44b 100644 --- a/Sming/Components/Storage/README.rst +++ b/Sming/Components/Storage/README.rst @@ -343,7 +343,9 @@ Entries are fixed 32-byte structures, :cpp:class:`Storage::esp_partition_info_t` Partition API ------------- -This is a C++ interface. Some examples:: +This is a C++ interface. Some examples: + +.. code-block:: c++ Storage::Partition part = Storage::findPartition("spiffs0"); // Find by name if(part) { @@ -371,7 +373,9 @@ This is usually :cpp:var:`Storage::spiFlash` for the main flash device. Other devices must be registered via :cpp:func:`Storage::PartitionTable::registerStorageDevice`. -You can query partition entries from a Storage object directly, for example:: +You can query partition entries from a Storage object directly, for example: + +.. code-block:: c++ #include @@ -400,5 +404,52 @@ See :library:`DiskStorage` for how devices such as SD flash cards are managed. API --- -.. doxygennamespace:: Storage +Core Functions +~~~~~~~~~~~~~~ + +.. doxygenfunction:: Storage::initialize +.. doxygenfunction:: Storage::getDevices +.. doxygenfunction:: Storage::registerDevice +.. doxygenfunction:: Storage::unRegisterDevice +.. doxygenfunction:: Storage::findDevice +.. doxygenfunction:: Storage::findPartition(const String&) +.. doxygenfunction:: Storage::findPartition(Partition::Type, uint8_t) +.. doxygenvariable:: Storage::spiFlash + + +Main classes +~~~~~~~~~~~~ + +.. doxygenclass:: Storage::Device :members: +.. doxygenclass:: Storage::SpiFlash + :members: +.. doxygenclass:: Storage::Partition + :members: +.. doxygenclass:: Storage::PartitionTable + :members: +.. doxygenclass:: Storage::FileDevice + :members: + + +Streaming +~~~~~~~~~ + +.. doxygenenum:: Storage::Mode +.. doxygenclass:: Storage::PartitionStream + :members: +.. doxygenclass:: Storage::StreamDevice + :members: + + +Debugging +~~~~~~~~~ + +.. doxygennamespace:: Storage::Debug + :members: + +.. doxygenvariable:: Storage::progMem +.. doxygenclass:: Storage::ProgMem + +.. doxygenvariable:: Storage::sysMem +.. doxygenclass:: Storage::SysMem diff --git a/Sming/Components/Storage/src/include/Storage/StreamDevice.h b/Sming/Components/Storage/src/include/Storage/StreamDevice.h index 7e9bd08906..0ebbfb2dca 100644 --- a/Sming/Components/Storage/src/include/Storage/StreamDevice.h +++ b/Sming/Components/Storage/src/include/Storage/StreamDevice.h @@ -14,10 +14,19 @@ namespace Storage class StreamDevice : public Device { public: + /** + * @brief Create a Device object using data from a stream with restricted size + * @param stream Backing data source for this device + * @param size Size of device in bytes + */ StreamDevice(IDataSourceStream* stream, size_t size) : Device(nameOf(stream), size), mStream(stream) { } + /** + * @brief Create a Device object using data from a stream with all available data + * @param stream Backing data source for this device + */ StreamDevice(IDataSourceStream* stream) : StreamDevice(stream, size_t(stream->available())) { } diff --git a/Sming/README.rst b/Sming/README.rst index 52b153693f..aa6d1e23a0 100644 --- a/Sming/README.rst +++ b/Sming/README.rst @@ -49,14 +49,38 @@ Change it like this:: .. note:: If you change these settings and want them applied to Sming, not just your project, then you'll - need to recompile all components like this: - - :: + need to recompile all components like this:: make components-clean make DEBUG_VERBOSE_LEVEL=3 +.. envvar:: STRICT + + * default: undefined (standard warnings, treat as errors) + * 1: Enable all warnings but do not treat as errors + + By default, Sming builds code with a few warnings disabled: + + - sign-compare: Comparison between signed and unsigned values could produce an incorrect result when the signed value is converted to unsigned. + - parentheses: Parentheses are omitted in certain contexts. + - unused-variable: A local or static variable is unused aside from its declaration. + - unused-but-set-variable: A local variable is assigned to, but otherwise unused. + - strict-aliasing: Casts which can violate alignment rules. + + These can be indicative of problematic code rather than errors. + Because new compiler releases can increase the level of checking, this set may also need to change + but it is kept minimal. See ``Sming/build.mk`` for the actual settings. + See https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html for further details + about all compiler settings. + See also :doc:`/information/develop/clang-tools`. + + Any remaining warnings will be treated as errors and compilation will be halted. + + It is a good idea to check your codebase with ``STRICT=1`` which enables **all** warnings. + However, it does not treat them as errors because of the above which could be false-positives. + + Release builds ~~~~~~~~~~~~~~ diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000000..78f79e1bca --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,5 @@ +# generated documentation +api +build +source/_inc +*.pyc diff --git a/docs/requirements.txt b/docs/requirements.txt index bbb9040a49..f753d06f17 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,6 +1,6 @@ # Requirements file for pip # list of Python packages used in documentation build -sphinx==7.2.6 +sphinx==7.3.7 sphinx-rtd-theme==2.0.0 m2r2==0.3.3.post2 breathe==4.35.0 diff --git a/docs/source/conf.py b/docs/source/conf.py index ed62d3a349..cf48957882 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -14,13 +14,12 @@ import os import sys import subprocess -from docutils import nodes, utils -from sphinx import roles, addnodes -from sphinx.util.nodes import set_role_source_info, split_explicit_title +from sphinx.util import logging # So our custom extensions can be found sys.path.insert(0, os.path.abspath('.')) from filemap import buildFileMap +logger = logging.getLogger(__name__) # -- Project information ----------------------------------------------------- @@ -126,7 +125,7 @@ # # on_rtd is whether we are on readthedocs.org env_readthedocs = os.environ.get('READTHEDOCS', None) -print(env_readthedocs) +logger.info(env_readthedocs) if env_readthedocs: # Start the build from a clean slate @@ -141,3 +140,20 @@ subprocess.call('make -C .. setup api API_VERSION="' + version + '"', shell=True) buildFileMap(html_context) + + +def setup(app): + ''' + Sphinx requires a placeholder 'api/index.rst' which is overwritten by the doxygen-generated version + via the above `html_extra_path` setting above. + As of Sphinx 7.3.7 (and 7.2.6) this doesn't happen, though it does if run a second time! + Simple fix is to just copy the one `index.html` file after sphinx has done it's build. + ''' + def fix_api_index(app, exception): + import shutil + src_file = os.path.join(app.srcdir, '../api/html/api/index.html') + dst_path = os.path.join(app.outdir, 'api') + logger.info(f'>> Copy from "{src_file}" -> "{dst_path}"') + shutil.copy(src_file, dst_path) + + app.connect('build-finished', fix_api_index) diff --git a/docs/source/link-roles.py b/docs/source/link-roles.py index bbf7152f0c..284cd58ff1 100644 --- a/docs/source/link-roles.py +++ b/docs/source/link-roles.py @@ -33,7 +33,7 @@ def get_github_rev(): def setup(app): app.add_role('source', SourceRole()) - app.add_role('issue', autolink('Issue #{0} <' + github_url + '/issue/{0}>')) + app.add_role('issue', autolink('Issue #{0} <' + github_url + '/issues/{0}>')) app.add_role('pull-request', autolink('Pull Request #{0} <' + github_url + '/pull/{0}>')) app.add_role('sample', SampleRole) diff --git a/docs/source/tools/vscode.rst b/docs/source/tools/vscode.rst index bf5d254aa7..c930d9097d 100644 --- a/docs/source/tools/vscode.rst +++ b/docs/source/tools/vscode.rst @@ -13,6 +13,17 @@ Software involved - `Visual Studio Code `__ - `C/C++ extension `__ +.. note:: + + Linux users may prefer `VSCodium `__ which does not contain telemetry or tracking. + + Standard C/C++ language support is available via the `cpptools `__ + extension which is not available in the vscodium repositories. + + Visit https://marketplace.visualstudio.com/items?itemName=ms-vscode.cpptools and go to ``Version History`` + to download the .vsix file. Open the ``Extensions`` pane in vscodium and drag the file there to install, + or run ``codium --install-extension NAME-OF-FILE.vsix``. + Installation ------------ @@ -70,6 +81,22 @@ To debug your application, follow these steps: VS Code debug selection +Editor window titles +-------------------- + +As Sming is a multi-architecture framework there are lots of files with the same name. +By default editor window titles contain only the filename, but in vscode this can be changed +to something more useful, like including the parent directory name. + +Open user settings JSON (via F1 hotkey) and add this to the config: + +.. code-block:: json + + "workbench.editor.customLabels.patterns": { + "**/*.*": "${dirname}/${filename}.${extname}" + } + + Manual configuration changes ---------------------------- diff --git a/docs/source/upgrading/5.1-5.2.rst b/docs/source/upgrading/5.1-5.2.rst index 9d3303d4e1..6eb1099064 100644 --- a/docs/source/upgrading/5.1-5.2.rst +++ b/docs/source/upgrading/5.1-5.2.rst @@ -10,12 +10,14 @@ The intended default behaviour is read-only, however previously this also allowe This can result in corrupted flash contents where the flash has not been explicitly erased beforehand. The new constructors instead use a :cpp:enum:`Storage::Mode` so behaviour is more explicit. -The default is read-only and writes will now be failed. +The default is read-only and writes will now be failed. For example:: + + Storage::PartitionStream stream(myPartition, Storage::Mode::BlockErase); **64-bit time_t** -There is some variability in whether `time_t` is 32 or 64 bits. See issue #2758. +There is some variability in whether `time_t` is 32 or 64 bits. See :issue:`2758`. This is dependent on the toolchain and accompanying C library. @@ -25,11 +27,18 @@ This is dependent on the toolchain and accompanying C library. - Windows Host (using mingw) - Linux host builds prior to Sming v5.2 +Building for these will generate a warning ``**Y2038** time_t is only 32-bits in this build configuration``. +If you cannot upgrade then build with :envvar:`STRICT` set to 1. + Range of time_t: - -0x80000000: 1901-12-13 20:45:52 - 0x00000000: 1970-01-01 00:00:00 - 0x7fffffff: 2038-01-19 03:14:07 +=========== =================== +value Timestamp +=========== =================== +-0x80000000 1901-12-13 20:45:52 +0x00000000 1970-01-01 00:00:00 +0x7fffffff 2038-01-19 03:14:07 +=========== =================== All others use 64-bit values. @@ -39,8 +48,8 @@ Rp2040 builds with standard ARM toolkit so probably accommodated by the standard Espressif toolchains use forks: -esp8266: https://github.com/earlephilhower/newlib-xtensa/blob/xtensa-4_0_0-lock-arduino/newlib/libc/ -esp32: https://github.com/espressif/newlib-esp32/blob/esp-4.3.0/newlib/libc/ +- esp8266: https://github.com/earlephilhower/newlib-xtensa/blob/xtensa-4_0_0-lock-arduino/newlib/libc/ +- esp32: https://github.com/espressif/newlib-esp32/blob/esp-4.3.0/newlib/libc/ **Toolchain versions updated** diff --git a/tests/HostTests/host-tests-1m.hw b/tests/HostTests/host-tests-1m.hw index 3ad4348efc..52d4449b59 100644 --- a/tests/HostTests/host-tests-1m.hw +++ b/tests/HostTests/host-tests-1m.hw @@ -5,12 +5,10 @@ "1m" ], "partitions": { - "fwfs0": { - "address": "0x000c0000", - "size": "128K" + "fwfs_httprequest": { + "address": "0x000c0000" }, "spiffs0": { - "size": "0x10000", "address": "0x000e0000" } } diff --git a/tests/HostTests/host-tests.hw b/tests/HostTests/host-tests.hw index 33131de523..4ef994422c 100644 --- a/tests/HostTests/host-tests.hw +++ b/tests/HostTests/host-tests.hw @@ -1,20 +1,14 @@ { "name": "Host Tests profile", "base_config": "spiffs", - "devices": { - "testDevice": { - "type": "spiram", - "size": "0x40000000" - } - }, "partitions": { "spiffs0": { "size": "0x10000", "filename": "" }, - "fwfs0": { + "fwfs_httprequest": { "address": "0x220000", - "size": "0x40000", + "size": "0x20000", "type": "data", "subtype": "fwfs", "filename": "$(FW_BASE)/fwfs0.bin", @@ -22,32 +16,6 @@ "target": "fwfs-build", "config": "fwfs0.json" } - }, - "external1": { - "device": "testDevice", - "address": 0, - "size": "4M", - "type": "data", - "subtype": "spiffs", - "filename": "$(FW_BASE)/test-spiffs.bin", - "build": { - "target": "spiffsgen", - "files": "resource" - } - }, - "external2": { - "device": "testDevice", - "address": "0x600000", - "size": "240K", - "type": "data", - "subtype": 37 - }, - "external3": { - "device": "testDevice", - "address": "0x800000", - "size": "240M", - "type": "data", - "subtype": "nvs" } } } \ No newline at end of file diff --git a/tests/HostTests/modules/Network/Arch/Host/HttpRequest.cpp b/tests/HostTests/modules/Network/Arch/Host/HttpRequest.cpp index 3941747b1b..3b287111e9 100644 --- a/tests/HostTests/modules/Network/Arch/Host/HttpRequest.cpp +++ b/tests/HostTests/modules/Network/Arch/Host/HttpRequest.cpp @@ -42,10 +42,7 @@ class HttpRequestTest : public TestGroup return; } - auto fs = IFS::createFirmwareFilesystem(*Storage::findPartition(Storage::Partition::SubType::Data::fwfs)); - CHECK(fs != nullptr); - CHECK(fs->mount() == FS_OK); - fileSetFileSystem(fs); + REQUIRE(fwfs_mount(Storage::findPartition("fwfs_httprequest"))); server->listen(80); server->paths.setDefault([](HttpRequest& request, HttpResponse& response) { From 307d6db7fb579752450c168b38c6253824e16f0f Mon Sep 17 00:00:00 2001 From: Mike Date: Thu, 6 Jun 2024 11:56:57 +0100 Subject: [PATCH 064/128] Use physical serial port if present on WSL2 (#2790) WSL2 doesn't natively support USB devices such as serial ports, so Sming uses runs the python terminal application inside Windows natively using powershell. However, as discussed in #2789 this prevents software such as [usbipd](https://github.com/dorssel/usbipd-win) from working. This PR adds an additional check when running inside WSL2 to see if the nominated port, via `COM_PORT`, is actually present. If so, then it runs the python terminal application normally. If the path isn't present then it is assumed to be a Windows port and powershell is invoked as usual. Because the `TERMINAL` environment variable is cached by Sming this can cause issues. To maintain existing behaviour this variable is still cached but left empty to indicate default behaviour. If `TERMINAL` has been redefined then that will be run instead. This will require `make config-clean` to clear the existing cached value. --- Sming/Arch/Rp2040/Components/uf2/component.mk | 2 +- Sming/Components/terminal/README.rst | 1 + Sming/Components/terminal/component.mk | 11 ++++++----- Sming/build.mk | 2 ++ Sming/component.mk | 4 ++-- Sming/project.mk | 2 +- docs/source/getting-started/windows/wsl.rst | 13 ++++++++++++- 7 files changed, 25 insertions(+), 10 deletions(-) diff --git a/Sming/Arch/Rp2040/Components/uf2/component.mk b/Sming/Arch/Rp2040/Components/uf2/component.mk index a2b36926e0..a2f33922a9 100644 --- a/Sming/Arch/Rp2040/Components/uf2/component.mk +++ b/Sming/Arch/Rp2040/Components/uf2/component.mk @@ -9,7 +9,7 @@ DEBUG_VARS += UF2CONV_PY UF2CONV_PY := $(COMPONENT_PATH)/uf2conv.py -# Invoke uf2conf utility +# Invoke uf2conv utility # $1 -> Parameters ifdef WSL_ROOT Uf2Conv = powershell.exe -Command "$(PYTHON) $(UF2CONV_PY) $(if $V,--verbose) $1" diff --git a/Sming/Components/terminal/README.rst b/Sming/Components/terminal/README.rst index a8d7e94235..9f003c164e 100644 --- a/Sming/Components/terminal/README.rst +++ b/Sming/Components/terminal/README.rst @@ -33,6 +33,7 @@ Options .. envvar:: TERMINAL Command line to use when running ``make terminal``. + This is normally empty (undefined) which causes the default python terminal application to run. Redefine if you want to use a different terminal application. .. envvar:: KILL_TERM diff --git a/Sming/Components/terminal/component.mk b/Sming/Components/terminal/component.mk index 959cc6f2c3..461fb425f1 100644 --- a/Sming/Components/terminal/component.mk +++ b/Sming/Components/terminal/component.mk @@ -17,11 +17,6 @@ endif CACHE_VARS += COM_OPTS KILL_TERM TERMINAL COM_OPTS ?= --raw --encoding ascii --rts 0 --dtr 0 KILL_TERM ?= pkill -9 -f "$(COM_PORT) $(COM_SPEED_SERIAL)" || exit 0 -ifdef WSL_ROOT -TERMINAL ?= powershell.exe -Command "python -m serial.tools.miniterm $(COM_OPTS) $(COM_PORT) $(COM_SPEED_SERIAL)" -else -TERMINAL ?= $(PYTHON) -m serial.tools.miniterm $(COM_OPTS) $(COM_PORT) $(COM_SPEED_SERIAL) -endif ##@Tools @@ -33,4 +28,10 @@ kill_term: .PHONY: terminal terminal: kill_term ##Open the serial terminal +ifdef TERMINAL $(TERMINAL) +else ifeq ($(WSL_COMPORT_POWERSHELL),1) + powershell.exe -Command "python -m serial.tools.miniterm $(COM_OPTS) $(COM_PORT) $(COM_SPEED_SERIAL)" +else + $(PYTHON) -m serial.tools.miniterm $(COM_OPTS) $(COM_PORT) $(COM_SPEED_SERIAL) +endif diff --git a/Sming/build.mk b/Sming/build.mk index 5dab2d4903..69ca8b885c 100644 --- a/Sming/build.mk +++ b/Sming/build.mk @@ -94,6 +94,8 @@ else ifeq ($(UNAME), Linux) ifdef WSL_DISTRO_NAME DEBUG_VARS += WSL_ROOT WSL_ROOT := //wsl$$/$(WSL_DISTRO_NAME) + # If serial device is available, use it directly, otherwise via powershell + WSL_COMPORT_POWERSHELL = $(if $(wildcard $(COM_PORT)),,1) endif else ifeq ($(UNAME), Darwin) #OS X diff --git a/Sming/component.mk b/Sming/component.mk index 21c69dc820..8898690acf 100644 --- a/Sming/component.mk +++ b/Sming/component.mk @@ -128,9 +128,9 @@ flash: all kill_term ##Write the boot loader and all defined partition images $(Q) $(call CheckPartitionChunks,$(FLASH_PARTITION_CHUNKS)) $(call WriteFlash,$(FLASH_BOOT_CHUNKS) $(FLASH_MAP_CHUNK) $(FLASH_PARTITION_CHUNKS)) ifeq ($(ENABLE_GDB), 1) - $(GDB_CMDLINE) + $(MAKE) gdb else ifneq ($(SMING_ARCH),Host) - $(TERMINAL) + $(MAKE) terminal endif .PHONY: mergeflash diff --git a/Sming/project.mk b/Sming/project.mk index 8f09896449..412a56ff07 100644 --- a/Sming/project.mk +++ b/Sming/project.mk @@ -589,7 +589,7 @@ TCP_SERIAL_REDIRECT = $(SMING_HOME)/../Tools/tcp_serial_redirect.py $(COM_PORT) .PHONY: tcp-serial-redirect tcp-serial-redirect: ##Redirect COM port to TCP port $(info Starting serial redirector) -ifdef WSL_ROOT +ifeq ($(WSL_COMPORT_POWERSHELL),1) $(Q) cmd.exe /c start /MIN python3 $(WSL_ROOT)/$(TCP_SERIAL_REDIRECT) else $(Q) $(call DetachCommand,$(PYTHON) $(TCP_SERIAL_REDIRECT)) diff --git a/docs/source/getting-started/windows/wsl.rst b/docs/source/getting-started/windows/wsl.rst index 1061aeccce..0107d9851c 100644 --- a/docs/source/getting-started/windows/wsl.rst +++ b/docs/source/getting-started/windows/wsl.rst @@ -91,7 +91,7 @@ This will ensure that the build system can run python scripts either in WSL2 or Flashing devices ---------------- -WSL2 does not currently support access to USB serial devices, so the Sming build system runs +WSL2 does not natively support access to USB serial devices, so the Sming build system runs the appropriate application directly under Windows using ``powershell.exe``. Therefore, use the normal Windows COM port name rather than the linux ones (such as /dev/ttyUSB0):: @@ -99,6 +99,17 @@ Therefore, use the normal Windows COM port name rather than the linux ones (such make flash COM_PORT=COM4 +Some USB serial adapters are supported by the `usbipd `__ project. +If so, then devices such as ``/dev/ttyUSB0``, etc. will be present as usual. +If Sming sees that ``COM_PORT`` actually exists when running in WSL2 then the above powershell workaround +will not be used. + +.. note:: + + The :envvar:`TERMINAL` environment variable is cached by Sming so if the terminal isn't running as expected + try ``make config-clean`` to clear the cached value. + + Serial debugging ---------------- From 7e11439bcad9afd3cec48808e0d9f38d51319bbf Mon Sep 17 00:00:00 2001 From: Mike Date: Fri, 7 Jun 2024 08:41:25 +0100 Subject: [PATCH 065/128] esptool needs same comport fix for WSL2 (#2791) Further to #2790 esptool of course uses COM ports and same issue applies when physical ports available in WSL2. --- Sming/Components/esptool/component.mk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sming/Components/esptool/component.mk b/Sming/Components/esptool/component.mk index 330f82a5b1..ed600e1fe9 100644 --- a/Sming/Components/esptool/component.mk +++ b/Sming/Components/esptool/component.mk @@ -28,7 +28,7 @@ ESPTOOL_CMDLINE := $(PYTHON) $(ESPTOOL) \ # USB serial ports are not available under WSL2, # but we can use powershell with the regular Windows COM port # $1 -> Arguments -ifdef WSL_ROOT +ifeq ($(WSL_COMPORT_POWERSHELL),1) ESPTOOL_EXECUTE = powershell.exe -Command "$(ESPTOOL_CMDLINE) $1" else ESPTOOL_EXECUTE = $(ESPTOOL_CMDLINE) $1 From 8a2dcc7e9f73c37ddbc015665c09b28dadedb6f0 Mon Sep 17 00:00:00 2001 From: Mike Date: Fri, 7 Jun 2024 08:51:58 +0100 Subject: [PATCH 066/128] Fix SSL issues (#2788) This PR aims to address some issues encountered whilst trying to use SSL for a basic `HttpClient` download session. **Fix malloc_count link error 'undefined reference to __wrap_strdup`** Implementation also needs to call mc_malloc, not malloc. **AxCertificate destructor accesses ssl after it's been destroyed** Picked up by valgrind. **Provide time implementations in RTC.cpp, add test** Library code requires libc implementations for `gettimeofday` and `time_t`. On Esp8266 typically get `please start sntp first !` message. This should be synced with `SystemClock` so removed the `time replacement` code from AXTLS and use that. Test added to HostTests to ensure SystemClock and `time()` are synced. Checked on esp8266, rp2040, esp32s2, host. **Replace automatic SSL certificate generation with `generate-cert` build target** These don't need to be auto-generated as they're not always required. There are also multiple ways to get this information into an application. Several samples don't make use of these files, so removed. NOTE: The `make_certs.sh` script no longer appears to work, at least with openssl 3.2.1 (Jan 2024). The headers are generated but Axtls fails to load the certificate with -269 (SSL_ERROR_INVALID_KEY). **Put generated SSL certificate information into PROGMEM** Bit wasteful of RAM. **Enforce consistent 'verifyLater' behaviour with Bearssl** When attempting to fetch an https resource (using HttpClient) *without* setting request `onSslInit` we get this behaviour: - Axtls: Fails with `X509_VFY_ERROR_NO_TRUSTED_CERT` - Bearssl: No problem, goes right ahead. This behaviour with Bearssl is not desirable as it could inadvertently compromise security. Add a check on `verifyLater` and fail with `X509_NOT_TRUSTED` as appropriate. **Notes** - Add `setSslInitHandler` method to HttpClient? NO ! `Use request->onSslInit` - Does this work with lwip2 on esp8266? Doesn't appear to make things worse, but lwip2 looks to be kinda broken. Needs major update. - Certificate generation throws errors with openssl 3, this needs addressing separately as it's only really appropriate for basic testing anyway. --- Sming/Arch/Esp32/Platform/RTC.cpp | 6 + Sming/Arch/Esp8266/Platform/RTC.cpp | 23 +++- Sming/Arch/Rp2040/Platform/RTC.cpp | 8 ++ Sming/Components/axtls-8266/axtls-8266.patch | 130 +----------------- .../Components/malloc_count/malloc_count.cpp | 53 ++++--- Sming/Components/ssl/Axtls/AxConnection.cpp | 6 +- Sming/Components/ssl/Axtls/AxConnection.h | 1 + Sming/Components/ssl/Axtls/AxContext.cpp | 10 +- .../ssl/BearSsl/BrClientConnection.cpp | 6 +- Sming/Components/ssl/BearSsl/BrConnection.cpp | 20 +-- Sming/Components/ssl/BearSsl/BrContext.cpp | 4 +- Sming/Components/ssl/BearSsl/BrPrivateKey.cpp | 2 +- Sming/Components/ssl/BearSsl/BrPublicKey.cpp | 2 +- .../ssl/BearSsl/BrServerConnection.cpp | 4 +- Sming/Components/ssl/BearSsl/X509Context.h | 14 +- Sming/Components/ssl/README.rst | 99 +++++++++++++ Sming/Components/ssl/Tools/make_certs.sh | 8 +- Sming/Components/ssl/component.mk | 8 +- Sming/Components/ssl/src/KeyCertPair.cpp | 8 +- docs/source/experimental/httpserver-ssl.rst | 83 ----------- docs/source/experimental/index.rst | 1 - docs/source/upgrading/5.1-5.2.rst | 12 ++ samples/Basic_AWS/component.mk | 2 +- samples/Basic_AWS/include/ssl/cert.h | 27 ---- samples/Basic_AWS/include/ssl/private_key.h | 34 ----- samples/Basic_Ssl/component.mk | 2 +- samples/Basic_Ssl/include/ssl/cert.h | 27 ---- samples/Basic_Ssl/include/ssl/private_key.h | 35 ----- samples/Echo_Ssl/component.mk | 2 +- samples/Echo_Ssl/include/ssl/cert.h | 27 ---- samples/Echo_Ssl/include/ssl/private_key.h | 35 ----- samples/HttpClient/include/ssl/cert.h | 27 ---- samples/HttpClient/include/ssl/private_key.h | 35 ----- .../include/ssl/cert.h | 27 ---- .../include/ssl/private_key.h | 35 ----- samples/MqttClient_Hello/include/ssl/cert.h | 2 +- .../include/ssl/private_key.h | 2 +- samples/SmtpClient/component.mk | 2 +- samples/SmtpClient/include/ssl/cert.h | 27 ---- samples/SmtpClient/include/ssl/private_key.h | 34 ----- samples/Websocket_Client/include/ssl/cert.h | 27 ---- .../include/ssl/private_key.h | 35 ----- tests/HostTests/modules/DateTime.cpp | 28 ++++ 43 files changed, 268 insertions(+), 712 deletions(-) delete mode 100644 docs/source/experimental/httpserver-ssl.rst delete mode 100644 samples/Basic_AWS/include/ssl/cert.h delete mode 100644 samples/Basic_AWS/include/ssl/private_key.h delete mode 100644 samples/Basic_Ssl/include/ssl/cert.h delete mode 100644 samples/Basic_Ssl/include/ssl/private_key.h delete mode 100644 samples/Echo_Ssl/include/ssl/cert.h delete mode 100644 samples/Echo_Ssl/include/ssl/private_key.h delete mode 100644 samples/HttpClient/include/ssl/cert.h delete mode 100644 samples/HttpClient/include/ssl/private_key.h delete mode 100644 samples/HttpServer_ConfigNetwork/include/ssl/cert.h delete mode 100644 samples/HttpServer_ConfigNetwork/include/ssl/private_key.h delete mode 100644 samples/SmtpClient/include/ssl/cert.h delete mode 100644 samples/SmtpClient/include/ssl/private_key.h delete mode 100644 samples/Websocket_Client/include/ssl/cert.h delete mode 100644 samples/Websocket_Client/include/ssl/private_key.h diff --git a/Sming/Arch/Esp32/Platform/RTC.cpp b/Sming/Arch/Esp32/Platform/RTC.cpp index b5b721f806..0b6236544e 100644 --- a/Sming/Arch/Esp32/Platform/RTC.cpp +++ b/Sming/Arch/Esp32/Platform/RTC.cpp @@ -9,6 +9,7 @@ ****/ #include +#include // #include extern "C" uint64_t esp_clk_rtc_time(void); @@ -45,5 +46,10 @@ bool RtcClass::setRtcNanoseconds(uint64_t nanoseconds) bool RtcClass::setRtcSeconds(uint32_t seconds) { + struct timeval tv { + seconds + }; + settimeofday(&tv, nullptr); + return setRtcNanoseconds(uint64_t(seconds) * NS_PER_SECOND); } diff --git a/Sming/Arch/Esp8266/Platform/RTC.cpp b/Sming/Arch/Esp8266/Platform/RTC.cpp index a90f06b9a2..49e5e5b0f2 100644 --- a/Sming/Arch/Esp8266/Platform/RTC.cpp +++ b/Sming/Arch/Esp8266/Platform/RTC.cpp @@ -10,6 +10,7 @@ #include #include +#include RtcClass RTC; @@ -97,9 +98,29 @@ void loadTime(RtcData& data) // Initialise the time struct if(data.magic != RTC_MAGIC) { - debugf("rtc time init..."); + debug_d("rtc time init..."); data.magic = RTC_MAGIC; data.time = 0; data.cycles = 0; } } + +extern "C" int _gettimeofday_r(struct _reent*, struct timeval* tp, void*) +{ + if(tp) { + // ensureBootTimeIsSet(); + uint32_t micros = RTC.getRtcNanoseconds() / 1000LL; + tp->tv_sec = micros / 1000; + tp->tv_usec = micros % 1000; + } + return 0; +} + +extern "C" time_t time(time_t* t) +{ + time_t seconds = RTC.getRtcSeconds(); + if(t) { + *t = seconds; + } + return seconds; +} diff --git a/Sming/Arch/Rp2040/Platform/RTC.cpp b/Sming/Arch/Rp2040/Platform/RTC.cpp index fef78b61d5..67b8e98e56 100644 --- a/Sming/Arch/Rp2040/Platform/RTC.cpp +++ b/Sming/Arch/Rp2040/Platform/RTC.cpp @@ -11,6 +11,9 @@ #include #include #include +#include + +extern "C" int settimeofday(const struct timeval*, const struct timezone*); RtcClass RTC; @@ -52,6 +55,11 @@ bool RtcClass::setRtcNanoseconds(uint64_t nanoseconds) bool RtcClass::setRtcSeconds(uint32_t seconds) { + struct timeval tv { + seconds + }; + settimeofday(&tv, nullptr); + DateTime dt{seconds}; datetime_t t = { diff --git a/Sming/Components/axtls-8266/axtls-8266.patch b/Sming/Components/axtls-8266/axtls-8266.patch index 16eff5af6a..19688134c4 100644 --- a/Sming/Components/axtls-8266/axtls-8266.patch +++ b/Sming/Components/axtls-8266/axtls-8266.patch @@ -220,140 +220,24 @@ index 53509d0..25c568d 100644 #endif diff --git a/replacements/time.c b/replacements/time.c -index 4972119..d39481c 100644 +index 4972119..1447711 100644 --- a/replacements/time.c +++ b/replacements/time.c -@@ -16,29 +16,25 @@ +@@ -16,6 +16,8 @@ * */ -+#define _C_TYPES_H_ -+#include ++#if 0 + #include --#include -+#include + #include - extern uint32_t system_get_time(void); - extern uint64_t system_mktime(uint32_t year, uint32_t mon, uint32_t day, uint32_t hour, uint32_t min, uint32_t sec); - - static int errno_var = 0; - --int* __errno(void) { -+// These functions are implemented in Espressif SDK versions 2 and later (libmain.a) so we weaken them to avoid linker problems -+#define WEAK_ATTR __attribute__((weak)) -+ -+int* WEAK_ATTR __errno(void) { - // DEBUGV("__errno is called last error: %d (not current)\n", errno_var); - return &errno_var; - } - --unsigned long millis(void) --{ -- return system_get_time() / 1000UL; --} -- --unsigned long micros(void) --{ -- return system_get_time(); --} -- - #ifndef _TIMEVAL_DEFINED - #define _TIMEVAL_DEFINED - struct timeval { -@@ -60,12 +56,12 @@ static time_t s_bootTime = 0; - // calculate offset used in gettimeofday - static void ensureBootTimeIsSet() - { -- if (!s_bootTime) -+ if (s_bootTime == 0) - { - time_t now = sntp_get_current_timestamp(); -- if (now) -+ if (now != 0) - { -- s_bootTime = now - millis() / 1000; -+ s_bootTime = now - (system_get_time() / 1000000); - } - } - } -@@ -79,7 +75,7 @@ static void setServer(int id, const char* name_or_ip) - } - } - --void configTime(int timezone, int daylightOffset_sec, const char* server1, const char* server2, const char* server3) -+void WEAK_ATTR configTime(int timezone, int daylightOffset_sec, const char* server1, const char* server2, const char* server3) - { - sntp_stop(); - -@@ -93,22 +89,16 @@ void configTime(int timezone, int daylightOffset_sec, const char* server1, const - sntp_init(); - } - --int clock_gettime(clockid_t unused, struct timespec *tp) -+int WEAK_ATTR clock_gettime(clockid_t unused, struct timespec *tp) - { -- tp->tv_sec = millis() / 1000; -- tp->tv_nsec = micros() * 1000; -+ unsigned long us = system_get_time(); -+ tp->tv_sec = us / 1000000UL; -+ us %= 1000000UL; -+ tp->tv_nsec = us * 1000; - return 0; - } - --// seconds since 1970 --time_t mktime(struct tm *t) --{ -- // system_mktime expects month in range 1..12 -- #define START_MONTH 1 -- return DIFF1900TO1970 + system_mktime(t->tm_year, t->tm_mon + START_MONTH, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec); --} -- --time_t time(time_t * t) -+time_t WEAK_ATTR time(time_t * t) - { - time_t seconds = sntp_get_current_timestamp(); - if (t) -@@ -118,30 +108,32 @@ time_t time(time_t * t) - return seconds; - } - --char* asctime(const struct tm *t) -+char* WEAK_ATTR asctime(const struct tm *t) - { - return sntp_asctime(t); - } - --struct tm* localtime(const time_t *clock) -+struct tm* WEAK_ATTR localtime(const time_t *clock) - { - return sntp_localtime(clock); - } - --char* ctime(const time_t *t) -+char* WEAK_ATTR ctime(const time_t *t) - { - struct tm* p_tm = localtime(t); - char* result = asctime(p_tm); - return result; - } - --int gettimeofday(struct timeval *tp, void *tzp) -+int WEAK_ATTR gettimeofday(struct timeval *tp, void *tzp) - { - if (tp) - { - ensureBootTimeIsSet(); -- tp->tv_sec = (s_bootTime + millis()) / 1000; -- tp->tv_usec = micros() * 1000; -+ unsigned long us = system_get_time(); -+ tp->tv_sec = s_bootTime + (us / 1000000UL); -+ us %= 1000000UL; -+ tp->tv_usec = us * 1000UL; +@@ -145,3 +147,5 @@ int gettimeofday(struct timeval *tp, void *tzp) } return 0; } ++ ++#endif diff --git a/ssl/asn1.c b/ssl/asn1.c index a08a618..3c64064 100644 --- a/ssl/asn1.c diff --git a/Sming/Components/malloc_count/malloc_count.cpp b/Sming/Components/malloc_count/malloc_count.cpp index 51ad1e638a..357ba7dff0 100644 --- a/Sming/Components/malloc_count/malloc_count.cpp +++ b/Sming/Components/malloc_count/malloc_count.cpp @@ -82,8 +82,17 @@ constexpr size_t alignment{16}; /* bytes (>= 2*sizeof(size_t)) */ /* a sentinel value prefixed to each allocation */ constexpr size_t sentinel{0xDEADC0DE}; -/* Macro to get pointer to sentinel */ -#define GET_SENTINEL(ptr) (size_t*)((char*)ptr - sizeof(size_t)) +/* Add an offset to a pointer return that as a pointer cast to required type */ +template T offsetPointer(void* ptr, intptr_t offset) +{ + return reinterpret_cast(reinterpret_cast(ptr) + offset); +} + +/* Get pointer to sentinel */ +size_t* getSentinel(void* ptr) +{ + return offsetPointer(ptr, -sizeof(size_t)); +} /* output */ #define PPREFIX "MC## " @@ -224,9 +233,9 @@ extern "C" void* mc_malloc(size_t size) } /* prepend allocation size and check sentinel */ - *(size_t*)ret = size; - ret = (char*)ret + alignment; - *GET_SENTINEL(ret) = sentinel; + *static_cast(ret) = size; + ret = offsetPointer(ret, alignment); + *getSentinel(ret) = sentinel; inc_count(size); if(size >= logThreshold) { @@ -252,19 +261,19 @@ extern "C" void mc_free(void* ptr) return; } - size_t* p_sentinel = GET_SENTINEL(ptr); + size_t* p_sentinel = getSentinel(ptr); if(*p_sentinel != sentinel) { log("free(%p) has no sentinel !!! memory corruption?", ptr); // ... or memory not allocated by our malloc() } else { *p_sentinel = 0; // Clear sentinel to avoid false-positives - ptr = (char*)ptr - alignment; + ptr = offsetPointer(ptr, -alignment); - size_t size = *(size_t*)ptr; + size_t size = *static_cast(ptr); dec_count(size); if(size >= logThreshold) { - log("free(%p) -> %u (cur %u)", (char*)ptr + alignment, size, stats.current); + log("free(%p) -> %u (cur %u)", offsetPointer(ptr, alignment), size, stats.current); } } @@ -289,7 +298,7 @@ extern "C" void* mc_realloc(void* ptr, size_t size) return mc_malloc(size); } - if(*GET_SENTINEL(ptr) != sentinel) { + if(*getSentinel(ptr) != sentinel) { log("free(%p) has no sentinel !!! memory corruption?", ptr); // ... or memory not allocated by our malloc() return REAL(F_REALLOC)(ptr, size); @@ -301,9 +310,9 @@ extern "C" void* mc_realloc(void* ptr, size_t size) return nullptr; } - ptr = (char*)ptr - alignment; + ptr = offsetPointer(ptr, -alignment); - size_t oldsize = *(size_t*)ptr; + size_t oldsize = *static_cast(ptr); void* newptr = REAL(F_REALLOC)(ptr, alignment + size); @@ -317,16 +326,16 @@ extern "C" void* mc_realloc(void* ptr, size_t size) if(size >= logThreshold) { if(newptr == ptr) { - log("realloc(%u -> %u) = %p (cur %u)", oldsize, size, (char*)newptr + alignment, stats.current); + log("realloc(%u -> %u) = %p (cur %u)", oldsize, size, offsetPointer(newptr, alignment), stats.current); } else { - log("realloc(%u -> %u) = %p -> %p (cur %u)", oldsize, size, (char*)ptr + alignment, - (char*)newptr + alignment, stats.current); + log("realloc(%u -> %u) = %p -> %p (cur %u)", oldsize, size, offsetPointer(ptr, alignment), + offsetPointer(newptr, alignment), stats.current); } } - *(size_t*)newptr = size; + *static_cast(newptr) = size; - return (char*)newptr + alignment; + return offsetPointer(newptr, alignment); } static __attribute__((destructor)) void finish() @@ -349,6 +358,8 @@ extern "C" void* WRAP(calloc)(size_t, size_t) __attribute__((alias("mc_calloc")) extern "C" void* WRAP(realloc)(void*, size_t) __attribute__((alias("mc_realloc"))); extern "C" void WRAP(free)(void*) __attribute__((alias("mc_free"))); +using namespace MallocCount; + #ifdef ARCH_ESP8266 extern "C" void* WRAP(pvPortMalloc)(size_t) __attribute__((alias("mc_malloc"))); @@ -360,8 +371,6 @@ extern "C" void WRAP(vPortFree)(void*) __attribute__((alias("mc_free"))); #else -using namespace MallocCount; - void* operator new(size_t size) { return mc_malloc(size); @@ -406,14 +415,14 @@ void operator delete[](void* ptr, size_t) #endif +#endif // ESP8266 + extern "C" char* WRAP(strdup)(const char* s) { auto len = strlen(s) + 1; - auto dup = (char*)malloc(len); + auto dup = static_cast(mc_malloc(len)); memcpy(dup, s, len); return dup; } -#endif - #endif // ENABLE_MALLOC_COUNT diff --git a/Sming/Components/ssl/Axtls/AxConnection.cpp b/Sming/Components/ssl/Axtls/AxConnection.cpp index 2fadb090f0..71dc504fa2 100644 --- a/Sming/Components/ssl/Axtls/AxConnection.cpp +++ b/Sming/Components/ssl/Axtls/AxConnection.cpp @@ -34,14 +34,14 @@ int AxConnection::write(const uint8_t* data, size_t length) int available = tcp_sndbuf(tcp); if(available < required) { - debug_i("SSL: Required: %d, Available: %d", required, available); + debug_i("[SSL] Required: %d, Available: %d", required, available); return SSL_NOT_OK; } int written = ssl_write(ssl, data, length); - debug_d("SSL: Write len: %d, Written: %d", length, written); + debug_d("[SSL] Write len: %d, Written: %d", length, written); if(written < 0) { - debug_e("SSL: Write Error: %d", written); + debug_e("[SSL] Write Error: %d", written); } return written; diff --git a/Sming/Components/ssl/Axtls/AxConnection.h b/Sming/Components/ssl/Axtls/AxConnection.h index 1099094b80..42324c197e 100644 --- a/Sming/Components/ssl/Axtls/AxConnection.h +++ b/Sming/Components/ssl/Axtls/AxConnection.h @@ -27,6 +27,7 @@ class AxConnection : public Connection ~AxConnection() { + certificate.reset(); // Typically sends out closing message ssl_free(ssl); } diff --git a/Sming/Components/ssl/Axtls/AxContext.cpp b/Sming/Components/ssl/Axtls/AxContext.cpp index 1961f2a41d..7283d3d910 100644 --- a/Sming/Components/ssl/Axtls/AxContext.cpp +++ b/Sming/Components/ssl/Axtls/AxContext.cpp @@ -44,19 +44,19 @@ bool AxContext::init() #if DEBUG_VERBOSE_LEVEL == DBG options |= SSL_DISPLAY_BYTES; #endif - debug_d("SSL: Show debug data ..."); + debug_d("[SSL] Show debug data ..."); #endif context = ssl_ctx_new(options, session.cacheSize); if(context == nullptr) { - debug_e("SSL: Unable to allocate context"); + debug_e("[SSL] Unable to allocate context"); return false; } auto keyCert = session.keyCert; if(!keyCert.isValid()) { - debug_w("Ignoring invalid keyCert"); + debug_w("[SSL] Ignoring invalid keyCert"); return true; } @@ -66,12 +66,12 @@ bool AxContext::init() }; if(!load(SSL_OBJ_RSA_KEY, keyCert.getKey(), keyCert.getKeyLength(), keyCert.getKeyPassword())) { - debug_e("SSL: Error loading key"); + debug_e("[SSL] Error %d loading key", lastError); return false; } if(!load(SSL_OBJ_X509_CERT, keyCert.getCertificate(), keyCert.getCertificateLength(), nullptr)) { - debug_e("SSL: Error loading certificate"); + debug_e("[SSL] Error %d loading certificate", lastError); return false; } diff --git a/Sming/Components/ssl/BearSsl/BrClientConnection.cpp b/Sming/Components/ssl/BearSsl/BrClientConnection.cpp index 35906f9172..3febd9231c 100644 --- a/Sming/Components/ssl/BearSsl/BrClientConnection.cpp +++ b/Sming/Components/ssl/BearSsl/BrClientConnection.cpp @@ -107,7 +107,11 @@ void BrClientConnection::endCert() bool BrClientConnection::endChain() { - return context.session.validateCertificate(); + auto& session = context.session; + if(!session.options.verifyLater && session.validators.isEmpty()) { + return false; + } + return session.validateCertificate(); } } // namespace Ssl diff --git a/Sming/Components/ssl/BearSsl/BrConnection.cpp b/Sming/Components/ssl/BearSsl/BrConnection.cpp index fc37057eef..2dea34644a 100644 --- a/Sming/Components/ssl/BearSsl/BrConnection.cpp +++ b/Sming/Components/ssl/BearSsl/BrConnection.cpp @@ -119,12 +119,12 @@ int BrConnection::init(size_t bufferSize, bool bidi) void BrConnection::setCipherSuites(const CipherSuites::Array* cipherSuites) { if(cipherSuites == nullptr) { - debug_w("Cipher suites not configured, defaulting to basic"); + debug_w("[SSL] Cipher suites not configured, defaulting to basic"); cipherSuites = &CipherSuites::basic; } auto count = cipherSuites->length(); if(count > BR_MAX_CIPHER_SUITES) { - debug_w("Too many cipher suites, truncating %u -> %u entries", count, BR_MAX_CIPHER_SUITES); + debug_w("[SSL] Too many cipher suites, truncating %u -> %u entries", count, BR_MAX_CIPHER_SUITES); count = BR_MAX_CIPHER_SUITES; } LOAD_FSTR_ARRAY(suites, *cipherSuites); @@ -146,7 +146,7 @@ int BrConnection::read(InputBuffer& input, uint8_t*& output) size_t len = 0; output = br_ssl_engine_recvapp_buf(engine, &len); - debug_hex(DBG, "READ", output, len, 0); + debug_hex(DBG, "[SSL] READ", output, len, 0); br_ssl_engine_recvapp_ack(engine, len); return len; } @@ -168,12 +168,12 @@ int BrConnection::write(const uint8_t* data, size_t length) size_t available; auto buf = br_ssl_engine_sendapp_buf(engine, &available); if(available == 0) { - debug_w("SSL: Send buffer full"); + debug_w("[SSL] Send buffer full"); return 0; } if(available < length) { - debug_i("SSL: Required: %d, Available: %u", length, available); + debug_i("[SSL] Required: %d, Available: %u", length, available); length = available; } @@ -200,7 +200,7 @@ int BrConnection::runUntil(InputBuffer& input, unsigned target) if(state & BR_SSL_CLOSED) { int err = getLastError(); - debug_w("SSL CLOSED, last error = %d (%s), heap free = %u", err, getErrorString(err).c_str(), + debug_w("[SSL] CLOSED, last error = %d (%s), heap free = %u", err, getErrorString(err).c_str(), system_get_free_heap_size()); return err; } @@ -208,7 +208,7 @@ int BrConnection::runUntil(InputBuffer& input, unsigned target) if(!handshakeDone && (state & BR_SSL_SENDAPP)) { handshakeDone = true; context.session.handshakeComplete(true); - debug_i("Negotiated MFLN: %u", br_ssl_engine_get_mfln_negotiated(engine)); + debug_i("[SSL] Negotiated MFLN: %u", br_ssl_engine_get_mfln_negotiated(engine)); continue; } @@ -224,7 +224,7 @@ int BrConnection::runUntil(InputBuffer& input, unsigned target) return 0; } if(wlen < 0) { - debug_w("SSL SHUTDOWN"); + debug_w("[SSL] SHUTDOWN"); /* * If we received a close_notify and we * still send something, then we have our @@ -251,7 +251,7 @@ int BrConnection::runUntil(InputBuffer& input, unsigned target) // Conflict: Application data hasn't been read if(state & BR_SSL_RECVAPP) { - debug_e("SSL: Protocol Error"); + debug_e("[SSL] Protocol Error"); return BR_ERR_BAD_STATE; } @@ -263,7 +263,7 @@ int BrConnection::runUntil(InputBuffer& input, unsigned target) return state; } - debug_hex(DBG, "READ", buf, len, 0); + debug_hex(DBG, "[SSL] READ", buf, len, 0); br_ssl_engine_recvrec_ack(engine, len); continue; diff --git a/Sming/Components/ssl/BearSsl/BrContext.cpp b/Sming/Components/ssl/BearSsl/BrContext.cpp index a51ad21869..e79e1c8800 100644 --- a/Sming/Components/ssl/BearSsl/BrContext.cpp +++ b/Sming/Components/ssl/BearSsl/BrContext.cpp @@ -23,7 +23,7 @@ Connection* BrContext::createClient(tcp_pcb* tcp) if(connection != nullptr) { int res = connection->init(); if(res < 0) { - debug_e("Connection init failed: %s", connection->getErrorString(res).c_str()); + debug_e("[SSL] Connection init failed: %s", connection->getErrorString(res).c_str()); delete connection; connection = nullptr; } @@ -37,7 +37,7 @@ Connection* BrContext::createServer(tcp_pcb* tcp) if(connection != nullptr) { int res = connection->init(); if(res < 0) { - debug_e("Connection init failed: %s", connection->getErrorString(res).c_str()); + debug_e("[SSL] Connection init failed: %s", connection->getErrorString(res).c_str()); delete connection; connection = nullptr; } diff --git a/Sming/Components/ssl/BearSsl/BrPrivateKey.cpp b/Sming/Components/ssl/BearSsl/BrPrivateKey.cpp index 31ff6997f9..5d88084824 100644 --- a/Sming/Components/ssl/BearSsl/BrPrivateKey.cpp +++ b/Sming/Components/ssl/BearSsl/BrPrivateKey.cpp @@ -37,7 +37,7 @@ bool BrPrivateKey::decode(const uint8_t* buf, size_t len) return copy(*br_skey_decoder_get_ec(&dc)); default: - debug_e("Unknown key type: %d", type); + debug_e("[SSL] Unknown key type: %d", type); return false; } } diff --git a/Sming/Components/ssl/BearSsl/BrPublicKey.cpp b/Sming/Components/ssl/BearSsl/BrPublicKey.cpp index ea7ff33636..a2c89fddda 100644 --- a/Sming/Components/ssl/BearSsl/BrPublicKey.cpp +++ b/Sming/Components/ssl/BearSsl/BrPublicKey.cpp @@ -37,7 +37,7 @@ bool BrPublicKey::decode(const uint8_t* buf, size_t len) return copy(*br_pkey_decoder_get_ec(&dc)); default: - debug_e("Unknown key type: %d", type); + debug_e("[SSL] Unknown key type: %d", type); return false; } } diff --git a/Sming/Components/ssl/BearSsl/BrServerConnection.cpp b/Sming/Components/ssl/BearSsl/BrServerConnection.cpp index f109cf0289..3dbda3f157 100644 --- a/Sming/Components/ssl/BearSsl/BrServerConnection.cpp +++ b/Sming/Components/ssl/BearSsl/BrServerConnection.cpp @@ -43,14 +43,14 @@ int BrServerConnection::init() cert.data = const_cast(keyCert.getCertificate()); cert.data_len = keyCert.getCertificateLength(); if(!key.decode(keyCert.getKey(), keyCert.getKeyLength())) { - debug_e("Failed to decode keyCert"); + debug_e("[SSL] Failed to decode keyCert"); return -BR_ERR_BAD_PARAM; } br_ssl_server_set_single_rsa(&serverContext, &cert, 1, key, BR_KEYTYPE_RSA | BR_KEYTYPE_KEYX | BR_KEYTYPE_SIGN, br_rsa_private_get_default(), br_rsa_pkcs1_sign_get_default()); // Warning: Inconsistent return type: not an error code if(!br_ssl_server_reset(&serverContext)) { - debug_e("br_ssl_client_reset failed"); + debug_e("[SSL] br_ssl_client_reset failed"); return getLastError(); } diff --git a/Sming/Components/ssl/BearSsl/X509Context.h b/Sming/Components/ssl/BearSsl/X509Context.h index 0118336bb1..1f176856f9 100644 --- a/Sming/Components/ssl/BearSsl/X509Context.h +++ b/Sming/Components/ssl/BearSsl/X509Context.h @@ -59,7 +59,7 @@ class X509Context // Callback on the first byte of any certificate static void start_chain(const br_x509_class** ctx, const char* server_name) { - debug_i("start_chain: %s", server_name); + debug_i("[SSL] start_chain: %s", server_name); GET_SELF(); self->certificateCount = 0; self->handler.startChain(server_name); @@ -68,7 +68,7 @@ class X509Context // Callback for each certificate present in the chain static void start_cert(const br_x509_class** ctx, uint32_t length) { - debug_i("start_cert: %u", length); + debug_i("[SSL] start_cert: %u", length); GET_SELF(); self->startCert(length); } @@ -81,15 +81,15 @@ class X509Context // Callback for each byte stream in the chain static void append(const br_x509_class** ctx, const unsigned char* buf, size_t len) { - debug_i("append: %u", len); + debug_i("[SSL] X509 append: %u", len); GET_SELF(); self->handler.appendCertData(buf, len); - debug_hex(DBG, "CERT", buf, len, 0); + debug_hex(DBG, "[SSL] CERT", buf, len, 0); } static void end_cert(const br_x509_class** ctx) { - debug_i("end_cert"); + debug_i("[SSL] end_cert"); GET_SELF(); self->handler.endCert(); ++self->certificateCount; @@ -98,7 +98,7 @@ class X509Context // Complete chain has been parsed, return 0 on validation success static unsigned end_chain(const br_x509_class** ctx) { - debug_i("end_chain"); + debug_i("[SSL] end_chain"); GET_SELF(); return self->endChain(); } @@ -106,7 +106,7 @@ class X509Context unsigned endChain() { if(certificateCount == 0) { - debug_w("No certificate processed"); + debug_w("[SSL] No certificate processed"); return BR_ERR_X509_EMPTY_CHAIN; } diff --git a/Sming/Components/ssl/README.rst b/Sming/Components/ssl/README.rst index de0340a589..f42f38c41c 100644 --- a/Sming/Components/ssl/README.rst +++ b/Sming/Components/ssl/README.rst @@ -12,6 +12,105 @@ If you want to use SSL then take a look at the :sample:`Basic_Ssl` sample for cr and :sample:`HttpServer_ConfigNetwork` for SSL servers. +Certificates +------------ + +For server applications you'll require an X509 certificate and private key. +This is typically generated using openssl, then the resulting binary files added to your project. +The :sample:`Basic_AWS` demonstrates this approach. + +.. important:: + + Clearly private key information must be guarded so take care not to publish this in a public repository! + +Sming also has a convenience script you can use:: + + make generate-cert + +This generates binary certificate and key information in ``out/ssl``, +and creates ``include/ssl/cert.h`` and ``include/ssl/private_key.h``. +See :sample:`MqttClient_Hello` for example code. + + +Enabling SSL in HttpServer +-------------------------- + +.. highlight:: c++ + +The :cpp:func:`TcpServer::listen` method, and the child :cpp:class:`HttpServer` class, +accepts a second optional ``useSsl`` parameter. +If you look at the ``startWebServer`` function in :source:`samples/HttpServer_WebSockets/app/application.cpp`, +it can be changed to something like:: + + void startWebServer() + { + // TODO: Make sure to set a server certificate and key + server.listen(443, true); + +And what is left is the actual setting of the server certificate:: + + void startWebServer() + { + // Assign the certificate + server.setSslInitHandler([](Ssl::Session& session) { + session.keyCert.assign(serverKey, serverCert); + }); + server.listen(443, true); + +The final code can be something like:: + + void startWebServer() + { + #ifdef ENABLE_SSL + server.setSslInitHandler([](Ssl::Session& session) { + session.keyCert.assign(serverKey, serverCert); + }); + server.listen(443, true); + #else + server.listen(80); + #endif + server.paths.set("/", onIndex); + //... + +This is also demonstrated for secure MQTT in the :sample:`MqttClient_Hello` sample. + + +Security Considerations +======================= + +Does it really make sense to use SSL for an HttpServer on an ESP8266 device? + +The certificate/private key pair should make it impossible for an +external user to decrypt your traffic so that the things that you sent +are kept private, but there are some complications with this: + +- The private key will not stay private for long. The private key should be + kept encrypted on the flash memory, to prevent casual reading. + But even with decryption there is a high probability that someone + will be able to disassemble your application and figure out how to + decrypt the key. + +- Costs for certificate. Let’s imagine that you have overcome the first + issue. Then comes the second issue - if you want your users to accept + the certificate it has to be signed by one of the trusted certificate + authorities. And that costs money. And if you want to use a unique + certificate/private key pair for every device than it will make + things worse, moneywise. Note: Free SSL certificates are now available, + for example https://letsencrypt.org/. These will expire if not kept + up to date so adds additional complexity to your application. + +- You can handle up to 2 or maximum 3 connections. SSL needs 16K of memory to + make the initial handshake. The memory consumption after a successful + handshake can decrease to 4K, just for the SSL, per request. But + realistically this means that you will end up with a server that can + handle maximum 2 or 3 simultaneous connections before the heap memory is + consumed and has to be released. + +Therefore, in our humble opinion, it would be better to rely on the WIFI security that your Access +Point (AP) provides and make this AP accessible only for your IoT devices. + + + Configuration Variables ----------------------- diff --git a/Sming/Components/ssl/Tools/make_certs.sh b/Sming/Components/ssl/Tools/make_certs.sh index b5d8d060dc..3e364870b2 100755 --- a/Sming/Components/ssl/Tools/make_certs.sh +++ b/Sming/Components/ssl/Tools/make_certs.sh @@ -144,7 +144,7 @@ date -s "$DATE_NOW" touch x509_bad_before.pem fi openssl x509 -req -in x509_512.req -out x509_bad_after.pem \ - -sha1 -CAcreateserial -days -365 \ + -sha1 -CAcreateserial -days -1 \ -CA ca_x509.pem -CAkey ca_key.pem # some cleanup @@ -176,7 +176,5 @@ openssl pkcs12 -export -in x509_1024.pem -inkey key_1024.pem -keypbe PBE-SHA1-RC cat ca_x509.pem >> x509_device.pem # set default key/cert for use in the server -xxd -i x509_1024.cer | sed -e \ - "s/x509_1024_cer/default_certificate/" > "$SSL_INCLUDE_DIR/cert.h" -xxd -i key_1024 | sed -e \ - "s/key_1024/default_private_key/" > "$SSL_INCLUDE_DIR/private_key.h" +xxd -n default_certificate -i x509_1024.cer | sed -e "s/\[\]/\[\] PROGMEM/" > "$SSL_INCLUDE_DIR/cert.h" +xxd -n default_private_key -i key_1024 | sed -e "s/\[\]/\[\] PROGMEM/" > "$SSL_INCLUDE_DIR/private_key.h" diff --git a/Sming/Components/ssl/component.mk b/Sming/Components/ssl/component.mk index 41ba7ae644..27b46ed9df 100644 --- a/Sming/Components/ssl/component.mk +++ b/Sming/Components/ssl/component.mk @@ -41,12 +41,14 @@ ifeq ($(ENABLE_SSL),0) else $(info Using $(ENABLE_SSL) SSL implementation) -# Application -CUSTOM_TARGETS += include/ssl/private_key.h +##@Tools + +.PHONY: generate-cert +generate-cert: include/ssl/private_key.h ##Generate X509 certificate and private key files in include/ssl SSL_TOOLS_PATH := $(COMPONENT_PATH)/Tools SSL_INCLUDE_DIR := $(PROJECT_DIR)/include/ssl -OUT_SSL := out/ssl/ +OUT_SSL := out/ssl/ SSL_CERT_DIR := $(OUT_SSL)/cert include/ssl/private_key.h: diff --git a/Sming/Components/ssl/src/KeyCertPair.cpp b/Sming/Components/ssl/src/KeyCertPair.cpp index 0de627b986..ca00b662de 100644 --- a/Sming/Components/ssl/src/KeyCertPair.cpp +++ b/Sming/Components/ssl/src/KeyCertPair.cpp @@ -21,14 +21,14 @@ bool KeyCertPair::assign(const uint8_t* newKey, unsigned newKeyLength, const uin if(!key.setLength(newKeyLength)) { return false; } - memcpy(key.begin(), newKey, newKeyLength); + memcpy_P(key.begin(), newKey, newKeyLength); } if(newCertificateLength != 0 && newCertificate != nullptr) { if(!certificate.setLength(newCertificateLength)) { return false; } - memcpy(certificate.begin(), newCertificate, newCertificateLength); + memcpy_P(certificate.begin(), newCertificate, newCertificateLength); } return setPassword(newKeyPassword); @@ -36,13 +36,13 @@ bool KeyCertPair::assign(const uint8_t* newKey, unsigned newKeyLength, const uin bool KeyCertPair::setPassword(const char* newKeyPassword) { - unsigned passwordLength = (newKeyPassword == nullptr) ? 0 : strlen(newKeyPassword); + unsigned passwordLength = (newKeyPassword == nullptr) ? 0 : strlen_P(newKeyPassword); if(passwordLength == 0) { keyPassword = nullptr; return true; } - keyPassword.setString(newKeyPassword, passwordLength); + keyPassword.setString(flash_string_t(newKeyPassword), passwordLength); return keyPassword; } diff --git a/docs/source/experimental/httpserver-ssl.rst b/docs/source/experimental/httpserver-ssl.rst deleted file mode 100644 index b4cf95f491..0000000000 --- a/docs/source/experimental/httpserver-ssl.rst +++ /dev/null @@ -1,83 +0,0 @@ -**************************** -Enabling SSL in HttpServer -**************************** - -.. highlight:: c++ - -* At the moment any TCP based server in Sming can use TLS/SSL. -* That applies to HttpServer (also with Websockets). -* But make sure to read the security considerations and limitations. - -Enabling SSL in HttpServer -========================== - -The ``listen`` method in the TcpServer, and the child HttpServer class, accepts a second optional -parameter. If you look at the original code: -:source:`samples/HttpServer_WebSockets/app/application.cpp#L95-L99`. - -That can be changed to something like:: - - void startWebServer() - { - // TODO: Make sure to set a server certificate and key - server.listen(443, true); - -And what is left is the actual setting of the server certificate:: - - void startWebServer() - { - // Assign the certificate - server.setSslInitHandler([](Ssl::Session& session) { - session.keyCert.assign(serverKey, serverCert); - }); - server.listen(443, true); - -The final code can be something like:: - - void startWebServer() - { - #ifdef ENABLE_SSL - server.setSslInitHandler([](Ssl::Session& session) { - session.keyCert.assign(serverKey, serverCert); - }); - server.listen(443, true); - #else - server.listen(80); - #endif - server.paths.set("/", onIndex); - //... - - -Security Considerations -======================= - -Does it really make sense to use SSL for an HttpServer on an ESP8266 device? - -The certificate/private key pair should make it impossible for an -external user to decrypt your traffic so that the things that you sent -are kept private, but there are some complications with this: - -- The private key will not stay private for long. The private key should be - kept encrypted on the flash memory, to prevent casual reading. - But even with decryption there is a high probability that someone - will be able to disassemble your application and figure out how to - decrypt the key. - -- Costs for certificate. Let’s imagine that you have overcome the first - issue. Then comes the second issue - if you want your users to accept - the certificate it has to be signed by one of the trusted certificate - authorities. And that costs money. And if you want to use a unique - certificate/private key pair for every device than it will make - things worse, moneywise. Note: Free SSL certificates are now available, - for example https://letsencrypt.org/. These will expire if not kept - up to date so adds additional complexity to your application. - -- You can handle up to 2 or maximum 3 connections. SSL needs 16K of memory to - make the initial handshake. The memory consumption after a successful - handshake can decrease to 4K, just for the SSL, per request. But - realistically this means that you will end up with a server that can - handle maximum 2 or 3 simultaneous connections before the heap memory is - consumed and has to be released. - -Therefore, in our humble opinion, it would be better to rely on the WIFI security that your Access -Point (AP) provides and make this AP accessible only for your IoT devices. diff --git a/docs/source/experimental/index.rst b/docs/source/experimental/index.rst index 0a5859ac45..701914ad3b 100644 --- a/docs/source/experimental/index.rst +++ b/docs/source/experimental/index.rst @@ -5,6 +5,5 @@ Experimental Stuff .. toctree:: :maxdepth: 1 - httpserver-ssl signed-ota wokwi diff --git a/docs/source/upgrading/5.1-5.2.rst b/docs/source/upgrading/5.1-5.2.rst index 6eb1099064..0d10a27c14 100644 --- a/docs/source/upgrading/5.1-5.2.rst +++ b/docs/source/upgrading/5.1-5.2.rst @@ -63,3 +63,15 @@ Esp32 IDF RP2040 The installer has been updated to use the latest toolchain (Oct 23), gcc 13.2. + + +**Bearssl client certificate validation** + +Using ENABLE_SSL=Bearssl in a client application, no verification on the server certificate is performed. +This is a potential security issue. + +Attempting the same thing with Axtls results in an ``X509_VFY_ERROR_NO_TRUSTED_CERT`` error. +Applications must explicitly call :cpp:func:`HttpRequest::onSslInit` and set the ``verifyLater`` flag. +This extra step ensures that security checks are not unintentionally bypassed. + +The same behaviour is now presented when using Bearssl, and will now fail with ``X509_NOT_TRUSTED``. diff --git a/samples/Basic_AWS/component.mk b/samples/Basic_AWS/component.mk index 6e25c28d7d..88a0932e81 100644 --- a/samples/Basic_AWS/component.mk +++ b/samples/Basic_AWS/component.mk @@ -1,3 +1,3 @@ -ENABLE_SSL := 1 +ENABLE_SSL ?= 1 MQTT_NO_COMPAT := 1 ENABLE_CUSTOM_HEAP := 1 diff --git a/samples/Basic_AWS/include/ssl/cert.h b/samples/Basic_AWS/include/ssl/cert.h deleted file mode 100644 index b0569a1e1c..0000000000 --- a/samples/Basic_AWS/include/ssl/cert.h +++ /dev/null @@ -1,27 +0,0 @@ -unsigned char default_certificate[] = { - 0x30, 0x82, 0x01, 0xd7, 0x30, 0x82, 0x01, 0x40, 0x02, 0x09, 0x00, 0xf2, 0x26, 0x4c, 0x02, 0x27, 0x42, 0x08, 0x09, - 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x05, 0x05, 0x00, 0x30, 0x34, 0x31, 0x32, - 0x30, 0x30, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x13, 0x29, 0x61, 0x78, 0x54, 0x4c, 0x53, 0x20, 0x50, 0x72, 0x6f, 0x6a, - 0x65, 0x63, 0x74, 0x20, 0x44, 0x6f, 0x64, 0x67, 0x79, 0x20, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, - 0x74, 0x65, 0x20, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x30, 0x1e, 0x17, 0x0d, 0x31, 0x39, 0x30, - 0x39, 0x31, 0x34, 0x32, 0x30, 0x35, 0x32, 0x33, 0x36, 0x5a, 0x17, 0x0d, 0x33, 0x33, 0x30, 0x35, 0x32, 0x33, 0x32, - 0x30, 0x35, 0x32, 0x33, 0x36, 0x5a, 0x30, 0x2c, 0x31, 0x16, 0x30, 0x14, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x13, 0x0d, - 0x61, 0x78, 0x54, 0x4c, 0x53, 0x20, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x31, 0x12, 0x30, 0x10, 0x06, 0x03, - 0x55, 0x04, 0x03, 0x13, 0x09, 0x31, 0x32, 0x37, 0x2e, 0x30, 0x2e, 0x30, 0x2e, 0x31, 0x30, 0x81, 0x9f, 0x30, 0x0d, - 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x81, 0x8d, 0x00, 0x30, 0x81, - 0x89, 0x02, 0x81, 0x81, 0x00, 0xb8, 0x03, 0x9f, 0xa7, 0x44, 0x83, 0xc9, 0x3d, 0x6d, 0xd3, 0x77, 0x93, 0x83, 0x54, - 0xed, 0x2e, 0xd6, 0xc3, 0xfb, 0xdf, 0xe3, 0x4d, 0x53, 0x0f, 0x26, 0x06, 0xb0, 0xb9, 0xc8, 0x8e, 0x7a, 0x38, 0x18, - 0x89, 0x2e, 0xe3, 0xa3, 0x53, 0xc2, 0xba, 0xe5, 0x3f, 0xa5, 0x25, 0x54, 0x8a, 0x67, 0x09, 0xf1, 0x9f, 0xf2, 0xad, - 0x2e, 0x42, 0xb7, 0x8d, 0x2c, 0x0b, 0x73, 0xf6, 0x5a, 0x89, 0x51, 0xac, 0xf0, 0x97, 0x77, 0x17, 0x4d, 0x66, 0xca, - 0x20, 0xc9, 0x17, 0xbf, 0x97, 0xd3, 0xfd, 0xf0, 0xf7, 0x4b, 0xad, 0x30, 0xa9, 0x09, 0x3c, 0x62, 0x85, 0xde, 0x47, - 0x60, 0x10, 0x2d, 0xe5, 0x0b, 0x9e, 0x50, 0x44, 0xb3, 0x99, 0xda, 0x51, 0xd4, 0xbe, 0x30, 0x4f, 0x6d, 0x9d, 0x23, - 0x49, 0x47, 0x4e, 0xe7, 0xcf, 0x2f, 0x1b, 0x5b, 0xa0, 0x49, 0xc0, 0xc2, 0x51, 0x35, 0xde, 0x61, 0x82, 0x35, 0xc1, - 0x02, 0x03, 0x01, 0x00, 0x01, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x05, 0x05, - 0x00, 0x03, 0x81, 0x81, 0x00, 0x20, 0x37, 0xa2, 0xc0, 0xd5, 0x9c, 0x8d, 0xd0, 0x6f, 0x9d, 0x39, 0x2c, 0xb1, 0xb6, - 0xd7, 0x25, 0xc1, 0x92, 0xf3, 0x6d, 0x44, 0xad, 0x89, 0x33, 0xa4, 0xcb, 0xde, 0xc0, 0xeb, 0x58, 0xcb, 0xa2, 0xd5, - 0xc7, 0x54, 0xac, 0x8a, 0x29, 0xfd, 0x8e, 0x4e, 0x0e, 0x2a, 0x09, 0xec, 0xae, 0x3d, 0x40, 0xd0, 0x4c, 0xd0, 0xd6, - 0xa5, 0x8a, 0x97, 0x40, 0x92, 0x6b, 0x51, 0xc1, 0xf5, 0xe9, 0xa8, 0xdc, 0x99, 0xac, 0x35, 0xae, 0x59, 0x1f, 0x14, - 0x4d, 0x7e, 0xbb, 0xa7, 0x31, 0x76, 0x8c, 0xf3, 0xfc, 0xf8, 0x91, 0x91, 0x3d, 0x6b, 0x15, 0xec, 0xdd, 0x04, 0xb4, - 0x67, 0x81, 0xee, 0xd8, 0x31, 0xaf, 0xfc, 0xf2, 0x6a, 0x36, 0x87, 0xc1, 0x03, 0x49, 0xc4, 0x64, 0x17, 0x29, 0x8b, - 0x44, 0x50, 0x78, 0x3a, 0x2a, 0x0b, 0x6b, 0xf5, 0xf5, 0x38, 0x87, 0x4e, 0x86, 0x68, 0x3b, 0x7d, 0xae, 0x1f, 0xd7}; -unsigned int default_certificate_len = 475; diff --git a/samples/Basic_AWS/include/ssl/private_key.h b/samples/Basic_AWS/include/ssl/private_key.h deleted file mode 100644 index 1e8c08d258..0000000000 --- a/samples/Basic_AWS/include/ssl/private_key.h +++ /dev/null @@ -1,34 +0,0 @@ -unsigned char default_private_key[] = { - 0x30, 0x82, 0x02, 0x5c, 0x02, 0x01, 0x00, 0x02, 0x81, 0x81, 0x00, 0xb8, 0x03, 0x9f, 0xa7, 0x44, 0x83, 0xc9, 0x3d, - 0x6d, 0xd3, 0x77, 0x93, 0x83, 0x54, 0xed, 0x2e, 0xd6, 0xc3, 0xfb, 0xdf, 0xe3, 0x4d, 0x53, 0x0f, 0x26, 0x06, 0xb0, - 0xb9, 0xc8, 0x8e, 0x7a, 0x38, 0x18, 0x89, 0x2e, 0xe3, 0xa3, 0x53, 0xc2, 0xba, 0xe5, 0x3f, 0xa5, 0x25, 0x54, 0x8a, - 0x67, 0x09, 0xf1, 0x9f, 0xf2, 0xad, 0x2e, 0x42, 0xb7, 0x8d, 0x2c, 0x0b, 0x73, 0xf6, 0x5a, 0x89, 0x51, 0xac, 0xf0, - 0x97, 0x77, 0x17, 0x4d, 0x66, 0xca, 0x20, 0xc9, 0x17, 0xbf, 0x97, 0xd3, 0xfd, 0xf0, 0xf7, 0x4b, 0xad, 0x30, 0xa9, - 0x09, 0x3c, 0x62, 0x85, 0xde, 0x47, 0x60, 0x10, 0x2d, 0xe5, 0x0b, 0x9e, 0x50, 0x44, 0xb3, 0x99, 0xda, 0x51, 0xd4, - 0xbe, 0x30, 0x4f, 0x6d, 0x9d, 0x23, 0x49, 0x47, 0x4e, 0xe7, 0xcf, 0x2f, 0x1b, 0x5b, 0xa0, 0x49, 0xc0, 0xc2, 0x51, - 0x35, 0xde, 0x61, 0x82, 0x35, 0xc1, 0x02, 0x03, 0x01, 0x00, 0x01, 0x02, 0x81, 0x80, 0x18, 0xfb, 0xd4, 0xf7, 0x5e, - 0xe4, 0x0d, 0xb4, 0x9b, 0x6e, 0xb4, 0xb3, 0x80, 0x87, 0x4d, 0x54, 0xa8, 0xb0, 0x1f, 0x48, 0x7c, 0x92, 0x09, 0x0e, - 0xeb, 0x78, 0xc9, 0x11, 0xd0, 0x5b, 0x17, 0xd8, 0xf9, 0xeb, 0xd6, 0x43, 0xed, 0xee, 0xf7, 0x67, 0x6c, 0xac, 0xc1, - 0x7d, 0x5a, 0x07, 0x18, 0x8a, 0x4f, 0x16, 0x0a, 0x5a, 0xdd, 0x07, 0x0d, 0xe8, 0xca, 0xec, 0x1b, 0x28, 0x7d, 0x8e, - 0x5b, 0x5a, 0x46, 0xab, 0xfe, 0x5b, 0xe8, 0x05, 0x03, 0x06, 0x66, 0xd6, 0xac, 0xc3, 0xea, 0x92, 0x96, 0x69, 0x0d, - 0x54, 0x81, 0x31, 0x5f, 0x11, 0xea, 0x3f, 0x4e, 0xf1, 0x11, 0xc8, 0xe7, 0xe3, 0x4a, 0x15, 0xa6, 0xc8, 0x7d, 0x19, - 0xba, 0x18, 0xa9, 0xfb, 0x19, 0x94, 0xdc, 0xc4, 0x47, 0x11, 0xea, 0xfa, 0x45, 0x15, 0x00, 0x39, 0xf9, 0xdd, 0x0b, - 0x50, 0x56, 0xd8, 0xb8, 0x24, 0x50, 0x75, 0x2b, 0x61, 0x02, 0x41, 0x00, 0xe1, 0xfc, 0x01, 0x09, 0x8c, 0x15, 0x9a, - 0x38, 0x1b, 0xc8, 0xe9, 0x0a, 0xdb, 0x97, 0x5f, 0xb9, 0x79, 0x00, 0x7d, 0x6c, 0x02, 0x5f, 0x7d, 0xaa, 0xe6, 0x4b, - 0x98, 0x41, 0xfc, 0xc6, 0x59, 0x99, 0x39, 0x2e, 0x78, 0x65, 0xc0, 0x9b, 0xf5, 0x49, 0x87, 0xc1, 0xd7, 0xb1, 0x34, - 0x51, 0x2d, 0xf5, 0x8e, 0x05, 0x8c, 0xe8, 0xbf, 0x11, 0xca, 0xd9, 0xaf, 0x57, 0xf7, 0x50, 0x1f, 0x4a, 0x91, 0x95, - 0x02, 0x41, 0x00, 0xd0, 0x74, 0x88, 0xa4, 0x71, 0xb6, 0x24, 0x3e, 0xb4, 0x69, 0x7d, 0xb0, 0xb8, 0xeb, 0x1d, 0x13, - 0x98, 0x65, 0xf7, 0x99, 0xc6, 0x7e, 0xb8, 0x0d, 0xa0, 0xcc, 0x49, 0xca, 0x93, 0x82, 0x6a, 0x2e, 0x7e, 0xa8, 0x19, - 0xe1, 0x8d, 0x5b, 0x4d, 0x14, 0x00, 0x88, 0xcc, 0xb9, 0xb4, 0x71, 0xbd, 0x47, 0x2a, 0x82, 0x88, 0x64, 0x26, 0xdb, - 0x1b, 0x42, 0x63, 0x19, 0x09, 0x3e, 0x33, 0xac, 0xa0, 0x7d, 0x02, 0x40, 0x60, 0x20, 0x16, 0xbc, 0xdd, 0xe6, 0x8e, - 0x7c, 0x11, 0x6d, 0x8b, 0x9b, 0x7f, 0xbe, 0xcb, 0x0c, 0x14, 0xe9, 0x5d, 0x70, 0x65, 0x2e, 0x03, 0x41, 0x7f, 0xc6, - 0x66, 0x14, 0xa3, 0x96, 0x27, 0xa4, 0xa2, 0x8b, 0x1e, 0xd1, 0x81, 0x75, 0x95, 0x87, 0xda, 0x84, 0x5c, 0xe0, 0x56, - 0xb5, 0xb5, 0x4b, 0xff, 0x46, 0x63, 0x22, 0xd9, 0xab, 0x92, 0xd2, 0xb7, 0xe0, 0x3e, 0x25, 0xc9, 0xb9, 0xa9, 0x65, - 0x02, 0x41, 0x00, 0xce, 0x9b, 0x72, 0x03, 0x6b, 0x21, 0x18, 0x73, 0x7d, 0xe5, 0x40, 0xca, 0xb3, 0xbd, 0x74, 0xa8, - 0x43, 0x58, 0x6d, 0x3c, 0x60, 0xdc, 0xa0, 0x18, 0x01, 0xd3, 0xf9, 0x1f, 0x6b, 0x6c, 0xcb, 0x49, 0x22, 0x08, 0x02, - 0xfe, 0xe7, 0x58, 0x22, 0xe1, 0x3c, 0x56, 0x5a, 0x73, 0x85, 0x41, 0x66, 0x54, 0xee, 0xf1, 0x49, 0xb5, 0xda, 0x3d, - 0x38, 0x9b, 0x68, 0x15, 0x1c, 0x70, 0x26, 0x4f, 0x67, 0x11, 0x02, 0x40, 0x02, 0x54, 0x5d, 0x63, 0x2c, 0x2c, 0x7c, - 0x4c, 0x09, 0x18, 0x38, 0xac, 0x0a, 0xec, 0xb7, 0x33, 0x86, 0x15, 0x23, 0x39, 0xab, 0x50, 0x22, 0x6f, 0x7a, 0x71, - 0xa8, 0x46, 0x13, 0x0f, 0xae, 0xb4, 0xa4, 0xc4, 0x63, 0x03, 0x35, 0x05, 0x51, 0x0b, 0x59, 0x8b, 0xcf, 0x60, 0xf2, - 0xa2, 0x15, 0xaf, 0x7e, 0x07, 0xb2, 0xb5, 0xc6, 0x08, 0x75, 0x84, 0xed, 0x58, 0xf0, 0xb7, 0x37, 0xa0, 0xcd, 0x21}; -unsigned int default_private_key_len = 608; diff --git a/samples/Basic_Ssl/component.mk b/samples/Basic_Ssl/component.mk index 59fa1e1767..e068be8488 100644 --- a/samples/Basic_Ssl/component.mk +++ b/samples/Basic_Ssl/component.mk @@ -4,4 +4,4 @@ COMPONENT_DEPENDS += malloc_count COMPONENT_CXXFLAGS += -DENABLE_MALLOC_COUNT=1 endif -ENABLE_SSL = Bearssl +ENABLE_SSL ?= Bearssl diff --git a/samples/Basic_Ssl/include/ssl/cert.h b/samples/Basic_Ssl/include/ssl/cert.h deleted file mode 100644 index 61ca8a654e..0000000000 --- a/samples/Basic_Ssl/include/ssl/cert.h +++ /dev/null @@ -1,27 +0,0 @@ -unsigned char default_certificate[] = { - 0x30, 0x82, 0x01, 0xd7, 0x30, 0x82, 0x01, 0x40, 0x02, 0x09, 0x00, 0xab, 0x08, 0x18, 0xa7, 0x03, 0x07, 0x27, 0xfd, - 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x05, 0x05, 0x00, 0x30, 0x34, 0x31, 0x32, - 0x30, 0x30, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x13, 0x29, 0x61, 0x78, 0x54, 0x4c, 0x53, 0x20, 0x50, 0x72, 0x6f, 0x6a, - 0x65, 0x63, 0x74, 0x20, 0x44, 0x6f, 0x64, 0x67, 0x79, 0x20, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, - 0x74, 0x65, 0x20, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x30, 0x1e, 0x17, 0x0d, 0x31, 0x30, 0x31, - 0x32, 0x32, 0x36, 0x32, 0x32, 0x33, 0x33, 0x33, 0x39, 0x5a, 0x17, 0x0d, 0x32, 0x34, 0x30, 0x39, 0x30, 0x33, 0x32, - 0x32, 0x33, 0x33, 0x33, 0x39, 0x5a, 0x30, 0x2c, 0x31, 0x16, 0x30, 0x14, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x13, 0x0d, - 0x61, 0x78, 0x54, 0x4c, 0x53, 0x20, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x31, 0x12, 0x30, 0x10, 0x06, 0x03, - 0x55, 0x04, 0x03, 0x13, 0x09, 0x31, 0x32, 0x37, 0x2e, 0x30, 0x2e, 0x30, 0x2e, 0x31, 0x30, 0x81, 0x9f, 0x30, 0x0d, - 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x81, 0x8d, 0x00, 0x30, 0x81, - 0x89, 0x02, 0x81, 0x81, 0x00, 0xcd, 0xfd, 0x89, 0x48, 0xbe, 0x36, 0xb9, 0x95, 0x76, 0xd4, 0x13, 0x30, 0x0e, 0xbf, - 0xb2, 0xed, 0x67, 0x0a, 0xc0, 0x16, 0x3f, 0x51, 0x09, 0x9d, 0x29, 0x2f, 0xb2, 0x6d, 0x3f, 0x3e, 0x6c, 0x2f, 0x90, - 0x80, 0xa1, 0x71, 0xdf, 0xbe, 0x38, 0xc5, 0xcb, 0xa9, 0x9a, 0x40, 0x14, 0x90, 0x0a, 0xf9, 0xb7, 0x07, 0x0b, 0xe1, - 0xda, 0xe7, 0x09, 0xbf, 0x0d, 0x57, 0x41, 0x86, 0x60, 0xa1, 0xc1, 0x27, 0x91, 0x5b, 0x0a, 0x98, 0x46, 0x1b, 0xf6, - 0xa2, 0x84, 0xf8, 0x65, 0xc7, 0xce, 0x2d, 0x96, 0x17, 0xaa, 0x91, 0xf8, 0x61, 0x04, 0x50, 0x70, 0xeb, 0xb4, 0x43, - 0xb7, 0xdc, 0x9a, 0xcc, 0x31, 0x01, 0x14, 0xd4, 0xcd, 0xcc, 0xc2, 0x37, 0x6d, 0x69, 0x82, 0xd6, 0xc6, 0xc4, 0xbe, - 0xf2, 0x34, 0xa5, 0xc9, 0xa6, 0x19, 0x53, 0x32, 0x7a, 0x86, 0x0e, 0x91, 0x82, 0x0f, 0xa1, 0x42, 0x54, 0xaa, 0x01, - 0x02, 0x03, 0x01, 0x00, 0x01, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x05, 0x05, - 0x00, 0x03, 0x81, 0x81, 0x00, 0x40, 0xb4, 0x94, 0x9a, 0xa8, 0x89, 0x72, 0x1d, 0x07, 0xe5, 0xb3, 0x6b, 0x88, 0x21, - 0xc2, 0x38, 0x36, 0x9e, 0x7a, 0x8c, 0x49, 0x48, 0x68, 0x0c, 0x06, 0xe8, 0xdb, 0x1f, 0x4e, 0x05, 0xe6, 0x31, 0xe3, - 0xfd, 0xe6, 0x0d, 0x6b, 0xd8, 0x13, 0x17, 0xe0, 0x2d, 0x0d, 0xb8, 0x7e, 0xcb, 0x20, 0x6c, 0xa8, 0x73, 0xa7, 0xfd, - 0xe3, 0xa7, 0xfa, 0xf3, 0x02, 0x60, 0x78, 0x1f, 0x13, 0x40, 0x45, 0xee, 0x75, 0xf5, 0x10, 0xfd, 0x8f, 0x68, 0x74, - 0xd4, 0xac, 0xae, 0x04, 0x09, 0x55, 0x2c, 0xdb, 0xd8, 0x07, 0x07, 0x65, 0x69, 0x27, 0x6e, 0xbf, 0x5e, 0x61, 0x40, - 0x56, 0x8b, 0xd7, 0x33, 0x3b, 0xff, 0x6e, 0x53, 0x7e, 0x9d, 0x3f, 0xc0, 0x40, 0x3a, 0xab, 0xa0, 0x50, 0x4e, 0x80, - 0x47, 0x46, 0x0d, 0x1e, 0xdb, 0x4c, 0xf1, 0x1b, 0x5d, 0x3c, 0x2a, 0x54, 0xa7, 0x4d, 0xfa, 0x7b, 0x72, 0x66, 0xc5}; -unsigned int default_certificate_len = 475; diff --git a/samples/Basic_Ssl/include/ssl/private_key.h b/samples/Basic_Ssl/include/ssl/private_key.h deleted file mode 100644 index 31de50cb53..0000000000 --- a/samples/Basic_Ssl/include/ssl/private_key.h +++ /dev/null @@ -1,35 +0,0 @@ -unsigned char default_private_key[] = { - 0x30, 0x82, 0x02, 0x5d, 0x02, 0x01, 0x00, 0x02, 0x81, 0x81, 0x00, 0xcd, 0xfd, 0x89, 0x48, 0xbe, 0x36, 0xb9, 0x95, - 0x76, 0xd4, 0x13, 0x30, 0x0e, 0xbf, 0xb2, 0xed, 0x67, 0x0a, 0xc0, 0x16, 0x3f, 0x51, 0x09, 0x9d, 0x29, 0x2f, 0xb2, - 0x6d, 0x3f, 0x3e, 0x6c, 0x2f, 0x90, 0x80, 0xa1, 0x71, 0xdf, 0xbe, 0x38, 0xc5, 0xcb, 0xa9, 0x9a, 0x40, 0x14, 0x90, - 0x0a, 0xf9, 0xb7, 0x07, 0x0b, 0xe1, 0xda, 0xe7, 0x09, 0xbf, 0x0d, 0x57, 0x41, 0x86, 0x60, 0xa1, 0xc1, 0x27, 0x91, - 0x5b, 0x0a, 0x98, 0x46, 0x1b, 0xf6, 0xa2, 0x84, 0xf8, 0x65, 0xc7, 0xce, 0x2d, 0x96, 0x17, 0xaa, 0x91, 0xf8, 0x61, - 0x04, 0x50, 0x70, 0xeb, 0xb4, 0x43, 0xb7, 0xdc, 0x9a, 0xcc, 0x31, 0x01, 0x14, 0xd4, 0xcd, 0xcc, 0xc2, 0x37, 0x6d, - 0x69, 0x82, 0xd6, 0xc6, 0xc4, 0xbe, 0xf2, 0x34, 0xa5, 0xc9, 0xa6, 0x19, 0x53, 0x32, 0x7a, 0x86, 0x0e, 0x91, 0x82, - 0x0f, 0xa1, 0x42, 0x54, 0xaa, 0x01, 0x02, 0x03, 0x01, 0x00, 0x01, 0x02, 0x81, 0x81, 0x00, 0x95, 0xaa, 0x6e, 0x11, - 0xf5, 0x6a, 0x8b, 0xa2, 0xc6, 0x48, 0xc6, 0x7c, 0x37, 0x6b, 0x1f, 0x55, 0x10, 0x76, 0x26, 0x24, 0xc3, 0xf2, 0x5c, - 0x5a, 0xdd, 0x2e, 0xf3, 0xa4, 0x1e, 0xbc, 0x7b, 0x1c, 0x80, 0x10, 0x85, 0xbc, 0xd8, 0x45, 0x3c, 0xb8, 0xb2, 0x06, - 0x53, 0xb5, 0xd5, 0x7a, 0xe7, 0x0e, 0x92, 0xe6, 0x42, 0xc2, 0xe2, 0x2a, 0xd5, 0xd1, 0x03, 0x9f, 0x6f, 0x53, 0x74, - 0x68, 0x72, 0x8e, 0xbf, 0x03, 0xbb, 0xab, 0xbd, 0xa1, 0xf9, 0x81, 0x7d, 0x12, 0xd4, 0x9d, 0xb6, 0xae, 0x4c, 0xad, - 0xca, 0xa8, 0xc9, 0x80, 0x8d, 0x0d, 0xd5, 0xd0, 0xa1, 0xbf, 0xec, 0x60, 0x48, 0x49, 0xed, 0x97, 0x0f, 0x5e, 0xed, - 0xfc, 0x39, 0x15, 0x96, 0x9e, 0x5d, 0xe2, 0xb4, 0x5d, 0x2e, 0x04, 0xdc, 0x08, 0xa2, 0x65, 0x29, 0x2d, 0x37, 0xfb, - 0x62, 0x90, 0x1b, 0x7b, 0xe5, 0x3a, 0x58, 0x05, 0x55, 0xc1, 0x02, 0x41, 0x00, 0xfc, 0x69, 0x28, 0xc9, 0xa8, 0xc4, - 0x5c, 0xe3, 0xd0, 0x5e, 0xaa, 0xda, 0xde, 0x87, 0x74, 0xdb, 0xcb, 0x40, 0x78, 0x8e, 0x1d, 0x12, 0x96, 0x16, 0x61, - 0x3f, 0xb3, 0x3e, 0xa3, 0x0d, 0xdc, 0x49, 0xa5, 0x25, 0x87, 0xc5, 0x97, 0x85, 0x9d, 0xbb, 0xb4, 0xf0, 0x44, 0xfd, - 0x6c, 0xe8, 0xd2, 0x8c, 0xec, 0x33, 0x81, 0x46, 0x1e, 0x10, 0x12, 0x33, 0x16, 0x95, 0x00, 0x4f, 0x75, 0xb4, 0xe5, - 0x79, 0x02, 0x41, 0x00, 0xd0, 0xeb, 0x65, 0x07, 0x10, 0x3b, 0xd9, 0x03, 0xeb, 0xdc, 0x6f, 0x4b, 0x8f, 0xc3, 0x87, - 0xce, 0x76, 0xd6, 0xc5, 0x14, 0x21, 0x4e, 0xe7, 0x4f, 0x1b, 0xe8, 0x05, 0xf8, 0x84, 0x1a, 0xe0, 0xc5, 0xd6, 0xe3, - 0x08, 0xb3, 0x54, 0x57, 0x02, 0x1f, 0xd4, 0xd9, 0xfb, 0xff, 0x40, 0xb1, 0x56, 0x1c, 0x60, 0xf7, 0xac, 0x91, 0xf3, - 0xd3, 0xc6, 0x7f, 0x84, 0xfd, 0x84, 0x9d, 0xea, 0x26, 0xee, 0xc9, 0x02, 0x41, 0x00, 0xa6, 0xcf, 0x1c, 0x6c, 0x81, - 0x03, 0x1c, 0x5c, 0x56, 0x05, 0x6a, 0x26, 0x70, 0xef, 0xd6, 0x13, 0xb7, 0x74, 0x28, 0xf7, 0xca, 0x50, 0xd1, 0x2d, - 0x83, 0x21, 0x64, 0xe4, 0xdd, 0x3f, 0x38, 0xb8, 0xd6, 0xd2, 0x41, 0xb3, 0x1c, 0x9a, 0xea, 0x0d, 0xf5, 0xda, 0xdf, - 0xcd, 0x17, 0x9f, 0x9a, 0x1e, 0x15, 0xaf, 0x48, 0x1c, 0xbd, 0x9b, 0x63, 0x5b, 0xad, 0xed, 0xd4, 0xa1, 0xae, 0xa9, - 0x59, 0x09, 0x02, 0x40, 0x4e, 0x08, 0xce, 0xa8, 0x8f, 0xc0, 0xba, 0xf3, 0x83, 0x02, 0xc8, 0x33, 0x62, 0x14, 0x77, - 0xc2, 0x7f, 0x93, 0x02, 0xf3, 0xdc, 0xe9, 0x1a, 0xee, 0xea, 0x8e, 0x84, 0xc4, 0x69, 0x9b, 0x9c, 0x7f, 0x69, 0x1f, - 0x4e, 0x1d, 0xa5, 0x90, 0x06, 0x44, 0x1b, 0x7d, 0xfc, 0x69, 0x40, 0x21, 0xbc, 0xf7, 0x46, 0xa4, 0xdc, 0x39, 0x7b, - 0xe8, 0x8b, 0x49, 0x10, 0x44, 0x9d, 0x67, 0x5a, 0x91, 0x86, 0x39, 0x02, 0x40, 0x41, 0x2c, 0x4e, 0xfe, 0xd9, 0x90, - 0x89, 0x00, 0x5c, 0x94, 0x0a, 0x4a, 0x7e, 0x1b, 0x1a, 0x80, 0x06, 0x01, 0x37, 0xda, 0x50, 0x61, 0x9d, 0x9c, 0xfe, - 0x25, 0x7f, 0xd8, 0xd4, 0xc4, 0x9e, 0x81, 0xf2, 0x0c, 0x1e, 0x38, 0x21, 0x1e, 0x90, 0x3f, 0xd4, 0xba, 0x6c, 0x53, - 0xcb, 0xf0, 0x77, 0x79, 0x9b, 0xf1, 0xfa, 0x3f, 0x81, 0xdc, 0xf3, 0x21, 0x02, 0x6d, 0xb7, 0x95, 0xc3, 0x2e, 0xce, - 0xd5}; -unsigned int default_private_key_len = 609; diff --git a/samples/Echo_Ssl/component.mk b/samples/Echo_Ssl/component.mk index ae82bad52a..a252892ef8 100644 --- a/samples/Echo_Ssl/component.mk +++ b/samples/Echo_Ssl/component.mk @@ -1 +1 @@ -ENABLE_SSL = 1 +ENABLE_SSL ?= 1 diff --git a/samples/Echo_Ssl/include/ssl/cert.h b/samples/Echo_Ssl/include/ssl/cert.h deleted file mode 100644 index 61ca8a654e..0000000000 --- a/samples/Echo_Ssl/include/ssl/cert.h +++ /dev/null @@ -1,27 +0,0 @@ -unsigned char default_certificate[] = { - 0x30, 0x82, 0x01, 0xd7, 0x30, 0x82, 0x01, 0x40, 0x02, 0x09, 0x00, 0xab, 0x08, 0x18, 0xa7, 0x03, 0x07, 0x27, 0xfd, - 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x05, 0x05, 0x00, 0x30, 0x34, 0x31, 0x32, - 0x30, 0x30, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x13, 0x29, 0x61, 0x78, 0x54, 0x4c, 0x53, 0x20, 0x50, 0x72, 0x6f, 0x6a, - 0x65, 0x63, 0x74, 0x20, 0x44, 0x6f, 0x64, 0x67, 0x79, 0x20, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, - 0x74, 0x65, 0x20, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x30, 0x1e, 0x17, 0x0d, 0x31, 0x30, 0x31, - 0x32, 0x32, 0x36, 0x32, 0x32, 0x33, 0x33, 0x33, 0x39, 0x5a, 0x17, 0x0d, 0x32, 0x34, 0x30, 0x39, 0x30, 0x33, 0x32, - 0x32, 0x33, 0x33, 0x33, 0x39, 0x5a, 0x30, 0x2c, 0x31, 0x16, 0x30, 0x14, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x13, 0x0d, - 0x61, 0x78, 0x54, 0x4c, 0x53, 0x20, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x31, 0x12, 0x30, 0x10, 0x06, 0x03, - 0x55, 0x04, 0x03, 0x13, 0x09, 0x31, 0x32, 0x37, 0x2e, 0x30, 0x2e, 0x30, 0x2e, 0x31, 0x30, 0x81, 0x9f, 0x30, 0x0d, - 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x81, 0x8d, 0x00, 0x30, 0x81, - 0x89, 0x02, 0x81, 0x81, 0x00, 0xcd, 0xfd, 0x89, 0x48, 0xbe, 0x36, 0xb9, 0x95, 0x76, 0xd4, 0x13, 0x30, 0x0e, 0xbf, - 0xb2, 0xed, 0x67, 0x0a, 0xc0, 0x16, 0x3f, 0x51, 0x09, 0x9d, 0x29, 0x2f, 0xb2, 0x6d, 0x3f, 0x3e, 0x6c, 0x2f, 0x90, - 0x80, 0xa1, 0x71, 0xdf, 0xbe, 0x38, 0xc5, 0xcb, 0xa9, 0x9a, 0x40, 0x14, 0x90, 0x0a, 0xf9, 0xb7, 0x07, 0x0b, 0xe1, - 0xda, 0xe7, 0x09, 0xbf, 0x0d, 0x57, 0x41, 0x86, 0x60, 0xa1, 0xc1, 0x27, 0x91, 0x5b, 0x0a, 0x98, 0x46, 0x1b, 0xf6, - 0xa2, 0x84, 0xf8, 0x65, 0xc7, 0xce, 0x2d, 0x96, 0x17, 0xaa, 0x91, 0xf8, 0x61, 0x04, 0x50, 0x70, 0xeb, 0xb4, 0x43, - 0xb7, 0xdc, 0x9a, 0xcc, 0x31, 0x01, 0x14, 0xd4, 0xcd, 0xcc, 0xc2, 0x37, 0x6d, 0x69, 0x82, 0xd6, 0xc6, 0xc4, 0xbe, - 0xf2, 0x34, 0xa5, 0xc9, 0xa6, 0x19, 0x53, 0x32, 0x7a, 0x86, 0x0e, 0x91, 0x82, 0x0f, 0xa1, 0x42, 0x54, 0xaa, 0x01, - 0x02, 0x03, 0x01, 0x00, 0x01, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x05, 0x05, - 0x00, 0x03, 0x81, 0x81, 0x00, 0x40, 0xb4, 0x94, 0x9a, 0xa8, 0x89, 0x72, 0x1d, 0x07, 0xe5, 0xb3, 0x6b, 0x88, 0x21, - 0xc2, 0x38, 0x36, 0x9e, 0x7a, 0x8c, 0x49, 0x48, 0x68, 0x0c, 0x06, 0xe8, 0xdb, 0x1f, 0x4e, 0x05, 0xe6, 0x31, 0xe3, - 0xfd, 0xe6, 0x0d, 0x6b, 0xd8, 0x13, 0x17, 0xe0, 0x2d, 0x0d, 0xb8, 0x7e, 0xcb, 0x20, 0x6c, 0xa8, 0x73, 0xa7, 0xfd, - 0xe3, 0xa7, 0xfa, 0xf3, 0x02, 0x60, 0x78, 0x1f, 0x13, 0x40, 0x45, 0xee, 0x75, 0xf5, 0x10, 0xfd, 0x8f, 0x68, 0x74, - 0xd4, 0xac, 0xae, 0x04, 0x09, 0x55, 0x2c, 0xdb, 0xd8, 0x07, 0x07, 0x65, 0x69, 0x27, 0x6e, 0xbf, 0x5e, 0x61, 0x40, - 0x56, 0x8b, 0xd7, 0x33, 0x3b, 0xff, 0x6e, 0x53, 0x7e, 0x9d, 0x3f, 0xc0, 0x40, 0x3a, 0xab, 0xa0, 0x50, 0x4e, 0x80, - 0x47, 0x46, 0x0d, 0x1e, 0xdb, 0x4c, 0xf1, 0x1b, 0x5d, 0x3c, 0x2a, 0x54, 0xa7, 0x4d, 0xfa, 0x7b, 0x72, 0x66, 0xc5}; -unsigned int default_certificate_len = 475; diff --git a/samples/Echo_Ssl/include/ssl/private_key.h b/samples/Echo_Ssl/include/ssl/private_key.h deleted file mode 100644 index 31de50cb53..0000000000 --- a/samples/Echo_Ssl/include/ssl/private_key.h +++ /dev/null @@ -1,35 +0,0 @@ -unsigned char default_private_key[] = { - 0x30, 0x82, 0x02, 0x5d, 0x02, 0x01, 0x00, 0x02, 0x81, 0x81, 0x00, 0xcd, 0xfd, 0x89, 0x48, 0xbe, 0x36, 0xb9, 0x95, - 0x76, 0xd4, 0x13, 0x30, 0x0e, 0xbf, 0xb2, 0xed, 0x67, 0x0a, 0xc0, 0x16, 0x3f, 0x51, 0x09, 0x9d, 0x29, 0x2f, 0xb2, - 0x6d, 0x3f, 0x3e, 0x6c, 0x2f, 0x90, 0x80, 0xa1, 0x71, 0xdf, 0xbe, 0x38, 0xc5, 0xcb, 0xa9, 0x9a, 0x40, 0x14, 0x90, - 0x0a, 0xf9, 0xb7, 0x07, 0x0b, 0xe1, 0xda, 0xe7, 0x09, 0xbf, 0x0d, 0x57, 0x41, 0x86, 0x60, 0xa1, 0xc1, 0x27, 0x91, - 0x5b, 0x0a, 0x98, 0x46, 0x1b, 0xf6, 0xa2, 0x84, 0xf8, 0x65, 0xc7, 0xce, 0x2d, 0x96, 0x17, 0xaa, 0x91, 0xf8, 0x61, - 0x04, 0x50, 0x70, 0xeb, 0xb4, 0x43, 0xb7, 0xdc, 0x9a, 0xcc, 0x31, 0x01, 0x14, 0xd4, 0xcd, 0xcc, 0xc2, 0x37, 0x6d, - 0x69, 0x82, 0xd6, 0xc6, 0xc4, 0xbe, 0xf2, 0x34, 0xa5, 0xc9, 0xa6, 0x19, 0x53, 0x32, 0x7a, 0x86, 0x0e, 0x91, 0x82, - 0x0f, 0xa1, 0x42, 0x54, 0xaa, 0x01, 0x02, 0x03, 0x01, 0x00, 0x01, 0x02, 0x81, 0x81, 0x00, 0x95, 0xaa, 0x6e, 0x11, - 0xf5, 0x6a, 0x8b, 0xa2, 0xc6, 0x48, 0xc6, 0x7c, 0x37, 0x6b, 0x1f, 0x55, 0x10, 0x76, 0x26, 0x24, 0xc3, 0xf2, 0x5c, - 0x5a, 0xdd, 0x2e, 0xf3, 0xa4, 0x1e, 0xbc, 0x7b, 0x1c, 0x80, 0x10, 0x85, 0xbc, 0xd8, 0x45, 0x3c, 0xb8, 0xb2, 0x06, - 0x53, 0xb5, 0xd5, 0x7a, 0xe7, 0x0e, 0x92, 0xe6, 0x42, 0xc2, 0xe2, 0x2a, 0xd5, 0xd1, 0x03, 0x9f, 0x6f, 0x53, 0x74, - 0x68, 0x72, 0x8e, 0xbf, 0x03, 0xbb, 0xab, 0xbd, 0xa1, 0xf9, 0x81, 0x7d, 0x12, 0xd4, 0x9d, 0xb6, 0xae, 0x4c, 0xad, - 0xca, 0xa8, 0xc9, 0x80, 0x8d, 0x0d, 0xd5, 0xd0, 0xa1, 0xbf, 0xec, 0x60, 0x48, 0x49, 0xed, 0x97, 0x0f, 0x5e, 0xed, - 0xfc, 0x39, 0x15, 0x96, 0x9e, 0x5d, 0xe2, 0xb4, 0x5d, 0x2e, 0x04, 0xdc, 0x08, 0xa2, 0x65, 0x29, 0x2d, 0x37, 0xfb, - 0x62, 0x90, 0x1b, 0x7b, 0xe5, 0x3a, 0x58, 0x05, 0x55, 0xc1, 0x02, 0x41, 0x00, 0xfc, 0x69, 0x28, 0xc9, 0xa8, 0xc4, - 0x5c, 0xe3, 0xd0, 0x5e, 0xaa, 0xda, 0xde, 0x87, 0x74, 0xdb, 0xcb, 0x40, 0x78, 0x8e, 0x1d, 0x12, 0x96, 0x16, 0x61, - 0x3f, 0xb3, 0x3e, 0xa3, 0x0d, 0xdc, 0x49, 0xa5, 0x25, 0x87, 0xc5, 0x97, 0x85, 0x9d, 0xbb, 0xb4, 0xf0, 0x44, 0xfd, - 0x6c, 0xe8, 0xd2, 0x8c, 0xec, 0x33, 0x81, 0x46, 0x1e, 0x10, 0x12, 0x33, 0x16, 0x95, 0x00, 0x4f, 0x75, 0xb4, 0xe5, - 0x79, 0x02, 0x41, 0x00, 0xd0, 0xeb, 0x65, 0x07, 0x10, 0x3b, 0xd9, 0x03, 0xeb, 0xdc, 0x6f, 0x4b, 0x8f, 0xc3, 0x87, - 0xce, 0x76, 0xd6, 0xc5, 0x14, 0x21, 0x4e, 0xe7, 0x4f, 0x1b, 0xe8, 0x05, 0xf8, 0x84, 0x1a, 0xe0, 0xc5, 0xd6, 0xe3, - 0x08, 0xb3, 0x54, 0x57, 0x02, 0x1f, 0xd4, 0xd9, 0xfb, 0xff, 0x40, 0xb1, 0x56, 0x1c, 0x60, 0xf7, 0xac, 0x91, 0xf3, - 0xd3, 0xc6, 0x7f, 0x84, 0xfd, 0x84, 0x9d, 0xea, 0x26, 0xee, 0xc9, 0x02, 0x41, 0x00, 0xa6, 0xcf, 0x1c, 0x6c, 0x81, - 0x03, 0x1c, 0x5c, 0x56, 0x05, 0x6a, 0x26, 0x70, 0xef, 0xd6, 0x13, 0xb7, 0x74, 0x28, 0xf7, 0xca, 0x50, 0xd1, 0x2d, - 0x83, 0x21, 0x64, 0xe4, 0xdd, 0x3f, 0x38, 0xb8, 0xd6, 0xd2, 0x41, 0xb3, 0x1c, 0x9a, 0xea, 0x0d, 0xf5, 0xda, 0xdf, - 0xcd, 0x17, 0x9f, 0x9a, 0x1e, 0x15, 0xaf, 0x48, 0x1c, 0xbd, 0x9b, 0x63, 0x5b, 0xad, 0xed, 0xd4, 0xa1, 0xae, 0xa9, - 0x59, 0x09, 0x02, 0x40, 0x4e, 0x08, 0xce, 0xa8, 0x8f, 0xc0, 0xba, 0xf3, 0x83, 0x02, 0xc8, 0x33, 0x62, 0x14, 0x77, - 0xc2, 0x7f, 0x93, 0x02, 0xf3, 0xdc, 0xe9, 0x1a, 0xee, 0xea, 0x8e, 0x84, 0xc4, 0x69, 0x9b, 0x9c, 0x7f, 0x69, 0x1f, - 0x4e, 0x1d, 0xa5, 0x90, 0x06, 0x44, 0x1b, 0x7d, 0xfc, 0x69, 0x40, 0x21, 0xbc, 0xf7, 0x46, 0xa4, 0xdc, 0x39, 0x7b, - 0xe8, 0x8b, 0x49, 0x10, 0x44, 0x9d, 0x67, 0x5a, 0x91, 0x86, 0x39, 0x02, 0x40, 0x41, 0x2c, 0x4e, 0xfe, 0xd9, 0x90, - 0x89, 0x00, 0x5c, 0x94, 0x0a, 0x4a, 0x7e, 0x1b, 0x1a, 0x80, 0x06, 0x01, 0x37, 0xda, 0x50, 0x61, 0x9d, 0x9c, 0xfe, - 0x25, 0x7f, 0xd8, 0xd4, 0xc4, 0x9e, 0x81, 0xf2, 0x0c, 0x1e, 0x38, 0x21, 0x1e, 0x90, 0x3f, 0xd4, 0xba, 0x6c, 0x53, - 0xcb, 0xf0, 0x77, 0x79, 0x9b, 0xf1, 0xfa, 0x3f, 0x81, 0xdc, 0xf3, 0x21, 0x02, 0x6d, 0xb7, 0x95, 0xc3, 0x2e, 0xce, - 0xd5}; -unsigned int default_private_key_len = 609; diff --git a/samples/HttpClient/include/ssl/cert.h b/samples/HttpClient/include/ssl/cert.h deleted file mode 100644 index 61ca8a654e..0000000000 --- a/samples/HttpClient/include/ssl/cert.h +++ /dev/null @@ -1,27 +0,0 @@ -unsigned char default_certificate[] = { - 0x30, 0x82, 0x01, 0xd7, 0x30, 0x82, 0x01, 0x40, 0x02, 0x09, 0x00, 0xab, 0x08, 0x18, 0xa7, 0x03, 0x07, 0x27, 0xfd, - 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x05, 0x05, 0x00, 0x30, 0x34, 0x31, 0x32, - 0x30, 0x30, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x13, 0x29, 0x61, 0x78, 0x54, 0x4c, 0x53, 0x20, 0x50, 0x72, 0x6f, 0x6a, - 0x65, 0x63, 0x74, 0x20, 0x44, 0x6f, 0x64, 0x67, 0x79, 0x20, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, - 0x74, 0x65, 0x20, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x30, 0x1e, 0x17, 0x0d, 0x31, 0x30, 0x31, - 0x32, 0x32, 0x36, 0x32, 0x32, 0x33, 0x33, 0x33, 0x39, 0x5a, 0x17, 0x0d, 0x32, 0x34, 0x30, 0x39, 0x30, 0x33, 0x32, - 0x32, 0x33, 0x33, 0x33, 0x39, 0x5a, 0x30, 0x2c, 0x31, 0x16, 0x30, 0x14, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x13, 0x0d, - 0x61, 0x78, 0x54, 0x4c, 0x53, 0x20, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x31, 0x12, 0x30, 0x10, 0x06, 0x03, - 0x55, 0x04, 0x03, 0x13, 0x09, 0x31, 0x32, 0x37, 0x2e, 0x30, 0x2e, 0x30, 0x2e, 0x31, 0x30, 0x81, 0x9f, 0x30, 0x0d, - 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x81, 0x8d, 0x00, 0x30, 0x81, - 0x89, 0x02, 0x81, 0x81, 0x00, 0xcd, 0xfd, 0x89, 0x48, 0xbe, 0x36, 0xb9, 0x95, 0x76, 0xd4, 0x13, 0x30, 0x0e, 0xbf, - 0xb2, 0xed, 0x67, 0x0a, 0xc0, 0x16, 0x3f, 0x51, 0x09, 0x9d, 0x29, 0x2f, 0xb2, 0x6d, 0x3f, 0x3e, 0x6c, 0x2f, 0x90, - 0x80, 0xa1, 0x71, 0xdf, 0xbe, 0x38, 0xc5, 0xcb, 0xa9, 0x9a, 0x40, 0x14, 0x90, 0x0a, 0xf9, 0xb7, 0x07, 0x0b, 0xe1, - 0xda, 0xe7, 0x09, 0xbf, 0x0d, 0x57, 0x41, 0x86, 0x60, 0xa1, 0xc1, 0x27, 0x91, 0x5b, 0x0a, 0x98, 0x46, 0x1b, 0xf6, - 0xa2, 0x84, 0xf8, 0x65, 0xc7, 0xce, 0x2d, 0x96, 0x17, 0xaa, 0x91, 0xf8, 0x61, 0x04, 0x50, 0x70, 0xeb, 0xb4, 0x43, - 0xb7, 0xdc, 0x9a, 0xcc, 0x31, 0x01, 0x14, 0xd4, 0xcd, 0xcc, 0xc2, 0x37, 0x6d, 0x69, 0x82, 0xd6, 0xc6, 0xc4, 0xbe, - 0xf2, 0x34, 0xa5, 0xc9, 0xa6, 0x19, 0x53, 0x32, 0x7a, 0x86, 0x0e, 0x91, 0x82, 0x0f, 0xa1, 0x42, 0x54, 0xaa, 0x01, - 0x02, 0x03, 0x01, 0x00, 0x01, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x05, 0x05, - 0x00, 0x03, 0x81, 0x81, 0x00, 0x40, 0xb4, 0x94, 0x9a, 0xa8, 0x89, 0x72, 0x1d, 0x07, 0xe5, 0xb3, 0x6b, 0x88, 0x21, - 0xc2, 0x38, 0x36, 0x9e, 0x7a, 0x8c, 0x49, 0x48, 0x68, 0x0c, 0x06, 0xe8, 0xdb, 0x1f, 0x4e, 0x05, 0xe6, 0x31, 0xe3, - 0xfd, 0xe6, 0x0d, 0x6b, 0xd8, 0x13, 0x17, 0xe0, 0x2d, 0x0d, 0xb8, 0x7e, 0xcb, 0x20, 0x6c, 0xa8, 0x73, 0xa7, 0xfd, - 0xe3, 0xa7, 0xfa, 0xf3, 0x02, 0x60, 0x78, 0x1f, 0x13, 0x40, 0x45, 0xee, 0x75, 0xf5, 0x10, 0xfd, 0x8f, 0x68, 0x74, - 0xd4, 0xac, 0xae, 0x04, 0x09, 0x55, 0x2c, 0xdb, 0xd8, 0x07, 0x07, 0x65, 0x69, 0x27, 0x6e, 0xbf, 0x5e, 0x61, 0x40, - 0x56, 0x8b, 0xd7, 0x33, 0x3b, 0xff, 0x6e, 0x53, 0x7e, 0x9d, 0x3f, 0xc0, 0x40, 0x3a, 0xab, 0xa0, 0x50, 0x4e, 0x80, - 0x47, 0x46, 0x0d, 0x1e, 0xdb, 0x4c, 0xf1, 0x1b, 0x5d, 0x3c, 0x2a, 0x54, 0xa7, 0x4d, 0xfa, 0x7b, 0x72, 0x66, 0xc5}; -unsigned int default_certificate_len = 475; diff --git a/samples/HttpClient/include/ssl/private_key.h b/samples/HttpClient/include/ssl/private_key.h deleted file mode 100644 index 31de50cb53..0000000000 --- a/samples/HttpClient/include/ssl/private_key.h +++ /dev/null @@ -1,35 +0,0 @@ -unsigned char default_private_key[] = { - 0x30, 0x82, 0x02, 0x5d, 0x02, 0x01, 0x00, 0x02, 0x81, 0x81, 0x00, 0xcd, 0xfd, 0x89, 0x48, 0xbe, 0x36, 0xb9, 0x95, - 0x76, 0xd4, 0x13, 0x30, 0x0e, 0xbf, 0xb2, 0xed, 0x67, 0x0a, 0xc0, 0x16, 0x3f, 0x51, 0x09, 0x9d, 0x29, 0x2f, 0xb2, - 0x6d, 0x3f, 0x3e, 0x6c, 0x2f, 0x90, 0x80, 0xa1, 0x71, 0xdf, 0xbe, 0x38, 0xc5, 0xcb, 0xa9, 0x9a, 0x40, 0x14, 0x90, - 0x0a, 0xf9, 0xb7, 0x07, 0x0b, 0xe1, 0xda, 0xe7, 0x09, 0xbf, 0x0d, 0x57, 0x41, 0x86, 0x60, 0xa1, 0xc1, 0x27, 0x91, - 0x5b, 0x0a, 0x98, 0x46, 0x1b, 0xf6, 0xa2, 0x84, 0xf8, 0x65, 0xc7, 0xce, 0x2d, 0x96, 0x17, 0xaa, 0x91, 0xf8, 0x61, - 0x04, 0x50, 0x70, 0xeb, 0xb4, 0x43, 0xb7, 0xdc, 0x9a, 0xcc, 0x31, 0x01, 0x14, 0xd4, 0xcd, 0xcc, 0xc2, 0x37, 0x6d, - 0x69, 0x82, 0xd6, 0xc6, 0xc4, 0xbe, 0xf2, 0x34, 0xa5, 0xc9, 0xa6, 0x19, 0x53, 0x32, 0x7a, 0x86, 0x0e, 0x91, 0x82, - 0x0f, 0xa1, 0x42, 0x54, 0xaa, 0x01, 0x02, 0x03, 0x01, 0x00, 0x01, 0x02, 0x81, 0x81, 0x00, 0x95, 0xaa, 0x6e, 0x11, - 0xf5, 0x6a, 0x8b, 0xa2, 0xc6, 0x48, 0xc6, 0x7c, 0x37, 0x6b, 0x1f, 0x55, 0x10, 0x76, 0x26, 0x24, 0xc3, 0xf2, 0x5c, - 0x5a, 0xdd, 0x2e, 0xf3, 0xa4, 0x1e, 0xbc, 0x7b, 0x1c, 0x80, 0x10, 0x85, 0xbc, 0xd8, 0x45, 0x3c, 0xb8, 0xb2, 0x06, - 0x53, 0xb5, 0xd5, 0x7a, 0xe7, 0x0e, 0x92, 0xe6, 0x42, 0xc2, 0xe2, 0x2a, 0xd5, 0xd1, 0x03, 0x9f, 0x6f, 0x53, 0x74, - 0x68, 0x72, 0x8e, 0xbf, 0x03, 0xbb, 0xab, 0xbd, 0xa1, 0xf9, 0x81, 0x7d, 0x12, 0xd4, 0x9d, 0xb6, 0xae, 0x4c, 0xad, - 0xca, 0xa8, 0xc9, 0x80, 0x8d, 0x0d, 0xd5, 0xd0, 0xa1, 0xbf, 0xec, 0x60, 0x48, 0x49, 0xed, 0x97, 0x0f, 0x5e, 0xed, - 0xfc, 0x39, 0x15, 0x96, 0x9e, 0x5d, 0xe2, 0xb4, 0x5d, 0x2e, 0x04, 0xdc, 0x08, 0xa2, 0x65, 0x29, 0x2d, 0x37, 0xfb, - 0x62, 0x90, 0x1b, 0x7b, 0xe5, 0x3a, 0x58, 0x05, 0x55, 0xc1, 0x02, 0x41, 0x00, 0xfc, 0x69, 0x28, 0xc9, 0xa8, 0xc4, - 0x5c, 0xe3, 0xd0, 0x5e, 0xaa, 0xda, 0xde, 0x87, 0x74, 0xdb, 0xcb, 0x40, 0x78, 0x8e, 0x1d, 0x12, 0x96, 0x16, 0x61, - 0x3f, 0xb3, 0x3e, 0xa3, 0x0d, 0xdc, 0x49, 0xa5, 0x25, 0x87, 0xc5, 0x97, 0x85, 0x9d, 0xbb, 0xb4, 0xf0, 0x44, 0xfd, - 0x6c, 0xe8, 0xd2, 0x8c, 0xec, 0x33, 0x81, 0x46, 0x1e, 0x10, 0x12, 0x33, 0x16, 0x95, 0x00, 0x4f, 0x75, 0xb4, 0xe5, - 0x79, 0x02, 0x41, 0x00, 0xd0, 0xeb, 0x65, 0x07, 0x10, 0x3b, 0xd9, 0x03, 0xeb, 0xdc, 0x6f, 0x4b, 0x8f, 0xc3, 0x87, - 0xce, 0x76, 0xd6, 0xc5, 0x14, 0x21, 0x4e, 0xe7, 0x4f, 0x1b, 0xe8, 0x05, 0xf8, 0x84, 0x1a, 0xe0, 0xc5, 0xd6, 0xe3, - 0x08, 0xb3, 0x54, 0x57, 0x02, 0x1f, 0xd4, 0xd9, 0xfb, 0xff, 0x40, 0xb1, 0x56, 0x1c, 0x60, 0xf7, 0xac, 0x91, 0xf3, - 0xd3, 0xc6, 0x7f, 0x84, 0xfd, 0x84, 0x9d, 0xea, 0x26, 0xee, 0xc9, 0x02, 0x41, 0x00, 0xa6, 0xcf, 0x1c, 0x6c, 0x81, - 0x03, 0x1c, 0x5c, 0x56, 0x05, 0x6a, 0x26, 0x70, 0xef, 0xd6, 0x13, 0xb7, 0x74, 0x28, 0xf7, 0xca, 0x50, 0xd1, 0x2d, - 0x83, 0x21, 0x64, 0xe4, 0xdd, 0x3f, 0x38, 0xb8, 0xd6, 0xd2, 0x41, 0xb3, 0x1c, 0x9a, 0xea, 0x0d, 0xf5, 0xda, 0xdf, - 0xcd, 0x17, 0x9f, 0x9a, 0x1e, 0x15, 0xaf, 0x48, 0x1c, 0xbd, 0x9b, 0x63, 0x5b, 0xad, 0xed, 0xd4, 0xa1, 0xae, 0xa9, - 0x59, 0x09, 0x02, 0x40, 0x4e, 0x08, 0xce, 0xa8, 0x8f, 0xc0, 0xba, 0xf3, 0x83, 0x02, 0xc8, 0x33, 0x62, 0x14, 0x77, - 0xc2, 0x7f, 0x93, 0x02, 0xf3, 0xdc, 0xe9, 0x1a, 0xee, 0xea, 0x8e, 0x84, 0xc4, 0x69, 0x9b, 0x9c, 0x7f, 0x69, 0x1f, - 0x4e, 0x1d, 0xa5, 0x90, 0x06, 0x44, 0x1b, 0x7d, 0xfc, 0x69, 0x40, 0x21, 0xbc, 0xf7, 0x46, 0xa4, 0xdc, 0x39, 0x7b, - 0xe8, 0x8b, 0x49, 0x10, 0x44, 0x9d, 0x67, 0x5a, 0x91, 0x86, 0x39, 0x02, 0x40, 0x41, 0x2c, 0x4e, 0xfe, 0xd9, 0x90, - 0x89, 0x00, 0x5c, 0x94, 0x0a, 0x4a, 0x7e, 0x1b, 0x1a, 0x80, 0x06, 0x01, 0x37, 0xda, 0x50, 0x61, 0x9d, 0x9c, 0xfe, - 0x25, 0x7f, 0xd8, 0xd4, 0xc4, 0x9e, 0x81, 0xf2, 0x0c, 0x1e, 0x38, 0x21, 0x1e, 0x90, 0x3f, 0xd4, 0xba, 0x6c, 0x53, - 0xcb, 0xf0, 0x77, 0x79, 0x9b, 0xf1, 0xfa, 0x3f, 0x81, 0xdc, 0xf3, 0x21, 0x02, 0x6d, 0xb7, 0x95, 0xc3, 0x2e, 0xce, - 0xd5}; -unsigned int default_private_key_len = 609; diff --git a/samples/HttpServer_ConfigNetwork/include/ssl/cert.h b/samples/HttpServer_ConfigNetwork/include/ssl/cert.h deleted file mode 100644 index 8515409e43..0000000000 --- a/samples/HttpServer_ConfigNetwork/include/ssl/cert.h +++ /dev/null @@ -1,27 +0,0 @@ -unsigned char x509_1024_cer[] = { - 0x30, 0x82, 0x01, 0xd7, 0x30, 0x82, 0x01, 0x40, 0x02, 0x09, 0x00, 0xaa, 0x8d, 0xd9, 0xdc, 0x30, 0x3f, 0xda, 0x4d, - 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x05, 0x05, 0x00, 0x30, 0x34, 0x31, 0x32, - 0x30, 0x30, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x13, 0x29, 0x53, 0x6d, 0x69, 0x6e, 0x67, 0x20, 0x50, 0x72, 0x6f, 0x6a, - 0x65, 0x63, 0x74, 0x20, 0x44, 0x6f, 0x64, 0x67, 0x79, 0x20, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, - 0x74, 0x65, 0x20, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x30, 0x1e, 0x17, 0x0d, 0x31, 0x39, 0x31, - 0x32, 0x32, 0x38, 0x32, 0x33, 0x31, 0x36, 0x31, 0x35, 0x5a, 0x17, 0x0d, 0x33, 0x33, 0x30, 0x39, 0x30, 0x35, 0x32, - 0x33, 0x31, 0x36, 0x31, 0x35, 0x5a, 0x30, 0x2c, 0x31, 0x16, 0x30, 0x14, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x13, 0x0d, - 0x53, 0x6d, 0x69, 0x6e, 0x67, 0x20, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x31, 0x12, 0x30, 0x10, 0x06, 0x03, - 0x55, 0x04, 0x03, 0x13, 0x09, 0x31, 0x32, 0x37, 0x2e, 0x30, 0x2e, 0x30, 0x2e, 0x31, 0x30, 0x81, 0x9f, 0x30, 0x0d, - 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x81, 0x8d, 0x00, 0x30, 0x81, - 0x89, 0x02, 0x81, 0x81, 0x00, 0xcf, 0xe2, 0x20, 0xf1, 0xd8, 0x18, 0x5f, 0x22, 0x09, 0xa8, 0x34, 0x00, 0xca, 0x15, - 0x39, 0xfc, 0x7b, 0xf3, 0x68, 0xd5, 0xb7, 0xd3, 0xbc, 0xb6, 0xa7, 0xb6, 0xbe, 0x0c, 0x4f, 0xfa, 0x98, 0x77, 0xe9, - 0x3f, 0x2c, 0x33, 0x0b, 0x5f, 0xb4, 0x3b, 0xc3, 0xc1, 0x7f, 0x91, 0x45, 0x46, 0xd7, 0x22, 0x9e, 0x81, 0x45, 0x80, - 0xb5, 0x71, 0x0c, 0xb0, 0xd7, 0x47, 0x48, 0xad, 0xe2, 0x9f, 0x6c, 0x55, 0x3c, 0x1c, 0xe9, 0x4a, 0x39, 0x78, 0xc6, - 0xa2, 0x0a, 0x46, 0xa2, 0xb8, 0x51, 0xaa, 0xa5, 0x88, 0xac, 0x8e, 0x1e, 0x90, 0x16, 0x4c, 0xac, 0x23, 0x2c, 0xfa, - 0x25, 0x62, 0x88, 0xb0, 0xa7, 0x6b, 0x83, 0x6a, 0x68, 0xed, 0x9c, 0xc5, 0xf6, 0xd0, 0x69, 0x1d, 0x49, 0x04, 0xe1, - 0x0e, 0xf8, 0x25, 0xfe, 0xa6, 0x53, 0x5b, 0x00, 0x09, 0x59, 0xaa, 0x2d, 0x71, 0x28, 0x68, 0x1a, 0x63, 0x0a, 0x93, - 0x02, 0x03, 0x01, 0x00, 0x01, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x05, 0x05, - 0x00, 0x03, 0x81, 0x81, 0x00, 0x57, 0x3a, 0xc7, 0xc5, 0x7a, 0x3d, 0x9b, 0x45, 0x6c, 0xcc, 0x18, 0x29, 0xa7, 0x54, - 0x10, 0x51, 0xd4, 0x1e, 0xe0, 0xf6, 0x18, 0xee, 0x93, 0xa5, 0xfc, 0x12, 0xf7, 0x5e, 0x93, 0x16, 0xed, 0x7d, 0xc3, - 0x2a, 0x2d, 0x0e, 0xc6, 0x3a, 0x03, 0x05, 0x85, 0x13, 0xc8, 0xfb, 0x50, 0xf4, 0x91, 0x23, 0xb2, 0x93, 0x6a, 0x45, - 0x9d, 0x15, 0x72, 0x8a, 0x09, 0x15, 0xae, 0x39, 0x71, 0xa6, 0xe7, 0x4c, 0xe7, 0xf5, 0x3a, 0x8a, 0xdc, 0xb7, 0xfd, - 0x20, 0x86, 0x94, 0x9e, 0x52, 0x3a, 0x44, 0x03, 0xd4, 0xdc, 0x9c, 0xbd, 0xbc, 0xcc, 0x3b, 0x03, 0x55, 0xed, 0x96, - 0x68, 0x35, 0x93, 0x3e, 0xbf, 0x74, 0x38, 0x87, 0x9b, 0x61, 0xb7, 0xf5, 0x0e, 0x16, 0x98, 0xbe, 0x14, 0x49, 0x73, - 0x15, 0xf2, 0x70, 0xf5, 0x48, 0xb6, 0x05, 0x8b, 0xbf, 0x89, 0x69, 0x73, 0x18, 0x8f, 0x85, 0xee, 0xca, 0x80, 0x4d}; -unsigned int x509_1024_cer_len = 475; diff --git a/samples/HttpServer_ConfigNetwork/include/ssl/private_key.h b/samples/HttpServer_ConfigNetwork/include/ssl/private_key.h deleted file mode 100644 index da28f9a732..0000000000 --- a/samples/HttpServer_ConfigNetwork/include/ssl/private_key.h +++ /dev/null @@ -1,35 +0,0 @@ -unsigned char key_1024[] = { - 0x30, 0x82, 0x02, 0x5d, 0x02, 0x01, 0x00, 0x02, 0x81, 0x81, 0x00, 0xcf, 0xe2, 0x20, 0xf1, 0xd8, 0x18, 0x5f, 0x22, - 0x09, 0xa8, 0x34, 0x00, 0xca, 0x15, 0x39, 0xfc, 0x7b, 0xf3, 0x68, 0xd5, 0xb7, 0xd3, 0xbc, 0xb6, 0xa7, 0xb6, 0xbe, - 0x0c, 0x4f, 0xfa, 0x98, 0x77, 0xe9, 0x3f, 0x2c, 0x33, 0x0b, 0x5f, 0xb4, 0x3b, 0xc3, 0xc1, 0x7f, 0x91, 0x45, 0x46, - 0xd7, 0x22, 0x9e, 0x81, 0x45, 0x80, 0xb5, 0x71, 0x0c, 0xb0, 0xd7, 0x47, 0x48, 0xad, 0xe2, 0x9f, 0x6c, 0x55, 0x3c, - 0x1c, 0xe9, 0x4a, 0x39, 0x78, 0xc6, 0xa2, 0x0a, 0x46, 0xa2, 0xb8, 0x51, 0xaa, 0xa5, 0x88, 0xac, 0x8e, 0x1e, 0x90, - 0x16, 0x4c, 0xac, 0x23, 0x2c, 0xfa, 0x25, 0x62, 0x88, 0xb0, 0xa7, 0x6b, 0x83, 0x6a, 0x68, 0xed, 0x9c, 0xc5, 0xf6, - 0xd0, 0x69, 0x1d, 0x49, 0x04, 0xe1, 0x0e, 0xf8, 0x25, 0xfe, 0xa6, 0x53, 0x5b, 0x00, 0x09, 0x59, 0xaa, 0x2d, 0x71, - 0x28, 0x68, 0x1a, 0x63, 0x0a, 0x93, 0x02, 0x03, 0x01, 0x00, 0x01, 0x02, 0x81, 0x80, 0x4c, 0x56, 0x5b, 0x86, 0xb3, - 0xb5, 0xef, 0x69, 0x4e, 0x66, 0x88, 0x02, 0x2a, 0x33, 0x35, 0x41, 0xf4, 0x3a, 0x64, 0x2a, 0xe7, 0x00, 0x47, 0xf2, - 0x43, 0x10, 0x26, 0x25, 0xdb, 0x50, 0xc8, 0xa0, 0x6f, 0xf4, 0x94, 0xc4, 0x81, 0xce, 0xb9, 0x1e, 0xa1, 0x39, 0xf3, - 0x20, 0x63, 0x72, 0x2a, 0x1a, 0x3a, 0x5e, 0x7a, 0x29, 0x53, 0x77, 0x9a, 0x13, 0x6e, 0x5a, 0x6c, 0xe3, 0xfd, 0xae, - 0x51, 0x57, 0x29, 0xb3, 0xb0, 0x01, 0x69, 0x58, 0xd7, 0x34, 0x36, 0xb2, 0xea, 0xc9, 0xb4, 0x03, 0x0f, 0xf2, 0x17, - 0xaf, 0xc1, 0x8c, 0xe1, 0x7e, 0x29, 0xb5, 0x97, 0x80, 0x61, 0xf7, 0x59, 0xe6, 0x53, 0xb0, 0xa2, 0x77, 0x38, 0x57, - 0xd2, 0x5e, 0x82, 0x8a, 0x3b, 0x3f, 0xef, 0x54, 0x60, 0x9f, 0x2e, 0xb5, 0xbe, 0x8e, 0x64, 0xb1, 0x18, 0xdf, 0x1e, - 0x15, 0x35, 0x3f, 0x9a, 0xe9, 0x65, 0xcc, 0xc0, 0x31, 0x02, 0x41, 0x00, 0xf4, 0x0e, 0x50, 0x85, 0x10, 0x96, 0x6b, - 0xa0, 0xd6, 0xeb, 0xcb, 0x72, 0xe9, 0xcf, 0x59, 0x01, 0x8c, 0x85, 0x87, 0xb9, 0xa0, 0x19, 0x1d, 0x55, 0x05, 0xd7, - 0x37, 0x19, 0xb5, 0x99, 0xdc, 0x3a, 0x92, 0x22, 0x8d, 0x1b, 0xe0, 0xb8, 0x88, 0xe5, 0xe5, 0x4a, 0x74, 0x63, 0x28, - 0x22, 0xeb, 0xe2, 0x1f, 0x01, 0x50, 0x57, 0x17, 0xa8, 0x04, 0x68, 0xf4, 0x6a, 0x7d, 0xb6, 0x4a, 0x8c, 0xc2, 0x25, - 0x02, 0x41, 0x00, 0xda, 0x0e, 0x9f, 0x03, 0x65, 0x3c, 0xfa, 0xfa, 0xec, 0xf1, 0xc5, 0x1d, 0xf9, 0x94, 0xc0, 0x86, - 0xf5, 0x76, 0xd7, 0xee, 0x93, 0x87, 0x3d, 0x7e, 0xd9, 0x45, 0x56, 0x7d, 0xcc, 0xe1, 0x55, 0x9d, 0xc2, 0xf7, 0x89, - 0x9f, 0x9f, 0xf1, 0xe3, 0xdb, 0x17, 0xc9, 0xc8, 0x5a, 0x53, 0x15, 0x8a, 0x7a, 0x94, 0x39, 0x4e, 0x29, 0xd2, 0xf9, - 0xb6, 0xe7, 0x1c, 0x15, 0x68, 0x95, 0x40, 0xe3, 0xd0, 0x57, 0x02, 0x41, 0x00, 0xb1, 0x2a, 0x9f, 0x0b, 0x29, 0xb2, - 0x78, 0x69, 0x26, 0xfb, 0xbf, 0x12, 0x29, 0x67, 0x0b, 0x0e, 0xd3, 0xca, 0xaf, 0x6f, 0x72, 0x28, 0x29, 0x21, 0xea, - 0x7e, 0x84, 0x12, 0x56, 0xc1, 0x5d, 0x9c, 0xeb, 0x2e, 0xc7, 0xce, 0xe0, 0x00, 0x35, 0xe8, 0xe5, 0xdd, 0x79, 0xc5, - 0xed, 0x82, 0x04, 0x48, 0x7f, 0x07, 0x7e, 0x21, 0xeb, 0x1b, 0x5e, 0x30, 0x2e, 0x96, 0x0b, 0xb2, 0x44, 0x46, 0x10, - 0x3d, 0x02, 0x40, 0x7d, 0x18, 0x18, 0x37, 0x1d, 0x74, 0x0f, 0x53, 0xb6, 0x6c, 0xb8, 0xb5, 0x8a, 0x81, 0xc0, 0xb5, - 0x6b, 0xca, 0x42, 0xf4, 0x36, 0x24, 0x46, 0xae, 0x27, 0xbc, 0xf4, 0x72, 0x74, 0xff, 0xec, 0x5a, 0xf6, 0x07, 0x86, - 0x27, 0x51, 0xdd, 0xb5, 0xe6, 0xf1, 0xcd, 0xab, 0xa7, 0xcd, 0xb4, 0x34, 0xde, 0x3f, 0x7c, 0x64, 0x8f, 0xef, 0xdd, - 0x9c, 0x05, 0x17, 0x82, 0x5c, 0x9e, 0x0b, 0x3c, 0xe6, 0x3f, 0x02, 0x41, 0x00, 0xcf, 0xb5, 0x87, 0xfa, 0xc3, 0x82, - 0x7b, 0x18, 0x01, 0x8f, 0x88, 0xa4, 0xd4, 0xd9, 0xaa, 0x45, 0x74, 0x83, 0x80, 0x2b, 0xa7, 0x36, 0x2d, 0x91, 0x11, - 0x81, 0x67, 0x44, 0xeb, 0x49, 0xc5, 0x18, 0xbe, 0xff, 0x9a, 0x5b, 0x33, 0x19, 0xf4, 0x40, 0x6e, 0x4d, 0xb9, 0x1b, - 0xc2, 0xda, 0xf8, 0xd3, 0x92, 0x4c, 0x22, 0x6f, 0x25, 0xc6, 0x68, 0x7a, 0x7e, 0x18, 0x99, 0x05, 0x04, 0x91, 0x26, - 0xbf}; -unsigned int key_1024_len = 609; diff --git a/samples/MqttClient_Hello/include/ssl/cert.h b/samples/MqttClient_Hello/include/ssl/cert.h index 61ca8a654e..656efaadbd 100644 --- a/samples/MqttClient_Hello/include/ssl/cert.h +++ b/samples/MqttClient_Hello/include/ssl/cert.h @@ -1,4 +1,4 @@ -unsigned char default_certificate[] = { +unsigned char default_certificate[] PROGMEM = { 0x30, 0x82, 0x01, 0xd7, 0x30, 0x82, 0x01, 0x40, 0x02, 0x09, 0x00, 0xab, 0x08, 0x18, 0xa7, 0x03, 0x07, 0x27, 0xfd, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x05, 0x05, 0x00, 0x30, 0x34, 0x31, 0x32, 0x30, 0x30, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x13, 0x29, 0x61, 0x78, 0x54, 0x4c, 0x53, 0x20, 0x50, 0x72, 0x6f, 0x6a, diff --git a/samples/MqttClient_Hello/include/ssl/private_key.h b/samples/MqttClient_Hello/include/ssl/private_key.h index 31de50cb53..e4d72a0bfc 100644 --- a/samples/MqttClient_Hello/include/ssl/private_key.h +++ b/samples/MqttClient_Hello/include/ssl/private_key.h @@ -1,4 +1,4 @@ -unsigned char default_private_key[] = { +unsigned char default_private_key[] PROGMEM = { 0x30, 0x82, 0x02, 0x5d, 0x02, 0x01, 0x00, 0x02, 0x81, 0x81, 0x00, 0xcd, 0xfd, 0x89, 0x48, 0xbe, 0x36, 0xb9, 0x95, 0x76, 0xd4, 0x13, 0x30, 0x0e, 0xbf, 0xb2, 0xed, 0x67, 0x0a, 0xc0, 0x16, 0x3f, 0x51, 0x09, 0x9d, 0x29, 0x2f, 0xb2, 0x6d, 0x3f, 0x3e, 0x6c, 0x2f, 0x90, 0x80, 0xa1, 0x71, 0xdf, 0xbe, 0x38, 0xc5, 0xcb, 0xa9, 0x9a, 0x40, 0x14, 0x90, diff --git a/samples/SmtpClient/component.mk b/samples/SmtpClient/component.mk index 7f43d35855..4113e94c06 100644 --- a/samples/SmtpClient/component.mk +++ b/samples/SmtpClient/component.mk @@ -1,2 +1,2 @@ HWCONFIG := spiffs-2m -ENABLE_SSL ?= 1 +ENABLE_SSL ?= 1 diff --git a/samples/SmtpClient/include/ssl/cert.h b/samples/SmtpClient/include/ssl/cert.h deleted file mode 100644 index 0f5317732d..0000000000 --- a/samples/SmtpClient/include/ssl/cert.h +++ /dev/null @@ -1,27 +0,0 @@ -unsigned char default_certificate[] = { - 0x30, 0x82, 0x01, 0xd7, 0x30, 0x82, 0x01, 0x40, 0x02, 0x09, 0x00, 0xa6, 0xf3, 0xaa, 0xf6, 0xaf, 0xcd, 0x0b, 0x3a, - 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x05, 0x05, 0x00, 0x30, 0x34, 0x31, 0x32, - 0x30, 0x30, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x0c, 0x29, 0x61, 0x78, 0x54, 0x4c, 0x53, 0x20, 0x50, 0x72, 0x6f, 0x6a, - 0x65, 0x63, 0x74, 0x20, 0x44, 0x6f, 0x64, 0x67, 0x79, 0x20, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, - 0x74, 0x65, 0x20, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x30, 0x1e, 0x17, 0x0d, 0x31, 0x38, 0x30, - 0x34, 0x32, 0x34, 0x30, 0x39, 0x34, 0x38, 0x32, 0x39, 0x5a, 0x17, 0x0d, 0x33, 0x32, 0x30, 0x31, 0x30, 0x31, 0x30, - 0x39, 0x34, 0x38, 0x32, 0x39, 0x5a, 0x30, 0x2c, 0x31, 0x16, 0x30, 0x14, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x0c, 0x0d, - 0x61, 0x78, 0x54, 0x4c, 0x53, 0x20, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x31, 0x12, 0x30, 0x10, 0x06, 0x03, - 0x55, 0x04, 0x03, 0x0c, 0x09, 0x31, 0x32, 0x37, 0x2e, 0x30, 0x2e, 0x30, 0x2e, 0x31, 0x30, 0x81, 0x9f, 0x30, 0x0d, - 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x81, 0x8d, 0x00, 0x30, 0x81, - 0x89, 0x02, 0x81, 0x81, 0x00, 0xa0, 0xbd, 0x8d, 0xf6, 0x21, 0x4b, 0x76, 0x92, 0xa8, 0x13, 0xe8, 0x74, 0x90, 0x79, - 0x05, 0x04, 0xfb, 0xf1, 0x4f, 0x4b, 0x02, 0xc2, 0xd9, 0x6f, 0xc1, 0x82, 0xa7, 0xf2, 0x8b, 0x03, 0x15, 0xe3, 0x56, - 0xce, 0x37, 0x4a, 0x2d, 0xcb, 0x80, 0xd1, 0xc0, 0xa0, 0x84, 0x3d, 0x1b, 0xd9, 0xda, 0x5c, 0xbc, 0x7a, 0x2c, 0x35, - 0xda, 0x9e, 0xe7, 0x56, 0x4b, 0xc5, 0x63, 0xa0, 0x6f, 0xa7, 0x39, 0x0c, 0x72, 0x56, 0x43, 0x34, 0xb0, 0x9a, 0x59, - 0xfb, 0x86, 0xa4, 0x3a, 0xdd, 0xfb, 0xf6, 0xfb, 0x3a, 0x26, 0x69, 0xe8, 0x45, 0x03, 0x3b, 0xde, 0xa9, 0x64, 0x97, - 0x8a, 0x95, 0xf1, 0xfc, 0xd5, 0x77, 0x2d, 0x49, 0x4c, 0x72, 0xf5, 0xcf, 0xcc, 0xb8, 0x54, 0x87, 0x40, 0x4a, 0x6f, - 0x0f, 0x40, 0xd1, 0xba, 0xf1, 0x8c, 0x5b, 0xf9, 0x5d, 0x94, 0xa6, 0xd4, 0xe9, 0x98, 0x38, 0x91, 0x77, 0x91, 0x9b, - 0x02, 0x03, 0x01, 0x00, 0x01, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x05, 0x05, - 0x00, 0x03, 0x81, 0x81, 0x00, 0x19, 0xe5, 0x9f, 0x6f, 0x26, 0x0a, 0x4b, 0xb4, 0xc2, 0x70, 0x5b, 0x2a, 0x31, 0xe6, - 0x0f, 0x37, 0x30, 0x1c, 0x9b, 0xaf, 0x0a, 0xe6, 0x12, 0xf8, 0xde, 0x37, 0x2b, 0xd3, 0xb0, 0x56, 0x2e, 0xeb, 0x95, - 0xdb, 0xd3, 0x2f, 0x70, 0xd9, 0xa5, 0x4e, 0x01, 0xd4, 0x2a, 0xb1, 0xa1, 0x79, 0x71, 0xb5, 0xe8, 0xf1, 0x0d, 0xb0, - 0x95, 0x98, 0xb5, 0xc8, 0x7d, 0x1d, 0x10, 0x64, 0x54, 0x16, 0x9f, 0x1e, 0x92, 0xc6, 0x26, 0xe6, 0xc5, 0xa7, 0xb0, - 0x1d, 0x3c, 0xd5, 0x92, 0xbf, 0xf3, 0xcf, 0xfe, 0x67, 0x03, 0x9d, 0x57, 0x76, 0x9a, 0xa2, 0x4e, 0x55, 0xad, 0xc2, - 0xcb, 0xbe, 0x7e, 0x83, 0xb1, 0xd8, 0x02, 0x59, 0x49, 0x7b, 0x48, 0x70, 0xec, 0xe1, 0x17, 0x76, 0x8e, 0x00, 0x3e, - 0x28, 0xd6, 0x79, 0x84, 0x65, 0x7e, 0x77, 0x8b, 0x0b, 0x3a, 0x19, 0xbb, 0x25, 0xec, 0x0a, 0xdd, 0x9a, 0xfe, 0x96}; -unsigned int default_certificate_len = 475; diff --git a/samples/SmtpClient/include/ssl/private_key.h b/samples/SmtpClient/include/ssl/private_key.h deleted file mode 100644 index e4a0f9130a..0000000000 --- a/samples/SmtpClient/include/ssl/private_key.h +++ /dev/null @@ -1,34 +0,0 @@ -unsigned char default_private_key[] = { - 0x30, 0x82, 0x02, 0x5c, 0x02, 0x01, 0x00, 0x02, 0x81, 0x81, 0x00, 0xa0, 0xbd, 0x8d, 0xf6, 0x21, 0x4b, 0x76, 0x92, - 0xa8, 0x13, 0xe8, 0x74, 0x90, 0x79, 0x05, 0x04, 0xfb, 0xf1, 0x4f, 0x4b, 0x02, 0xc2, 0xd9, 0x6f, 0xc1, 0x82, 0xa7, - 0xf2, 0x8b, 0x03, 0x15, 0xe3, 0x56, 0xce, 0x37, 0x4a, 0x2d, 0xcb, 0x80, 0xd1, 0xc0, 0xa0, 0x84, 0x3d, 0x1b, 0xd9, - 0xda, 0x5c, 0xbc, 0x7a, 0x2c, 0x35, 0xda, 0x9e, 0xe7, 0x56, 0x4b, 0xc5, 0x63, 0xa0, 0x6f, 0xa7, 0x39, 0x0c, 0x72, - 0x56, 0x43, 0x34, 0xb0, 0x9a, 0x59, 0xfb, 0x86, 0xa4, 0x3a, 0xdd, 0xfb, 0xf6, 0xfb, 0x3a, 0x26, 0x69, 0xe8, 0x45, - 0x03, 0x3b, 0xde, 0xa9, 0x64, 0x97, 0x8a, 0x95, 0xf1, 0xfc, 0xd5, 0x77, 0x2d, 0x49, 0x4c, 0x72, 0xf5, 0xcf, 0xcc, - 0xb8, 0x54, 0x87, 0x40, 0x4a, 0x6f, 0x0f, 0x40, 0xd1, 0xba, 0xf1, 0x8c, 0x5b, 0xf9, 0x5d, 0x94, 0xa6, 0xd4, 0xe9, - 0x98, 0x38, 0x91, 0x77, 0x91, 0x9b, 0x02, 0x03, 0x01, 0x00, 0x01, 0x02, 0x81, 0x80, 0x0d, 0x1f, 0xcd, 0x02, 0x86, - 0xaf, 0x69, 0xac, 0x09, 0xcb, 0x2e, 0x54, 0xae, 0x23, 0x23, 0x74, 0xc7, 0xb9, 0x69, 0x36, 0xff, 0xaf, 0xb7, 0x1f, - 0x37, 0xd6, 0x9a, 0x2d, 0xe4, 0x89, 0xc8, 0xf4, 0xb9, 0xf6, 0xb6, 0x6e, 0xf9, 0x14, 0x3f, 0x9d, 0x60, 0xb3, 0xfa, - 0x78, 0x1e, 0xd9, 0x07, 0xca, 0x40, 0x9d, 0x5d, 0x14, 0xbc, 0x97, 0xf2, 0xdd, 0x89, 0xec, 0x40, 0xf9, 0x2d, 0x84, - 0xa2, 0xd4, 0xaf, 0x29, 0x42, 0x13, 0x74, 0xa7, 0x8e, 0xe5, 0xe8, 0xec, 0xbc, 0xde, 0x64, 0xd9, 0xf5, 0x84, 0x9e, - 0x1d, 0x19, 0x4c, 0xf6, 0xdc, 0xb3, 0x7d, 0x66, 0x49, 0xef, 0x45, 0x57, 0x49, 0xed, 0x4d, 0x4a, 0xe3, 0xec, 0xea, - 0x5b, 0xe4, 0xdf, 0x0d, 0x72, 0x3a, 0xd9, 0xbd, 0x9e, 0x37, 0x28, 0x9f, 0x5f, 0xa4, 0x74, 0xdd, 0xdb, 0xeb, 0x65, - 0xfa, 0x42, 0x36, 0x21, 0xd3, 0x3f, 0x47, 0x68, 0xc9, 0x02, 0x41, 0x00, 0xd5, 0xcf, 0xdb, 0xed, 0xa4, 0x73, 0x47, - 0xf9, 0x39, 0x40, 0xaa, 0x7d, 0xc3, 0x18, 0x5d, 0x7b, 0x3b, 0x73, 0x48, 0x5b, 0x64, 0x5f, 0xe9, 0xbb, 0x02, 0x83, - 0xe3, 0xd8, 0xe7, 0x85, 0x70, 0x26, 0x0c, 0x92, 0x57, 0xe9, 0x13, 0xdf, 0xae, 0x5d, 0x67, 0x5b, 0xdf, 0x0c, 0x64, - 0x61, 0x8b, 0x9f, 0x34, 0xc7, 0x85, 0xc5, 0x55, 0x1b, 0xed, 0xd1, 0xd2, 0xfc, 0xbd, 0xb0, 0x1f, 0xd6, 0x31, 0xdf, - 0x02, 0x41, 0x00, 0xc0, 0x74, 0xee, 0x0f, 0x90, 0x6f, 0x0b, 0x9f, 0xa9, 0x5b, 0x70, 0xd7, 0x8d, 0x63, 0x80, 0x95, - 0x2b, 0x77, 0xa2, 0x28, 0x79, 0xfd, 0x49, 0x14, 0x61, 0x3f, 0x5a, 0x09, 0x08, 0xc7, 0x74, 0x47, 0xe2, 0x9b, 0x1e, - 0xe9, 0x9f, 0x61, 0x1f, 0x70, 0xba, 0x89, 0xf6, 0xc5, 0xc5, 0x2c, 0xed, 0x19, 0x40, 0x46, 0x9c, 0x51, 0x3b, 0xda, - 0x9b, 0x20, 0x96, 0x8c, 0xd5, 0x7d, 0xd1, 0x6c, 0xef, 0xc5, 0x02, 0x40, 0x16, 0x28, 0x9c, 0x9a, 0x5c, 0x58, 0xb6, - 0x34, 0xd6, 0x02, 0x25, 0xa9, 0x32, 0xf6, 0xeb, 0x79, 0x42, 0x08, 0x08, 0x8f, 0xb0, 0x2f, 0x60, 0x81, 0xc9, 0x18, - 0xf2, 0x1c, 0x20, 0xa2, 0x6b, 0xa5, 0x05, 0xd8, 0x84, 0xd3, 0xdb, 0x03, 0x6b, 0x86, 0xb2, 0x97, 0x8a, 0xde, 0x35, - 0xe9, 0x06, 0x17, 0x51, 0xd8, 0xfb, 0xbc, 0x1f, 0xbd, 0xed, 0x3f, 0xb9, 0xa6, 0x07, 0xe2, 0xa0, 0xea, 0x09, 0xf1, - 0x02, 0x40, 0x68, 0xec, 0xd7, 0x05, 0x61, 0x47, 0x49, 0x5d, 0x08, 0xa6, 0x33, 0xc5, 0x30, 0xee, 0x78, 0xa1, 0xdb, - 0x0a, 0xe4, 0x3b, 0x91, 0x16, 0x88, 0x0b, 0x36, 0x61, 0xa5, 0xa2, 0x9b, 0x48, 0xb2, 0x9a, 0xa6, 0x6e, 0xcf, 0xd1, - 0xaa, 0xf4, 0xf6, 0x81, 0x2d, 0x12, 0x1e, 0x9a, 0x00, 0x3f, 0xd8, 0x1c, 0x16, 0x30, 0xe8, 0xf4, 0x58, 0xdf, 0x7c, - 0x07, 0xae, 0x4c, 0xa5, 0xf0, 0x6c, 0x87, 0x29, 0xc9, 0x02, 0x41, 0x00, 0x94, 0x5f, 0x43, 0xe0, 0x51, 0x27, 0xe5, - 0xe9, 0x19, 0x0c, 0x22, 0x45, 0xe9, 0xd3, 0x88, 0xde, 0x15, 0x33, 0x30, 0x31, 0x07, 0xd2, 0x7c, 0xd9, 0x5a, 0xeb, - 0xc0, 0x7c, 0x39, 0x5e, 0xa5, 0x5f, 0x98, 0x13, 0xed, 0xc9, 0x92, 0x1b, 0x50, 0xce, 0x01, 0xf7, 0x4f, 0xd1, 0x36, - 0x66, 0xd1, 0x48, 0xf7, 0xcb, 0x9b, 0x2c, 0x80, 0x48, 0xb0, 0xef, 0x1e, 0xf3, 0xc4, 0xca, 0x1d, 0xd5, 0x41, 0x96}; -unsigned int default_private_key_len = 608; diff --git a/samples/Websocket_Client/include/ssl/cert.h b/samples/Websocket_Client/include/ssl/cert.h deleted file mode 100644 index c16737b162..0000000000 --- a/samples/Websocket_Client/include/ssl/cert.h +++ /dev/null @@ -1,27 +0,0 @@ -unsigned char default_certificate[] = { - 0x30, 0x82, 0x01, 0xd7, 0x30, 0x82, 0x01, 0x40, 0x02, 0x09, 0x00, 0xe1, 0x1b, 0x5e, 0x9f, 0xbd, 0x6a, 0x8f, 0xd9, - 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x05, 0x05, 0x00, 0x30, 0x34, 0x31, 0x32, - 0x30, 0x30, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x13, 0x29, 0x61, 0x78, 0x54, 0x4c, 0x53, 0x20, 0x50, 0x72, 0x6f, 0x6a, - 0x65, 0x63, 0x74, 0x20, 0x44, 0x6f, 0x64, 0x67, 0x79, 0x20, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, - 0x74, 0x65, 0x20, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x30, 0x1e, 0x17, 0x0d, 0x31, 0x36, 0x31, - 0x31, 0x32, 0x38, 0x31, 0x35, 0x32, 0x30, 0x33, 0x39, 0x5a, 0x17, 0x0d, 0x33, 0x30, 0x30, 0x38, 0x30, 0x37, 0x31, - 0x35, 0x32, 0x30, 0x33, 0x39, 0x5a, 0x30, 0x2c, 0x31, 0x16, 0x30, 0x14, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x13, 0x0d, - 0x61, 0x78, 0x54, 0x4c, 0x53, 0x20, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x31, 0x12, 0x30, 0x10, 0x06, 0x03, - 0x55, 0x04, 0x03, 0x13, 0x09, 0x31, 0x32, 0x37, 0x2e, 0x30, 0x2e, 0x30, 0x2e, 0x31, 0x30, 0x81, 0x9f, 0x30, 0x0d, - 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x81, 0x8d, 0x00, 0x30, 0x81, - 0x89, 0x02, 0x81, 0x81, 0x00, 0xda, 0x2c, 0x2b, 0xed, 0x4a, 0x24, 0xa5, 0x02, 0x6a, 0x39, 0x31, 0x4e, 0xf2, 0x0d, - 0x07, 0x6b, 0x4b, 0xc2, 0xa9, 0x95, 0x06, 0xa3, 0x6e, 0xe1, 0x15, 0x7c, 0x12, 0xcc, 0xe4, 0xf7, 0xa7, 0xf8, 0x09, - 0x7f, 0x50, 0xa5, 0x97, 0xf1, 0x6f, 0x8f, 0x80, 0xa3, 0x1e, 0x88, 0x9f, 0x8e, 0xbd, 0x3b, 0x33, 0x5c, 0xfc, 0x2f, - 0xb3, 0x3e, 0x97, 0xf5, 0xbe, 0xe5, 0xe6, 0x23, 0xf1, 0x16, 0xf3, 0xc4, 0xc9, 0xa4, 0xa4, 0xf9, 0xc0, 0x96, 0xdc, - 0x11, 0x4e, 0x8a, 0xf9, 0x83, 0xde, 0xa2, 0x67, 0xe5, 0xa2, 0x07, 0x63, 0x67, 0x08, 0xc3, 0x4f, 0x5f, 0xac, 0xc2, - 0x8e, 0xc0, 0x9a, 0x17, 0x19, 0xc4, 0xa7, 0x82, 0x22, 0xa6, 0x36, 0x34, 0x38, 0x18, 0x0c, 0x1a, 0x86, 0x6c, 0x70, - 0x1d, 0x70, 0x9c, 0x71, 0x12, 0xec, 0xcb, 0x15, 0xda, 0x97, 0xfe, 0xe5, 0x54, 0xf2, 0x98, 0xfa, 0x28, 0x2d, 0x97, - 0x02, 0x03, 0x01, 0x00, 0x01, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x05, 0x05, - 0x00, 0x03, 0x81, 0x81, 0x00, 0x2c, 0x35, 0xcb, 0x03, 0x38, 0xa9, 0x25, 0x40, 0x20, 0x9c, 0x4a, 0x7a, 0x15, 0x1a, - 0x4e, 0x1f, 0xec, 0x34, 0xd3, 0x43, 0x76, 0xb8, 0x90, 0xcf, 0x9f, 0x3b, 0x1c, 0x6d, 0x15, 0xfd, 0xd3, 0x21, 0xfe, - 0x1d, 0x07, 0x84, 0x1f, 0x8a, 0x0e, 0xcf, 0x93, 0x19, 0xdc, 0x16, 0xcf, 0x1e, 0xac, 0x9c, 0xf6, 0x37, 0x97, 0xfb, - 0x7d, 0xef, 0x2b, 0xfa, 0x7c, 0x94, 0x59, 0x4b, 0x6c, 0x04, 0xad, 0xe7, 0x99, 0xfa, 0x0a, 0x25, 0x8b, 0xcd, 0x8e, - 0x0e, 0x9f, 0x63, 0x80, 0x71, 0x4f, 0xf5, 0x5e, 0x0f, 0xf4, 0xdb, 0xe2, 0x17, 0x13, 0x48, 0x68, 0x1b, 0xdd, 0x85, - 0xd8, 0xba, 0xe2, 0xbb, 0xfc, 0x07, 0xf0, 0x3a, 0x29, 0xd1, 0x71, 0xf8, 0xe3, 0x69, 0x4f, 0xb6, 0xc2, 0x26, 0xfc, - 0x94, 0x67, 0xfb, 0xbb, 0x7e, 0x30, 0x8a, 0x55, 0x28, 0x09, 0x35, 0xf1, 0x37, 0x62, 0x19, 0x42, 0x0e, 0x98, 0x55}; -unsigned int default_certificate_len = 475; diff --git a/samples/Websocket_Client/include/ssl/private_key.h b/samples/Websocket_Client/include/ssl/private_key.h deleted file mode 100644 index eb0d88821b..0000000000 --- a/samples/Websocket_Client/include/ssl/private_key.h +++ /dev/null @@ -1,35 +0,0 @@ -unsigned char default_private_key[] = { - 0x30, 0x82, 0x02, 0x5e, 0x02, 0x01, 0x00, 0x02, 0x81, 0x81, 0x00, 0xda, 0x2c, 0x2b, 0xed, 0x4a, 0x24, 0xa5, 0x02, - 0x6a, 0x39, 0x31, 0x4e, 0xf2, 0x0d, 0x07, 0x6b, 0x4b, 0xc2, 0xa9, 0x95, 0x06, 0xa3, 0x6e, 0xe1, 0x15, 0x7c, 0x12, - 0xcc, 0xe4, 0xf7, 0xa7, 0xf8, 0x09, 0x7f, 0x50, 0xa5, 0x97, 0xf1, 0x6f, 0x8f, 0x80, 0xa3, 0x1e, 0x88, 0x9f, 0x8e, - 0xbd, 0x3b, 0x33, 0x5c, 0xfc, 0x2f, 0xb3, 0x3e, 0x97, 0xf5, 0xbe, 0xe5, 0xe6, 0x23, 0xf1, 0x16, 0xf3, 0xc4, 0xc9, - 0xa4, 0xa4, 0xf9, 0xc0, 0x96, 0xdc, 0x11, 0x4e, 0x8a, 0xf9, 0x83, 0xde, 0xa2, 0x67, 0xe5, 0xa2, 0x07, 0x63, 0x67, - 0x08, 0xc3, 0x4f, 0x5f, 0xac, 0xc2, 0x8e, 0xc0, 0x9a, 0x17, 0x19, 0xc4, 0xa7, 0x82, 0x22, 0xa6, 0x36, 0x34, 0x38, - 0x18, 0x0c, 0x1a, 0x86, 0x6c, 0x70, 0x1d, 0x70, 0x9c, 0x71, 0x12, 0xec, 0xcb, 0x15, 0xda, 0x97, 0xfe, 0xe5, 0x54, - 0xf2, 0x98, 0xfa, 0x28, 0x2d, 0x97, 0x02, 0x03, 0x01, 0x00, 0x01, 0x02, 0x81, 0x81, 0x00, 0xd2, 0xbf, 0xb6, 0x93, - 0x1a, 0x7a, 0xf9, 0x76, 0xb2, 0xbb, 0x9a, 0x99, 0x03, 0x12, 0x78, 0xe7, 0x39, 0xa0, 0xca, 0x05, 0xae, 0x0a, 0xf3, - 0xd1, 0xb3, 0xda, 0x4d, 0xa2, 0xe5, 0x4f, 0x22, 0x4f, 0x64, 0x85, 0x3a, 0x97, 0x2b, 0x86, 0x4a, 0xd8, 0xd9, 0x4f, - 0x38, 0xf0, 0x8b, 0x08, 0xec, 0x5c, 0xa2, 0x8a, 0x21, 0x05, 0xc6, 0xe5, 0x21, 0x0f, 0x7f, 0x1f, 0x3f, 0x47, 0xda, - 0xdc, 0xec, 0x0d, 0x94, 0x90, 0xe4, 0xab, 0x1d, 0x9a, 0x8f, 0x2c, 0x72, 0x4b, 0xd0, 0xba, 0xf9, 0x16, 0xd3, 0x57, - 0x9c, 0xbe, 0x2c, 0x64, 0xbf, 0xbd, 0x4a, 0xdf, 0x31, 0x0e, 0x37, 0x1a, 0x0d, 0xf7, 0x6c, 0x87, 0x9a, 0x36, 0xb6, - 0x39, 0x7b, 0x1c, 0x53, 0x8e, 0xb8, 0xb3, 0x6a, 0xb1, 0x26, 0x16, 0x36, 0xc1, 0xa2, 0xc6, 0x2d, 0x5d, 0x3d, 0x13, - 0xa7, 0x40, 0x80, 0x83, 0x14, 0x2e, 0x86, 0x04, 0x24, 0xf1, 0x02, 0x41, 0x00, 0xed, 0x1e, 0x2f, 0x12, 0x72, 0x6b, - 0xe2, 0xb1, 0xfb, 0x1e, 0xe2, 0x33, 0xbc, 0xd7, 0xf0, 0xdc, 0x54, 0xc5, 0x60, 0xc9, 0x3d, 0x9e, 0x57, 0x7f, 0x52, - 0xed, 0x16, 0x32, 0x7b, 0x47, 0x43, 0x58, 0xb8, 0x09, 0xef, 0xb1, 0x4c, 0xfd, 0xfd, 0x44, 0x66, 0xe0, 0x2c, 0x30, - 0x56, 0x91, 0xd1, 0xe1, 0xed, 0x48, 0xb8, 0xb4, 0xaa, 0x62, 0x02, 0xb0, 0xb2, 0xbf, 0x64, 0x9c, 0xfc, 0x75, 0x04, - 0xd9, 0x02, 0x41, 0x00, 0xeb, 0x8b, 0xc5, 0xed, 0x0d, 0xf4, 0x2f, 0x8d, 0x0e, 0x1e, 0x66, 0xd7, 0xd0, 0x5b, 0xa0, - 0x89, 0x33, 0xce, 0x9f, 0xcf, 0x91, 0x66, 0x22, 0xad, 0x8a, 0x80, 0xd4, 0x6c, 0xc3, 0x4c, 0x8d, 0xd5, 0xe6, 0x5c, - 0x95, 0x0e, 0xe4, 0x15, 0xc8, 0xa6, 0x75, 0x87, 0xea, 0x2d, 0xdc, 0xcd, 0x5d, 0x53, 0x31, 0xa4, 0x1e, 0x57, 0xab, - 0x27, 0x2c, 0x21, 0x46, 0xb0, 0x03, 0xad, 0x22, 0xe9, 0x7f, 0xef, 0x02, 0x41, 0x00, 0xb8, 0xa0, 0x78, 0xfc, 0x7f, - 0x15, 0x5b, 0xf5, 0x43, 0x58, 0x1f, 0xbf, 0x33, 0x3a, 0x5c, 0xb3, 0xe2, 0x59, 0xb1, 0x6b, 0xe0, 0x4b, 0xab, 0x4b, - 0x5b, 0x71, 0x79, 0x88, 0x23, 0x0f, 0x30, 0xf4, 0x22, 0x90, 0xb2, 0x0e, 0xb6, 0xa9, 0x49, 0x8b, 0xfa, 0x22, 0x70, - 0xa5, 0xce, 0xb2, 0x49, 0xdf, 0x05, 0x98, 0x4b, 0x21, 0x79, 0x4d, 0x49, 0x54, 0xf6, 0x49, 0x2a, 0x79, 0x45, 0xe5, - 0x83, 0xb9, 0x02, 0x41, 0x00, 0x88, 0x37, 0x50, 0xc3, 0x02, 0x6a, 0xd0, 0x84, 0xf6, 0x41, 0x46, 0xa0, 0x4f, 0xf2, - 0x6d, 0x28, 0x6b, 0x39, 0x76, 0xda, 0x06, 0xef, 0xd5, 0xe6, 0x1e, 0x4e, 0xda, 0x89, 0xfb, 0x77, 0x6e, 0x1e, 0xe1, - 0x15, 0x71, 0x6e, 0x27, 0x21, 0x21, 0xe4, 0x81, 0xdb, 0x93, 0xe5, 0xe9, 0xe7, 0x29, 0xad, 0x4e, 0xeb, 0xe6, 0x50, - 0x34, 0xbe, 0x76, 0x9a, 0xd7, 0xd2, 0x3a, 0x8e, 0x09, 0xbe, 0x97, 0x29, 0x02, 0x40, 0x4a, 0x3c, 0x78, 0x22, 0x42, - 0x5b, 0xce, 0x60, 0xa0, 0x48, 0x70, 0x16, 0x7c, 0x92, 0xc2, 0xe1, 0xe3, 0x24, 0xed, 0x58, 0xce, 0xea, 0x91, 0x80, - 0x2d, 0x7c, 0x59, 0xcb, 0xb7, 0x47, 0xa7, 0xc5, 0xab, 0xc6, 0xf6, 0x25, 0x35, 0xd9, 0xbc, 0xd2, 0x11, 0x6e, 0xc2, - 0xfc, 0x97, 0xa9, 0x03, 0xba, 0x5c, 0x8e, 0x92, 0xff, 0x2c, 0xab, 0xab, 0x95, 0x02, 0x1a, 0x9a, 0xd3, 0xb0, 0x4c, - 0x79, 0xfd}; -unsigned int default_private_key_len = 610; diff --git a/tests/HostTests/modules/DateTime.cpp b/tests/HostTests/modules/DateTime.cpp index 5f262a8c0d..544789b0c9 100644 --- a/tests/HostTests/modules/DateTime.cpp +++ b/tests/HostTests/modules/DateTime.cpp @@ -90,6 +90,34 @@ class DateTimeTest : public TestGroup << DateTime::getLocaleMonthName(month) << endl; } } + + TEST_CASE("time() sync") + { + auto curTime = SystemClock.now(eTZ_UTC); + DateTime dt; + dt.fromISO8601(F("2024-01-01T13:57Z")); + time_t newTime = dt; + Serial << _F("curTime ") << curTime << _F(", newTime ") << newTime << _F(" ...") << endl; + SystemClock.setTime(newTime, eTZ_UTC); + auto timer = new AutoDeleteTimer; + const auto delay = 2000; + timer->initializeMs([newTime, this]() { + auto sysClockTime = SystemClock.now(eTZ_UTC); + auto ctime = ::time(nullptr); + auto diff = sysClockTime - ctime; + auto timeDelay = sysClockTime - newTime; + Serial << _F("sysClockTime ") << sysClockTime << _F(", delay ") << timeDelay << endl; + Serial << _F("time() ") << ctime << _F(", diff ") << diff << endl; + REQUIRE(abs(1000 * timeDelay - delay) <= 1000); +#ifndef ARCH_HOST + // Can't check time() on host + REQUIRE(abs(diff) < 2); +#endif + complete(); + }); + timer->startOnce(); + pending(); + } } void checkHttpDates(const FSTR::Array& dates) From 52ee9d0129a1bb434bde7b2df0cc2865c0005163 Mon Sep 17 00:00:00 2001 From: Mike Date: Sat, 8 Jun 2024 13:48:29 +0100 Subject: [PATCH 067/128] Update lwip2, use upstream repo. (#2793) This PR at long last brings lwip2 out of the dark ages. Seems to work reliably, here it is for testing anyway. Instead of using a fork this uses the lwip2 repo used by esp8266 arduino, with some minimal patching. lwip2 now updated from 2.0.2 to 2.1.3. --- .gitmodules | 2 +- .../Components/esp8266/include/esp_wifi.h | 18 +-- .../Components/esp8266/include/ipv4_addr.h | 76 ++++++++++ .../Esp8266/Components/lwip2/Makefile.sming | 50 +++++++ .../Arch/Esp8266/Components/lwip2/README.rst | 34 ++++- .../Esp8266/Components/lwip2/component.mk | 62 ++++++-- Sming/Arch/Esp8266/Components/lwip2/lwip2 | 2 +- .../Arch/Esp8266/Components/lwip2/lwip2.patch | 134 ++++++++++++++++++ Sming/Arch/Esp8266/Platform/RTC.cpp | 28 +++- 9 files changed, 379 insertions(+), 27 deletions(-) create mode 100644 Sming/Arch/Esp8266/Components/esp8266/include/ipv4_addr.h create mode 100644 Sming/Arch/Esp8266/Components/lwip2/Makefile.sming create mode 100644 Sming/Arch/Esp8266/Components/lwip2/lwip2.patch diff --git a/.gitmodules b/.gitmodules index 67d22b225b..4205c4fd6d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -97,7 +97,7 @@ ignore = dirty [submodule "Esp8266.lwip2"] path = Sming/Arch/Esp8266/Components/lwip2/lwip2 - url = https://github.com/mikee47/esp82xx-nonos-linklayer.git + url = https://github.com/d-a-v/esp82xx-nonos-linklayer.git ignore = dirty [submodule "Esp8266.umm_malloc"] path = Sming/Arch/Esp8266/Components/heap/umm_malloc diff --git a/Sming/Arch/Esp8266/Components/esp8266/include/esp_wifi.h b/Sming/Arch/Esp8266/Components/esp8266/include/esp_wifi.h index 093eeb9c07..8d59b7b293 100644 --- a/Sming/Arch/Esp8266/Components/esp8266/include/esp_wifi.h +++ b/Sming/Arch/Esp8266/Components/esp8266/include/esp_wifi.h @@ -29,7 +29,7 @@ #pragma once #include "os_type.h" -#include "lwip/ip_addr.h" +#include "ipv4_addr.h" #include "../sdk/include/queue.h" #include "gpio.h" @@ -227,13 +227,13 @@ struct station_info { STAILQ_ENTRY(station_info) next; uint8_t bssid[6]; - struct ip_addr ip; + struct ipv4_addr ip; }; struct dhcps_lease { bool enable; - struct ip_addr start_ip; - struct ip_addr end_ip; + struct ipv4_addr start_ip; + struct ipv4_addr end_ip; }; enum dhcps_offer_option { @@ -244,7 +244,7 @@ enum dhcps_offer_option { uint8_t wifi_softap_get_station_num(void); struct station_info* wifi_softap_get_station_info(void); -bool wifi_softap_set_station_info (uint8_t* mac, struct ip_addr*); +bool wifi_softap_set_station_info (uint8_t* mac, struct ipv4_addr*); void wifi_softap_free_station_info(void); bool wifi_softap_dhcps_start(void); @@ -375,9 +375,9 @@ typedef struct { } Event_StaMode_AuthMode_Change_t; typedef struct { - struct ip_addr ip; - struct ip_addr mask; - struct ip_addr gw; + struct ipv4_addr ip; + struct ipv4_addr mask; + struct ipv4_addr gw; } Event_StaMode_Got_IP_t; typedef struct { @@ -387,7 +387,7 @@ typedef struct { typedef struct { uint8_t mac[6]; - struct ip_addr ip; + struct ipv4_addr ip; uint8_t aid; } Event_SoftAPMode_Distribute_Sta_IP_t; diff --git a/Sming/Arch/Esp8266/Components/esp8266/include/ipv4_addr.h b/Sming/Arch/Esp8266/Components/esp8266/include/ipv4_addr.h new file mode 100644 index 0000000000..aea46425b6 --- /dev/null +++ b/Sming/Arch/Esp8266/Components/esp8266/include/ipv4_addr.h @@ -0,0 +1,76 @@ +/* + * ESPRESSIF MIT License + * + * Copyright (c) 2016 + * + * Permission is hereby granted for use on ESPRESSIF SYSTEMS ESP8266 only, in which case, + * it is free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software is furnished + * to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ + +#ifndef __IPV4_ADDR_H__ +#define __IPV4_ADDR_H__ + +struct ip_info; + +#include + +#ifdef LWIP14GLUE + +#define ipv4_addr ip_addr + +#else + +#ifdef __LWIP_IP_ADDR_H__ + +#define ipv4_addr ip_addr + +#else + +#include + +// ipv4_addr is necessary for lwIP-v2 because +// - espressif binary firmware is IPv4 only, under the name of ip_addr/_t +// - ip_addr/_t is different when IPv6 is enabled with lwIP-v2 +// hence ipv4_addr/t is IPv4 version/copy of IPv4 ip_addr/_t +// when IPv6 is enabled so we can deal with IPv4 use from firmware API. + +#define ipv4_addr ip4_addr +#define ipv4_addr_t ip4_addr_t + +// official lwIP's definitions +#if LWIP_VERSION_MAJOR == 1 +struct ip4_addr { uint32_t addr; }; +typedef struct ip4_addr ip4_addr_t; +#else + +#include + +// defined in lwip-v1.4 sources only, used in fw +struct ip_info { + struct ipv4_addr ip; + struct ipv4_addr netmask; + struct ipv4_addr gw; +}; + +#endif + +#endif // __LWIP_IP_ADDR_H__ + +#endif // LWIP14GLUE + +#endif // __IPV4_ADDR_H__ diff --git a/Sming/Arch/Esp8266/Components/lwip2/Makefile.sming b/Sming/Arch/Esp8266/Components/lwip2/Makefile.sming new file mode 100644 index 0000000000..75e0453f2f --- /dev/null +++ b/Sming/Arch/Esp8266/Components/lwip2/Makefile.sming @@ -0,0 +1,50 @@ +# Modified version of lwip2/makefiles/Makefile.build-lwip2 +# Make from ./lwip2 directory + +include makefiles/Makefile.defs + +export LWIP_ESP=glue-esp/lwip-1.4-arduino/include +export LWIP_LIB=$(BUILD)/liblwip2.a +export target=arduino +export BUILD +export TCP_MSS +export LWIP_FEATURES +export LWIP_IPV6 +export V +export DEFINE_TARGET=ARDUINO +export LWIP_INCLUDES_RELEASE=include + +AUTO := \ + glue-lwip/lwip-err-t.h \ + glue-lwip/lwip-git-hash.h + +all: patch $(LWIP_LIB_RELEASE) + +.PHONY: upstream +upstream: lwip2-src/README + +.PHONY: patch +patch: upstream $(AUTO) + $(Q) $(MAKE) --no-print-directory -C lwip2-src/src -f ../../makefiles/Makefile.patches + +lwip2-src/README: + $(Q) git clone --depth=1 -b $(UPSTREAM_VERSION) https://github.com/lwip-tcpip/lwip lwip2-src + +glue-lwip/lwip-err-t.h: $(LWIP_ESP)/arch/cc.h upstream + $(Q) ( \ + echo "// script-generated, extracted from espressif SDK's lwIP arch/cc.h"; \ + echo "#define LWIP_NO_STDINT_H 1"; \ + echo "typedef signed short sint16_t;"; \ + grep -e LWIP_ERR_T -e ^typedef $< \ + ) > $@ + +glue-lwip/lwip-git-hash.h: upstream + makefiles/make-lwip-hash + +$(LWIP_LIB_RELEASE): $(LWIP_LIB) + @cp $< $@ + +$(LWIP_LIB): + $(Q) $(MAKE) -f makefiles/Makefile.glue-esp + $(Q) $(MAKE) -f makefiles/Makefile.glue + $(Q) $(MAKE) -C lwip2-src/src -f ../../makefiles/Makefile.lwip2 diff --git a/Sming/Arch/Esp8266/Components/lwip2/README.rst b/Sming/Arch/Esp8266/Components/lwip2/README.rst index 9432be959c..c1a5d22606 100644 --- a/Sming/Arch/Esp8266/Components/lwip2/README.rst +++ b/Sming/Arch/Esp8266/Components/lwip2/README.rst @@ -1,4 +1,36 @@ Esp8266 LWIP Version 2 ====================== -This Component implements the current Version 2 LWIP stack. Note that at present espconn\_* functions are not supported. +This Component implements the current Version 2 LWIP stack. +Note that at present espconn\_* functions are not supported. + + +.. envvar:: TCP_MSS + + Maximum TCP segment size. Default 1460. + + +.. envvar:: LWIP_IPV6 + + default: 0 (disabled) + + Set to enable IPv6 support. + + +.. envvar:: LWIP_FEATURES + + If anyone knows of an actual reference for this setting, link here please! + + Looking at glue-lwip/arduino/lwipopts.h, setting ``LWIP_FEATURES`` to 1 enables these LWIP flags: + + - IP_FORWARD + - IP_REASSEMBLY + - IP_FRAG + - IP_NAPT (IPV4 only) + - LWIP_AUTOIP + - LWIP_DHCP_AUTOIP_COOP + - LWIP_TCP_SACK_OUT + - TCP_LISTEN_BACKLOG + - SNTP_MAX_SERVERS = 3 + + Also DHCP discovery gets hooked. diff --git a/Sming/Arch/Esp8266/Components/lwip2/component.mk b/Sming/Arch/Esp8266/Components/lwip2/component.mk index 20fb20b408..301af99494 100644 --- a/Sming/Arch/Esp8266/Components/lwip2/component.mk +++ b/Sming/Arch/Esp8266/Components/lwip2/component.mk @@ -10,33 +10,69 @@ ifeq ($(SMING_RELEASE),1) endif COMPONENT_VARS := ENABLE_LWIPDEBUG ENABLE_ESPCONN -ENABLE_LWIPDEBUG ?= 0 +ifneq ($(ENABLE_LWIPDEBUG),1) +override ENABLE_LWIPDEBUG := 0 +endif ENABLE_ESPCONN ?= 0 EXTRA_CFLAGS_LWIP := \ -I$(SMING_HOME)/System/include \ -I$(ARCH_COMPONENTS)/esp8266/include \ - -I$(ARCH_COMPONENTS)/libc/include + -I$(ARCH_COMPONENTS)/libc/include \ + -DULWIPDEBUG=$(ENABLE_LWIPDEBUG) -ifeq ($(ENABLE_LWIPDEBUG), 1) - EXTRA_CFLAGS_LWIP += -DLWIP_DEBUG -endif -ifeq ($(ENABLE_ESPCONN), 1) -$(error LWIP2 does not support espconn_* functions. Make sure to set ENABLE_CUSTOM_LWIP to 0 or 1.) +COMPONENT_SUBMODULES := lwip2 +COMPONENT_INCDIRS := \ + lwip2/glue-lwip/arduino \ + lwip2/glue-lwip \ + lwip2/glue \ + lwip2/lwip2-src/src/include + +LWIP2_PATH := $(COMPONENT_PATH)/lwip2 +LWIP2_LIBPATH := $(COMPONENT_LIBDIR) + +# +COMPONENT_VARS += TCP_MSS LWIP_IPV6 LWIP_FEATURES +TCP_MSS ?= 1460 +ifneq ($(LWIP_IPV6),1) +override LWIP_IPV6 := 0 +endif +ifneq ($(LWIP_FEATURES),1) +LWIP_FEATURES := 0 endif -COMPONENT_SUBMODULES := lwip2 -COMPONENT_INCDIRS := lwip2/glue-esp/include-esp lwip2/include +LWIP2_HASHVAL := $(foreach v,$(COMPONENT_VARS),$v=$($v)) +LWIP2_LIBHASH := $(call CalculateVariantHash,LWIP2_HASHVAL) +LWIP2_BUILD_DIR := $(COMPONENT_BUILD_BASE)/$(LWIP2_LIBHASH) -LWIP2_PATH := $(COMPONENT_PATH)/lwip2 +GLOBAL_CFLAGS += \ + -DTCP_MSS=$(TCP_MSS) \ + -DLWIP_IPV6=$(LWIP_IPV6) \ + -DLWIP_FEATURES=$(LWIP_FEATURES) \ + -DULWIPDEBUG=$(ENABLE_LWIPDEBUG) # Make is pretty complex for LWIP2, and mucks about with output sections so build as a regular library LWIP2_LIB := $(COMPONENT_NAME) -LWIP2_TARGET := $(COMPONENT_LIBDIR)/lib$(LWIP2_LIB).a +LWIP2_TARGET := $(LWIP2_BUILD_DIR)/lib$(LWIP2_LIB).a COMPONENT_TARGETS := $(LWIP2_TARGET) EXTRA_LIBS := $(LWIP2_LIB) +LIBDIRS += $(LWIP2_BUILD_DIR) + +COMPONENT_PREREQUISITES := $(LWIP2_PATH)/glue-lwip/lwip-err-t.h + +$(LWIP2_PATH)/glue-lwip/lwip-err-t.h: + $(Q) $(MAKE) -C $(LWIP2_PATH) -f ../Makefile.sming patch + $(COMPONENT_RULE)$(LWIP2_TARGET): - $(Q) $(MAKE) -C $(LWIP2_PATH) -f Makefile.sming BUILD=$(COMPONENT_BUILD_DIR) \ - USER_LIBDIR=$(COMPONENT_LIBDIR)/ CFLAGS_EXTRA="$(EXTRA_CFLAGS_LWIP)" CC=$(CC) AR=$(AR) all + $(Q) $(MAKE) --no-print-directory -C $(LWIP2_PATH) -f ../Makefile.sming \ + all \ + CFLAGS_EXTRA="$(EXTRA_CFLAGS_LWIP)" \ + LWIP_LIB_RELEASE=$(LWIP2_TARGET) \ + TOOLS=$(TOOLSPEC) \ + TCP_MSS=$(TCP_MSS) \ + LWIP_FEATURES=$(LWIP_FEATURES) \ + LWIP_IPV6=$(LWIP_IPV6) \ + BUILD=$(LWIP2_BUILD_DIR)/build \ + Q=$(Q) V=$(if $V,1,0) diff --git a/Sming/Arch/Esp8266/Components/lwip2/lwip2 b/Sming/Arch/Esp8266/Components/lwip2/lwip2 index 2e85a7ad86..4087efd9d2 160000 --- a/Sming/Arch/Esp8266/Components/lwip2/lwip2 +++ b/Sming/Arch/Esp8266/Components/lwip2/lwip2 @@ -1 +1 @@ -Subproject commit 2e85a7ad86b1af23d4b092bd5484406a644a6bcf +Subproject commit 4087efd9d2a8e1cee9a159e0796d831dc1e0c497 diff --git a/Sming/Arch/Esp8266/Components/lwip2/lwip2.patch b/Sming/Arch/Esp8266/Components/lwip2/lwip2.patch new file mode 100644 index 0000000000..ba551c5af1 --- /dev/null +++ b/Sming/Arch/Esp8266/Components/lwip2/lwip2.patch @@ -0,0 +1,134 @@ +diff --git a/glue-lwip/arch/cc.h b/glue-lwip/arch/cc.h +index 0b73cba..fff12b0 100644 +--- a/glue-lwip/arch/cc.h ++++ b/glue-lwip/arch/cc.h +@@ -56,8 +56,6 @@ void sntp_set_system_time (uint32_t t); + #endif + #endif // defined(LWIP_BUILD) + +-#include "mem.h" // useful for os_malloc used in esp-arduino's mDNS +- + #include "glue.h" // include assembly locking macro used below + typedef uint32_t sys_prot_t; + #define SYS_ARCH_DECL_PROTECT(lev) sys_prot_t lev +diff --git a/glue-lwip/esp-dhcpserver.c b/glue-lwip/esp-dhcpserver.c +index 6ac8a41..cd1faf0 100644 +--- a/glue-lwip/esp-dhcpserver.c ++++ b/glue-lwip/esp-dhcpserver.c +@@ -12,7 +12,7 @@ + #include "lwip/apps-esp/dhcpserver.h" + + #include "user_interface.h" +-#include "mem.h" ++#include + + #include "glue.h" + #include "lwip-helper.h" +diff --git a/glue/esp-missing.h b/glue/esp-missing.h +index b9fea2a..63bbb45 100644 +--- a/glue/esp-missing.h ++++ b/glue/esp-missing.h +@@ -12,18 +12,15 @@ uint32_t r_rand (void); + + // TODO: Patch these in from SDK + +-#if ARDUINO +-void* pvPortZalloc (size_t, const char*, int); +-void* pvPortMalloc (size_t xWantedSize, const char* file, int line) __attribute__((malloc, alloc_size(1))); +-void vPortFree (void *ptr, const char* file, int line); +-#else +-void *pvPortZalloc (size_t sz, const char *, unsigned); +-void *pvPortMalloc (size_t sz, const char *, unsigned) __attribute__((malloc, alloc_size(1))); +-void vPortFree (void *p, const char *, unsigned); +-#endif ++#include ++ ++#define ets_post system_os_post ++#define ets_task system_os_task + + struct netif* eagle_lwip_getif (int netif_index); + ++#if 0 ++ + void ets_intr_lock (void); + void ets_intr_unlock (void); + +@@ -61,3 +58,5 @@ struct ip_info; + #define os_memcpy ets_memcpy + + #endif ++ ++#endif +diff --git a/glue/gluedebug.h b/glue/gluedebug.h +index 9358878..905e41e 100644 +--- a/glue/gluedebug.h ++++ b/glue/gluedebug.h +@@ -10,10 +10,10 @@ + // because it is shared by both sides of glue + + #define UNDEBUG 1 // 0 or 1 (1: uassert removed = saves flash) +-#define UDEBUG 0 // 0 or 1 (glue debug) ++#define UDEBUG ULWIPDEBUG // 0 or 1 (glue debug) + #define UDUMP 0 // 0 or 1 (glue: dump packet) + +-#define ULWIPDEBUG 0 // 0 or 1 (trigger lwip debug) ++// #define ULWIPDEBUG 0 // 0 or 1 (trigger lwip debug) + #define ULWIPASSERT 0 // 0 or 1 (trigger lwip self-check, 0 saves flash) + + #if ARDUINO +@@ -58,7 +58,7 @@ extern void (*phy_capture) (int netif_idx, const char* data, size_t len, int out + + #include // os_printf* definitions + ICACHE_RODATA_ATTR + +-#if defined(ARDUINO) ++#if 0 + // os_printf() does not understand ("%hhx",0x12345678) => "78") and prints 'h' instead + // now hacking/using ::printf() from updated and patched esp-quick-toolchain-by-Earle + #include +diff --git a/makefiles/Makefile.glue b/makefiles/Makefile.glue +index 366d849..3fe3a6c 100644 +--- a/makefiles/Makefile.glue ++++ b/makefiles/Makefile.glue +@@ -3,6 +3,7 @@ + + GLUE_LWIP = lwip-git.c + GLUE_LWIP += esp-ping.c ++GLUE_LWIP += esp-dhcpserver.c + ifneq ($(target),arduino) + GLUE_LWIP += esp-dhcpserver.c + GLUE_LWIP += esp-time.c +@@ -14,7 +15,7 @@ OBJ = \ + $(patsubst %.c,$(BUILD)/%.o,$(wildcard glue/*.c)) \ + $(patsubst %.c,$(BUILD)/glue-lwip/%.o,$(GLUE_LWIP)) \ + +-BUILD_INCLUDES = -I$(BUILD) -Iglue -Iglue-lwip -Iglue-lwip/$(target) -Ilwip2-src/src/include -I$(SDK)/include ++BUILD_INCLUDES = -I$(BUILD) -Iglue -Iglue-lwip -Iglue-lwip/$(target) -Ilwip2-src/src/include $(CFLAGS_EXTRA) + + include makefiles/Makefile.defs + include makefiles/Makefile.rules +diff --git a/makefiles/Makefile.glue-esp b/makefiles/Makefile.glue-esp +index 8b756ff..ea414b1 100644 +--- a/makefiles/Makefile.glue-esp ++++ b/makefiles/Makefile.glue-esp +@@ -6,7 +6,7 @@ ifeq ($(LWIP_ESP),) + $(error LWIP_ESP must point to espressif sdk lwip/include) + endif + +-BUILD_INCLUDES = -I$(BUILD) -I$(SDK)/include -I$(LWIP_ESP) -Iglue ++BUILD_INCLUDES = -I$(BUILD) $(CFLAGS_EXTRA) -I$(LWIP_ESP) -Iglue + BUILD_FLAGS += -DLWIP14GLUE + + include makefiles/Makefile.defs +diff --git a/makefiles/Makefile.lwip2 b/makefiles/Makefile.lwip2 +index 6a4ccbd..01133ec 100644 +--- a/makefiles/Makefile.lwip2 ++++ b/makefiles/Makefile.lwip2 +@@ -13,7 +13,7 @@ OBJ = \ + # $(subst ../../lwip2-contrib-src/,contrib/, \ + # $(patsubst %.c,$(BUILD)/%.o,$(wildcard ../../lwip2-contrib-src/apps/ping/*.c))) + +-BUILD_INCLUDES = -I$(BUILD) -I$(SDK)/include -Iinclude -I../../glue -I../../glue-lwip -I../../glue-lwip/$(target) ++BUILD_INCLUDES = -I$(BUILD) $(CFLAGS_EXTRA) -Iinclude -I../../glue -I../../glue-lwip -I../../glue-lwip/$(target) + #BUILD_INCLUDES += -I../../lwip2-contrib-src/apps/ping + + all: $(LWIP_LIB) diff --git a/Sming/Arch/Esp8266/Platform/RTC.cpp b/Sming/Arch/Esp8266/Platform/RTC.cpp index 49e5e5b0f2..d5f3229e86 100644 --- a/Sming/Arch/Esp8266/Platform/RTC.cpp +++ b/Sming/Arch/Esp8266/Platform/RTC.cpp @@ -11,12 +11,13 @@ #include #include #include +#include RtcClass RTC; #define RTC_MAGIC 0x55aaaa55 #define RTC_DES_ADDR 64 + 3 ///< rBoot may require 3 words at start -#define NS_PER_SECOND 1000000000 +#define NS_PER_SECOND 1'000'000'000 /** @brief Structure to hold RTC data * @addtogroup structures @@ -105,10 +106,33 @@ void loadTime(RtcData& data) } } +extern "C" int settimeofday(const struct timeval* tv, const struct timezone* tz) +{ + // os_printf_plus("** settimeofday(%p, %p), secs = %u\r\n", tv, tz, tv ? unsigned(tv->tv_sec) : 0); + + // Received from lwip2 SNTP + const uint32_t LWIP2_SNTP_MAGIC = 0xfeedC0de; + + if(reinterpret_cast(tz) == LWIP2_SNTP_MAGIC) { + tz = nullptr; + } + + if(tz || !tv) { + // tz is obsolete (cf. man settimeofday) + return EINVAL; + } + + // lwip2 calls this during static initialisation, before RTC is initialised, so ignore value 0 + if(tv->tv_sec) { + uint64_t ns = uint64_t(tv->tv_sec) * NS_PER_SECOND + tv->tv_usec * 1000; + RTC.setRtcNanoseconds(ns); + } + return 0; +} + extern "C" int _gettimeofday_r(struct _reent*, struct timeval* tp, void*) { if(tp) { - // ensureBootTimeIsSet(); uint32_t micros = RTC.getRtcNanoseconds() / 1000LL; tp->tv_sec = micros / 1000; tp->tv_usec = micros % 1000; From 759b89ab519ff1b7ef77b4dbf0a65e9ba748b60a Mon Sep 17 00:00:00 2001 From: Mike Date: Mon, 10 Jun 2024 12:36:12 +0100 Subject: [PATCH 068/128] Remove legacy esp8266 toolchain support (#2794) This PR removes legacy toolchain cruft from the framework. - Old esp8266 libc support not required. All esp-quick-toolchain compiler versions use newlib. - Change compiler version check to error. Code won't work with old toolchains. - Remove use of GCC_VERSION_COMPATIBLE from UPnP library. Framework does check, libraries don't need to. --- .../Esp8266/Components/gdbstub/component.mk | 4 +- .../Arch/Esp8266/Components/libc/component.mk | 8 - .../Esp8266/Components/libc/lib/libmicroc.a | Bin 1713488 -> 0 bytes .../Esp8266/Components/libc/lib/libmicrogcc.a | Bin 320894 -> 0 bytes .../Esp8266/Components/libc/lib/libsetjmp.a | Bin 2184 -> 0 bytes .../libc/src/{newlib => }/libc_replacements.c | 0 .../Components/libc/src/oldlib/README.rst | 7 - .../Components/libc/src/oldlib/pgmspace.c | 139 ------------------ .../Components/libc/src/oldlib/strcspn.c | 48 ------ .../Components/libc/src/oldlib/strerror.c | 22 --- .../Components/libc/src/oldlib/strspn.c | 52 ------- Sming/Arch/Esp8266/build.mk | 3 - Sming/Libraries/UPnP | 2 +- Sming/build.mk | 4 +- docs/source/getting-started/linux/index.rst | 8 +- 15 files changed, 6 insertions(+), 291 deletions(-) delete mode 100644 Sming/Arch/Esp8266/Components/libc/lib/libmicroc.a delete mode 100644 Sming/Arch/Esp8266/Components/libc/lib/libmicrogcc.a delete mode 100644 Sming/Arch/Esp8266/Components/libc/lib/libsetjmp.a rename Sming/Arch/Esp8266/Components/libc/src/{newlib => }/libc_replacements.c (100%) delete mode 100644 Sming/Arch/Esp8266/Components/libc/src/oldlib/README.rst delete mode 100644 Sming/Arch/Esp8266/Components/libc/src/oldlib/pgmspace.c delete mode 100644 Sming/Arch/Esp8266/Components/libc/src/oldlib/strcspn.c delete mode 100644 Sming/Arch/Esp8266/Components/libc/src/oldlib/strerror.c delete mode 100644 Sming/Arch/Esp8266/Components/libc/src/oldlib/strspn.c diff --git a/Sming/Arch/Esp8266/Components/gdbstub/component.mk b/Sming/Arch/Esp8266/Components/gdbstub/component.mk index 814d8d1bd7..6265aaa305 100644 --- a/Sming/Arch/Esp8266/Components/gdbstub/component.mk +++ b/Sming/Arch/Esp8266/Components/gdbstub/component.mk @@ -41,7 +41,5 @@ ifeq ($(GDB_UART_SWAP),1) APP_CFLAGS += -DGDB_UART_SWAP=1 endif -# -ifeq ($(USE_NEWLIB),1) +# All supported compiler versions now use unpatched GDB APP_CFLAGS += -DGDBSTUB_GDB_PATCHED=0 -endif diff --git a/Sming/Arch/Esp8266/Components/libc/component.mk b/Sming/Arch/Esp8266/Components/libc/component.mk index 8e5247d0f3..8f98addef3 100644 --- a/Sming/Arch/Esp8266/Components/libc/component.mk +++ b/Sming/Arch/Esp8266/Components/libc/component.mk @@ -1,17 +1,9 @@ -COMPONENT_SRCDIRS := COMPONENT_INCDIRS := include COMPONENT_SRCDIRS := src COMPONENT_DOXYGEN_INPUT := include/sys -ifeq ($(USE_NEWLIB),1) -COMPONENT_SRCDIRS += src/newlib EXTRA_LIBS += m c gcc -else -COMPONENT_SRCDIRS += src/oldlib -LIBDIRS += $(COMPONENT_PATH)/lib -EXTRA_LIBS += microc microgcc setjmp -endif ifndef MAKE_CLEAN diff --git a/Sming/Arch/Esp8266/Components/libc/lib/libmicroc.a b/Sming/Arch/Esp8266/Components/libc/lib/libmicroc.a deleted file mode 100644 index f130480852b84ab9c2437a7dc22eb511d0dba82f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1713488 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

>ALL9I6yTPST%xGP$gbR{=^Vy@dmXBHt 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

j@KKu%cDOBRgb<}?Eo9}vfsDP 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∾!lYPq1|Q^rR;9(9M#R&C-Of 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�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%0DN4kCIzjKdE?~l36 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

HrDL91Cg(Y2 z%>CV%=aDhbAY-08#yo3`>mAQ^yvQ;4ZqvWc@hZpnIDW|SQ;s(}-s1Q@$AvmyY+u}G zjaxfz@0feL>C`&z?|9Ofv9qSS)ev)QAs$p&SzU=s|97dbKJdT;D=YWkzk9bJ{v2x| zex!4mdtEnqdNRkg5Ovd1O}aE+(^RW(aZdZSw_1}VI^NR?WhZIeGTxw;Za-_V1!lH>&>XX86h%;K=DRt~Bvy3ykNDDLN#6?=Nsem!=_S%Xm94aN5 zAq9WU3@ItrZ>k4hUdTF8Ss5x-RFbX9V6!azBc<&A^VNd#3j`HzzXlIW3)NP9`|w?u z7^{V43rf>Pg|a&V8&{yfqi|OK{~HMjn2|vYTXTK!nuu+44AmxA@v zQ-6Zd^s$rbI!~N_>g4LqlP1=WonAL7>Fi|nV`tSToyXMG*Cm}t&zKS8#*7_3bNtA0 z)9WUW-A*!*-@|rJW9vqbo<8=p?V=~C7E|L)6Dp-a4HidD?uAynh>W=j86&^0K|05W z_9y9y<2FKiZ?C(;b`*p;8?-~cy;ZVTAsP0#m72X;G4_z*j!wPYf8uboJ2}dqr8iJI zmPVNL*H#TgXL({j#zZ}&{Bh4Ef82B}f1}0T-fV?8X)|YW%sKM*TI(LfdjR&-wUDzH z`VB0x{87?b;KF8ao@B;E|4D&#af>AI z_HL4qwvu75ag4+6_1Y|rFnkB$1Ry$#TcNPGw@!&U9@uLdS z*zSqG=`GR;(cF|`9C{CG<2ah*J)U#b96C52+s1@9NH5$?2*_F76JlSEkCbWhO*xqJ zMjT=CY-xnyo!;_F*y3IkdwaYgpocv@CUf?hiIt_C<9+`1Tqti3yWA1Y4cAFmEg{!6 zHrQ8FZJdkV-Xfj%^gWrg{8`w1FaN*z`#|aq$Gwi<*EI>D^dI$_dnrht!10lv#@lbK zxCb-86bIW}Zo*&mTu|))ug=vRMBaEuljELYJj(HS$KjqC_0M$j`EWex7mBTK_PfaA zMSPRvA36O;;b>EsOUrd)b~vuaEgiRa+|lv=jt_Dijs^eZuxWYrHV=35qr|k$aRar% zBvyt?O z{o`Qc>?K5vChmD^b5#Ye_@{W;63&NgwrC+ibXnefbF21$yt$?IySO|k>pyO3 z4K@{8T7y4#ON*+_CI{;O=V@tuV$rS7Y-)}Bv{uyTYkr0Gec}aja`LaVywp6Z3=OjX zWSc9^nSMV0MXuYEUQ_9{wcT@`8=V~I0JkH%#JH5cT#toH_@t|ph&qh7`*0t$`bFEA zdz3NqFWXz`b8WQh!k>e*Z+KTWXL0{-dyCaR7nm{mW^bNjqF+)(Zy-9;`*+)0d+7qI zG-LA3-XgI-4?b^u>j>F9@l))r*umcCZEr1?y;pXyM;>=_N%?`U&unvb>-xFJ zxmp$}le+KyC)!**6Vm5XVdg1uXmi~nz}X9-#<@bHDN{-pp*|ABkye|FgQGSVO*FN+ zS_iL)oLevk_%fK_i`NfNdLZVykmQ8wO&BVrMs&((xXS1^xhut zxON*9=4{i!WVVs7lRf(5u%}yf&R(q;d&qD+hrA;W*GC&SuFuk=ztGYMKlk|8CgpF4 z^vPd|HY@LFvA0M2=g->AS=^58vF>`1Gxr61^qrf%DPl{54Sp}W0Eo`w<|%AU6u-sn z7j^I~ZpZf6R2`&Iif6wZFWc`Dv853v{C=_$h|b~`DeRBidKqae8TNR%nLVCjmPVKi zmAw;z=qzr9!rtCi*`s}eJ>Fx@-mPNN5QkeOdw)$a{3niW$MzU&{5Jm-y&cTu{~BTdsZ+1Z2W64W#aR+$Lc?4 zdyIaUFWX}+e3_qu=F9^-Kx+GCTFv^_Q{Cd;ie$bIpm(u>%+GgYHCQq*W zSK4O#cm2F`5X=8T+f2nvdg@YI^JW=KGOX5JH_k*%@2mf4E@HUXauI1Rq8<^+TqWKp zKfN~>W_~LU>uLU2fFl7;jh|t7nbMIqcWs?ZUt}@H+)PKgsa^hm1o4?fdiFt>-Xr$8=%fj=y5KAv;YXCHW zv_Mm6o&7f(XWeq2L9E&3P(2*Va+4F?Ck7j&(-Rvu(dQF$^Aznx&D`<51lfKRxk_ZIanL5x%{{ zaI|bjB1e#;YDY5Ye>o$OD*A-0l!Q9uHiqk_&FYu1y=U&&CVNURA=ZW>J$ibb{zRMX zP|55#2y^y!Y?Ez~y|#+yU0koJIeWEY>?6Y+mwLJXcKrKDLcTvsk1N{JU}T6k-gV7c zd1?C@6W3I2D0Io+KWmfCSNtYz=4@X(w#mlG-shQ<$Zrs?F=O)0-aN-dH_!>+9*EBL zW-IK=u~-L*@=^|t7jgLEo-f~ASJ|tyfPAyJNbK#cR^qmjVeg-{$p*QhJ}KO}0sT{`#MfNPO2+ZD&ZL;pNhaUF+S(~i2cJw-iqBDE=sak_47>+${H0s$L@%cB0{EI;D z%^>$YPE*+W<{Tqi-<%k0yKz3k#KX3Rrtk}$%g8f=?aR50`rRq@5HF~^VBODg4`?$c zZ0$0RjWNgHnDfLq)Ghzy&@ua7r;C#xEVgm78plUO)~zMd5KVO8czwv#tksJFt6bq>XBD&Jq3lWHR}?g+7^?yXTRoNuBji8Sh3wPqfOgnmYU@ zMGTHloNn9p7j2h*Rf4&xbYqNfTB~qPMW^0FKCCK(SF1KxD^1P1bz5qBO?;(h)D4f; zeDvOl7uQ|%Le2fRe`t1Ve|h^~YijNtdh#=)->uoUZI9X`UfubHE%zlg2fsaG#vScW zs;Rr`^0Qm_{cg>>Z@+x+vY+2ybHef#$6osEQ#G?n|8U?%^M76Q(5g9q9RI*iYHH{G ztEaX@|wM0{BYgtnYA^~KmX+A6}!GtQ~$#UM&EeE zJGOsiwSxT0wX6+ZQd+Ar_S)3z`whQ$=ZW_{{@dsJ-#z~5nx3yTS@i3zwyw%@BY4~`=VcNX?sq$`(C}N?l0vp_OE%m<*Ps0)_F?J zmiHTc`;~E*)s#KB_Ts%a-Cwiy$@@1S_VEEVTQ<&E*7Sy+H9I}LQ|Bko9bI$EmL+#D z-{bt6@Amxd6^CE6s%DRGUwGXkzZ!VY;TQh$p1XFrFP3lPePut~_r8n9UsTh!Ec4DS z$2?jyaQEV&CibD#_F?tNX?MPhVe9JLrQ&hu2U2;IIKNH+_A}+Z}60)STMviVFwV zZ2rrocN|-LcFm2WkKZ*p?ADrthF!P#lJiSyN``Nl@ zel))3tRa`JY}4XfH9u-{!vp=Veaan=^eBA!|H~Tq|4I!sU$g4|y5@hVYx@3Mk7l!V zEy|leQP=DxewSXQL7OfQXR6<5`f9omKA?O=6WxecYtZN2Z1sLc9jc`AO4knE ztCqFsse5nc&2H6wyF8km{r1NNZN?;>%6mk0Mb_?L-RHIJu&P!sx2g-BXQNK8K8`d! ziYNBW1f!F)a|$M2ea5Y6Xu{RAZXJ*vQP8Ql2TONor-V}K8Jcz5>!&fhwW)luXrC9> zKVH?TQ7X~%FNO5~C(ZT@#?si7+jVNyu1C9eUny9thj1D(C;LJ{acNSgcj^PHN3V#| z1@CXm#aAUs<&)K^flejsdsI%ZV<~9md)BR6vQ%BZJ}8Owb6Hz^ZI=E-)kmk29*wL9 zX5D&2^V05JX20E`pid@|e&q|*eKW&)qLW(Q(M~GuPns`tPu9mU(S#rw05SGpdT&u52>B$azscW4$= zcGiF8c|?-|<(W{j*RvlNtknXk#CFZDy_NYT6e4%vFV23iU_lqGymDKW{(mBMD(jFr zxY^o|Lm6MWtxalVb;rs#YbrBgKdA&`{W2#s>+_XQ?B`KsByVZ~>tBaV-IiUjD(Rt` zPwiZsyR>^6)PeSC{+Hr7I^_ReSfo6ra327cO}{d+tLYe_U|Kum zz^t7-zt8S?-f%9p&vb9*?WN3h(55QsT9x_l#4TL1bRPSh+rDz~!T;de z<9u4c0{ZS&)H9qA`8@lYC_X%syp$wwsYj=3SwZ`{?$ZV|wnyFCLo*5X)*h0fQahFQ zP{m$WSDIQ1xsjOqNhLi$^VEQWkd|S=T}cW`nty@Ob1hb?yVO#>Oz!nWC)M^zwu{=~dG?=(`Aqv?xVAp)zL7u2KUu%|>daU1C$EeAy`3v~ z!7u)vt!i<9Rr4pRn*E`w>3eTAUo%k4>?D(mv|gAtEen%8u6N_Rwb5s-z7j2Im6^@Q z_}VLMZXfy>=~QYwFia9&l)a?z(xO835NqiB;(}zV21aB;t}t`6{%5LmPFZW3#5j$8 zkCqoltu+L`5n)EJtMalX3xikmH+hBI;3g;Xzp&)A;1w}HL!;q)$z$o~B-`T5ASt0! zO9SI!B)za?BB4wvw{@j$F0befi85D`Ftb|!iZv`ALM2+BXK6=ClG5MkU*?CX;K*@i z*#vc-l}R@zZtz{jWqyeKIZ?yX%#WJNe)+g0^COa$PZXRgNE*!)QIdlc->5#x+(P{0 z1kQ+o_X(UC1HX{T&^MPVzM`sbG&RPtrg@{OU1I3SB0^nbs9`ao{bT4!LepZPiw;Sn z=`rwQ0;k6S3%oX(sDk~NZo#7!YQn2!W))S@NM#7HLh`2HlVqkgTxLxd<+9n2g!^i& zAPlXvxI>}q6>7Ou&{BJ8*-oZ92zJurHt8j3r&O&nLo?M$OTE;EIH`@1{%G`{kSqY} zR|U96MGU~|)B!fDU_i2SA$hErs!Sd~Px__S*|!VzTTxe?WQGR0b;woJsp7sCMApYs zQK#A)7DU!fR8glYJRyjzN2sE%Tar03h^$koqV9ks^YtLI{-cVz1Cz|~AhIr~in@c6 z%!nYe-lmGW?n&mPAhHgtin@c7%*Y_JzA0r@*&h{T*1sj0@?RHZ)|XXLuQ>;!gUGrp zE9!L$o*YE6=k30VdYyG+gUCAND(Yo_To76RUq!vlj}M~Q)u)82o(Vx@y?Pb(s-B5K z6gv>5rRq5)h^()$qF&YWjUbBMg3?m;ObR0Ffvl)k^-K<;*x4v8RnL?lvVO&idR5P< zK@__%rA75j4KnMlluY@b7G$w&5?ZMbjTM3k#(Y1 zOxGU28AR6aT`^sITo^>wC0;RId%Pfsthc;ky7qWs5Lrih#dPiQq9C$9_KNA+N-PpGGD=D^DpNEx zQ%WpSQYuO`GQNgJhDk;G|Gf8E>&}|-0Zi>z|F8QsclLXK&N}Pt^St*y=j?rjGBGzv z*7`0hW#ThQ@`m{~S11$nlH_gn`z+BVsq|4UOk5B51#6-*9@v zL9bm|M{&Bm)_>(H>rw7=S&wq+7|sppQBE&nWqwa-NwR9Md{q^TVF68dE1K#CQ?~#N zo7KJMMRNDLq;5a^mHM;GwUD*$JKnV& zOKu%Hx7*9#5o=SIgSINc?0rz`;clP$>)I=Io40TFdRf~vEo~My8*ihfz&+4kG=tAc z>C~U!A_^~;r1a2vWh3U5!L!12_!B9ey{HC1t3#kdoifiWn_D}tj8v^|a*cAChrZ%# zLwb&w!FGK5FU3t&Sn%{$%Eh{KdiuBGW;(PyO=tY4hO{zJ44p;l!jFh|BT92~S6&C4 zn(0KWP||_vnNC!#!c)=#N>Zh{OIKc}2`TKR{|z@APyAA*)UP1Sf;^M(iJsPUc#x;H zSz$JNwtuBj$Nd*Qr76GD&-Vfc{s7tdmidCV)aJCM zvaOtLm8>`Ky>t+l+;TxCo4aw1FeqNUI21yWCYG1k*w`1Q0esr@U>I_{rsLN9XS-ixVj?uA=Vdy&}F zUXb>*7r#@w{UYC%=V?E1)b_VE@**3H^rzQp-Iew-q`UN|**@J%we5@BY3Td8o!)XC zyokv`&M|&t7`=>HfsMZE+n&+B&>%JL4_a!;8hNhd=rEe0xdV^X+c}_;jy3 z)a_|MYbMzGcwr6iUY2ujUflDvmk2%Wg+@<%xzf|w(Y3Wt-O{Ora+T()Nz4_t?&^`t zaYf9ove{Lg8)YF~S+g|UA*Hk1HO$r5uOD!ebe*^~y^+N=!>2Vo*3;ElS8~@o2#!^L z^hOLfmKhmx580q=zf>|-txLX;n|PI$C1jEPGW6@dRCnsPsghjHBELnIA zjoX3RgS!DOxyzaw1>hn2hpXeCUoyFZkx>kH*NXu0U?pODOMs@du=C%93 z+UeEYqV=*lRCDEX!*F;;hBmAEsb=j6T4xL1ltq$u5z0GP>vw~GtR*fywAXAUxhs`? z^DhW4=y}+QQzuUuGj;0tv4>e7z#|6rJ?*gZ6EB!>+2}F3x>$YXPE=>P_U52oze-;n zTrgtVm6OLz%{~OTPr`M)Oc;OuCDKx+vya0|@66{*8&@{hmkC!^mgs+_#zf4QUYq`x zCgt+P6^Zld{}oATVhZ-xRPL_-Wh(gRi@htCC9cr_>%zQ|OOl_G%9hEJc|Verj_JR1 z;xqLut$borX3NF@ z)s^I}JmvrDl>Z8S{lRjvY@hZ*-tyB_N@-yl00lslx9 zRJKhv*>>Z9woglJU$jUHxh+leUQ^jcT$Xr7V$UafBlFJ|)*+(*p3iLivjs}hntBe| zJJ(gJ7HwPOI?LPFlO_A>d)HMSs^}-VkoWT{_fB)L$F8g7*zt8@jpJIDR$e6K+Df)l zN%G*^LoK)UEc40Im!BOq{bW;~*5lf=)$4n>^$i|AnUri^{!gqtK>zb;jbpA#DnFJi z$yIg~_;ME{z0N~+%*J{1X>%OPv9B{iSB8nBMqiY>OyAuc*?q)Y_a|QOtDV?6_*-`& zUhD+#Ejw-~eneE)B}08F`$}mtpD9h6lG{l?TW4N%T@1!PL|K|SQ=>_j`r+b}rXC)AaYj^R3BC4O< z1c(XLD&QmZYp)-5UjjOOw|?jcqEfq}yI!qIQW=j=IMMW|0q^u+U)cO7U*a!dr z({A~51#LrZ@wAKCoE%DiNg!=UX8MN18GXQ!xm>OG!Tw_AXBvBq`J=y)I^2AhX;RVO zVS&+aK33l^slbUp4oUY{E5;uh;gc=0*#9$)aFTu;5<9aSARFf4kR;5Jzg8Kh|Gti= zDdo(B{vXNZdMPG<9O0fnzHRe72y=_&4?7R;V~7N2rp=bWk&5xhnc@C;cpCj}kiThm zN@-GY+C?@G8ey~iF^(4g)QD5`N7+338=#h3#$xl}cvAzrpY5Z|mYuTLy4@$laXmH+ z*Vs|AW4S!P_4T+d%p=S#?~uzC01NK#R;8oA72~pR>hQ;RAnuRbiq9j=t$hoB->`YG zA*`3bev0wOt;YTRNF4obl|ROx^5D}}NeFVTFka_}_6PnrZrtC`#mrNqAEEUjdOgRT zC=K%4V6oese5gH0mQHZH-;2X;&z&nfzXiJ;@vHiU#pLG7j`P5SPkUKHT#x>fv-6ep z&>>M=k21~R^WfuF`Fq$;7NvdK>oyNIgdy^W9sc&!&$o*X-{LtbR6SEg0e7ObBXL+v zZbCO*UnCqLol8Idn(L=St)9PSDu;Yong{piX^*4+@Bc=R!cSc&&E9k<3Z-eyib<^> zM`~Rtt#^fD{ae>f6saiPAWgxdE|jLPm1CMcUKjX6&+fKKWIJ=4_duHc>VfpZo={Bs z@CNDL4bohnJzziB6N*U>ZIB+*Abm-L^p%$8-oiJAvl>jF-5|~N%>&Du?+L}EKW}OL zwU@ux(lBDgC6`SYJ+>R=@ypK7VASZ*8H^o2ee~F=<25an|EG?hI##S1rluJ*qR*I# zqo7YbW~%~sg*vn}v;*tYGCpmT`1&UrTTRfr2fatodj-8V=zW9UFX;V)J|O6WgFXbd{W~=1 zBLj~P%$S?LO(q0=a?qy*eR|Mm1bt@EXTf&N%nm#Uw!GaO?B~O_Z5ISy7;F{?UIN>8 zUK;deu=!aY^cBJWo}jM`_N#*aaIjw;^ff_W3)?!bgKfF%gZ+k}Zw&gTz?%ba3HDoq zz74kZ-5&HE!JZqs@5eIO?CIy)^`?QF2b)$wZv)%9@cqBfTL|_Yf?gf$I|aQa=!{Y7 zetN)`w_ZW74fcJ5-Y?iQcB%Ur5cI)89|BwchXx)QY(@tj2U~t71buSQrv-g_&}RgF zX3%E^eKu_QnG^Jz1J8#o4-106FzAbez9i^NgT5^2%Y(im==Z>W{SW%8VE=I7)xl;> z(ANfiUC`GDeM8VU27Ob|HwS%7(6M^Wr=Gwx_1JA^Gq6PCT ziY7*E$TRQbqVs}ozo291KF>17=X7IX?rLN0!rUy_)|VzppZ8P7%=;Cy`KDr;Cp{0V zj9ITA8na$B5n|Iz{IYSam}X1#zT&3F{lxnlvtB&e#iqZwld&+z*m~&Xxfa%sCbqt& z50HMk@nG=)<00Zvuzob*jW&I#^oxu~iZ3x9E#{#r%VNJ=Z_IX}Jn74wWBNGhpD~^w z{;ct2@e*VFeAO5~cf4iuj7x6nRksb^F9gdN3$c%vDih zyjI-Dc%68N@p|z`j5mm<84Gh)7%R%%WxP>Lvp0F$B);ExvzT|U*hjoyMH4tS1g_Pv zqBITOBL0=>TgAMC#k{;{MKih2%eBallWod8u6pX-Nox+ZHFdJb=;b+?C1Vy%(^i8G≈!{Z3nu|j&4vE=lo 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 z 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 OrwX4T%){HY`2PVv7)N#h diff --git a/Sming/Arch/Esp8266/Components/libc/lib/libsetjmp.a b/Sming/Arch/Esp8266/Components/libc/lib/libsetjmp.a deleted file mode 100644 index 1bf5b618823a3a63c48b65597c355714ecc4621f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2184 zcmcgsPe>F|82@HRYZbLzBeErz%wk%EaK6>d@xB(pl|+PbssjJX{miEcp= zT{}ewA?Vbplcy-?)Tu7fA?VM^Q&b-6_nUdoMk|sIeemY{eShBXeeZkk&9%P46NX`k z7DFz|PfO%&Eh47K{xD&$G)t4zC0bL@cfHPj?EN~_kZ4>?CZ6qjmHp8BWoS#PA(iR4 z+ql?(^mYtVoe$iS?~DwT#yuI%^^NrO=1va|pY0nN95~xI)H9ennyK&U9XQ%mFS3OE zgxugvW2vbT->^2&81|$$?b@?b6ZVi>_H5TLr@gXUO8aA%(%DSc!Ay5HYmZhu-w!;m zm_F5STltwnam@BBqjt%iEf(_Wtd+4c%CO-WwWpoY$wJAst6`BMz8g$Um#tjXgxUlu z5%axam_}Vf*7i-VAu|!nNz|D_c5Jp48x5C~8g?#}4t)Q|-tFbB|MO~56Vt_NSEf7d z7RT-A0K#uNZ1*o2(ZT%oEc#CkxA)}QvsU(i)g{K9(JUL+H5)0zOm0i=PPXu7N*jji z7|l@FvDH$uH}Y(RVm>t`2|sZQ2K{<&5?Cz%%5>o5p@T}KCp8M(Y9Q8_6F6e!eP66- zXI6o?+^V=m#~O3Tmm?OdJH5<6C% zA95-`>(l2-I@pb30ZYO+Pmv>#SdmT8= z_ZECd;M1aGSK)~BJpdo`$j5$YzGh&3W`NQL<0gR;%}0%;mSrA!3$S>Hb$!JAv&8u( Ku@L=QG~W;8`TEEJ diff --git a/Sming/Arch/Esp8266/Components/libc/src/newlib/libc_replacements.c b/Sming/Arch/Esp8266/Components/libc/src/libc_replacements.c similarity index 100% rename from Sming/Arch/Esp8266/Components/libc/src/newlib/libc_replacements.c rename to Sming/Arch/Esp8266/Components/libc/src/libc_replacements.c diff --git a/Sming/Arch/Esp8266/Components/libc/src/oldlib/README.rst b/Sming/Arch/Esp8266/Components/libc/src/oldlib/README.rst deleted file mode 100644 index ee596e987e..0000000000 --- a/Sming/Arch/Esp8266/Components/libc/src/oldlib/README.rst +++ /dev/null @@ -1,7 +0,0 @@ -This directory contains the following source code from sourceware's newlib -at https://sourceware.org/git/?p=newlib-cygwin.git;a=tree;f=newlib/libc/string - -strcspn.c -strspn.c - -License LGPL3 applies. diff --git a/Sming/Arch/Esp8266/Components/libc/src/oldlib/pgmspace.c b/Sming/Arch/Esp8266/Components/libc/src/oldlib/pgmspace.c deleted file mode 100644 index 741a4cce4f..0000000000 --- a/Sming/Arch/Esp8266/Components/libc/src/oldlib/pgmspace.c +++ /dev/null @@ -1,139 +0,0 @@ -/**** - * Sming Framework Project - Open Source framework for high efficiency native ESP8266 development. - * Created 2015 by Skurydin Alexey - * http://github.com/SmingHub/Sming - * All files of the Sming Core are provided under the LGPL v3 license. - * - * pgmspace.c - for use with newlib < 2.2 - * - ****/ - -#include -#include - -void* memcpy_P(void* dest, const void* src_P, size_t length) -{ - // Yes, it seems dest must also be aligned - if(IS_ALIGNED(dest) && IS_ALIGNED(src_P) && IS_ALIGNED(length)) { - return memcpy(dest, src_P, length); - } - - char* dest0 = (char*)dest; - const char* src0 = (const char*)src_P; - while(length-- != 0) { - *dest0++ = pgm_read_byte(src0++); - } - - return dest; -} - -int memcmp_P(const void* a1, const void* b1, size_t len) -{ - const uint8_t* a = (const uint8_t*)a1; - const uint8_t* b = (const uint8_t*)b1; - for(size_t i = 0; i < len; i++, ++a, ++b) { - uint8_t d = pgm_read_byte(a) - pgm_read_byte(b); - if(d != 0) { - return d; - } - } - return 0; -} - -size_t strlen_P(const char* src_P) -{ - char val; - size_t len = 0; - while((val = pgm_read_byte(src_P)) != 0) { - ++len; - ++src_P; - } - - return len; -} - -char* strcpy_P(char* dest, const char* src_P) -{ - for(char* p = dest; (*p = pgm_read_byte(src_P++)); p++) - ; - return dest; -} - -char* strncpy_P(char* dest, const char* src_P, size_t max_len) -{ - size_t len = strlen_P(src_P); - if(len > max_len) { - len = max_len; - } else if(len < max_len) { - dest[len] = '\0'; - } - memcpy_P(dest, src_P, len); - return dest; -} - -int strcmp_P(const char* str1, const char* str2_P) -{ - for(; *str1 == pgm_read_byte(str2_P); str1++, str2_P++) { - if(*str1 == '\0') - return 0; - } - return *(unsigned char*)str1 < (unsigned char)pgm_read_byte(str2_P) ? -1 : 1; -} - -int strncmp_P(const char* str1, const char* str2_P, const size_t size) -{ - for(unsigned i = 0; *str1 == pgm_read_byte(str2_P); str1++, str2_P++, i++) { - if(i == size) { - return 0; - } - } - return *(unsigned char*)str1 < (unsigned char)pgm_read_byte(str2_P) ? -1 : 1; -} - -char* strstr_P(char* haystack, const char* needle_P) -{ - const char* b = needle_P; - if(pgm_read_byte(b) == 0) { - return haystack; - } - - for(; *haystack != 0; haystack++) { - if(*haystack != pgm_read_byte(b)) - continue; - - char* a = haystack; - while(1) { - char c = pgm_read_byte(b); - if(c == 0) { - return haystack; - } - - if(*a != c) { - break; - } - - a++; - b++; - } - - b = needle_P; - } - - return 0; -} - -int strcasecmp_P(const char* str1, const char* str2_P) -{ - for(; tolower((unsigned char)*str1) == tolower(pgm_read_byte(str2_P)); str1++, str2_P++) { - if(*str1 == '\0') - return 0; - } - return tolower(*(unsigned char*)str1) < tolower(pgm_read_byte(str2_P)) ? -1 : 1; -} - -char* strcat_P(char* dest, const char* src_P) -{ - dest += strlen(dest); - strcpy_P(dest, src_P); - return dest; -} diff --git a/Sming/Arch/Esp8266/Components/libc/src/oldlib/strcspn.c b/Sming/Arch/Esp8266/Components/libc/src/oldlib/strcspn.c deleted file mode 100644 index abaa93ad67..0000000000 --- a/Sming/Arch/Esp8266/Components/libc/src/oldlib/strcspn.c +++ /dev/null @@ -1,48 +0,0 @@ -/* -FUNCTION - <>---count characters not in string - -INDEX - strcspn - -SYNOPSIS - size_t strcspn(const char *<[s1]>, const char *<[s2]>); - -DESCRIPTION - This function computes the length of the initial part of - the string pointed to by <[s1]> which consists entirely of - characters <[NOT]> from the string pointed to by <[s2]> - (excluding the terminating null character). - -RETURNS - <> returns the length of the substring found. - -PORTABILITY -<> is ANSI C. - -<> requires no supporting OS subroutines. - */ - -#include - -size_t -strcspn (const char *s1, - const char *s2) -{ - const char *s = s1; - const char *c; - - while (*s1) - { - for (c = s2; *c; c++) - { - if (*s1 == *c) - break; - } - if (*c) - break; - s1++; - } - - return s1 - s; -} diff --git a/Sming/Arch/Esp8266/Components/libc/src/oldlib/strerror.c b/Sming/Arch/Esp8266/Components/libc/src/oldlib/strerror.c deleted file mode 100644 index 149954c15e..0000000000 --- a/Sming/Arch/Esp8266/Components/libc/src/oldlib/strerror.c +++ /dev/null @@ -1,22 +0,0 @@ -/**** - * Sming Framework Project - Open Source framework for high efficiency native ESP8266 development. - * Created 2015 by Skurydin Alexey - * http://github.com/SmingHub/Sming - * All files of the Sming Core are provided under the LGPL v3 license. - * - * strerror.c - microc library doesn't contain strerror_r - * - ****/ - -#include - -int __xpg_strerror_r(int err, char* buf, size_t bufSize) -{ - m_snprintf(buf, bufSize, "ERROR #%u", err); - if(buf != NULL && bufSize > 0) { - buf[bufSize - 1] = '\0'; - } - return 0; -} - -int strerror_r(int err, char* buf, size_t bufSize) __attribute__((weak, alias("__xpg_strerror_r"))); diff --git a/Sming/Arch/Esp8266/Components/libc/src/oldlib/strspn.c b/Sming/Arch/Esp8266/Components/libc/src/oldlib/strspn.c deleted file mode 100644 index baf2399478..0000000000 --- a/Sming/Arch/Esp8266/Components/libc/src/oldlib/strspn.c +++ /dev/null @@ -1,52 +0,0 @@ -/* -FUNCTION - <>---find initial match - -INDEX - strspn - -SYNOPSIS - #include - size_t strspn(const char *<[s1]>, const char *<[s2]>); - -DESCRIPTION - This function computes the length of the initial segment of - the string pointed to by <[s1]> which consists entirely of - characters from the string pointed to by <[s2]> (excluding the - terminating null character). - -RETURNS - <> returns the length of the segment found. - -PORTABILITY -<> is ANSI C. - -<> requires no supporting OS subroutines. - -QUICKREF - strspn ansi pure -*/ - -#include - -size_t -strspn (const char *s1, - const char *s2) -{ - const char *s = s1; - const char *c; - - while (*s1) - { - for (c = s2; *c; c++) - { - if (*s1 == *c) - break; - } - if (*c == '\0') - break; - s1++; - } - - return s1 - s; -} diff --git a/Sming/Arch/Esp8266/build.mk b/Sming/Arch/Esp8266/build.mk index a094406823..33ce518890 100644 --- a/Sming/Arch/Esp8266/build.mk +++ b/Sming/Arch/Esp8266/build.mk @@ -53,8 +53,5 @@ ifeq (,$(wildcard $(XTENSA_TOOLS_ROOT))) $(error ESP_HOME not set correctly: "$(ESP_HOME)") endif -# Identifies which library we're building with -USE_NEWLIB = $(GCC_VERSION_COMPATIBLE) - # => Tools MEMANALYZER = $(PYTHON) $(ARCH_TOOLS)/memanalyzer.py $(OBJDUMP)$(TOOL_EXT) diff --git a/Sming/Libraries/UPnP b/Sming/Libraries/UPnP index 6b73de1097..9bfc241efa 160000 --- a/Sming/Libraries/UPnP +++ b/Sming/Libraries/UPnP @@ -1 +1 @@ -Subproject commit 6b73de10973c513201acac11d10ae38d13580c6d +Subproject commit 9bfc241efa147b8aba46e5ba25f4bbdfaa18efeb diff --git a/Sming/build.mk b/Sming/build.mk index 69ca8b885c..8d482cceae 100644 --- a/Sming/build.mk +++ b/Sming/build.mk @@ -238,10 +238,10 @@ GCC_MIN_MAJOR_VERSION := 8 GCC_VERSION_COMPATIBLE := $(shell expr $$(echo $(GCC_VERSION) | cut -f1 -d.) \>= $(GCC_MIN_MAJOR_VERSION)) ifeq ($(GCC_VERSION_COMPATIBLE),0) -$(warning ***** Please, upgrade your GCC compiler to version $(GCC_MIN_MAJOR_VERSION) or newer *****) ifneq ($(GCC_UPGRADE_URL),) $(info Instructions for upgrading your compiler can be found here: $(GCC_UPGRADE_URL)) -endif +endif +$(error Please, upgrade your GCC compiler to version $(GCC_MIN_MAJOR_VERSION) or newer.) endif endif diff --git a/docs/source/getting-started/linux/index.rst b/docs/source/getting-started/linux/index.rst index 9c237ab553..174a426bf2 100644 --- a/docs/source/getting-started/linux/index.rst +++ b/docs/source/getting-started/linux/index.rst @@ -30,15 +30,11 @@ Debian (Ubuntu) and Fedora systems can use the scripted installer. dnf install -y git -3. Fetch the Sming repository - - :: +3. Fetch the Sming repository:: git clone https://github.com/SmingHub/Sming /opt/sming -4. Run the installer - - :: +4. Run the installer:: source /opt/sming/Tools/install.sh all From 16e3fd5808f30540974f6b87a89d9025774ba41a Mon Sep 17 00:00:00 2001 From: Mike Date: Mon, 10 Jun 2024 15:21:13 +0100 Subject: [PATCH 069/128] Update ArduinoJson6 to current 6.x release (#2795) ArduinoJson is currently at v7 but there are many breaking changes to consider so would require a new library. This PR updates v6 to the latest release: Was v6.15.2, released15/5/20 v6.21.3 released 23/7/23 Now v6.x, released 6/11/23 FlashString (FSTR::String) support has been simplified, and includes a couple of minor patches to ArduinoJson. This ensures FlashString objects are always passed by reference. --- Sming/Libraries/ArduinoJson6/ArduinoJson | 2 +- .../Libraries/ArduinoJson6/ArduinoJson.patch | 48 ++++++++++++++ .../include/FlashStringReader.hpp | 30 ++------- .../include/FlashStringRefAdapter.hpp | 64 +++---------------- tests/HostTests/modules/ArduinoJson6.cpp | 8 ++- 5 files changed, 72 insertions(+), 80 deletions(-) create mode 100644 Sming/Libraries/ArduinoJson6/ArduinoJson.patch diff --git a/Sming/Libraries/ArduinoJson6/ArduinoJson b/Sming/Libraries/ArduinoJson6/ArduinoJson index 6fb52c3638..7517ecb91b 160000 --- a/Sming/Libraries/ArduinoJson6/ArduinoJson +++ b/Sming/Libraries/ArduinoJson6/ArduinoJson @@ -1 +1 @@ -Subproject commit 6fb52c363849557c69485f97110371d0a4454432 +Subproject commit 7517ecb91b50b8ed4b6c94a4b83031ef86a01a26 diff --git a/Sming/Libraries/ArduinoJson6/ArduinoJson.patch b/Sming/Libraries/ArduinoJson6/ArduinoJson.patch new file mode 100644 index 0000000000..34595bfae4 --- /dev/null +++ b/Sming/Libraries/ArduinoJson6/ArduinoJson.patch @@ -0,0 +1,48 @@ +diff --git a/src/ArduinoJson/Document/JsonDocument.hpp b/src/ArduinoJson/Document/JsonDocument.hpp +index 39c1536f..1b129120 100644 +--- a/src/ArduinoJson/Document/JsonDocument.hpp ++++ b/src/ArduinoJson/Document/JsonDocument.hpp +@@ -181,6 +181,11 @@ class JsonDocument : public detail::VariantOperators { + return {*this, key}; + } + ++ inline detail::MemberProxy ++ operator[](const FSTR::String& key) { ++ return {*this, ::String(key)}; ++ } ++ + // Gets or sets a root object's member. + // https://arduinojson.org/v6/api/jsondocument/subscript/ + template +diff --git a/src/ArduinoJson/Object/JsonObject.hpp b/src/ArduinoJson/Object/JsonObject.hpp +index 7cdc1c76..2e006b1e 100644 +--- a/src/ArduinoJson/Object/JsonObject.hpp ++++ b/src/ArduinoJson/Object/JsonObject.hpp +@@ -115,6 +115,11 @@ class JsonObject : public detail::VariantOperators { + return {*this, key}; + } + ++ inline detail::MemberProxy ++ operator[](const FSTR::String& key) const { ++ return {*this, ::String(key)}; ++ } ++ + // Gets or sets the member with specified key. + // https://arduinojson.org/v6/api/jsonobject/subscript/ + template +diff --git a/src/ArduinoJson/Variant/VariantRefBase.hpp b/src/ArduinoJson/Variant/VariantRefBase.hpp +index 2afdda6a..6b48e102 100644 +--- a/src/ArduinoJson/Variant/VariantRefBase.hpp ++++ b/src/ArduinoJson/Variant/VariantRefBase.hpp +@@ -244,6 +244,11 @@ class VariantRefBase : public VariantTag { + MemberProxy>::type + operator[](TChar* key) const; + ++ inline MemberProxy ++ operator[](const FSTR::String& key) const { ++ return MemberProxy(derived(), ::String(key)); ++ } ++ + // Creates an array and adds it to the object. + // https://arduinojson.org/v6/api/jsonvariant/createnestedarray/ + template diff --git a/Sming/Libraries/ArduinoJson6/include/FlashStringReader.hpp b/Sming/Libraries/ArduinoJson6/include/FlashStringReader.hpp index 18058249cc..156454c9a6 100644 --- a/Sming/Libraries/ArduinoJson6/include/FlashStringReader.hpp +++ b/Sming/Libraries/ArduinoJson6/include/FlashStringReader.hpp @@ -6,33 +6,15 @@ #pragma once -namespace ARDUINOJSON_NAMESPACE -{ -template <> struct Reader { - explicit Reader(const FlashString& str) : str(str) - { - } +#include +#include - int read() - { - if(index >= str.length()) { - return -1; - } - unsigned char c = str[index]; - ++index; - return c; - } +ARDUINOJSON_BEGIN_PRIVATE_NAMESPACE - size_t readBytes(char* buffer, size_t length) +template <> struct Reader : public BoundedReader { + explicit Reader(const FSTR::String& str) : BoundedReader(str.data(), str.length()) { - auto count = str.read(index, buffer, length); - index += count; - return count; } - -private: - const FlashString& str; - unsigned index = 0; }; -} // namespace ARDUINOJSON_NAMESPACE +ARDUINOJSON_END_PRIVATE_NAMESPACE diff --git a/Sming/Libraries/ArduinoJson6/include/FlashStringRefAdapter.hpp b/Sming/Libraries/ArduinoJson6/include/FlashStringRefAdapter.hpp index 5f55ae6591..b4e287d559 100644 --- a/Sming/Libraries/ArduinoJson6/include/FlashStringRefAdapter.hpp +++ b/Sming/Libraries/ArduinoJson6/include/FlashStringRefAdapter.hpp @@ -6,67 +6,23 @@ #pragma once -#include +#include +#include -namespace ARDUINOJSON_NAMESPACE -{ -class FlashStringRefAdapter -{ -public: - explicit FlashStringRefAdapter(const FlashString& str) : str(str) - { - } - - bool equals(const char* expected) const - { - return str.equals(expected); - } - - bool isNull() const - { - return str.isNull(); - } +ARDUINOJSON_BEGIN_PRIVATE_NAMESPACE - char* save(MemoryPool* pool) const - { - size_t n = str.size(); - char* dup = pool->allocFrozenString(n); - if(dup) { - str.read(0, dup, n); - } - return dup; - } +template <> struct StringAdapter { + typedef FlashString AdaptedString; - const char* data() const + static AdaptedString adapt(const FSTR::String& str) { - // Cannot access directly using a char* - return nullptr; + return FlashString(str.data(), str.length()); } - - size_t size() const - { - return str.length(); - } - - bool isStatic() const - { - // Whilst our value won't change, it cannot be accessed using a regular char* - return false; - } - -private: - const FlashString& str; }; -inline FlashStringRefAdapter adaptString(const FlashString& str) +inline CompareResult compare(JsonVariantConst lhs, const FSTR::String& rhs) { - return FlashStringRefAdapter(str); + return compare(lhs, String(rhs)); } -template <> struct IsString : true_type { -}; - -template <> struct IsWriteableString : false_type { -}; - -} // namespace ARDUINOJSON_NAMESPACE +ARDUINOJSON_END_PRIVATE_NAMESPACE diff --git a/tests/HostTests/modules/ArduinoJson6.cpp b/tests/HostTests/modules/ArduinoJson6.cpp index e56d813818..302b5f603c 100644 --- a/tests/HostTests/modules/ArduinoJson6.cpp +++ b/tests/HostTests/modules/ArduinoJson6.cpp @@ -80,7 +80,7 @@ class JsonTest6 : public TestGroup } // Keep a reference copy for when doc gets messed up - StaticJsonDocument<512> sourceDoc = doc; + auto sourceDoc = doc; TEST_CASE("Json::serialize(doc, String), then save to file") { @@ -248,6 +248,12 @@ class JsonTest6 : public TestGroup REQUIRE(root["longtest"] == testnum); } + TEST_CASE("FSTR comparison") + { + doc[FS_number2] = FS_number2; + REQUIRE(doc[FS_number2] == FS_number2); + } + /* * Dangling reference https://github.com/bblanchon/ArduinoJson/issues/1120 * Fixed in ArduinoJson 6.13.0 From cf4e1c9ca27b6f964ae8e19a14b3da2b70572773 Mon Sep 17 00:00:00 2001 From: Mike Date: Tue, 11 Jun 2024 10:18:38 +0100 Subject: [PATCH 070/128] Improve FlashString portability (#2796) This PR removes copy support from the FlashString library. Doing so simplifies the code, improves performance and portability to other compilers (e.g. clang). The FlashString library previously supported copies (references) like this:: ``` FlashString emptyString; FlashString stringCopy(FS("Inline string")); DEFINE_FSTR_DATA_LOCAL(flashHelloData, "Hello"); auto myCopy = flashHelloData; ``` These will now fail to compile. Copy construction and assignment has been explicitly deleted so avoid unintentional side-effects. Objects should always be passed by reference. This change has the additional benefit of catching pass-by-copy errors. These function/method templates have been fixed: - `fileSetContent` - `IFS::File::setAttribute` - `IFS::FileSystem::setUserAttribute` - `IFS::ArchiveStream::setUserAttribute` --- Sming/Components/FlashString | 2 +- Sming/Components/IFS | 2 +- Sming/Core/FileSystem.h | 3 ++- docs/source/upgrading/5.1-5.2.rst | 16 ++++++++++++++++ 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/Sming/Components/FlashString b/Sming/Components/FlashString index c7f2b606c1..a44b584a10 160000 --- a/Sming/Components/FlashString +++ b/Sming/Components/FlashString @@ -1 +1 @@ -Subproject commit c7f2b606c121d757a385b17faa50b7b7367d223a +Subproject commit a44b584a100848fdb89cc6ea5f1778b4557d3227 diff --git a/Sming/Components/IFS b/Sming/Components/IFS index 4db9a92b7e..4d30df4bf9 160000 --- a/Sming/Components/IFS +++ b/Sming/Components/IFS @@ -1 +1 @@ -Subproject commit 4db9a92b7e893a0406f1c29ca30072adb676e753 +Subproject commit 4d30df4bf9e2bb155009595381d0822bdd81dbfa diff --git a/Sming/Core/FileSystem.h b/Sming/Core/FileSystem.h index 211813000f..c2682fc4f2 100644 --- a/Sming/Core/FileSystem.h +++ b/Sming/Core/FileSystem.h @@ -235,7 +235,8 @@ template inline int fileSetContent(const TFileName& fileNam return fileSystem->setContent(fileName, content, length); } -template inline int fileSetContent(const TFileName& fileName, TContent content) +template +inline int fileSetContent(const TFileName& fileName, const TContent& content) { CHECK_FS(setContent) return fileSystem->setContent(fileName, content); diff --git a/docs/source/upgrading/5.1-5.2.rst b/docs/source/upgrading/5.1-5.2.rst index 0d10a27c14..96f7fc41a4 100644 --- a/docs/source/upgrading/5.1-5.2.rst +++ b/docs/source/upgrading/5.1-5.2.rst @@ -75,3 +75,19 @@ Applications must explicitly call :cpp:func:`HttpRequest::onSslInit` and set the This extra step ensures that security checks are not unintentionally bypassed. The same behaviour is now presented when using Bearssl, and will now fail with ``X509_NOT_TRUSTED``. + + +**FlashString copy support removed** + +The :library:`FlashString` previously supported copies (references) like this:: + + FlashString emptyString; + FlashString stringCopy(FS("Inline string")); + + DEFINE_FSTR_DATA_LOCAL(flashHelloData, "Hello"); + auto myCopy = flashHelloData; + +These will now fail to compile. +Copy construction and assignment has been explicitly deleted so avoid unintentional side-effects. + +Objects should always be passed by reference. From 5e91b29169e931687d7444790c7d97e18047851c Mon Sep 17 00:00:00 2001 From: Mike Date: Wed, 12 Jun 2024 10:09:28 +0100 Subject: [PATCH 071/128] Fix warnings flagged by clang build (#2801) Co-authored-by: mikee47 --- .../Network/src/Network/SmtpClient.h | 2 ++ .../Storage/src/include/Storage/Partition.h | 2 +- Sming/Core/Data/Stream/LimitedWriteStream.h | 2 +- Sming/Libraries/MHZ19 | 2 +- Sming/Libraries/SignalGenerator | 2 +- Sming/Libraries/TFT_S1D13781 | 2 +- Sming/Libraries/UPnP | 2 +- Sming/Libraries/UPnP-Schema | 2 +- .../Libraries/WebCam/src/Camera/FakeCamera.h | 2 +- samples/Basic_ProgMem/app/TestProgmem.cpp | 20 +++++++++++-------- samples/Basic_ProgMem/app/application.cpp | 1 - samples/Basic_Ssl/include/CounterStream.h | 2 +- samples/Basic_Utility/app/utility.cpp | 4 ---- .../app/application.cpp | 2 ++ samples/Humidity_AM2321/app/application.cpp | 1 - samples/LiveDebug/app/application.cpp | 4 ++-- samples/MeteoControl/app/webserver.cpp | 3 +-- samples/MqttClient_Hello/app/application.cpp | 10 +++------- 18 files changed, 31 insertions(+), 34 deletions(-) diff --git a/Sming/Components/Network/src/Network/SmtpClient.h b/Sming/Components/Network/src/Network/SmtpClient.h index 91839a7b5c..90a1b695f2 100644 --- a/Sming/Components/Network/src/Network/SmtpClient.h +++ b/Sming/Components/Network/src/Network/SmtpClient.h @@ -182,6 +182,8 @@ class SmtpClient : protected TcpClient void sendMailHeaders(MailMessage* mail); bool sendMailBody(MailMessage* mail); + using TcpClient::connect; + private: Url url; Vector authMethods; diff --git a/Sming/Components/Storage/src/include/Storage/Partition.h b/Sming/Components/Storage/src/include/Storage/Partition.h index 1a2e4c0d0e..7ab5cdc168 100644 --- a/Sming/Components/Storage/src/include/Storage/Partition.h +++ b/Sming/Components/Storage/src/include/Storage/Partition.h @@ -76,7 +76,7 @@ struct esp_partition_info_t; namespace Disk { -class DiskPart; +struct DiskPart; } /** diff --git a/Sming/Core/Data/Stream/LimitedWriteStream.h b/Sming/Core/Data/Stream/LimitedWriteStream.h index a2d0bae843..890b99e58f 100644 --- a/Sming/Core/Data/Stream/LimitedWriteStream.h +++ b/Sming/Core/Data/Stream/LimitedWriteStream.h @@ -26,7 +26,7 @@ class LimitedWriteStream : public StreamWrapper { } - bool isValid() + bool isValid() const override { return writePos <= maxBytes; } diff --git a/Sming/Libraries/MHZ19 b/Sming/Libraries/MHZ19 index a2ecfec4c1..b177ff0b81 160000 --- a/Sming/Libraries/MHZ19 +++ b/Sming/Libraries/MHZ19 @@ -1 +1 @@ -Subproject commit a2ecfec4c1fa07e7be68dab0d1a2fcc2cc6190de +Subproject commit b177ff0b819d3c3bacc1b36e8d7f9bbd19698355 diff --git a/Sming/Libraries/SignalGenerator b/Sming/Libraries/SignalGenerator index b43d09e8d3..8ef328870f 160000 --- a/Sming/Libraries/SignalGenerator +++ b/Sming/Libraries/SignalGenerator @@ -1 +1 @@ -Subproject commit b43d09e8d31e980ce562e48a0d190eb89f7fe5f7 +Subproject commit 8ef328870fead0a9a47e6d1610e58327913e3b00 diff --git a/Sming/Libraries/TFT_S1D13781 b/Sming/Libraries/TFT_S1D13781 index 0903c3e305..3d5ddab2f7 160000 --- a/Sming/Libraries/TFT_S1D13781 +++ b/Sming/Libraries/TFT_S1D13781 @@ -1 +1 @@ -Subproject commit 0903c3e30545e1a023e6200bca2d9d9ebc73ea9b +Subproject commit 3d5ddab2f7dec460ce649ca69bc5ab625c2c7215 diff --git a/Sming/Libraries/UPnP b/Sming/Libraries/UPnP index 9bfc241efa..2700835f8e 160000 --- a/Sming/Libraries/UPnP +++ b/Sming/Libraries/UPnP @@ -1 +1 @@ -Subproject commit 9bfc241efa147b8aba46e5ba25f4bbdfaa18efeb +Subproject commit 2700835f8ef23efd415751db0a7843b1626cb8da diff --git a/Sming/Libraries/UPnP-Schema b/Sming/Libraries/UPnP-Schema index 0bb97e2287..9cb308fc65 160000 --- a/Sming/Libraries/UPnP-Schema +++ b/Sming/Libraries/UPnP-Schema @@ -1 +1 @@ -Subproject commit 0bb97e228763fb7253dc20a221fb34771e7b6ff3 +Subproject commit 9cb308fc652a1d14b5f9df8351da7794d6248602 diff --git a/Sming/Libraries/WebCam/src/Camera/FakeCamera.h b/Sming/Libraries/WebCam/src/Camera/FakeCamera.h index eff18f6873..8edf082d73 100644 --- a/Sming/Libraries/WebCam/src/Camera/FakeCamera.h +++ b/Sming/Libraries/WebCam/src/Camera/FakeCamera.h @@ -88,7 +88,7 @@ class FakeCamera: public CameraInterface * * @retval bytes successfully read and stored in the buffer */ - size_t read(char* buffer, size_t size, size_t offset = 0) + size_t read(char* buffer, size_t size, size_t offset = 0) override { // get the current picture and read the desired data from it. file.seekFrom(offset, SeekOrigin::Start); diff --git a/samples/Basic_ProgMem/app/TestProgmem.cpp b/samples/Basic_ProgMem/app/TestProgmem.cpp index 1815dc374d..fe86770329 100644 --- a/samples/Basic_ProgMem/app/TestProgmem.cpp +++ b/samples/Basic_ProgMem/app/TestProgmem.cpp @@ -167,50 +167,54 @@ void testSpeed(Print& out) out.printf("Speed tests, %u iterations, times in microseconds\n", iterations); ElapseTimer timer; - unsigned tmp = 0; uint32_t baseline, elapsed; _FPUTS("Baseline test, read string in RAM..."); + unsigned sum = 0; timer.start(); for(unsigned i = 0; i < iterations; ++i) { - tmp += sumBuffer(demoText, sizeof(demoText)); + sum += sumBuffer(demoText, sizeof(demoText)); } baseline = timer.elapsedTime(); - out << "Elapsed: " << baseline << endl; + out << "Elapsed: " << baseline << ", sum " << sum << endl; #define END() \ elapsed = timer.elapsedTime(); \ - out << "Elapsed: " << elapsed << " (baseline + " << elapsed - baseline << ')' << endl; + out << "Elapsed: " << elapsed << " (baseline + " << elapsed - baseline << "), sum " << sum << endl; _FPUTS("Load PSTR into stack buffer..."); + sum = 0; timer.start(); for(unsigned i = 0; i < iterations; ++i) { LOAD_PSTR(buf, demoPSTR1); - tmp += sumBuffer(buf, sizeof(buf)); + sum += sumBuffer(buf, sizeof(buf)); } END() _FPUTS("Load PSTR into String..."); + sum = 0; timer.start(); for(unsigned i = 0; i < iterations; ++i) { String s(demoFSTR1.data()); - tmp += sumBuffer(s.c_str(), s.length() + 1); + sum += sumBuffer(s.c_str(), s.length() + 1); } END() _FPUTS("Load FlashString into stack buffer..."); + sum = 0; timer.start(); for(unsigned i = 0; i < iterations; ++i) { LOAD_FSTR(buf, demoFSTR1); - tmp += sumBuffer(buf, sizeof(buf)); + sum += sumBuffer(buf, sizeof(buf)); } END() _FPUTS("Load FlashString into String..."); + sum = 0; timer.start(); for(unsigned i = 0; i < iterations; ++i) { String s(demoFSTR1); - tmp += sumBuffer(s.c_str(), s.length() + 1); + sum += sumBuffer(s.c_str(), s.length() + 1); } END() } diff --git a/samples/Basic_ProgMem/app/application.cpp b/samples/Basic_ProgMem/app/application.cpp index 498cdf68ef..abdc5d24f4 100644 --- a/samples/Basic_ProgMem/app/application.cpp +++ b/samples/Basic_ProgMem/app/application.cpp @@ -16,7 +16,6 @@ const uint8_t demoRam[] = {1, 2, 3, 4, 5}; const PROGMEM uint8_t demoPgm[] = {1, 2, 3, 4, 5}; const PROGMEM char demoString[] = "Demo"; -const PROGMEM char demoString2[] = "Demo"; const PROGMEM char demoFormat[] = "Demo %d"; const PROGMEM uint8_t bytes[] = {1, 2, 3, 4, 5, 6, 7, 8}; diff --git a/samples/Basic_Ssl/include/CounterStream.h b/samples/Basic_Ssl/include/CounterStream.h index 07b0c66cf1..b33f9676d3 100644 --- a/samples/Basic_Ssl/include/CounterStream.h +++ b/samples/Basic_Ssl/include/CounterStream.h @@ -9,7 +9,7 @@ class CounterStream : public ReadWriteStream { public: - size_t write(const uint8_t* buffer, size_t size) + size_t write(const uint8_t* buffer, size_t size) override { streamSize += size; return size; diff --git a/samples/Basic_Utility/app/utility.cpp b/samples/Basic_Utility/app/utility.cpp index 3ef86868de..cfd203822d 100644 --- a/samples/Basic_Utility/app/utility.cpp +++ b/samples/Basic_Utility/app/utility.cpp @@ -6,10 +6,6 @@ auto& output = Host::standardOutput; namespace { -#define XX(name, ext, mime) ext "\0" -DEFINE_FSTR_LOCAL(fstr_ext, "htm\0" MIME_TYPE_MAP(XX)) -#undef XX - namespace Command { DEFINE_FSTR(testWebConstants, "testWebConstants") diff --git a/samples/HttpServer_ConfigNetwork/app/application.cpp b/samples/HttpServer_ConfigNetwork/app/application.cpp index 2301a5257e..2c1e3d882a 100644 --- a/samples/HttpServer_ConfigNetwork/app/application.cpp +++ b/samples/HttpServer_ConfigNetwork/app/application.cpp @@ -27,8 +27,10 @@ DEFINE_FSTR(DEFAULT_GATEWAY, "192.168.1.1") // Instead of using a SPIFFS file, here we demonstrate usage of imported Flash Strings IMPORT_FSTR_LOCAL(flashSettings, PROJECT_DIR "/web/build/settings.html") +#ifdef ENABLE_SSL IMPORT_FSTR_LOCAL(serverKey, PROJECT_DIR "/cert/key_1024"); IMPORT_FSTR_LOCAL(serverCert, PROJECT_DIR "/cert/x509_1024.cer"); +#endif void onIndex(HttpRequest& request, HttpResponse& response) { diff --git a/samples/Humidity_AM2321/app/application.cpp b/samples/Humidity_AM2321/app/application.cpp index 5b01a7f96b..19fb17275c 100644 --- a/samples/Humidity_AM2321/app/application.cpp +++ b/samples/Humidity_AM2321/app/application.cpp @@ -6,7 +6,6 @@ namespace AM2321 am2321; SimpleTimer procTimer; -bool state = true; // You can change I2C pins here: const int SCL = 5; diff --git a/samples/LiveDebug/app/application.cpp b/samples/LiveDebug/app/application.cpp index 6606f63742..146ec9affc 100644 --- a/samples/LiveDebug/app/application.cpp +++ b/samples/LiveDebug/app/application.cpp @@ -398,7 +398,7 @@ COMMAND_HANDLER(read0) "At GDB prompt, enter `set $pc = $pc + 3` to skip offending instruction,\r\n" "then enter `c` to continue")); Serial.flush(); - uint8_t value = *(uint8_t*)0; + uint8_t value = *(volatile uint8_t*)0; Serial << _F("Value at address 0 = 0x") << String(value, HEX, 2) << endl; return true; } @@ -409,7 +409,7 @@ COMMAND_HANDLER(write0) "At GDB prompt, enter `set $pc = $pc + 3` to skip offending instruction,\r\n" "then enter `c` to continue")); Serial.flush(); - *(uint8_t*)0 = 0; + *(volatile uint8_t*)0 = 0; Serial.println(_F("...still running!")); return true; } diff --git a/samples/MeteoControl/app/webserver.cpp b/samples/MeteoControl/app/webserver.cpp index 530dda51da..fff647f7bc 100644 --- a/samples/MeteoControl/app/webserver.cpp +++ b/samples/MeteoControl/app/webserver.cpp @@ -8,8 +8,7 @@ namespace { HttpServer server; HttpClient downloadClient; -bool serverStarted = false; -int dowfid = 0; +bool serverStarted; void onIndex(HttpRequest& request, HttpResponse& response) { diff --git a/samples/MqttClient_Hello/app/application.cpp b/samples/MqttClient_Hello/app/application.cpp index dc51bb8980..b27b3a7302 100644 --- a/samples/MqttClient_Hello/app/application.cpp +++ b/samples/MqttClient_Hello/app/application.cpp @@ -9,17 +9,13 @@ namespace { -// For testing purposes, try a few different URL formats -DEFINE_FSTR(MQTT_URL1, "mqtt://test.mosquitto.org:1883") -DEFINE_FSTR(MQTT_URL2, "mqtts://test.mosquitto.org:8883") // (Need ENABLE_SSL) -DEFINE_FSTR(MQTT_URL3, "mqtt://frank:fiddle@192.168.100.107:1883") - #ifdef ENABLE_SSL #include #include -#define MQTT_URL MQTT_URL2 +DEFINE_FSTR(MQTT_URL, "mqtts://test.mosquitto.org:8883") #else -#define MQTT_URL MQTT_URL1 +DEFINE_FSTR(MQTT_URL, "mqtt://test.mosquitto.org:1883") +// DEFINE_FSTR(MQTT_URL, "mqtt://frank:fiddle@192.168.100.107:1883") #endif // Forward declarations From fa58a70c3223ac57d45ec53d7f474238ebc1021b Mon Sep 17 00:00:00 2001 From: Mike Date: Wed, 12 Jun 2024 10:42:41 +0100 Subject: [PATCH 072/128] Update Adafruit_GFX (#2799) Update from version 1.10.12 (17/9/2021) to version 1.11.9 (10/10/2023). Comparison of sources shows no functional difference. --- Sming/Libraries/.patches/Adafruit_GFX.patch | 8 ++++---- Sming/Libraries/Adafruit_GFX | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Sming/Libraries/.patches/Adafruit_GFX.patch b/Sming/Libraries/.patches/Adafruit_GFX.patch index acaa94e23b..e4067cbca8 100644 --- a/Sming/Libraries/.patches/Adafruit_GFX.patch +++ b/Sming/Libraries/.patches/Adafruit_GFX.patch @@ -1,5 +1,5 @@ diff --git a/Adafruit_SPITFT.cpp b/Adafruit_SPITFT.cpp -index b78d5ce..f919595 100644 +index eeffce7..94e07f1 100644 --- a/Adafruit_SPITFT.cpp +++ b/Adafruit_SPITFT.cpp @@ -35,6 +35,10 @@ @@ -211,7 +211,7 @@ index b78d5ce..f919595 100644 } /*! -@@ -1184,150 +1034,6 @@ void Adafruit_SPITFT::writeColor(uint16_t color, uint32_t len) { +@@ -1197,150 +1047,6 @@ void Adafruit_SPITFT::writeColor(uint16_t color, uint32_t len) { uint8_t hi = color >> 8, lo = color; @@ -364,7 +364,7 @@ index b78d5ce..f919595 100644 do { diff --git a/Adafruit_SPITFT.h b/Adafruit_SPITFT.h -index 7f5d80f..9725e13 100644 +index 8064a74..5c119ad 100644 --- a/Adafruit_SPITFT.h +++ b/Adafruit_SPITFT.h @@ -138,13 +138,11 @@ public: @@ -382,7 +382,7 @@ index 7f5d80f..9725e13 100644 // Parallel constructor: expects width & height (rotation 0), flag // indicating whether 16-bit (true) or 8-bit (false) interface, 3 signal -@@ -403,7 +401,7 @@ protected: +@@ -404,7 +402,7 @@ protected: union { #endif struct { // Values specific to HARDWARE SPI: diff --git a/Sming/Libraries/Adafruit_GFX b/Sming/Libraries/Adafruit_GFX index 223f914d0e..126007f2c5 160000 --- a/Sming/Libraries/Adafruit_GFX +++ b/Sming/Libraries/Adafruit_GFX @@ -1 +1 @@ -Subproject commit 223f914d0e092cc24723182a2e3273e61c4b22ea +Subproject commit 126007f2c52d3238b7a1133ec14192c3d1deb8a9 From d4f490f1ad437f805627d005f06a9eebf20178d4 Mon Sep 17 00:00:00 2001 From: Mike Date: Wed, 12 Jun 2024 10:44:11 +0100 Subject: [PATCH 073/128] Support host builds using clang toolchain (#2798) This PR adds the `CLANG_BUILD` for host builds. The toolchain detection logic is in the main build.mk file as there might be future support for clang toolchains for actual devices. It's also a revision to the existing logic which checks GCC compiler version. Try it out using `make SMING_SOC=host CLANG_BUILD=1`. To build with a specific (installed) version of clang, for example clang-15, use `CLANG_BUILD=15` . Further customisation can be made by editing `Sming/Arch/Host/build.mk`. Clang-tidy support (#2648) is also improved as there are some compiler flag differences between GCC and clang which are now shared between CLANG_TIDY and CLANG_BUILD operation. An extra CI build has been added using clang. Further to #2773, the default toolchain for macos is a version of clang (Apple Clang). This PR doesn't quite support that because there are other issues to address, but it's a step in the right direction. --- .github/workflows/ci.yml | 6 +++ .github/workflows/library.yml | 6 +++ Sming/Arch/Host/README.rst | 13 ++++++ Sming/Arch/Host/build.mk | 21 +++++++++ Sming/Libraries/DiskStorage | 2 +- Sming/Libraries/FlashIP | 2 +- Sming/Libraries/GoogleCast | 2 +- Sming/Libraries/Graphics | 2 +- Sming/Libraries/HueEmulator | 2 +- Sming/Libraries/RingTone | 2 +- Sming/Libraries/USB | 2 +- Sming/Libraries/jerryscript | 2 +- Sming/build.mk | 61 ++++++++++++++++++--------- Sming/component-wrapper.mk | 8 +--- Sming/project.mk | 1 + samples/Basic_Ota/app/application.cpp | 4 +- samples/LiveDebug/app/application.cpp | 4 +- 17 files changed, 102 insertions(+), 38 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f565d8a942..9d9fb17b35 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,11 +13,16 @@ jobs: matrix: os: [ubuntu-latest, windows-latest] variant: [esp8266, host, rp2040] + toolchain: [gcc] include: - variant: esp8266 arch: Esp8266 - variant: host arch: Host + - os: ubuntu-latest + variant: host + arch: Host + toolchain: clang - variant: rp2040 arch: Rp2040 @@ -30,6 +35,7 @@ jobs: env: SMING_ARCH: ${{ matrix.arch }} SMING_SOC: ${{ matrix.variant }} + CLANG_BUILD: ${{ matrix.toolchain == 'clang' && '15' || '0' }} steps: - name: Fix autocrlf setting diff --git a/.github/workflows/library.yml b/.github/workflows/library.yml index 44c6f6c4f8..7ae69a023b 100644 --- a/.github/workflows/library.yml +++ b/.github/workflows/library.yml @@ -24,11 +24,16 @@ jobs: os: [ubuntu-latest, windows-latest] variant: [esp8266, host, esp32, esp32s2, esp32c3, esp32s3, esp32c2, rp2040] idf_version: ["4.4", ""] # "" denotes default, currently 5.2 + toolchain: [gcc] include: - variant: esp8266 arch: Esp8266 - variant: host arch: Host + - os: ubuntu-latest + variant: host + arch: Host + toolchain: clang - variant: esp32 arch: Esp32 - variant: esp32s2 @@ -61,6 +66,7 @@ jobs: SMING_ARCH: ${{ matrix.arch }} SMING_SOC: ${{ matrix.variant }} INSTALL_IDF_VER: ${{ matrix.idf_version }} + CLANG_BUILD: ${{ matrix.toolchain == 'clang' && '15' || '0' }} steps: - name: Fix autocrlf setting diff --git a/Sming/Arch/Host/README.rst b/Sming/Arch/Host/README.rst index d4fc206122..92718875e0 100644 --- a/Sming/Arch/Host/README.rst +++ b/Sming/Arch/Host/README.rst @@ -25,6 +25,9 @@ Ensure you are using relatively recent compilers, with 32-bit libraries availabl For Linux, you may require the ``gcc-multilib`` and ``g++-multilib`` packages to build 32-bit executables on a 64-bit OS. +MacOS comes pre-installed with ``Apple Clang`` as the standard toolchain. +This should be sufficient to build Sming in Host mode. + For Windows, make sure your ``MinGW`` distro is up to date. See :doc:`/getting-started/windows/index` for further details. @@ -58,6 +61,16 @@ Configuration Note: These settings are not 'sticky' + +.. envvar:: CLANG_BUILD + + 0: Use GCC (default) + 1: Use standard ``clang`` + N: Use specific installed version, ``clang-N`` + + Note: This setting is not 'sticky' + + Components ---------- diff --git a/Sming/Arch/Host/build.mk b/Sming/Arch/Host/build.mk index ba2380bae1..2129b2e506 100644 --- a/Sming/Arch/Host/build.mk +++ b/Sming/Arch/Host/build.mk @@ -8,6 +8,25 @@ CPPFLAGS += -DARCH_HOST TOOLSPEC := +ifndef CLANG_BUILD +override CLANG_BUILD := 0 +endif + +ifneq ($(CLANG_BUILD),0) +ifeq ($(CLANG_BUILD),1) +CLANG_VER := +else +CLANG_VER := -$(CLANG_BUILD) +endif +AS := $(TOOLSPEC)clang$(CLANG_VER) +CC := $(TOOLSPEC)clang$(CLANG_VER) +CXX := $(TOOLSPEC)clang++$(CLANG_VER) +AR := $(TOOLSPEC)ar +LD := $(TOOLSPEC)clang++$(CLANG_VER) +NM := $(TOOLSPEC)nm +OBJCOPY := $(TOOLSPEC)objcopy +OBJDUMP := $(TOOLSPEC)objdump +else AS := $(TOOLSPEC)gcc CC := $(TOOLSPEC)gcc CXX := $(TOOLSPEC)g++ @@ -16,6 +35,8 @@ LD := $(TOOLSPEC)g++ NM := $(TOOLSPEC)nm OBJCOPY := $(TOOLSPEC)objcopy OBJDUMP := $(TOOLSPEC)objdump +endif + GDB := $(TOOLSPEC)gdb GCC_UPGRADE_URL := https://sming.readthedocs.io/en/latest/arch/host/host-emulator.html\#c-c-32-bit-compiler-and-libraries diff --git a/Sming/Libraries/DiskStorage b/Sming/Libraries/DiskStorage index 6bfcd80140..f545e91216 160000 --- a/Sming/Libraries/DiskStorage +++ b/Sming/Libraries/DiskStorage @@ -1 +1 @@ -Subproject commit 6bfcd80140f1436f6da90d6ab6a438903f8fa752 +Subproject commit f545e91216d2cc94153e29ef41de87bd1cde2e8c diff --git a/Sming/Libraries/FlashIP b/Sming/Libraries/FlashIP index 820d08aab8..1e67f9a47d 160000 --- a/Sming/Libraries/FlashIP +++ b/Sming/Libraries/FlashIP @@ -1 +1 @@ -Subproject commit 820d08aab817dde262d5d5f9fb453dca88b374e9 +Subproject commit 1e67f9a47d9e9f479c1b2c34d36800994515c8c2 diff --git a/Sming/Libraries/GoogleCast b/Sming/Libraries/GoogleCast index 83ad9974fd..4a95b75612 160000 --- a/Sming/Libraries/GoogleCast +++ b/Sming/Libraries/GoogleCast @@ -1 +1 @@ -Subproject commit 83ad9974fd80b048aec88bcc84a939f4cb0500bf +Subproject commit 4a95b756127799ef25697fa6bca28c35094eec52 diff --git a/Sming/Libraries/Graphics b/Sming/Libraries/Graphics index 1809db3740..89d315bdaf 160000 --- a/Sming/Libraries/Graphics +++ b/Sming/Libraries/Graphics @@ -1 +1 @@ -Subproject commit 1809db3740086700d895e1c0b3c5a7b632c4e210 +Subproject commit 89d315bdaf45ea9b75692a8f21e07376fa6d72ba diff --git a/Sming/Libraries/HueEmulator b/Sming/Libraries/HueEmulator index 9c275794b9..af80123b02 160000 --- a/Sming/Libraries/HueEmulator +++ b/Sming/Libraries/HueEmulator @@ -1 +1 @@ -Subproject commit 9c275794b958e68ea67ebfd974f9189dfd36b695 +Subproject commit af80123b02937df710fd515b989ea688c0ef2c7f diff --git a/Sming/Libraries/RingTone b/Sming/Libraries/RingTone index 27abfdae6e..12b438adb4 160000 --- a/Sming/Libraries/RingTone +++ b/Sming/Libraries/RingTone @@ -1 +1 @@ -Subproject commit 27abfdae6ea684646be347cbfbf757d9882cc0bb +Subproject commit 12b438adb417983263425dc2895e9cb0a8f96e69 diff --git a/Sming/Libraries/USB b/Sming/Libraries/USB index 2e3e524172..52424de587 160000 --- a/Sming/Libraries/USB +++ b/Sming/Libraries/USB @@ -1 +1 @@ -Subproject commit 2e3e524172505835bca270dc7f198034bab01a10 +Subproject commit 52424de587e636f36b2cdaa345e88de37075cddc diff --git a/Sming/Libraries/jerryscript b/Sming/Libraries/jerryscript index c8f24895a5..21ac3e053d 160000 --- a/Sming/Libraries/jerryscript +++ b/Sming/Libraries/jerryscript @@ -1 +1 @@ -Subproject commit c8f24895a59ff64e9bba212946dcf2dad504b27a +Subproject commit 21ac3e053d2a63ebc1955f2a62c2978c7447e119 diff --git a/Sming/build.mk b/Sming/build.mk index 8d482cceae..99b31cba90 100644 --- a/Sming/build.mk +++ b/Sming/build.mk @@ -30,6 +30,7 @@ ifdef CLANG_TIDY ifneq (Host,$(SMING_ARCH)) $(error CLANG_TIDY supported only for Host architecture.) endif +USE_CLANG := 1 endif export SMING_ARCH @@ -157,18 +158,10 @@ endif # Common C/C++ flags passed to user libraries CPPFLAGS = \ - -Wl,-EL \ -finline-functions \ -fdata-sections \ -ffunction-sections \ - -D_POSIX_C_SOURCE=200809L - -# Required to access peripheral registers using structs -# e.g. `uint32_t value: 8` sitting at a byte or word boundary will be 'optimised' to -# an 8-bit fetch/store instruction which will not work; it must be a full 32-bit access. -CPPFLAGS += -fstrict-volatile-bitfields - -CPPFLAGS += \ + -D_POSIX_C_SOURCE=200809L \ -Wall \ -Wpointer-arith \ -Wno-comment \ @@ -216,9 +209,41 @@ include $(ARCH_BASE)/build.mk ifndef MAKE_CLEAN -# Detect compiler version -DEBUG_VARS += GCC_VERSION -GCC_VERSION := $(shell $(CC) -dumpversion) +# Detect compiler version and name +DEBUG_VARS += COMPILER_VERSION_FULL COMPILER_VERSION COMPILER_NAME +COMPILER_VERSION_FULL := $(shell $(CC) -v 2>&1 | $(AWK) -F " version " '/ version /{ a=$$1; gsub(/ +/, "-", a); print a, $$2}') +COMPILER_NAME := $(word 1,$(COMPILER_VERSION_FULL)) +COMPILER_VERSION := $(word 2,$(COMPILER_VERSION_FULL)) + +ifndef USE_CLANG +# Required to access peripheral registers using structs +# e.g. `uint32_t value: 8` sitting at a byte or word boundary will be 'optimised' to +# an 8-bit fetch/store instruction which will not work; it must be a full 32-bit access. +ifeq ($(COMPILER_NAME),gcc) +CPPFLAGS += -fstrict-volatile-bitfields +COMPILER_VERSION_MIN := 8 +else +ifeq (,$(findstring clang,$(COMPILER_NAME))) +$(error Compiler '$(COMPILER_VERSION_FULL)' not recognised. Please install GCC tools.) +endif +COMPILER_VERSION_MIN := 15 +ifndef COMPILER_NOTICE_PRINTED +$(info Note: Building with $(COMPILER_NAME) $(COMPILER_VERSION).) +COMPILER_NOTICE_PRINTED := 1 +endif +USE_CLANG := 1 +endif +endif + +ifdef USE_CLANG +CPPFLAGS += \ + -Wno-vla-extension \ + -Wno-unused-private-field \ + -Wno-bitfield-constant-conversion \ + -Wno-unknown-pragmas \ + -Wno-initializer-overrides +endif + # Use c11 by default. Every architecture can override it DEBUG_VARS += SMING_C_STD @@ -227,21 +252,17 @@ CFLAGS += -std=$(SMING_C_STD) # Select C++17 if supported, defaulting to C++11 otherwise DEBUG_VARS += SMING_CXX_STD -ifeq ($(GCC_VERSION),4.8.5) -SMING_CXX_STD ?= c++11 -else SMING_CXX_STD ?= c++17 -endif CXXFLAGS += -std=$(SMING_CXX_STD) -GCC_MIN_MAJOR_VERSION := 8 -GCC_VERSION_COMPATIBLE := $(shell expr $$(echo $(GCC_VERSION) | cut -f1 -d.) \>= $(GCC_MIN_MAJOR_VERSION)) +COMPILER_VERSION_MAJOR := $(firstword $(subst ., ,$(COMPILER_VERSION))) +COMPILER_VERSION_COMPATIBLE := $(shell expr $(COMPILER_VERSION_MAJOR) \>= $(COMPILER_VERSION_MIN)) -ifeq ($(GCC_VERSION_COMPATIBLE),0) +ifeq ($(COMPILER_VERSION_COMPATIBLE),0) ifneq ($(GCC_UPGRADE_URL),) $(info Instructions for upgrading your compiler can be found here: $(GCC_UPGRADE_URL)) endif -$(error Please, upgrade your GCC compiler to version $(GCC_MIN_MAJOR_VERSION) or newer.) +$(error Please upgrade your compiler to $(COMPILER_NAME) $(COMPILER_VERSION_MIN) or newer) endif endif diff --git a/Sming/component-wrapper.mk b/Sming/component-wrapper.mk index 320399dda5..88aca58d41 100644 --- a/Sming/component-wrapper.mk +++ b/Sming/component-wrapper.mk @@ -120,10 +120,6 @@ endif # Additional flags to pass to clang CLANG_FLAG_EXTRA ?= -# Flags which clang doesn't recognise -CLANG_FLAG_UNKNOWN := \ - -fstrict-volatile-bitfields - # $1 -> absolute source directory, no trailing path separator # $2 -> relative output build directory, with trailing path separator define GenerateCompileTargets @@ -142,7 +138,7 @@ ifneq (,$(filter $1/%.c,$(SOURCE_FILES))) ifdef CLANG_TIDY $2%.o: $1/%.c $(vecho) "TIDY $$<" - $(Q) $(CLANG_TIDY) $$< -- $(addprefix -I,$(INCDIR)) $$(filter-out $(CLANG_FLAG_UNKNOWN),$(CPPFLAGS) $(CFLAGS)) $(CLANG_FLAG_EXTRA) + $(Q) $(CLANG_TIDY) $$< -- $(addprefix -I,$(INCDIR)) $(CPPFLAGS) $(CFLAGS) $(CLANG_FLAG_EXTRA) else $2%.o: $1/%.c $2%.c.d $(vecho) "CC $$<" @@ -156,7 +152,7 @@ ifneq (,$(filter $1/%.cpp,$(SOURCE_FILES))) ifdef CLANG_TIDY $2%.o: $1/%.cpp $(vecho) "TIDY $$<" - $(Q) $(CLANG_TIDY) $$< -- $(addprefix -I,$(INCDIR)) $$(filter-out $(CLANG_FLAG_UNKNOWN),$(CPPFLAGS) $(CXXFLAGS)) $(CLANG_FLAG_EXTRA) + $(Q) $(CLANG_TIDY) $$< -- $(addprefix -I,$(INCDIR)) $(CPPFLAGS) $(CXXFLAGS) $(CLANG_FLAG_EXTRA) else $2%.o: $1/%.cpp $2%.cpp.d $(vecho) "C+ $$<" diff --git a/Sming/project.mk b/Sming/project.mk index 412a56ff07..cf928ea1b0 100644 --- a/Sming/project.mk +++ b/Sming/project.mk @@ -100,6 +100,7 @@ LIBS := $(EXTRA_LIBS) # Common linker flags LDFLAGS = \ + -Wl,-EL \ -Wl,--gc-sections \ -Wl,-Map=$(basename $@).map diff --git a/samples/Basic_Ota/app/application.cpp b/samples/Basic_Ota/app/application.cpp index 51770d421c..c0a8bc3515 100644 --- a/samples/Basic_Ota/app/application.cpp +++ b/samples/Basic_Ota/app/application.cpp @@ -218,14 +218,14 @@ void handleCommand(const String& str) void serialCallBack(Stream& stream, char arrivedChar, unsigned short availableCharsCount) { switch(commandBuffer.process(stream, Serial)) { - case commandBuffer.Action::submit: + case LineBufferBase::Action::submit: if(commandBuffer) { handleCommand(String(commandBuffer)); commandBuffer.clear(); } showPrompt(); break; - case commandBuffer.Action::clear: + case LineBufferBase::Action::clear: showPrompt(); break; default:; diff --git a/samples/LiveDebug/app/application.cpp b/samples/LiveDebug/app/application.cpp index 146ec9affc..918b849ca0 100644 --- a/samples/LiveDebug/app/application.cpp +++ b/samples/LiveDebug/app/application.cpp @@ -123,10 +123,10 @@ void onDataReceived(Stream& source, char arrivedChar, unsigned short availableCh } switch(commandBuffer.process(source, Serial)) { - case commandBuffer.Action::clear: + case LineBufferBase::Action::clear: showPrompt(); break; - case commandBuffer.Action::submit: { + case LineBufferBase::Action::submit: { if(commandBuffer) { handleCommand(String(commandBuffer)); commandBuffer.clear(); From 666c0c25562822ec5f607e45e55ab87f0844b7cd Mon Sep 17 00:00:00 2001 From: Mike Date: Wed, 12 Jun 2024 10:45:23 +0100 Subject: [PATCH 074/128] Change SparkFun_APDS9960 to submodule (#2800) Submodule allows easier maintenance and tracking of upstream changes. Only one function change, addition of `getStatusRegister` function. Also patch fixing issue flagged by clang build: ``` /home/runner/work/Sming/Sming/Sming/Libraries/SparkFun_APDS9960/SparkFun_APDS9960.cpp:508:32: warning: result of comparison of constant -1 with expression of type 'uint8_t' (aka 'unsigned char') is always false [-Wtautological-constant-out-of-range-compare] if( bytes_read == -1 ) { ~~~~~~~~~~ ^ ~~ ``` --- .gitmodules | 4 + .../.patches/SparkFun_APDS9960.patch | 29 + .../.patches/SparkFun_APDS9960/component.mk | 2 + Sming/Libraries/SparkFun_APDS9960 | 1 + Sming/Libraries/SparkFun_APDS9960/README.md | 82 - .../SparkFun_APDS9960/SparkFun_APDS9960.cpp | 2216 ----------------- .../SparkFun_APDS9960/SparkFun_APDS9960.h | 347 --- samples/Gesture_APDS-9960/app/application.cpp | 2 +- 8 files changed, 37 insertions(+), 2646 deletions(-) create mode 100644 Sming/Libraries/.patches/SparkFun_APDS9960.patch create mode 100644 Sming/Libraries/.patches/SparkFun_APDS9960/component.mk create mode 160000 Sming/Libraries/SparkFun_APDS9960 delete mode 100644 Sming/Libraries/SparkFun_APDS9960/README.md delete mode 100644 Sming/Libraries/SparkFun_APDS9960/SparkFun_APDS9960.cpp delete mode 100644 Sming/Libraries/SparkFun_APDS9960/SparkFun_APDS9960.h diff --git a/.gitmodules b/.gitmodules index 4205c4fd6d..9d62de59ac 100644 --- a/.gitmodules +++ b/.gitmodules @@ -349,6 +349,10 @@ path = Sming/Libraries/SolarCalculator url = https://github.com/mikee47/SolarCalculator ignore = dirty +[submodule "Libraries.SparkFun_APDS9960"] + path = Sming/Libraries/SparkFun_APDS9960 + url = https://github.com/sparkfun/SparkFun_APDS-9960_Sensor_Arduino_Library + ignore = dirty [submodule "Libraries.spiffs"] path = Sming/Libraries/Spiffs/spiffs url = https://github.com/pellepl/spiffs.git diff --git a/Sming/Libraries/.patches/SparkFun_APDS9960.patch b/Sming/Libraries/.patches/SparkFun_APDS9960.patch new file mode 100644 index 0000000000..eaa5d0ec7a --- /dev/null +++ b/Sming/Libraries/.patches/SparkFun_APDS9960.patch @@ -0,0 +1,29 @@ +diff --git a/src/SparkFun_APDS9960.cpp b/src/SparkFun_APDS9960.cpp +index 530c1f6..25968d0 100644 +--- a/src/SparkFun_APDS9960.cpp ++++ b/src/SparkFun_APDS9960.cpp +@@ -163,7 +163,7 @@ bool SparkFun_APDS9960::init() + return false; + } + +-#if 0 ++#if DEBUG + /* Gesture config register dump */ + uint8_t reg; + uint8_t val; +@@ -518,12 +518,13 @@ int SparkFun_APDS9960::readGesture() + + /* If there's stuff in the FIFO, read it into our data block */ + if( fifo_level > 0) { +- bytes_read = wireReadDataBlock( APDS9960_GFIFO_U, ++ int res = wireReadDataBlock( APDS9960_GFIFO_U, + (uint8_t*)fifo_data, + (fifo_level * 4) ); +- if( bytes_read == -1 ) { ++ if( res == -1 ) { + return ERROR; + } ++ bytes_read = res; + #if DEBUG + Serial.print("FIFO Dump: "); + for ( i = 0; i < bytes_read; i++ ) { diff --git a/Sming/Libraries/.patches/SparkFun_APDS9960/component.mk b/Sming/Libraries/.patches/SparkFun_APDS9960/component.mk new file mode 100644 index 0000000000..379e0c2aa9 --- /dev/null +++ b/Sming/Libraries/.patches/SparkFun_APDS9960/component.mk @@ -0,0 +1,2 @@ +COMPONENT_INCDIRS := src +COMPONENT_SRCDIRS := src diff --git a/Sming/Libraries/SparkFun_APDS9960 b/Sming/Libraries/SparkFun_APDS9960 new file mode 160000 index 0000000000..bb9633dd1e --- /dev/null +++ b/Sming/Libraries/SparkFun_APDS9960 @@ -0,0 +1 @@ +Subproject commit bb9633dd1ea27c64164d3405b936caa287b7c83f diff --git a/Sming/Libraries/SparkFun_APDS9960/README.md b/Sming/Libraries/SparkFun_APDS9960/README.md deleted file mode 100644 index b5d3fb6541..0000000000 --- a/Sming/Libraries/SparkFun_APDS9960/README.md +++ /dev/null @@ -1,82 +0,0 @@ -SparkFun APDS9960 RGB and Gesture Sensor Arduino Library -========================================================= - -![Avago APDS-9960 Breakout Board - SEN-12787 ](https://cdn.sparkfun.com/r/92-92/assets/parts/9/6/0/3/12787-01.jpg) - -[*Avago APDS-9960 Breakout Board (SEN-12787)*](https://www.sparkfun.com/products/12787) - -Getting Started ---------------- - -* Download the Git repository as a ZIP ("Download ZIP" button) -* Unzip -* Copy the entire library directory (APDS-9960_RGB_and_Gesture_Sensor_Arduino_Library -) to \/libraries -* Open the Arduino program -* Select File -> Examples -> SparkFun_APDS9960 -> GestureTest -* Plug in your Arduino and APDS-9960 with the following connections - -*-OR-* - -* Use the library manager - -| Arduino Pin | APDS-9960 Board | Function | -|---|---|---| -| 3.3V | VCC | Power | -| GND | GND | Ground | -| A4 | SDA | I2C Data | -| A5 | SCL | I2C Clock | -| 2 | INT | Interrupt | - -* Go to Tools -> Board and select your Arduino board -* Go to Tools -> Serial Port and select the COM port of your Arduino board -* Click "Upload" -* Go to Tools -> Serial Monitor -* Ensure the baud rate is set at 9600 baud -* Swipe your hand over the sensor in various directions! - -Repository Contents -------------------- - -* **/examples** - Example sketches for the library (.ino). Run these from the Arduino IDE. -* **/src** - Source files for the library (.cpp, .h). -* **library.properties** - General library properties for the Arduino package manager. - -Documentation --------------- - -* **[Installing an Arduino Library Guide](https://learn.sparkfun.com/tutorials/installing-an-arduino-library)** - Basic information on how to install an Arduino library. -* **[Product Repository](https://github.com/sparkfun/APDS-9960_RGB_and_Gesture_Sensor)** - Main repository (including hardware files) for the SparkFun_APDS9960 RGB and Gesture Sensor. -* **[Hookup Guide](https://learn.sparkfun.com/tutorials/apds-9960-rgb-and-gesture-sensor-hookup-guide)** - Basic hookup guide for the sensor. - -Products that use this Library ---------------------------------- - -* [SEN-12787](https://www.sparkfun.com/products/12787)- Avago APDS-9960 - -Version History ---------------- -* [V_1.4.1](https://github.com/sparkfun/SparkFun_APDS-9960_Sensor_Arduino_Library/tree/V_1.4.1) - Removing blank files, updating library.properties file. -* [V_1.4.0](https://github.com/sparkfun/APDS-9960_RGB_and_Gesture_Sensor_Arduino_Library/tree/V_1.4.0) - Updated to new library structure -* V_1.3.0 - Implemented disableProximitySensor(). Thanks to jmg5150 for catching that! -* V_1.2.0 - Added pinMode line to GestureTest demo to fix interrupt bug with some Arduinos -* V_1.1.0 - Updated GestureTest demo to not freeze with fast swipes -* V_1.0.0: Initial release -* Ambient and RGB light sensing implemented -* Ambient light interrupts working -* Proximity sensing implemented -* Proximity interrupts working -* Gesture (UP, DOWN, LEFT, RIGHT, NEAR, FAR) sensing implemented - -License Information -------------------- - -This product is _**open source**_! - -The **code** is beerware; if you see me (or any other SparkFun employee) at the local, and you've found our code helpful, please buy us a round! - -Please use, reuse, and modify these files as you see fit. Please maintain attribution to SparkFun Electronics and release anything derivative under the same license. - -Distributed as-is; no warranty is given. - -- Your friends at SparkFun. diff --git a/Sming/Libraries/SparkFun_APDS9960/SparkFun_APDS9960.cpp b/Sming/Libraries/SparkFun_APDS9960/SparkFun_APDS9960.cpp deleted file mode 100644 index 8d4fb67754..0000000000 --- a/Sming/Libraries/SparkFun_APDS9960/SparkFun_APDS9960.cpp +++ /dev/null @@ -1,2216 +0,0 @@ -/** - * @file SparkFun_APDS-9960.cpp - * @brief Library for the SparkFun APDS-9960 breakout board - * @author Shawn Hymel (SparkFun Electronics) - * - * @copyright This code is public domain but you buy me a beer if you use - * this and we meet someday (Beerware license). - * - * This library interfaces the Avago APDS-9960 to Arduino over I2C. The library - * relies on the Arduino Wire (I2C) library. to use the library, instantiate an - * APDS9960 object, call init(), and call the appropriate functions. - * - * APDS-9960 current draw tests (default parameters): - * Off: 1mA - * Waiting for gesture: 14mA - * Gesture in progress: 35mA - */ - - #include - #include "Wire.h" - - #include "SparkFun_APDS9960.h" - -/** - * @brief Constructor - Instantiates SparkFun_APDS9960 object - */ -SparkFun_APDS9960::SparkFun_APDS9960() -{ - gesture_ud_delta_ = 0; - gesture_lr_delta_ = 0; - - gesture_ud_count_ = 0; - gesture_lr_count_ = 0; - - gesture_near_count_ = 0; - gesture_far_count_ = 0; - - gesture_state_ = 0; - gesture_motion_ = DIR_NONE; -} - -/** - * @brief Destructor - */ -SparkFun_APDS9960::~SparkFun_APDS9960() -{ - -} - -/** - * @brief Configures I2C communications and initializes registers to defaults - * - * @return True if initialized successfully. False otherwise. - */ -bool SparkFun_APDS9960::init() -{ - uint8_t id; - - /* Initialize I2C */ - Wire.begin(); - - /* Read ID register and check against known values for APDS-9960 */ - if( !wireReadDataByte(APDS9960_ID, id) ) { - return false; - } - if( !(id == APDS9960_ID_1 || id == APDS9960_ID_2) ) { - return false; - } - - /* Set ENABLE register to 0 (disable all features) */ - if( !setMode(ALL, OFF) ) { - return false; - } - - /* Set default values for ambient light and proximity registers */ - if( !wireWriteDataByte(APDS9960_ATIME, DEFAULT_ATIME) ) { - return false; - } - if( !wireWriteDataByte(APDS9960_WTIME, DEFAULT_WTIME) ) { - return false; - } - if( !wireWriteDataByte(APDS9960_PPULSE, DEFAULT_PROX_PPULSE) ) { - return false; - } - if( !wireWriteDataByte(APDS9960_POFFSET_UR, DEFAULT_POFFSET_UR) ) { - return false; - } - if( !wireWriteDataByte(APDS9960_POFFSET_DL, DEFAULT_POFFSET_DL) ) { - return false; - } - if( !wireWriteDataByte(APDS9960_CONFIG1, DEFAULT_CONFIG1) ) { - return false; - } - if( !setLEDDrive(DEFAULT_LDRIVE) ) { - return false; - } - if( !setProximityGain(DEFAULT_PGAIN) ) { - return false; - } - if( !setAmbientLightGain(DEFAULT_AGAIN) ) { - return false; - } - if( !setProxIntLowThresh(DEFAULT_PILT) ) { - return false; - } - if( !setProxIntHighThresh(DEFAULT_PIHT) ) { - return false; - } - if( !setLightIntLowThreshold(DEFAULT_AILT) ) { - return false; - } - if( !setLightIntHighThreshold(DEFAULT_AIHT) ) { - return false; - } - if( !wireWriteDataByte(APDS9960_PERS, DEFAULT_PERS) ) { - return false; - } - if( !wireWriteDataByte(APDS9960_CONFIG2, DEFAULT_CONFIG2) ) { - return false; - } - if( !wireWriteDataByte(APDS9960_CONFIG3, DEFAULT_CONFIG3) ) { - return false; - } - - /* Set default values for gesture sense registers */ - if( !setGestureEnterThresh(DEFAULT_GPENTH) ) { - return false; - } - if( !setGestureExitThresh(DEFAULT_GEXTH) ) { - return false; - } - if( !wireWriteDataByte(APDS9960_GCONF1, DEFAULT_GCONF1) ) { - return false; - } - if( !setGestureGain(DEFAULT_GGAIN) ) { - return false; - } - if( !setGestureLEDDrive(DEFAULT_GLDRIVE) ) { - return false; - } - if( !setGestureWaitTime(DEFAULT_GWTIME) ) { - return false; - } - if( !wireWriteDataByte(APDS9960_GOFFSET_U, DEFAULT_GOFFSET) ) { - return false; - } - if( !wireWriteDataByte(APDS9960_GOFFSET_D, DEFAULT_GOFFSET) ) { - return false; - } - if( !wireWriteDataByte(APDS9960_GOFFSET_L, DEFAULT_GOFFSET) ) { - return false; - } - if( !wireWriteDataByte(APDS9960_GOFFSET_R, DEFAULT_GOFFSET) ) { - return false; - } - if( !wireWriteDataByte(APDS9960_GPULSE, DEFAULT_GPULSE) ) { - return false; - } - if( !wireWriteDataByte(APDS9960_GCONF3, DEFAULT_GCONF3) ) { - return false; - } - if( !setGestureIntEnable(DEFAULT_GIEN) ) { - return false; - } - -#if DEBUG - /* Gesture config register dump */ - uint8_t reg; - uint8_t val; - - for(reg = 0x80; reg <= 0xAF; reg++) { - if( (reg != 0x82) && \ - (reg != 0x8A) && \ - (reg != 0x91) && \ - (reg != 0xA8) && \ - (reg != 0xAC) && \ - (reg != 0xAD) ) - { - wireReadDataByte(reg, val); - Serial.print(reg, HEX); - Serial.print(": 0x"); - Serial.println(val, HEX); - } - } - for(reg = 0xE4; reg <= 0xE7; reg++) { - wireReadDataByte(reg, val); - Serial.print(reg, HEX); - Serial.print(": 0x"); - Serial.println(val, HEX); - } -#endif - - return true; -} - -/******************************************************************************* - * Public methods for controlling the APDS-9960 - ******************************************************************************/ - -/** - * @brief Reads and returns the contents of the ENABLE register - * - * @return Contents of the ENABLE register. 0xFF if error. - */ -uint8_t SparkFun_APDS9960::getMode() -{ - uint8_t enable_value; - - /* Read current ENABLE register */ - if( !wireReadDataByte(APDS9960_ENABLE, enable_value) ) { - return ERROR; - } - - return enable_value; -} - -/** - * @brief Enables or disables a feature in the APDS-9960 - * - * @param[in] mode which feature to enable - * @param[in] enable ON (1) or OFF (0) - * @return True if operation success. False otherwise. - */ -bool SparkFun_APDS9960::setMode(uint8_t mode, uint8_t enable) -{ - uint8_t reg_val; - - /* Read current ENABLE register */ - reg_val = getMode(); - if( reg_val == ERROR ) { - Serial.printf("Error: Read current ENABLE register"); - return false; - } - - /* Change bit(s) in ENABLE register */ - enable = enable & 0x01; - if( mode >= 0 && mode <= 6 ) { - if (enable) { - reg_val |= (1 << mode); - } else { - reg_val &= ~(1 << mode); - } - } else if( mode == ALL ) { - if (enable) { - reg_val = 0x7F; - } else { - reg_val = 0x00; - } - } - - /* Write value back to ENABLE register */ - if( !wireWriteDataByte(APDS9960_ENABLE, reg_val) ) { - Serial.printf("Error: Write value back to ENABLE registerr"); - return false; - } - - return true; -} - -/** - * @brief Starts the light (R/G/B/Ambient) sensor on the APDS-9960 - * - * @param[in] interrupts true to enable hardware interrupt on high or low light - * @return True if sensor enabled correctly. False on error. - */ -bool SparkFun_APDS9960::enableLightSensor(bool interrupts) -{ - - /* Set default gain, interrupts, enable power, and enable sensor */ - if( !setAmbientLightGain(DEFAULT_AGAIN) ) { - return false; - } - if( interrupts ) { - if( !setAmbientLightIntEnable(1) ) { - return false; - } - } else { - if( !setAmbientLightIntEnable(0) ) { - return false; - } - } - if( !enablePower() ){ - return false; - } - if( !setMode(AMBIENT_LIGHT, 1) ) { - return false; - } - - return true; - -} - -/** - * @brief Ends the light sensor on the APDS-9960 - * - * @return True if sensor disabled correctly. False on error. - */ -bool SparkFun_APDS9960::disableLightSensor() -{ - if( !setAmbientLightIntEnable(0) ) { - return false; - } - if( !setMode(AMBIENT_LIGHT, 0) ) { - return false; - } - - return true; -} - -/** - * @brief Starts the proximity sensor on the APDS-9960 - * - * @param[in] interrupts true to enable hardware external interrupt on proximity - * @return True if sensor enabled correctly. False on error. - */ -bool SparkFun_APDS9960::enableProximitySensor(bool interrupts) -{ - /* Set default gain, LED, interrupts, enable power, and enable sensor */ - if( !setProximityGain(DEFAULT_PGAIN) ) { - return false; - } - if( !setLEDDrive(DEFAULT_LDRIVE) ) { - return false; - } - if( interrupts ) { - if( !setProximityIntEnable(1) ) { - return false; - } - } else { - if( !setProximityIntEnable(0) ) { - return false; - } - } - if( !enablePower() ){ - return false; - } - if( !setMode(PROXIMITY, 1) ) { - return false; - } - - return true; -} - -/** - * @brief Ends the proximity sensor on the APDS-9960 - * - * @return True if sensor disabled correctly. False on error. - */ -bool SparkFun_APDS9960::disableProximitySensor() -{ - if( !setProximityIntEnable(0) ) { - return false; - } - if( !setMode(PROXIMITY, 0) ) { - return false; - } - - return true; -} - -/** - * @brief Starts the gesture recognition engine on the APDS-9960 - * - * @param[in] interrupts true to enable hardware external interrupt on gesture - * @return True if engine enabled correctly. False on error. - */ -bool SparkFun_APDS9960::enableGestureSensor(bool interrupts) -{ - - /* Enable gesture mode - Set ENABLE to 0 (power off) - Set WTIME to 0xFF - Set AUX to LED_BOOST_300 - Enable PON, WEN, PEN, GEN in ENABLE - */ - resetGestureParameters(); - if( !wireWriteDataByte(APDS9960_WTIME, 0xFF) ) { - return false; - } - if( !wireWriteDataByte(APDS9960_PPULSE, DEFAULT_GESTURE_PPULSE) ) { - return false; - } - if( !setLEDBoost(LED_BOOST_300) ) { - return false; - } - if( interrupts ) { - if( !setGestureIntEnable(1) ) { - return false; - } - } else { - if( !setGestureIntEnable(0) ) { - return false; - } - } - if( !setGestureMode(1) ) { - return false; - } - if( !enablePower() ){ - return false; - } - if( !setMode(WAIT, 1) ) { - return false; - } - if( !setMode(PROXIMITY, 1) ) { - return false; - } - if( !setMode(GESTURE, 1) ) { - return false; - } - - return true; -} - -/** - * @brief Ends the gesture recognition engine on the APDS-9960 - * - * @return True if engine disabled correctly. False on error. - */ -bool SparkFun_APDS9960::disableGestureSensor() -{ - resetGestureParameters(); - if( !setGestureIntEnable(0) ) { - return false; - } - if( !setGestureMode(0) ) { - return false; - } - if( !setMode(GESTURE, 0) ) { - return false; - } - - return true; -} - -/** - * @brief Determines if there is a gesture available for reading - * - * @return True if gesture available. False otherwise. - */ -bool SparkFun_APDS9960::isGestureAvailable() -{ - uint8_t val; - - /* Read value from GSTATUS register */ - if( !wireReadDataByte(APDS9960_GSTATUS, val) ) { - return ERROR; - } - - /* Shift and mask out GVALID bit */ - val &= APDS9960_GVALID; - - /* Return true/false based on GVALID bit */ - if( val == 1) { - return true; - } else { - return false; - } -} - -/** - * @brief Processes a gesture event and returns best guessed gesture - * - * @return Number corresponding to gesture. -1 on error. - */ -int SparkFun_APDS9960::readGesture() -{ - uint8_t fifo_level = 0; - uint8_t bytes_read = 0; - uint8_t fifo_data[128]; - uint8_t gstatus; - int motion; - int i; - - /* Make sure that power and gesture is on and data is valid */ - if( !isGestureAvailable() || !(getMode() & 0b01000001) ) { - return DIR_NONE; - } - - /* Keep looping as long as gesture data is valid */ - while(1) { - - /* Wait some time to collect next batch of FIFO data */ - delay(FIFO_PAUSE_TIME); - - /* Get the contents of the STATUS register. Is data still valid? */ - if( !wireReadDataByte(APDS9960_GSTATUS, gstatus) ) { - return ERROR; - } - - /* If we have valid data, read in FIFO */ - if( (gstatus & APDS9960_GVALID) == APDS9960_GVALID ) { - - /* Read the current FIFO level */ - if( !wireReadDataByte(APDS9960_GFLVL, fifo_level) ) { - return ERROR; - } - -#if DEBUG - Serial.print("FIFO Level: "); - Serial.println(fifo_level); -#endif - - /* If there's stuff in the FIFO, read it into our data block */ - if( fifo_level > 0) { - bytes_read = wireReadDataBlock( APDS9960_GFIFO_U, - (uint8_t*)fifo_data, - (fifo_level * 4) ); - if( bytes_read == -1 ) { - return ERROR; - } -#if DEBUG - Serial.print("FIFO Dump: "); - for ( i = 0; i < bytes_read; i++ ) { - Serial.print(fifo_data[i]); - Serial.print(" "); - } - Serial.println(); -#endif - - /* If at least 1 set of data, sort the data into U/D/L/R */ - if( bytes_read >= 4 ) { - for( i = 0; i < bytes_read; i += 4 ) { - gesture_data_.u_data[gesture_data_.index] = \ - fifo_data[i + 0]; - gesture_data_.d_data[gesture_data_.index] = \ - fifo_data[i + 1]; - gesture_data_.l_data[gesture_data_.index] = \ - fifo_data[i + 2]; - gesture_data_.r_data[gesture_data_.index] = \ - fifo_data[i + 3]; - gesture_data_.index++; - gesture_data_.total_gestures++; - } - -#if DEBUG - Serial.print("Up Data: "); - for ( i = 0; i < gesture_data_.total_gestures; i++ ) { - Serial.print(gesture_data_.u_data[i]); - Serial.print(" "); - } - Serial.println(); -#endif - - /* Filter and process gesture data. Decode near/far state */ - if( processGestureData() ) { - if( decodeGesture() ) { - //***TODO: U-Turn Gestures -#if DEBUG - //Serial.println(gesture_motion_); -#endif - } - } - - /* Reset data */ - gesture_data_.index = 0; - gesture_data_.total_gestures = 0; - } - } - } else { - - /* Determine best guessed gesture and clean up */ - delay(FIFO_PAUSE_TIME); - decodeGesture(); - motion = gesture_motion_; -#if DEBUG - Serial.print("END: "); - Serial.println(gesture_motion_); -#endif - resetGestureParameters(); - return motion; - } - } -} - -/** - * Turn the APDS-9960 on - * - * @return True if operation successful. False otherwise. - */ -bool SparkFun_APDS9960::enablePower() -{ - if( !setMode(POWER, 1) ) { - return false; - } - - return true; -} - -/** - * Turn the APDS-9960 off - * - * @return True if operation successful. False otherwise. - */ -bool SparkFun_APDS9960::disablePower() -{ - if( !setMode(POWER, 0) ) { - return false; - } - - return true; -} - -/******************************************************************************* - * Ambient light and color sensor controls - ******************************************************************************/ - -/** - * @brief Reads the ambient (clear) light level as a 16-bit value - * - * @param[out] val value of the light sensor. - * @return True if operation successful. False otherwise. - */ -bool SparkFun_APDS9960::readAmbientLight(uint16_t &val) -{ - uint8_t val_byte; - val = 0; - - /* Read value from clear channel, low byte register */ - if( !wireReadDataByte(APDS9960_CDATAL, val_byte) ) { - return false; - } - val = val_byte; - - /* Read value from clear channel, high byte register */ - if( !wireReadDataByte(APDS9960_CDATAH, val_byte) ) { - return false; - } - val = val + ((uint16_t)val_byte << 8); - - return true; -} - -/** - * @brief Reads the red light level as a 16-bit value - * - * @param[out] val value of the light sensor. - * @return True if operation successful. False otherwise. - */ -bool SparkFun_APDS9960::readRedLight(uint16_t &val) -{ - uint8_t val_byte; - val = 0; - - /* Read value from clear channel, low byte register */ - if( !wireReadDataByte(APDS9960_RDATAL, val_byte) ) { - return false; - } - val = val_byte; - - /* Read value from clear channel, high byte register */ - if( !wireReadDataByte(APDS9960_RDATAH, val_byte) ) { - return false; - } - val = val + ((uint16_t)val_byte << 8); - - return true; -} - -/** - * @brief Reads the green light level as a 16-bit value - * - * @param[out] val value of the light sensor. - * @return True if operation successful. False otherwise. - */ -bool SparkFun_APDS9960::readGreenLight(uint16_t &val) -{ - uint8_t val_byte; - val = 0; - - /* Read value from clear channel, low byte register */ - if( !wireReadDataByte(APDS9960_GDATAL, val_byte) ) { - return false; - } - val = val_byte; - - /* Read value from clear channel, high byte register */ - if( !wireReadDataByte(APDS9960_GDATAH, val_byte) ) { - return false; - } - val = val + ((uint16_t)val_byte << 8); - - return true; -} - -/** - * @brief Reads the red light level as a 16-bit value - * - * @param[out] val value of the light sensor. - * @return True if operation successful. False otherwise. - */ -bool SparkFun_APDS9960::readBlueLight(uint16_t &val) -{ - uint8_t val_byte; - val = 0; - - /* Read value from clear channel, low byte register */ - if( !wireReadDataByte(APDS9960_BDATAL, val_byte) ) { - return false; - } - val = val_byte; - - /* Read value from clear channel, high byte register */ - if( !wireReadDataByte(APDS9960_BDATAH, val_byte) ) { - return false; - } - val = val + ((uint16_t)val_byte << 8); - - return true; -} - -/******************************************************************************* - * Proximity sensor controls - ******************************************************************************/ - -/** - * @brief Reads the proximity level as an 8-bit value - * - * @param[out] val value of the proximity sensor. - * @return True if operation successful. False otherwise. - */ -bool SparkFun_APDS9960::readProximity(uint8_t &val) -{ - val = 0; - - /* Read value from proximity data register */ - if( !wireReadDataByte(APDS9960_PDATA, val) ) { - return false; - } - - return true; -} - -/******************************************************************************* - * High-level gesture controls - ******************************************************************************/ - -/** - * @brief Resets all the parameters in the gesture data member - */ -void SparkFun_APDS9960::resetGestureParameters() -{ - gesture_data_.index = 0; - gesture_data_.total_gestures = 0; - - gesture_ud_delta_ = 0; - gesture_lr_delta_ = 0; - - gesture_ud_count_ = 0; - gesture_lr_count_ = 0; - - gesture_near_count_ = 0; - gesture_far_count_ = 0; - - gesture_state_ = 0; - gesture_motion_ = DIR_NONE; -} - -/** - * @brief Processes the raw gesture data to determine swipe direction - * - * @return True if near or far state seen. False otherwise. - */ -bool SparkFun_APDS9960::processGestureData() -{ - uint8_t u_first = 0; - uint8_t d_first = 0; - uint8_t l_first = 0; - uint8_t r_first = 0; - uint8_t u_last = 0; - uint8_t d_last = 0; - uint8_t l_last = 0; - uint8_t r_last = 0; - int ud_ratio_first; - int lr_ratio_first; - int ud_ratio_last; - int lr_ratio_last; - int ud_delta; - int lr_delta; - int i; - - /* If we have less than 4 total gestures, that's not enough */ - if( gesture_data_.total_gestures <= 4 ) { - return false; - } - - /* Check to make sure our data isn't out of bounds */ - if( (gesture_data_.total_gestures <= 32) && \ - (gesture_data_.total_gestures > 0) ) { - - /* Find the first value in U/D/L/R above the threshold */ - for( i = 0; i < gesture_data_.total_gestures; i++ ) { - if( (gesture_data_.u_data[i] > GESTURE_THRESHOLD_OUT) && - (gesture_data_.d_data[i] > GESTURE_THRESHOLD_OUT) && - (gesture_data_.l_data[i] > GESTURE_THRESHOLD_OUT) && - (gesture_data_.r_data[i] > GESTURE_THRESHOLD_OUT) ) { - - u_first = gesture_data_.u_data[i]; - d_first = gesture_data_.d_data[i]; - l_first = gesture_data_.l_data[i]; - r_first = gesture_data_.r_data[i]; - break; - } - } - - /* If one of the _first values is 0, then there is no good data */ - if( (u_first == 0) || (d_first == 0) || \ - (l_first == 0) || (r_first == 0) ) { - - return false; - } - /* Find the last value in U/D/L/R above the threshold */ - for( i = gesture_data_.total_gestures - 1; i >= 0; i-- ) { -#if DEBUG - Serial.print(F("Finding last: ")); - Serial.print(F("U:")); - Serial.print(gesture_data_.u_data[i]); - Serial.print(F(" D:")); - Serial.print(gesture_data_.d_data[i]); - Serial.print(F(" L:")); - Serial.print(gesture_data_.l_data[i]); - Serial.print(F(" R:")); - Serial.println(gesture_data_.r_data[i]); -#endif - if( (gesture_data_.u_data[i] > GESTURE_THRESHOLD_OUT) && - (gesture_data_.d_data[i] > GESTURE_THRESHOLD_OUT) && - (gesture_data_.l_data[i] > GESTURE_THRESHOLD_OUT) && - (gesture_data_.r_data[i] > GESTURE_THRESHOLD_OUT) ) { - - u_last = gesture_data_.u_data[i]; - d_last = gesture_data_.d_data[i]; - l_last = gesture_data_.l_data[i]; - r_last = gesture_data_.r_data[i]; - break; - } - } - } - - /* Calculate the first vs. last ratio of up/down and left/right */ - ud_ratio_first = ((u_first - d_first) * 100) / (u_first + d_first); - lr_ratio_first = ((l_first - r_first) * 100) / (l_first + r_first); - ud_ratio_last = ((u_last - d_last) * 100) / (u_last + d_last); - lr_ratio_last = ((l_last - r_last) * 100) / (l_last + r_last); - -#if DEBUG - Serial.print(F("Last Values: ")); - Serial.print(F("U:")); - Serial.print(u_last); - Serial.print(F(" D:")); - Serial.print(d_last); - Serial.print(F(" L:")); - Serial.print(l_last); - Serial.print(F(" R:")); - Serial.println(r_last); - - Serial.print(F("Ratios: ")); - Serial.print(F("UD Fi: ")); - Serial.print(ud_ratio_first); - Serial.print(F(" UD La: ")); - Serial.print(ud_ratio_last); - Serial.print(F(" LR Fi: ")); - Serial.print(lr_ratio_first); - Serial.print(F(" LR La: ")); - Serial.println(lr_ratio_last); -#endif - - /* Determine the difference between the first and last ratios */ - ud_delta = ud_ratio_last - ud_ratio_first; - lr_delta = lr_ratio_last - lr_ratio_first; - -#if DEBUG - Serial.print("Deltas: "); - Serial.print("UD: "); - Serial.print(ud_delta); - Serial.print(" LR: "); - Serial.println(lr_delta); -#endif - - /* Accumulate the UD and LR delta values */ - gesture_ud_delta_ += ud_delta; - gesture_lr_delta_ += lr_delta; - -#if DEBUG - Serial.print("Accumulations: "); - Serial.print("UD: "); - Serial.print(gesture_ud_delta_); - Serial.print(" LR: "); - Serial.println(gesture_lr_delta_); -#endif - - /* Determine U/D gesture */ - if( gesture_ud_delta_ >= GESTURE_SENSITIVITY_1 ) { - gesture_ud_count_ = 1; - } else if( gesture_ud_delta_ <= -GESTURE_SENSITIVITY_1 ) { - gesture_ud_count_ = -1; - } else { - gesture_ud_count_ = 0; - } - - /* Determine L/R gesture */ - if( gesture_lr_delta_ >= GESTURE_SENSITIVITY_1 ) { - gesture_lr_count_ = 1; - } else if( gesture_lr_delta_ <= -GESTURE_SENSITIVITY_1 ) { - gesture_lr_count_ = -1; - } else { - gesture_lr_count_ = 0; - } - - /* Determine Near/Far gesture */ - if( (gesture_ud_count_ == 0) && (gesture_lr_count_ == 0) ) { - if( (abs(ud_delta) < GESTURE_SENSITIVITY_2) && \ - (abs(lr_delta) < GESTURE_SENSITIVITY_2) ) { - - if( (ud_delta == 0) && (lr_delta == 0) ) { - gesture_near_count_++; - } else if( (ud_delta != 0) || (lr_delta != 0) ) { - gesture_far_count_++; - } - - if( (gesture_near_count_ >= 10) && (gesture_far_count_ >= 2) ) { - if( (ud_delta == 0) && (lr_delta == 0) ) { - gesture_state_ = NEAR_STATE; - } else if( (ud_delta != 0) && (lr_delta != 0) ) { - gesture_state_ = FAR_STATE; - } - return true; - } - } - } else { - if( (abs(ud_delta) < GESTURE_SENSITIVITY_2) && \ - (abs(lr_delta) < GESTURE_SENSITIVITY_2) ) { - - if( (ud_delta == 0) && (lr_delta == 0) ) { - gesture_near_count_++; - } - - if( gesture_near_count_ >= 10 ) { - gesture_ud_count_ = 0; - gesture_lr_count_ = 0; - gesture_ud_delta_ = 0; - gesture_lr_delta_ = 0; - } - } - } - -#if DEBUG - Serial.print("UD_CT: "); - Serial.print(gesture_ud_count_); - Serial.print(" LR_CT: "); - Serial.print(gesture_lr_count_); - Serial.print(" NEAR_CT: "); - Serial.print(gesture_near_count_); - Serial.print(" FAR_CT: "); - Serial.println(gesture_far_count_); - Serial.println("----------"); -#endif - - return false; -} - -/** - * @brief Determines swipe direction or near/far state - * - * @return True if near/far event. False otherwise. - */ -bool SparkFun_APDS9960::decodeGesture() -{ - /* Return if near or far event is detected */ - if( gesture_state_ == NEAR_STATE ) { - gesture_motion_ = DIR_NEAR; - return true; - } else if ( gesture_state_ == FAR_STATE ) { - gesture_motion_ = DIR_FAR; - return true; - } - - /* Determine swipe direction */ - if( (gesture_ud_count_ == -1) && (gesture_lr_count_ == 0) ) { - gesture_motion_ = DIR_UP; - } else if( (gesture_ud_count_ == 1) && (gesture_lr_count_ == 0) ) { - gesture_motion_ = DIR_DOWN; - } else if( (gesture_ud_count_ == 0) && (gesture_lr_count_ == 1) ) { - gesture_motion_ = DIR_RIGHT; - } else if( (gesture_ud_count_ == 0) && (gesture_lr_count_ == -1) ) { - gesture_motion_ = DIR_LEFT; - } else if( (gesture_ud_count_ == -1) && (gesture_lr_count_ == 1) ) { - if( abs(gesture_ud_delta_) > abs(gesture_lr_delta_) ) { - gesture_motion_ = DIR_UP; - } else { - gesture_motion_ = DIR_RIGHT; - } - } else if( (gesture_ud_count_ == 1) && (gesture_lr_count_ == -1) ) { - if( abs(gesture_ud_delta_) > abs(gesture_lr_delta_) ) { - gesture_motion_ = DIR_DOWN; - } else { - gesture_motion_ = DIR_LEFT; - } - } else if( (gesture_ud_count_ == -1) && (gesture_lr_count_ == -1) ) { - if( abs(gesture_ud_delta_) > abs(gesture_lr_delta_) ) { - gesture_motion_ = DIR_UP; - } else { - gesture_motion_ = DIR_LEFT; - } - } else if( (gesture_ud_count_ == 1) && (gesture_lr_count_ == 1) ) { - if( abs(gesture_ud_delta_) > abs(gesture_lr_delta_) ) { - gesture_motion_ = DIR_DOWN; - } else { - gesture_motion_ = DIR_RIGHT; - } - } else { - return false; - } - - return true; -} - -/******************************************************************************* - * Getters and setters for register values - ******************************************************************************/ - -/** - * @brief Returns the lower threshold for proximity detection - * - * @return lower threshold - */ -uint8_t SparkFun_APDS9960::getProxIntLowThresh() -{ - uint8_t val; - - /* Read value from PILT register */ - if( !wireReadDataByte(APDS9960_PILT, val) ) { - val = 0; - } - - return val; -} - -/** - * @brief Sets the lower threshold for proximity detection - * - * @param[in] threshold the lower proximity threshold - * @return True if operation successful. False otherwise. - */ -bool SparkFun_APDS9960::setProxIntLowThresh(uint8_t threshold) -{ - if( !wireWriteDataByte(APDS9960_PILT, threshold) ) { - return false; - } - - return true; -} - -/** - * @brief Returns the high threshold for proximity detection - * - * @return high threshold - */ -uint8_t SparkFun_APDS9960::getProxIntHighThresh() -{ - uint8_t val; - - /* Read value from PIHT register */ - if( !wireReadDataByte(APDS9960_PIHT, val) ) { - val = 0; - } - - return val; -} - -/** - * @brief Sets the high threshold for proximity detection - * - * @param[in] threshold the high proximity threshold - * @return True if operation successful. False otherwise. - */ -bool SparkFun_APDS9960::setProxIntHighThresh(uint8_t threshold) -{ - if( !wireWriteDataByte(APDS9960_PIHT, threshold) ) { - return false; - } - - return true; -} - -/** - * @brief Returns LED drive strength for proximity and ALS - * - * Value LED Current - * 0 100 mA - * 1 50 mA - * 2 25 mA - * 3 12.5 mA - * - * @return the value of the LED drive strength. 0xFF on failure. - */ -uint8_t SparkFun_APDS9960::getLEDDrive() -{ - uint8_t val; - - /* Read value from CONTROL register */ - if( !wireReadDataByte(APDS9960_CONTROL, val) ) { - return ERROR; - } - - /* Shift and mask out LED drive bits */ - val = (val >> 6) & 0b00000011; - - return val; -} - -/** - * @brief Sets the LED drive strength for proximity and ALS - * - * Value LED Current - * 0 100 mA - * 1 50 mA - * 2 25 mA - * 3 12.5 mA - * - * @param[in] drive the value (0-3) for the LED drive strength - * @return True if operation successful. False otherwise. - */ -bool SparkFun_APDS9960::setLEDDrive(uint8_t drive) -{ - uint8_t val; - - /* Read value from CONTROL register */ - if( !wireReadDataByte(APDS9960_CONTROL, val) ) { - return false; - } - - /* Set bits in register to given value */ - drive &= 0b00000011; - drive = drive << 6; - val &= 0b00111111; - val |= drive; - - /* Write register value back into CONTROL register */ - if( !wireWriteDataByte(APDS9960_CONTROL, val) ) { - return false; - } - - return true; -} - -/** - * @brief Returns receiver gain for proximity detection - * - * Value Gain - * 0 1x - * 1 2x - * 2 4x - * 3 8x - * - * @return the value of the proximity gain. 0xFF on failure. - */ -uint8_t SparkFun_APDS9960::getProximityGain() -{ - uint8_t val; - - /* Read value from CONTROL register */ - if( !wireReadDataByte(APDS9960_CONTROL, val) ) { - return ERROR; - } - - /* Shift and mask out PDRIVE bits */ - val = (val >> 2) & 0b00000011; - - return val; -} - -/** - * @brief Sets the receiver gain for proximity detection - * - * Value Gain - * 0 1x - * 1 2x - * 2 4x - * 3 8x - * - * @param[in] drive the value (0-3) for the gain - * @return True if operation successful. False otherwise. - */ -bool SparkFun_APDS9960::setProximityGain(uint8_t drive) -{ - uint8_t val; - - /* Read value from CONTROL register */ - if( !wireReadDataByte(APDS9960_CONTROL, val) ) { - return false; - } - - /* Set bits in register to given value */ - drive &= 0b00000011; - drive = drive << 2; - val &= 0b11110011; - val |= drive; - - /* Write register value back into CONTROL register */ - if( !wireWriteDataByte(APDS9960_CONTROL, val) ) { - return false; - } - - return true; -} - -/** - * @brief Returns receiver gain for the ambient light sensor (ALS) - * - * Value Gain - * 0 1x - * 1 4x - * 2 16x - * 3 64x - * - * @return the value of the ALS gain. 0xFF on failure. - */ -uint8_t SparkFun_APDS9960::getAmbientLightGain() -{ - uint8_t val; - - /* Read value from CONTROL register */ - if( !wireReadDataByte(APDS9960_CONTROL, val) ) { - return ERROR; - } - - /* Shift and mask out ADRIVE bits */ - val &= 0b00000011; - - return val; -} - -/** - * @brief Sets the receiver gain for the ambient light sensor (ALS) - * - * Value Gain - * 0 1x - * 1 4x - * 2 16x - * 3 64x - * - * @param[in] drive the value (0-3) for the gain - * @return True if operation successful. False otherwise. - */ -bool SparkFun_APDS9960::setAmbientLightGain(uint8_t drive) -{ - uint8_t val; - - /* Read value from CONTROL register */ - if( !wireReadDataByte(APDS9960_CONTROL, val) ) { - return false; - } - - /* Set bits in register to given value */ - drive &= 0b00000011; - val &= 0b11111100; - val |= drive; - - /* Write register value back into CONTROL register */ - if( !wireWriteDataByte(APDS9960_CONTROL, val) ) { - return false; - } - - return true; -} - -/** - * @brief Get the current LED boost value - * - * Value Boost Current - * 0 100% - * 1 150% - * 2 200% - * 3 300% - * - * @return The LED boost value. 0xFF on failure. - */ -uint8_t SparkFun_APDS9960::getLEDBoost() -{ - uint8_t val; - - /* Read value from CONFIG2 register */ - if( !wireReadDataByte(APDS9960_CONFIG2, val) ) { - return ERROR; - } - - /* Shift and mask out LED_BOOST bits */ - val = (val >> 4) & 0b00000011; - - return val; -} - -/** - * @brief Sets the LED current boost value - * - * Value Boost Current - * 0 100% - * 1 150% - * 2 200% - * 3 300% - * - * @param[in] drive the value (0-3) for current boost (100-300%) - * @return True if operation successful. False otherwise. - */ -bool SparkFun_APDS9960::setLEDBoost(uint8_t boost) -{ - uint8_t val; - - /* Read value from CONFIG2 register */ - if( !wireReadDataByte(APDS9960_CONFIG2, val) ) { - return false; - } - - /* Set bits in register to given value */ - boost &= 0b00000011; - boost = boost << 4; - val &= 0b11001111; - val |= boost; - - /* Write register value back into CONFIG2 register */ - if( !wireWriteDataByte(APDS9960_CONFIG2, val) ) { - return false; - } - - return true; -} - -/** - * @brief Gets proximity gain compensation enable - * - * @return 1 if compensation is enabled. 0 if not. 0xFF on error. - */ -uint8_t SparkFun_APDS9960::getProxGainCompEnable() -{ - uint8_t val; - - /* Read value from CONFIG3 register */ - if( !wireReadDataByte(APDS9960_CONFIG3, val) ) { - return ERROR; - } - - /* Shift and mask out PCMP bits */ - val = (val >> 5) & 0b00000001; - - return val; -} - -/** - * @brief Sets the proximity gain compensation enable - * - * @param[in] enable 1 to enable compensation. 0 to disable compensation. - * @return True if operation successful. False otherwise. - */ - bool SparkFun_APDS9960::setProxGainCompEnable(uint8_t enable) -{ - uint8_t val; - - /* Read value from CONFIG3 register */ - if( !wireReadDataByte(APDS9960_CONFIG3, val) ) { - return false; - } - - /* Set bits in register to given value */ - enable &= 0b00000001; - enable = enable << 5; - val &= 0b11011111; - val |= enable; - - /* Write register value back into CONFIG3 register */ - if( !wireWriteDataByte(APDS9960_CONFIG3, val) ) { - return false; - } - - return true; -} - -/** - * @brief Gets the current mask for enabled/disabled proximity photodiodes - * - * 1 = disabled, 0 = enabled - * Bit Photodiode - * 3 UP - * 2 DOWN - * 1 LEFT - * 0 RIGHT - * - * @return Current proximity mask for photodiodes. 0xFF on error. - */ -uint8_t SparkFun_APDS9960::getProxPhotoMask() -{ - uint8_t val; - - /* Read value from CONFIG3 register */ - if( !wireReadDataByte(APDS9960_CONFIG3, val) ) { - return ERROR; - } - - /* Mask out photodiode enable mask bits */ - val &= 0b00001111; - - return val; -} - -/** - * @brief Sets the mask for enabling/disabling proximity photodiodes - * - * 1 = disabled, 0 = enabled - * Bit Photodiode - * 3 UP - * 2 DOWN - * 1 LEFT - * 0 RIGHT - * - * @param[in] mask 4-bit mask value - * @return True if operation successful. False otherwise. - */ -bool SparkFun_APDS9960::setProxPhotoMask(uint8_t mask) -{ - uint8_t val; - - /* Read value from CONFIG3 register */ - if( !wireReadDataByte(APDS9960_CONFIG3, val) ) { - return false; - } - - /* Set bits in register to given value */ - mask &= 0b00001111; - val &= 0b11110000; - val |= mask; - - /* Write register value back into CONFIG3 register */ - if( !wireWriteDataByte(APDS9960_CONFIG3, val) ) { - return false; - } - - return true; -} - -/** - * @brief Gets the entry proximity threshold for gesture sensing - * - * @return Current entry proximity threshold. - */ -uint8_t SparkFun_APDS9960::getGestureEnterThresh() -{ - uint8_t val; - - /* Read value from GPENTH register */ - if( !wireReadDataByte(APDS9960_GPENTH, val) ) { - val = 0; - } - - return val; -} - -/** - * @brief Sets the entry proximity threshold for gesture sensing - * - * @param[in] threshold proximity value needed to start gesture mode - * @return True if operation successful. False otherwise. - */ -bool SparkFun_APDS9960::setGestureEnterThresh(uint8_t threshold) -{ - if( !wireWriteDataByte(APDS9960_GPENTH, threshold) ) { - return false; - } - - return true; -} - -/** - * @brief Gets the exit proximity threshold for gesture sensing - * - * @return Current exit proximity threshold. - */ -uint8_t SparkFun_APDS9960::getGestureExitThresh() -{ - uint8_t val; - - /* Read value from GEXTH register */ - if( !wireReadDataByte(APDS9960_GEXTH, val) ) { - val = 0; - } - - return val; -} - -/** - * @brief Sets the exit proximity threshold for gesture sensing - * - * @param[in] threshold proximity value needed to end gesture mode - * @return True if operation successful. False otherwise. - */ -bool SparkFun_APDS9960::setGestureExitThresh(uint8_t threshold) -{ - if( !wireWriteDataByte(APDS9960_GEXTH, threshold) ) { - return false; - } - - return true; -} - -/** - * @brief Gets the gain of the photodiode during gesture mode - * - * Value Gain - * 0 1x - * 1 2x - * 2 4x - * 3 8x - * - * @return the current photodiode gain. 0xFF on error. - */ -uint8_t SparkFun_APDS9960::getGestureGain() -{ - uint8_t val; - - /* Read value from GCONF2 register */ - if( !wireReadDataByte(APDS9960_GCONF2, val) ) { - return ERROR; - } - - /* Shift and mask out GGAIN bits */ - val = (val >> 5) & 0b00000011; - - return val; -} - -/** - * @brief Sets the gain of the photodiode during gesture mode - * - * Value Gain - * 0 1x - * 1 2x - * 2 4x - * 3 8x - * - * @param[in] gain the value for the photodiode gain - * @return True if operation successful. False otherwise. - */ -bool SparkFun_APDS9960::setGestureGain(uint8_t gain) -{ - uint8_t val; - - /* Read value from GCONF2 register */ - if( !wireReadDataByte(APDS9960_GCONF2, val) ) { - return false; - } - - /* Set bits in register to given value */ - gain &= 0b00000011; - gain = gain << 5; - val &= 0b10011111; - val |= gain; - - /* Write register value back into GCONF2 register */ - if( !wireWriteDataByte(APDS9960_GCONF2, val) ) { - return false; - } - - return true; -} - -/** - * @brief Gets the drive current of the LED during gesture mode - * - * Value LED Current - * 0 100 mA - * 1 50 mA - * 2 25 mA - * 3 12.5 mA - * - * @return the LED drive current value. 0xFF on error. - */ -uint8_t SparkFun_APDS9960::getGestureLEDDrive() -{ - uint8_t val; - - /* Read value from GCONF2 register */ - if( !wireReadDataByte(APDS9960_GCONF2, val) ) { - return ERROR; - } - - /* Shift and mask out GLDRIVE bits */ - val = (val >> 3) & 0b00000011; - - return val; -} - -/** - * @brief Sets the LED drive current during gesture mode - * - * Value LED Current - * 0 100 mA - * 1 50 mA - * 2 25 mA - * 3 12.5 mA - * - * @param[in] drive the value for the LED drive current - * @return True if operation successful. False otherwise. - */ -bool SparkFun_APDS9960::setGestureLEDDrive(uint8_t drive) -{ - uint8_t val; - - /* Read value from GCONF2 register */ - if( !wireReadDataByte(APDS9960_GCONF2, val) ) { - return false; - } - - /* Set bits in register to given value */ - drive &= 0b00000011; - drive = drive << 3; - val &= 0b11100111; - val |= drive; - - /* Write register value back into GCONF2 register */ - if( !wireWriteDataByte(APDS9960_GCONF2, val) ) { - return false; - } - - return true; -} - -/** - * @brief Gets the time in low power mode between gesture detections - * - * Value Wait time - * 0 0 ms - * 1 2.8 ms - * 2 5.6 ms - * 3 8.4 ms - * 4 14.0 ms - * 5 22.4 ms - * 6 30.8 ms - * 7 39.2 ms - * - * @return the current wait time between gestures. 0xFF on error. - */ -uint8_t SparkFun_APDS9960::getGestureWaitTime() -{ - uint8_t val; - - /* Read value from GCONF2 register */ - if( !wireReadDataByte(APDS9960_GCONF2, val) ) { - return ERROR; - } - - /* Mask out GWTIME bits */ - val &= 0b00000111; - - return val; -} - -/** - * @brief Sets the time in low power mode between gesture detections - * - * Value Wait time - * 0 0 ms - * 1 2.8 ms - * 2 5.6 ms - * 3 8.4 ms - * 4 14.0 ms - * 5 22.4 ms - * 6 30.8 ms - * 7 39.2 ms - * - * @param[in] the value for the wait time - * @return True if operation successful. False otherwise. - */ -bool SparkFun_APDS9960::setGestureWaitTime(uint8_t time) -{ - uint8_t val; - - /* Read value from GCONF2 register */ - if( !wireReadDataByte(APDS9960_GCONF2, val) ) { - return false; - } - - /* Set bits in register to given value */ - time &= 0b00000111; - val &= 0b11111000; - val |= time; - - /* Write register value back into GCONF2 register */ - if( !wireWriteDataByte(APDS9960_GCONF2, val) ) { - return false; - } - - return true; -} - -/** - * @brief Gets the low threshold for ambient light interrupts - * - * @param[out] threshold current low threshold stored on the APDS-9960 - * @return True if operation successful. False otherwise. - */ -bool SparkFun_APDS9960::getLightIntLowThreshold(uint16_t &threshold) -{ - uint8_t val_byte; - threshold = 0; - - /* Read value from ambient light low threshold, low byte register */ - if( !wireReadDataByte(APDS9960_AILTL, val_byte) ) { - return false; - } - threshold = val_byte; - - /* Read value from ambient light low threshold, high byte register */ - if( !wireReadDataByte(APDS9960_AILTH, val_byte) ) { - return false; - } - threshold = threshold + ((uint16_t)val_byte << 8); - - return true; -} - -/** - * @brief Sets the low threshold for ambient light interrupts - * - * @param[in] threshold low threshold value for interrupt to trigger - * @return True if operation successful. False otherwise. - */ -bool SparkFun_APDS9960::setLightIntLowThreshold(uint16_t threshold) -{ - uint8_t val_low; - uint8_t val_high; - - /* Break 16-bit threshold into 2 8-bit values */ - val_low = threshold & 0x00FF; - val_high = (threshold & 0xFF00) >> 8; - - /* Write low byte */ - if( !wireWriteDataByte(APDS9960_AILTL, val_low) ) { - return false; - } - - /* Write high byte */ - if( !wireWriteDataByte(APDS9960_AILTH, val_high) ) { - return false; - } - - return true; -} - -/** - * @brief Gets the high threshold for ambient light interrupts - * - * @param[out] threshold current low threshold stored on the APDS-9960 - * @return True if operation successful. False otherwise. - */ -bool SparkFun_APDS9960::getLightIntHighThreshold(uint16_t &threshold) -{ - uint8_t val_byte; - threshold = 0; - - /* Read value from ambient light high threshold, low byte register */ - if( !wireReadDataByte(APDS9960_AIHTL, val_byte) ) { - return false; - } - threshold = val_byte; - - /* Read value from ambient light high threshold, high byte register */ - if( !wireReadDataByte(APDS9960_AIHTH, val_byte) ) { - return false; - } - threshold = threshold + ((uint16_t)val_byte << 8); - - return true; -} - -/** - * @brief Sets the high threshold for ambient light interrupts - * - * @param[in] threshold high threshold value for interrupt to trigger - * @return True if operation successful. False otherwise. - */ -bool SparkFun_APDS9960::setLightIntHighThreshold(uint16_t threshold) -{ - uint8_t val_low; - uint8_t val_high; - - /* Break 16-bit threshold into 2 8-bit values */ - val_low = threshold & 0x00FF; - val_high = (threshold & 0xFF00) >> 8; - - /* Write low byte */ - if( !wireWriteDataByte(APDS9960_AIHTL, val_low) ) { - return false; - } - - /* Write high byte */ - if( !wireWriteDataByte(APDS9960_AIHTH, val_high) ) { - return false; - } - - return true; -} - -/** - * @brief Gets the low threshold for proximity interrupts - * - * @param[out] threshold current low threshold stored on the APDS-9960 - * @return True if operation successful. False otherwise. - */ -bool SparkFun_APDS9960::getProximityIntLowThreshold(uint8_t &threshold) -{ - threshold = 0; - - /* Read value from proximity low threshold register */ - if( !wireReadDataByte(APDS9960_PILT, threshold) ) { - return false; - } - - return true; -} - -/** - * @brief Sets the low threshold for proximity interrupts - * - * @param[in] threshold low threshold value for interrupt to trigger - * @return True if operation successful. False otherwise. - */ -bool SparkFun_APDS9960::setProximityIntLowThreshold(uint8_t threshold) -{ - - /* Write threshold value to register */ - if( !wireWriteDataByte(APDS9960_PILT, threshold) ) { - return false; - } - - return true; -} - -/** - * @brief Gets the high threshold for proximity interrupts - * - * @param[out] threshold current low threshold stored on the APDS-9960 - * @return True if operation successful. False otherwise. - */ -bool SparkFun_APDS9960::getProximityIntHighThreshold(uint8_t &threshold) -{ - threshold = 0; - - /* Read value from proximity low threshold register */ - if( !wireReadDataByte(APDS9960_PIHT, threshold) ) { - return false; - } - - return true; -} - -/** - * @brief Sets the high threshold for proximity interrupts - * - * @param[in] threshold high threshold value for interrupt to trigger - * @return True if operation successful. False otherwise. - */ -bool SparkFun_APDS9960::setProximityIntHighThreshold(uint8_t threshold) -{ - - /* Write threshold value to register */ - if( !wireWriteDataByte(APDS9960_PIHT, threshold) ) { - return false; - } - - return true; -} - -/** - * @brief Gets if ambient light interrupts are enabled or not - * - * @return 1 if interrupts are enabled, 0 if not. 0xFF on error. - */ -uint8_t SparkFun_APDS9960::getAmbientLightIntEnable() -{ - uint8_t val; - - /* Read value from ENABLE register */ - if( !wireReadDataByte(APDS9960_ENABLE, val) ) { - return ERROR; - } - - /* Shift and mask out AIEN bit */ - val = (val >> 4) & 0b00000001; - - return val; -} - -/** - * @brief Turns ambient light interrupts on or off - * - * @param[in] enable 1 to enable interrupts, 0 to turn them off - * @return True if operation successful. False otherwise. - */ -bool SparkFun_APDS9960::setAmbientLightIntEnable(uint8_t enable) -{ - uint8_t val; - - /* Read value from ENABLE register */ - if( !wireReadDataByte(APDS9960_ENABLE, val) ) { - return false; - } - - /* Set bits in register to given value */ - enable &= 0b00000001; - enable = enable << 4; - val &= 0b11101111; - val |= enable; - - /* Write register value back into ENABLE register */ - if( !wireWriteDataByte(APDS9960_ENABLE, val) ) { - return false; - } - - return true; -} - -/** - * @brief Gets if proximity interrupts are enabled or not - * - * @return 1 if interrupts are enabled, 0 if not. 0xFF on error. - */ -uint8_t SparkFun_APDS9960::getProximityIntEnable() -{ - uint8_t val; - - /* Read value from ENABLE register */ - if( !wireReadDataByte(APDS9960_ENABLE, val) ) { - return ERROR; - } - - /* Shift and mask out PIEN bit */ - val = (val >> 5) & 0b00000001; - - return val; -} - -/** - * @brief Turns proximity interrupts on or off - * - * @param[in] enable 1 to enable interrupts, 0 to turn them off - * @return True if operation successful. False otherwise. - */ -bool SparkFun_APDS9960::setProximityIntEnable(uint8_t enable) -{ - uint8_t val; - - /* Read value from ENABLE register */ - if( !wireReadDataByte(APDS9960_ENABLE, val) ) { - return false; - } - - /* Set bits in register to given value */ - enable &= 0b00000001; - enable = enable << 5; - val &= 0b11011111; - val |= enable; - - /* Write register value back into ENABLE register */ - if( !wireWriteDataByte(APDS9960_ENABLE, val) ) { - return false; - } - - return true; -} - -/** - * @brief Gets if gesture interrupts are enabled or not - * - * @return 1 if interrupts are enabled, 0 if not. 0xFF on error. - */ -uint8_t SparkFun_APDS9960::getGestureIntEnable() -{ - uint8_t val; - - /* Read value from GCONF4 register */ - if( !wireReadDataByte(APDS9960_GCONF4, val) ) { - return ERROR; - } - - /* Shift and mask out GIEN bit */ - val = (val >> 1) & 0b00000001; - - return val; -} - -/** - * @brief Turns gesture-related interrupts on or off - * - * @param[in] enable 1 to enable interrupts, 0 to turn them off - * @return True if operation successful. False otherwise. - */ -bool SparkFun_APDS9960::setGestureIntEnable(uint8_t enable) -{ - uint8_t val; - - /* Read value from GCONF4 register */ - if( !wireReadDataByte(APDS9960_GCONF4, val) ) { - return false; - } - - /* Set bits in register to given value */ - enable &= 0b00000001; - enable = enable << 1; - val &= 0b11111101; - val |= enable; - - /* Write register value back into GCONF4 register */ - if( !wireWriteDataByte(APDS9960_GCONF4, val) ) { - return false; - } - - return true; -} - -/** - * @brief Clears the ambient light interrupt - * - * @return True if operation completed successfully. False otherwise. - */ -bool SparkFun_APDS9960::clearAmbientLightInt() -{ - uint8_t throwaway; - if( !wireReadDataByte(APDS9960_AICLEAR, throwaway) ) { - return false; - } - - return true; -} - -/** - * @brief Clears the proximity interrupt - * - * @return True if operation completed successfully. False otherwise. - */ -bool SparkFun_APDS9960::clearProximityInt() -{ - uint8_t throwaway; - if( !wireReadDataByte(APDS9960_PICLEAR, throwaway) ) { - return false; - } - - return true; -} - -/** - * @brief Tells if the gesture state machine is currently running - * - * @return 1 if gesture state machine is running, 0 if not. 0xFF on error. - */ -uint8_t SparkFun_APDS9960::getGestureMode() -{ - uint8_t val; - - /* Read value from GCONF4 register */ - if( !wireReadDataByte(APDS9960_GCONF4, val) ) { - return ERROR; - } - - /* Mask out GMODE bit */ - val &= 0b00000001; - - return val; -} - -/** - * @brief Tells the state machine to either enter or exit gesture state machine - * - * @param[in] mode 1 to enter gesture state machine, 0 to exit. - * @return True if operation successful. False otherwise. - */ -bool SparkFun_APDS9960::setGestureMode(uint8_t mode) -{ - uint8_t val; - - /* Read value from GCONF4 register */ - if( !wireReadDataByte(APDS9960_GCONF4, val) ) { - return false; - } - - /* Set bits in register to given value */ - mode &= 0b00000001; - val &= 0b11111110; - val |= mode; - - /* Write register value back into GCONF4 register */ - if( !wireWriteDataByte(APDS9960_GCONF4, val) ) { - return false; - } - - return true; -} - -/******************************************************************************* - * Raw I2C Reads and Writes - ******************************************************************************/ - -/** - * @brief Writes a single byte to the I2C device (no register) - * - * @param[in] val the 1-byte value to write to the I2C device - * @return True if successful write operation. False otherwise. - */ -bool SparkFun_APDS9960::wireWriteByte(uint8_t val) -{ - Wire.beginTransmission(APDS9960_I2C_ADDR); - Wire.write(val); - if( Wire.endTransmission() != 0 ) { - return false; - } - - return true; -} - -/** - * @brief Writes a single byte to the I2C device and specified register - * - * @param[in] reg the register in the I2C device to write to - * @param[in] val the 1-byte value to write to the I2C device - * @return True if successful write operation. False otherwise. - */ -bool SparkFun_APDS9960::wireWriteDataByte(uint8_t reg, uint8_t val) -{ - Wire.beginTransmission(APDS9960_I2C_ADDR); - Wire.write(reg); - Wire.write(val); - if( Wire.endTransmission() != 0 ) { - return false; - } - - return true; -} - -/** - * @brief Writes a block (array) of bytes to the I2C device and register - * - * @param[in] reg the register in the I2C device to write to - * @param[in] val pointer to the beginning of the data byte array - * @param[in] len the length (in bytes) of the data to write - * @return True if successful write operation. False otherwise. - */ -bool SparkFun_APDS9960::wireWriteDataBlock( uint8_t reg, - uint8_t *val, - unsigned int len) -{ - unsigned int i; - - Wire.beginTransmission(APDS9960_I2C_ADDR); - Wire.write(reg); - for(i = 0; i < len; i++) { - Wire.beginTransmission(val[i]); - } - if( Wire.endTransmission() != 0 ) { - return false; - } - - return true; -} - -/** - * @brief Reads a single byte from the I2C device and specified register - * - * @param[in] reg the register to read from - * @param[out] the value returned from the register - * @return True if successful read operation. False otherwise. - */ -bool SparkFun_APDS9960::wireReadDataByte(uint8_t reg, uint8_t &val) -{ - - /* Indicate which register we want to read from */ - if (!wireWriteByte(reg)) { - return false; - } - - /* Read from register */ - Wire.requestFrom(APDS9960_I2C_ADDR, 1); - while (Wire.available()) { - val = Wire.read(); - } - - return true; -} - -/** - * @brief Reads a block (array) of bytes from the I2C device and register - * - * @param[in] reg the register to read from - * @param[out] val pointer to the beginning of the data - * @param[in] len number of bytes to read - * @return Number of bytes read. -1 on read error. - */ -int SparkFun_APDS9960::wireReadDataBlock( uint8_t reg, - uint8_t *val, - unsigned int len) -{ - unsigned char i = 0; - - /* Indicate which register we want to read from */ - if (!wireWriteByte(reg)) { - return -1; - } - - /* Read block data */ - Wire.requestFrom(APDS9960_I2C_ADDR, len); - while (Wire.available()) { - if (i >= len) { - return -1; - } - val[i] = Wire.read(); - i++; - } - - return i; -} - diff --git a/Sming/Libraries/SparkFun_APDS9960/SparkFun_APDS9960.h b/Sming/Libraries/SparkFun_APDS9960/SparkFun_APDS9960.h deleted file mode 100644 index 987da5f3a0..0000000000 --- a/Sming/Libraries/SparkFun_APDS9960/SparkFun_APDS9960.h +++ /dev/null @@ -1,347 +0,0 @@ -/** - * @file SparkFun_APDS-9960.h - * @brief Library for the SparkFun APDS-9960 breakout board - * @author Shawn Hymel (SparkFun Electronics) - * - * @copyright This code is public domain but you buy me a beer if you use - * this and we meet someday (Beerware license). - * - * This library interfaces the Avago APDS-9960 to Arduino over I2C. The library - * relies on the Arduino Wire (I2C) library. to use the library, instantiate an - * APDS9960 object, call init(), and call the appropriate functions. - */ - -#ifndef SparkFun_APDS9960_H -#define SparkFun_APDS9960_H - -#include - -/* Debug */ -#define DEBUG 0 - -/* APDS-9960 I2C address */ -#define APDS9960_I2C_ADDR 0x39 - -/* Gesture parameters */ -#define GESTURE_THRESHOLD_OUT 10 -#define GESTURE_SENSITIVITY_1 50 -#define GESTURE_SENSITIVITY_2 20 - -/* Error code for returned values */ -#define ERROR 0xFF - -/* Acceptable device IDs */ -#define APDS9960_ID_1 0xAB -#define APDS9960_ID_2 0x9C - -/* Misc parameters */ -#define FIFO_PAUSE_TIME 30 // Wait period (ms) between FIFO reads - -/* APDS-9960 register addresses */ -#define APDS9960_ENABLE 0x80 -#define APDS9960_ATIME 0x81 -#define APDS9960_WTIME 0x83 -#define APDS9960_AILTL 0x84 -#define APDS9960_AILTH 0x85 -#define APDS9960_AIHTL 0x86 -#define APDS9960_AIHTH 0x87 -#define APDS9960_PILT 0x89 -#define APDS9960_PIHT 0x8B -#define APDS9960_PERS 0x8C -#define APDS9960_CONFIG1 0x8D -#define APDS9960_PPULSE 0x8E -#define APDS9960_CONTROL 0x8F -#define APDS9960_CONFIG2 0x90 -#define APDS9960_ID 0x92 -#define APDS9960_STATUS 0x93 -#define APDS9960_CDATAL 0x94 -#define APDS9960_CDATAH 0x95 -#define APDS9960_RDATAL 0x96 -#define APDS9960_RDATAH 0x97 -#define APDS9960_GDATAL 0x98 -#define APDS9960_GDATAH 0x99 -#define APDS9960_BDATAL 0x9A -#define APDS9960_BDATAH 0x9B -#define APDS9960_PDATA 0x9C -#define APDS9960_POFFSET_UR 0x9D -#define APDS9960_POFFSET_DL 0x9E -#define APDS9960_CONFIG3 0x9F -#define APDS9960_GPENTH 0xA0 -#define APDS9960_GEXTH 0xA1 -#define APDS9960_GCONF1 0xA2 -#define APDS9960_GCONF2 0xA3 -#define APDS9960_GOFFSET_U 0xA4 -#define APDS9960_GOFFSET_D 0xA5 -#define APDS9960_GOFFSET_L 0xA7 -#define APDS9960_GOFFSET_R 0xA9 -#define APDS9960_GPULSE 0xA6 -#define APDS9960_GCONF3 0xAA -#define APDS9960_GCONF4 0xAB -#define APDS9960_GFLVL 0xAE -#define APDS9960_GSTATUS 0xAF -#define APDS9960_IFORCE 0xE4 -#define APDS9960_PICLEAR 0xE5 -#define APDS9960_CICLEAR 0xE6 -#define APDS9960_AICLEAR 0xE7 -#define APDS9960_GFIFO_U 0xFC -#define APDS9960_GFIFO_D 0xFD -#define APDS9960_GFIFO_L 0xFE -#define APDS9960_GFIFO_R 0xFF - -/* Bit fields */ -#define APDS9960_PON 0b00000001 -#define APDS9960_AEN 0b00000010 -#define APDS9960_PEN 0b00000100 -#define APDS9960_WEN 0b00001000 -#define APSD9960_AIEN 0b00010000 -#define APDS9960_PIEN 0b00100000 -#define APDS9960_GEN 0b01000000 -#define APDS9960_GVALID 0b00000001 - -/* On/Off definitions */ -#define OFF 0 -#define ON 1 - -/* Acceptable parameters for setMode */ -#define POWER 0 -#define AMBIENT_LIGHT 1 -#define PROXIMITY 2 -#define WAIT 3 -#define AMBIENT_LIGHT_INT 4 -#define PROXIMITY_INT 5 -#define GESTURE 6 -#define ALL 7 - -/* LED Drive values */ -#define LED_DRIVE_100MA 0 -#define LED_DRIVE_50MA 1 -#define LED_DRIVE_25MA 2 -#define LED_DRIVE_12_5MA 3 - -/* Proximity Gain (PGAIN) values */ -#define PGAIN_1X 0 -#define PGAIN_2X 1 -#define PGAIN_4X 2 -#define PGAIN_8X 3 - -/* ALS Gain (AGAIN) values */ -#define AGAIN_1X 0 -#define AGAIN_4X 1 -#define AGAIN_16X 2 -#define AGAIN_64X 3 - -/* Gesture Gain (GGAIN) values */ -#define GGAIN_1X 0 -#define GGAIN_2X 1 -#define GGAIN_4X 2 -#define GGAIN_8X 3 - -/* LED Boost values */ -#define LED_BOOST_100 0 -#define LED_BOOST_150 1 -#define LED_BOOST_200 2 -#define LED_BOOST_300 3 - -/* Gesture wait time values */ -#define GWTIME_0MS 0 -#define GWTIME_2_8MS 1 -#define GWTIME_5_6MS 2 -#define GWTIME_8_4MS 3 -#define GWTIME_14_0MS 4 -#define GWTIME_22_4MS 5 -#define GWTIME_30_8MS 6 -#define GWTIME_39_2MS 7 - -/* Default values */ -#define DEFAULT_ATIME 219 // 103ms -#define DEFAULT_WTIME 246 // 27ms -#define DEFAULT_PROX_PPULSE 0x87 // 16us, 8 pulses -#define DEFAULT_GESTURE_PPULSE 0x89 // 16us, 10 pulses -#define DEFAULT_POFFSET_UR 0 // 0 offset -#define DEFAULT_POFFSET_DL 0 // 0 offset -#define DEFAULT_CONFIG1 0x60 // No 12x wait (WTIME) factor -#define DEFAULT_LDRIVE LED_DRIVE_100MA -#define DEFAULT_PGAIN PGAIN_4X -#define DEFAULT_AGAIN AGAIN_4X -#define DEFAULT_PILT 0 // Low proximity threshold -#define DEFAULT_PIHT 50 // High proximity threshold -#define DEFAULT_AILT 0xFFFF // Force interrupt for calibration -#define DEFAULT_AIHT 0 -#define DEFAULT_PERS 0x11 // 2 consecutive prox or ALS for int. -#define DEFAULT_CONFIG2 0x01 // No saturation interrupts or LED boost -#define DEFAULT_CONFIG3 0 // Enable all photodiodes, no SAI -#define DEFAULT_GPENTH 40 // Threshold for entering gesture mode -#define DEFAULT_GEXTH 30 // Threshold for exiting gesture mode -#define DEFAULT_GCONF1 0x40 // 4 gesture events for int., 1 for exit -#define DEFAULT_GGAIN GGAIN_4X -#define DEFAULT_GLDRIVE LED_DRIVE_100MA -#define DEFAULT_GWTIME GWTIME_2_8MS -#define DEFAULT_GOFFSET 0 // No offset scaling for gesture mode -#define DEFAULT_GPULSE 0xC9 // 32us, 10 pulses -#define DEFAULT_GCONF3 0 // All photodiodes active during gesture -#define DEFAULT_GIEN 0 // Disable gesture interrupts - -/* Direction definitions */ -enum { - DIR_NONE, - DIR_LEFT, - DIR_RIGHT, - DIR_UP, - DIR_DOWN, - DIR_NEAR, - DIR_FAR, - DIR_ALL -}; - -/* State definitions */ -enum { - NA_STATE, - NEAR_STATE, - FAR_STATE, - ALL_STATE -}; - -/* Container for gesture data */ -typedef struct gesture_data_type { - uint8_t u_data[32]; - uint8_t d_data[32]; - uint8_t l_data[32]; - uint8_t r_data[32]; - uint8_t index; - uint8_t total_gestures; - uint8_t in_threshold; - uint8_t out_threshold; -} gesture_data_type; - -/* APDS9960 Class */ -class SparkFun_APDS9960 { -public: - - /* Initialization methods */ - SparkFun_APDS9960(); - ~SparkFun_APDS9960(); - bool init(); - uint8_t getMode(); - bool setMode(uint8_t mode, uint8_t enable); - - /* Turn the APDS-9960 on and off */ - bool enablePower(); - bool disablePower(); - - /* Enable or disable specific sensors */ - bool enableLightSensor(bool interrupts = false); - bool disableLightSensor(); - bool enableProximitySensor(bool interrupts = false); - bool disableProximitySensor(); - bool enableGestureSensor(bool interrupts = true); - bool disableGestureSensor(); - - /* LED drive strength control */ - uint8_t getLEDDrive(); - bool setLEDDrive(uint8_t drive); - uint8_t getGestureLEDDrive(); - bool setGestureLEDDrive(uint8_t drive); - - /* Gain control */ - uint8_t getAmbientLightGain(); - bool setAmbientLightGain(uint8_t gain); - uint8_t getProximityGain(); - bool setProximityGain(uint8_t gain); - uint8_t getGestureGain(); - bool setGestureGain(uint8_t gain); - - /* Get and set light interrupt thresholds */ - bool getLightIntLowThreshold(uint16_t &threshold); - bool setLightIntLowThreshold(uint16_t threshold); - bool getLightIntHighThreshold(uint16_t &threshold); - bool setLightIntHighThreshold(uint16_t threshold); - - /* Get and set proximity interrupt thresholds */ - bool getProximityIntLowThreshold(uint8_t &threshold); - bool setProximityIntLowThreshold(uint8_t threshold); - bool getProximityIntHighThreshold(uint8_t &threshold); - bool setProximityIntHighThreshold(uint8_t threshold); - - /* Get and set interrupt enables */ - uint8_t getAmbientLightIntEnable(); - bool setAmbientLightIntEnable(uint8_t enable); - uint8_t getProximityIntEnable(); - bool setProximityIntEnable(uint8_t enable); - uint8_t getGestureIntEnable(); - bool setGestureIntEnable(uint8_t enable); - - /* Clear interrupts */ - bool clearAmbientLightInt(); - bool clearProximityInt(); - - /* Ambient light methods */ - bool readAmbientLight(uint16_t &val); - bool readRedLight(uint16_t &val); - bool readGreenLight(uint16_t &val); - bool readBlueLight(uint16_t &val); - - /* Proximity methods */ - bool readProximity(uint8_t &val); - - /* Gesture methods */ - bool isGestureAvailable(); - int readGesture(); - -private: - - /* Gesture processing */ - void resetGestureParameters(); - bool processGestureData(); - bool decodeGesture(); - - /* Proximity Interrupt Threshold */ - uint8_t getProxIntLowThresh(); - bool setProxIntLowThresh(uint8_t threshold); - uint8_t getProxIntHighThresh(); - bool setProxIntHighThresh(uint8_t threshold); - - /* LED Boost Control */ - uint8_t getLEDBoost(); - bool setLEDBoost(uint8_t boost); - - /* Proximity photodiode select */ - uint8_t getProxGainCompEnable(); - bool setProxGainCompEnable(uint8_t enable); - uint8_t getProxPhotoMask(); - bool setProxPhotoMask(uint8_t mask); - - /* Gesture threshold control */ - uint8_t getGestureEnterThresh(); - bool setGestureEnterThresh(uint8_t threshold); - uint8_t getGestureExitThresh(); - bool setGestureExitThresh(uint8_t threshold); - - /* Gesture LED, gain, and time control */ - uint8_t getGestureWaitTime(); - bool setGestureWaitTime(uint8_t time); - - /* Gesture mode */ - uint8_t getGestureMode(); - bool setGestureMode(uint8_t mode); - - /* Raw I2C Commands */ - bool wireWriteByte(uint8_t val); - bool wireWriteDataByte(uint8_t reg, uint8_t val); - bool wireWriteDataBlock(uint8_t reg, uint8_t *val, unsigned int len); - bool wireReadDataByte(uint8_t reg, uint8_t &val); - int wireReadDataBlock(uint8_t reg, uint8_t *val, unsigned int len); - - /* Members */ - gesture_data_type gesture_data_; - int gesture_ud_delta_; - int gesture_lr_delta_; - int gesture_ud_count_; - int gesture_lr_count_; - int gesture_near_count_; - int gesture_far_count_; - int gesture_state_; - int gesture_motion_; -}; - -#endif - diff --git a/samples/Gesture_APDS-9960/app/application.cpp b/samples/Gesture_APDS-9960/app/application.cpp index 6dbdd3ce12..1ecd19215a 100644 --- a/samples/Gesture_APDS-9960/app/application.cpp +++ b/samples/Gesture_APDS-9960/app/application.cpp @@ -1,6 +1,6 @@ #include -#include +#include namespace { From e7fd78703f6bb5738955c4594abe408989b3c1ad Mon Sep 17 00:00:00 2001 From: slaff Date: Fri, 14 Jun 2024 08:58:38 +0200 Subject: [PATCH 075/128] SMTP Client improvements. (#2802) - Continue plain-text communication if SSL is not compiled - Fix memory leaks in MailMessage - Expose `TcpConnection::enableSsl(hostName)` which allows a plain-text TCP connection to be changed to SSL one after the tcp connection is established. --- .../Network/src/Network/MailMessage.cpp | 6 ++-- .../Network/src/Network/MailMessage.h | 10 +++++- .../Network/src/Network/SmtpClient.cpp | 33 ++++++++++++++----- .../Network/src/Network/TcpConnection.cpp | 23 +++++++++++++ .../Network/src/Network/TcpConnection.h | 7 ++++ Sming/Components/Network/src/Network/Url.cpp | 2 ++ samples/SmtpClient/app/application.cpp | 2 +- 7 files changed, 68 insertions(+), 15 deletions(-) diff --git a/Sming/Components/Network/src/Network/MailMessage.cpp b/Sming/Components/Network/src/Network/MailMessage.cpp index 19e33f92cb..6fdb8666de 100644 --- a/Sming/Components/Network/src/Network/MailMessage.cpp +++ b/Sming/Components/Network/src/Network/MailMessage.cpp @@ -48,13 +48,11 @@ MailMessage& MailMessage::setBody(String&& body, MimeType mime) noexcept MailMessage& MailMessage::setBody(IDataSourceStream* stream, MimeType mime) { - if(this->stream != nullptr) { + if(this->stream) { debug_e("MailMessage::setBody: Discarding already set stream!"); - delete this->stream; - this->stream = nullptr; } - this->stream = stream; + this->stream.reset(stream); headers[HTTP_HEADER_CONTENT_TYPE] = toString(mime); return *this; diff --git a/Sming/Components/Network/src/Network/MailMessage.h b/Sming/Components/Network/src/Network/MailMessage.h index f5357ebc9e..5d6f6eb745 100644 --- a/Sming/Components/Network/src/Network/MailMessage.h +++ b/Sming/Components/Network/src/Network/MailMessage.h @@ -38,6 +38,14 @@ class MailMessage String subject; String cc; + ~MailMessage() + { + for(auto& attachment : attachments) { + delete attachment.headers; + delete attachment.stream; + } + } + /** * @brief Set a header value * @param name @@ -106,7 +114,7 @@ class MailMessage MailMessage& addAttachment(IDataSourceStream* stream, const String& mime, const String& filename = ""); private: - IDataSourceStream* stream = nullptr; + std::unique_ptr stream = nullptr; HttpHeaders headers; Vector attachments; }; diff --git a/Sming/Components/Network/src/Network/SmtpClient.cpp b/Sming/Components/Network/src/Network/SmtpClient.cpp index a604133bd1..eb42a9fe53 100644 --- a/Sming/Components/Network/src/Network/SmtpClient.cpp +++ b/Sming/Components/Network/src/Network/SmtpClient.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #define ADVANCE \ { \ @@ -288,7 +289,7 @@ void SmtpClient::sendMailHeaders(MailMessage* mail) if(!mail->headers.contains(HTTP_HEADER_CONTENT_TRANSFER_ENCODING)) { mail->headers[HTTP_HEADER_CONTENT_TRANSFER_ENCODING] = _F("quoted-printable"); - mail->stream = new QuotedPrintableOutputStream(mail->stream); + mail->stream = std::make_unique(mail->stream.release()); } if(!mail->attachments.isEmpty()) { @@ -297,13 +298,13 @@ void SmtpClient::sendMailHeaders(MailMessage* mail) text.headers = new HttpHeaders(); (*text.headers)[HTTP_HEADER_CONTENT_TYPE] = mail->headers[HTTP_HEADER_CONTENT_TYPE]; (*text.headers)[HTTP_HEADER_CONTENT_TRANSFER_ENCODING] = mail->headers[HTTP_HEADER_CONTENT_TRANSFER_ENCODING]; - text.stream = mail->stream; + text.stream = mail->stream.release(); mail->attachments.insertElementAt(text, 0); mail->headers.remove(HTTP_HEADER_CONTENT_TRANSFER_ENCODING); mail->headers[HTTP_HEADER_CONTENT_TYPE] = F("multipart/mixed; boundary=") + mStream->getBoundary(); - mail->stream = mStream; + mail->stream.reset(mStream); } for(auto hdr : mail->headers) { @@ -319,8 +320,7 @@ bool SmtpClient::sendMailBody(MailMessage* mail) } delete stream; - stream = mail->stream; // avoid intermediate buffers - mail->stream = nullptr; + stream = mail->stream.release(); // avoid intermediate buffers return false; } @@ -390,8 +390,15 @@ int SmtpClient::smtpParse(char* buffer, size_t len) RETURN_ON_ERROR(SMTP_CODE_SERVICE_READY); if(!useSsl && (options & SMTP_OPT_STARTTLS)) { - useSsl = true; - TcpConnection::internalOnConnected(ERR_OK); + if(!enableSsl(url.Host)) { + /* + * Excerpt from RFC 3207: If, + * after having issued the STARTTLS command, the client finds out that + * some failure prevents it from actually starting a TLS handshake, then + * it SHOULD abort the connection. + */ + return 0; + } } sendString(F("EHLO ") + url.Host + "\r\n"); @@ -420,8 +427,16 @@ int SmtpClient::smtpParse(char* buffer, size_t len) if(isLastLine) { state = eSMTP_Ready; if(!useSsl && (options & SMTP_OPT_STARTTLS)) { - state = eSMTP_StartTLS; - } else if(url.User && authMethods.count()) { + if(Ssl::factory != nullptr) { + state = eSMTP_StartTLS; + break; + } + + bitClear(options, SMTP_OPT_STARTTLS); + debug_w("[SMTP] SSL required, no factory. Continue plain-text communication."); + } + + if(url.User && authMethods.count()) { state = eSMTP_SendAuth; } } diff --git a/Sming/Components/Network/src/Network/TcpConnection.cpp b/Sming/Components/Network/src/Network/TcpConnection.cpp index 0aae89fc2e..5f442a7c30 100644 --- a/Sming/Components/Network/src/Network/TcpConnection.cpp +++ b/Sming/Components/Network/src/Network/TcpConnection.cpp @@ -423,6 +423,29 @@ err_t TcpConnection::internalOnConnected(err_t err) return res; } +bool TcpConnection::enableSsl(const String& hostName) +{ + if(tcp == nullptr) { + return false; + } + + if(tcp->state != ESTABLISHED) { + return false; + } + + if(!sslCreateSession()) { + return false; + } + + ssl->hostName = hostName; + + useSsl = true; + if(internalOnConnected(ERR_OK) != ERR_OK) { + useSsl = false; + } + return useSsl; +} + err_t TcpConnection::internalOnReceive(pbuf* p, err_t err) { sleep = 0; diff --git a/Sming/Components/Network/src/Network/TcpConnection.h b/Sming/Components/Network/src/Network/TcpConnection.h index 3ea5367418..e61e8715e8 100644 --- a/Sming/Components/Network/src/Network/TcpConnection.h +++ b/Sming/Components/Network/src/Network/TcpConnection.h @@ -154,6 +154,13 @@ class TcpConnection : public IpConnection return ssl; } + /** + * @brief Enables Secure Socket Layer on the current connection + * @param hostName + * @retval true on success, false otherwise + */ + bool enableSsl(const String& hostName = nullptr); + protected: void initialize(tcp_pcb* pcb); bool internalConnect(IpAddress addr, uint16_t port); diff --git a/Sming/Components/Network/src/Network/Url.cpp b/Sming/Components/Network/src/Network/Url.cpp index b23df5aaac..722ec71648 100644 --- a/Sming/Components/Network/src/Network/Url.cpp +++ b/Sming/Components/Network/src/Network/Url.cpp @@ -86,6 +86,8 @@ String Url::toString() const result += ':'; result += Password; } + + result += '@'; } result += getHostWithPort(); diff --git a/samples/SmtpClient/app/application.cpp b/samples/SmtpClient/app/application.cpp index 1b6083cc6c..411150add1 100644 --- a/samples/SmtpClient/app/application.cpp +++ b/samples/SmtpClient/app/application.cpp @@ -24,7 +24,7 @@ int onServerError(SmtpClient& client, int code, char* status) { debugf("Status: %s", status); - return 0; // return non-zero value to abort the connection + return -1; // return non-zero value to abort the connection } int onMailSent(SmtpClient& client, int code, char* status) From f7225be62c9ef1342787e78aa9e9c8429475c1d4 Mon Sep 17 00:00:00 2001 From: Mike Date: Tue, 18 Jun 2024 09:25:32 +0100 Subject: [PATCH 076/128] Use `intptr_t` etc. when casting pointers (#2811) Assuming that, for example, `void*` can be cast to `uint32_t` and vice-versa is not portable since platforms can have different sizes. Correct types to use are `intptr_t` and `uintptr_t` which are 32-bit for all the architectures we care about, and 64-bit for modern OS builds without 32-bit compatibility mode. It also makes it clearer that values are likely cast from pointers. --- .../Host/Components/driver/include/driver/os_timer.h | 2 +- Sming/Arch/Host/Components/driver/os_timer.cpp | 2 +- Sming/Arch/Host/Components/esp_hal/include/esp_tasks.h | 8 ++++---- Sming/Arch/Host/Components/esp_hal/tasks.cpp | 2 +- .../Network/Arch/Host/Platform/StationImpl.cpp | 8 ++++---- .../Network/src/Data/Stream/ChunkedStream.cpp | 3 ++- Sming/Components/Storage/src/include/Storage/SysMem.h | 2 +- .../rboot/include/Network/RbootHttpUpdater.h | 5 +++-- Sming/Components/ssl/Axtls/AxContext.cpp | 4 ++-- Sming/Core/CallbackTimer.h | 2 +- Sming/Core/Data/Buffer/CircularBuffer.h | 2 +- Sming/Core/Task.h | 4 ++-- Sming/Libraries/FlashIP | 2 +- Sming/Libraries/IOControl | 2 +- Sming/Libraries/Spiffs/src/FileSystem.cpp | 2 +- Sming/Libraries/TFT_S1D13781 | 2 +- Sming/Libraries/UPnP | 2 +- Sming/Platform/System.cpp | 9 +++++---- Sming/Platform/System.h | 10 +++++----- Sming/Wiring/FakePgmSpace.h | 2 +- samples/Basic_ProgMem/app/application.cpp | 4 ++-- tests/HostTests/modules/Clocks.cpp | 2 +- tests/HostTests/modules/Serial.cpp | 2 +- 23 files changed, 43 insertions(+), 40 deletions(-) diff --git a/Sming/Arch/Host/Components/driver/include/driver/os_timer.h b/Sming/Arch/Host/Components/driver/include/driver/os_timer.h index 4502fc2e23..fc8ae7affc 100644 --- a/Sming/Arch/Host/Components/driver/include/driver/os_timer.h +++ b/Sming/Arch/Host/Components/driver/include/driver/os_timer.h @@ -52,7 +52,7 @@ void os_timer_setfn(os_timer_t* ptimer, os_timer_func_t* pfunction, void* parg); static inline uint64_t os_timer_expire(const os_timer_t* ptimer) { - if(ptimer == nullptr || int(ptimer->timer_next) == -1) { + if(ptimer == nullptr || intptr_t(ptimer->timer_next) == -1) { return 0; } return ptimer->timer_expire; diff --git a/Sming/Arch/Host/Components/driver/os_timer.cpp b/Sming/Arch/Host/Components/driver/os_timer.cpp index e546c35256..1d0f77f4dd 100644 --- a/Sming/Arch/Host/Components/driver/os_timer.cpp +++ b/Sming/Arch/Host/Components/driver/os_timer.cpp @@ -67,7 +67,7 @@ void os_timer_disarm(struct os_timer_t* ptimer) { assert(ptimer != nullptr); - if(int(ptimer->timer_next) == -1) { + if(intptr_t(ptimer->timer_next) == -1) { return; } diff --git a/Sming/Arch/Host/Components/esp_hal/include/esp_tasks.h b/Sming/Arch/Host/Components/esp_hal/include/esp_tasks.h index be1d289e63..48882c54da 100644 --- a/Sming/Arch/Host/Components/esp_hal/include/esp_tasks.h +++ b/Sming/Arch/Host/Components/esp_hal/include/esp_tasks.h @@ -6,8 +6,8 @@ extern "C" { #endif -typedef uint32_t os_signal_t; -typedef uint32_t os_param_t; +typedef uintptr_t os_signal_t; +typedef uintptr_t os_param_t; typedef struct { os_signal_t sig; @@ -32,9 +32,9 @@ void host_init_tasks(); // Hook function to process task queues void host_service_tasks(); -typedef void (*host_task_callback_t)(uint32_t param); +typedef void (*host_task_callback_t)(os_param_t param); -bool host_queue_callback(host_task_callback_t callback, uint32_t param); +bool host_queue_callback(host_task_callback_t callback, os_param_t param); #ifdef __cplusplus } diff --git a/Sming/Arch/Host/Components/esp_hal/tasks.cpp b/Sming/Arch/Host/Components/esp_hal/tasks.cpp index bd242df133..1c561564a0 100644 --- a/Sming/Arch/Host/Components/esp_hal/tasks.cpp +++ b/Sming/Arch/Host/Components/esp_hal/tasks.cpp @@ -114,7 +114,7 @@ void host_service_tasks() } } -bool host_queue_callback(host_task_callback_t callback, uint32_t param) +bool host_queue_callback(host_task_callback_t callback, os_param_t param) { return task_queues[HOST_TASK_PRIO]->post(os_signal_t(callback), param); } diff --git a/Sming/Components/Network/Arch/Host/Platform/StationImpl.cpp b/Sming/Components/Network/Arch/Host/Platform/StationImpl.cpp index 25a71a9021..8b496710a4 100644 --- a/Sming/Components/Network/Arch/Host/Platform/StationImpl.cpp +++ b/Sming/Components/Network/Arch/Host/Platform/StationImpl.cpp @@ -89,8 +89,8 @@ void StationImpl::initialise(netif* nif) } auto netif_callback = [](netif* nif) { - host_queue_callback([](uint32_t param) { station.statusCallback(reinterpret_cast(param)); }, - uint32_t(nif)); + host_queue_callback([](os_param_t param) { station.statusCallback(reinterpret_cast(param)); }, + os_param_t(nif)); }; netif_set_status_callback(nif, netif_callback); @@ -328,7 +328,7 @@ bool StationImpl::startScan(ScanCompletedDelegate scanCompleted) } host_queue_callback( - [](uint32_t param) { + [](os_param_t param) { auto self = reinterpret_cast(param); BssList list; for(const auto& info : apInfoList) { @@ -336,7 +336,7 @@ bool StationImpl::startScan(ScanCompletedDelegate scanCompleted) } self->scanCompletedCallback(true, list); }, - uint32_t(this)); + os_param_t(this)); return true; } diff --git a/Sming/Components/Network/src/Data/Stream/ChunkedStream.cpp b/Sming/Components/Network/src/Data/Stream/ChunkedStream.cpp index be8df057d3..8b8c14b0ce 100644 --- a/Sming/Components/Network/src/Data/Stream/ChunkedStream.cpp +++ b/Sming/Components/Network/src/Data/Stream/ChunkedStream.cpp @@ -25,7 +25,8 @@ size_t ChunkedStream::transform(const uint8_t* source, size_t sourceLength, uint } // Header - unsigned offset = m_snprintf(reinterpret_cast(target), targetLength, "%X\r\n", sourceLength); + unsigned offset = + m_snprintf(reinterpret_cast(target), targetLength, "%X\r\n", static_cast(sourceLength)); // Content memcpy(target + offset, source, sourceLength); diff --git a/Sming/Components/Storage/src/include/Storage/SysMem.h b/Sming/Components/Storage/src/include/Storage/SysMem.h index 7b6f20361d..1193e5722c 100644 --- a/Sming/Components/Storage/src/include/Storage/SysMem.h +++ b/Sming/Components/Storage/src/include/Storage/SysMem.h @@ -80,7 +80,7 @@ class SysMem : public Device */ Partition add(const String& name, const FSTR::ObjectBase& fstr, Partition::FullType type) { - return PartitionTable::add(name, type, reinterpret_cast(fstr.data()), fstr.size(), + return PartitionTable::add(name, type, reinterpret_cast(fstr.data()), fstr.size(), Partition::Flag::readOnly); } }; diff --git a/Sming/Components/rboot/include/Network/RbootHttpUpdater.h b/Sming/Components/rboot/include/Network/RbootHttpUpdater.h index fda0ce60c8..c83f13bf95 100644 --- a/Sming/Components/rboot/include/Network/RbootHttpUpdater.h +++ b/Sming/Components/rboot/include/Network/RbootHttpUpdater.h @@ -36,7 +36,7 @@ class RbootHttpUpdater : protected HttpClient size_t size; // << max allowed size std::unique_ptr stream; // (optional) output stream to use. - Item(String url, uint32_t targetOffset, size_t size, RbootOutputStream* stream) + Item(String url, size_t targetOffset, size_t size, RbootOutputStream* stream) : url(url), targetOffset(targetOffset), size(size), stream(stream) { } @@ -99,7 +99,8 @@ class RbootHttpUpdater : protected HttpClient return false; } - return items.addNew(new Item{firmwareFileUrl, stream->getStartAddress(), stream->getMaxLength(), stream}); + return items.addNew( + new Item{firmwareFileUrl, uint32_t(stream->getStartAddress()), stream->getMaxLength(), stream}); } void start(); diff --git a/Sming/Components/ssl/Axtls/AxContext.cpp b/Sming/Components/ssl/Axtls/AxContext.cpp index 7283d3d910..03195386d1 100644 --- a/Sming/Components/ssl/Axtls/AxContext.cpp +++ b/Sming/Components/ssl/Axtls/AxContext.cpp @@ -89,7 +89,7 @@ Connection* AxContext::createClient(tcp_pcb* tcp) auto id = session.getSessionId(); auto connection = new AxConnection(*this, tcp); auto client = - ssl_client_new(context, int(connection), id ? id->getValue() : nullptr, id ? id->getLength() : 0, ssl_ext); + ssl_client_new(context, intptr_t(connection), id ? id->getValue() : nullptr, id ? id->getLength() : 0, ssl_ext); if(client == nullptr) { ssl_ext_free(ssl_ext); delete connection; @@ -105,7 +105,7 @@ Connection* AxContext::createServer(tcp_pcb* tcp) assert(context != nullptr); auto connection = new AxConnection(*this, tcp); - auto server = ssl_server_new(context, int(connection)); + auto server = ssl_server_new(context, intptr_t(connection)); if(server == nullptr) { delete connection; return nullptr; diff --git a/Sming/Core/CallbackTimer.h b/Sming/Core/CallbackTimer.h index f56ef2dc58..89eed46d0a 100644 --- a/Sming/Core/CallbackTimer.h +++ b/Sming/Core/CallbackTimer.h @@ -44,7 +44,7 @@ template struct CallbackTimerApi { String s; s += typeName(); s += '@'; - s += String(uint32_t(this), HEX); + s += String(uintptr_t(this), HEX); return s; } diff --git a/Sming/Core/Data/Buffer/CircularBuffer.h b/Sming/Core/Data/Buffer/CircularBuffer.h index fad71b793e..ec1c63e0fd 100644 --- a/Sming/Core/Data/Buffer/CircularBuffer.h +++ b/Sming/Core/Data/Buffer/CircularBuffer.h @@ -80,7 +80,7 @@ class CircularBuffer : public ReadWriteStream */ String id() const override { - return String(reinterpret_cast(&buffer), HEX); + return String(reinterpret_cast(&buffer), HEX); } size_t write(uint8_t charToWrite) override; diff --git a/Sming/Core/Task.h b/Sming/Core/Task.h index 1c04dc72e0..bb82f73896 100644 --- a/Sming/Core/Task.h +++ b/Sming/Core/Task.h @@ -164,12 +164,12 @@ class Task } scheduled = System.queueCallback( - [](uint32_t param) { + [](void* param) { auto task = reinterpret_cast(param); task->scheduled = false; task->service(); }, - uint32_t(this)); + this); return scheduled; } diff --git a/Sming/Libraries/FlashIP b/Sming/Libraries/FlashIP index 1e67f9a47d..5d1dae0541 160000 --- a/Sming/Libraries/FlashIP +++ b/Sming/Libraries/FlashIP @@ -1 +1 @@ -Subproject commit 1e67f9a47d9e9f479c1b2c34d36800994515c8c2 +Subproject commit 5d1dae05417171ea23081b44a5401661bbefb599 diff --git a/Sming/Libraries/IOControl b/Sming/Libraries/IOControl index cd42d86b26..d297b3ae71 160000 --- a/Sming/Libraries/IOControl +++ b/Sming/Libraries/IOControl @@ -1 +1 @@ -Subproject commit cd42d86b26901af9fedddb04a933807c5206ee5d +Subproject commit d297b3ae715e5fac82425e7c316cbc0cdd919cd4 diff --git a/Sming/Libraries/Spiffs/src/FileSystem.cpp b/Sming/Libraries/Spiffs/src/FileSystem.cpp index 7bc5f373cb..6e79df5ee8 100644 --- a/Sming/Libraries/Spiffs/src/FileSystem.cpp +++ b/Sming/Libraries/Spiffs/src/FileSystem.cpp @@ -152,7 +152,7 @@ int FileSystem::mount() .hal_erase_f = f_erase, .phys_size = uint32_t(partSize), .phys_addr = 0, - .phys_erase_block = partition.getBlockSize(), + .phys_erase_block = uint32_t(partition.getBlockSize()), .log_block_size = logicalBlockSize, .log_page_size = LOG_PAGE_SIZE, }; diff --git a/Sming/Libraries/TFT_S1D13781 b/Sming/Libraries/TFT_S1D13781 index 3d5ddab2f7..04e19b5003 160000 --- a/Sming/Libraries/TFT_S1D13781 +++ b/Sming/Libraries/TFT_S1D13781 @@ -1 +1 @@ -Subproject commit 3d5ddab2f7dec460ce649ca69bc5ab625c2c7215 +Subproject commit 04e19b5003be0d1edacdb07b9b511e3b1de60af1 diff --git a/Sming/Libraries/UPnP b/Sming/Libraries/UPnP index 2700835f8e..17ea2953be 160000 --- a/Sming/Libraries/UPnP +++ b/Sming/Libraries/UPnP @@ -1 +1 @@ -Subproject commit 2700835f8ef23efd415751db0a7843b1626cb8da +Subproject commit 17ea2953be95336e1b0cae3090fe992caab56b0a diff --git a/Sming/Platform/System.cpp b/Sming/Platform/System.cpp index c490fc3018..63a08d95a9 100644 --- a/Sming/Platform/System.cpp +++ b/Sming/Platform/System.cpp @@ -46,9 +46,9 @@ void SystemClass::taskHandler(os_event_t* event) --taskCount; restoreInterrupts(level); #endif - auto callback = reinterpret_cast(event->sig); + auto callback = reinterpret_cast(event->sig); if(callback != nullptr) { - callback(event->par); + callback(reinterpret_cast(event->par)); } } @@ -74,7 +74,7 @@ bool SystemClass::initialize() return true; } -bool SystemClass::queueCallback(TaskCallback32 callback, uint32_t param) +bool SystemClass::queueCallback(TaskCallback callback, void* param) { if(callback == nullptr) { return false; @@ -89,7 +89,8 @@ bool SystemClass::queueCallback(TaskCallback32 callback, uint32_t param) restoreInterrupts(level); #endif - return system_os_post(USER_TASK_PRIO_1, reinterpret_cast(callback), param); + return system_os_post(USER_TASK_PRIO_1, reinterpret_cast(callback), + reinterpret_cast(param)); } bool SystemClass::queueCallback(TaskDelegate callback) diff --git a/Sming/Platform/System.h b/Sming/Platform/System.h index ac5467527b..c4cca4fd9f 100644 --- a/Sming/Platform/System.h +++ b/Sming/Platform/System.h @@ -191,15 +191,15 @@ class SystemClass * Note also that this method is typically called from interrupt context so must avoid things * like heap allocation, etc. */ - static bool IRAM_ATTR queueCallback(TaskCallback32 callback, uint32_t param = 0); + static bool IRAM_ATTR queueCallback(TaskCallback32 callback, uint32_t param = 0) + { + return queueCallback(reinterpret_cast(callback), reinterpret_cast(param)); + } /** * @brief Queue a deferred callback, with optional void* parameter */ - __forceinline static bool IRAM_ATTR queueCallback(TaskCallback callback, void* param = nullptr) - { - return queueCallback(reinterpret_cast(callback), reinterpret_cast(param)); - } + static bool IRAM_ATTR queueCallback(TaskCallback callback, void* param = nullptr); /** * @brief Queue a deferred callback with no callback parameter diff --git a/Sming/Wiring/FakePgmSpace.h b/Sming/Wiring/FakePgmSpace.h index fae56002c7..82fd39066b 100644 --- a/Sming/Wiring/FakePgmSpace.h +++ b/Sming/Wiring/FakePgmSpace.h @@ -31,7 +31,7 @@ extern "C" { /** * @brief determines if the given value is aligned to a word (4-byte) boundary */ -#define IS_ALIGNED(_x) (((uint32_t)(_x)&3) == 0) +#define IS_ALIGNED(_x) (((uintptr_t)(_x)&3) == 0) /** * @brief Align a size up to the nearest word boundary diff --git a/samples/Basic_ProgMem/app/application.cpp b/samples/Basic_ProgMem/app/application.cpp index abdc5d24f4..8bccf27b0d 100644 --- a/samples/Basic_ProgMem/app/application.cpp +++ b/samples/Basic_ProgMem/app/application.cpp @@ -119,6 +119,6 @@ void init() Serial.println("> 0x3FFE8000 ~ 0x3FFFBFFF - User data RAM, 80kb. Available to applications."); Serial.println("> 0x40200000 ~ ... - SPI Flash."); - Serial << "> demoRam array address: 0x" << String(uint32_t(demoRam), HEX) << " is in the RAM" << endl; - Serial << "> demoPgm array address: 0x" << String(uint32_t(demoPgm), HEX) << " is in the Flash" << endl; + Serial << "> demoRam array address: 0x" << String(uintptr_t(demoRam), HEX) << " is in the RAM" << endl; + Serial << "> demoPgm array address: 0x" << String(uintptr_t(demoPgm), HEX) << " is in the Flash" << endl; } diff --git a/tests/HostTests/modules/Clocks.cpp b/tests/HostTests/modules/Clocks.cpp index b6c78fc775..b16097a04a 100644 --- a/tests/HostTests/modules/Clocks.cpp +++ b/tests/HostTests/modules/Clocks.cpp @@ -50,7 +50,7 @@ template class ClockTestTemplate : public TestG // Run for a second or two and check timer ticks correspond approximately with system clock constexpr uint64_t maxDuration = Clock::maxTicks().template as() - 5000ULL; - constexpr uint32_t duration = std::min(2000000ULL, maxDuration); + constexpr uint32_t duration = std::min(uint64_t(2000000ULL), maxDuration); auto startTime = system_get_time(); startTicks = Clock::ticks(); uint32_t time; diff --git a/tests/HostTests/modules/Serial.cpp b/tests/HostTests/modules/Serial.cpp index 069e98e22a..544f1f6bf2 100644 --- a/tests/HostTests/modules/Serial.cpp +++ b/tests/HostTests/modules/Serial.cpp @@ -41,7 +41,7 @@ class SerialTest : public TestGroup String compareBuffer; String readBuffer; for(unsigned i = 0; i < 10; ++i) { - m_printf("txfree = %u\n", txbuf.getFreeSpace()); + Serial << _F("txfree = ") << txbuf.getFreeSpace() << endl; for(char c = 'a'; c <= 'z'; ++c) { if(txbuf.getFreeSpace() < 10) { readBuffer += read(); From 17db0dbc89d43894e9ed728090f83f7eb9392d68 Mon Sep 17 00:00:00 2001 From: Mike Date: Tue, 18 Jun 2024 09:25:57 +0100 Subject: [PATCH 077/128] Fix GoogleCast reference (#2808) --- Sming/Libraries/GoogleCast | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sming/Libraries/GoogleCast b/Sming/Libraries/GoogleCast index 4a95b75612..930d641cd9 160000 --- a/Sming/Libraries/GoogleCast +++ b/Sming/Libraries/GoogleCast @@ -1 +1 @@ -Subproject commit 4a95b756127799ef25697fa6bca28c35094eec52 +Subproject commit 930d641cd93862b554633e860afb6104de41b3c1 From 69c4294f70c2ffa56dda199e03c2b93c8ff0b747 Mon Sep 17 00:00:00 2001 From: Mike Date: Tue, 18 Jun 2024 09:26:22 +0100 Subject: [PATCH 078/128] Fix spiFlash type mismatch warning during init (#2809) This PR makes a small change to Host builds, enabling all `debug_x` statements during init/exit. This highlights one issue, fixed, caused by missing device entry in Rp2040, Host hardware configs: ``` [Device] 'spiFlash' type mismatch, 'unknown' in partition table but device reports 'flash' ``` --- Sming/Arch/Host/Components/hostlib/startup.cpp | 4 +++- Sming/Arch/Host/standard.hw | 7 ++++++- Sming/Arch/Rp2040/standard.hw | 9 ++++++--- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/Sming/Arch/Host/Components/hostlib/startup.cpp b/Sming/Arch/Host/Components/hostlib/startup.cpp index 3fce4bec04..f74fdf3405 100644 --- a/Sming/Arch/Host/Components/hostlib/startup.cpp +++ b/Sming/Arch/Host/Components/hostlib/startup.cpp @@ -246,6 +246,8 @@ int main(int argc, char* argv[]) } } + m_setPuts(&host_nputs); + host_debug_i("\nWelcome to the Sming Host emulator\n\n"); auto i = get_first_non_option(); @@ -309,7 +311,7 @@ int main(int argc, char* argv[]) pause(config.exitpause); // Avoid issues with debug statements whilst running exit handlers - m_setPuts(nullptr); + m_setPuts(&host_nputs); return exitCode; } diff --git a/Sming/Arch/Host/standard.hw b/Sming/Arch/Host/standard.hw index 7244be0578..287e348737 100644 --- a/Sming/Arch/Host/standard.hw +++ b/Sming/Arch/Host/standard.hw @@ -3,7 +3,12 @@ "arch": "Host", "bootloader_size": "0x2000", "partition_table_offset": "0x2000", - "options": ["4m"], + "devices": { + "spiFlash": { + "type": "flash", + "size": "4M" + } + }, "partitions": { "rom0": { "address": "0x008000", diff --git a/Sming/Arch/Rp2040/standard.hw b/Sming/Arch/Rp2040/standard.hw index d143cd3ba0..2955c37e9d 100644 --- a/Sming/Arch/Rp2040/standard.hw +++ b/Sming/Arch/Rp2040/standard.hw @@ -4,9 +4,12 @@ "arch": "Rp2040", "bootloader_size": 0, "partition_table_offset": "self.devices[0].size - 0x1000", - "options": [ - "2m" - ], + "devices": { + "spiFlash": { + "type": "flash", + "size": "2M" + } + }, "partitions": { "rom0": { "address": 0, From 7020094accbf752deee1cb930676ec7765aeab9f Mon Sep 17 00:00:00 2001 From: Mike Date: Tue, 18 Jun 2024 09:28:11 +0100 Subject: [PATCH 079/128] Extend `CsvReader` capabilities, move into new library (#2805) This PR extends the capabilities `CsvReader` class, introduced in #2403. Goals: - Extend testing, verify efficient use of memory - Add Parser capability to allow filtering and processing of (very) large files streamed via network, in files, etc. - Add seeking support to allow indexing, bookmarking, etc. - Add iterator support The code has been moved into a separate library. Changes: **Fix String move to avoid de-allocating buffers** Use longer of two buffers for result. Example: ``` String s1 = "Greater than SSO buffer length"; String s2; s1 = ""; // Buffer remains allocated s2 = std::move(s1); // The move we need to fix ``` At present this move will result in s1's buffer being de-allocated. This can lead to performance degratation due to subsequent memory reallocations where a String is being passed around as a general buffer. This change uses the larger of the two buffers when deciding how to behave. Checks added to HostTests to enforce predictable behaviour. **Add CStringArray::release method** Allows efficient conversion to a regular `String` object for manipulation. **Fix CStringArray operator+=** Must take reference, not copy - inefficient and fails when used with FlashString. **Move CsvReader into separate library** The `CsvReader` class has been moved out of `Core/Data` and into `CsvReader` which has additional capabilities. Changes to existing code: - Add ``CsvReader`` to your project's :cpp:envvar:`COMPONENT_DEPENDS` - Change ``#include `` to ``#include `` - Change ``CsvReader`` class to :cpp:class:`CSV::Reader` --- .gitmodules | 4 + Sming/Core/Data/CStringArray.h | 10 +- Sming/Core/Data/CsvReader.cpp | 124 ------------------- Sming/Core/Data/CsvReader.h | 130 +------------------- Sming/Libraries/CsvReader | 1 + Sming/Wiring/WString.cpp | 6 +- docs/source/upgrading/5.1-5.2.rst | 10 ++ samples/Basic_Templates/app/CsvTemplate.h | 4 +- samples/Basic_Templates/app/application.cpp | 3 +- samples/Basic_Templates/component.mk | 1 + tests/HostTests/modules/CStringArray.cpp | 26 ++++ tests/HostTests/modules/String.cpp | 64 ++++++++-- tests/HostTests/modules/TemplateStream.cpp | 27 ---- 13 files changed, 112 insertions(+), 298 deletions(-) delete mode 100644 Sming/Core/Data/CsvReader.cpp create mode 160000 Sming/Libraries/CsvReader diff --git a/.gitmodules b/.gitmodules index 9d62de59ac..a1404fb3cd 100644 --- a/.gitmodules +++ b/.gitmodules @@ -217,6 +217,10 @@ path = Sming/Libraries/CS5460/CS5460 url = https://github.com/xxzl0130/CS5460.git ignore = dirty +[submodule "Libraries.CsvReader"] + path = Sming/Libraries/CsvReader + url = https://github.com/mikee47/CsvReader + ignore = dirty [submodule "Libraries.DFRobotDFPlayerMini"] path = Sming/Libraries/DFRobotDFPlayerMini url = https://github.com/DFRobot/DFRobotDFPlayerMini.git diff --git a/Sming/Core/Data/CStringArray.h b/Sming/Core/Data/CStringArray.h index 27afb9a66c..3b192c6b54 100644 --- a/Sming/Core/Data/CStringArray.h +++ b/Sming/Core/Data/CStringArray.h @@ -89,6 +89,14 @@ class CStringArray : private String return *this; } + /** + * @brief Give up underlying String object to caller so it can be manipulated + */ + String release() + { + return std::move(*this); + } + /** @brief Append a new string (or array of strings) to the end of the array * @param str * @param length Length of new string in array (default is length of str) @@ -127,7 +135,7 @@ class CStringArray : private String * @brief Append numbers, etc. to the array * @param value char, int, float, etc. as supported by String */ - template CStringArray& operator+=(T value) + template CStringArray& operator+=(const T& value) { add(String(value)); return *this; diff --git a/Sming/Core/Data/CsvReader.cpp b/Sming/Core/Data/CsvReader.cpp deleted file mode 100644 index 569ce02a0b..0000000000 --- a/Sming/Core/Data/CsvReader.cpp +++ /dev/null @@ -1,124 +0,0 @@ -/**** - * Sming Framework Project - Open Source framework for high efficiency native ESP8266 development. - * Created 2015 by Skurydin Alexey - * http://github.com/SmingHub/Sming - * All files of the Sming Core are provided under the LGPL v3 license. - * - * CsvReader.cpp - * - * @author: 2021 - Mikee47 - * - ****/ - -#include "CsvReader.h" -#include - -void CsvReader::reset() -{ - source->seekFrom(0, SeekOrigin::Start); - if(!userHeadingsProvided) { - readRow(); - headings = row; - } - row = nullptr; -} - -bool CsvReader::readRow() -{ - constexpr size_t blockSize{512}; - - String buffer(std::move(reinterpret_cast(row))); - constexpr char quoteChar{'"'}; - enum class FieldKind { - unknown, - quoted, - unquoted, - }; - FieldKind fieldKind{}; - bool escape{false}; - bool quote{false}; - char lc{'\0'}; - unsigned writepos{0}; - - while(true) { - if(buffer.length() == maxLineLength) { - debug_w("[CSV] Line buffer limit reached %u", maxLineLength); - return false; - } - size_t buflen = std::min(writepos + blockSize, maxLineLength); - if(!buffer.setLength(buflen)) { - debug_e("[CSV] Out of memory %u", buflen); - return false; - } - auto len = source->readBytes(buffer.begin() + writepos, buflen - writepos); - if(len == 0) { - if(writepos == 0) { - return false; - } - buffer.setLength(writepos); - row = std::move(buffer); - return true; - } - buflen = writepos + len; - unsigned readpos = writepos; - - for(; readpos < buflen; ++readpos) { - char c = buffer[readpos]; - if(escape) { - switch(c) { - case 'n': - c = '\n'; - break; - case 'r': - c = '\r'; - break; - case 't': - c = '\t'; - break; - default:; - // Just accept character - } - escape = false; - } else { - if(fieldKind == FieldKind::unknown) { - if(c == quoteChar) { - fieldKind = FieldKind::quoted; - quote = true; - lc = '\0'; - continue; - } - fieldKind = FieldKind::unquoted; - } - if(c == quoteChar) { - quote = !quote; - if(fieldKind == FieldKind::quoted) { - if(lc == quoteChar) { - buffer[writepos++] = c; - lc = '\0'; - } else { - lc = c; - } - continue; - } - } else if(c == '\\') { - escape = true; - continue; - } else if(!quote) { - if(c == fieldSeparator) { - c = '\0'; - fieldKind = FieldKind::unknown; - } else if(c == '\r') { - continue; - } else if(c == '\n') { - source->seekFrom(readpos + 1 - buflen, SeekOrigin::Current); - buffer.setLength(writepos); - row = std::move(buffer); - return true; - } - } - } - buffer[writepos++] = c; - lc = c; - } - } -} diff --git a/Sming/Core/Data/CsvReader.h b/Sming/Core/Data/CsvReader.h index 596a62a27e..2fcd76036a 100644 --- a/Sming/Core/Data/CsvReader.h +++ b/Sming/Core/Data/CsvReader.h @@ -12,132 +12,4 @@ #pragma once -#include "Stream/DataSourceStream.h" -#include "CStringArray.h" -#include - -/** - * @brief Class to parse a CSV file - * - * Spec: https://www.ietf.org/rfc/rfc4180.txt - * - * 1. Each record is located on a separate line - * 2. Line ending for last record in the file is optional - * 3. Field headings are provided either in the source data or in constructor (but not both) - * 4. Fields separated with ',' and whitespace considered part of field content - * 5. Fields may or may not be quoted - if present, will be removed during parsing - * 6. Fields may contain line breaks, quotes or commas - * 7. Quotes may be escaped thus "" if field itself is quoted - * - * Additional features: - * - * - Line breaks can be \n or \r\n - * - Escapes codes within fields will be converted: \n \r \t \", \\ - * - Field separator can be changed in constructor - */ -class CsvReader -{ -public: - /** - * @brief Construct a CSV reader - * @param source Stream to read CSV text from - * @param fieldSeparator - * @param headings Required if source data does not contain field headings as first row - * @param maxLineLength Limit size of buffer to guard against malformed data - */ - CsvReader(IDataSourceStream* source, char fieldSeparator = ',', const CStringArray& headings = nullptr, - size_t maxLineLength = 2048) - : source(source), fieldSeparator(fieldSeparator), userHeadingsProvided(headings), maxLineLength(maxLineLength), - headings(headings) - { - reset(); - } - - /** - * @brief Reset reader to start of CSV file - * - * Cursor is set to 'before start'. - * Call 'next()' to fetch first record. - */ - void reset(); - - /** - * @brief Seek to next record - */ - bool next() - { - return readRow(); - } - - /** - * @brief Get number of columns - */ - unsigned count() const - { - return headings.count(); - } - - /** - * @brief Get a value from the current row - * @param index Column index, starts at 0 - * @retval const char* nullptr if index is not valid - */ - const char* getValue(unsigned index) - { - return row[index]; - } - - /** - * @brief Get a value from the current row - * @param index Column name - * @retval const char* nullptr if name is not found - */ - const char* getValue(const char* name) - { - return getValue(getColumn(name)); - } - - /** - * @brief Get index of column given its name - * @param name Column name to find - * @retval int -1 if name is not found - */ - int getColumn(const char* name) - { - return headings.indexOf(name); - } - - /** - * @brief Determine if row is valid - */ - explicit operator bool() const - { - return bool(row); - } - - /** - * @brief Get headings - */ - const CStringArray& getHeadings() const - { - return headings; - } - - /** - * @brief Get current row - */ - const CStringArray& getRow() const - { - return row; - } - -private: - bool readRow(); - - std::unique_ptr source; - char fieldSeparator; - bool userHeadingsProvided; - size_t maxLineLength; - CStringArray headings; - CStringArray row; -}; +static_assert(false, "CsvReader class has been moved to the CsvReader library."); diff --git a/Sming/Libraries/CsvReader b/Sming/Libraries/CsvReader new file mode 160000 index 0000000000..8f4d416442 --- /dev/null +++ b/Sming/Libraries/CsvReader @@ -0,0 +1 @@ +Subproject commit 8f4d416442292927d15fe00d80130fb2fc7d8bb6 diff --git a/Sming/Wiring/WString.cpp b/Sming/Wiring/WString.cpp index b2d3b83aac..401851540b 100644 --- a/Sming/Wiring/WString.cpp +++ b/Sming/Wiring/WString.cpp @@ -276,16 +276,14 @@ void String::move(String& rhs) (void)reserve(rhs_len); } - // If we already have capacity, copy the data and free rhs buffers - if(capacity() >= rhs_len) { + // If we have more capacity than the target, copy the data and free rhs buffers + if(rhs.sso.set || capacity() > rhs.capacity()) { memmove(buffer(), rhs.buffer(), rhs_len); setlen(rhs_len); rhs.invalidate(); return; } - assert(!rhs.sso.set); - // We don't have enough space so perform a pointer swap if(!sso.set) { free(ptr.buffer); diff --git a/docs/source/upgrading/5.1-5.2.rst b/docs/source/upgrading/5.1-5.2.rst index 96f7fc41a4..cc8815a84f 100644 --- a/docs/source/upgrading/5.1-5.2.rst +++ b/docs/source/upgrading/5.1-5.2.rst @@ -91,3 +91,13 @@ These will now fail to compile. Copy construction and assignment has been explicitly deleted so avoid unintentional side-effects. Objects should always be passed by reference. + + +**CsvReader library** + +The :cpp:type:`CsvReader` class has been moved out of ``Core/Data`` and into :library:`CsvReader` +which has additional capabilities. Changes to existing code: + +- Add ``CsvReader`` to your project's :cpp:envvar:`COMPONENT_DEPENDS` +- Change ``#include `` to ``#include `` +- Change ``CsvReader`` class to :cpp:class:`CSV::Reader` diff --git a/samples/Basic_Templates/app/CsvTemplate.h b/samples/Basic_Templates/app/CsvTemplate.h index b29b4ae349..cdf56fdd8a 100644 --- a/samples/Basic_Templates/app/CsvTemplate.h +++ b/samples/Basic_Templates/app/CsvTemplate.h @@ -1,6 +1,6 @@ #pragma once -#include +#include #include /** @@ -35,5 +35,5 @@ class CsvTemplate : public SectionTemplate } private: - CsvReader csv; + CSV::Reader csv; }; diff --git a/samples/Basic_Templates/app/application.cpp b/samples/Basic_Templates/app/application.cpp index efca95a384..a2bbae2d40 100644 --- a/samples/Basic_Templates/app/application.cpp +++ b/samples/Basic_Templates/app/application.cpp @@ -1,7 +1,6 @@ #include #include #include -#include #include "CsvTemplate.h" namespace @@ -51,7 +50,7 @@ void printCars() void printClassics(const FlashString& templateSource, Format::Formatter& formatter) { // The CSV data source - CsvReader csv(new FileStream(Filename::classics_csv)); + CSV::Reader csv(new FileStream(Filename::classics_csv)); // Use a regular SectionTemplate class to process the template SectionTemplate tmpl(new FSTR::Stream(templateSource)); diff --git a/samples/Basic_Templates/component.mk b/samples/Basic_Templates/component.mk index 092ca88129..bcb5ca8f81 100644 --- a/samples/Basic_Templates/component.mk +++ b/samples/Basic_Templates/component.mk @@ -1,3 +1,4 @@ +COMPONENT_DEPENDS := CsvReader HWCONFIG := basic_templates DISABLE_NETWORK := 1 diff --git a/tests/HostTests/modules/CStringArray.cpp b/tests/HostTests/modules/CStringArray.cpp index 61c8394099..bb640bf83f 100644 --- a/tests/HostTests/modules/CStringArray.cpp +++ b/tests/HostTests/modules/CStringArray.cpp @@ -24,6 +24,7 @@ class CStringArrayTest : public TestGroup "b\0" "c\0" "d\0"); + DEFINE_FSTR_LOCAL(FS_BasicJoined, "a,b,c,d") TEST_CASE("Empty construction") { @@ -231,6 +232,31 @@ class CStringArrayTest : public TestGroup csa.add(F("test\0again")); REQUIRE_EQ(csa.join("}+{"), "a}+{}+{test}+{again"); REQUIRE_EQ(csa.join(nullptr), "atestagain"); + + csa = FS_Basic; + REQUIRE_EQ(csa.join(), FS_BasicJoined); + } + + TEST_CASE("release") + { + CStringArray csa(FS_Basic); + csa += FS_Basic; // Allocate > SSO + Serial << csa.join() << endl; + auto cstrWant = csa.c_str(); + String s = csa.release(); + REQUIRE(!csa); + REQUIRE(s.c_str() == cstrWant); + + REQUIRE(s == String(FS_Basic) + FS_Basic); + + csa = std::move(s); + REQUIRE(csa == String(FS_Basic) + FS_Basic); + + String js; + js += FS_BasicJoined; + js += ','; + js += FS_BasicJoined; + REQUIRE(csa.join() == js); } } }; diff --git a/tests/HostTests/modules/String.cpp b/tests/HostTests/modules/String.cpp index 8c48e8e5e5..b453142c27 100644 --- a/tests/HostTests/modules/String.cpp +++ b/tests/HostTests/modules/String.cpp @@ -15,6 +15,7 @@ class StringTest : public TestGroup nonTemplateTest(); testString(); + testMove(); testMakeHexString(); } @@ -154,17 +155,62 @@ class StringTest : public TestGroup } } + void testMove() + { + DEFINE_FSTR_LOCAL(shortText, "Not long") + DEFINE_FSTR_LOCAL(longText, "Greater than SSO buffer length") + + TEST_CASE("Move into unassigned string") + { + // Normal move + String s1 = longText; + String s2 = std::move(s1); + REQUIRE(!s1); + REQUIRE(s2.length() == longText.length()); + } + + TEST_CASE("Move between allocated strings of same length") + { + String s1 = longText; + auto cstrWant = s1.c_str(); + String s2 = std::move(s1); + REQUIRE(s2.c_str() == cstrWant); + } + + TEST_CASE("Move to allocated string of shorter length") + { + String s1 = longText; + String s2 = shortText; + auto cstrWant = s1.c_str(); + s2 = std::move(s1); + REQUIRE(s2.c_str() == cstrWant); + } + + TEST_CASE("Move to allocated string of longer length") + { + String s1 = longText; + String s2; + auto cstrWant = s1.c_str(); + s1 = ""; // Buffer remains allocated + s2 = std::move(s1); + REQUIRE(s2.c_str() == cstrWant); + } + } + void testMakeHexString() { - uint8_t hwaddr[] = {0xaa, 0xbb, 0xcc, 0xdd, 0x12, 0x55, 0x00}; - REQUIRE(makeHexString(nullptr, 6) == String::empty); - REQUIRE(makeHexString(hwaddr, 0) == String::empty); - REQUIRE(makeHexString(hwaddr, 6) == F("aabbccdd1255")); - REQUIRE(makeHexString(hwaddr, 6, ':') == F("aa:bb:cc:dd:12:55")); - REQUIRE(makeHexString(hwaddr, 7) == F("aabbccdd125500")); - REQUIRE(makeHexString(hwaddr, 7, ':') == F("aa:bb:cc:dd:12:55:00")); - REQUIRE(makeHexString(hwaddr, 1, ':') == F("aa")); - REQUIRE(makeHexString(hwaddr, 0, ':') == String::empty); + TEST_CASE("makeHexString") + { + uint8_t hwaddr[] = {0xaa, 0xbb, 0xcc, 0xdd, 0x12, 0x55, 0x00}; + REQUIRE(makeHexString(nullptr, 6) == String::empty); + REQUIRE(makeHexString(hwaddr, 0) == String::empty); + REQUIRE(makeHexString(hwaddr, 6) == F("aabbccdd1255")); + REQUIRE(makeHexString(hwaddr, 6, ':') == F("aa:bb:cc:dd:12:55")); + REQUIRE(makeHexString(hwaddr, 7) == F("aabbccdd125500")); + REQUIRE(makeHexString(hwaddr, 7, ':') == F("aa:bb:cc:dd:12:55:00")); + REQUIRE(makeHexString(hwaddr, 1, ':') == F("aa")); + REQUIRE(makeHexString(hwaddr, 0, ':') == String::empty); + } } }; diff --git a/tests/HostTests/modules/TemplateStream.cpp b/tests/HostTests/modules/TemplateStream.cpp index 66f1a6c9bc..36c725dd5a 100644 --- a/tests/HostTests/modules/TemplateStream.cpp +++ b/tests/HostTests/modules/TemplateStream.cpp @@ -3,7 +3,6 @@ #include #include #include -#include #ifdef ARCH_HOST #include @@ -23,12 +22,6 @@ DEFINE_FSTR_LOCAL(template3_1, "Document Title

cEX(~Z)<_6h(maMNY4LYsk?6~y9_NEtB(0RS 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