diff --git a/.gitignore b/.gitignore index 9a00b9b..bdf120b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,9 @@ build .DS_Store sdkconfig.old desktop.ini -.PVS-Studio -sdkconfig \ No newline at end of file +sdkconfig +dependencies.lock +managed_components +commit-rules.md +readme-rules.md +wiki-rules.md \ No newline at end of file diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 9f4cfe4..0000000 --- a/.gitmodules +++ /dev/null @@ -1,27 +0,0 @@ -[submodule "components/zh_inclinometer"] - path = components/zh_inclinometer - url = https://github.com/aZholtikov/zh_inclinometer -[submodule "components/zh_encoder"] - path = components/zh_encoder - url = https://github.com/aZholtikov/zh_encoder -[submodule "components/zh_160x_i2c"] - path = components/zh_160x_i2c - url = https://github.com/aZholtikov/zh_160x_i2c -[submodule "components/zh_pcf8574"] - path = components/zh_pcf8574 - url = https://github.com/aZholtikov/zh_pcf8574 -[submodule "components/zh_vector"] - path = components/zh_vector - url = https://github.com/aZholtikov/zh_vector -[submodule "components/zh_ota_server"] - path = components/zh_ota_server - url = https://github.com/aZholtikov/zh_ota_server -[submodule "components/zh_espnow"] - path = components/zh_espnow - url = https://github.com/aZholtikov/zh_espnow -[submodule "components/zh_tachometer"] - path = components/zh_tachometer - url = https://github.com/aZholtikov/zh_tachometer -[submodule "components/zh_syslog"] - path = components/zh_syslog - url = https://github.com/aZholtikov/zh_syslog diff --git a/README.md b/README.md index 6c81274..d149c0f 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,31 @@ # Universal Optical 2-Channel Angle Gauge / Tachometer -## Tested on +## Features -1. [ESP32 ESP-IDF v5.5.3](https://docs.espressif.com/projects/esp-idf/en/v5.5.3/esp32/index.html) +1. Two independent measuring channels for optical encoders with 3600 pulses per revolution. +2. Four operating modes selected through the on-device menu: angle/angle, tacho/tacho, angle/tacho and remote. +3. Angle measurement with 0.1 degree resolution and resettable current angle value. +4. Rotational speed measurement in RPM with 100 ms refresh period. +5. 16x2 LCD display on PCF8574 I2C expander with boot animation, progress bar and live value updates. +6. Rotary encoder menu for mode switching and push-button long press for angle reset. +7. WiFi access point and station modes running simultaneously. +8. Over-the-Air firmware updates through the built-in web server with rollback self-check. +9. Remote syslog logging of task stack usage and driver error counters after station connection. +10. FreeRTOS multitasking with separate tasks for each channel, encoder, LCD expander and statistics. -## SAST Tools +## Attention -[PVS-Studio](https://pvs-studio.com/pvs-studio/?utm_source=website&utm_medium=github&utm_campaign=open_source) - static analyzer for C, C++, C#, and Java code. +1. Remote mode via ESP-NOW is not implemented yet. +2. WiFi station credentials are hardcoded in the firmware configuration. +3. Long press of the encoder button resets the angle value, short press switches the menu mode. +4. The GPIO pin mapping matches the ATE0005 board connectors and must not be changed without a hardware revision. -## Build and flash +## Using -Run the following command to firmware build and flash module: +Clone and build the project: -```text -cd your_projects_folder -git clone --recurse-submodules https://github.com/aZholtikov/ate0005 +```bash +git clone https://github.com/aZholtikov/ate0005 cd ate0005 -idf.py build -idf.py flash +build ``` diff --git a/components/zh_160x_i2c b/components/zh_160x_i2c deleted file mode 160000 index 19fedfc..0000000 --- a/components/zh_160x_i2c +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 19fedfc816854bc1437f9e216d4f11d1bf933f2c diff --git a/components/zh_encoder b/components/zh_encoder deleted file mode 160000 index 911d402..0000000 --- a/components/zh_encoder +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 911d402e47dc585db53b2ddde73ce53c7b6f954e diff --git a/components/zh_espnow b/components/zh_espnow deleted file mode 160000 index af76d31..0000000 --- a/components/zh_espnow +++ /dev/null @@ -1 +0,0 @@ -Subproject commit af76d3172f05b5b9ad73006e18abe333898097a6 diff --git a/components/zh_inclinometer b/components/zh_inclinometer deleted file mode 160000 index e05bc42..0000000 --- a/components/zh_inclinometer +++ /dev/null @@ -1 +0,0 @@ -Subproject commit e05bc424cf5ac6839d32302fb561c8b36c91fd7b diff --git a/components/zh_ota_server b/components/zh_ota_server deleted file mode 160000 index 9cf8f35..0000000 --- a/components/zh_ota_server +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 9cf8f359abbea13ef7eda2263361f9b9913c04d6 diff --git a/components/zh_pcf8574 b/components/zh_pcf8574 deleted file mode 160000 index 60ee3f5..0000000 --- a/components/zh_pcf8574 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 60ee3f5b4baf28b38c282b5411f56616e466186e diff --git a/components/zh_syslog b/components/zh_syslog deleted file mode 160000 index 0c4f929..0000000 --- a/components/zh_syslog +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 0c4f9297ead26cb2d7ad52bc9f4523e91b1ed42b diff --git a/components/zh_tachometer b/components/zh_tachometer deleted file mode 160000 index 935d3b4..0000000 --- a/components/zh_tachometer +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 935d3b4b97a3816d2112ce9a5ed362e660c3e5d8 diff --git a/components/zh_vector b/components/zh_vector deleted file mode 160000 index 43d818a..0000000 --- a/components/zh_vector +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 43d818a8d542923ef2aa0dc6083f5972ac8fbb3e diff --git a/main/ate0005.c b/main/ate0005.c index 91e346d..bd99606 100644 --- a/main/ate0005.c +++ b/main/ate0005.c @@ -1,46 +1,261 @@ #include "ate0005.h" -static task_stats_t _task_stats = {A_INCLINOMETER_TASK_STACK_SIZE, B_INCLINOMETER_TASK_STACK_SIZE, A_TACHOMETER_TASK_STACK_SIZE, B_TACHOMETER_TASK_STACK_SIZE}; +/** + * @brief Internal structure holding the tasks minimum free stack sizes. + * + * Each field stores the high water mark of the corresponding processing + * task, reported by uxTaskGetStackHighWaterMark(). The statistics task reads + * these values to log stack usage and to raise a warning when a value falls + * below the threshold. + */ +typedef struct +{ + uint32_t a_inclinometer_task_min_stack_size; /*!< Minimum free stack size of the A inclinometer task */ + uint32_t b_inclinometer_task_min_stack_size; /*!< Minimum free stack size of the B inclinometer task */ + uint32_t a_tachometer_task_min_stack_size; /*!< Minimum free stack size of the A tachometer task */ + uint32_t b_tachometer_task_min_stack_size; /*!< Minimum free stack size of the B tachometer task */ +} _task_stats_t; -static httpd_handle_t _http_server_handle = NULL; - -static zh_pcf8574_handle_t _lcd_handle = {0}; -static zh_encoder_handle_t _mode_encoder_handle = {0}; -static zh_inclinometer_handle_t _a_inclinometer_handle = {0}; -static zh_inclinometer_handle_t _b_inclinometer_handle = {0}; -static zh_tachometer_handle_t _a_tachometer_handle = {0}; -static zh_tachometer_handle_t _b_tachometer_handle = {0}; -static esp_timer_handle_t _inclinometer_reset_timer_handle = {0}; - -static SemaphoreHandle_t _lcd_mutex = NULL; -static TaskHandle_t _a_inclinometer_task_handle = NULL; -static TaskHandle_t _b_inclinometer_task_handle = NULL; -static TaskHandle_t _a_tachometer_task_handle = NULL; -static TaskHandle_t _b_tachometer_task_handle = NULL; -static TaskHandle_t _zh_stats_task_handle = NULL; +static httpd_handle_t _http_server_handle = NULL; /*!< HTTP server handle */ +static zh_pcf8574_handle_t *_lcd_handle = NULL; /*!< PCF8574 LCD expander handle */ +static zh_160x_i2c_handle_t *_lcd_160x_handle = NULL; /*!< 16x2 LCD display handle */ +static zh_encoder_handle_t *_mode_encoder_handle = NULL; /*!< Mode selector rotary encoder handle */ +static zh_inclinometer_handle_t *_a_inclinometer_handle = NULL; /*!< A channel inclinometer handle */ +static zh_inclinometer_handle_t *_b_inclinometer_handle = NULL; /*!< B channel inclinometer handle */ +static zh_tachometer_handle_t *_a_tachometer_handle = NULL; /*!< A channel tachometer handle */ +static zh_tachometer_handle_t *_b_tachometer_handle = NULL; /*!< B channel tachometer handle */ +static esp_timer_handle_t _inclinometer_reset_timer_handle = {0}; /*!< Inclinometer reset software timer handle */ +static SemaphoreHandle_t _lcd_mutex = NULL; /*!< Mutex to protect the LCD display access */ +static TaskHandle_t _a_inclinometer_task_handle = NULL; /*!< Handle of the A inclinometer processing task */ +static TaskHandle_t _b_inclinometer_task_handle = NULL; /*!< Handle of the B inclinometer processing task */ +static TaskHandle_t _a_tachometer_task_handle = NULL; /*!< Handle of the A tachometer processing task */ +static TaskHandle_t _b_tachometer_task_handle = NULL; /*!< Handle of the B tachometer processing task */ +static TaskHandle_t _zh_stats_task_handle = NULL; /*!< Handle of the statistics processing task */ +/** + * @brief Initialize WiFi in AP+STA mode. + * + * Creates default AP and STA netifs, configures the AP with the SSID, + * password, channel and maximum number of connections and the STA with the + * SSID and password. Registers the Wi-Fi event handler and starts Wi-Fi. + */ static void _zh_wifi_init(void); + +/** + * @brief Initialize the web server with OTA support. + * + * Starts the HTTP server with default configuration and registers the OTA + * update page handler at the root URI. + */ static void _zh_webserver_init(void); + +/** + * @brief Initialize the ESP-NOW wireless protocol. + * + * Not implemented yet. + */ static void _zh_espnow_init(void); + +/** + * @brief Initialize the mode selector rotary encoder. + * + * Registers the encoder event handler and initializes the encoder with the + * task configuration, GPIO numbers, queue size and position limits + * corresponding to the number of main menu entries. + */ static void _zh_encoder_init(void); + +/** + * @brief Initialize the A channel inclinometer. + * + * Initializes the inclinometer with the A channel optical device GPIO numbers + * and the encoder pulse count, then creates the A inclinometer processing + * task. + */ static void _zh_a_inclinometer_init(void); + +/** + * @brief Initialize the B channel inclinometer. + * + * Initializes the inclinometer with the B channel optical device GPIO numbers + * and the encoder pulse count, then creates the B inclinometer processing + * task. + */ static void _zh_b_inclinometer_init(void); + +/** + * @brief Initialize the A channel tachometer. + * + * Initializes the tachometer with the A channel optical device GPIO numbers + * and the encoder pulse count, then creates the A tachometer processing + * task. + */ static void _zh_a_tachometer_init(void); + +/** + * @brief Initialize the B channel tachometer. + * + * Initializes the tachometer with the B channel optical device GPIO numbers + * and the encoder pulse count, then creates the B tachometer processing + * task. + */ static void _zh_b_tachometer_init(void); + +/** + * @brief Initialize the PCF8574 I2C expander of the LCD display. + * + * Creates the I2C master bus, initializes the PCF8574 expander with the task + * configuration and I2C address, initializes the 16x2 LCD driver on top of + * it and creates the LCD access mutex. + */ static void _zh_pcf8574_init(void); + +/** + * @brief Show the system loading screen on the LCD display. + * + * Displays the manufacturer name, project name and firmware version, then an + * animated progress bar. After loading, shows the current operating mode + * label. + */ static void _zh_system_load(void); + +/** + * @brief Create the inclinometer reset timer. + * + * Creates a one-shot software timer that resets the inclinometer angle on + * long press of the mode encoder button. + */ static void _zh_timer_init(void); + +/** + * @brief Handle the mode encoder events. + * + * On button press starts the inclinometer reset timer and enters reset mode. + * On button release stops the timer and either ignores the event if the reset + * already occurred, or toggles the menu mode. + * + * Entering the menu mode stops the tasks and deinitializes the active devices + * of the current operating mode and enables the LCD cursor. Leaving the menu + * mode initializes the devices of the selected mode. + * + * On encoder rotation in the menu mode updates and displays the selected menu + * entry, otherwise restores the encoder position to the current menu value. + * + * @param arg User data registered with the event handler (unused) + * @param event_base Event base of the registered event + * @param event_id ID of the encoder or button event + * @param event_data Pointer to the event payload structure + */ static void _zh_encoder_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data); + +/** + * @brief Handle the ESP-NOW events. + * + * Not implemented yet. + * + * @param arg User data registered with the event handler (unused) + * @param event_base Event base of the registered event + * @param event_id ID of the ESP-NOW event (unused) + * @param event_data Pointer to the event payload (unused) + */ static void _zh_espnow_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data); + +/** + * @brief Handle the Wi-Fi station events. + * + * Initiates connection on station start, reconnects with a limited number of + * retries on disconnection and stops the syslog statistics task. On IP + * assignment resets the retry counter, initializes the syslog client and + * creates the statistics processing task. + * + * @param arg User data registered with the event handler (unused) + * @param event_base Event base of the registered event + * @param event_id ID of the Wi-Fi or IP event + * @param event_data Pointer to the event payload structure (unused) + */ static void _zh_wifi_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data); + +/** + * @brief Inclinometer reset timer callback. + * + * Called on long press of the mode encoder button. Resets the angle of the + * active inclinometers of the current operating mode when the device is not + * in the menu mode. + * + * @param arg User data registered with the timer (unused) + */ static void _zh_inclinometer_reset_timer_on_alarm_cb(void *arg); +/** + * @brief A channel inclinometer processing task. + * + * Displays the A channel angle with 1 decimal place on the second LCD row. + * The value is refreshed only when it changes. Updates the task minimum free + * stack size for the statistics task. + * + * @param pvParameter Task parameter (unused) + */ static void _zh_a_inclinometer_processing_task(void *pvParameter); + +/** + * @brief B channel inclinometer processing task. + * + * Displays the B channel angle with 1 decimal place on the second LCD row. + * The value is refreshed only when it changes. Updates the task minimum free + * stack size for the statistics task. + * + * @param pvParameter Task parameter (unused) + */ static void _zh_b_inclinometer_processing_task(void *pvParameter); + +/** + * @brief A channel tachometer processing task. + * + * Displays the A channel RPM on the second LCD row. The value is refreshed + * only when it changes. Updates the task minimum free stack size for the + * statistics task. + * + * @param pvParameter Task parameter (unused) + */ static void _zh_a_tachometer_processing_task(void *pvParameter); + +/** + * @brief B channel tachometer processing task. + * + * Displays the B channel RPM on the second LCD row. The value is refreshed + * only when it changes. Updates the task minimum free stack size for the + * statistics task. + * + * @param pvParameter Task parameter (unused) + */ static void _zh_b_tachometer_processing_task(void *pvParameter); + +/** + * @brief Statistics processing task. + * + * Periodically sends the tasks minimum free stack sizes and the component + * error counters to the remote syslog server. Messages are logged with the + * warning severity when a stack size falls below the threshold or an error + * counter is not zero. + * + * @param pvParameter Task parameter (unused) + */ static void _zh_stats_processing_task(void *pvParameter); +/** + * @brief Main application entry point. + * + * Initializes NVS flash with erase and re-init on empty or version mismatch, + * the default event loop, the PCF8574 LCD expander, the system loading + * screen, the mode encoder, the inclinometer reset timer, the A and B + * inclinometers, WiFi in AP+STA mode, the web server and ESP-NOW. + * + * Then performs the OTA rollback self-check: if the running image is pending + * verification, waits 60 seconds and marks it as valid. + */ +void app_main(void); + +volatile static _task_stats_t _task_stats = {A_INCLINOMETER_TASK_STACK_SIZE, B_INCLINOMETER_TASK_STACK_SIZE, A_TACHOMETER_TASK_STACK_SIZE, B_TACHOMETER_TASK_STACK_SIZE}; static const char *_main_menu[] = {MAIN_MENU_ANGLE_ANGLE_NAME, MAIN_MENU_TACHO_TACHO_NAME, MAIN_MENU_ANGLE_TACHO_NAME, MAIN_MENU_REMOTE_NAME}; volatile static bool _is_menu_mode = false; volatile static bool _is_reset_mode = false; @@ -51,8 +266,12 @@ volatile static bool _is_wifi_connected = false; void app_main(void) { - _lcd_mutex = xSemaphoreCreateMutex(); - nvs_flash_init(); + esp_err_t err = nvs_flash_init(); + if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) + { + ESP_ERROR_CHECK(nvs_flash_erase()); + ESP_ERROR_CHECK(nvs_flash_init()); + } esp_event_loop_create_default(); _zh_pcf8574_init(); _zh_system_load(); @@ -108,7 +327,6 @@ static void _zh_webserver_init(void) static void _zh_espnow_init(void) { - // To Do. } static void _zh_encoder_init(void) @@ -121,7 +339,6 @@ static void _zh_encoder_init(void) config.a_gpio_number = MODE_ENCODER_A_GPIO; config.b_gpio_number = MODE_ENCODER_B_GPIO; config.s_gpio_number = MODE_ENCODER_S_GPIO; - config.s_gpio_debounce_time = 1000; config.encoder_min_value = 0; config.encoder_max_value = (sizeof(_main_menu) / sizeof(_main_menu[0])) - 1; config.encoder_step = 1; @@ -137,8 +354,8 @@ static void _zh_a_inclinometer_init(void) config.b_gpio_number = A_OPTICAL_DEVICE_B_GPIO; config.encoder_pulses = 3600; zh_inclinometer_init(&config, &_a_inclinometer_handle); - xTaskCreatePinnedToCore(&_zh_a_inclinometer_processing_task, "a_inclinometer_processing_task", A_INCLINOMETER_TASK_STACK_SIZE, NULL, A_INCLINOMETER_TASK_PRIORITY, - &_a_inclinometer_task_handle, tskNO_AFFINITY); + xTaskCreatePinnedToCore(&_zh_a_inclinometer_processing_task, "a_inclinometer_processing_task", A_INCLINOMETER_TASK_STACK_SIZE, NULL, + A_INCLINOMETER_TASK_PRIORITY, &_a_inclinometer_task_handle, tskNO_AFFINITY); } static void _zh_b_inclinometer_init(void) @@ -148,8 +365,8 @@ static void _zh_b_inclinometer_init(void) config.b_gpio_number = B_OPTICAL_DEVICE_B_GPIO; config.encoder_pulses = 3600; zh_inclinometer_init(&config, &_b_inclinometer_handle); - xTaskCreatePinnedToCore(&_zh_b_inclinometer_processing_task, "b_inclinometer_processing_task", B_INCLINOMETER_TASK_STACK_SIZE, NULL, B_INCLINOMETER_TASK_PRIORITY, - &_b_inclinometer_task_handle, tskNO_AFFINITY); + xTaskCreatePinnedToCore(&_zh_b_inclinometer_processing_task, "b_inclinometer_processing_task", B_INCLINOMETER_TASK_STACK_SIZE, NULL, + B_INCLINOMETER_TASK_PRIORITY, &_b_inclinometer_task_handle, tskNO_AFFINITY); } static void _zh_a_tachometer_init(void) @@ -159,8 +376,8 @@ static void _zh_a_tachometer_init(void) config.b_gpio_number = A_OPTICAL_DEVICE_B_GPIO; config.encoder_pulses = 3600; zh_tachometer_init(&config, &_a_tachometer_handle); - xTaskCreatePinnedToCore(&_zh_a_tachometer_processing_task, "a_tachometer_processing_task", A_TACHOMETER_TASK_STACK_SIZE, NULL, A_TACHOMETER_TASK_PRIORITY, - &_a_tachometer_task_handle, tskNO_AFFINITY); + xTaskCreatePinnedToCore(&_zh_a_tachometer_processing_task, "a_tachometer_processing_task", A_TACHOMETER_TASK_STACK_SIZE, NULL, + A_TACHOMETER_TASK_PRIORITY, &_a_tachometer_task_handle, tskNO_AFFINITY); } static void _zh_b_tachometer_init(void) @@ -170,8 +387,8 @@ static void _zh_b_tachometer_init(void) config.b_gpio_number = B_OPTICAL_DEVICE_B_GPIO; config.encoder_pulses = 3600; zh_tachometer_init(&config, &_b_tachometer_handle); - xTaskCreatePinnedToCore(&_zh_b_tachometer_processing_task, "b_tachometer_processing_task", B_TACHOMETER_TASK_STACK_SIZE, NULL, B_TACHOMETER_TASK_PRIORITY, - &_b_tachometer_task_handle, tskNO_AFFINITY); + xTaskCreatePinnedToCore(&_zh_b_tachometer_processing_task, "b_tachometer_processing_task", B_TACHOMETER_TASK_STACK_SIZE, NULL, + B_TACHOMETER_TASK_PRIORITY, &_b_tachometer_task_handle, tskNO_AFFINITY); } static void _zh_pcf8574_init(void) @@ -191,34 +408,35 @@ static void _zh_pcf8574_init(void) config.i2c_handle = _i2c_bus_handle; config.i2c_address = LCD_I2C_ADDRESS; zh_pcf8574_init(&config, &_lcd_handle); - zh_160x_init(&_lcd_handle, ZH_LCD_16X2); + zh_160x_init(&_lcd_handle, &_lcd_160x_handle, ZH_LCD_16X2); + _lcd_mutex = xSemaphoreCreateMutex(); } static void _zh_system_load(void) { - zh_160x_set_cursor(&_lcd_handle, 0, 0); - zh_160x_print_char(&_lcd_handle, "LLC AEROTECH"); - zh_160x_set_cursor(&_lcd_handle, 1, 0); + zh_160x_set_cursor(&_lcd_160x_handle, 0, 0); + zh_160x_print_char(&_lcd_160x_handle, "LLC AEROTECH"); + zh_160x_set_cursor(&_lcd_160x_handle, 1, 0); const esp_app_desc_t *esp_app_desc = esp_app_get_description(); - zh_160x_print_char(&_lcd_handle, esp_app_desc->project_name); - zh_160x_set_cursor(&_lcd_handle, 1, 8); - zh_160x_print_char(&_lcd_handle, "V"); - zh_160x_print_char(&_lcd_handle, esp_app_desc->version); + zh_160x_print_char(&_lcd_160x_handle, esp_app_desc->project_name); + zh_160x_set_cursor(&_lcd_160x_handle, 1, 8); + zh_160x_print_char(&_lcd_160x_handle, "V"); + zh_160x_print_char(&_lcd_160x_handle, esp_app_desc->version); vTaskDelay(1000 / portTICK_PERIOD_MS); - zh_160x_lcd_clear(&_lcd_handle); - zh_160x_set_cursor(&_lcd_handle, 0, 0); - zh_160x_print_char(&_lcd_handle, "SYSTEM LOADING:"); + zh_160x_lcd_clear(&_lcd_160x_handle); + zh_160x_set_cursor(&_lcd_160x_handle, 0, 0); + zh_160x_print_char(&_lcd_160x_handle, "SYSTEM LOADING:"); for (uint8_t i = 0; i <= 100; i += 10) { - zh_160x_print_progress_bar(&_lcd_handle, 1, i); + zh_160x_print_progress_bar(&_lcd_160x_handle, 1, i); vTaskDelay(100 / portTICK_PERIOD_MS); } vTaskDelay(500 / portTICK_PERIOD_MS); - zh_160x_lcd_clear(&_lcd_handle); + zh_160x_lcd_clear(&_lcd_160x_handle); vTaskDelay(500 / portTICK_PERIOD_MS); - zh_160x_set_cursor(&_lcd_handle, 0, 0); - zh_160x_print_char(&_lcd_handle, "MODE:"); - zh_160x_print_char(&_lcd_handle, _main_menu[0]); + zh_160x_set_cursor(&_lcd_160x_handle, 0, 0); + zh_160x_print_char(&_lcd_160x_handle, "MODE:"); + zh_160x_print_char(&_lcd_160x_handle, _main_menu[0]); } static void _zh_timer_init(void) @@ -231,11 +449,12 @@ static void _zh_timer_init(void) static void _zh_encoder_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data) { + (void)arg; switch (event_id) { case ZH_BUTTON_EVENT: - zh_encoder_button_event_on_isr_t *button_event = event_data; - if (button_event->button_status == ENCODER_BUTTON_PRESSED) + zh_encoder_button_event_on_isr_t *button_event = (zh_encoder_button_event_on_isr_t *)event_data; + if (button_event->button_status == ZH_ENCODER_BUTTON_STATUS_PRESSED) { esp_timer_start_once(_inclinometer_reset_timer_handle, 2000000); _is_reset_mode = true; @@ -258,36 +477,53 @@ static void _zh_encoder_event_handler(void *arg, esp_event_base_t event_base, in switch (_current_menu_value) { case MODE_ANGLE_ANGLE: - vTaskDelete(_a_inclinometer_task_handle); - vTaskDelete(_b_inclinometer_task_handle); + if (_a_inclinometer_task_handle != NULL) + { + vTaskDelete(_a_inclinometer_task_handle); + } + if (_b_inclinometer_task_handle != NULL) + { + vTaskDelete(_b_inclinometer_task_handle); + } zh_inclinometer_deinit(&_a_inclinometer_handle); zh_inclinometer_deinit(&_b_inclinometer_handle); break; case MODE_TACHO_TACHO: - vTaskDelete(_a_tachometer_task_handle); - vTaskDelete(_b_tachometer_task_handle); + if (_a_tachometer_task_handle != NULL) + { + vTaskDelete(_a_tachometer_task_handle); + } + if (_b_tachometer_task_handle != NULL) + { + vTaskDelete(_b_tachometer_task_handle); + } zh_tachometer_deinit(&_a_tachometer_handle); zh_tachometer_deinit(&_b_tachometer_handle); break; case MODE_ANGLE_TACHO: - vTaskDelete(_a_inclinometer_task_handle); - vTaskDelete(_b_tachometer_task_handle); + if (_a_inclinometer_task_handle != NULL) + { + vTaskDelete(_a_inclinometer_task_handle); + } + if (_b_tachometer_task_handle != NULL) + { + vTaskDelete(_b_tachometer_task_handle); + } zh_inclinometer_deinit(&_a_inclinometer_handle); zh_tachometer_deinit(&_b_tachometer_handle); break; case MODE_REMOTE: - // To Do. break; default: break; } - zh_160x_clear_row(&_lcd_handle, 1); - zh_160x_set_cursor(&_lcd_handle, 0, 0); - zh_160x_on_cursor(&_lcd_handle, true); + zh_160x_clear_row(&_lcd_160x_handle, 1); + zh_160x_set_cursor(&_lcd_160x_handle, 0, 0); + zh_160x_on_cursor(&_lcd_160x_handle, true); } else { - zh_160x_off_cursor(&_lcd_handle); + zh_160x_off_cursor(&_lcd_160x_handle); switch (_current_menu_value) { case MODE_ANGLE_ANGLE: @@ -303,8 +539,8 @@ static void _zh_encoder_event_handler(void *arg, esp_event_base_t event_base, in _zh_b_tachometer_init(); break; case MODE_REMOTE: - zh_160x_set_cursor(&_lcd_handle, 1, 0); - zh_160x_print_char(&_lcd_handle, "NOT CONNECTED"); + zh_160x_set_cursor(&_lcd_160x_handle, 1, 0); + zh_160x_print_char(&_lcd_160x_handle, "NOT CONNECTED"); break; default: break; @@ -313,15 +549,15 @@ static void _zh_encoder_event_handler(void *arg, esp_event_base_t event_base, in } break; case ZH_ENCODER_EVENT: - zh_encoder_event_on_isr_t *encoder_event = event_data; + zh_encoder_event_on_isr_t *encoder_event = (zh_encoder_event_on_isr_t *)event_data; if (_is_menu_mode == true) { - _current_menu_value = (uint8_t)encoder_event->encoder_position; // -V2004 - zh_160x_clear_row(&_lcd_handle, 0); - zh_160x_set_cursor(&_lcd_handle, 0, 0); - zh_160x_print_char(&_lcd_handle, "MODE:"); - zh_160x_print_char(&_lcd_handle, _main_menu[(uint8_t)encoder_event->encoder_position]); // -V2004 - zh_160x_set_cursor(&_lcd_handle, 0, 0); + _current_menu_value = (uint8_t)encoder_event->encoder_position; + zh_160x_clear_row(&_lcd_160x_handle, 0); + zh_160x_set_cursor(&_lcd_160x_handle, 0, 0); + zh_160x_print_char(&_lcd_160x_handle, "MODE:"); + zh_160x_print_char(&_lcd_160x_handle, _main_menu[(uint8_t)encoder_event->encoder_position]); + zh_160x_set_cursor(&_lcd_160x_handle, 0, 0); } else { @@ -335,11 +571,12 @@ static void _zh_encoder_event_handler(void *arg, esp_event_base_t event_base, in static void _zh_espnow_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data) { - // To Do. } static void _zh_wifi_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data) { + (void)arg; + (void)event_data; switch (event_id) { case WIFI_EVENT_STA_START: @@ -348,7 +585,10 @@ static void _zh_wifi_event_handler(void *arg, esp_event_base_t event_base, int32 case WIFI_EVENT_STA_DISCONNECTED: if (_is_wifi_connected == true) { - vTaskDelete(_zh_stats_task_handle); + if (_zh_stats_task_handle != NULL) + { + vTaskDelete(_zh_stats_task_handle); + } zh_syslog_deinit(); _is_wifi_connected = false; } @@ -364,7 +604,11 @@ static void _zh_wifi_event_handler(void *arg, esp_event_base_t event_base, int32 zh_syslog_init_config_t config = ZH_SYSLOG_INIT_CONFIG_DEFAULT(); strcpy(config.syslog_ip, SYSLOG_IP); zh_syslog_init(&config); - xTaskCreatePinnedToCore(&_zh_stats_processing_task, "stats_processing_task", STATS_TASK_STACK_SIZE, NULL, STATS_TASK_PRIORITY, &_zh_stats_task_handle, tskNO_AFFINITY); + if (_zh_stats_task_handle == NULL) + { + xTaskCreatePinnedToCore(&_zh_stats_processing_task, "stats_processing_task", STATS_TASK_STACK_SIZE, NULL, STATS_TASK_PRIORITY, + &_zh_stats_task_handle, tskNO_AFFINITY); + } break; default: break; @@ -373,6 +617,7 @@ static void _zh_wifi_event_handler(void *arg, esp_event_base_t event_base, int32 static void _zh_inclinometer_reset_timer_on_alarm_cb(void *arg) { + (void)arg; _is_reset_mode = false; _is_reset_occurred = true; if (_is_menu_mode == false) @@ -394,11 +639,13 @@ static void _zh_inclinometer_reset_timer_on_alarm_cb(void *arg) static void _zh_a_inclinometer_processing_task(void *pvParameter) { - float prev_angle = 0, current_angle = 0; + (void)pvParameter; + float prev_angle = 0.0; + float current_angle = 0.0; xSemaphoreTake(_lcd_mutex, portMAX_DELAY); - zh_160x_set_cursor(&_lcd_handle, 1, 0); - zh_160x_print_char(&_lcd_handle, "A:"); - zh_160x_print_float(&_lcd_handle, current_angle, 1); + zh_160x_set_cursor(&_lcd_160x_handle, 1, 0); + zh_160x_print_char(&_lcd_160x_handle, "A:"); + zh_160x_print_float(&_lcd_160x_handle, current_angle, 1); xSemaphoreGive(_lcd_mutex); for (;;) { @@ -407,10 +654,10 @@ static void _zh_a_inclinometer_processing_task(void *pvParameter) { prev_angle = current_angle; xSemaphoreTake(_lcd_mutex, portMAX_DELAY); - zh_160x_set_cursor(&_lcd_handle, 1, 5); - zh_160x_print_char(&_lcd_handle, " "); - zh_160x_set_cursor(&_lcd_handle, 1, 2); - zh_160x_print_float(&_lcd_handle, current_angle, 1); + zh_160x_set_cursor(&_lcd_160x_handle, 1, 5); + zh_160x_print_char(&_lcd_160x_handle, " "); + zh_160x_set_cursor(&_lcd_160x_handle, 1, 2); + zh_160x_print_float(&_lcd_160x_handle, current_angle, 1); xSemaphoreGive(_lcd_mutex); } vTaskDelay(100 / portTICK_PERIOD_MS); @@ -421,11 +668,13 @@ static void _zh_a_inclinometer_processing_task(void *pvParameter) static void _zh_b_inclinometer_processing_task(void *pvParameter) { - float prev_angle = 0, current_angle = 0; + (void)pvParameter; + float prev_angle = 0.0; + float current_angle = 0.0; xSemaphoreTake(_lcd_mutex, portMAX_DELAY); - zh_160x_set_cursor(&_lcd_handle, 1, 8); - zh_160x_print_char(&_lcd_handle, "B:"); - zh_160x_print_float(&_lcd_handle, current_angle, 1); + zh_160x_set_cursor(&_lcd_160x_handle, 1, 8); + zh_160x_print_char(&_lcd_160x_handle, "B:"); + zh_160x_print_float(&_lcd_160x_handle, current_angle, 1); xSemaphoreGive(_lcd_mutex); for (;;) { @@ -434,10 +683,10 @@ static void _zh_b_inclinometer_processing_task(void *pvParameter) { prev_angle = current_angle; xSemaphoreTake(_lcd_mutex, portMAX_DELAY); - zh_160x_set_cursor(&_lcd_handle, 1, 13); - zh_160x_print_char(&_lcd_handle, " "); - zh_160x_set_cursor(&_lcd_handle, 1, 10); - zh_160x_print_float(&_lcd_handle, current_angle, 1); + zh_160x_set_cursor(&_lcd_160x_handle, 1, 13); + zh_160x_print_char(&_lcd_160x_handle, " "); + zh_160x_set_cursor(&_lcd_160x_handle, 1, 10); + zh_160x_print_float(&_lcd_160x_handle, current_angle, 1); xSemaphoreGive(_lcd_mutex); } vTaskDelay(100 / portTICK_PERIOD_MS); @@ -448,11 +697,13 @@ static void _zh_b_inclinometer_processing_task(void *pvParameter) static void _zh_a_tachometer_processing_task(void *pvParameter) { - uint16_t prev_rpm = 0, current_rpm = 0; + (void)pvParameter; + uint16_t prev_rpm = 0; + uint16_t current_rpm = 0; xSemaphoreTake(_lcd_mutex, portMAX_DELAY); - zh_160x_set_cursor(&_lcd_handle, 1, 0); - zh_160x_print_char(&_lcd_handle, "A:"); - zh_160x_print_int(&_lcd_handle, current_rpm); + zh_160x_set_cursor(&_lcd_160x_handle, 1, 0); + zh_160x_print_char(&_lcd_160x_handle, "A:"); + zh_160x_print_int(&_lcd_160x_handle, current_rpm); xSemaphoreGive(_lcd_mutex); for (;;) { @@ -461,10 +712,10 @@ static void _zh_a_tachometer_processing_task(void *pvParameter) { prev_rpm = current_rpm; xSemaphoreTake(_lcd_mutex, portMAX_DELAY); - zh_160x_set_cursor(&_lcd_handle, 1, 3); - zh_160x_print_char(&_lcd_handle, " "); - zh_160x_set_cursor(&_lcd_handle, 1, 2); - zh_160x_print_int(&_lcd_handle, current_rpm); + zh_160x_set_cursor(&_lcd_160x_handle, 1, 3); + zh_160x_print_char(&_lcd_160x_handle, " "); + zh_160x_set_cursor(&_lcd_160x_handle, 1, 2); + zh_160x_print_int(&_lcd_160x_handle, current_rpm); xSemaphoreGive(_lcd_mutex); } vTaskDelay(100 / portTICK_PERIOD_MS); @@ -475,11 +726,13 @@ static void _zh_a_tachometer_processing_task(void *pvParameter) static void _zh_b_tachometer_processing_task(void *pvParameter) { - uint16_t prev_rpm = 0, current_rpm = 0; + (void)pvParameter; + uint16_t prev_rpm = 0; + uint16_t current_rpm = 0; xSemaphoreTake(_lcd_mutex, portMAX_DELAY); - zh_160x_set_cursor(&_lcd_handle, 1, 8); - zh_160x_print_char(&_lcd_handle, "B:"); - zh_160x_print_int(&_lcd_handle, current_rpm); + zh_160x_set_cursor(&_lcd_160x_handle, 1, 8); + zh_160x_print_char(&_lcd_160x_handle, "B:"); + zh_160x_print_int(&_lcd_160x_handle, current_rpm); xSemaphoreGive(_lcd_mutex); for (;;) { @@ -488,10 +741,10 @@ static void _zh_b_tachometer_processing_task(void *pvParameter) { prev_rpm = current_rpm; xSemaphoreTake(_lcd_mutex, portMAX_DELAY); - zh_160x_set_cursor(&_lcd_handle, 1, 11); - zh_160x_print_char(&_lcd_handle, " "); - zh_160x_set_cursor(&_lcd_handle, 1, 10); - zh_160x_print_int(&_lcd_handle, current_rpm); + zh_160x_set_cursor(&_lcd_160x_handle, 1, 11); + zh_160x_print_char(&_lcd_160x_handle, " "); + zh_160x_set_cursor(&_lcd_160x_handle, 1, 10); + zh_160x_print_int(&_lcd_160x_handle, current_rpm); xSemaphoreGive(_lcd_mutex); } vTaskDelay(100 / portTICK_PERIOD_MS); @@ -502,51 +755,52 @@ static void _zh_b_tachometer_processing_task(void *pvParameter) static void _zh_stats_processing_task(void *pvParameter) { + (void)pvParameter; const esp_app_desc_t *esp_app_desc = esp_app_get_description(); for (;;) { char buffer[1024] = {0}; const zh_encoder_stats_t *encoder_stats = zh_encoder_get_stats(); const zh_pcf8574_stats_t *pcf8574_stats = zh_pcf8574_get_stats(); - sprintf(buffer, "A inclinometer task minimum free stack size: %ld.\n", _task_stats.a_inclinometer_task_min_stack_size); + snprintf(buffer, sizeof(buffer), "A inclinometer task minimum free stack size: %lu.\n", _task_stats.a_inclinometer_task_min_stack_size); zh_syslog_send(ZH_KERN, _task_stats.a_inclinometer_task_min_stack_size > 100 ? ZH_INFO : ZH_WARNING, esp_app_desc->project_name, "zh_inclinometer", buffer); vTaskDelay(500 / portTICK_PERIOD_MS); - sprintf(buffer, "B inclinometer task minimum free stack size: %ld.\n", _task_stats.b_inclinometer_task_min_stack_size); + snprintf(buffer, sizeof(buffer), "B inclinometer task minimum free stack size: %lu.\n", _task_stats.b_inclinometer_task_min_stack_size); zh_syslog_send(ZH_KERN, _task_stats.b_inclinometer_task_min_stack_size > 100 ? ZH_INFO : ZH_WARNING, esp_app_desc->project_name, "zh_inclinometer", buffer); vTaskDelay(500 / portTICK_PERIOD_MS); - sprintf(buffer, "A tachometer task minimum free stack size: %ld.\n", _task_stats.b_tachometer_task_min_stack_size); + snprintf(buffer, sizeof(buffer), "A tachometer task minimum free stack size: %lu.\n", _task_stats.a_tachometer_task_min_stack_size); + zh_syslog_send(ZH_KERN, _task_stats.a_tachometer_task_min_stack_size > 100 ? ZH_INFO : ZH_WARNING, esp_app_desc->project_name, "zh_tachometer", buffer); + vTaskDelay(500 / portTICK_PERIOD_MS); + snprintf(buffer, sizeof(buffer), "B tachometer task minimum free stack size: %lu.\n", _task_stats.b_tachometer_task_min_stack_size); zh_syslog_send(ZH_KERN, _task_stats.b_tachometer_task_min_stack_size > 100 ? ZH_INFO : ZH_WARNING, esp_app_desc->project_name, "zh_tachometer", buffer); vTaskDelay(500 / portTICK_PERIOD_MS); - sprintf(buffer, "B tachometer task minimum free stack size: %ld.\n", _task_stats.b_tachometer_task_min_stack_size); - zh_syslog_send(ZH_KERN, _task_stats.b_tachometer_task_min_stack_size > 100 ? ZH_INFO : ZH_WARNING, esp_app_desc->project_name, "zh_tachometer", buffer); - vTaskDelay(500 / portTICK_PERIOD_MS); - sprintf(buffer, "Stats task minimum free stack size: %ld.\n", (uint32_t)uxTaskGetStackHighWaterMark(NULL)); + snprintf(buffer, sizeof(buffer), "Stats task minimum free stack size: %lu.\n", (uint32_t)uxTaskGetStackHighWaterMark(NULL)); zh_syslog_send(ZH_KERN, (uint32_t)uxTaskGetStackHighWaterMark(NULL) > 100 ? ZH_INFO : ZH_WARNING, esp_app_desc->project_name, "zh_syslog", buffer); vTaskDelay(500 / portTICK_PERIOD_MS); - sprintf(buffer, "Encoder task minimum free stack size: %ld.\n", encoder_stats->min_stack_size); + snprintf(buffer, sizeof(buffer), "Encoder task minimum free stack size: %lu.\n", encoder_stats->min_stack_size); zh_syslog_send(ZH_KERN, encoder_stats->min_stack_size > 100 ? ZH_INFO : ZH_WARNING, esp_app_desc->project_name, "zh_encoder", buffer); vTaskDelay(500 / portTICK_PERIOD_MS); - sprintf(buffer, "Expander task minimum free stack size: %ld.\n", pcf8574_stats->min_stack_size); + snprintf(buffer, sizeof(buffer), "Expander task minimum free stack size: %lu.\n", pcf8574_stats->min_stack_size); zh_syslog_send(ZH_KERN, pcf8574_stats->min_stack_size > 100 ? ZH_INFO : ZH_WARNING, esp_app_desc->project_name, "zh_pcf8574", buffer); vTaskDelay(500 / portTICK_PERIOD_MS); - sprintf(buffer, "Number of event post error: %ld.\n", encoder_stats->event_post_error); + snprintf(buffer, sizeof(buffer), "Number of event post error: %lu.\n", encoder_stats->event_post_error); zh_syslog_send(ZH_KERN, encoder_stats->event_post_error == 0 ? ZH_INFO : ZH_WARNING, esp_app_desc->project_name, "zh_encoder", buffer); vTaskDelay(500 / portTICK_PERIOD_MS); - sprintf(buffer, "Number of queue overflow error: %ld.\n", encoder_stats->queue_overflow_error); + snprintf(buffer, sizeof(buffer), "Number of queue overflow error: %lu.\n", encoder_stats->queue_overflow_error); zh_syslog_send(ZH_KERN, encoder_stats->queue_overflow_error == 0 ? ZH_INFO : ZH_WARNING, esp_app_desc->project_name, "zh_encoder", buffer); vTaskDelay(500 / portTICK_PERIOD_MS); - sprintf(buffer, "Number of i2c driver error: %ld.\n", pcf8574_stats->i2c_driver_error); + snprintf(buffer, sizeof(buffer), "Number of i2c driver error: %lu.\n", pcf8574_stats->i2c_driver_error); zh_syslog_send(ZH_KERN, pcf8574_stats->i2c_driver_error == 0 ? ZH_INFO : ZH_WARNING, esp_app_desc->project_name, "zh_pcf8574", buffer); vTaskDelay(500 / portTICK_PERIOD_MS); - sprintf(buffer, "Number of event post error: %ld.\n", pcf8574_stats->event_post_error); + snprintf(buffer, sizeof(buffer), "Number of event post error: %lu.\n", pcf8574_stats->event_post_error); zh_syslog_send(ZH_KERN, pcf8574_stats->event_post_error == 0 ? ZH_INFO : ZH_WARNING, esp_app_desc->project_name, "zh_pcf8574", buffer); vTaskDelay(500 / portTICK_PERIOD_MS); - sprintf(buffer, "Number of vector error: %ld.\n", pcf8574_stats->vector_error); + snprintf(buffer, sizeof(buffer), "Number of vector error: %lu.\n", pcf8574_stats->vector_error); zh_syslog_send(ZH_KERN, pcf8574_stats->vector_error == 0 ? ZH_INFO : ZH_WARNING, esp_app_desc->project_name, "zh_pcf8574", buffer); vTaskDelay(500 / portTICK_PERIOD_MS); - sprintf(buffer, "Number of queue overflow error: %ld.\n", pcf8574_stats->queue_overflow_error); + snprintf(buffer, sizeof(buffer), "Number of queue overflow error: %lu.\n", pcf8574_stats->queue_overflow_error); zh_syslog_send(ZH_KERN, pcf8574_stats->queue_overflow_error == 0 ? ZH_INFO : ZH_WARNING, esp_app_desc->project_name, "zh_pcf8574", buffer); vTaskDelay(60000 / portTICK_PERIOD_MS); } vTaskDelete(NULL); -} \ No newline at end of file +} diff --git a/main/ate0005.h b/main/ate0005.h index a0b2d50..966c02f 100644 --- a/main/ate0005.h +++ b/main/ate0005.h @@ -1,5 +1,27 @@ /** * @file ate0005.h + * + * @brief Configuration and interface definitions of the ATE0005 universal + * optical 2-channel angle gauge and tachometer. + * + * This header aggregates the includes required by the application and defines + * the FreeRTOS task priorities and stack sizes, the operating mode constants + * with the main menu labels, the WiFi access point and station parameters and + * the GPIO pin mapping of the peripherals of the ATE0005 device running on the + * ESP32 platform. + * + * Key features: + * - Task priorities and stack sizes for the inclinometer, tachometer, encoder, + * expander and statistics tasks + * - Operating mode constants and the matching main menu labels + * - WiFi access point and station configuration with the remote syslog server + * address + * - I2C bus, LCD expander and optical device GPIO pin definitions + * + * @note The GPIO definitions match the connectors of the ATE0005 board and + * must not be changed without a hardware revision. + * @note The optical device of each channel is shared by the inclinometer and + * the tachometer of the same channel. */ #pragma once @@ -15,82 +37,62 @@ #include "zh_espnow.h" #include "zh_syslog.h" -#define STATS_TASK_PRIORITY tskIDLE_PRIORITY -#define STATS_TASK_STACK_SIZE configMINIMAL_STACK_SIZE + 1280 -#define A_INCLINOMETER_TASK_PRIORITY tskIDLE_PRIORITY + 5 -#define A_INCLINOMETER_TASK_STACK_SIZE configMINIMAL_STACK_SIZE + 512 -#define B_INCLINOMETER_TASK_PRIORITY tskIDLE_PRIORITY + 5 -#define B_INCLINOMETER_TASK_STACK_SIZE configMINIMAL_STACK_SIZE + 512 -#define A_TACHOMETER_TASK_PRIORITY tskIDLE_PRIORITY + 5 -#define A_TACHOMETER_TASK_STACK_SIZE configMINIMAL_STACK_SIZE + 512 -#define B_TACHOMETER_TASK_PRIORITY tskIDLE_PRIORITY + 5 -#define B_TACHOMETER_TASK_STACK_SIZE configMINIMAL_STACK_SIZE + 512 -#define ENCODER_TASK_PRIORITY tskIDLE_PRIORITY + 4 -#define ENCODER_TASK_STACK_SIZE configMINIMAL_STACK_SIZE -#define EXPANDER_TASK_PRIORITY tskIDLE_PRIORITY + 4 -#define EXPANDER_TASK_STACK_SIZE configMINIMAL_STACK_SIZE +#define STATS_TASK_PRIORITY tskIDLE_PRIORITY /*!< Priority of the statistics logging task */ +#define STATS_TASK_STACK_SIZE (configMINIMAL_STACK_SIZE + 1280) /*!< Stack size for the statistics logging task */ +#define A_INCLINOMETER_TASK_PRIORITY tskIDLE_PRIORITY + 5 /*!< Priority of the A inclinometer processing task */ +#define A_INCLINOMETER_TASK_STACK_SIZE (configMINIMAL_STACK_SIZE + 512) /*!< Stack size for the A inclinometer processing task */ +#define B_INCLINOMETER_TASK_PRIORITY tskIDLE_PRIORITY + 5 /*!< Priority of the B inclinometer processing task */ +#define B_INCLINOMETER_TASK_STACK_SIZE (configMINIMAL_STACK_SIZE + 512) /*!< Stack size for the B inclinometer processing task */ +#define A_TACHOMETER_TASK_PRIORITY tskIDLE_PRIORITY + 5 /*!< Priority of the A tachometer processing task */ +#define A_TACHOMETER_TASK_STACK_SIZE (configMINIMAL_STACK_SIZE + 512) /*!< Stack size for the A tachometer processing task */ +#define B_TACHOMETER_TASK_PRIORITY tskIDLE_PRIORITY + 5 /*!< Priority of the B tachometer processing task */ +#define B_TACHOMETER_TASK_STACK_SIZE (configMINIMAL_STACK_SIZE + 512) /*!< Stack size for the B tachometer processing task */ +#define ENCODER_TASK_PRIORITY tskIDLE_PRIORITY + 4 /*!< Priority of the mode encoder task */ +#define ENCODER_TASK_STACK_SIZE configMINIMAL_STACK_SIZE /*!< Stack size for the mode encoder task */ +#define EXPANDER_TASK_PRIORITY tskIDLE_PRIORITY + 4 /*!< Priority of the PCF8574 expander task */ +#define EXPANDER_TASK_STACK_SIZE configMINIMAL_STACK_SIZE /*!< Stack size for the PCF8574 expander task */ -#define TRIAC_ON true -#define TRIAC_OFF false +#define MODE_ANGLE_ANGLE 0 /*!< Operating mode: both inclinometers active */ +#define MAIN_MENU_ANGLE_ANGLE_NAME "ANGLE/ANGLE" /*!< Menu label for angle/angle mode */ +#define MODE_TACHO_TACHO 1 /*!< Operating mode: both tachometers active */ +#define MAIN_MENU_TACHO_TACHO_NAME "TACHO/TACHO" /*!< Menu label for tachometer/tachometer mode */ +#define MODE_ANGLE_TACHO 2 /*!< Operating mode: A inclinometer and B tachometer active */ +#define MAIN_MENU_ANGLE_TACHO_NAME "ANGLE/TACHO" /*!< Menu label for angle/tachometer mode */ +#define MODE_REMOTE 3 /*!< Operating mode: remote control via ESP-NOW */ +#define MAIN_MENU_REMOTE_NAME "REMOTE" /*!< Menu label for remote mode */ -#define ENCODER_BUTTON_RELEASED true -#define ENCODER_BUTTON_PRESSED false +#define WIFI_AP_SSID "ATE0005" /*!< WiFi AP SSID */ +#define WIFI_AP_PASS "repairlab" /*!< WiFi AP password */ +#define WIFI_AP_CHANNEL 1 /*!< WiFi AP channel */ +#define MAX_AP_CONNECTION 4 /*!< Maximum number of stations connected to AP */ -#define LED_OFF ZH_PCF8574_GPIO_LOW -#define LED_ON ZH_PCF8574_GPIO_HIGH +#define WIFI_STA_SSID "rick_professor" /*!< WiFi STA SSID */ +#define WIFI_STA_PASS "repairlab" /*!< WiFi STA password */ +#define WIFI_STA_MAXIMUM_RETRY 10 /*!< Maximum number of reconnection attempts for STA mode */ -#define RELAY_OFF ZH_PCF8574_GPIO_LOW -#define RELAY_ON ZH_PCF8574_GPIO_HIGH +#define SYSLOG_IP "77.51.201.8" /*!< IP address of the remote syslog server */ -#define MODE_ANGLE_ANGLE 0 -#define MAIN_MENU_ANGLE_ANGLE_NAME "ANGLE/ANGLE" -#define MODE_TACHO_TACHO 1 -#define MAIN_MENU_TACHO_TACHO_NAME "TACHO/TACHO" -#define MODE_ANGLE_TACHO 2 -#define MAIN_MENU_ANGLE_TACHO_NAME "ANGLE/TACHO" -#define MODE_REMOTE 3 -#define MAIN_MENU_REMOTE_NAME "REMOTE" +#define I2C_SCL_GPIO GPIO_NUM_22 /*!< I2C clock line GPIO */ +#define I2C_SDA_GPIO GPIO_NUM_21 /*!< I2C data line GPIO */ -#define WIFI_AP_SSID "ATE0005" -#define WIFI_AP_PASS "repairlab" -#define WIFI_AP_CHANNEL 1 -#define MAX_AP_CONNECTION 4 +#define LCD_I2C_ADDRESS 0x20 /*!< I2C address of the LCD PCF8574 expander (U2) */ -#define WIFI_STA_SSID "rick_professor" -#define WIFI_STA_PASS "repairlab" -#define WIFI_STA_MAXIMUM_RETRY 10 +#define MODE_ENCODER_NUMBER 1 /*!< Encoder instance number (Connector EN1) */ +#define MODE_ENCODER_A_GPIO GPIO_NUM_27 /*!< Mode encoder A-phase GPIO */ +#define MODE_ENCODER_B_GPIO GPIO_NUM_26 /*!< Mode encoder B-phase GPIO */ +#define MODE_ENCODER_S_GPIO GPIO_NUM_25 /*!< Mode encoder switch GPIO */ -#define SYSLOG_IP "77.51.201.8" +#define A_OPTICAL_DEVICE_A_GPIO GPIO_NUM_14 /*!< Optical device A-phase GPIO for channel A (Connector EN2) */ +#define A_OPTICAL_DEVICE_B_GPIO GPIO_NUM_13 /*!< Optical device B-phase GPIO for channel A */ -#define I2C_SCL_GPIO GPIO_NUM_22 -#define I2C_SDA_GPIO GPIO_NUM_21 - -#define LCD_I2C_ADDRESS 0x20 /*!< U2. */ - -#define MODE_ENCODER_NUMBER 0x01 /*!< Connector EN1. */ -#define MODE_ENCODER_A_GPIO GPIO_NUM_27 -#define MODE_ENCODER_B_GPIO GPIO_NUM_26 -#define MODE_ENCODER_S_GPIO GPIO_NUM_25 - -#define A_OPTICAL_DEVICE_A_GPIO GPIO_NUM_14 /*!< Connector EN2. */ -#define A_OPTICAL_DEVICE_B_GPIO GPIO_NUM_13 - -#define B_OPTICAL_DEVICE_A_GPIO GPIO_NUM_18 /*!< Connector EN3. */ -#define B_OPTICAL_DEVICE_B_GPIO GPIO_NUM_19 +#define B_OPTICAL_DEVICE_A_GPIO GPIO_NUM_18 /*!< Optical device A-phase GPIO for channel B (Connector EN3) */ +#define B_OPTICAL_DEVICE_B_GPIO GPIO_NUM_19 /*!< Optical device B-phase GPIO for channel B */ #ifdef __cplusplus extern "C" { #endif - typedef struct - { - uint32_t a_inclinometer_task_min_stack_size; - uint32_t b_inclinometer_task_min_stack_size; - uint32_t a_tachometer_task_min_stack_size; - uint32_t b_tachometer_task_min_stack_size; - } task_stats_t; - #ifdef __cplusplus } -#endif \ No newline at end of file +#endif diff --git a/main/idf_component.yml b/main/idf_component.yml new file mode 100644 index 0000000..245a70e --- /dev/null +++ b/main/idf_component.yml @@ -0,0 +1,22 @@ +description: Universal Optical 2-Channel Angle Gauge / Tachometer +url: https://github.com/aZholtikov/ate0005 +version: "1.1.2" +license: "Apache-2.0" +maintainers: + - "Alexey Zholtikov " +dependencies: + idf: ">=5.0" + zh_160x_i2c: + git: "https://github.com/aZholtikov/zh_160x_i2c.git" + zh_syslog: + git: "https://github.com/aZholtikov/zh_syslog.git" + zh_ota_server: + git: "https://github.com/aZholtikov/zh_ota_server.git" + zh_encoder: + git: "https://github.com/aZholtikov/zh_encoder.git" + zh_inclinometer: + git: "https://github.com/aZholtikov/zh_inclinometer" + zh_tachometer: + git: "https://github.com/aZholtikov/zh_tachometer" + zh_espnow: + git: "https://github.com/aZholtikov/zh_espnow" \ No newline at end of file diff --git a/sdkconfig.defaults b/sdkconfig.defaults index 3d044a7..bf457e8 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -1,21 +1,8 @@ CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y -CONFIG_BOOTLOADER_LOG_LEVEL_ERROR=y +CONFIG_BOOTLOADER_LOG_LEVEL_NONE=y CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y CONFIG_PARTITION_TABLE_CUSTOM=y CONFIG_COMPILER_OPTIMIZATION_SIZE=y -CONFIG_GPIO_CTRL_FUNC_IN_IRAM=y -CONFIG_GPTIMER_CTRL_FUNC_IN_IRAM=y -CONFIG_GPTIMER_ISR_CACHE_SAFE=y -CONFIG_I2C_ISR_IRAM_SAFE=y -CONFIG_LEDC_CTRL_FUNC_IN_IRAM=y -CONFIG_PCNT_CTRL_FUNC_IN_IRAM=y -CONFIG_PCNT_ISR_IRAM_SAFE=y -CONFIG_RMT_RECV_FUNC_IN_IRAM=y -CONFIG_RMT_TX_ISR_CACHE_SAFE=y -CONFIG_RMT_RX_ISR_CACHE_SAFE=y -CONFIG_SPI_MASTER_ISR_IN_IRAM=y -CONFIG_UART_ISR_IN_IRAM=y -CONFIG_HTTPD_WS_SUPPORT=y CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y CONFIG_FREERTOS_HZ=1000 -CONFIG_LOG_DEFAULT_LEVEL_ERROR=y \ No newline at end of file +CONFIG_LOG_DEFAULT_LEVEL_NONE=y \ No newline at end of file diff --git a/wiki_ru.md b/wiki_ru.md new file mode 100644 index 0000000..98a5668 --- /dev/null +++ b/wiki_ru.md @@ -0,0 +1,337 @@ +# Описание компонента ate0005 + +## Общие сведения + +**ATE0005** — это прошивка ESP32 для ESP-IDF, реализующая универсальный оптический двухканальный угломер/тахометр. + +Приложение измеряет угол поворота оси и/или скорость её вращения по двум независимым каналам A и B, подключённым к оптическим энкодерам, и выводит результат на LCD-экран 16x2. Управление приборами осуществляется поворотным энкодером MODE с кнопкой. + +### Основные возможности + +1. **Два независимых канала** — каналы A и B работают с собственными оптическими энкодерами. +2. **Четыре режима работы** — `ANGLE/ANGLE`, `TACHO/TACHO`, `ANGLE/TACHO`, `REMOTE`. +3. **Измерение угла** — текущий угол с разрешением 0.1°, сброс длительным нажатием кнопки. +4. **Измерение скорости** — обороты в минуту (RPM) с обновлением каждые 100 мс. +5. **LCD 16x2 через PCF8574** — заставка, анимированный прогресс-бар загрузки, живой вывод значений. +6. **Меню на поворотном энкодере** — выбор режима поворотом, вход/выход коротким нажатием. +7. **WiFi AP+STA** — точка доступа для локального доступа и подключение к внешней сети. +8. **OTA-обновления** — встроенный веб-сервер с самопроверкой образа и откатом (rollback). +9. **Удалённый syslog** — периодическая отправка статистики задач и счётчиков ошибок драйверов. +10. **FreeRTOS-многозадачность** — отдельные задачи на каждый канал, энкодер, расширитель и статистику. + +## Архитектура прошивки + +Приложение состоит из единственного исходного файла `main/ate0005.c` и заголовка конфигурации `main/ate0005.h`. Вся прикладная логика построена вокруг компонентов проекта (`zh_*`), которые выполняют непосредственную работу с железом; `ate0005.c` отвечает за инициализацию, коммутацию режимов и вывод. + +Точка входа `app_main()` выполняет последовательную инициализацию подсистем: + +1. `nvs_flash_init()` с очисткой и повторной инициализацией при пустом разделе или несовпадении версии. +2. `esp_event_loop_create_default()` — общая петля событий. +3. `_zh_pcf8574_init()` — шина I2C, расширитель PCF8574, драйвер LCD, мьютекс экрана. +4. `_zh_system_load()` — заставка и прогресс-бар загрузки. +5. `_zh_encoder_init()` — энкодер выбора режима с обработчиком событий. +6. `_zh_timer_init()` — программный таймер сброса угла. +7. `_zh_a_inclinometer_init()`, `_zh_b_inclinometer_init()` — угломеры каналов A и B. +8. `_zh_wifi_init()` — WiFi в режиме AP+STA. +9. `_zh_webserver_init()` — HTTP-сервер с OTA-страницей. +10. `_zh_espnow_init()` — заготовка под ESP-NOW (пока пуста). + +После инициализации выполняется самопроверка OTA: если запущенный образ находится в состоянии `ESP_OTA_IMG_PENDING_VERIFY`, задача ждёт 60 секунд и помечает образ валидным через `esp_ota_mark_app_valid_cancel_rollback()`. + +### Задачи FreeRTOS + +| Задача | Приоритет | Стек, слов | Назначение | +| -------- | ----------- | ------------ | ------------ | +| `_zh_a_inclinometer_processing_task` | `tskIDLE_PRIORITY + 5` | `configMINIMAL_STACK_SIZE + 512` | Вывод угла канала A | +| `_zh_b_inclinometer_processing_task` | `tskIDLE_PRIORITY + 5` | `configMINIMAL_STACK_SIZE + 512` | Вывод угла канала B | +| `_zh_a_tachometer_processing_task` | `tskIDLE_PRIORITY + 5` | `configMINIMAL_STACK_SIZE + 512` | Вывод RPM канала A | +| `_zh_b_tachometer_processing_task` | `tskIDLE_PRIORITY + 5` | `configMINIMAL_STACK_SIZE + 512` | Вывод RPM канала B | +| `_zh_encoder_event_handler` | обрабатывается в задаче `zh_encoder` | — | События энкодера и кнопки | +| `_zh_pcf8574_*` (внутри `zh_pcf8574`) | `tskIDLE_PRIORITY + 4` | `configMINIMAL_STACK_SIZE` | Обмен с расширителем LCD | +| `_zh_stats_processing_task` | `tskIDLE_PRIORITY` | `configMINIMAL_STACK_SIZE + 1280` | Отправка статистики в syslog | + +Все задачи создаются через `xTaskCreatePinnedToCore()` с `tskNO_AFFINITY`, то есть планировщик сам выбирает ядро. + +## Внутреннее устройство + +### Структура `_task_stats_t` + +Хранит минимальный свободный объём стека (high water mark) каждой измерительной задачи. Статистическая задача читает эти значения для отправки в syslog. + +| Поле | Тип | Описание | +| ------ | ----- | ---------- | +| `a_inclinometer_task_min_stack_size` | `uint32_t` | Минимальный свободный стек задачи угломера A | +| `b_inclinometer_task_min_stack_size` | `uint32_t` | Минимальный свободный стек задачи угломера B | +| `a_tachometer_task_min_stack_size` | `uint32_t` | Минимальный свободный стек задачи тахометра A | +| `b_tachometer_task_min_stack_size` | `uint32_t` | Минимальный свободный стек задачи тахометра B | + +Статический экземпляр `_task_stats` инициализируется полными размерами стеков, чтобы до первого замера статистика не давала ложных предупреждений. + +### Глобальные дескрипторы и флаги + +| Переменная | Тип | Описание | +| ------------ | ----- | ---------- | +| `_http_server_handle` | `httpd_handle_t` | Дескриптор HTTP-сервера | +| `_lcd_handle` | `zh_pcf8574_handle_t *` | Расширитель PCF8574 | +| `_lcd_160x_handle` | `zh_160x_i2c_handle_t *` | Драйвер LCD 16x2 | +| `_mode_encoder_handle` | `zh_encoder_handle_t *` | Энкодер выбора режима | +| `_a_inclinometer_handle`, `_b_inclinometer_handle` | `zh_inclinometer_handle_t *` | Угломеры каналов A/B | +| `_a_tachometer_handle`, `_b_tachometer_handle` | `zh_tachometer_handle_t *` | Тахометры каналов A/B | +| `_inclinometer_reset_timer_handle` | `esp_timer_handle_t` | Таймер сброса угла | +| `_lcd_mutex` | `SemaphoreHandle_t` | Мьютекс доступа к LCD | +| `_is_menu_mode` | `volatile bool` | Флаг нахождения в меню выбора режима | +| `_is_reset_mode` | `volatile bool` | Флаг нажатия кнопки (идёт отсчёт долгого нажатия) | +| `_is_reset_occurred` | `volatile bool` | Признак того, что сброс уже выполнен, чтобы отпустить кнопку «вхолостую» | +| `_current_menu_value` | `volatile uint8_t` | Индекс выбранного режима | +| `_wifi_reconnect_retry_num` | `volatile uint8_t` | Счётчик попыток переподключения STA | +| `_is_wifi_connected` | `volatile bool` | Флаг подключения STA | + +Все разделяемые между задачами и обработчиками прерываний флаги объявлены `volatile`, поскольку изменяются из разных контекстов без блокировок. + +### Константы конфигурации + +Режимы работы заданы константами и подписями меню: + +| Константа | Значение | Подпись меню | +| ----------- | ---------- | -------------- | +| `MODE_ANGLE_ANGLE` | `0` | `"ANGLE/ANGLE"` | +| `MODE_TACHO_TACHO` | `1` | `"TACHO/TACHO"` | +| `MODE_ANGLE_TACHO` | `2` | `"ANGLE/TACHO"` | +| `MODE_REMOTE` | `3` | `"REMOTE"` | + +Массив `_main_menu[]` хранит подписи в порядке их индексов; энкодер режима ограничен диапазоном `0 .. sizeof(_main_menu)/sizeof(_main_menu[0]) - 1`. + +### Распиновка GPIO + +| Периферия | Вывод A | Вывод B | Вывод S | Примечание | +| ----------- | --------- | --------- | --------- | ------------ | +| I2C-шина | `SDA = GPIO_NUM_21` | `SCL = GPIO_NUM_22` | — | Внутренние подтяжки включены | +| Расширитель LCD | адрес `0x20` | — | — | PCF8574 (U2) | +| Энкодер MODE | `GPIO_NUM_27` | `GPIO_NUM_26` | `GPIO_NUM_25` | Разъём EN1, экземпляр `1` | +| Оптический энкодер A | `GPIO_NUM_14` | `GPIO_NUM_13` | — | Разъём EN2 | +| Оптический энкодер B | `GPIO_NUM_18` | `GPIO_NUM_19` | — | Разъём EN3 | + +Оптическое устройство каждого канала общее для угломера и тахометра этого канала: они не могут работать одновременно на одном канале. + +## API (интерфейс) + +Прикладной код `ate0005.c` не экспортирует публичных функций — это законченное приложение, а не библиотека. Все функции модуля объявлены `static` и вызываются только из `app_main()` и обработчиков событий. Ниже приведена их роль как внутренняя справка. + +### Инициализация подсистем + +| Функция | Описание | +| --------- | ---------- | +| `app_main()` | Точка входа: NVS, событийная петля, LCD, энкодер, таймер, угломеры, WiFi, веб-сервер, ESP-NOW, самопроверка OTA. | +| `_zh_pcf8574_init()` | Создаёт шину I2C, инициализирует PCF8574 и LCD 16x2, создаёт мьютекс экрана. | +| `_zh_encoder_init()` | Регистрирует обработчик энкодера, задаёт GPIO, диапазон и шаг, сбрасывает позицию в 0. | +| `_zh_timer_init()` | Создаёт одноразовый программный таймер сброса угла. | +| `_zh_a_inclinometer_init()` | Инициализирует угломер канала A и создаёт его задачу. | +| `_zh_b_inclinometer_init()` | Инициализирует угломер канала B и создаёт его задачу. | +| `_zh_a_tachometer_init()` | Инициализирует тахометр канала A и создаёт его задачу. | +| `_zh_b_tachometer_init()` | Инициализирует тахометр канала B и создаёт его задачу. | +| `_zh_wifi_init()` | Настраивает AP и STA, регистрирует обработчики событий WiFi/IP, запускает WiFi. | +| `_zh_webserver_init()` | Запускает HTTP-сервер и регистрирует OTA-страницу на `"/"`. | +| `_zh_espnow_init()` | Заготовка под ESP-NOW, тело пусто. | + +### Обработчики событий + +| Функция | Описание | +| --------- | ---------- | +| `_zh_encoder_event_handler()` | Обрабатывает нажатие/отпускание кнопки и поворот энкодера: переключение меню, сброс угла. | +| `_zh_wifi_event_handler()` | Подключение STA, переподключение с ограничением попыток, инициализация syslog и создание задачи статистики при получении IP. | +| `_zh_espnow_event_handler()` | Заготовка под ESP-NOW, тело пусто. | +| `_zh_inclinometer_reset_timer_on_alarm_cb()` | Колбэк долгого нажатия: сбрасывает угол активных угломеров текущего режима. | + +### Задачи обработки и вывода + +| Функция | Описание | +| --------- | ---------- | +| `_zh_a_inclinometer_processing_task()` | Выводит угол канала A на 2-ю строку LCD с 1 знаком после запятой. | +| `_zh_b_inclinometer_processing_task()` | Выводит угол канала B на 2-ю строку LCD с 1 знаком после запятой. | +| `_zh_a_tachometer_processing_task()` | Выводит RPM канала A на 2-ю строку LCD. | +| `_zh_b_tachometer_processing_task()` | Выводит RPM канала B на 2-ю строку LCD. | +| `_zh_stats_processing_task()` | Периодически отправляет свободный стек задач и счётчики ошибок в syslog. | + +### Служебные функции вывода + +| Функция | Описание | +|---------|----------| +| `_zh_system_load()` | Показывает заставку с названием и версией, затем прогресс-бар загрузки. | + +## Потокобезопасность + +1. **Мьютекс `_lcd_mutex`** — защищает последовательности обращений к LCD (`set_cursor` → `print_*`) от одновременного доступа нескольких задач. Захватывается на время вывода значения и освобождается сразу после. +2. **`volatile` флаги состояния** — `_is_menu_mode`, `_is_reset_mode`, `_is_reset_occurred`, `_current_menu_value` изменяются из обработчика событий энкодера и читаются из задач и колбэка таймера; прямой доступ без блокировки допустим для однобайтовых флагов. +3. **`_task_stats`** — каждая измерительная задача пишет только своё поле, читатель (статистическая задача) только читает; гонка исключена за счёт разделения полей. +4. **Событийная модель `zh_encoder`** — прерывания энкодера постят события в очередь, обработка идёт в задаче компонента, а не в ISR. + +## Алгоритмы + +### Переключение режима работы + +1. Короткое нажатие кнопки энкодера инвертирует `_is_menu_mode`. +2. При входе в меню текущие активные устройства режима останавливаются: удаляются соответствующие задачи через `vTaskDelete()`, затем вызываются `zh_inclinometer_deinit()` / `zh_tachometer_deinit()`. +3. Включается курсор LCD, очищается 2-я строка, на 1-й отображается текущая позиция меню. +4. При выходе из меню курсор выключается, инициализируются устройства выбранного режима и запускаются их задачи. +5. Поворот энкодера в меню обновляет `_current_menu_value` и подпись режима; вне меню позиция энкодера принудительно возвращается к `_current_menu_value` через `zh_encoder_set()`. + +### Сброс угла долгим нажатием + +1. При нажатии кнопки запускается одноразовый таймер на 2 секунды и выставляется `_is_reset_mode`. +2. Если кнопка отпущена раньше 2 секунд, таймер останавливается, событие считается коротким нажатием (переключение меню). +3. Если таймер сработал, колбэк выставляет `_is_reset_occurred` и вызывает `zh_inclinometer_reset()` для активных угломеров режима (в `ANGLE/ANGLE` — оба, в `ANGLE/TACHO` — только A). +4. Последующее отпускание кнопки игнорируется, если сброс уже произошёл (`_is_reset_occurred == true`). + +### Обновление значения на LCD + +1. Задача кэширует предыдущее показание (`prev_angle` / `prev_rpm`). +2. Каждые 100 мс читается новое значение через `zh_inclinometer_get()` / `zh_tachometer_get()`. +3. Значение перерисовывается только при изменении: сначала область значения затишается пробелами, затем печатается новое число. Это снижает нагрузку на I2C и мигание экрана. + +### Статистика и syslog + +1. Задача статистики создаётся только после получения STA-адреса (`IP_EVENT_STA_GOT_IP`). +2. Каждые ~60 секунд (с промежуточными задержками по 500 мс) отправляются сообщения о свободном стеке каждой задачи и о счётчиках ошибок `zh_encoder` и `zh_pcf8574`. +3. Уровень `ZH_WARNING` ставится, если свободный стек задачи меньше 100 слов или счётчик ошибок ненулевой; иначе `ZH_INFO`. +4. При разрыве WiFi задача статистики удаляется, syslog деинициализируется. + +## Примеры использования + +### Инициализация угломера канала + +```c +#include "zh_inclinometer.h" + +static zh_inclinometer_handle_t *_a_inclinometer_handle = NULL; + +void app_main(void) +{ + zh_inclinometer_init_config_t config = ZH_INCLINOMETER_INIT_CONFIG_DEFAULT(); + config.a_gpio_number = A_OPTICAL_DEVICE_A_GPIO; // GPIO_NUM_14 + config.b_gpio_number = A_OPTICAL_DEVICE_B_GPIO; // GPIO_NUM_13 + config.encoder_pulses = 3600; // импульсов на оборот + zh_inclinometer_init(&config, &_a_inclinometer_handle); + // ... использование ... + zh_inclinometer_deinit(&_a_inclinometer_handle); // освобождение ресурсов +} +``` + +### Вывод угла на LCD под мьютексом + +```c +#include "zh_160x_i2c.h" + +static zh_160x_i2c_handle_t *_lcd_160x_handle = NULL; +static SemaphoreHandle_t _lcd_mutex = NULL; + +void app_main(void) +{ + _lcd_mutex = xSemaphoreCreateMutex(); + float current_angle = 0.0; + for (;;) + { + // читаем угол и выводим только при изменении + zh_inclinometer_get(&_a_inclinometer_handle, ¤t_angle); + xSemaphoreTake(_lcd_mutex, portMAX_DELAY); + zh_160x_set_cursor(&_lcd_160x_handle, 1, 2); + zh_160x_print_float(&_lcd_160x_handle, current_angle, 1); + xSemaphoreGive(_lcd_mutex); + vTaskDelay(100 / portTICK_PERIOD_MS); + } + vSemaphoreDelete(_lcd_mutex); +} +``` + +### Инициализация удалённого syslog + +```c +#include "zh_syslog.h" + +void app_main(void) +{ + zh_syslog_init_config_t config = ZH_SYSLOG_INIT_CONFIG_DEFAULT(); + strcpy(config.syslog_ip, SYSLOG_IP); // адрес сервера из ate0005.h + esp_err_t err = zh_syslog_init(&config); + if (err != ESP_OK) + { + printf("Syslog initialization error %s\n", esp_err_to_name(err)); + return; + } + zh_syslog_send(ZH_KERN, ZH_INFO, "ATE0005", "zh_syslog", "Stats task started.\n"); + zh_syslog_deinit(); // освобождение ресурсов +} +``` + +## Конфигурация сборки + +Прошивка опирается на следующие параметры `sdkconfig.defaults`: + +| Параметр | Значение | Назначение | +| ---------- | ---------- | ------------ | +| `CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE` | `y` | Разрешает OTA-откат при непройденной самопроверке | +| `CONFIG_BOOTLOADER_LOG_LEVEL_NONE` | `y` | Отключает лог загрузчика | +| `CONFIG_ESPTOOLPY_FLASHSIZE_8MB` | `8MB` | Размер флеш под две OTA-партиции | +| `CONFIG_PARTITION_TABLE_CUSTOM` | `y` | Используется `partitions.csv` | +| `CONFIG_COMPILER_OPTIMIZATION_SIZE` | `y` | Оптимизация по размеру | +| `CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240` | `240` | Максимальная частота CPU | +| `CONFIG_FREERTOS_HZ` | `1000` | Тик 1 мс для точных задержек | +| `CONFIG_LOG_DEFAULT_LEVEL_NONE` | `y` | Отключает ESP-IDF лог по умолчанию | + +### Партиции (`partitions.csv`) + +| Имя | Тип | Подтип | Размер | Назначение | +| ----- | ----- | -------- | -------- | ------------ | +| `nvs` | data | nvs | `0x6000` | Хранилище настроек | +| `otadata` | data | ota | `0x2000` | Состояние OTA | +| `phy_init` | data | phy | `0x1000` | Калибровка PHY | +| `ota_0` | app | ota_0 | 3500K | Образ прошивки A | +| `ota_1` | app | ota_1 | 3500K | Образ прошивки B | +| `storage` | data | fat | 1M | Файловое хранилище | + +## Ошибки + +Прикладной код не возвращает пользовательских кодов ошибок и полагается на коды ESP-IDF, транслируемые компонентами `zh_*`. Ниже типовые коды, встречающиеся в путях инициализации. + +| Код | Описание | +| ----- | ---------- | +| `ESP_OK` | Операция выполнена успешно | +| `ESP_ERR_INVALID_ARG` | Неверный аргумент функции (например, нулевой дескриптор) | +| `ESP_ERR_NO_MEM` | Недостаточно памяти | +| `ESP_ERR_NOT_FOUND` | Устройство или партиция не найдены | +| `ESP_ERR_TIMEOUT` | Таймаут операции (I2C, WiFi) | +| `ESP_ERR_NVS_NO_FREE_PAGES` | Раздел NVS переполнен (требует очистки) | +| `ESP_ERR_NVS_NEW_VERSION_FOUND` | Версия формата NVS не совпадает (требует очистки) | + +## Ограничения + +1. **Режим `REMOTE` не реализован** — функции `_zh_espnow_init()` и `_zh_espnow_event_handler()` имеют пустое тело, поэтому в этом режиме на экране выводится `NOT CONNECTED`. +2. **Общее оптическое устройство на канал** — угломер и тахометр одного канала используют одни GPIO, поэтому не могут работать одновременно; при переключении режима предыдущие устройства деинициализируются. +3. **WiFi-кредиты зашиты в прошивку** — `WIFI_STA_SSID` и `WIFI_STA_PASS` заданы константами в `ate0005.h` и требуют пересборки для смены сети. +4. **Жёсткая распиновка GPIO** — номера выводов соответствуют разъёмам платы ATE0005 и не должны меняться без аппаратной ревизии. +5. **Самопроверка OTA блокирует на 60 секунд** — при первом запуске после OTA-обновления `app_main()` выдерживает задержку перед подтверждением образа. +6. **Порог предупреждения стека — 100 слов** — статистика выдаёт `ZH_WARNING` при свободном стеке ниже 100 слов; значение подобрано под конкретные размеры стеков задач. + +## Требования для сборки + +- ESP-IDF версии 5.0 или выше. +- FreeRTOS (входит в ESP-IDF). +- Компоненты проекта: `zh_160x_i2c`, `zh_encoder`, `zh_inclinometer`, `zh_tachometer`, `zh_espnow`, `zh_ota_server`, `zh_syslog`. +- Встроенные компоненты ESP-IDF: `esp_wifi`, `nvs_flash`, `esp_timer`, `esp_app_format`. +- Флеш-память 8 МБ под две OTA-партиции по 3500K. + +## Установка + +```bash +cd your_projects_folder +git clone https://github.com/aZholtikov/ate0005 +cd ate0005 +idf.py set-target esp32 +idf.py build +idf.py -p /dev/ttyUSB0 flash monitor +``` + +Зависимости-компоненты подтягиваются автоматически из `main/idf_component.yml` при первой сборке. Для подключения в собственном коде используется единый заголовок конфигурации: + +```c +#include "ate0005.h" +```