├── .gitignore ├── main ├── telnet.h ├── cmd.h ├── gpio_config.h ├── wifi.h ├── CMakeLists.txt ├── idf_component.yml ├── i2c.h ├── main.c ├── cmd.c ├── bq.h ├── wifi.c ├── i2c.c ├── telnet.c └── bq.c ├── doc ├── console_1.png ├── console_2.png ├── console_3.png ├── console_4.png ├── console_5.png ├── console_6.png ├── console_7.png ├── contact_1.jpg └── contact_2.jpg ├── CMakeLists.txt ├── sdkconfig.defaults └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | sdkconfig 2 | dependencies.lock 3 | build 4 | .vscode 5 | 6 | -------------------------------------------------------------------------------- /main/telnet.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | 4 | void telnet_start(void); 5 | -------------------------------------------------------------------------------- /doc/console_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/g3gg0/esp32_battgauge_tool/master/doc/console_1.png -------------------------------------------------------------------------------- /doc/console_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/g3gg0/esp32_battgauge_tool/master/doc/console_2.png -------------------------------------------------------------------------------- /doc/console_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/g3gg0/esp32_battgauge_tool/master/doc/console_3.png -------------------------------------------------------------------------------- /doc/console_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/g3gg0/esp32_battgauge_tool/master/doc/console_4.png -------------------------------------------------------------------------------- /doc/console_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/g3gg0/esp32_battgauge_tool/master/doc/console_5.png -------------------------------------------------------------------------------- /doc/console_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/g3gg0/esp32_battgauge_tool/master/doc/console_6.png -------------------------------------------------------------------------------- /doc/console_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/g3gg0/esp32_battgauge_tool/master/doc/console_7.png -------------------------------------------------------------------------------- /doc/contact_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/g3gg0/esp32_battgauge_tool/master/doc/contact_1.jpg -------------------------------------------------------------------------------- /doc/contact_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/g3gg0/esp32_battgauge_tool/master/doc/contact_2.jpg -------------------------------------------------------------------------------- /main/cmd.h: -------------------------------------------------------------------------------- 1 | 2 | #pragma once 3 | 4 | 5 | /* Function declarations */ 6 | void cmd_start(void); 7 | 8 | -------------------------------------------------------------------------------- /main/gpio_config.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | 4 | #define GPIO_I2C_SDA GPIO_NUM_4 5 | #define GPIO_I2C_SCL GPIO_NUM_3 6 | 7 | -------------------------------------------------------------------------------- /main/wifi.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Wi-Fi WPS Connection Helper 3 | */ 4 | #ifndef WIFI_CONNECT_H 5 | #define WIFI_CONNECT_H 6 | 7 | #include "esp_err.h" 8 | 9 | void wifi_start(void); 10 | 11 | #endif /* WIFI_CONNECT_H */ 12 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # The following lines of boilerplate have to be in your project's CMakeLists 2 | # in this exact order for cmake to work correctly 3 | cmake_minimum_required(VERSION 3.16) 4 | 5 | include($ENV{IDF_PATH}/tools/cmake/project.cmake) 6 | project(i2c_shell) 7 | 8 | 9 | -------------------------------------------------------------------------------- /main/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | idf_component_register( 2 | SRCS 3 | "main.c" 4 | "cmd.c" 5 | "i2c.c" 6 | "bq.c" 7 | "wifi.c" 8 | "telnet.c" 9 | 10 | INCLUDE_DIRS 11 | "." 12 | 13 | PRIV_REQUIRES driver esp_driver_gpio esp_hw_support esp_psram esp_wifi wpa_supplicant esp_event) 14 | -------------------------------------------------------------------------------- /main/idf_component.yml: -------------------------------------------------------------------------------- 1 | dependencies: 2 | cmd_system: 3 | path: ${IDF_PATH}/examples/system/console/advanced/components/cmd_system 4 | cmd_nvs: 5 | path: ${IDF_PATH}/examples/system/console/advanced/components/cmd_nvs 6 | cmd_wifi: 7 | path: ${IDF_PATH}/examples/system/console/advanced/components/cmd_wifi 8 | -------------------------------------------------------------------------------- /main/i2c.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | void i2c_init(); 4 | int i2c_write(uint8_t addr, const uint8_t *data, size_t len); 5 | int i2c_write_partial(uint8_t addr, const uint8_t *data, size_t len, bool stop); 6 | int i2c_read(uint8_t addr, uint8_t *data, size_t len); 7 | int i2c_write_read(uint8_t addr, const uint8_t *wdata, size_t wlen, uint8_t *rdata, size_t rlen); 8 | -------------------------------------------------------------------------------- /sdkconfig.defaults: -------------------------------------------------------------------------------- 1 | # This file was generated using idf.py save-defconfig. It can be edited manually. 2 | # Espressif IoT Development Framework (ESP-IDF) 5.4.1 Project Minimal Configuration 3 | # 4 | CONFIG_IDF_TARGET="esp32c3" 5 | CONFIG_BOOTLOADER_LOG_LEVEL_WARN=y 6 | CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y 7 | CONFIG_UART_ISR_IN_IRAM=y 8 | CONFIG_ESP_SLEEP_MSPI_NEED_ALL_IO_PU=y 9 | CONFIG_ESP_SLEEP_WAIT_FLASH_READY_EXTRA_DELAY=2000 10 | CONFIG_ESP_SYSTEM_PANIC_PRINT_HALT=y 11 | CONFIG_ESP_MAIN_TASK_STACK_SIZE=7168 12 | CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y 13 | CONFIG_ESP_PANIC_HANDLER_IRAM=y 14 | CONFIG_ESP_IPC_TASK_STACK_SIZE=1280 15 | CONFIG_ESP_COREDUMP_ENABLE_TO_UART=y 16 | CONFIG_ESP_COREDUMP_DATA_FORMAT_BIN=y 17 | CONFIG_ESP_COREDUMP_UART_DELAY=500 18 | CONFIG_FREERTOS_USE_TRACE_FACILITY=y 19 | CONFIG_FREERTOS_USE_STATS_FORMATTING_FUNCTIONS=y 20 | CONFIG_FREERTOS_WATCHPOINT_END_OF_STACK=y 21 | CONFIG_FREERTOS_ISR_STACKSIZE=2096 22 | -------------------------------------------------------------------------------- /main/main.c: -------------------------------------------------------------------------------- 1 | /* Basic console example (esp_console_repl API) 2 | 3 | This example code is in the Public Domain (or CC0 licensed, at your option.) 4 | 5 | Unless required by applicable law or agreed to in writing, this 6 | software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 7 | CONDITIONS OF ANY KIND, either express or implied. 8 | */ 9 | 10 | #include 11 | #include 12 | #include "esp_system.h" 13 | #include "esp_log.h" 14 | #include "esp_wifi.h" 15 | #include "nvs.h" 16 | #include "nvs_flash.h" 17 | #include "i2c.h" 18 | #include "wifi.h" 19 | #include "telnet.h" 20 | #include "cmd.h" 21 | #include "bq.h" 22 | 23 | void app_main(void) 24 | { 25 | ESP_ERROR_CHECK_WITHOUT_ABORT(esp_netif_init()); 26 | ESP_ERROR_CHECK_WITHOUT_ABORT(esp_event_loop_create_default()); 27 | 28 | /* init NVS */ 29 | esp_err_t err = nvs_flash_init(); 30 | if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) 31 | { 32 | ESP_ERROR_CHECK(nvs_flash_erase()); 33 | err = nvs_flash_init(); 34 | } 35 | 36 | i2c_init(); 37 | wifi_start(); 38 | cmd_start(); 39 | bq_start(); 40 | telnet_start(); 41 | } 42 | -------------------------------------------------------------------------------- /main/cmd.c: -------------------------------------------------------------------------------- 1 | 2 | 3 | #include 4 | #include 5 | #include "esp_system.h" 6 | #include "esp_log.h" 7 | #include "esp_console.h" 8 | 9 | #include "cmd_system.h" 10 | #include "cmd_wifi.h" 11 | #include "cmd_nvs.h" 12 | 13 | 14 | #define PROMPT_STR CONFIG_IDF_TARGET 15 | #define CONSOLE_MAX_COMMAND_LINE_LENGTH 1024 16 | 17 | 18 | /* 19 | * We warn if a secondary serial console is enabled. A secondary serial console is always output-only and 20 | * hence not very useful for interactive console applications. If you encounter this warning, consider disabling 21 | * the secondary serial console in menuconfig unless you know what you are doing. 22 | */ 23 | #if SOC_USB_SERIAL_JTAG_SUPPORTED 24 | #if !CONFIG_ESP_CONSOLE_SECONDARY_NONE 25 | #warning "A secondary serial console is not useful when using the console component. Please disable it in menuconfig." 26 | #endif 27 | #endif 28 | 29 | void cmd_start() 30 | { 31 | esp_console_repl_t *repl = NULL; 32 | esp_console_repl_config_t repl_config = ESP_CONSOLE_REPL_CONFIG_DEFAULT(); 33 | 34 | repl_config.prompt = PROMPT_STR ">"; 35 | repl_config.max_cmdline_length = CONSOLE_MAX_COMMAND_LINE_LENGTH; 36 | 37 | #if defined(CONFIG_ESP_CONSOLE_UART_DEFAULT) || defined(CONFIG_ESP_CONSOLE_UART_CUSTOM) 38 | esp_console_dev_uart_config_t hw_config = ESP_CONSOLE_DEV_UART_CONFIG_DEFAULT(); 39 | ESP_ERROR_CHECK(esp_console_new_repl_uart(&hw_config, &repl_config, &repl)); 40 | 41 | #elif defined(CONFIG_ESP_CONSOLE_USB_CDC) 42 | esp_console_dev_usb_cdc_config_t hw_config = ESP_CONSOLE_DEV_CDC_CONFIG_DEFAULT(); 43 | ESP_ERROR_CHECK(esp_console_new_repl_usb_cdc(&hw_config, &repl_config, &repl)); 44 | 45 | #elif defined(CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG) 46 | esp_console_dev_usb_serial_jtag_config_t hw_config = ESP_CONSOLE_DEV_USB_SERIAL_JTAG_CONFIG_DEFAULT(); 47 | ESP_ERROR_CHECK(esp_console_new_repl_usb_serial_jtag(&hw_config, &repl_config, &repl)); 48 | 49 | #else 50 | #error Unsupported console type 51 | #endif 52 | 53 | /* Register commands */ 54 | esp_console_register_help_command(); 55 | register_system_common(); 56 | #if (CONFIG_ESP_WIFI_ENABLED || CONFIG_ESP_HOST_WIFI_ENABLED) 57 | register_wifi(); 58 | #endif 59 | register_nvs(); 60 | 61 | ESP_ERROR_CHECK(esp_console_start_repl(repl)); 62 | } 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # esp32_battgauge_tool: ESP32 I2C Command-Line Tool for battery gauge chips 2 | 3 | This project, `esp32_battgauge_tool`, is an ESP-IDF application that provides a versatile command-line interface over Telnet for interacting with I2C devices using an ESP32. 4 | It was initially created as a helper to read and write data from the TI BQ40Z555 battery gauge, which is commonly used in certain Acer battery packs. 5 | 6 | I made heavy use of LLMs like ChatGPT-o3 and Gemini 2.5 Pro to speed up my programming here. It's a huge time saver to just not implement a commandline arg parser the 100th time by hand :) 7 | 8 | ## Story 9 | I received a notebook which was said to have trouble charging the battery. Power supply and battery were swapped and still no charging. 10 | When checking, the battery didn't supply any voltage when plugged into the laptop. 11 | I tore the battery down and all cells were quite low, that's why the battery controller denied voltage supply. 12 | 13 | Tried to charge the cells and still the controller didn't give me any voltage. 14 | 15 | Checking the interwebs, I only could find the NLBA1 or other commercial tools to get deeper into the root of my problems here. 16 | So the rabbit hole began... 17 | 18 | ## What can it do? 19 | You can only read the current status bits, voltages, history data and such. 20 | The chip might be locked into a secure mode, so you can not write data to it, but thats not the use case here. 21 | So no actions were taken to crack the key required for unsealing and such. 22 | 23 | ## Features 24 | 25 | * **Serial Terminal:** A command-line shell with a few commands for I2C and BQ commands 26 | * **Wi-Fi Telnet Access:** A command-line shell accessible over Wi-Fi via any standard Telnet client. 27 | * **Generic I2C Commands:** 28 | * `i2cscan`: Scans the I2C bus to discover connected devices. 29 | * `i2c_r`: Reads a specified number of bytes from any I2C device. 30 | * `i2c_w`: Writes one or more bytes to any I2C device. 31 | * `i2c_rw`: Performs a combined write-then-read operation, ideal for accessing device registers. This command also supports cyclic execution for repeated polling. 32 | * **TI BQ40Z555 Gas Gauge Support:** 33 | * `bq_show`: Dumps all known registers and status fields from the BQ40Z555, providing a comprehensive overview of the battery's state. 34 | * `bq_lifetime`: Decodes and displays lifetime data blocks from the device, offering insights into its long-term usage and health. 35 | 36 | ## Getting Started 37 | 38 | 1. **Hardware:** An ESP32 development board. 39 | 2. **Software:** Requires the ESP-IDF (Espressif IoT Development Framework). 40 | 3. **Configuration:** 41 | * Enable WPS 42 | * Define the I2C pins (SDA and SCL) in `main/gpio_config.h` to match your hardware setup. 43 | 4. **Build and Flash:** 44 | ```bash 45 | idf.py build 46 | idf.py -p (YOUR_SERIAL_PORT) flash 47 | ``` 48 | 5. **Connect:** 49 | * After flashing, the ESP32 will connect to your Wi-Fi network and print its IP address to the serial monitor. 50 | * Use a Telnet client to connect to the device: 51 | ```bash 52 | telnet 53 | ``` 54 | 55 | ## Example Usage 56 | 57 | Once connected via Telnet, you can issue commands directly to your I2C devices: 58 | 59 | ``` 60 | # Scan for all I2C devices on the bus 61 | i2cscan 62 | 63 | # Read 2 bytes from a device at address 0x0B 64 | i2c_r 0x0b -n 2 65 | 66 | # Write the byte 0x01 to a device at address 0x0B 67 | i2c_w 0x0b 0x01 68 | 69 | # Read all available data from a BQ40Z555 gas gauge 70 | bq_show 71 | ``` 72 | 73 | 74 | ## Images 75 | 76 | ![Console Screenshot 1](doc/console_1.png) 77 | ![Console Screenshot 2](doc/console_2.png) 78 | ![Console Screenshot 3](doc/console_3.png) 79 | ![Console Screenshot 4](doc/console_4.png) 80 | ![Console Screenshot 5](doc/console_5.png) 81 | ![Console Screenshot 6](doc/console_6.png) 82 | ![Console Screenshot 7](doc/console_7.png) 83 | ![Contacting 1](doc/contact_1.jpg) 84 | ![Contacting 2](doc/contact_2.jpg) 85 | 86 | -------------------------------------------------------------------------------- /main/bq.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // ────────────────────────────────────────────────────────────────────────────── 4 | // Generic WORD helper 5 | // ────────────────────────────────────────────────────────────────────────────── 6 | /** 7 | * A *word* in the SBS spec is a 16‑bit little‑endian value returned by the 8 | * gauge. Many commands (Voltage, Temperature, Current…) simply map that WORD to 9 | * engineering units through a linear transform: result = raw * scaling + offset. 10 | * 11 | * Define a `bq_entry` describing each command you care about and call 12 | * `bq_generic_word()` to fetch/print it – or use it inside console commands. 13 | */ 14 | 15 | typedef enum 16 | { 17 | BQ40Z555_TYPE_BYTE = 0, 18 | BQ40Z555_TYPE_WORD_FLOAT, 19 | BQ40Z555_TYPE_WORD_INTEGER, 20 | BQ40Z555_TYPE_WORD_HEX, 21 | BQ40Z555_TYPE_BLOCK_ASCII, 22 | BQ40Z555_TYPE_BLOCK_HEX, 23 | BQ40Z555_TYPE_BLOCK_BITS 24 | } bq_data_type; 25 | 26 | 27 | // ────────────────────────────────────────────────────────────────────────────── 28 | // Self‑describing **bit‑field** support 29 | // ────────────────────────────────────────────────────────────────────────────── 30 | /** 31 | * Describe a sub‑field within a WORD (bit‑mapped flags, enums…). 32 | * 33 | * `bit` – least‑significant bit position (0 = LSB) 34 | * `width` – number of bits (1 for a single flag) 35 | * `desc` – short human‑readable description 36 | */ 37 | typedef struct { 38 | uint8_t bit; 39 | uint8_t width; 40 | const char *desc; 41 | const char *long_desc; 42 | } bq_bit_desc_t; 43 | 44 | 45 | typedef struct bq_entry 46 | { 47 | uint8_t reg; ///< SBS command code (0x00‑0xFF) 48 | const char *name; ///< Human‑readable name (for printf & logging) 49 | const char *unit; ///< Engineering unit string (e.g. "V", "°C") 50 | float offset; ///< Additive offset after scaling (e.g. –273.15 for K→°C) 51 | float scaling; ///< Multiplier applied to RAW word before offset 52 | bq_data_type type; 53 | const bq_bit_desc_t *bits; 54 | uint8_t bits_count; 55 | } bq_entry; 56 | 57 | #define BQ40Z555_CMD_MANUFACTURER_ACCESS 0x00 58 | #define BQ40Z555_CMD_REMAINING_CAPACITY_ALARM 0x01 59 | #define BQ40Z555_CMD_REMAINING_TIME_ALARM 0x02 60 | #define BQ40Z555_CMD_BATTERY_MODE 0x03 61 | #define BQ40Z555_CMD_AT_RATE 0x04 62 | #define BQ40Z555_CMD_AT_RATE_TIME_TO_FULL 0x05 63 | #define BQ40Z555_CMD_AT_RATE_TIME_TO_EMPTY 0x06 64 | #define BQ40Z555_CMD_AT_RATE_OK 0x07 65 | #define BQ40Z555_CMD_TEMPERATURE 0x08 66 | #define BQ40Z555_CMD_VOLTAGE 0x09 67 | #define BQ40Z555_CMD_CURRENT 0x0A 68 | #define BQ40Z555_CMD_AVERAGE_CURRENT 0x0B 69 | #define BQ40Z555_CMD_MAX_ERROR 0x0C 70 | #define BQ40Z555_CMD_RELATIVE_STATE_OF_CHARGE 0x0D 71 | #define BQ40Z555_CMD_ABSOLUTE_STATE_OF_CHARGE 0x0E 72 | #define BQ40Z555_CMD_REMAINING_CAPACITY 0x0F 73 | #define BQ40Z555_CMD_FULL_CHARGE_CAPACITY 0x10 74 | #define BQ40Z555_CMD_RUN_TIME_TO_EMPTY 0x11 75 | #define BQ40Z555_CMD_AVERAGE_TIME_TO_EMPTY 0x12 76 | #define BQ40Z555_CMD_AVERAGE_TIME_TO_FULL 0x13 77 | #define BQ40Z555_CMD_CHARGING_CURRENT 0x14 78 | #define BQ40Z555_CMD_CHARGING_VOLTAGE 0x15 79 | #define BQ40Z555_CMD_BATTERY_STATUS 0x16 80 | #define BQ40Z555_CMD_CYCLE_COUNT 0x17 81 | #define BQ40Z555_CMD_DESIGN_CAPACITY 0x18 82 | #define BQ40Z555_CMD_DESIGN_VOLTAGE 0x19 83 | #define BQ40Z555_CMD_SPECIFICATION_INFO 0x1A 84 | #define BQ40Z555_CMD_MANUFACTURER_DATE 0x1B 85 | #define BQ40Z555_CMD_SERIAL_NUMBER 0x1C 86 | #define BQ40Z555_CMD_MANUFACTURER_NAME 0x20 87 | #define BQ40Z555_CMD_DEVICE_NAME 0x21 88 | #define BQ40Z555_CMD_DEVICE_CHEMISTRY 0x22 89 | #define BQ40Z555_CMD_MANUFACTURER_DATA 0x23 90 | #define BQ40Z555_CMD_AUTHENTICATE 0x2F 91 | #define BQ40Z555_CMD_CELL_VOLTAGE4 0x3C // Cell 3 → index order per TI 92 | #define BQ40Z555_CMD_CELL_VOLTAGE3 0x3D 93 | #define BQ40Z555_CMD_CELL_VOLTAGE2 0x3E 94 | #define BQ40Z555_CMD_CELL_VOLTAGE1 0x3F 95 | #define BQ40Z555_CMD_STATE_OF_HEALTH 0x4F 96 | #define BQ40Z555_CMD_SAFETY_ALERT 0x50 97 | #define BQ40Z555_CMD_SAFETY_STATUS 0x51 98 | #define BQ40Z555_CMD_PF_ALERT 0x52 99 | #define BQ40Z555_CMD_PF_STATUS 0x53 100 | #define BQ40Z555_CMD_OPERATION_STATUS 0x54 101 | #define BQ40Z555_CMD_CHARGING_STATUS 0x55 102 | #define BQ40Z555_CMD_GAUGING_STATUS 0x56 103 | #define BQ40Z555_CMD_MANUFACTURING_STATUS 0x57 104 | #define BQ40Z555_CMD_AFE_REGISTERS 0x58 105 | #define BQ40Z555_CMD_TURBO_POWER 0x59 106 | #define BQ40Z555_CMD_TURBO_FINAL 0x5A 107 | #define BQ40Z555_CMD_TURBO_PACK_R 0x5B 108 | #define BQ40Z555_CMD_TURBO_SYS_R 0x5C 109 | #define BQ40Z555_CMD_MIN_SYS_V 0x5D 110 | #define BQ40Z555_CMD_TURBO_CURRENT 0x5E 111 | #define BQ40Z555_CMD_LIFETIME_DATA1 0x60 112 | #define BQ40Z555_CMD_LIFETIME_DATA2 0x61 113 | #define BQ40Z555_CMD_LIFETIME_DATA3 0x62 114 | #define BQ40Z555_CMD_MANUFACTURER_INFO 0x70 115 | #define BQ40Z555_CMD_VOLTAGES 0x71 116 | #define BQ40Z555_CMD_TEMPERATURES 0x72 117 | #define BQ40Z555_CMD_IT_STATUS1 0x73 118 | #define BQ40Z555_CMD_IT_STATUS2 0x74 119 | // (ManufacturerAccess() sub‑commands 0x0000‑0x0074, 0x01yy, 0xF080‑… remain 120 | // word/block writes through CMD 0x00 and are therefore *not* enumerated here.) 121 | 122 | // ────────────────────────────────────────────────────────────────────────────── 123 | // Configuration 124 | // ────────────────────────────────────────────────────────────────────────────── 125 | 126 | #ifndef BQ40Z555_I2C_ADDR 127 | /// SMBus/I²C 7‑bit slave address of the BQ40Z555 (TI default 0x0B) 128 | #define BQ40Z555_I2C_ADDR 0x0B 129 | #endif 130 | 131 | void bq_start(); 132 | -------------------------------------------------------------------------------- /main/wifi.c: -------------------------------------------------------------------------------- 1 | #include "freertos/FreeRTOS.h" 2 | #include "freertos/event_groups.h" 3 | #include "esp_wifi.h" 4 | #include "esp_log.h" 5 | #include "esp_wps.h" 6 | #include "esp_event.h" 7 | #include "nvs_flash.h" 8 | #include "nvs.h" /* Added for NVS functions */ 9 | #include 10 | 11 | 12 | /*set wps mode via project configuration */ 13 | #define WPS_MODE WPS_TYPE_PBC 14 | 15 | #define MAX_RETRY_ATTEMPTS 2 16 | 17 | #ifndef PIN2STR 18 | #define PIN2STR(a) (a)[0], (a)[1], (a)[2], (a)[3], (a)[4], (a)[5], (a)[6], (a)[7] 19 | #define PINSTR "%c%c%c%c%c%c%c%c" 20 | #endif 21 | 22 | /* NVS definitions for Wi-Fi credentials */ 23 | #define NVS_NAMESPACE "wifi_creds" 24 | #define NVS_KEY_SSID "ssid" 25 | #define NVS_KEY_PASSWORD "password" 26 | 27 | static const char *TAG = "example_wps"; 28 | static esp_wps_config_t config = WPS_CONFIG_INIT_DEFAULT(WPS_MODE); 29 | static wifi_config_t wps_ap_creds[MAX_WPS_AP_CRED]; /* MAX_WPS_AP_CRED is usually 1 from sdkconfig */ 30 | static int s_ap_creds_num = 0; 31 | static int s_retry_num = 0; 32 | static bool s_tried_nvs_creds = false; /* Flag to track if we tried connecting with NVS credentials */ 33 | 34 | static void wifi_event_handler(void* arg, esp_event_base_t event_base, 35 | int32_t event_id, void* event_data) 36 | { 37 | static int ap_idx = 1; /* Start from 1 for subsequent APs if wps_ap_creds[0] fails */ 38 | 39 | switch (event_id) { 40 | case WIFI_EVENT_STA_START: 41 | ESP_LOGI(TAG, "WIFI_EVENT_STA_START"); 42 | /* esp_wifi_connect() will be called either from wifi_start (NVS) or WPS logic */ 43 | break; 44 | case WIFI_EVENT_STA_DISCONNECTED: 45 | ESP_LOGI(TAG, "WIFI_EVENT_STA_DISCONNECTED"); 46 | if (s_tried_nvs_creds) { 47 | ESP_LOGI(TAG, "Connection with saved NVS credentials failed."); 48 | s_tried_nvs_creds = false; /* Reset flag */ 49 | s_retry_num = 0; /* Reset retry for WPS */ 50 | ap_idx = 1; /* Reset AP index for WPS logic */ 51 | s_ap_creds_num = 0; /* Clear any old WPS creds count */ 52 | 53 | /* Optionally, clear the failed NVS credentials */ 54 | /* 55 | nvs_handle_t my_handle; 56 | esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &my_handle); 57 | if (err == ESP_OK) { 58 | nvs_erase_key(my_handle, NVS_KEY_SSID); 59 | nvs_erase_key(my_handle, NVS_KEY_PASSWORD); 60 | nvs_commit(my_handle); 61 | nvs_close(my_handle); 62 | ESP_LOGI(TAG, "Cleared saved credentials from NVS."); 63 | } 64 | */ 65 | 66 | ESP_LOGI(TAG, "Starting WPS to get new credentials..."); 67 | ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_wps_disable()); /* Ensure WPS is reset */ 68 | ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_wps_enable(&config)); 69 | ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_wps_start(0)); 70 | } else { 71 | /* This is a disconnect during WPS or subsequent retries with WPS-obtained credentials */ 72 | ESP_LOGI(TAG, "Disconnected during WPS operation or retries."); 73 | if (s_retry_num < MAX_RETRY_ATTEMPTS) { 74 | esp_wifi_connect(); /* Retry current creds */ 75 | s_retry_num++; 76 | ESP_LOGI(TAG, "Retrying connection with current credentials, attempt %d/%d", s_retry_num, MAX_RETRY_ATTEMPTS); 77 | } else if (s_ap_creds_num > 0 && ap_idx < s_ap_creds_num) { 78 | ESP_LOGI(TAG, "Trying next WPS AP credential (index %d of %d): SSID: %s", 79 | ap_idx, s_ap_creds_num, (char*)wps_ap_creds[ap_idx].sta.ssid); 80 | ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_set_config(WIFI_IF_STA, &wps_ap_creds[ap_idx++])); 81 | esp_wifi_connect(); 82 | s_retry_num = 0; /* Reset retry count for the new AP */ 83 | } else { 84 | ESP_LOGI(TAG, "All connection attempts for WPS credentials failed."); 85 | /* WPS module might re-trigger WPS on ER_FAILED or ER_TIMEOUT events */ 86 | } 87 | } 88 | break; 89 | case WIFI_EVENT_STA_WPS_ER_SUCCESS: 90 | ESP_LOGI(TAG, "WIFI_EVENT_STA_WPS_ER_SUCCESS"); 91 | { 92 | wifi_event_sta_wps_er_success_t *evt = 93 | (wifi_event_sta_wps_er_success_t *)event_data; 94 | int i; 95 | 96 | if (evt) { 97 | s_ap_creds_num = evt->ap_cred_cnt; 98 | for (i = 0; i < s_ap_creds_num; i++) { 99 | memcpy(wps_ap_creds[i].sta.ssid, evt->ap_cred[i].ssid, 100 | sizeof(evt->ap_cred[i].ssid)); 101 | memcpy(wps_ap_creds[i].sta.password, evt->ap_cred[i].passphrase, 102 | sizeof(evt->ap_cred[i].passphrase)); 103 | } 104 | /* If multiple AP credentials are received from WPS, connect with first one */ 105 | ESP_LOGI(TAG, "Connecting to SSID: %s, Passphrase: %s", 106 | wps_ap_creds[0].sta.ssid, wps_ap_creds[0].sta.password); 107 | ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_set_config(WIFI_IF_STA, &wps_ap_creds[0]) ); 108 | } 109 | /* 110 | * If only one AP credential is received from WPS, there will be no event data and 111 | * esp_wifi_set_config() is already called by WPS modules for backward compatibility 112 | * with legacy apps. So directly attempt connection here. 113 | */ 114 | ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_wps_disable()); 115 | esp_wifi_connect(); 116 | } 117 | break; 118 | case WIFI_EVENT_STA_WPS_ER_FAILED: 119 | ESP_LOGI(TAG, "WIFI_EVENT_STA_WPS_ER_FAILED"); 120 | ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_wps_disable()); 121 | ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_wps_enable(&config)); 122 | ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_wps_start(0)); 123 | break; 124 | case WIFI_EVENT_STA_WPS_ER_TIMEOUT: 125 | ESP_LOGI(TAG, "WIFI_EVENT_STA_WPS_ER_TIMEOUT"); 126 | ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_wps_disable()); 127 | ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_wps_enable(&config)); 128 | ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_wps_start(0)); 129 | break; 130 | case WIFI_EVENT_STA_WPS_ER_PIN: 131 | ESP_LOGI(TAG, "WIFI_EVENT_STA_WPS_ER_PIN"); 132 | /* display the PIN code */ 133 | wifi_event_sta_wps_er_pin_t* event_pin = (wifi_event_sta_wps_er_pin_t*) event_data; /* Renamed to avoid conflict */ 134 | ESP_LOGI(TAG, "WPS_PIN = " PINSTR, PIN2STR(event_pin->pin_code)); 135 | break; 136 | default: 137 | break; 138 | } 139 | } 140 | 141 | static void got_ip_event_handler(void* arg, esp_event_base_t event_base, 142 | int32_t event_id, void* event_data) 143 | { 144 | ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data; 145 | ESP_LOGI(TAG, "Got IP address: " IPSTR, IP2STR(&event->ip_info.ip)); 146 | 147 | /* Save the successful credentials to NVS */ 148 | wifi_config_t current_config; 149 | esp_err_t err = esp_wifi_get_config(ESP_IF_WIFI_STA, ¤t_config); 150 | if (err == ESP_OK) { 151 | if (strlen((const char*)current_config.sta.ssid) > 0) { /* Ensure SSID is not empty */ 152 | ESP_LOGI(TAG, "Successfully connected to SSID: %s/%s. Saving credentials to NVS.", (char*)current_config.sta.ssid, (char*)current_config.sta.password); 153 | nvs_handle_t my_handle; 154 | err = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &my_handle); 155 | if (err == ESP_OK) { 156 | esp_err_t err_ssid = nvs_set_str(my_handle, NVS_KEY_SSID, (const char*)current_config.sta.ssid); 157 | esp_err_t err_pass = nvs_set_str(my_handle, NVS_KEY_PASSWORD, (const char*)current_config.sta.password); 158 | 159 | if (err_ssid == ESP_OK && err_pass == ESP_OK) { 160 | err = nvs_commit(my_handle); 161 | if (err == ESP_OK) { 162 | ESP_LOGI(TAG, "Credentials saved to NVS successfully."); 163 | } else { 164 | ESP_LOGE(TAG, "NVS commit failed: %s", esp_err_to_name(err)); 165 | } 166 | } else { 167 | ESP_LOGE(TAG, "NVS set_str for SSID or Password failed. SSID_err: %s, Pass_err: %s", 168 | esp_err_to_name(err_ssid), esp_err_to_name(err_pass)); 169 | } 170 | nvs_close(my_handle); 171 | } else { 172 | ESP_LOGE(TAG, "Error opening NVS to save credentials: %s", esp_err_to_name(err)); 173 | } 174 | } else { 175 | ESP_LOGW(TAG, "Connected, but SSID is empty. Not saving credentials."); 176 | } 177 | } else { 178 | ESP_LOGE(TAG, "Error getting current Wi-Fi config to save: %s", esp_err_to_name(err)); 179 | } 180 | s_tried_nvs_creds = false; /* Reset flag, as we are successfully connected (either via NVS or new WPS) */ 181 | } 182 | 183 | /*init wifi as sta and start wps*/ 184 | void wifi_start(void) 185 | { 186 | esp_netif_t *sta_netif = esp_netif_create_default_wifi_sta(); 187 | assert(sta_netif); 188 | 189 | wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); 190 | ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_init(&cfg)); 191 | 192 | ESP_ERROR_CHECK_WITHOUT_ABORT(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler, NULL)); 193 | ESP_ERROR_CHECK_WITHOUT_ABORT(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &got_ip_event_handler, NULL)); 194 | 195 | ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_set_mode(WIFI_MODE_STA)); 196 | ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_start()); 197 | 198 | /* Attempt to load and connect with saved credentials */ 199 | nvs_handle_t my_handle; 200 | esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READONLY, &my_handle); 201 | if (err == ESP_OK) { 202 | wifi_config_t saved_config = {0}; 203 | size_t ssid_len = sizeof(saved_config.sta.ssid); 204 | size_t pass_len = sizeof(saved_config.sta.password); 205 | 206 | err = nvs_get_str(my_handle, NVS_KEY_SSID, (char *)saved_config.sta.ssid, &ssid_len); 207 | if (err == ESP_OK && ssid_len > 1) { /* ssid_len includes null terminator */ 208 | err = nvs_get_str(my_handle, NVS_KEY_PASSWORD, (char *)saved_config.sta.password, &pass_len); 209 | if (err == ESP_OK) { /* Password can be empty for open networks */ 210 | ESP_LOGI(TAG, "Found saved credentials for SSID: %s. Attempting to connect.", saved_config.sta.ssid); 211 | ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_set_config(WIFI_IF_STA, &saved_config)); 212 | s_tried_nvs_creds = true; 213 | s_retry_num = 0; /* Reset retry count for NVS attempt */ 214 | esp_wifi_connect(); 215 | nvs_close(my_handle); 216 | return; /* Exit wifi_start, connection attempt with NVS creds is in progress */ 217 | } else { 218 | ESP_LOGI(TAG, "Failed to read saved password (err: %s). Proceeding to WPS.", esp_err_to_name(err)); 219 | } 220 | } else { 221 | ESP_LOGI(TAG, "Failed to read saved SSID (err: %s, len: %d). Proceeding to WPS.", esp_err_to_name(err), ssid_len); 222 | } 223 | nvs_close(my_handle); /* Close NVS if opened but creds not fully read/used */ 224 | } else { 225 | ESP_LOGI(TAG, "NVS open failed or no '%s' namespace (err: %s). Proceeding to WPS.", NVS_NAMESPACE, esp_err_to_name(err)); 226 | } 227 | 228 | /* If NVS load/connect wasn't initiated, or failed, start WPS */ 229 | ESP_LOGI(TAG, "Starting WPS..."); 230 | s_tried_nvs_creds = false; /* Ensure flag is false if WPS is started */ 231 | /* The original vTaskDelay(pdMS_TO_TICKS(500)) was here. If needed for WPS stability, keep it. */ 232 | /* For now, assuming it's not strictly necessary before wps_enable. */ 233 | ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_wps_enable(&config)); 234 | ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_wps_start(0)); 235 | } 236 | -------------------------------------------------------------------------------- /main/i2c.c: -------------------------------------------------------------------------------- 1 | #include "driver/gpio.h" 2 | #include "driver/i2c.h" 3 | 4 | #include "esp_log.h" 5 | #include "esp_console.h" 6 | 7 | #include "i2c.h" 8 | #include "gpio_config.h" 9 | 10 | #include 11 | #include 12 | #include 13 | #include "argtable3/argtable3.h" 14 | #include "freertos/FreeRTOS.h" 15 | #include "freertos/task.h" 16 | 17 | #define MAX_I2C_WRITE_BYTES 256 18 | 19 | static const char *TAG = "i2c_cmd"; /* Added for ESP_LOG */ 20 | 21 | static struct 22 | { 23 | struct arg_int *start; 24 | struct arg_int *end; 25 | struct arg_end *end_arg; 26 | } i2cscan_args; 27 | 28 | static struct 29 | { 30 | struct arg_int *address; 31 | struct arg_int *nbytes; 32 | struct arg_end *end_arg; 33 | } i2c_r_args; 34 | 35 | static struct 36 | { 37 | struct arg_int *address; 38 | struct arg_int *data; /* Array of data bytes */ 39 | struct arg_end *end_arg; 40 | } i2c_w_args; 41 | 42 | static struct 43 | { 44 | struct arg_int *address; 45 | struct arg_str *wdata; /* Array of data bytes to write */ 46 | struct arg_int *rbytes; /* Number of bytes to read */ 47 | struct arg_int *cyclic_ms; /* Optional: cyclic delay in ms */ 48 | struct arg_int *cyclic_count; /* Optional: number of repetitions */ 49 | struct arg_end *end_arg; 50 | } i2c_rw_args; 51 | 52 | static int do_i2cscan(int argc, char **argv) 53 | { 54 | int nerrors = arg_parse(argc, argv, (void **)&i2cscan_args); 55 | if (nerrors != 0) 56 | { 57 | arg_print_errors(stderr, i2cscan_args.end_arg, argv[0]); 58 | return 1; 59 | } 60 | 61 | int start_addr = i2cscan_args.start->count ? i2cscan_args.start->ival[0] : 0x01; 62 | int end_addr = i2cscan_args.end->count ? i2cscan_args.end->ival[0] : 0x77; 63 | 64 | ESP_LOGI(TAG, "Scanning I2C bus from 0x%02X to 0x%02X:", start_addr, end_addr); 65 | 66 | for (int addr = start_addr; addr <= end_addr; ++addr) 67 | { 68 | i2c_cmd_handle_t cmd = i2c_cmd_link_create(); 69 | i2c_master_start(cmd); 70 | i2c_master_write_byte(cmd, (addr << 1) | I2C_MASTER_WRITE, 1); 71 | i2c_master_stop(cmd); 72 | esp_err_t ret = i2c_master_cmd_begin(I2C_NUM_0, cmd, 100 / portTICK_PERIOD_MS); 73 | i2c_cmd_link_delete(cmd); 74 | 75 | if (ret == ESP_OK) 76 | { 77 | ESP_LOGI(TAG, "Found device at 0x%02X", addr); 78 | } 79 | } 80 | return 0; 81 | } 82 | 83 | static int do_i2c_read_cmd(int argc, char **argv) 84 | { 85 | int nerrors = arg_parse(argc, argv, (void **)&i2c_r_args); 86 | if (nerrors != 0) 87 | { 88 | arg_print_errors(stderr, i2c_r_args.end_arg, argv[0]); 89 | return 1; 90 | } 91 | 92 | /* Check if mandatory arguments are provided */ 93 | if (i2c_r_args.address->count == 0 || i2c_r_args.nbytes->count == 0) 94 | { 95 | ESP_LOGE(TAG, "Address and number of bytes (-n) are required."); 96 | return 1; 97 | } 98 | 99 | uint8_t addr = (uint8_t)i2c_r_args.address->ival[0]; 100 | int num_bytes_to_read = i2c_r_args.nbytes->ival[0]; 101 | 102 | if (num_bytes_to_read <= 0 || num_bytes_to_read > 256 /* Arbitrary limit, adjust as needed */) 103 | { 104 | ESP_LOGE(TAG, "Invalid number of bytes to read (must be between 1 and 256)."); 105 | return 1; 106 | } 107 | 108 | uint8_t *data_buf = malloc(num_bytes_to_read); 109 | if (!data_buf) 110 | { 111 | ESP_LOGE(TAG, "Failed to allocate memory for read buffer."); 112 | return 1; 113 | } 114 | 115 | ESP_LOGI(TAG, "Reading %d byte(s) from I2C address 0x%02X...", num_bytes_to_read, addr); 116 | int ret = i2c_read(addr, data_buf, num_bytes_to_read); 117 | 118 | if (ret == 0) 119 | { 120 | char *hex_buf = malloc(num_bytes_to_read * 5 + 1); 121 | for (int i = 0; i < num_bytes_to_read; ++i) 122 | { 123 | sprintf(&hex_buf[5 * i], "0x%02X ", data_buf[i]); 124 | } 125 | ESP_LOGI(TAG, "Read data: %s", hex_buf); 126 | free(hex_buf); 127 | } 128 | else 129 | { 130 | ESP_LOGE(TAG, "Failed to read from I2C address 0x%02X.", addr); 131 | } 132 | 133 | free(data_buf); 134 | return ret == 0 ? 0 : 1; 135 | } 136 | 137 | static int do_i2c_write_cmd(int argc, char **argv) 138 | { 139 | int nerrors = arg_parse(argc, argv, (void **)&i2c_w_args); 140 | if (nerrors != 0) 141 | { 142 | arg_print_errors(stderr, i2c_w_args.end_arg, argv[0]); 143 | return 1; 144 | } 145 | 146 | /* Check if mandatory arguments are provided */ 147 | if (i2c_w_args.address->count == 0 || i2c_w_args.data->count == 0) 148 | { 149 | ESP_LOGE(TAG, "Address and at least one data byte are required."); 150 | return 1; 151 | } 152 | 153 | uint8_t addr = (uint8_t)i2c_w_args.address->ival[0]; 154 | int num_bytes_to_write = i2c_w_args.data->count; 155 | 156 | /* arg_intn already constrains count, but an explicit check is fine */ 157 | if (num_bytes_to_write <= 0) 158 | { 159 | ESP_LOGE(TAG, "Number of data bytes must be bigger than one."); 160 | return 1; 161 | } 162 | 163 | uint8_t *data_buf = malloc(num_bytes_to_write); 164 | if (!data_buf) 165 | { 166 | ESP_LOGE(TAG, "Failed to allocate memory for write buffer."); 167 | return 1; 168 | } 169 | 170 | /* Prepare buffer for hex output of data to be written */ 171 | char *hex_buf = malloc(num_bytes_to_write * 5 + 1); 172 | for (int i = 0; i < num_bytes_to_write; ++i) 173 | { 174 | if (i2c_w_args.data->ival[i] < 0 || i2c_w_args.data->ival[i] > 0xFF) 175 | { 176 | ESP_LOGE(TAG, "Data byte 0x%X (at index %d) is out of range (0x00-0xFF).", i2c_w_args.data->ival[i], i); 177 | free(data_buf); 178 | free(hex_buf); 179 | return 1; 180 | } 181 | data_buf[i] = (uint8_t)i2c_w_args.data->ival[i]; 182 | sprintf(&hex_buf[5 * i], "0x%02X ", data_buf[i]); 183 | } 184 | ESP_LOGI(TAG, "Writing %d byte(s) to I2C address 0x%02X: %s", num_bytes_to_write, addr, hex_buf); 185 | free(hex_buf); 186 | 187 | int ret = i2c_write(addr, data_buf, num_bytes_to_write); 188 | 189 | if (ret == 0) 190 | { 191 | ESP_LOGI(TAG, "Successfully wrote to I2C address 0x%02X.", addr); 192 | } 193 | else 194 | { 195 | ESP_LOGE(TAG, "Failed to write to I2C address 0x%02X.", addr); 196 | } 197 | 198 | free(data_buf); 199 | return ret == 0 ? 0 : 1; 200 | } 201 | 202 | static int do_i2c_rw_cmd(int argc, char **argv) 203 | { 204 | int nerrors = arg_parse(argc, argv, (void **)&i2c_rw_args); 205 | if (nerrors != 0) 206 | { 207 | arg_print_errors(stderr, i2c_rw_args.end_arg, argv[0]); 208 | return 1; 209 | } 210 | 211 | /* Check if mandatory arguments are provided */ 212 | if (i2c_rw_args.address->count == 0 || i2c_rw_args.wdata->count == 0 || i2c_rw_args.rbytes->count == 0) 213 | { 214 | ESP_LOGE(TAG, "Address, at least one data byte to write (-w), and number of bytes to read (-r) are required."); 215 | return 1; 216 | } 217 | 218 | uint8_t addr = (uint8_t)i2c_rw_args.address->ival[0]; 219 | 220 | int num_bytes_to_write = i2c_rw_args.wdata->count; 221 | 222 | uint8_t *w_data_buf = malloc(num_bytes_to_write); 223 | for (int i = 0; i < num_bytes_to_write; ++i) 224 | { 225 | char *end; 226 | long v = strtol(i2c_rw_args.wdata->sval[i], &end, 0); // auto 0x / dec 227 | if (*end || v < 0 || v > 0xFF) 228 | { 229 | ESP_LOGE(TAG, "Bad hex byte: %s", i2c_rw_args.wdata->sval[i]); 230 | return 1; 231 | } 232 | w_data_buf[i] = (uint8_t)v; 233 | } 234 | 235 | int num_bytes_to_read = i2c_rw_args.rbytes->ival[0]; 236 | 237 | int cyclic_ms = 0; 238 | int cyclic_count = 1; /* Default to 1 execution if --cyclic is not provided */ 239 | 240 | if (i2c_rw_args.cyclic_ms->count > 0 && i2c_rw_args.cyclic_count->count > 0) 241 | { 242 | cyclic_ms = i2c_rw_args.cyclic_ms->ival[0]; 243 | cyclic_count = i2c_rw_args.cyclic_count->ival[0]; 244 | if (cyclic_ms < 0) 245 | { 246 | ESP_LOGE(TAG, "Cyclic delay (ms) must be non-negative."); 247 | return 1; 248 | } 249 | if (cyclic_count <= 0) 250 | { 251 | ESP_LOGE(TAG, "Cyclic count must be positive."); 252 | return 1; 253 | } 254 | ESP_LOGI(TAG, "Cyclic mode: %d times, %d ms delay", cyclic_count, cyclic_ms); 255 | } 256 | else if (i2c_rw_args.cyclic_ms->count > 0 || i2c_rw_args.cyclic_count->count > 0) 257 | { 258 | ESP_LOGE(TAG, "Both and must be provided for --cyclic option."); 259 | return 1; 260 | } 261 | 262 | if (num_bytes_to_write <= 0) 263 | { 264 | ESP_LOGE(TAG, "Data bytes to write missing."); 265 | return 1; 266 | } 267 | 268 | if (num_bytes_to_read <= 0 || num_bytes_to_read > 256 /* Arbitrary limit, adjust as needed */) 269 | { 270 | ESP_LOGE(TAG, "Invalid number of bytes to read (must be between 1 and 256)."); 271 | return 1; 272 | } 273 | 274 | uint8_t *r_data_buf = malloc(num_bytes_to_read); 275 | if (!r_data_buf) 276 | { 277 | ESP_LOGE(TAG, "Failed to allocate memory for read buffer."); 278 | free(w_data_buf); 279 | return 1; 280 | } 281 | 282 | /* Prepare write-data buffer and log it */ 283 | char *hex_buf = malloc(num_bytes_to_write * 5 + 1); 284 | 285 | for (int i = 0; i < num_bytes_to_write; ++i) 286 | { 287 | /* Parse each byte string (accepts 0xNN, decimal, etc.) */ 288 | char *end; 289 | long v = strtol(i2c_rw_args.wdata->sval[i], &end, 0); 290 | if (*end != '\0' || v < 0 || v > 0xFF) 291 | { 292 | ESP_LOGE(TAG, "Invalid byte \"%s\" at index %d (expect 0x00-0xFF).", 293 | i2c_rw_args.wdata->sval[i], i); 294 | free(w_data_buf); 295 | free(r_data_buf); 296 | free(hex_buf); 297 | return 1; 298 | } 299 | 300 | w_data_buf[i] = (uint8_t)v; 301 | sprintf(&hex_buf[i * 5], "0x%02X ", w_data_buf[i]); 302 | } 303 | 304 | int overall_ret = 0; 305 | 306 | for (int cycle = 0; cycle < cyclic_count; ++cycle) 307 | { 308 | if (cyclic_count > 1) 309 | { 310 | ESP_LOGI(TAG, "Cycle %d/%d: Writing %d byte(s) [%s] to I2C addr 0x%02X, then reading %d byte(s)...", 311 | cycle + 1, cyclic_count, num_bytes_to_write, hex_buf, addr, num_bytes_to_read); 312 | } 313 | else 314 | { 315 | ESP_LOGI(TAG, "Writing %d byte(s) [%s] to I2C addr 0x%02X, then reading %d byte(s)...", num_bytes_to_write, hex_buf, addr, num_bytes_to_read); 316 | } 317 | 318 | int ret = i2c_write_read(addr, w_data_buf, num_bytes_to_write, r_data_buf, num_bytes_to_read); 319 | 320 | if (ret == 0) 321 | { 322 | char *hex_buf = malloc(num_bytes_to_read * 5 + 1); 323 | for (int i = 0; i < num_bytes_to_read; ++i) 324 | { 325 | sprintf(&hex_buf[i * 5], "0x%02X ", r_data_buf[i]); 326 | } 327 | ESP_LOGI(TAG, "Read data: %s", hex_buf); 328 | free(hex_buf); 329 | } 330 | else 331 | { 332 | ESP_LOGE(TAG, "Failed to write/read I2C address 0x%02X in cycle %d.", addr, cycle + 1); 333 | overall_ret = 1; /* Mark failure if any cycle fails */ 334 | if (cyclic_count > 1) 335 | { /* Potentially break or continue based on desired error handling for cyclic */ 336 | /* For now, continue all cycles but report overall failure */ 337 | } 338 | } 339 | if (cycle < cyclic_count - 1) 340 | { 341 | vTaskDelay(pdMS_TO_TICKS(cyclic_ms)); 342 | } 343 | } 344 | 345 | free(w_data_buf); 346 | free(r_data_buf); 347 | free(hex_buf); 348 | return overall_ret; 349 | } 350 | 351 | void register_i2c_commands(void) /* Renamed from register_i2cscan_command */ 352 | { 353 | /* i2cscan command registration (existing) */ 354 | i2cscan_args.start = arg_int0("s", "start", "", "Start address (default 0x03)"); 355 | i2cscan_args.end = arg_int0("e", "end", "", "End address (default 0x77)"); 356 | i2cscan_args.end_arg = arg_end(2); 357 | 358 | const esp_console_cmd_t i2cscan_cmd_config = { 359 | .command = "i2cscan", 360 | .help = "Scan for I2C devices on the bus", 361 | .hint = NULL, 362 | .func = &do_i2cscan, 363 | .argtable = &i2cscan_args}; 364 | ESP_ERROR_CHECK(esp_console_cmd_register(&i2cscan_cmd_config)); 365 | 366 | /* i2c_r command registration */ 367 | i2c_r_args.address = arg_int1(NULL, NULL, "", "I2C device address (e.g., 0x50 or 80)"); 368 | i2c_r_args.nbytes = arg_int1("n", "num", "", "Number of bytes to read (1-256)"); 369 | i2c_r_args.end_arg = arg_end(2); /* Max 2 errors to store for address and nbytes */ 370 | 371 | const esp_console_cmd_t i2c_r_cmd_config = { 372 | .command = "i2c_r", 373 | .help = "Read N bytes from an I2C device. Usage: i2c_r -n ", 374 | .hint = " -n ", 375 | .func = &do_i2c_read_cmd, 376 | .argtable = &i2c_r_args}; 377 | ESP_ERROR_CHECK(esp_console_cmd_register(&i2c_r_cmd_config)); 378 | 379 | /* i2c_w command registration */ 380 | i2c_w_args.address = arg_int1(NULL, NULL, "", "I2C device address (e.g., 0x50 or 80)"); 381 | i2c_w_args.data = arg_intn(NULL, NULL, "", 1, MAX_I2C_WRITE_BYTES, "Data byte(s) to write (e.g., 0x01 0xAA 255)"); 382 | i2c_w_args.end_arg = arg_end(MAX_I2C_WRITE_BYTES + 1); /* Max errors: 1 for addr + N for data bytes */ 383 | 384 | const esp_console_cmd_t i2c_w_cmd_config = { 385 | .command = "i2c_w", 386 | .help = "Write byte(s) to an I2C device. Usage: i2c_w [byte2 ... byteN]", 387 | .hint = " [byte2...]", 388 | .func = &do_i2c_write_cmd, 389 | .argtable = &i2c_w_args}; 390 | ESP_ERROR_CHECK(esp_console_cmd_register(&i2c_w_cmd_config)); 391 | 392 | /* i2c_rw command registration */ 393 | i2c_rw_args.address = arg_int1(NULL, NULL, "", "I2C device address (e.g., 0x50 or 80)"); 394 | i2c_rw_args.wdata = arg_strn("w", "wdata", "", 1, MAX_I2C_WRITE_BYTES, "Data byte(s) to write"); 395 | i2c_rw_args.rbytes = arg_int1("r", "rbytes", "", "Number of bytes to read (1-256)"); 396 | i2c_rw_args.cyclic_ms = arg_int0(NULL, "cyclic", " ", "Optional: Repeat times with delay. Both ms and count required if used."); 397 | i2c_rw_args.cyclic_count = arg_int0(NULL, NULL, "", "Number of repetitions for --cyclic"); 398 | i2c_rw_args.end_arg = arg_end(MAX_I2C_WRITE_BYTES + 4); /* Max errors: 1 for addr, N for wdata, 1 for rbytes, 1 for ms, 1 for count */ 399 | 400 | const esp_console_cmd_t i2c_rw_cmd_config = { 401 | .command = "i2c_rw", 402 | .help = "Write byte(s) to an I2C device then read N bytes. Usage: i2c_rw -w ... -r [--cyclic ]", 403 | .hint = " -w ... -r [--cyclic ]", 404 | .func = &do_i2c_rw_cmd, 405 | .argtable = &i2c_rw_args}; 406 | ESP_ERROR_CHECK(esp_console_cmd_register(&i2c_rw_cmd_config)); 407 | } 408 | 409 | int i2c_write(uint8_t addr, const uint8_t *data, size_t len) 410 | { 411 | i2c_cmd_handle_t cmd = i2c_cmd_link_create(); 412 | i2c_master_start(cmd); 413 | i2c_master_write_byte(cmd, (addr << 1) | I2C_MASTER_WRITE, 1); 414 | i2c_master_write(cmd, (uint8_t *)data, len, 1); 415 | i2c_master_stop(cmd); 416 | esp_err_t ret = i2c_master_cmd_begin(I2C_NUM_0, cmd, 100 / portTICK_PERIOD_MS); 417 | i2c_cmd_link_delete(cmd); 418 | return ret == ESP_OK ? 0 : -1; 419 | } 420 | 421 | int i2c_write_partial(uint8_t addr, const uint8_t *data, size_t len, bool stop) 422 | { 423 | i2c_cmd_handle_t cmd = i2c_cmd_link_create(); 424 | i2c_master_start(cmd); 425 | i2c_master_write_byte(cmd, (addr << 1) | I2C_MASTER_WRITE, 1); 426 | i2c_master_write(cmd, (uint8_t *)data, len, 1); 427 | if (stop) 428 | { 429 | i2c_master_stop(cmd); 430 | } 431 | esp_err_t ret = i2c_master_cmd_begin(I2C_NUM_0, cmd, 100 / portTICK_PERIOD_MS); 432 | i2c_cmd_link_delete(cmd); 433 | return ret == ESP_OK ? 0 : -1; 434 | } 435 | 436 | int i2c_read(uint8_t addr, uint8_t *data, size_t len) 437 | { 438 | i2c_cmd_handle_t cmd = i2c_cmd_link_create(); 439 | i2c_master_start(cmd); 440 | i2c_master_write_byte(cmd, (addr << 1) | I2C_MASTER_READ, 1); 441 | if (len > 1) 442 | { 443 | i2c_master_read(cmd, data, len - 1, I2C_MASTER_ACK); 444 | } 445 | i2c_master_read_byte(cmd, data + len - 1, I2C_MASTER_NACK); 446 | i2c_master_stop(cmd); 447 | esp_err_t ret = i2c_master_cmd_begin(I2C_NUM_0, cmd, 100 / portTICK_PERIOD_MS); 448 | i2c_cmd_link_delete(cmd); 449 | return ret == ESP_OK ? 0 : -1; 450 | } 451 | 452 | int i2c_write_read(uint8_t addr, const uint8_t *wdata, size_t wlen, uint8_t *rdata, size_t rlen) 453 | { 454 | i2c_cmd_handle_t cmd = i2c_cmd_link_create(); 455 | i2c_master_start(cmd); 456 | i2c_master_write_byte(cmd, (addr << 1) | I2C_MASTER_WRITE, 1); 457 | if (wlen > 0) 458 | { 459 | i2c_master_write(cmd, (uint8_t *)wdata, wlen, 1); 460 | } 461 | i2c_master_start(cmd); 462 | i2c_master_write_byte(cmd, (addr << 1) | I2C_MASTER_READ, 1); 463 | if (rlen > 1) 464 | { 465 | i2c_master_read(cmd, rdata, rlen - 1, I2C_MASTER_ACK); 466 | } 467 | i2c_master_read_byte(cmd, rdata + rlen - 1, I2C_MASTER_NACK); 468 | i2c_master_stop(cmd); 469 | 470 | esp_err_t ret = i2c_master_cmd_begin(I2C_NUM_0, cmd, 100 / portTICK_PERIOD_MS); 471 | i2c_cmd_link_delete(cmd); 472 | return ret == ESP_OK ? 0 : -1; 473 | } 474 | 475 | void i2c_init() 476 | { 477 | /* Configure I2C parameters */ 478 | i2c_config_t conf = { 479 | .mode = I2C_MODE_MASTER, 480 | .sda_io_num = GPIO_I2C_SDA, 481 | .scl_io_num = GPIO_I2C_SCL, 482 | .sda_pullup_en = 1, 483 | .scl_pullup_en = 1, 484 | .master.clk_speed = 100000, 485 | .clk_flags = 0}; 486 | i2c_param_config(I2C_NUM_0, &conf); 487 | i2c_driver_install(I2C_NUM_0, conf.mode, 0, 0, 0); 488 | register_i2c_commands(); 489 | } 490 | -------------------------------------------------------------------------------- /main/telnet.c: -------------------------------------------------------------------------------- 1 | /* telnet.c */ 2 | #include 3 | #include /* For MIN/MAX */ 4 | #include "freertos/FreeRTOS.h" 5 | #include "freertos/task.h" 6 | #include "esp_system.h" 7 | #include "esp_log.h" 8 | #include "esp_netif.h" /* For esp_netif_init, if not done elsewhere */ 9 | #include "esp_event.h" /* For esp_event_loop_create_default, if not done elsewhere */ 10 | 11 | #include "lwip/err.h" 12 | #include "lwip/sockets.h" 13 | #include "lwip/sys.h" 14 | #include 15 | 16 | /* Added for ESP Console integration */ 17 | #include "esp_console.h" 18 | #include 19 | #include 20 | #include /* Required for va_list, va_copy, etc. */ 21 | 22 | /* Telnet Command Definitions */ 23 | #define TELNET_IAC 255 /* Interpret As Command */ 24 | #define TELNET_DONT 254 /* Don't perform option */ 25 | #define TELNET_DO 253 /* Do perform option */ 26 | #define TELNET_WONT 252 /* Won't perform option */ 27 | #define TELNET_WILL 251 /* Will perform option */ 28 | #define TELNET_SB 250 /* Subnegotiation Begin */ 29 | #define TELNET_SE 240 /* Subnegotiation End */ 30 | 31 | /* Telnet Options */ 32 | #define TELNET_OPT_BINARY 0 /* Binary Transmission */ 33 | #define TELNET_OPT_ECHO 1 /* Echo */ 34 | #define TELNET_OPT_RECONNECTION 2 /* Reconnection */ 35 | #define TELNET_OPT_SGA 3 /* Suppress Go Ahead */ 36 | #define TELNET_OPT_TTYPE 24 /* Terminal Type */ 37 | #define TELNET_OPT_NAWS 31 /* Negotiate About Window Size */ 38 | #define TELNET_OPT_LINEMODE 34 /* Linemode */ 39 | 40 | /* Static variable for the current Telnet client socket, -1 if none. */ 41 | static int s_telnet_client_sock = -1; 42 | /* Static variable to store the original vprintf log handler. */ 43 | static vprintf_like_t s_original_vprintf_handler = NULL; 44 | 45 | #define TELNET_PORT 23 46 | #define TELNET_KEEPALIVE_IDLE 5 /* Keepalive idle time (seconds) */ 47 | #define TELNET_KEEPALIVE_INTERVAL 5 /* Keepalive interval time (seconds) */ 48 | #define TELNET_KEEPALIVE_COUNT 3 /* Keepalive packet retry count */ 49 | #define TELNET_RX_BUFFER_SIZE 128 50 | #define TELNET_TASK_STACK_SIZE 6188 51 | #define TELNET_TASK_PRIORITY 5 52 | #define TELNET_MAX_CONNECTIONS 1 /* Max simultaneous connections (listen backlog) */ 53 | 54 | static const char *TAG_TELNET = "telnet_server"; 55 | 56 | /* 57 | * Custom vprintf implementation that redirects log output to the Telnet socket 58 | * in addition to calling the original vprintf handler. 59 | */ 60 | static int telnet_vprintf_redirect(const char *format, va_list args) 61 | { 62 | int ret = 0; 63 | 64 | /* Call the original vprintf handler to output to UART/default log. */ 65 | 66 | /* If a Telnet client is connected, send the log to them. */ 67 | if (s_telnet_client_sock == -1) 68 | { 69 | return s_original_vprintf_handler(format, args); 70 | } 71 | 72 | char temp_buffer[256]; /* Buffer for the formatted log line. */ 73 | int len = vsnprintf(temp_buffer, sizeof(temp_buffer), format, args); 74 | 75 | if (len > 0) 76 | { 77 | /* Send character by character, converting \n to \r\n for Telnet. */ 78 | for (int i = 0; i < len && i < sizeof(temp_buffer) - 1; ++i) 79 | { 80 | if (temp_buffer[i] == '\n') 81 | { 82 | if (send(s_telnet_client_sock, "\r\n", 2, 0) < 0) 83 | { 84 | /* Error sending, client might have disconnected abruptly. */ 85 | /* No easy way to fully handle this here without more state. */ 86 | break; 87 | } 88 | } 89 | else 90 | { 91 | if (send(s_telnet_client_sock, &temp_buffer[i], 1, 0) < 0) 92 | { 93 | break; 94 | } 95 | } 96 | } 97 | } 98 | 99 | return ret; 100 | } 101 | 102 | /* Helper to send Telnet command sequences */ 103 | static void send_telnet_iac(const int sock, unsigned char command, unsigned char option) 104 | { 105 | unsigned char seq[3]; 106 | seq[0] = TELNET_IAC; 107 | seq[1] = command; 108 | seq[2] = option; 109 | if (send(sock, seq, 3, 0) < 0) 110 | { 111 | ESP_LOGE(TAG_TELNET, "Error sending IAC command %u %u: errno %d", command, option, errno); 112 | } 113 | } 114 | 115 | /* 116 | * Handles a single Telnet client connection. 117 | * This function will run indefinitely for the duration of the connection, 118 | * passing input to the ESP console and sending output back. 119 | * Implements basic character-by-character handling and server-side echo. 120 | */ 121 | static void handle_telnet_client_connection(const int sock) 122 | { 123 | char line_buffer[TELNET_RX_BUFFER_SIZE]; 124 | int line_len = 0; 125 | char input_char_buf[1]; /* Buffer for single character recv */ 126 | 127 | const char *welcome_msg = "Welcome to ESP32 Telnet Console!\r\nType 'help' for a list of commands.\r\n"; 128 | const char *prompt = "> "; 129 | char *output_buffer = NULL; 130 | size_t output_buffer_size = 0; 131 | FILE *temp_stdout = NULL; 132 | FILE *original_stdout = stdout; /* Store original stdout */ 133 | 134 | ESP_LOGI(TAG_TELNET, "New client connection, attempting to set character mode."); 135 | s_telnet_client_sock = sock; /* Set the active telnet socket for logging */ 136 | 137 | /* Negotiate Telnet options: Server WILL ECHO, Server WILL SGA */ 138 | /* This tells the client that the server will handle echoing and suppress go-ahead prompts */ 139 | send_telnet_iac(sock, TELNET_WILL, TELNET_OPT_ECHO); 140 | send_telnet_iac(sock, TELNET_WILL, TELNET_OPT_SGA); 141 | /* Optionally, ask client to DO SGA and DO ECHO if we want to confirm client state, */ 142 | /* but for simplicity, we assume client will adapt or server-side handling is sufficient. */ 143 | /* send_telnet_iac(sock, TELNET_DO, TELNET_OPT_SGA); */ 144 | /* send_telnet_iac(sock, TELNET_DO, TELNET_OPT_ECHO); // If we want client to also echo, usually not with server echo */ 145 | 146 | /* Send welcome message */ 147 | if (send(sock, welcome_msg, strlen(welcome_msg), 0) < 0) 148 | { 149 | ESP_LOGE(TAG_TELNET, "Error sending welcome message: errno %d", errno); 150 | goto close_socket_cleanup; 151 | } 152 | 153 | /* Send initial prompt */ 154 | if (send(sock, prompt, strlen(prompt), 0) < 0) 155 | { 156 | ESP_LOGE(TAG_TELNET, "Error sending initial prompt: errno %d", errno); 157 | goto close_socket_cleanup; 158 | } 159 | 160 | memset(line_buffer, 0, sizeof(line_buffer)); 161 | line_len = 0; 162 | 163 | do 164 | { 165 | int len_recv = recv(sock, input_char_buf, 1, 0); 166 | 167 | if (len_recv < 0) 168 | { 169 | ESP_LOGE(TAG_TELNET, "Error occurred during receiving: errno %d", errno); 170 | break; /* Break loop on receive error */ 171 | } 172 | else if (len_recv == 0) 173 | { 174 | ESP_LOGI(TAG_TELNET, "Connection closed by client"); 175 | break; /* Break loop if client closes connection */ 176 | } 177 | 178 | unsigned char c = input_char_buf[0]; 179 | 180 | if (c == TELNET_IAC) 181 | { 182 | unsigned char iac_cmd_buf[2]; 183 | int iac_len = recv(sock, iac_cmd_buf, 2, 0); 184 | if (iac_len == 2) 185 | { 186 | unsigned char iac_command = iac_cmd_buf[0]; 187 | unsigned char iac_option = iac_cmd_buf[1]; 188 | ESP_LOGD(TAG_TELNET, "Received IAC %u %u", iac_command, iac_option); 189 | /* Basic handling: if client says DONT ECHO, we should stop server-side echo. */ 190 | /* This part can be expanded to a full Telnet option state machine. */ 191 | /* For now, we mostly ignore client's responses to keep it simple, */ 192 | /* assuming our initial WILLs are accepted. */ 193 | if (iac_command == TELNET_DO && iac_option == TELNET_OPT_TTYPE) 194 | { 195 | /* Client wants to send terminal type, respond with WILL TTYPE then SB ... SE */ 196 | /* This is more advanced, for now just acknowledge by sending WONT */ 197 | send_telnet_iac(sock, TELNET_WONT, TELNET_OPT_TTYPE); 198 | } 199 | } 200 | else 201 | { 202 | ESP_LOGE(TAG_TELNET, "Error receiving IAC sequence or connection closed."); 203 | break; 204 | } 205 | continue; /* Processed IAC, get next char */ 206 | } 207 | 208 | /* Handle actual characters */ 209 | if (c == '\r') 210 | { /* Carriage return */ 211 | /* Typically followed by \n (from client) or client sends \r as Enter */ 212 | line_buffer[line_len] = 0; /* Null-terminate */ 213 | if (send(sock, "\r\n", 2, 0) < 0) 214 | { 215 | break; 216 | } /* Echo CR LF */ 217 | 218 | ESP_LOGI(TAG_TELNET, "Received command: '%s'", line_buffer); 219 | 220 | if (strcmp(line_buffer, "exit") == 0) 221 | { 222 | ESP_LOGI(TAG_TELNET, "Client requested exit"); 223 | if (send(sock, "Goodbye!\r\n", strlen("Goodbye!\r\n"), 0) < 0) 224 | { /* Ignore error on exit */ 225 | } 226 | break; 227 | } 228 | else if (line_len > 0) 229 | { 230 | temp_stdout = open_memstream(&output_buffer, &output_buffer_size); 231 | if (!temp_stdout) 232 | { 233 | ESP_LOGE(TAG_TELNET, "Failed to open memstream"); 234 | if (send(sock, "Error: Internal server error (memstream)\r\n", strlen("Error: Internal server error (memstream)\r\n"), 0) < 0) 235 | { 236 | break; 237 | } 238 | /* No continue here, will go to prompt sending */ 239 | } 240 | else 241 | { 242 | stdout = temp_stdout; 243 | int cmd_ret_code; 244 | esp_err_t exec_ret = esp_console_run(line_buffer, &cmd_ret_code); 245 | stdout = original_stdout; 246 | fflush(temp_stdout); 247 | fclose(temp_stdout); 248 | temp_stdout = NULL; 249 | 250 | if (exec_ret == ESP_ERR_NOT_FOUND) 251 | { 252 | if (send(sock, "Error: Command not found\r\n", strlen("Error: Command not found\r\n"), 0) < 0) 253 | { 254 | break; 255 | } 256 | } 257 | else if (exec_ret == ESP_ERR_INVALID_ARG) 258 | { 259 | if (send(sock, "Error: Invalid arguments\r\n", strlen("Error: Invalid arguments\r\n"), 0) < 0) 260 | { 261 | break; 262 | } 263 | } 264 | else if (exec_ret != ESP_OK) 265 | { 266 | char err_msg[80]; 267 | snprintf(err_msg, sizeof(err_msg), "Error: Command failed (err %d)\r\n", exec_ret); 268 | if (send(sock, err_msg, strlen(err_msg), 0) < 0) 269 | { 270 | break; 271 | } 272 | } 273 | 274 | if (output_buffer && output_buffer_size > 0) 275 | { 276 | if (send(sock, output_buffer, output_buffer_size, 0) < 0) 277 | { 278 | break; 279 | } 280 | /* Ensure CRLF if not present, common for console output */ 281 | if (output_buffer_size > 0 && output_buffer[output_buffer_size - 1] != '\n') 282 | { 283 | if (send(sock, "\r\n", 2, 0) < 0) 284 | { 285 | break; 286 | } 287 | } 288 | } 289 | else if (exec_ret == ESP_OK && cmd_ret_code == ESP_OK) 290 | { 291 | /* If command was successful but produced no output, still send a CRLF for neatness */ 292 | if (send(sock, "\r\n", 2, 0) < 0) 293 | { 294 | break; 295 | } 296 | } 297 | if (output_buffer) 298 | { 299 | free(output_buffer); 300 | output_buffer = NULL; 301 | output_buffer_size = 0; 302 | } 303 | } 304 | } 305 | else 306 | { /* Empty line submitted */ 307 | /* send(sock, "\r\n", 2, 0); // Already sent CR LF above */ 308 | } 309 | 310 | /* Reset line buffer and send prompt for next command */ 311 | memset(line_buffer, 0, sizeof(line_buffer)); 312 | line_len = 0; 313 | if (send(sock, prompt, strlen(prompt), 0) < 0) 314 | { 315 | break; 316 | } 317 | } 318 | else if (c == '\n') 319 | { /* Line feed */ 320 | /* Typically ignored if \r was just processed. Some clients might send \n alone. */ 321 | /* For simplicity, we primarily act on \r. If \r handling also sends prompt, this might double prompt. */ 322 | /* If line_buffer is not empty and last char was not \r, could treat as enter. */ 323 | /* Current logic: \r is the main EOL trigger. */ 324 | continue; 325 | } 326 | else if (c == '\b' || c == '\x7f') 327 | { /* Backspace or Delete */ 328 | if (line_len > 0) 329 | { 330 | line_len--; 331 | line_buffer[line_len] = 0; /* Effectively delete char */ 332 | /* Echo backspace, space, backspace to erase on client terminal */ 333 | if (send(sock, "\b \b", 3, 0) < 0) 334 | { 335 | break; 336 | } 337 | } 338 | } 339 | else if (c >= 32 && c < 127) 340 | { /* Printable ASCII characters */ 341 | if (line_len < (sizeof(line_buffer) - 1)) 342 | { 343 | line_buffer[line_len++] = c; 344 | line_buffer[line_len] = 0; /* Keep null-terminated */ 345 | /* Echo character back to client */ 346 | if (send(sock, &c, 1, 0) < 0) 347 | { 348 | break; 349 | } 350 | } 351 | } 352 | else 353 | { 354 | /* Other control characters or non-ASCII, log or ignore */ 355 | ESP_LOGD(TAG_TELNET, "Received unhandled char: 0x%02X", c); 356 | } 357 | 358 | } while (1); /* Loop indefinitely until break */ 359 | 360 | close_socket_cleanup: 361 | ESP_LOGI(TAG_TELNET, "Shutting down client socket and closing connection"); 362 | s_telnet_client_sock = -1; /* Clear the active telnet socket for logging */ 363 | 364 | /* Ensure stdout is restored if loop broken unexpectedly while redirected */ 365 | if (stdout != original_stdout) 366 | { 367 | stdout = original_stdout; 368 | } 369 | /* Clean up resources if they were allocated and not freed due to an error exit */ 370 | if (temp_stdout) 371 | { 372 | fclose(temp_stdout); 373 | } 374 | if (output_buffer) 375 | { 376 | free(output_buffer); 377 | } 378 | 379 | shutdown(sock, SHUT_RDWR); /* SHUT_RDWR to signal no more send/receive */ 380 | close(sock); 381 | } 382 | 383 | /* 384 | * Main Telnet server task. 385 | * Creates a listening socket and accepts incoming connections. 386 | */ 387 | static void telnet_server_main_task(void *pvParameters) 388 | { 389 | char addr_str[128]; 390 | int addr_family = AF_INET; /* For IPv4 */ 391 | int ip_protocol = IPPROTO_IP; 392 | struct sockaddr_storage dest_addr; /* Use sockaddr_storage for IPv4/IPv6 compatibility */ 393 | 394 | /* Prepare the destination address structure */ 395 | if (addr_family == AF_INET) 396 | { 397 | struct sockaddr_in *dest_addr_ip4 = (struct sockaddr_in *)&dest_addr; 398 | dest_addr_ip4->sin_addr.s_addr = htonl(INADDR_ANY); /* Listen on all interfaces */ 399 | dest_addr_ip4->sin_family = AF_INET; 400 | dest_addr_ip4->sin_port = htons(TELNET_PORT); 401 | } 402 | else 403 | { 404 | ESP_LOGE(TAG_TELNET, "Unsupported address family: %d", addr_family); 405 | vTaskDelete(NULL); 406 | return; 407 | } 408 | 409 | int listen_sock = socket(addr_family, SOCK_STREAM, ip_protocol); 410 | if (listen_sock < 0) 411 | { 412 | ESP_LOGE(TAG_TELNET, "Unable to create socket: errno %d", errno); 413 | vTaskDelete(NULL); 414 | return; 415 | } 416 | ESP_LOGI(TAG_TELNET, "Socket created successfully"); 417 | 418 | /* Set SO_REUSEADDR to allow binding to the same address/port if socket was recently closed */ 419 | int opt = 1; 420 | if (setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) 421 | { 422 | ESP_LOGW(TAG_TELNET, "setsockopt(SO_REUSEADDR) failed: errno %d", errno); 423 | /* Not fatal, but can be an issue on quick restarts */ 424 | } 425 | 426 | int err = bind(listen_sock, (struct sockaddr *)&dest_addr, sizeof(dest_addr)); 427 | if (err != 0) 428 | { 429 | ESP_LOGE(TAG_TELNET, "Socket unable to bind: errno %d", errno); 430 | goto CLEAN_UP_SOCKET; 431 | } 432 | ESP_LOGI(TAG_TELNET, "Socket bound to port %d", TELNET_PORT); 433 | 434 | /* Set our custom vprintf handler. Store the original one. */ 435 | /* This should ideally be done only once if the task persists.*/ 436 | if (s_original_vprintf_handler == NULL) 437 | { 438 | s_original_vprintf_handler = esp_log_set_vprintf(telnet_vprintf_redirect); 439 | ESP_LOGI(TAG_TELNET, "Telnet vprintf redirector installed."); 440 | } 441 | 442 | err = listen(listen_sock, TELNET_MAX_CONNECTIONS); 443 | if (err != 0) 444 | { 445 | ESP_LOGE(TAG_TELNET, "Error occurred during listen: errno %d", errno); 446 | goto CLEAN_UP_SOCKET; 447 | } 448 | 449 | ESP_LOGI(TAG_TELNET, "Telnet server listening on port %d", TELNET_PORT); 450 | 451 | while (1) 452 | { 453 | ESP_LOGI(TAG_TELNET, "Waiting for a new connection..."); 454 | 455 | struct sockaddr_storage source_addr; /* Used to store client address */ 456 | socklen_t addr_len = sizeof(source_addr); 457 | int client_sock = accept(listen_sock, (struct sockaddr *)&source_addr, &addr_len); 458 | 459 | if (client_sock < 0) 460 | { 461 | ESP_LOGE(TAG_TELNET, "Unable to accept connection: errno %d", errno); 462 | /* Consider adding a delay here or checking for specific errors if accept fails continuously */ 463 | if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK) 464 | { 465 | /* Non-fatal errors, continue listening */ 466 | continue; 467 | } 468 | break; /* For other errors, break the loop and clean up */ 469 | } 470 | 471 | /* Connection accepted */ 472 | if (source_addr.ss_family == PF_INET) 473 | { /* PF_INET is the same as AF_INET */ 474 | struct sockaddr_in *s = (struct sockaddr_in *)&source_addr; 475 | inet_ntoa_r(s->sin_addr, addr_str, sizeof(addr_str) - 1); 476 | ESP_LOGI(TAG_TELNET, "Connection accepted from: %s:%u", addr_str, ntohs(s->sin_port)); 477 | } 478 | 479 | /* Configure TCP keepalive */ 480 | int keepAlive = 1; 481 | int keepIdle = TELNET_KEEPALIVE_IDLE; 482 | int keepInterval = TELNET_KEEPALIVE_INTERVAL; 483 | int keepCount = TELNET_KEEPALIVE_COUNT; 484 | setsockopt(client_sock, SOL_SOCKET, SO_KEEPALIVE, &keepAlive, sizeof(int)); 485 | setsockopt(client_sock, IPPROTO_TCP, TCP_KEEPIDLE, &keepIdle, sizeof(int)); 486 | setsockopt(client_sock, IPPROTO_TCP, TCP_KEEPINTVL, &keepInterval, sizeof(int)); 487 | setsockopt(client_sock, IPPROTO_TCP, TCP_KEEPCNT, &keepCount, sizeof(int)); 488 | 489 | /* 490 | * Handle the client connection. 491 | * For simplicity, this example handles one client at a time. 492 | * For concurrent clients, a new task should be created for each connection. 493 | */ 494 | handle_telnet_client_connection(client_sock); 495 | /* After handle_telnet_client_connection returns, the client socket is closed by it. */ 496 | } 497 | 498 | CLEAN_UP_SOCKET: 499 | ESP_LOGI(TAG_TELNET, "Closing listen socket."); 500 | close(listen_sock); 501 | vTaskDelete(NULL); /* Delete this task */ 502 | } 503 | 504 | /* 505 | * Public function to initialize and start the Telnet server. 506 | * This should be called once from your application's main initialization sequence. 507 | * Ensure network (Wi-Fi/Ethernet) is initialized before calling this. 508 | * 509 | * 510 | */ 511 | void telnet_start(void) /* Renamed to avoid conflict with potential framework start_ functions */ 512 | { 513 | /* 514 | * It's assumed that network interface (Wi-Fi or Ethernet) has been initialized 515 | * and the device is connected to the network before this function is called. 516 | */ 517 | BaseType_t xReturned = xTaskCreate(telnet_server_main_task, /* Task function */ 518 | "telnet_srv_task", /* Name of task */ 519 | TELNET_TASK_STACK_SIZE, /* Stack size of task */ 520 | NULL, /* Parameter of the task */ 521 | TELNET_TASK_PRIORITY, /* Priority of the task */ 522 | NULL); /* Task handle (NULL if not needed) */ 523 | 524 | if (xReturned == pdPASS) 525 | { 526 | ESP_LOGI(TAG_TELNET, "Telnet server task created successfully."); 527 | } 528 | else 529 | { 530 | ESP_LOGE(TAG_TELNET, "Failed to create Telnet server task."); 531 | /* Optionally, add more robust error handling here */ 532 | } 533 | } 534 | -------------------------------------------------------------------------------- /main/bq.c: -------------------------------------------------------------------------------- 1 | // bq.c – ESP-IDF esp_console command(s) for TI BQ40Z555 gas-gauge 2 | // 3 | // This module registers commands that talk to a BQ40Z555 over SMBus/I²C using 4 | // the helper function below provided by board support code: 5 | // int i2c_write_read(uint8_t addr, 6 | // const uint8_t *wdata, size_t wlen, 7 | // uint8_t *rdata, size_t rlen); 8 | // 9 | // Copyright (c) 2025 10 | // SPDX-License-Identifier: MIT 11 | 12 | #include 13 | #include 14 | #include 15 | #include "esp_console.h" 16 | #include "esp_log.h" 17 | #include "argtable3/argtable3.h" 18 | #include "bq.h" 19 | 20 | #define COUNT(x) (sizeof(x) / sizeof((x)[0])) 21 | 22 | // ────────────────────────────────────────────────────────────────────────────── 23 | // Utility 24 | // ────────────────────────────────────────────────────────────────────────────── 25 | static inline uint16_t le16(const uint8_t *p) 26 | { 27 | return (uint16_t)p[0] | ((uint16_t)p[1] << 8); 28 | } 29 | 30 | // ────────────────────────────────────────────────────────────────────────────── 31 | // Configuration 32 | // ────────────────────────────────────────────────────────────────────────────── 33 | 34 | static const char *TAG = "bq"; 35 | 36 | // Declaration of BSP helper (implemented elsewhere) 37 | int i2c_write_read(uint8_t addr, const uint8_t *wdata, size_t wlen, 38 | uint8_t *rdata, size_t rlen); 39 | 40 | // ────────────────────────────────────────────────────────────────────────────── 41 | // SafetyAlert() bit descriptions (global, can be shared by other tables) 42 | // ────────────────────────────────────────────────────────────────────────────── 43 | static const bq_bit_desc_t SAFETY_ALERT_BITS[] = { 44 | {0, 1, .desc = "OCC", .long_desc = "Over-Charge Current"}, 45 | {1, 1, .desc = "OCD", .long_desc = "Over-Discharge Current"}, 46 | {2, 1, .desc = "COV", .long_desc = "Cell Over-Voltage"}, 47 | {3, 1, .desc = "CUV", .long_desc = "Cell Under-Voltage"}, 48 | {4, 1, .desc = "OTC", .long_desc = "Over-Temp Charge"}, 49 | {5, 1, .desc = "OTD", .long_desc = "Over-Temp Discharge"}, 50 | {6, 1, .desc = "SCD", .long_desc = "Short-Circuit Discharge"}, 51 | {7, 1, .desc = "OLD", .long_desc = "Overload Protection"}, 52 | {8, 1, .desc = "RSVD8", .long_desc = "Reserved"}, 53 | {9, 1, .desc = "RSVD9", .long_desc = "Reserved"}, 54 | {10, 1, .desc = "RSVD10", .long_desc = "Reserved"}, 55 | {11, 1, .desc = "RSVD11", .long_desc = "Reserved"}, 56 | {12, 1, .desc = "RSVD12", .long_desc = "Reserved"}, 57 | {13, 1, .desc = "PF", .long_desc = "Permanent Fail"}, 58 | {14, 1, .desc = "SLEEP", .long_desc = "Sleep"}, 59 | {15, 1, .desc = "RSVD15", .long_desc = "Reserved"}}; 60 | 61 | // ────────────────────────────────────────────────────────────────────────────── 62 | // SafetyStatus (0x51) 63 | // ────────────────────────────────────────────────────────────────────────────── 64 | static const bq_bit_desc_t BITS_SAFETY_STATUS[] = { 65 | {0, 1, .desc = "CUV", .long_desc = "Cell UnderVoltage"}, 66 | {1, 1, .desc = "COV", .long_desc = "Cell Overvoltage"}, 67 | {2, 1, .desc = "OCC1", .long_desc = "Overcurrent in Charge 1st Tier"}, 68 | {3, 1, .desc = "OCC2", .long_desc = "Overcurrent in Charge 2nd Tier"}, 69 | {4, 1, .desc = "OCD1", .long_desc = "Overcurrent in Discharge 1st Tier"}, 70 | {5, 1, .desc = "OCD2", .long_desc = "Overcurrent in Discharge 2nd Tier"}, 71 | {6, 1, .desc = "OLD", .long_desc = "Overload in discharge"}, 72 | {7, 1, .desc = "OLDL", .long_desc = "Overload in discharge latch"}, 73 | {8, 1, .desc = "SCC", .long_desc = "Short circuit in charge"}, 74 | {9, 1, .desc = "SCCL", .long_desc = "Short circuit in charge latch"}, 75 | {10, 1, .desc = "SCD", .long_desc = "Short circuit in discharge"}, 76 | {11, 1, .desc = "SCDL", .long_desc = "Short circuit in discharge latch"}, 77 | {12, 1, .desc = "OTC", .long_desc = "Overtemperature in charge"}, 78 | {13, 1, .desc = "OTD", .long_desc = "Overtemperature in discharge"}, 79 | {14, 1, .desc = "CUVC", .long_desc = "I*R compensated CUV"}, 80 | {15, 1, .desc = "RSVD15", .long_desc = "Reserved"}, 81 | {16, 1, .desc = "OTF", .long_desc = "FET overtemperature"}, 82 | {17, 1, .desc = "HWD", .long_desc = "SBS Host watchdog timeout"}, 83 | {18, 1, .desc = "PTO", .long_desc = "Precharging timeout"}, 84 | {19, 1, .desc = "RSVD19", .long_desc = "Reserved"}, 85 | {20, 1, .desc = "CTO", .long_desc = "Charging timeout"}, 86 | {21, 1, .desc = "RSVD21", .long_desc = "Reserved"}, 87 | {22, 1, .desc = "OC", .long_desc = "Overcharge"}, 88 | {23, 1, .desc = "CHGC", .long_desc = "Charging Current higher than requested"}, 89 | {24, 1, .desc = "CHGV", .long_desc = "Charging Voltage higher than requested"}, 90 | {25, 1, .desc = "RSVD25", .long_desc = "Reserved"}, 91 | {26, 1, .desc = "RSVD26", .long_desc = "Reserved"}, 92 | {27, 1, .desc = "RSVD27", .long_desc = "Reserved"}, 93 | {28, 1, .desc = "RSVD28", .long_desc = "Reserved"}, 94 | {29, 1, .desc = "RSVD29", .long_desc = "Reserved"}, 95 | {30, 1, .desc = "RSVD30", .long_desc = "Reserved"}, 96 | {31, 1, .desc = "RSVD31", .long_desc = "Reserved"}}; 97 | 98 | // ────────────────────────────────────────────────────────────────────────────── 99 | // PFAlert (0x52) – Permanent Failure flags (latched alert) 100 | // ────────────────────────────────────────────────────────────────────────────── 101 | static const bq_bit_desc_t BITS_PF_ALERT[] = { 102 | {0, 1, .desc = "CUV", .long_desc = "CUV Latched"}, 103 | {1, 1, .desc = "COV", .long_desc = "COV Latched"}, 104 | {2, 1, .desc = "CUDEP", .long_desc = "Copper deposition"}, 105 | {3, 1, .desc = "RSVD3", .long_desc = "Reserved"}, 106 | {4, 1, .desc = "OTCE", .long_desc = "Overtemperature"}, 107 | {5, 1, .desc = "RSVD5", .long_desc = "Reserved"}, 108 | {6, 1, .desc = "OTF", .long_desc = "Overtemperature FET"}, 109 | {7, 1, .desc = "QIM", .long_desc = "QMAX Imbalance"}, 110 | {8, 1, .desc = "CB", .long_desc = "Cell balancing"}, 111 | {9, 1, .desc = "IMP", .long_desc = "Cell impedance"}, 112 | {10, 1, .desc = "CD", .long_desc = "Capacity Deterioration"}, 113 | {11, 1, .desc = "VIMR", .long_desc = "Voltage imbalance at Rest"}, 114 | {12, 1, .desc = "VIMA", .long_desc = "Voltage imbalance at Rest"}, 115 | {13, 1, .desc = "RSVD13", .long_desc = "Reserved"}, 116 | {14, 1, .desc = "RSVD14", .long_desc = "Reserved"}, 117 | {15, 1, .desc = "RSVD15", .long_desc = "Reserved"}, 118 | {16, 1, .desc = "CFETF", .long_desc = "Charge FET"}, 119 | {17, 1, .desc = "DFET", .long_desc = "Discharge FET"}, 120 | {18, 1, .desc = "THERM", .long_desc = "Thermistor"}, 121 | {19, 1, .desc = "FUSE", .long_desc = "Fuse"}, 122 | {20, 1, .desc = "AFER", .long_desc = "AFE Register"}, 123 | {21, 1, .desc = "AFEC", .long_desc = "AFE Communication"}, 124 | {22, 1, .desc = "2LVL", .long_desc = "FUSE input indicating fuse trigger by external 2nd level protection"}, 125 | {23, 1, .desc = "RSVD23", .long_desc = "Reserved"}, 126 | {24, 1, .desc = "RSVD24", .long_desc = "Reserved"}, 127 | {25, 1, .desc = "OCECO", .long_desc = "Open VCx"}, 128 | {26, 1, .desc = "RSVD26", .long_desc = "Reserved"}, 129 | {27, 1, .desc = "RSVD27", .long_desc = "Reserved"}, 130 | {28, 1, .desc = "RSVD28", .long_desc = "Reserved"}, 131 | {29, 1, .desc = "RSVD29", .long_desc = "Reserved"}, 132 | {30, 1, .desc = "RSVD30", .long_desc = "Reserved"}, 133 | {31, 1, .desc = "RSVD31", .long_desc = "Reserved"}}; 134 | 135 | // ────────────────────────────────────────────────────────────────────────────── 136 | // PFStatus (0x53) 137 | // ────────────────────────────────────────────────────────────────────────────── 138 | static const bq_bit_desc_t BITS_PF_STATUS[] = { 139 | {0, 1, .desc = "CUV", .long_desc = "CUV Latched"}, 140 | {1, 1, .desc = "COV", .long_desc = "COV Latched"}, 141 | {2, 1, .desc = "CUDEP", .long_desc = "Copper deposition"}, 142 | {3, 1, .desc = "RSVD3", .long_desc = "Reserved"}, 143 | {4, 1, .desc = "OTCE", .long_desc = "Overtemperature"}, 144 | {5, 1, .desc = "RSVD5", .long_desc = "Reserved"}, 145 | {6, 1, .desc = "OTF", .long_desc = "Overtemperature FET"}, 146 | {7, 1, .desc = "QIM", .long_desc = "QMAX Imbalance"}, 147 | {8, 1, .desc = "CB", .long_desc = "Cell balancing"}, 148 | {9, 1, .desc = "IMP", .long_desc = "Cell impedance"}, 149 | {10, 1, .desc = "CD", .long_desc = "Capacity Deterioration"}, 150 | {11, 1, .desc = "VIMR", .long_desc = "Voltage imbalance at Rest"}, 151 | {12, 1, .desc = "VIMA", .long_desc = "Voltage imbalance at Rest"}, 152 | {13, 1, .desc = "RSVD13", .long_desc = "Reserved"}, 153 | {14, 1, .desc = "RSVD14", .long_desc = "Reserved"}, 154 | {15, 1, .desc = "RSVD15", .long_desc = "Reserved"}, 155 | {16, 1, .desc = "CFETF", .long_desc = "Charge FET"}, 156 | {17, 1, .desc = "DFET", .long_desc = "Discharge FET"}, 157 | {18, 1, .desc = "THERM", .long_desc = "Thermistor"}, 158 | {19, 1, .desc = "FUSE", .long_desc = "Fuse"}, 159 | {20, 1, .desc = "AFER", .long_desc = "AFE Register"}, 160 | {21, 1, .desc = "AFEC", .long_desc = "AFE Communication"}, 161 | {22, 1, .desc = "2LVL", .long_desc = "FUSE input indicating fuse trigger by external 2nd level protection"}, 162 | {23, 1, .desc = "RSVD23", .long_desc = "Reserved"}, 163 | {24, 1, .desc = "RSVD24", .long_desc = "Reserved"}, 164 | {25, 1, .desc = "OCECO", .long_desc = "Open VCx"}, 165 | {26, 1, .desc = "RSVD26", .long_desc = "Reserved"}, 166 | {27, 1, .desc = "RSVD27", .long_desc = "Reserved"}, 167 | {28, 1, .desc = "RSVD28", .long_desc = "Reserved"}, 168 | {29, 1, .desc = "RSVD29", .long_desc = "Reserved"}, 169 | {30, 1, .desc = "RSVD30", .long_desc = "Reserved"}, 170 | {31, 1, .desc = "RSVD31", .long_desc = "Reserved"}}; 171 | 172 | // ────────────────────────────────────────────────────────────────────────────── 173 | // OperationStatus (0x54) 174 | // ────────────────────────────────────────────────────────────────────────────── 175 | static const bq_bit_desc_t BITS_OPERATION_STATUS[] = { 176 | {0, 1, .desc = "PRES", .long_desc = "PRES input (active = low detected)"}, 177 | {1, 1, .desc = "DSG", .long_desc = "Discharge FET Enabled"}, 178 | {2, 1, .desc = "CHG", .long_desc = "Charge FET Enabled"}, 179 | {3, 1, .desc = "PCHG", .long_desc = "PCHG FET Enabled"}, 180 | {4, 1, .desc = "GPOD", .long_desc = "GPOD FET Enabled"}, 181 | {5, 1, .desc = "FUSE", .long_desc = "Fuse Input High"}, 182 | {6, 1, .desc = "CB", .long_desc = "Cell Balancing Active"}, 183 | {7, 1, .desc = "RSVD7", .long_desc = "Reserved"}, 184 | {8, 2, .desc = "SEC0/1", .long_desc = "Security Mode (0: Reserved, 1: Full access, 2: Unsealed, 3: Sealed)"}, 185 | {10, 1, .desc = "CAL", .long_desc = "Cal mode active"}, 186 | {11, 1, .desc = "SS", .long_desc = "SafetyStatus active"}, 187 | {12, 1, .desc = "PF", .long_desc = "Permanent Failure active"}, 188 | {13, 1, .desc = "XDSG", .long_desc = "Discharging Disabled"}, 189 | {14, 1, .desc = "XCHG", .long_desc = "Charging Disabled"}, 190 | {15, 1, .desc = "SLEEP", .long_desc = "Sleep condition met"}, 191 | {16, 1, .desc = "SDM", .long_desc = "Shutdown via MfgAccess"}, 192 | {17, 1, .desc = "RSVD17", .long_desc = "Reserved"}, 193 | {18, 1, .desc = "AUTH", .long_desc = "Authentication ongoing"}, 194 | {19, 1, .desc = "AWD", .long_desc = "AFE Watchdog failure"}, 195 | {20, 1, .desc = "FVS", .long_desc = "Fast Voltage Sampling"}, 196 | {21, 1, .desc = "CALO", .long_desc = "Raw ADC/CC offset active"}, 197 | {22, 1, .desc = "SDV", .long_desc = "Shutdown via voltage"}, 198 | {23, 1, .desc = "SLEEPM", .long_desc = "Sleep via MfgAccess"}, 199 | {24, 1, .desc = "INIT", .long_desc = "Init after full reset"}, 200 | {25, 1, .desc = "SMBLCAL", .long_desc = "CC auto offset cal"}, 201 | {26, 1, .desc = "SLEEPQMAX", .long_desc = "QMAX update in sleep"}, 202 | {27, 1, .desc = "SLEEPC", .long_desc = "Current check in sleep"}, 203 | {28, 1, .desc = "XLSBS", .long_desc = "Fast SBS mode"}, 204 | {29, 1, .desc = "RSVD29", .long_desc = "Reserved"}, 205 | {30, 1, .desc = "RSVD30", .long_desc = "Reserved"}, 206 | {31, 1, .desc = "RSVD31", .long_desc = "Reserved"}}; 207 | 208 | // ────────────────────────────────────────────────────────────────────────────── 209 | // ChargingStatus (0x55) 210 | // ────────────────────────────────────────────────────────────────────────────── 211 | static const bq_bit_desc_t BITS_CHARGING_STATUS[] = { 212 | {0, 1, .desc = "UT", .long_desc = "Under Temp"}, 213 | {1, 1, .desc = "LT", .long_desc = "Low Temp"}, 214 | {2, 1, .desc = "STL", .long_desc = "Std Low Temp"}, 215 | {3, 1, .desc = "RT", .long_desc = "Recommended Temp"}, 216 | {4, 1, .desc = "ST", .long_desc = "Std High Temp"}, 217 | {5, 1, .desc = "HT", .long_desc = "High Temp"}, 218 | {6, 1, .desc = "OT", .long_desc = "Over Temp"}, 219 | {7, 1, .desc = "PV", .long_desc = "Precharge Voltage"}, 220 | {8, 1, .desc = "LV", .long_desc = "Low Voltage Range"}, 221 | {9, 1, .desc = "MV", .long_desc = "Mid Voltage Range"}, 222 | {10, 1, .desc = "HV", .long_desc = "High Voltage Range"}, 223 | {11, 1, .desc = "IN", .long_desc = "Charge Inhibit"}, 224 | {12, 1, .desc = "SU", .long_desc = "Charge Suspend"}, 225 | {13, 1, .desc = "CCR", .long_desc = "Charging Current Rate"}, 226 | {14, 1, .desc = "CVR", .long_desc = "Charging Voltage Rate"}, 227 | {15, 1, .desc = "CCC", .long_desc = "Charging Current Comp"}}; 228 | 229 | // ────────────────────────────────────────────────────────────────────────────── 230 | // GaugingStatus (0x56) - algorithm + QMAX + mode flags 231 | // ────────────────────────────────────────────────────────────────────────────── 232 | static const bq_bit_desc_t BITS_GAUGING_STATUS[] = { 233 | {0, 1, .desc = "RESTDOD0", .long_desc = "OCV/QMAX Updated"}, 234 | {1, 1, .desc = "DSG", .long_desc = "Discharging Detected"}, 235 | {2, 1, .desc = "RU", .long_desc = "Resistance Update Enabled"}, 236 | {3, 1, .desc = "VOK", .long_desc = "Voltage OK for QMAX"}, 237 | {4, 1, .desc = "QEN", .long_desc = "QMAX Updates Enabled"}, 238 | {5, 1, .desc = "FD", .long_desc = "Fully Discharged detected"}, 239 | {6, 1, .desc = "FC", .long_desc = "Fully Charged detected"}, 240 | {7, 1, .desc = "NSFM", .long_desc = "Negative Scale Factor Mode"}, 241 | {8, 1, .desc = "VDQ", .long_desc = "Qualified Discharge"}, 242 | {9, 1, .desc = "QMAX", .long_desc = "QMAX Updated"}, 243 | {10, 1, .desc = "RX", .long_desc = "Resistance Updated"}, 244 | {11, 1, .desc = "LDMD", .long_desc = "Load Mode (0 = CC, 1 = CP)"}, 245 | {12, 1, .desc = "OCVFR", .long_desc = "OCV in Flat Region"}, 246 | {13, 1, .desc = "TDA", .long_desc = "Terminate Discharge Alarm"}, 247 | {14, 1, .desc = "TCA", .long_desc = "Terminate Charge Alarm"}, 248 | {15, 1, .desc = "LPF", .long_desc = "LiPh Relax (0x400)"}}; 249 | 250 | // ────────────────────────────────────────────────────────────────────────────── 251 | // ManufacturingStatus (0x57) 252 | // ────────────────────────────────────────────────────────────────────────────── 253 | static const bq_bit_desc_t BITS_MANUFACTURING_STATUS[] = { 254 | {0, 1, .desc = "PCHG", .long_desc = "Precharge FET"}, 255 | {1, 1, .desc = "CHG", .long_desc = "Charge FET"}, 256 | {2, 1, .desc = "DSG", .long_desc = "Discharge FET"}, 257 | {3, 1, .desc = "GAUGE", .long_desc = "Gauging"}, 258 | {4, 1, .desc = "FET", .long_desc = "FET Action"}, 259 | {5, 1, .desc = "LF", .long_desc = "Lifetime Data"}, 260 | {6, 1, .desc = "PF", .long_desc = "Permanent Fail"}, 261 | {7, 1, .desc = "BBR", .long_desc = "Black Box Recorder"}, 262 | {8, 1, .desc = "FUSE", .long_desc = "Fuse Action"}, 263 | {9, 1, .desc = "RSVD9", .long_desc = "Reserved"}, 264 | {10, 1, .desc = "RSVD10", .long_desc = "Reserved"}, 265 | {11, 1, .desc = "RSVD11", .long_desc = "Reserved"}, 266 | {12, 1, .desc = "RSVD12", .long_desc = "Reserved"}, 267 | {13, 1, .desc = "RSVD13", .long_desc = "Reserved"}, 268 | {14, 1, .desc = "RSVD14", .long_desc = "Reserved"}, 269 | {15, 1, .desc = "CAL", .long_desc = "Cal Mode ADC/CC"}}; 270 | 271 | // ────────────────────────────────────────────────────────────────────────────── 272 | // BatteryStatus (0x16) - alarms, state flags, and error code 273 | // ────────────────────────────────────────────────────────────────────────────── 274 | static const bq_bit_desc_t BITS_BATTERY_STATUS[] = { 275 | {0, 4, .desc = "ERR", .long_desc = "Error Code (0: OK, 1: Busy, 2: Reserved command, 3: Unsupported command, 4: Access denied, 5: Over/underflow, 6: Bad size, 7: Unknown)"}, 276 | {4, 1, .desc = "FD", .long_desc = "Fully Discharged"}, 277 | {5, 1, .desc = "FC", .long_desc = "Fully Charged"}, 278 | {6, 1, .desc = "DSG", .long_desc = "Discharging"}, 279 | {7, 1, .desc = "INIT", .long_desc = "Initialization Active"}, 280 | {8, 1, .desc = "RTA", .long_desc = "Remaining Time Alarm"}, 281 | {9, 1, .desc = "RCA", .long_desc = "Remaining Capacity Alarm"}, 282 | {10, 1, .desc = "RSVD10", .long_desc = "Reserved"}, 283 | {11, 1, .desc = "TDA", .long_desc = "Terminate Discharge Alarm"}, 284 | {12, 1, .desc = "OTA", .long_desc = "Overtemperature Alarm"}, 285 | {13, 1, .desc = "RSVD13", .long_desc = "Reserved"}, 286 | {14, 1, .desc = "TCA", .long_desc = "Terminate Charge Alarm"}, 287 | {15, 1, .desc = "OCA", .long_desc = "Overcharged Alarm"}}; 288 | 289 | static const bq_entry bq_commands[] = { 290 | {BQ40Z555_CMD_SERIAL_NUMBER, "SerialNumber", "", 0.0f, 1.0f, .type = BQ40Z555_TYPE_WORD_INTEGER}, 291 | {BQ40Z555_CMD_MANUFACTURER_NAME, "ManufacturerName", "", 0.0f, 1.0f, .type = BQ40Z555_TYPE_BLOCK_ASCII}, 292 | {BQ40Z555_CMD_DEVICE_NAME, "DeviceName", "", 0.0f, 1.0f, .type = BQ40Z555_TYPE_BLOCK_ASCII}, 293 | {BQ40Z555_CMD_DEVICE_CHEMISTRY, "DeviceChemistry", "", 0.0f, 1.0f, .type = BQ40Z555_TYPE_BLOCK_ASCII}, 294 | {BQ40Z555_CMD_MANUFACTURER_DATA, "ManufacturerData", "", 0.0f, 1.0f, .type = BQ40Z555_TYPE_BLOCK_ASCII}, 295 | {BQ40Z555_CMD_MANUFACTURER_DATE, "ManufacturerDate", "", 0.0f, 1.0f, .type = BQ40Z555_TYPE_WORD_HEX}, 296 | {BQ40Z555_CMD_VOLTAGE, "Voltage", "V", 0.0f, 0.001f, .type = BQ40Z555_TYPE_WORD_FLOAT}, // mV → V 297 | {BQ40Z555_CMD_TEMPERATURE, "Temperature", "°C", -273.15f, 0.1f, .type = BQ40Z555_TYPE_WORD_FLOAT}, // 0.1 K → °C 298 | {BQ40Z555_CMD_CURRENT, "Current", "A", 0.0f, 0.001f, .type = BQ40Z555_TYPE_WORD_FLOAT}, 299 | {BQ40Z555_CMD_CELL_VOLTAGE1, "Cell1Voltage", "V", 0.0f, 0.001f, .type = BQ40Z555_TYPE_WORD_FLOAT}, 300 | {BQ40Z555_CMD_CELL_VOLTAGE2, "Cell2Voltage", "V", 0.0f, 0.001f, .type = BQ40Z555_TYPE_WORD_FLOAT}, 301 | {BQ40Z555_CMD_CELL_VOLTAGE3, "Cell3Voltage", "V", 0.0f, 0.001f, .type = BQ40Z555_TYPE_WORD_FLOAT}, 302 | {BQ40Z555_CMD_CELL_VOLTAGE4, "Cell4Voltage", "V", 0.0f, 0.001f, .type = BQ40Z555_TYPE_WORD_FLOAT}, 303 | {BQ40Z555_CMD_CYCLE_COUNT, "CycleCount", "cycles", 0.0f, 1.0f, .type = BQ40Z555_TYPE_WORD_INTEGER}, 304 | {BQ40Z555_CMD_CHARGING_VOLTAGE, "ChargingVoltage", "V", 0.0f, 0.001f, .type = BQ40Z555_TYPE_WORD_FLOAT}, 305 | {BQ40Z555_CMD_DESIGN_VOLTAGE, "DesignVoltage", "V", 0.0f, 0.001f, .type = BQ40Z555_TYPE_WORD_FLOAT}, 306 | {BQ40Z555_CMD_MIN_SYS_V, "MinSystemVoltage", "V", 0.0f, 0.001f, .type = BQ40Z555_TYPE_WORD_FLOAT}, 307 | {BQ40Z555_CMD_AVERAGE_CURRENT, "AverageCurrent", "A", 0.0f, 0.001f, .type = BQ40Z555_TYPE_WORD_FLOAT}, 308 | {BQ40Z555_CMD_CHARGING_CURRENT, "ChargingCurrent", "A", 0.0f, 0.001f, .type = BQ40Z555_TYPE_WORD_FLOAT}, 309 | {BQ40Z555_CMD_TURBO_CURRENT, "TurboCurrent", "A", 0.0f, 0.001f, .type = BQ40Z555_TYPE_WORD_FLOAT}, 310 | {BQ40Z555_CMD_RELATIVE_STATE_OF_CHARGE, "RelativeSoC", "%", 0.0f, 1.0f, .type = BQ40Z555_TYPE_WORD_INTEGER}, 311 | {BQ40Z555_CMD_ABSOLUTE_STATE_OF_CHARGE, "AbsoluteSoC", "%", 0.0f, 1.0f, .type = BQ40Z555_TYPE_WORD_INTEGER}, 312 | {BQ40Z555_CMD_STATE_OF_HEALTH, "State of Health", "%", 0.0f, 1.0f, .type = BQ40Z555_TYPE_WORD_INTEGER}, 313 | {BQ40Z555_CMD_REMAINING_CAPACITY, "RemainingCapacity", "mAh", 0.0f, 1.0f, .type = BQ40Z555_TYPE_WORD_INTEGER}, 314 | {BQ40Z555_CMD_FULL_CHARGE_CAPACITY, "FullChargeCapacity", "mAh", 0.0f, 1.0f, .type = BQ40Z555_TYPE_WORD_INTEGER}, 315 | {BQ40Z555_CMD_DESIGN_CAPACITY, "DesignCapacity", "mAh", 0.0f, 1.0f, .type = BQ40Z555_TYPE_WORD_INTEGER}, 316 | {BQ40Z555_CMD_RUN_TIME_TO_EMPTY, "RunTimeToEmpty", "min", 0.0f, 1.0f, .type = BQ40Z555_TYPE_WORD_INTEGER}, 317 | {BQ40Z555_CMD_AVERAGE_TIME_TO_EMPTY, "AvgTimeToEmpty", "min", 0.0f, 1.0f, .type = BQ40Z555_TYPE_WORD_INTEGER}, 318 | {BQ40Z555_CMD_AVERAGE_TIME_TO_FULL, "AvgTimeToFull", "min", 0.0f, 1.0f, .type = BQ40Z555_TYPE_WORD_INTEGER}, 319 | {BQ40Z555_CMD_BATTERY_STATUS, "BatteryStatus", "", .type = BQ40Z555_TYPE_BLOCK_BITS, .bits = BITS_BATTERY_STATUS, .bits_count = COUNT(BITS_BATTERY_STATUS)}, 320 | {BQ40Z555_CMD_SAFETY_ALERT, "SafetyAlert", "", .type = BQ40Z555_TYPE_BLOCK_BITS, .bits = SAFETY_ALERT_BITS, .bits_count = COUNT(SAFETY_ALERT_BITS)}, 321 | {BQ40Z555_CMD_SAFETY_STATUS, "SafetyStatus", .type = BQ40Z555_TYPE_BLOCK_BITS, .bits = BITS_SAFETY_STATUS, .bits_count = COUNT(BITS_SAFETY_STATUS)}, 322 | {BQ40Z555_CMD_PF_ALERT, "PFAlert", .type = BQ40Z555_TYPE_BLOCK_BITS, .bits = BITS_PF_ALERT, .bits_count = COUNT(BITS_PF_ALERT)}, 323 | {BQ40Z555_CMD_PF_STATUS, "PFStatus", .type = BQ40Z555_TYPE_BLOCK_BITS, .bits = BITS_PF_STATUS, .bits_count = COUNT(BITS_PF_STATUS)}, 324 | {BQ40Z555_CMD_OPERATION_STATUS, "OperationStatus", .type = BQ40Z555_TYPE_BLOCK_BITS, .bits = BITS_OPERATION_STATUS, .bits_count = COUNT(BITS_OPERATION_STATUS)}, 325 | {BQ40Z555_CMD_CHARGING_STATUS, "ChargingStatus", .type = BQ40Z555_TYPE_BLOCK_BITS, .bits = BITS_CHARGING_STATUS, .bits_count = COUNT(BITS_CHARGING_STATUS)}, 326 | {BQ40Z555_CMD_GAUGING_STATUS, "GaugingStatus", .type = BQ40Z555_TYPE_BLOCK_BITS, .bits = BITS_GAUGING_STATUS, .bits_count = COUNT(BITS_GAUGING_STATUS)}, 327 | {BQ40Z555_CMD_MANUFACTURING_STATUS, "ManufacturingStatus", .type = BQ40Z555_TYPE_BLOCK_BITS, .bits = BITS_MANUFACTURING_STATUS, .bits_count = COUNT(BITS_MANUFACTURING_STATUS)}, 328 | 329 | }; 330 | 331 | // ────────────────────────────────────────────────────────────────────────────── 332 | // Bit-extraction helper 333 | // ────────────────────────────────────────────────────────────────────────────── 334 | /** 335 | * Extract an arbitrary bit-field spanning one or more bytes. 336 | * 337 | * @param data Buffer with little-endian byte order (LSB = byte[0]). 338 | * @param len Length of buffer. 339 | * @param lsb_index Index of least-significant bit to extract (0 = bit0 of byte0). 340 | * @param width Width of field in bits (1-32 supported). 341 | * @return Extracted value right-aligned (bit0 = LSB of return). 342 | */ 343 | static uint32_t bq_extract_bits(const uint8_t *data, size_t len, uint16_t lsb_index, uint8_t width) 344 | { 345 | uint32_t value = 0; 346 | for (uint8_t i = 0; i < width; ++i) 347 | { 348 | uint16_t bit_idx = lsb_index + i; 349 | size_t byte_idx = bit_idx >> 3; // /8 350 | if (byte_idx >= len) 351 | break; // out-of-range safety 352 | uint8_t bit_in_byte = bit_idx & 0x07; // %8 353 | if (data[byte_idx] & (1u << bit_in_byte)) 354 | { 355 | value |= (1u << i); // place into result 356 | } 357 | } 358 | return value; 359 | } 360 | 361 | // ────────────────────────────────────────────────────────────────────────────── 362 | // Generic bit-field printer (buffer-aware, arbitrary size) 363 | // ────────────────────────────────────────────────────────────────────────────── 364 | static int bq_print_bits_from_buffer(const bq_entry *e, 365 | const uint8_t *data, size_t data_len) 366 | { 367 | if (!e || !data || data_len == 0 || e->type != BQ40Z555_TYPE_BLOCK_BITS) 368 | { 369 | return ESP_ERR_INVALID_ARG; 370 | } 371 | 372 | printf("%s:\n", e->name); 373 | for (size_t i = 0; i < e->bits_count; ++i) 374 | { 375 | const bq_bit_desc_t *d = &e->bits[i]; 376 | uint32_t field = bq_extract_bits(data, data_len, d->bit, d->width); 377 | if (d->width == 1) 378 | { 379 | printf(" %s%10s%s [%s] \033[90m(%s)\033[0m\n", 380 | field ? "\033[32m" : "", 381 | d->desc, 382 | field ? "\033[0m" : "", 383 | field ? "\033[32mX\033[0m" : " ", 384 | d->long_desc ? d->long_desc : ""); 385 | } 386 | else 387 | { 388 | printf(" %10s [\033[32m%" PRIu32 "\033[0m] \033[90m(%s)\033[0m\n", 389 | d->desc, 390 | field, 391 | d->long_desc ? d->long_desc : ""); 392 | } 393 | } 394 | return 0; 395 | } 396 | 397 | /** 398 | * @brief Fetch an SBS WORD and print it. 399 | * 400 | * Prints a single line: ": ". 401 | * Returns 0 on success or the I²C error code. 402 | */ 403 | int bq_generic_dump(const bq_entry *entry) 404 | { 405 | if (!entry) 406 | return ESP_ERR_INVALID_ARG; 407 | 408 | uint8_t cmd = entry->reg; 409 | 410 | switch (entry->type) 411 | { 412 | case BQ40Z555_TYPE_BLOCK_BITS: 413 | { 414 | uint8_t resp_len[1] = {0}; 415 | int err = i2c_write_read(BQ40Z555_I2C_ADDR, &cmd, sizeof(cmd), resp_len, sizeof(resp_len)); 416 | if (err) 417 | { 418 | ESP_LOGE(TAG, "%s: i2c_write_read failed (err=%d)", entry->name, err); 419 | return err; 420 | } 421 | 422 | uint8_t len = resp_len[0]; 423 | uint8_t resp_data[256]; 424 | err = i2c_write_read(BQ40Z555_I2C_ADDR, &cmd, sizeof(cmd), resp_data, 1 + len); 425 | if (err) 426 | { 427 | ESP_LOGE(TAG, "%s: i2c_write_read failed (err=%d)", entry->name, err); 428 | return err; 429 | } 430 | 431 | bq_print_bits_from_buffer(entry, &resp_data[1], len); 432 | 433 | break; 434 | } 435 | case BQ40Z555_TYPE_BLOCK_ASCII: 436 | case BQ40Z555_TYPE_BLOCK_HEX: 437 | { 438 | uint8_t resp_len[1] = {0}; 439 | int err = i2c_write_read(BQ40Z555_I2C_ADDR, &cmd, sizeof(cmd), resp_len, sizeof(resp_len)); 440 | if (err) 441 | { 442 | ESP_LOGE(TAG, "%s: i2c_write_read failed (err=%d)", entry->name, err); 443 | return err; 444 | } 445 | 446 | uint8_t len = resp_len[0]; 447 | uint8_t resp_data[256]; 448 | err = i2c_write_read(BQ40Z555_I2C_ADDR, &cmd, sizeof(cmd), resp_data, 1 + len); 449 | if (err) 450 | { 451 | ESP_LOGE(TAG, "%s: i2c_write_read failed (err=%d)", entry->name, err); 452 | return err; 453 | } 454 | 455 | for (int pos = 0; pos < len; pos++) 456 | { 457 | if (resp_data[1 + pos] < 0x20 || resp_data[1 + pos] >= 0x80) 458 | { 459 | resp_data[1 + pos] = '.'; 460 | } 461 | } 462 | switch (entry->type) 463 | { 464 | case BQ40Z555_TYPE_BLOCK_ASCII: 465 | { 466 | printf("%-32s: '%.*s' %s\n", entry->name, len, &resp_data[1], entry->unit); 467 | break; 468 | } 469 | case BQ40Z555_TYPE_BLOCK_HEX: 470 | { 471 | printf("%-32s: '", entry->name); 472 | for (int pos = 0; pos < len; pos++) 473 | { 474 | char tmp_buf[4]; 475 | sprintf(tmp_buf, "%02X ", resp_data[1 + pos]); 476 | printf("%s", tmp_buf); 477 | } 478 | printf("' %s\n", entry->unit); 479 | break; 480 | } 481 | default: 482 | break; 483 | } 484 | 485 | break; 486 | } 487 | case BQ40Z555_TYPE_WORD_HEX: 488 | case BQ40Z555_TYPE_WORD_FLOAT: 489 | case BQ40Z555_TYPE_WORD_INTEGER: 490 | { 491 | uint8_t resp[2] = {0}; 492 | int err = i2c_write_read(BQ40Z555_I2C_ADDR, &cmd, sizeof(cmd), resp, sizeof(resp)); 493 | if (err) 494 | { 495 | ESP_LOGE(TAG, "%s: i2c_write_read failed (err=%d)", entry->name, err); 496 | return err; 497 | } 498 | 499 | uint16_t raw = (uint16_t)resp[0] | ((uint16_t)resp[1] << 8); 500 | 501 | switch (entry->type) 502 | { 503 | case BQ40Z555_TYPE_WORD_HEX: 504 | { 505 | printf("%-32s: 0x%08X %s\n", entry->name, raw, entry->unit); 506 | break; 507 | } 508 | case BQ40Z555_TYPE_WORD_FLOAT: 509 | { 510 | float val = raw * entry->scaling + entry->offset; 511 | printf("%-32s: %02.3f %s\n", entry->name, val, entry->unit); 512 | break; 513 | } 514 | case BQ40Z555_TYPE_WORD_INTEGER: 515 | { 516 | float val = raw * entry->scaling + entry->offset; 517 | printf("%-32s: %d %s\n", entry->name, (int)val, entry->unit); 518 | break; 519 | } 520 | default: 521 | break; 522 | } 523 | break; 524 | } 525 | default: 526 | break; 527 | } 528 | return 0; 529 | } 530 | 531 | /** 532 | * @brief Decode and print Lifetime Data Block n (n = 1‥3). 533 | * 534 | * Mapping – **only fields that are 16-bit voltages or currents** are interpreted. 535 | * Everything else is shown as raw hex words to keep the output compact yet 536 | * useful. 537 | * 538 | * Block 1 (0x60) 539 | * ──────────────── 540 | * Word Idx | Description | Unit 541 | * ---------|-----------------------------|------ 542 | * 0-3 | Max Cell Voltage 1-4 | mV 543 | * 4-7 | Min Cell Voltage 1-4 | mV 544 | * 8 | Max Delta Cell Voltage | mV 545 | * 9 | Max Charge Current | mA 546 | * 10 | Max Discharge Current | mA 547 | * 11 | Max Avg Discharge Current | mA 548 | * 549 | * Block 2 (0x61) - no voltage/current *word* fields; printed raw. 550 | * Block 3 (0x62) - time counters only; printed raw. 551 | */ 552 | int bq_print_lifetime_block_decoded(int n) 553 | { 554 | if (n < 1 || n > 3) 555 | { 556 | ESP_LOGE(TAG, "LifetimeData block index %d out of range (1..3)", n); 557 | return ESP_ERR_INVALID_ARG; 558 | } 559 | 560 | uint8_t cmd = (uint8_t)(BQ40Z555_CMD_LIFETIME_DATA1 + (n - 1)); 561 | 562 | uint8_t resp_len[1] = {0}; 563 | int err = i2c_write_read(BQ40Z555_I2C_ADDR, &cmd, sizeof(cmd), resp_len, sizeof(resp_len)); 564 | if (err) 565 | { 566 | ESP_LOGE(TAG, "LifetimeData%d: i2c I/O err %d", n, err); 567 | return err; 568 | } 569 | 570 | uint8_t len = resp_len[0]; 571 | 572 | uint8_t resp[256] = {0}; 573 | 574 | err = i2c_write_read(BQ40Z555_I2C_ADDR, &cmd, sizeof(cmd), resp, 1 + len); 575 | if (err) 576 | { 577 | ESP_LOGE(TAG, "LifetimeData%d: i2c I/O err %d", n, err); 578 | return err; 579 | } 580 | 581 | if (n == 1) 582 | { 583 | int offset = 1; 584 | puts("LifetimeData1 decoded (voltages in V, currents in A):"); 585 | for (int i = 0; i < 4; ++i) 586 | { 587 | float v = le16(&resp[offset]) / 1000.0f; // mV → V 588 | printf(" Max Cell Voltage %d: %.3f V\n", i + 1, v); 589 | offset += 2; 590 | } 591 | for (int i = 0; i < 4; ++i) 592 | { 593 | float v = le16(&resp[offset]) / 1000.0f; 594 | printf(" Min Cell Voltage %d: %.3f V\n", i + 1, v); 595 | offset += 2; 596 | } 597 | printf(" Max Δ Cell Voltage : %.3f V\n", le16(&resp[offset]) / 1000.0f); 598 | offset += 2; 599 | printf(" Max Charge Current : %.3f A\n", le16(&resp[offset]) / 1000.0f); 600 | offset += 2; 601 | printf(" Max Disch Current : %.3f A\n", le16(&resp[offset]) / 1000.0f); 602 | offset += 2; 603 | printf(" Max Avg Current : %.3f A\n", le16(&resp[offset]) / 1000.0f); 604 | offset += 2; 605 | printf(" Max Avg Disch Power: %d W\n", resp[offset]); 606 | offset += 1; 607 | return 0; 608 | } 609 | 610 | // Blocks 2 & 3: show raw words for reference 611 | printf("LifetimeData%d raw words:\n", n); 612 | for (int i = 0; i < len; ++i) 613 | { 614 | printf(" 0x%02x: 0x%02X\n", i, resp[1 + i]); 615 | } 616 | return 0; 617 | } 618 | 619 | // ────────────────────────────────────────────────────────────────────────────── 620 | // Voltage command implementation 621 | // ────────────────────────────────────────────────────────────────────────────── 622 | 623 | /** 624 | * @brief Read pack voltage from the BQ40Z555. 625 | * 626 | * The SBS Voltage() command returns a *word* (little-endian 16 bit) that 627 | * represents the pack voltage in millivolts. We issue a single-byte write of 628 | * `0x09`, then read back two bytes and assemble them into a host-endian `uint16_t`. 629 | */ 630 | static int cmd_bq_dump(int argc, char **argv) 631 | { 632 | (void)argc; 633 | (void)argv; // Unused 634 | 635 | for (int pos = 0; pos < COUNT(bq_commands); pos++) 636 | { 637 | bq_generic_dump(&bq_commands[pos]); 638 | } 639 | 640 | return 0; 641 | } 642 | static int cmd_bq_lifetime(int argc, char **argv) 643 | { 644 | int block = 1; // default 645 | if (argc == 2) 646 | { 647 | block = atoi(argv[1]); 648 | } 649 | return bq_print_lifetime_block_decoded(block); 650 | } 651 | // ────────────────────────────────────────────────────────────────────────────── 652 | // Command registration helper 653 | // ────────────────────────────────────────────────────────────────────────────── 654 | 655 | void register_bq_commands(void) 656 | { 657 | const esp_console_cmd_t dump_cmd = { 658 | .command = "bq_show", 659 | .help = "Read all known fields", 660 | .hint = NULL, 661 | .func = &cmd_bq_dump, 662 | .argtable = NULL, 663 | }; 664 | 665 | ESP_ERROR_CHECK(esp_console_cmd_register(&dump_cmd)); 666 | const esp_console_cmd_t lifetime_cmd = { 667 | .command = "bq_lifetime", 668 | .help = "Show Lifetime Data block 1-3 (default 1)", 669 | .hint = NULL, 670 | .func = &cmd_bq_lifetime, 671 | .argtable = NULL, // simple argv parsing 672 | }; 673 | ESP_ERROR_CHECK(esp_console_cmd_register(&lifetime_cmd)); 674 | } 675 | 676 | void bq_start(void) 677 | { 678 | register_bq_commands(); 679 | } 680 | --------------------------------------------------------------------------------