From eb12a00efefd86c06107a89da280b985d16aa3c8 Mon Sep 17 00:00:00 2001 From: Ning Yu Xiang Date: Tue, 28 Apr 2026 15:43:18 +0800 Subject: [PATCH] feat: add support esp vocat --- .gitlab/ci/build.yml | 3 + .gitlab/ci/build_apps.py | 1 + .../esp_vocat_board_v1_2/board_devices.yaml | 176 +++++ .../esp_vocat_board_v1_2/board_info.yaml | 4 + .../board_peripherals.yaml | 94 +++ .../sdkconfig.defaults.board | 11 + .../esp_vocat_board_v1_2/setup_device.c | 262 ++++++++ .../claw_capabilities/cap_lua/src/cap_lua.c | 5 + .../cap_lua/src/cap_lua_internal.h | 2 +- components/common/app_claw/CMakeLists.txt | 12 + components/common/app_claw/Kconfig | 21 + components/common/app_claw/app_lua_modules.c | 34 + components/common/app_claw/idf_component.yml | 18 + .../lua_module_bq27220/CMakeLists.txt | 1 + .../lua_module_bq27220/idf_component.yml | 2 + .../lua_scripts/basic_bq27220.lua | 69 ++ .../lua_scripts/lib_bq27220.lua | 100 +++ .../skills/lua_module_bq27220.md | 46 ++ .../skills/skills_list.json | 12 + .../lua_modules/lua_module_imu/CMakeLists.txt | 17 + components/lua_modules/lua_module_imu/Kconfig | 21 + .../lua_module_imu/idf_component.yml | 6 + .../lua_module_imu/include/lua_module_imu.h | 29 + .../lua_module_imu/lua_scripts/basic_imu.lua | 54 ++ .../lua_module_imu/skills/lua_module_imu.md | 47 ++ .../lua_module_imu/skills/skills_list.json | 12 + .../lua_module_imu/src/lua_module_imu.c | 585 +++++++++++++++++ .../lua_module_lcd_touch/CMakeLists.txt | 1 + .../src/lua_module_lcd_touch.c | 83 ++- .../lua_module_touch/CMakeLists.txt | 12 + .../lua_modules/lua_module_touch/Kconfig | 10 + .../lua_module_touch/idf_component.yml | 2 + .../include/lua_module_touch.h | 20 + .../lua_scripts/basic_touch.lua | 41 ++ .../skills/lua_module_touch.md | 8 + .../lua_module_touch/skills/skills_list.json | 12 + .../lua_module_touch/src/lua_module_touch.c | 606 ++++++++++++++++++ 37 files changed, 2437 insertions(+), 2 deletions(-) create mode 100644 application/edge_agent/boards/esp_vocat_board_v1_2/board_devices.yaml create mode 100644 application/edge_agent/boards/esp_vocat_board_v1_2/board_info.yaml create mode 100644 application/edge_agent/boards/esp_vocat_board_v1_2/board_peripherals.yaml create mode 100644 application/edge_agent/boards/esp_vocat_board_v1_2/sdkconfig.defaults.board create mode 100644 application/edge_agent/boards/esp_vocat_board_v1_2/setup_device.c create mode 100644 components/lua_modules/lua_module_bq27220/CMakeLists.txt create mode 100644 components/lua_modules/lua_module_bq27220/idf_component.yml create mode 100644 components/lua_modules/lua_module_bq27220/lua_scripts/basic_bq27220.lua create mode 100644 components/lua_modules/lua_module_bq27220/lua_scripts/lib_bq27220.lua create mode 100644 components/lua_modules/lua_module_bq27220/skills/lua_module_bq27220.md create mode 100644 components/lua_modules/lua_module_bq27220/skills/skills_list.json create mode 100644 components/lua_modules/lua_module_imu/CMakeLists.txt create mode 100644 components/lua_modules/lua_module_imu/Kconfig create mode 100644 components/lua_modules/lua_module_imu/idf_component.yml create mode 100644 components/lua_modules/lua_module_imu/include/lua_module_imu.h create mode 100644 components/lua_modules/lua_module_imu/lua_scripts/basic_imu.lua create mode 100644 components/lua_modules/lua_module_imu/skills/lua_module_imu.md create mode 100644 components/lua_modules/lua_module_imu/skills/skills_list.json create mode 100644 components/lua_modules/lua_module_imu/src/lua_module_imu.c create mode 100644 components/lua_modules/lua_module_touch/CMakeLists.txt create mode 100644 components/lua_modules/lua_module_touch/Kconfig create mode 100644 components/lua_modules/lua_module_touch/idf_component.yml create mode 100644 components/lua_modules/lua_module_touch/include/lua_module_touch.h create mode 100644 components/lua_modules/lua_module_touch/lua_scripts/basic_touch.lua create mode 100644 components/lua_modules/lua_module_touch/skills/lua_module_touch.md create mode 100644 components/lua_modules/lua_module_touch/skills/skills_list.json create mode 100644 components/lua_modules/lua_module_touch/src/lua_module_touch.c diff --git a/.gitlab/ci/build.yml b/.gitlab/ci/build.yml index 9e1a074..ff215f1 100644 --- a/.gitlab/ci/build.yml +++ b/.gitlab/ci/build.yml @@ -106,5 +106,8 @@ build_application_edge_agent: - IMAGE: espressif/idf:release-v5.5 EXAMPLE_BOARD: m5stack_cores3 EXAMPLE_TARGET: esp32s3 + - IMAGE: espressif/idf:release-v5.5 + EXAMPLE_BOARD: esp_vocat_board_v1_2 + EXAMPLE_TARGET: esp32s3 variables: EXAMPLE_DIR: application/edge_agent diff --git a/.gitlab/ci/build_apps.py b/.gitlab/ci/build_apps.py index 736119d..495b5f3 100644 --- a/.gitlab/ci/build_apps.py +++ b/.gitlab/ci/build_apps.py @@ -55,6 +55,7 @@ IGNORE_WARNINGS = [ r'.+MultiCommand.+', r'warning: \'ISP_AWB_SAMPLE_POINT_BEFORE_CCM\' is deprecated: Use ISP_AWB_SAMPLE_POINT_0 instead \[-Wdeprecated-declarations\]', r'warning: \'ISP_AE_SAMPLE_POINT_AFTER_DEMOSAIC\' is deprecated: Use ISP_AE_SAMPLE_POINT_0 instead \[-Wdeprecated-declarations\]', + r'warning: \'touch_cst816s_read_id\' declared \'static\' but never defined \[-Wunused-function\]', ] class CustomApp(CMakeApp): diff --git a/application/edge_agent/boards/esp_vocat_board_v1_2/board_devices.yaml b/application/edge_agent/boards/esp_vocat_board_v1_2/board_devices.yaml new file mode 100644 index 0000000..f7a37a4 --- /dev/null +++ b/application/edge_agent/boards/esp_vocat_board_v1_2/board_devices.yaml @@ -0,0 +1,176 @@ +# Devices Configuration for EchoEar Core Board V1.0 +devices: + # Audio Power Control after Audio init + - name: audio_power + type: gpio_ctrl + version: default + config: + active_level: 1 + default_level: 0 + peripherals: + - name: gpio_power_audio + + # LCD and SDCard Power Control before LCD init + - name: lcd_sdcard_power + type: power_ctrl + sub_type: gpio + peripherals: + - name: gpio_power_lcd_sdcard + active_level: 0 + + - name: lcd_brightness + type: ledc_ctrl + version: default + config: + default_percent: 100 # 0-100, default 0 percent + peripherals: + - name: ledc_backlight # LEDC peripheral for GPIO 44 + + - name: audio_dac + chip: es8311 + type: audio_codec + version: default + config: + adc_enabled: false + dac_enabled: true + dac_max_channel: 1 + # DAC channel mask, 1-enable, 0-disable, layout: left,right + dac_channel_mask: "1" # Left, Right enable + mclk_enabled: true + + # Power amplifier configuration + peripherals: + - name: gpio_pa_control + gain: 6 # dB + active_level: 1 # 0-low, 1-high + + # I2S interface configuration + - name: i2s_audio_out + # I2C interface configuration + - name: i2c_master + address: 0x30 + frequency: 400000 # Hz + + # Audio ADC Configuration (ES7210) + - name: audio_adc + chip: es7210 + type: audio_codec + version: default + config: + adc_enabled: true + adc_max_channel: 4 + # The channel mask, 1-enable, 0-disable + # e.g."0111", bit0: Mic0 enable, bit1: Mic1 enable, bit2: Mic2 enable, bit3: Mic3 enable, bit4: Mic4 disable + adc_channel_mask: "0111" # Mic0, Mic1, Mic2 enable + adc_channel_labels: ['NA', 'RE', 'FR', 'FL'] # MSB ~ LSB + adc_init_gain: 0 # dB + + # I2S interface configuration + peripherals: + # I2S interface configuration + - name: i2s_audio_in + # I2C interface configuration + - name: i2c_master + address: 0x80 + frequency: 400000 # Hz + + - name: fs_sdcard + type: fs_fat + sub_type: sdmmc + power_ctrl_device: lcd_sdcard_power + version: default + config: + vfs_config: + format_if_mount_failed: true + sub_config: + bus_width: 1 # 1, 4, or 8 for slot 0; 1 or 4 for slot 1 + slot_flags: "SDMMC_SLOT_FLAG_INTERNAL_PULLUP" # SDMMC_SLOT_FLAG_INTERNAL_PULLUP, SDMMC_SLOT_FLAG_WP_ACTIVE_HIGH, SDMMC_SLOT_FLAG_UHS1 + pins: + clk: 16 # GPIO_NUM_16 + cmd: 38 # GPIO_NUM_38 + d0: 17 # GPIO_NUM_17 + + # LCD Display Configuration + - name: display_lcd + chip: st77916 + type: display_lcd + sub_type: spi + power_ctrl_device: lcd_sdcard_power + version: default + dependencies: + espressif/esp_lcd_st77916: "*" + config: + x_max: 360 + y_max: 360 + need_reset: false + # esp_lcd_panel_io_spi_config_t fields + io_spi_config: + cs_gpio_num: 14 # GPIO14 - LCD_CS (BSP_LCD_CS) + dc_gpio_num: 45 # GPIO45 - LCD_DC (BSP_LCD_DC) + spi_mode: 0 # Traditional SPI mode (0~3) + lcd_cmd_bits: 32 # Bit-width of LCD command + flags: + quad_mode: true # Quad mode (4 data lines) - ST77916 uses quad mode + + lcd_panel_config: + reset_gpio_num: 47 + flags: + reset_active_high: true # Reset pin active level (default: false) + # ST77916 specific configuration + vendor_config: NULL + + peripherals: + - name: spi_display # SPI peripheral for LCD communication + + # Touch Panel Configuration + - name: lcd_touch + chip: cst816t + type: lcd_touch_i2c + version: default + dependencies: + espressif/esp_lcd_touch_cst816s: "*" + config: + # esp_lcd_panel_io_i2c_config_t fields for touch communication + io_i2c_config: + dev_addr: 0x15 # CST816S I2C address (different from default 0x5A) + peripherals: + - name: i2c_master # I2C peripheral for touch communication + + # esp_lcd_touch_config_t fields for touch configuration + touch_config: + x_max: 360 # Maximum X coordinate (BSP_LCD_H_RES) + y_max: 360 # Maximum Y coordinate (BSP_LCD_V_RES) + int_gpio_num: 10 # Interrupt GPIO (BSP_LCD_TOUCH_INT) + + - name: imu_sensor + chip: bmi270 + type: custom + version: 1.0.0 + init_skip: true + config: + i2c_addr: 0x68 + frequency: 400000 + int_gpio_num: 21 + peripherals: + - name: i2c_master + + - name: touch_keys + chip: esp32s3_touch + type: custom + version: 1.0.0 + init_skip: true + config: + key1_gpio_num: 6 + key2_gpio_num: 7 + threshold_milli: 15 + + + # LED GREEN + - name: led_green + type: gpio_ctrl + version: default + config: + active_level: 0 + default_level: 1 + peripherals: + - name: gpio_led_g diff --git a/application/edge_agent/boards/esp_vocat_board_v1_2/board_info.yaml b/application/edge_agent/boards/esp_vocat_board_v1_2/board_info.yaml new file mode 100644 index 0000000..049f080 --- /dev/null +++ b/application/edge_agent/boards/esp_vocat_board_v1_2/board_info.yaml @@ -0,0 +1,4 @@ +board: esp_vocat_board_v1_2 +chip: esp32s3 +description: "ESP VoCat Board V1.2 AI Pet Companion Development Board" +manufacturer: "ESPRESSIF" diff --git a/application/edge_agent/boards/esp_vocat_board_v1_2/board_peripherals.yaml b/application/edge_agent/boards/esp_vocat_board_v1_2/board_peripherals.yaml new file mode 100644 index 0000000..1a73e97 --- /dev/null +++ b/application/edge_agent/boards/esp_vocat_board_v1_2/board_peripherals.yaml @@ -0,0 +1,94 @@ +peripherals: + # I2C Configuration + - name: i2c_master + type: i2c + role: master + config: + port: 0 + pins: + sda: 2 # GPIO2 - I2C SDA + scl: 1 # GPIO1 - I2C SCL + + # I2S Audio Configuration + - name: i2s_audio_out + type: i2s + role: master + format: std-out # std-out, std-in + config: &i2s_config + port: 0 + # Clock configuration + sample_rate_hz: 16000 # Sample rate in Hz + mclk_multiple: 256 # Multiple of MCLK to sample rate (256 or 384 for 24-bit) + # Slot configuration + data_bit_width: 32 # Data bit width (valid data bits per sample) + slot_bit_width: I2S_SLOT_BIT_WIDTH_AUTO # Total bits per slot (I2S_SLOT_BIT_WIDTH_AUTO or specific value) + slot_mode: I2S_SLOT_MODE_STEREO # I2S_SLOT_MODE_MONO or I2S_SLOT_MODE_STEREO + slot_mask: I2S_STD_SLOT_BOTH # I2S_STD_SLOT_LEFT, I2S_STD_SLOT_RIGHT, or I2S_STD_SLOT_BOTH slots + ws_width: 32 # WS signal width in BCLK ticks + + # GPIO configuration + pins: + mclk: 42 # GPIO42 - I2S_MCLK (BSP_I2S_MCLK) + bclk: 40 # GPIO40 - I2S_SCLK (BSP_I2S_SCLK) + ws: 39 # GPIO39 - I2S_LCLK (BSP_I2S_LCLK) + dout: 41 # GPIO41 - I2S_DOUT (BSP_I2S_DOUT) + din: 3 # GPIO3 - I2S_DSIN (BSP_I2S_DSIN) + + - name: i2s_audio_in + type: i2s + role: master + format: std-in + config: *i2s_config + + # LCD SPI Configuration for ST77916 QSPI + - name: spi_display + type: spi + role: master + config: + # spi_bus_config_t fields + spi_bus_config: + spi_port: SPI2_HOST # 0: SPI1_HOST, 1: SPI2_HOST, 2: SPI3_HOST + data0_io_num: 46 # MOSI_IO_NUM + data1_io_num: 13 # MISO_IO_NUM + data2_io_num: 11 # QUADWP_IO_NUM + data3_io_num: 12 # QUADHD_IO_NUM + sclk_io_num: 18 # SPI Clock signal + max_transfer_sz: 14400 # Maximum transfer size in bytes : width * 2 * 20 + + - name: ledc_backlight + type: ledc + role: none + config: + gpio_num: 44 # GPIO44 - LED control pin + freq_hz: 2000 # PWM frequency 2kHz + duty: 100 # Initial duty cycle (0%) + duty_resolution: LEDC_TIMER_10_BIT # 10-bit duty resolution + + ## CHIP POWER_ON_OFF LCD and SD card power control + - name: gpio_power_lcd_sdcard + type: gpio + config: + pin: 9 # GPIO9 - LCD_RST (CHIP POWER_ON_OFF) + mode: GPIO_MODE_OUTPUT + default_level: 1 + + - name: gpio_power_audio + type: gpio + config: + pin: 48 # GPIO48 - POWER_AUDIO + mode: GPIO_MODE_OUTPUT + default_level: 1 + + - name: gpio_pa_control + type: gpio + config: + pin: 15 # GPIO15 - AMP Control + mode: GPIO_MODE_OUTPUT + default_level: 0 + + - name: gpio_led_g + type: gpio + config: + pin: 43 # GPIO43 - LED GREEN + mode: GPIO_MODE_OUTPUT + default_level: 1 diff --git a/application/edge_agent/boards/esp_vocat_board_v1_2/sdkconfig.defaults.board b/application/edge_agent/boards/esp_vocat_board_v1_2/sdkconfig.defaults.board new file mode 100644 index 0000000..d5f187e --- /dev/null +++ b/application/edge_agent/boards/esp_vocat_board_v1_2/sdkconfig.defaults.board @@ -0,0 +1,11 @@ +CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y +CONFIG_ESPTOOLPY_FLASHMODE_QIO=y +CONFIG_ESPTOOLPY_FLASHFREQ_80M=y +CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y +CONFIG_SPIRAM=y +CONFIG_SPIRAM_MODE_OCT=y +CONFIG_SPIRAM_SPEED_80M=y + +CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y + +CONFIG_ESP_LCD_TOUCH_CST816S_DISABLE_READ_ID=y diff --git a/application/edge_agent/boards/esp_vocat_board_v1_2/setup_device.c b/application/edge_agent/boards/esp_vocat_board_v1_2/setup_device.c new file mode 100644 index 0000000..970796b --- /dev/null +++ b/application/edge_agent/boards/esp_vocat_board_v1_2/setup_device.c @@ -0,0 +1,262 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include "esp_board_device.h" +#include "esp_board_periph.h" +#include "esp_lcd_st77916.h" +#include "esp_lcd_touch_cst816s.h" +#include "esp_log.h" +#include "esp_attr.h" +#include "esp_err.h" +#include "driver/gpio.h" +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" + +static const st77916_lcd_init_cmd_t vendor_specific_init_default[] = { + {0xF0, (uint8_t[]) {0x28}, 1, 0}, + {0xF2, (uint8_t[]) {0x28}, 1, 0}, + {0x73, (uint8_t[]) {0xF0}, 1, 0}, + {0x7C, (uint8_t[]) {0xD1}, 1, 0}, + {0x83, (uint8_t[]) {0xE0}, 1, 0}, + {0x84, (uint8_t[]) {0x61}, 1, 0}, + {0xF2, (uint8_t[]) {0x82}, 1, 0}, + {0xF0, (uint8_t[]) {0x00}, 1, 0}, + {0xF0, (uint8_t[]) {0x01}, 1, 0}, + {0xF1, (uint8_t[]) {0x01}, 1, 0}, + {0xB0, (uint8_t[]) {0x56}, 1, 0}, + {0xB1, (uint8_t[]) {0x4D}, 1, 0}, + {0xB2, (uint8_t[]) {0x24}, 1, 0}, + {0xB4, (uint8_t[]) {0x87}, 1, 0}, + {0xB5, (uint8_t[]) {0x44}, 1, 0}, + {0xB6, (uint8_t[]) {0x8B}, 1, 0}, + {0xB7, (uint8_t[]) {0x40}, 1, 0}, + {0xB8, (uint8_t[]) {0x86}, 1, 0}, + {0xBA, (uint8_t[]) {0x00}, 1, 0}, + {0xBB, (uint8_t[]) {0x08}, 1, 0}, + {0xBC, (uint8_t[]) {0x08}, 1, 0}, + {0xBD, (uint8_t[]) {0x00}, 1, 0}, + {0xC0, (uint8_t[]) {0x80}, 1, 0}, + {0xC1, (uint8_t[]) {0x10}, 1, 0}, + {0xC2, (uint8_t[]) {0x37}, 1, 0}, + {0xC3, (uint8_t[]) {0x80}, 1, 0}, + {0xC4, (uint8_t[]) {0x10}, 1, 0}, + {0xC5, (uint8_t[]) {0x37}, 1, 0}, + {0xC6, (uint8_t[]) {0xA9}, 1, 0}, + {0xC7, (uint8_t[]) {0x41}, 1, 0}, + {0xC8, (uint8_t[]) {0x01}, 1, 0}, + {0xC9, (uint8_t[]) {0xA9}, 1, 0}, + {0xCA, (uint8_t[]) {0x41}, 1, 0}, + {0xCB, (uint8_t[]) {0x01}, 1, 0}, + {0xD0, (uint8_t[]) {0x91}, 1, 0}, + {0xD1, (uint8_t[]) {0x68}, 1, 0}, + {0xD2, (uint8_t[]) {0x68}, 1, 0}, + {0xF5, (uint8_t[]) {0x00, 0xA5}, 2, 0}, + {0xDD, (uint8_t[]) {0x4F}, 1, 0}, + {0xDE, (uint8_t[]) {0x4F}, 1, 0}, + {0xF1, (uint8_t[]) {0x10}, 1, 0}, + {0xF0, (uint8_t[]) {0x00}, 1, 0}, + {0xF0, (uint8_t[]) {0x02}, 1, 0}, + {0xE0, (uint8_t[]) {0xF0, 0x0A, 0x10, 0x09, 0x09, 0x36, 0x35, 0x33, 0x4A, 0x29, 0x15, 0x15, 0x2E, 0x34}, 14, 0}, + {0xE1, (uint8_t[]) {0xF0, 0x0A, 0x0F, 0x08, 0x08, 0x05, 0x34, 0x33, 0x4A, 0x39, 0x15, 0x15, 0x2D, 0x33}, 14, 0}, + {0xF0, (uint8_t[]) {0x10}, 1, 0}, + {0xF3, (uint8_t[]) {0x10}, 1, 0}, + {0xE0, (uint8_t[]) {0x07}, 1, 0}, + {0xE1, (uint8_t[]) {0x00}, 1, 0}, + {0xE2, (uint8_t[]) {0x00}, 1, 0}, + {0xE3, (uint8_t[]) {0x00}, 1, 0}, + {0xE4, (uint8_t[]) {0xE0}, 1, 0}, + {0xE5, (uint8_t[]) {0x06}, 1, 0}, + {0xE6, (uint8_t[]) {0x21}, 1, 0}, + {0xE7, (uint8_t[]) {0x01}, 1, 0}, + {0xE8, (uint8_t[]) {0x05}, 1, 0}, + {0xE9, (uint8_t[]) {0x02}, 1, 0}, + {0xEA, (uint8_t[]) {0xDA}, 1, 0}, + {0xEB, (uint8_t[]) {0x00}, 1, 0}, + {0xEC, (uint8_t[]) {0x00}, 1, 0}, + {0xED, (uint8_t[]) {0x0F}, 1, 0}, + {0xEE, (uint8_t[]) {0x00}, 1, 0}, + {0xEF, (uint8_t[]) {0x00}, 1, 0}, + {0xF8, (uint8_t[]) {0x00}, 1, 0}, + {0xF9, (uint8_t[]) {0x00}, 1, 0}, + {0xFA, (uint8_t[]) {0x00}, 1, 0}, + {0xFB, (uint8_t[]) {0x00}, 1, 0}, + {0xFC, (uint8_t[]) {0x00}, 1, 0}, + {0xFD, (uint8_t[]) {0x00}, 1, 0}, + {0xFE, (uint8_t[]) {0x00}, 1, 0}, + {0xFF, (uint8_t[]) {0x00}, 1, 0}, + {0x60, (uint8_t[]) {0x40}, 1, 0}, + {0x61, (uint8_t[]) {0x04}, 1, 0}, + {0x62, (uint8_t[]) {0x00}, 1, 0}, + {0x63, (uint8_t[]) {0x42}, 1, 0}, + {0x64, (uint8_t[]) {0xD9}, 1, 0}, + {0x65, (uint8_t[]) {0x00}, 1, 0}, + {0x66, (uint8_t[]) {0x00}, 1, 0}, + {0x67, (uint8_t[]) {0x00}, 1, 0}, + {0x68, (uint8_t[]) {0x00}, 1, 0}, + {0x69, (uint8_t[]) {0x00}, 1, 0}, + {0x6A, (uint8_t[]) {0x00}, 1, 0}, + {0x6B, (uint8_t[]) {0x00}, 1, 0}, + {0x70, (uint8_t[]) {0x40}, 1, 0}, + {0x71, (uint8_t[]) {0x03}, 1, 0}, + {0x72, (uint8_t[]) {0x00}, 1, 0}, + {0x73, (uint8_t[]) {0x42}, 1, 0}, + {0x74, (uint8_t[]) {0xD8}, 1, 0}, + {0x75, (uint8_t[]) {0x00}, 1, 0}, + {0x76, (uint8_t[]) {0x00}, 1, 0}, + {0x77, (uint8_t[]) {0x00}, 1, 0}, + {0x78, (uint8_t[]) {0x00}, 1, 0}, + {0x79, (uint8_t[]) {0x00}, 1, 0}, + {0x7A, (uint8_t[]) {0x00}, 1, 0}, + {0x7B, (uint8_t[]) {0x00}, 1, 0}, + {0x80, (uint8_t[]) {0x48}, 1, 0}, + {0x81, (uint8_t[]) {0x00}, 1, 0}, + {0x82, (uint8_t[]) {0x06}, 1, 0}, + {0x83, (uint8_t[]) {0x02}, 1, 0}, + {0x84, (uint8_t[]) {0xD6}, 1, 0}, + {0x85, (uint8_t[]) {0x04}, 1, 0}, + {0x86, (uint8_t[]) {0x00}, 1, 0}, + {0x87, (uint8_t[]) {0x00}, 1, 0}, + {0x88, (uint8_t[]) {0x48}, 1, 0}, + {0x89, (uint8_t[]) {0x00}, 1, 0}, + {0x8A, (uint8_t[]) {0x08}, 1, 0}, + {0x8B, (uint8_t[]) {0x02}, 1, 0}, + {0x8C, (uint8_t[]) {0xD8}, 1, 0}, + {0x8D, (uint8_t[]) {0x04}, 1, 0}, + {0x8E, (uint8_t[]) {0x00}, 1, 0}, + {0x8F, (uint8_t[]) {0x00}, 1, 0}, + {0x90, (uint8_t[]) {0x48}, 1, 0}, + {0x91, (uint8_t[]) {0x00}, 1, 0}, + {0x92, (uint8_t[]) {0x0A}, 1, 0}, + {0x93, (uint8_t[]) {0x02}, 1, 0}, + {0x94, (uint8_t[]) {0xDA}, 1, 0}, + {0x95, (uint8_t[]) {0x04}, 1, 0}, + {0x96, (uint8_t[]) {0x00}, 1, 0}, + {0x97, (uint8_t[]) {0x00}, 1, 0}, + {0x98, (uint8_t[]) {0x48}, 1, 0}, + {0x99, (uint8_t[]) {0x00}, 1, 0}, + {0x9A, (uint8_t[]) {0x0C}, 1, 0}, + {0x9B, (uint8_t[]) {0x02}, 1, 0}, + {0x9C, (uint8_t[]) {0xDC}, 1, 0}, + {0x9D, (uint8_t[]) {0x04}, 1, 0}, + {0x9E, (uint8_t[]) {0x00}, 1, 0}, + {0x9F, (uint8_t[]) {0x00}, 1, 0}, + {0xA0, (uint8_t[]) {0x48}, 1, 0}, + {0xA1, (uint8_t[]) {0x00}, 1, 0}, + {0xA2, (uint8_t[]) {0x05}, 1, 0}, + {0xA3, (uint8_t[]) {0x02}, 1, 0}, + {0xA4, (uint8_t[]) {0xD5}, 1, 0}, + {0xA5, (uint8_t[]) {0x04}, 1, 0}, + {0xA6, (uint8_t[]) {0x00}, 1, 0}, + {0xA7, (uint8_t[]) {0x00}, 1, 0}, + {0xA8, (uint8_t[]) {0x48}, 1, 0}, + {0xA9, (uint8_t[]) {0x00}, 1, 0}, + {0xAA, (uint8_t[]) {0x07}, 1, 0}, + {0xAB, (uint8_t[]) {0x02}, 1, 0}, + {0xAC, (uint8_t[]) {0xD7}, 1, 0}, + {0xAD, (uint8_t[]) {0x04}, 1, 0}, + {0xAE, (uint8_t[]) {0x00}, 1, 0}, + {0xAF, (uint8_t[]) {0x00}, 1, 0}, + {0xB0, (uint8_t[]) {0x48}, 1, 0}, + {0xB1, (uint8_t[]) {0x00}, 1, 0}, + {0xB2, (uint8_t[]) {0x09}, 1, 0}, + {0xB3, (uint8_t[]) {0x02}, 1, 0}, + {0xB4, (uint8_t[]) {0xD9}, 1, 0}, + {0xB5, (uint8_t[]) {0x04}, 1, 0}, + {0xB6, (uint8_t[]) {0x00}, 1, 0}, + {0xB7, (uint8_t[]) {0x00}, 1, 0}, + {0xB8, (uint8_t[]) {0x48}, 1, 0}, + {0xB9, (uint8_t[]) {0x00}, 1, 0}, + {0xBA, (uint8_t[]) {0x0B}, 1, 0}, + {0xBB, (uint8_t[]) {0x02}, 1, 0}, + {0xBC, (uint8_t[]) {0xDB}, 1, 0}, + {0xBD, (uint8_t[]) {0x04}, 1, 0}, + {0xBE, (uint8_t[]) {0x00}, 1, 0}, + {0xBF, (uint8_t[]) {0x00}, 1, 0}, + {0xC0, (uint8_t[]) {0x10}, 1, 0}, + {0xC1, (uint8_t[]) {0x47}, 1, 0}, + {0xC2, (uint8_t[]) {0x56}, 1, 0}, + {0xC3, (uint8_t[]) {0x65}, 1, 0}, + {0xC4, (uint8_t[]) {0x74}, 1, 0}, + {0xC5, (uint8_t[]) {0x88}, 1, 0}, + {0xC6, (uint8_t[]) {0x99}, 1, 0}, + {0xC7, (uint8_t[]) {0x01}, 1, 0}, + {0xC8, (uint8_t[]) {0xBB}, 1, 0}, + {0xC9, (uint8_t[]) {0xAA}, 1, 0}, + {0xD0, (uint8_t[]) {0x10}, 1, 0}, + {0xD1, (uint8_t[]) {0x47}, 1, 0}, + {0xD2, (uint8_t[]) {0x56}, 1, 0}, + {0xD3, (uint8_t[]) {0x65}, 1, 0}, + {0xD4, (uint8_t[]) {0x74}, 1, 0}, + {0xD5, (uint8_t[]) {0x88}, 1, 0}, + {0xD6, (uint8_t[]) {0x99}, 1, 0}, + {0xD7, (uint8_t[]) {0x01}, 1, 0}, + {0xD8, (uint8_t[]) {0xBB}, 1, 0}, + {0xD9, (uint8_t[]) {0xAA}, 1, 0}, + {0xF3, (uint8_t[]) {0x01}, 1, 0}, + {0xF0, (uint8_t[]) {0x00}, 1, 0}, + {0x21, (uint8_t[]) {}, 0, 0}, + {0x11, (uint8_t[]) {}, 0, 0}, + {0x00, (uint8_t[]) {}, 0, 120}, +}; + +// define st77916_vendor_config_t +static const st77916_vendor_config_t vendor_config = { + .init_cmds = vendor_specific_init_default, + .init_cmds_size = sizeof(vendor_specific_init_default) / sizeof(vendor_specific_init_default[0]), + .flags = { + .use_qspi_interface = 1, // QSPI + } +}; + +esp_err_t lcd_panel_factory_entry_t(esp_lcd_panel_io_handle_t io, const esp_lcd_panel_dev_config_t *panel_dev_config, esp_lcd_panel_handle_t *ret_panel) +{ + esp_lcd_panel_dev_config_t panel_dev_cfg = {0}; + memcpy(&panel_dev_cfg, panel_dev_config, sizeof(esp_lcd_panel_dev_config_t)); + + panel_dev_cfg.vendor_config = (void *)&vendor_config; + int ret = esp_lcd_new_panel_st77916(io, &panel_dev_cfg, ret_panel); + if (ret != ESP_OK) { + ESP_LOGE("lcd_panel_factory_entry_t", "New ST77916 panel failed"); + return ret; + } + return ESP_OK; +} + +/* CST816T 无触摸时进入低功耗、I2C 不响应,需中断驱动读取。 + * 契约: interrupt_callback 非空且 user_data 是二值信号量 -> 上层走中断模式。 */ +static IRAM_ATTR void cst816t_interrupt_cb(esp_lcd_touch_handle_t tp) +{ + SemaphoreHandle_t sem = (tp != NULL) ? (SemaphoreHandle_t)tp->config.user_data : NULL; + if (sem == NULL) { + return; + } + BaseType_t higher_prio_woken = pdFALSE; + xSemaphoreGiveFromISR(sem, &higher_prio_woken); + if (higher_prio_woken == pdTRUE) { + portYIELD_FROM_ISR(); + } +} + +esp_err_t lcd_touch_factory_entry_t(esp_lcd_panel_io_handle_t io, const esp_lcd_touch_config_t *touch_dev_config, esp_lcd_touch_handle_t *ret_touch) +{ + static SemaphoreHandle_t s_cst816t_int_sem = NULL; + esp_lcd_touch_config_t touch_cfg = {0}; + memcpy(&touch_cfg, touch_dev_config, sizeof(esp_lcd_touch_config_t)); + + if (s_cst816t_int_sem == NULL) { + s_cst816t_int_sem = xSemaphoreCreateBinary(); + } + touch_cfg.interrupt_callback = cst816t_interrupt_cb; + touch_cfg.user_data = (void *)s_cst816t_int_sem; + + esp_err_t ret = esp_lcd_touch_new_i2c_cst816s(io, &touch_cfg, ret_touch); + if (ret != ESP_OK) { + ESP_LOGE("lcd_touch_factory_entry_t", "Failed to create CST816S touch driver: %s", esp_err_to_name(ret)); + return ret; + } + return ESP_OK; +} diff --git a/components/claw_capabilities/cap_lua/src/cap_lua.c b/components/claw_capabilities/cap_lua/src/cap_lua.c index 102dedb..d93ff02 100644 --- a/components/claw_capabilities/cap_lua/src/cap_lua.c +++ b/components/claw_capabilities/cap_lua/src/cap_lua.c @@ -1382,6 +1382,11 @@ esp_err_t cap_lua_register_module(const char *name, lua_CFunction open_fn) } if (s_module_count >= CAP_LUA_MAX_MODULES) { + ESP_LOGE(TAG, + "Lua module registry full: tried to add '%s' with %u/%u slots used", + name, + (unsigned)s_module_count, + (unsigned)CAP_LUA_MAX_MODULES); return ESP_ERR_NO_MEM; } diff --git a/components/claw_capabilities/cap_lua/src/cap_lua_internal.h b/components/claw_capabilities/cap_lua/src/cap_lua_internal.h index 5d1c62b..a5820f4 100644 --- a/components/claw_capabilities/cap_lua/src/cap_lua_internal.h +++ b/components/claw_capabilities/cap_lua/src/cap_lua_internal.h @@ -19,7 +19,7 @@ #define CAP_LUA_ASYNC_DEFAULT_TIMEOUT_MS 0 #define CAP_LUA_ASYNC_MAX_JOBS 16 #define CAP_LUA_ASYNC_MAX_CONCURRENT 4 -#define CAP_LUA_MAX_MODULES 18 +#define CAP_LUA_MAX_MODULES 32 #define CAP_LUA_JOB_NAME_MAX 32 #define CAP_LUA_JOB_EXCLUSIVE_MAX 16 diff --git a/components/common/app_claw/CMakeLists.txt b/components/common/app_claw/CMakeLists.txt index 006c196..14030d1 100644 --- a/components/common/app_claw/CMakeLists.txt +++ b/components/common/app_claw/CMakeLists.txt @@ -90,6 +90,18 @@ if(CONFIG_APP_CLAW_CAP_LUA AND CONFIG_APP_CLAW_LUA_MODULE_BOARD_MANAGER) list(APPEND app_claw_requires lua_module_board_manager) endif() +if(CONFIG_APP_CLAW_CAP_LUA AND CONFIG_APP_CLAW_LUA_MODULE_IMU) + list(APPEND app_claw_requires lua_module_imu) +endif() + +if(CONFIG_APP_CLAW_CAP_LUA AND CONFIG_APP_CLAW_LUA_MODULE_BQ27220) + list(APPEND app_claw_requires lua_module_bq27220) +endif() + +if(CONFIG_APP_CLAW_CAP_LUA AND CONFIG_APP_CLAW_LUA_MODULE_TOUCH) + list(APPEND app_claw_requires lua_module_touch) +endif() + if(CONFIG_APP_CLAW_CAP_LUA AND CONFIG_APP_CLAW_LUA_MODULE_CAPABILITY) list(APPEND app_claw_requires lua_module_call_capability) endif() diff --git a/components/common/app_claw/Kconfig b/components/common/app_claw/Kconfig index 89b88fd..b47015a 100644 --- a/components/common/app_claw/Kconfig +++ b/components/common/app_claw/Kconfig @@ -153,6 +153,27 @@ menu "App Claw Config" menu "App Claw Lua Modules" + config APP_CLAW_LUA_MODULE_IMU + bool "Enable IMU Lua module" + default y + help + Enable the IMU Lua module and keep app_claw's direct dependency + on lua_module_imu when Lua capability is enabled. + + config APP_CLAW_LUA_MODULE_BQ27220 + bool "Enable BQ27220 Lua module" + default y + help + Enable the BQ27220 Lua module and keep app_claw's direct dependency + on lua_module_bq27220 when Lua capability is enabled. + + config APP_CLAW_LUA_MODULE_TOUCH + bool "Enable touch Lua module" + default y + help + Enable the touch Lua module and keep app_claw's direct dependency + on lua_module_touch when Lua capability is enabled. + config APP_CLAW_LUA_MODULE_ADC bool "Enable ADC Lua module" default y diff --git a/components/common/app_claw/app_lua_modules.c b/components/common/app_claw/app_lua_modules.c index 560b9b6..9c2f78f 100644 --- a/components/common/app_claw/app_lua_modules.c +++ b/components/common/app_claw/app_lua_modules.c @@ -49,6 +49,12 @@ #if CONFIG_APP_CLAW_LUA_MODULE_BOARD_MANAGER #include "lua_module_board_manager.h" #endif +#if CONFIG_APP_CLAW_LUA_MODULE_IMU +#include "lua_module_imu.h" +#endif +#if CONFIG_APP_CLAW_LUA_MODULE_TOUCH +#include "lua_module_touch.h" +#endif #if CONFIG_APP_CLAW_LUA_MODULE_MCPWM #include "lua_module_mcpwm.h" #endif @@ -286,6 +292,22 @@ static esp_err_t app_lua_register_board_manager(const char *fatfs_base_path) } #endif +#if CONFIG_APP_CLAW_LUA_MODULE_IMU +static esp_err_t app_lua_register_imu(const char *fatfs_base_path) +{ + (void)fatfs_base_path; + return lua_module_imu_register(); +} +#endif + +#if CONFIG_APP_CLAW_LUA_MODULE_TOUCH +static esp_err_t app_lua_register_touch(const char *fatfs_base_path) +{ + (void)fatfs_base_path; + return lua_module_touch_register(); +} +#endif + #if CONFIG_APP_CLAW_LUA_MODULE_MCPWM static esp_err_t app_lua_register_mcpwm(const char *fatfs_base_path) { @@ -368,6 +390,12 @@ static const app_lua_module_entry_t s_lua_module_entries[] = { #if CONFIG_APP_CLAW_LUA_MODULE_BOARD_MANAGER { "board_manager", "Board Manager", app_lua_register_board_manager }, #endif +#if CONFIG_APP_CLAW_LUA_MODULE_IMU + { "imu", "IMU", app_lua_register_imu }, +#endif +#if CONFIG_APP_CLAW_LUA_MODULE_TOUCH + { "touch", "Touch", app_lua_register_touch }, +#endif #if CONFIG_APP_CLAW_LUA_MODULE_LCD_TOUCH && defined(CONFIG_ESP_BOARD_DEV_LCD_TOUCH_I2C_SUPPORT) { "lcd_touch", "LCD Touch", app_lua_register_lcd_touch }, #endif @@ -425,6 +453,12 @@ static const app_lua_module_info_t s_lua_module_infos[] = { #if CONFIG_APP_CLAW_LUA_MODULE_BOARD_MANAGER { "board_manager", "Board Manager" }, #endif +#if CONFIG_APP_CLAW_LUA_MODULE_IMU + { "imu", "IMU" }, +#endif +#if CONFIG_APP_CLAW_LUA_MODULE_TOUCH + { "touch", "Touch" }, +#endif #if CONFIG_APP_CLAW_LUA_MODULE_LCD_TOUCH && defined(CONFIG_ESP_BOARD_DEV_LCD_TOUCH_I2C_SUPPORT) { "lcd_touch", "LCD Touch" }, #endif diff --git a/components/common/app_claw/idf_component.yml b/components/common/app_claw/idf_component.yml index 77556ae..052024d 100644 --- a/components/common/app_claw/idf_component.yml +++ b/components/common/app_claw/idf_component.yml @@ -118,6 +118,24 @@ dependencies: - if: $CONFIG{APP_CLAW_LUA_MODULE_BOARD_MANAGER} == True path: ../../lua_modules/lua_module_board_manager + lua_module_imu: + rules: + - if: $CONFIG{APP_CLAW_CAP_LUA} == True + - if: $CONFIG{APP_CLAW_LUA_MODULE_IMU} == True + path: ../../lua_modules/lua_module_imu + + lua_module_bq27220: + rules: + - if: $CONFIG{APP_CLAW_CAP_LUA} == True + - if: $CONFIG{APP_CLAW_LUA_MODULE_BQ27220} == True + path: ../../lua_modules/lua_module_bq27220 + + lua_module_touch: + rules: + - if: $CONFIG{APP_CLAW_CAP_LUA} == True + - if: $CONFIG{APP_CLAW_LUA_MODULE_TOUCH} == True + path: ../../lua_modules/lua_module_touch + lua_module_adc: rules: - if: $CONFIG{APP_CLAW_CAP_LUA} == True diff --git a/components/lua_modules/lua_module_bq27220/CMakeLists.txt b/components/lua_modules/lua_module_bq27220/CMakeLists.txt new file mode 100644 index 0000000..a96dced --- /dev/null +++ b/components/lua_modules/lua_module_bq27220/CMakeLists.txt @@ -0,0 +1 @@ +idf_component_register() diff --git a/components/lua_modules/lua_module_bq27220/idf_component.yml b/components/lua_modules/lua_module_bq27220/idf_component.yml new file mode 100644 index 0000000..d4e300f --- /dev/null +++ b/components/lua_modules/lua_module_bq27220/idf_component.yml @@ -0,0 +1,2 @@ +## IDF Component Manager Manifest File +dependencies: {} diff --git a/components/lua_modules/lua_module_bq27220/lua_scripts/basic_bq27220.lua b/components/lua_modules/lua_module_bq27220/lua_scripts/basic_bq27220.lua new file mode 100644 index 0000000..0d200e1 --- /dev/null +++ b/components/lua_modules/lua_module_bq27220/lua_scripts/basic_bq27220.lua @@ -0,0 +1,69 @@ +local bq27220 = require("lib_bq27220") +local delay = require("delay") +local i2c = require("i2c") + +local a = type(args) == "table" and args or {} +local function int_arg(k, default) + local v = a[k] + if type(v) == "number" then + return math.floor(v) + end + return default +end + +local I2C_ADDR = int_arg("addr", 0x55) +local FREQ_HZ = int_arg("freq_hz", 400000) +local SAMPLE_COUNT = int_arg("samples", 20) +local INTERVAL_MS = int_arg("interval_ms", 1000) + +local gauge +local bus + +local function cleanup() + if gauge then + pcall(function() + gauge:close() + end) + gauge = nil + end + if bus then + pcall(function() + bus:close() + end) + bus = nil + end +end + +local function run() + local port = int_arg("port", 0) + local sda = int_arg("sda", 14) + local scl = int_arg("scl", 13) + bus = a.bus or i2c.new(port, sda, scl, FREQ_HZ) + print(string.format( + "[bq27220] opening addr=0x%02X freq=%d", + I2C_ADDR, FREQ_HZ + )) + gauge = bq27220.new({ + bus = bus, + addr = I2C_ADDR, + freq_hz = FREQ_HZ, + }) + + for i = 1, SAMPLE_COUNT do + local sample = gauge:read() + print(string.format( + "[bq27220] #%d soc=%d%% voltage=%dmV current=%dmA", + i, + sample.soc, + sample.voltage_mv, + sample.current_ma + )) + delay.delay_ms(INTERVAL_MS) + end +end + +local ok, err = xpcall(run, debug.traceback) +cleanup() +if not ok then + error(err) +end diff --git a/components/lua_modules/lua_module_bq27220/lua_scripts/lib_bq27220.lua b/components/lua_modules/lua_module_bq27220/lua_scripts/lib_bq27220.lua new file mode 100644 index 0000000..8fa408e --- /dev/null +++ b/components/lua_modules/lua_module_bq27220/lua_scripts/lib_bq27220.lua @@ -0,0 +1,100 @@ +local i2c = require("i2c") + +local M = {} + +local DEFAULT_ADDR = 0x55 +local DEFAULT_FREQ_HZ = 400000 +local REG_VOLTAGE = 0x08 +local REG_CURRENT = 0x0C +local REG_SOC = 0x2C + +local mt = {} +mt.__index = mt + +local function u16le(data) + local lo = string.byte(data, 1) or 0 + local hi = string.byte(data, 2) or 0 + return lo | (hi << 8) +end + +local function i16le(data) + local v = u16le(data) + if v >= 0x8000 then + return v - 0x10000 + end + return v +end + +local function new_device_from_opts(opts) + local bus + local owns_bus = false + + if opts.bus ~= nil then + bus = opts.bus + else + bus = i2c.new( + assert(opts.port, "bq27220.new: missing 'port'"), + assert(opts.sda, "bq27220.new: missing 'sda'"), + assert(opts.scl, "bq27220.new: missing 'scl'"), + opts.frequency or opts.freq_hz or DEFAULT_FREQ_HZ + ) + owns_bus = true + end + + local dev = bus:device(opts.addr or DEFAULT_ADDR, 0) + return bus, dev, owns_bus +end + +function M.new(opts) + opts = type(opts) == "table" and opts or {} + local bus, dev, owns_bus = new_device_from_opts(opts) + return setmetatable({ + _bus = bus, + _dev = dev, + _owns_bus = owns_bus, + _addr = opts.addr or DEFAULT_ADDR, + }, mt) +end + +function mt:address() + return self._addr +end + +function mt:read_voltage_mv() + return u16le(self._dev:read(2, REG_VOLTAGE)) +end + +function mt:read_current_ma() + return i16le(self._dev:read(2, REG_CURRENT)) +end + +function mt:read_soc() + return u16le(self._dev:read(2, REG_SOC)) +end + +function mt:read() + return { + voltage_mv = self:read_voltage_mv(), + current_ma = self:read_current_ma(), + soc = self:read_soc(), + } +end + +function mt:close() + if self._dev then + self._dev:close() + self._dev = nil + end + if self._owns_bus and self._bus then + self._bus:close() + self._bus = nil + end +end + +function mt:__gc() + pcall(function() + self:close() + end) +end + +return M diff --git a/components/lua_modules/lua_module_bq27220/skills/lua_module_bq27220.md b/components/lua_modules/lua_module_bq27220/skills/lua_module_bq27220.md new file mode 100644 index 0000000..3b55337 --- /dev/null +++ b/components/lua_modules/lua_module_bq27220/skills/lua_module_bq27220.md @@ -0,0 +1,46 @@ +# Lua BQ27220 + +This skill describes how to read `BQ27220` battery data from Lua using the existing `i2c` module. +When a request mentions `bq27220`, `battery voltage`, `battery current`, or `battery percentage`, use this module by default. + +## How to call +- Import it with `local bq27220 = require("lib_bq27220")` +- Create or reuse an I2C bus, then open the gauge with `local gauge = bq27220.new({ bus = bus })` +- Read all three values with `local sample = gauge:read()` +- Or read one value at a time: + - `gauge:read_voltage_mv()` + - `gauge:read_current_ma()` + - `gauge:read_soc()` +- Call `gauge:close()` when needed + +## Options table +| Field | Type | Meaning | +|------------|---------|--------------------------------------------| +| `port` | integer | I2C port number | +| `sda` | integer | SDA GPIO number | +| `scl` | integer | SCL GPIO number | +| `freq_hz` | integer | I2C clock in Hz (default `400000`) | +| `frequency`| integer | Alias of `freq_hz` | +| `addr` | integer | BQ27220 7-bit I2C address (default `0x55`) | +| `bus` | userdata| Existing `i2c` bus handle, recommended | + +## Data format +- `sample.voltage_mv` +- `sample.current_ma` +- `sample.soc` + +## Example +```lua +local bq27220 = require("lib_bq27220") +local i2c = require("i2c") + +local bus = i2c.new(0, 14, 13, 400000) +local gauge = bq27220.new({ + bus = bus, +}) + +local sample = gauge:read() +print(sample.voltage_mv, sample.current_ma, sample.soc) +gauge:close() +bus:close() +``` diff --git a/components/lua_modules/lua_module_bq27220/skills/skills_list.json b/components/lua_modules/lua_module_bq27220/skills/skills_list.json new file mode 100644 index 0000000..2b40fb9 --- /dev/null +++ b/components/lua_modules/lua_module_bq27220/skills/skills_list.json @@ -0,0 +1,12 @@ +{ + "skills": [ + { + "id": "lua_module_bq27220", + "file": "lua_module_bq27220.md", + "summary": "How to read battery fuel-gauge data from Lua.", + "cap_groups": [ + "cap_lua" + ] + } + ] +} diff --git a/components/lua_modules/lua_module_imu/CMakeLists.txt b/components/lua_modules/lua_module_imu/CMakeLists.txt new file mode 100644 index 0000000..d2949c8 --- /dev/null +++ b/components/lua_modules/lua_module_imu/CMakeLists.txt @@ -0,0 +1,17 @@ +idf_component_register( + SRCS + "src/lua_module_imu.c" + INCLUDE_DIRS + "include" + REQUIRES + cap_lua + esp_board_manager + PRIV_REQUIRES + espressif__i2c_bus +) + +if(CONFIG_LUA_MODULE_IMU_CHIP_BMI270) + idf_component_optional_requires(PRIVATE espressif__bmi270_sensor) +endif() + +idf_component_set_property(${COMPONENT_NAME} WHOLE_ARCHIVE TRUE) diff --git a/components/lua_modules/lua_module_imu/Kconfig b/components/lua_modules/lua_module_imu/Kconfig new file mode 100644 index 0000000..cd04300 --- /dev/null +++ b/components/lua_modules/lua_module_imu/Kconfig @@ -0,0 +1,21 @@ +menu "Lua IMU Module" + + choice LUA_MODULE_IMU_CHIP + prompt "IMU chip model" + default LUA_MODULE_IMU_CHIP_BMI270 + help + Select which IMU backend this Lua module should build against. + + config LUA_MODULE_IMU_CHIP_BMI270 + bool "BMI270" + help + Build the IMU Lua module with the BMI270 backend. + + config LUA_MODULE_IMU_CHIP_MPU6050 + bool "MPU6050" + help + Reserved for a future MPU6050 backend. The code path is not + implemented yet and will fail at compile time if selected. + endchoice + +endmenu diff --git a/components/lua_modules/lua_module_imu/idf_component.yml b/components/lua_modules/lua_module_imu/idf_component.yml new file mode 100644 index 0000000..00da88f --- /dev/null +++ b/components/lua_modules/lua_module_imu/idf_component.yml @@ -0,0 +1,6 @@ +## IDF Component Manager Manifest File +dependencies: + espressif/bmi270_sensor: + version: "*" + rules: + - if: "$CONFIG{LUA_MODULE_IMU_CHIP_BMI270} == True" diff --git a/components/lua_modules/lua_module_imu/include/lua_module_imu.h b/components/lua_modules/lua_module_imu/include/lua_module_imu.h new file mode 100644 index 0000000..2661eac --- /dev/null +++ b/components/lua_modules/lua_module_imu/include/lua_module_imu.h @@ -0,0 +1,29 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include "sdkconfig.h" +#include "esp_err.h" +#include "lua.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#if CONFIG_LUA_MODULE_IMU_CHIP_BMI270 +#define LUA_MODULE_IMU_SELECTED_CHIP_NAME "bmi270" +#elif CONFIG_LUA_MODULE_IMU_CHIP_MPU6050 +#define LUA_MODULE_IMU_SELECTED_CHIP_NAME "mpu6050" +#else +#error "No IMU chip model selected for lua_module_imu" +#endif + +int luaopen_imu(lua_State *L); +esp_err_t lua_module_imu_register(void); + +#ifdef __cplusplus +} +#endif diff --git a/components/lua_modules/lua_module_imu/lua_scripts/basic_imu.lua b/components/lua_modules/lua_module_imu/lua_scripts/basic_imu.lua new file mode 100644 index 0000000..46ad798 --- /dev/null +++ b/components/lua_modules/lua_module_imu/lua_scripts/basic_imu.lua @@ -0,0 +1,54 @@ +-- IMU demo: open board device, print accel/gyro/temperature samples. +-- Optional args: device, samples, interval_ms. +local imu = require("imu") +local delay = require("delay") + +local a = type(args) == "table" and args or {} +local function int_arg(k, default) + local v = a[k] + if type(v) == "number" then + return math.floor(v) + end + return default +end + +local DEVICE_NAME = type(a.device) == "string" and a.device or "imu_sensor" +local SAMPLE_COUNT = int_arg("samples", 20) +local INTERVAL_MS = int_arg("interval_ms", 200) + +local sensor + +local function cleanup() + if sensor then + pcall(function() + sensor:close() + end) + sensor = nil + end +end + +local function run() + print("[imu] opening device " .. DEVICE_NAME) + sensor = imu.new(DEVICE_NAME) + print("[imu] opened " .. sensor:name()) + + for i = 1, SAMPLE_COUNT do + local sample = sensor:read() + local temp = sensor:read_temperature() + print(string.format( + "[imu] #%d acc=(%d,%d,%d) gyr=(%d,%d,%d) temp_raw=%d status=0x%02X", + i, + sample.accel.x, sample.accel.y, sample.accel.z, + sample.gyro.x, sample.gyro.y, sample.gyro.z, + temp, + sample.status + )) + delay.delay_ms(INTERVAL_MS) + end +end + +local ok, err = xpcall(run, debug.traceback) +cleanup() +if not ok then + error(err) +end diff --git a/components/lua_modules/lua_module_imu/skills/lua_module_imu.md b/components/lua_modules/lua_module_imu/skills/lua_module_imu.md new file mode 100644 index 0000000..40ddd61 --- /dev/null +++ b/components/lua_modules/lua_module_imu/skills/lua_module_imu.md @@ -0,0 +1,47 @@ +# Lua IMU + +This skill describes how to read IMU data from Lua. +When a request mentions `bmi270`, `imu`, `accelerometer`, or `gyroscope`, use this module by default. + +## How to call +- Import it with `local imu = require("imu")` +- Open the sensor with one of: + - `local sensor = imu.new()` — use board defaults from the device named `imu_sensor` + - `local sensor = imu.new("imu_sensor")` — choose a device name explicitly + - `local sensor = imu.new({ peripheral = "i2c_master", int_gpio = 21 })` — provide options directly + - `local sensor = imu.new("imu_sensor", { frequency = 100000 })` — board defaults + per-field overrides +- Call `local sample = sensor:read()` to get accel and gyro raw data +- Call `local temp = sensor:read_temperature()` to read the raw temperature value +- Call `local status = sensor:read_int_status()` to get the interrupt status bits +- Call `sensor:close()` when needed + +## Options table +All fields are optional. Any field omitted falls back to the board +`board_devices.yaml` value for the device named by `device` (default +`"imu_sensor"`). On a board that does not declare the device, missing +required fields will raise an error. + +| Field | Type | Meaning | +|--------------|----------|---------------------------------------------------------------| +| `device` | string | Board device name to read defaults from (default `imu_sensor`)| +| `peripheral` | string | Board I2C master peripheral name (e.g. `"i2c_master"`) | +| `i2c_addr` | integer | BMI270 7-bit I2C address (default `0x68`) | +| `frequency` | integer | I2C clock in Hz (default `400000`) | +| `int_gpio` | integer | GPIO number wired to BMI270 INT1 | + +## Data format +- `sample.accel.x`, `sample.accel.y`, `sample.accel.z` +- `sample.gyro.x`, `sample.gyro.y`, `sample.gyro.z` +- `sample.sens_time` +- `sample.status` + +## Example +```lua +local imu = require("imu") + +local sensor = imu.new() -- uses board defaults +local sample = sensor:read() +print(sample.accel.x, sample.accel.y, sample.accel.z) +print(sample.gyro.x, sample.gyro.y, sample.gyro.z) +sensor:close() +``` diff --git a/components/lua_modules/lua_module_imu/skills/skills_list.json b/components/lua_modules/lua_module_imu/skills/skills_list.json new file mode 100644 index 0000000..c5ace60 --- /dev/null +++ b/components/lua_modules/lua_module_imu/skills/skills_list.json @@ -0,0 +1,12 @@ +{ + "skills": [ + { + "id": "lua_module_imu", + "file": "lua_module_imu.md", + "summary": "How to read IMU data from Lua.", + "cap_groups": [ + "cap_lua" + ] + } + ] +} diff --git a/components/lua_modules/lua_module_imu/src/lua_module_imu.c b/components/lua_modules/lua_module_imu/src/lua_module_imu.c new file mode 100644 index 0000000..a03a118 --- /dev/null +++ b/components/lua_modules/lua_module_imu/src/lua_module_imu.c @@ -0,0 +1,585 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "lua_module_imu.h" + +#include +#include +#include + +#include "cap_lua.h" +#include "driver/gpio.h" +#include "esp_board_manager.h" +#include "esp_board_periph.h" +#include "esp_check.h" +#include "esp_log.h" +#include "i2c_bus.h" +#include "lauxlib.h" + +#if CONFIG_LUA_MODULE_IMU_CHIP_BMI270 +#include "bmi270_api.h" +#elif CONFIG_LUA_MODULE_IMU_CHIP_MPU6050 +#error "CONFIG_LUA_MODULE_IMU_CHIP_MPU6050 is reserved but not implemented yet" +#else +#error "Unsupported IMU chip selection" +#endif + +#define LUA_MODULE_IMU_METATABLE "imu.device" +#define LUA_MODULE_IMU_DEFAULT_NAME "imu_sensor" +#define LUA_MODULE_IMU_LEGACY_NAME "bmi270_sensor" +#define LUA_MODULE_IMU_MAX_NAME_LEN 64 + +typedef struct { +#if CONFIG_LUA_MODULE_IMU_CHIP_BMI270 + bmi270_handle_t sensor_handle; +#endif + i2c_bus_handle_t i2c_bus_handle; + char peripheral_name[LUA_MODULE_IMU_MAX_NAME_LEN]; + bool peripheral_ref_held; + gpio_num_t int_gpio_num; +} lua_module_imu_handle_t; + +typedef struct { + lua_module_imu_handle_t *handle; + char device_name[LUA_MODULE_IMU_MAX_NAME_LEN]; +} lua_module_imu_ud_t; + +/* + * Local mirror of the dev_custom_imu_sensor_config_t struct that the ESP + * Board Manager auto-generates from the board's board_devices.yaml. + * + * We mirror it (instead of #including gen_board_device_custom.h) so this + * component compiles uniformly on every board, including boards that do + * not declare the imu_sensor custom device. The board manager exposes the + * parsed struct via esp_board_manager_get_device_config(), and we cast + * the void* to this mirror type to read defaults. + * + * KEEP THE FIELDS IN SYNC with the generator output for the imu_sensor + * YAML schema (single peripheral, fields below). + */ +typedef struct { + const char *name; + const char *type; + const char *chip; + int8_t i2c_addr; + int32_t frequency; + int8_t int_gpio_num; + uint8_t peripheral_count; + const char *peripheral_name; +} lua_imu_board_cfg_t; + +typedef struct { + char peripheral_name[LUA_MODULE_IMU_MAX_NAME_LEN]; + int i2c_addr; + int frequency; + int int_gpio_num; + bool has_peripheral; + bool has_i2c_addr; + bool has_frequency; + bool has_int_gpio; +} lua_imu_resolved_cfg_t; + +static const char *TAG = "lua_module_imu"; + +#if CONFIG_LUA_MODULE_IMU_CHIP_BMI270 + +static esp_err_t lua_module_imu_configure_interrupt_pin(int int_gpio_num) +{ + const gpio_config_t int_pin_cfg = { + .pin_bit_mask = 1ULL << int_gpio_num, + .mode = GPIO_MODE_INPUT, + .pull_up_en = GPIO_PULLUP_DISABLE, + .pull_down_en = GPIO_PULLDOWN_DISABLE, + .intr_type = GPIO_INTR_DISABLE, + }; + + return gpio_config(&int_pin_cfg); +} + +static esp_err_t lua_module_imu_open_i2c_bus(const char *peripheral_name, + int frequency, + i2c_bus_handle_t *i2c_bus_handle, + bool *peripheral_ref_held) +{ + i2c_master_bus_handle_t i2c_master_handle = NULL; + i2c_master_bus_config_t *i2c_master_cfg = NULL; + + *peripheral_ref_held = false; + + ESP_RETURN_ON_ERROR(esp_board_periph_ref_handle(peripheral_name, (void **)&i2c_master_handle), + TAG, "Failed to reference board I2C bus '%s'", peripheral_name); + *peripheral_ref_held = true; + + esp_err_t err = esp_board_periph_get_config(peripheral_name, (void **)&i2c_master_cfg); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to get board I2C config '%s': %s", peripheral_name, esp_err_to_name(err)); + esp_board_periph_unref_handle(peripheral_name); + *peripheral_ref_held = false; + return err; + } + + const i2c_config_t i2c_bus_cfg = { + .mode = I2C_MODE_MASTER, + .sda_io_num = i2c_master_cfg->sda_io_num, + .scl_io_num = i2c_master_cfg->scl_io_num, + .sda_pullup_en = i2c_master_cfg->flags.enable_internal_pullup, + .scl_pullup_en = i2c_master_cfg->flags.enable_internal_pullup, + .master.clk_speed = (uint32_t)frequency, + .clk_flags = 0, + }; + + (void)i2c_master_handle; + *i2c_bus_handle = i2c_bus_create(i2c_master_cfg->i2c_port, &i2c_bus_cfg); + if (*i2c_bus_handle == NULL) { + esp_board_periph_unref_handle(peripheral_name); + *peripheral_ref_held = false; + return ESP_FAIL; + } + + return ESP_OK; +} + +static esp_err_t lua_module_imu_apply_default_runtime_config(bmi270_handle_t sensor_handle) +{ + int8_t rslt; + uint8_t sens_list[2] = { BMI2_ACCEL, BMI2_GYRO }; + struct bmi2_sens_config config[2] = { 0 }; + + config[BMI2_ACCEL].type = BMI2_ACCEL; + config[BMI2_GYRO].type = BMI2_GYRO; + + rslt = bmi2_set_adv_power_save(BMI2_DISABLE, sensor_handle); + if (rslt != BMI2_OK) { + ESP_LOGE(TAG, "Failed to disable BMI270 APS mode: %d", rslt); + return ESP_FAIL; + } + + rslt = bmi2_get_sensor_config(config, 2, sensor_handle); + if (rslt != BMI2_OK) { + ESP_LOGE(TAG, "Failed to get BMI270 sensor config: %d", rslt); + return ESP_FAIL; + } + + config[BMI2_ACCEL].cfg.acc.odr = BMI2_ACC_ODR_200HZ; + config[BMI2_ACCEL].cfg.acc.range = BMI2_ACC_RANGE_16G; + config[BMI2_ACCEL].cfg.acc.bwp = BMI2_ACC_NORMAL_AVG4; + config[BMI2_ACCEL].cfg.acc.filter_perf = BMI2_PERF_OPT_MODE; + + config[BMI2_GYRO].cfg.gyr.odr = BMI2_GYR_ODR_200HZ; + config[BMI2_GYRO].cfg.gyr.range = BMI2_GYR_RANGE_2000; + config[BMI2_GYRO].cfg.gyr.bwp = BMI2_GYR_NORMAL_MODE; + config[BMI2_GYRO].cfg.gyr.noise_perf = BMI2_PERF_OPT_MODE; + config[BMI2_GYRO].cfg.gyr.filter_perf = BMI2_PERF_OPT_MODE; + + rslt = bmi2_set_sensor_config(config, 2, sensor_handle); + if (rslt != BMI2_OK) { + ESP_LOGE(TAG, "Failed to set BMI270 sensor config: %d", rslt); + return ESP_FAIL; + } + + rslt = bmi270_sensor_enable(sens_list, 2, sensor_handle); + if (rslt != BMI2_OK) { + ESP_LOGE(TAG, "Failed to enable BMI270 accel/gyro: %d", rslt); + return ESP_FAIL; + } + + return ESP_OK; +} + +static esp_err_t lua_module_imu_create_handle(const lua_imu_resolved_cfg_t *cfg, + lua_module_imu_handle_t **out_handle) +{ + lua_module_imu_handle_t *handle = calloc(1, sizeof(lua_module_imu_handle_t)); + if (handle == NULL) { + return ESP_ERR_NO_MEM; + } + + snprintf(handle->peripheral_name, sizeof(handle->peripheral_name), "%s", cfg->peripheral_name); + handle->int_gpio_num = (gpio_num_t)cfg->int_gpio_num; + + esp_err_t err = lua_module_imu_configure_interrupt_pin(cfg->int_gpio_num); + if (err != ESP_OK) { + free(handle); + return err; + } + + err = lua_module_imu_open_i2c_bus(cfg->peripheral_name, cfg->frequency, + &handle->i2c_bus_handle, &handle->peripheral_ref_held); + if (err != ESP_OK) { + free(handle); + return err; + } + + err = bmi270_sensor_create(handle->i2c_bus_handle, &handle->sensor_handle, + bmi270_config_file, BMI2_GYRO_CROSS_SENS_ENABLE | BMI2_CRT_RTOSK_ENABLE); + if (err != ESP_OK || handle->sensor_handle == NULL) { + ESP_LOGE(TAG, "Failed to create BMI270 sensor: %s", esp_err_to_name(err)); + if (handle->peripheral_ref_held) { + esp_board_periph_unref_handle(handle->peripheral_name); + } + free(handle); + return err != ESP_OK ? err : ESP_FAIL; + } + + struct bmi2_int_pin_config int_pin_cfg = { 0 }; + int_pin_cfg.pin_type = BMI2_INT1; + int_pin_cfg.pin_cfg[0].input_en = BMI2_INT_INPUT_DISABLE; + int_pin_cfg.pin_cfg[0].lvl = BMI2_INT_ACTIVE_HIGH; + int_pin_cfg.pin_cfg[0].od = BMI2_INT_PUSH_PULL; + int_pin_cfg.pin_cfg[0].output_en = BMI2_INT_OUTPUT_ENABLE; + int_pin_cfg.int_latch = BMI2_INT_NON_LATCH; + + int8_t rslt = bmi2_set_int_pin_config(&int_pin_cfg, handle->sensor_handle); + if (rslt != BMI2_OK) { + ESP_LOGE(TAG, "Failed to configure BMI270 INT1 pin: %d", rslt); + bmi270_sensor_del(&handle->sensor_handle); + if (handle->peripheral_ref_held) { + esp_board_periph_unref_handle(handle->peripheral_name); + } + free(handle); + return ESP_FAIL; + } + + err = lua_module_imu_apply_default_runtime_config(handle->sensor_handle); + if (err != ESP_OK) { + bmi270_sensor_del(&handle->sensor_handle); + if (handle->peripheral_ref_held) { + esp_board_periph_unref_handle(handle->peripheral_name); + } + free(handle); + return err; + } + + *out_handle = handle; + ESP_LOGI(TAG, "BMI270 IMU initialized on %s, INT GPIO%d, addr 0x%02x, freq %d Hz", + cfg->peripheral_name, cfg->int_gpio_num, cfg->i2c_addr, cfg->frequency); + return ESP_OK; +} + +static void lua_module_imu_destroy_handle(lua_module_imu_handle_t *handle) +{ + if (handle == NULL) { + return; + } + if (handle->sensor_handle != NULL) { + bmi270_sensor_del(&handle->sensor_handle); + } + if (handle->int_gpio_num >= 0) { + gpio_reset_pin(handle->int_gpio_num); + } + if (handle->peripheral_ref_held && handle->peripheral_name[0] != '\0') { + esp_board_periph_unref_handle(handle->peripheral_name); + } + free(handle); +} + +#endif // CONFIG_LUA_MODULE_IMU_CHIP_BMI270 + +static lua_module_imu_ud_t *lua_module_imu_get_ud(lua_State *L, int idx) +{ + lua_module_imu_ud_t *ud = + (lua_module_imu_ud_t *)luaL_checkudata(L, idx, LUA_MODULE_IMU_METATABLE); + if (!ud || !ud->handle || !ud->handle->sensor_handle) { + luaL_error(L, "imu: invalid or closed handle"); + } + return ud; +} + +static void lua_module_imu_push_axes_table(lua_State *L, const struct bmi2_sens_axes_data *axes) +{ + lua_newtable(L); + lua_pushinteger(L, axes->x); + lua_setfield(L, -2, "x"); + lua_pushinteger(L, axes->y); + lua_setfield(L, -2, "y"); + lua_pushinteger(L, axes->z); + lua_setfield(L, -2, "z"); +} + +static int lua_module_imu_close_impl(lua_State *L, lua_module_imu_ud_t *ud) +{ + (void)L; + if (ud->handle != NULL) { + lua_module_imu_destroy_handle(ud->handle); + ud->handle = NULL; + } + ud->device_name[0] = '\0'; + return 0; +} + +static int lua_module_imu_gc(lua_State *L) +{ + lua_module_imu_ud_t *ud = + (lua_module_imu_ud_t *)luaL_testudata(L, 1, LUA_MODULE_IMU_METATABLE); + if (ud && ud->handle) { + return lua_module_imu_close_impl(L, ud); + } + return 0; +} + +static int lua_module_imu_close(lua_State *L) +{ + lua_module_imu_ud_t *ud = + (lua_module_imu_ud_t *)luaL_checkudata(L, 1, LUA_MODULE_IMU_METATABLE); + if (ud->handle) { + return lua_module_imu_close_impl(L, ud); + } + return 0; +} + +static int lua_module_imu_name(lua_State *L) +{ + lua_module_imu_ud_t *ud = lua_module_imu_get_ud(L, 1); + lua_pushstring(L, ud->device_name); + return 1; +} + +static int lua_module_imu_read(lua_State *L) +{ + lua_module_imu_ud_t *ud = lua_module_imu_get_ud(L, 1); + struct bmi2_sens_data data = { 0 }; + int8_t rslt = bmi2_get_sensor_data(&data, ud->handle->sensor_handle); + + if (rslt != BMI2_OK) { + return luaL_error(L, "imu read failed: %d", rslt); + } + + lua_newtable(L); + lua_module_imu_push_axes_table(L, &data.acc); + lua_setfield(L, -2, "accel"); + lua_module_imu_push_axes_table(L, &data.gyr); + lua_setfield(L, -2, "gyro"); + lua_pushinteger(L, data.sens_time); + lua_setfield(L, -2, "sens_time"); + lua_pushinteger(L, data.status); + lua_setfield(L, -2, "status"); + return 1; +} + +static int lua_module_imu_read_temperature(lua_State *L) +{ + lua_module_imu_ud_t *ud = lua_module_imu_get_ud(L, 1); + int16_t temp = 0; + int8_t rslt = bmi2_get_temperature_data(&temp, ud->handle->sensor_handle); + + if (rslt != BMI2_OK) { + return luaL_error(L, "imu read_temperature failed: %d", rslt); + } + + lua_pushinteger(L, temp); + return 1; +} + +static int lua_module_imu_read_int_status(lua_State *L) +{ + lua_module_imu_ud_t *ud = lua_module_imu_get_ud(L, 1); + uint16_t int_status = 0; + int8_t rslt = bmi2_get_int_status(&int_status, ud->handle->sensor_handle); + + if (rslt != BMI2_OK) { + return luaL_error(L, "imu read_int_status failed: %d", rslt); + } + + lua_pushinteger(L, int_status); + return 1; +} + +/* + * Pull defaults for the named device from the board manager (parsed from + * board_devices.yaml). Returns ESP_OK if the device exists; ESP_ERR_NOT_FOUND + * if the active board does not declare this device. In both cases `out` + * is left in a defined state and the caller can still override fields + * from Lua opts. + */ +static esp_err_t lua_module_imu_load_board_defaults(const char *device_name, + lua_imu_resolved_cfg_t *out) +{ + void *raw = NULL; + esp_err_t err = esp_board_manager_get_device_config(device_name, &raw); + if (err != ESP_OK || raw == NULL) { + return ESP_ERR_NOT_FOUND; + } + + const lua_imu_board_cfg_t *board = (const lua_imu_board_cfg_t *)raw; + + if (board->chip != NULL && strcmp(board->chip, "bmi270") != 0) { + ESP_LOGW(TAG, "Board device '%s' chip='%s' does not match bmi270 backend", + device_name, board->chip); + } + + if (board->peripheral_name != NULL && board->peripheral_name[0] != '\0') { + snprintf(out->peripheral_name, sizeof(out->peripheral_name), "%s", board->peripheral_name); + out->has_peripheral = true; + } + if (board->i2c_addr != 0) { + out->i2c_addr = board->i2c_addr; + out->has_i2c_addr = true; + } + if (board->frequency > 0) { + out->frequency = board->frequency; + out->has_frequency = true; + } + if (board->int_gpio_num >= 0) { + out->int_gpio_num = board->int_gpio_num; + out->has_int_gpio = true; + } + + return ESP_OK; +} + +static void lua_module_imu_apply_lua_overrides(lua_State *L, int opts_idx, + lua_imu_resolved_cfg_t *cfg) +{ + if (opts_idx == 0 || lua_type(L, opts_idx) != LUA_TTABLE) { + return; + } + + lua_getfield(L, opts_idx, "peripheral"); + if (lua_isstring(L, -1)) { + const char *p = lua_tostring(L, -1); + snprintf(cfg->peripheral_name, sizeof(cfg->peripheral_name), "%s", p); + cfg->has_peripheral = true; + } + lua_pop(L, 1); + + lua_getfield(L, opts_idx, "i2c_addr"); + if (lua_isnumber(L, -1)) { + cfg->i2c_addr = (int)lua_tointeger(L, -1); + cfg->has_i2c_addr = true; + } + lua_pop(L, 1); + + lua_getfield(L, opts_idx, "frequency"); + if (lua_isnumber(L, -1)) { + cfg->frequency = (int)lua_tointeger(L, -1); + cfg->has_frequency = true; + } + lua_pop(L, 1); + + lua_getfield(L, opts_idx, "int_gpio"); + if (lua_isnumber(L, -1)) { + cfg->int_gpio_num = (int)lua_tointeger(L, -1); + cfg->has_int_gpio = true; + } + lua_pop(L, 1); +} + +static int lua_module_imu_new(lua_State *L) +{ + const char *device_name = LUA_MODULE_IMU_DEFAULT_NAME; + int opts_idx = 0; + + /* + * Accepted call shapes: + * imu.new() + * imu.new("imu_sensor") -- device name only + * imu.new({ ... }) -- opts only, default device name + * imu.new("imu_sensor", { ... }) -- device name + opts + */ + if (lua_isstring(L, 1)) { + device_name = lua_tostring(L, 1); + if (lua_istable(L, 2)) { + opts_idx = 2; + } + } else if (lua_istable(L, 1)) { + opts_idx = 1; + lua_getfield(L, 1, "device"); + if (lua_isstring(L, -1)) { + device_name = lua_tostring(L, -1); + } + lua_pop(L, 1); + } + + if (strlen(device_name) >= LUA_MODULE_IMU_MAX_NAME_LEN) { + return luaL_error(L, "imu device name too long"); + } + + lua_imu_resolved_cfg_t cfg = { 0 }; + cfg.int_gpio_num = -1; + + /* Defaults from board manager (if device declared on this board). Falls + * back to the legacy "bmi270_sensor" name when the user did not request + * a specific device, mirroring the previous behaviour. */ + esp_err_t err = lua_module_imu_load_board_defaults(device_name, &cfg); + const char *opened_device_name = device_name; + if (err != ESP_OK && strcmp(device_name, LUA_MODULE_IMU_DEFAULT_NAME) == 0) { + if (lua_module_imu_load_board_defaults(LUA_MODULE_IMU_LEGACY_NAME, &cfg) == ESP_OK) { + opened_device_name = LUA_MODULE_IMU_LEGACY_NAME; + ESP_LOGW(TAG, "Default device '%s' not declared, using legacy '%s'", + LUA_MODULE_IMU_DEFAULT_NAME, LUA_MODULE_IMU_LEGACY_NAME); + } + } + + lua_module_imu_apply_lua_overrides(L, opts_idx, &cfg); + + if (!cfg.has_peripheral) { + return luaL_error(L, "imu.new: missing 'peripheral' (board declares no '%s', " + "and no override given)", device_name); + } + if (!cfg.has_i2c_addr) { + cfg.i2c_addr = BMI270_I2C_ADDRESS; + cfg.has_i2c_addr = true; + } + if (!cfg.has_frequency) { + cfg.frequency = 400000; + cfg.has_frequency = true; + } + if (!cfg.has_int_gpio) { + return luaL_error(L, "imu.new: missing 'int_gpio' (board declares no '%s', " + "and no override given)", device_name); + } + if (cfg.i2c_addr != BMI270_I2C_ADDRESS) { + return luaL_error(L, "imu.new: unsupported BMI270 I2C address 0x%02x", cfg.i2c_addr); + } + + lua_module_imu_handle_t *handle = NULL; + err = lua_module_imu_create_handle(&cfg, &handle); + if (err != ESP_OK || handle == NULL) { + return luaL_error(L, "imu.new failed: %s", + esp_err_to_name(err != ESP_OK ? err : ESP_FAIL)); + } + + lua_module_imu_ud_t *ud = + (lua_module_imu_ud_t *)lua_newuserdata(L, sizeof(*ud)); + memset(ud, 0, sizeof(*ud)); + ud->handle = handle; + snprintf(ud->device_name, sizeof(ud->device_name), "%s", opened_device_name); + + luaL_getmetatable(L, LUA_MODULE_IMU_METATABLE); + lua_setmetatable(L, -2); + return 1; +} + +int luaopen_imu(lua_State *L) +{ + if (luaL_newmetatable(L, LUA_MODULE_IMU_METATABLE)) { + lua_pushcfunction(L, lua_module_imu_gc); + lua_setfield(L, -2, "__gc"); + lua_pushvalue(L, -1); + lua_setfield(L, -2, "__index"); + lua_pushcfunction(L, lua_module_imu_read); + lua_setfield(L, -2, "read"); + lua_pushcfunction(L, lua_module_imu_read_temperature); + lua_setfield(L, -2, "read_temperature"); + lua_pushcfunction(L, lua_module_imu_read_int_status); + lua_setfield(L, -2, "read_int_status"); + lua_pushcfunction(L, lua_module_imu_name); + lua_setfield(L, -2, "name"); + lua_pushcfunction(L, lua_module_imu_close); + lua_setfield(L, -2, "close"); + } + lua_pop(L, 1); + + lua_newtable(L); + lua_pushcfunction(L, lua_module_imu_new); + lua_setfield(L, -2, "new"); + return 1; +} + +esp_err_t lua_module_imu_register(void) +{ + return cap_lua_register_module("imu", luaopen_imu); +} diff --git a/components/lua_modules/lua_module_lcd_touch/CMakeLists.txt b/components/lua_modules/lua_module_lcd_touch/CMakeLists.txt index 16e07d9..0147282 100644 --- a/components/lua_modules/lua_module_lcd_touch/CMakeLists.txt +++ b/components/lua_modules/lua_module_lcd_touch/CMakeLists.txt @@ -6,4 +6,5 @@ idf_component_register( REQUIRES cap_lua esp_lcd_touch + esp_timer ) diff --git a/components/lua_modules/lua_module_lcd_touch/src/lua_module_lcd_touch.c b/components/lua_modules/lua_module_lcd_touch/src/lua_module_lcd_touch.c index 00aa44f..b8746c5 100644 --- a/components/lua_modules/lua_module_lcd_touch/src/lua_module_lcd_touch.c +++ b/components/lua_modules/lua_module_lcd_touch/src/lua_module_lcd_touch.c @@ -11,8 +11,12 @@ #include "cap_lua.h" #include "esp_err.h" #include "esp_lcd_touch.h" +#include "esp_log.h" #include "esp_timer.h" #include "lauxlib.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/semphr.h" static const char *LUA_LCD_TOUCH_STATE_KEY = "esp-claw.lcd_touch_state"; @@ -23,6 +27,78 @@ static esp_lcd_touch_handle_t lua_lcd_touch_check_handle(lua_State *L, int index return (esp_lcd_touch_handle_t)handle; } +/* + * 与 setup_device 的契约 (board 通用): + * - touch_handle->config.interrupt_callback != NULL => 中断驱动模式 + * - touch_handle->config.user_data => SemaphoreHandle_t (二值) + * + * 中断驱动模式下,本模块在首次见到该 handle 时启动一个后台 task,阻塞在 + * 信号量上;ISR give 一次即完成一次 esp_lcd_touch_read_data 把数据写入 + * tp->data 缓存。Lua 端的 read/poll/sync 只消费缓存 (esp_lcd_touch_get_data), + * 不再亲自发 I2C —— "收到中断就读,用不用是 lua 的事"。 + * + * 没挂 interrupt_callback 的板子 (例如 CST816S 走轮询) 维持原有行为不变。 + */ +#define LUA_LCD_TOUCH_MAX_INT_HANDLES 2 +#define LUA_LCD_TOUCH_INT_TASK_STACK 4096 +#define LUA_LCD_TOUCH_INT_TASK_PRIO 5 + +static const char *LUA_LCD_TOUCH_TAG = "lua_lcd_touch"; + +static esp_lcd_touch_handle_t s_int_handles[LUA_LCD_TOUCH_MAX_INT_HANDLES]; +static portMUX_TYPE s_int_handles_mux = portMUX_INITIALIZER_UNLOCKED; + +static void lua_lcd_touch_int_task(void *arg) +{ + esp_lcd_touch_handle_t tp = (esp_lcd_touch_handle_t)arg; + SemaphoreHandle_t sem = (SemaphoreHandle_t)tp->config.user_data; + while (1) { + if (xSemaphoreTake(sem, portMAX_DELAY) == pdTRUE) { + (void)esp_lcd_touch_read_data(tp); + } + } +} + +static void lua_lcd_touch_ensure_int_task(esp_lcd_touch_handle_t tp) +{ + if (tp->config.interrupt_callback == NULL || tp->config.user_data == NULL) { + return; + } + + bool already_started = false; + int free_slot = -1; + portENTER_CRITICAL(&s_int_handles_mux); + for (int i = 0; i < LUA_LCD_TOUCH_MAX_INT_HANDLES; i++) { + if (s_int_handles[i] == tp) { + already_started = true; + break; + } + if (s_int_handles[i] == NULL && free_slot < 0) { + free_slot = i; + } + } + if (!already_started && free_slot >= 0) { + s_int_handles[free_slot] = tp; + } + portEXIT_CRITICAL(&s_int_handles_mux); + + if (already_started) { + return; + } + if (free_slot < 0) { + ESP_LOGE(LUA_LCD_TOUCH_TAG, "no slot for interrupt-driven touch handle %p", tp); + return; + } + + if (xTaskCreate(lua_lcd_touch_int_task, "lcd_touch_int", LUA_LCD_TOUCH_INT_TASK_STACK, + tp, LUA_LCD_TOUCH_INT_TASK_PRIO, NULL) != pdPASS) { + ESP_LOGE(LUA_LCD_TOUCH_TAG, "failed to start interrupt task for handle %p", tp); + portENTER_CRITICAL(&s_int_handles_mux); + s_int_handles[free_slot] = NULL; + portEXIT_CRITICAL(&s_int_handles_mux); + } +} + static esp_err_t lua_lcd_touch_read_raw(esp_lcd_touch_handle_t touch_handle, bool *pressed, uint16_t *x, uint16_t *y) { @@ -34,7 +110,12 @@ static esp_err_t lua_lcd_touch_read_raw(esp_lcd_touch_handle_t touch_handle, return ESP_ERR_INVALID_ARG; } - esp_lcd_touch_read_data(touch_handle); + if (touch_handle->config.interrupt_callback != NULL) { + lua_lcd_touch_ensure_int_task(touch_handle); + } else { + (void)esp_lcd_touch_read_data(touch_handle); + } + err = esp_lcd_touch_get_data(touch_handle, points, &point_count, 1); if (err != ESP_OK) { return err; diff --git a/components/lua_modules/lua_module_touch/CMakeLists.txt b/components/lua_modules/lua_module_touch/CMakeLists.txt new file mode 100644 index 0000000..a540499 --- /dev/null +++ b/components/lua_modules/lua_module_touch/CMakeLists.txt @@ -0,0 +1,12 @@ +idf_component_register( + SRCS + "src/lua_module_touch.c" + INCLUDE_DIRS + "include" + REQUIRES + cap_lua + esp_board_manager + esp_driver_touch_sens +) + +idf_component_set_property(${COMPONENT_NAME} WHOLE_ARCHIVE TRUE) diff --git a/components/lua_modules/lua_module_touch/Kconfig b/components/lua_modules/lua_module_touch/Kconfig new file mode 100644 index 0000000..e92b8d7 --- /dev/null +++ b/components/lua_modules/lua_module_touch/Kconfig @@ -0,0 +1,10 @@ +menu "Lua Touch Module" + + config LUA_MODULE_TOUCH_DEFAULT_THRESHOLD_MILLI + int "Default touch threshold in permille" + default 15 + range 1 500 + help + Relative touch threshold in permille. For example, 15 means 1.5%. + +endmenu diff --git a/components/lua_modules/lua_module_touch/idf_component.yml b/components/lua_modules/lua_module_touch/idf_component.yml new file mode 100644 index 0000000..d4e300f --- /dev/null +++ b/components/lua_modules/lua_module_touch/idf_component.yml @@ -0,0 +1,2 @@ +## IDF Component Manager Manifest File +dependencies: {} diff --git a/components/lua_modules/lua_module_touch/include/lua_module_touch.h b/components/lua_modules/lua_module_touch/include/lua_module_touch.h new file mode 100644 index 0000000..beba237 --- /dev/null +++ b/components/lua_modules/lua_module_touch/include/lua_module_touch.h @@ -0,0 +1,20 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include "esp_err.h" +#include "lua.h" + +#ifdef __cplusplus +extern "C" { +#endif + +int luaopen_touch(lua_State *L); +esp_err_t lua_module_touch_register(void); + +#ifdef __cplusplus +} +#endif diff --git a/components/lua_modules/lua_module_touch/lua_scripts/basic_touch.lua b/components/lua_modules/lua_module_touch/lua_scripts/basic_touch.lua new file mode 100644 index 0000000..32a59fe --- /dev/null +++ b/components/lua_modules/lua_module_touch/lua_scripts/basic_touch.lua @@ -0,0 +1,41 @@ +-- Touch key demo: open board device and print two key states. +local touch = require("touch") +local delay = require("delay") + +local a = type(args) == "table" and args or {} +local DEVICE_NAME = type(a.device) == "string" and a.device or "touch_keys" +local SAMPLE_COUNT = type(a.samples) == "number" and math.floor(a.samples) or 20 +local INTERVAL_MS = type(a.interval_ms) == "number" and math.floor(a.interval_ms) or 200 + +local keys + +local function cleanup() + if keys then + pcall(function() + keys:close() + end) + keys = nil + end +end + +local function run() + keys = touch.new(DEVICE_NAME) + print("[touch] opened " .. keys:name()) + for i = 1, SAMPLE_COUNT do + local sample = keys:read() + for _, key in ipairs(sample.keys) do + print(string.format( + "[touch] #%d key%d gpio=%d pressed=%s smooth=%d benchmark=%d delta=%d threshold=%d", + i, key.index, key.gpio, tostring(key.pressed), + key.smooth, key.benchmark, key.delta, key.threshold + )) + end + delay.delay_ms(INTERVAL_MS) + end +end + +local ok, err = xpcall(run, debug.traceback) +cleanup() +if not ok then + error(err) +end diff --git a/components/lua_modules/lua_module_touch/skills/lua_module_touch.md b/components/lua_modules/lua_module_touch/skills/lua_module_touch.md new file mode 100644 index 0000000..4395045 --- /dev/null +++ b/components/lua_modules/lua_module_touch/skills/lua_module_touch.md @@ -0,0 +1,8 @@ +This skill describes how to use `touch` from Lua. + +Quick rules: +- Import it with `local touch = require("touch")` +- Open the board device with `local keys = touch.new("touch_keys")` +- Call `keys:read()` to get the current touch-key state table +- Call `keys:is_pressed(index)` when you only need one key +- Call `keys:close()` when finished diff --git a/components/lua_modules/lua_module_touch/skills/skills_list.json b/components/lua_modules/lua_module_touch/skills/skills_list.json new file mode 100644 index 0000000..9b16b8a --- /dev/null +++ b/components/lua_modules/lua_module_touch/skills/skills_list.json @@ -0,0 +1,12 @@ +{ + "skills": [ + { + "id": "lua_module_touch", + "file": "lua_module_touch.md", + "summary": "How to read touch key states from Lua.", + "cap_groups": [ + "cap_lua" + ] + } + ] +} diff --git a/components/lua_modules/lua_module_touch/src/lua_module_touch.c b/components/lua_modules/lua_module_touch/src/lua_module_touch.c new file mode 100644 index 0000000..a94e6f0 --- /dev/null +++ b/components/lua_modules/lua_module_touch/src/lua_module_touch.c @@ -0,0 +1,606 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "lua_module_touch.h" + +#include +#include +#include +#include + +#include "cap_lua.h" +#include "driver/touch_sens.h" +#include "esp_board_manager.h" +#include "esp_check.h" +#include "esp_log.h" +#include "lauxlib.h" +#include "sdkconfig.h" +#include "soc/soc_caps.h" +#include "soc/touch_sensor_channel.h" + +#define LUA_MODULE_TOUCH_METATABLE "touch.device" +#define LUA_MODULE_TOUCH_DEFAULT_NAME "touch_keys" +#define LUA_MODULE_TOUCH_MAX_NAME_LEN 64 +#define LUA_MODULE_TOUCH_MAX_KEYS 2 +#define LUA_MODULE_TOUCH_INIT_SCAN_TIMES 3 + +#if SOC_TOUCH_SENSOR_VERSION == 1 +#define LUA_TOUCH_SAMPLE_CFG_DEFAULT() TOUCH_SENSOR_V1_DEFAULT_SAMPLE_CONFIG(5.0, TOUCH_VOLT_LIM_L_0V5, TOUCH_VOLT_LIM_H_1V7) +#define LUA_TOUCH_CHAN_CFG_DEFAULT() { \ + .abs_active_thresh = {1000}, \ + .charge_speed = TOUCH_CHARGE_SPEED_7, \ + .init_charge_volt = TOUCH_INIT_CHARGE_VOLT_DEFAULT, \ + .group = TOUCH_CHAN_TRIG_GROUP_BOTH, \ +} +#elif SOC_TOUCH_SENSOR_VERSION == 2 +#define LUA_TOUCH_SAMPLE_CFG_DEFAULT() TOUCH_SENSOR_V2_DEFAULT_SAMPLE_CONFIG(500, TOUCH_VOLT_LIM_L_0V5, TOUCH_VOLT_LIM_H_2V2) +#define LUA_TOUCH_CHAN_CFG_DEFAULT() { \ + .active_thresh = {2000}, \ + .charge_speed = TOUCH_CHARGE_SPEED_7, \ + .init_charge_volt = TOUCH_INIT_CHARGE_VOLT_DEFAULT, \ +} +#elif SOC_TOUCH_SENSOR_VERSION == 3 +#define LUA_TOUCH_SAMPLE_CFG_DEFAULT() TOUCH_SENSOR_V3_DEFAULT_SAMPLE_CONFIG2(3, 29, 8, 3), \ + TOUCH_SENSOR_V3_DEFAULT_SAMPLE_CONFIG2(2, 88, 31, 7), \ + TOUCH_SENSOR_V3_DEFAULT_SAMPLE_CONFIG2(3, 10, 31, 7) +#define LUA_TOUCH_CHAN_CFG_DEFAULT() { \ + .active_thresh = {1000, 2500, 5000}, \ +} +#else +#error "Unsupported touch sensor version" +#endif + +typedef struct { + touch_sensor_handle_t sens_handle; + touch_channel_handle_t chan_handle[LUA_MODULE_TOUCH_MAX_KEYS]; + int channel_id[LUA_MODULE_TOUCH_MAX_KEYS]; + int gpio_num[LUA_MODULE_TOUCH_MAX_KEYS]; + uint32_t threshold[LUA_MODULE_TOUCH_MAX_KEYS]; + int key_count; + bool enabled; + bool scanning; +} lua_module_touch_handle_t; + +typedef struct { + lua_module_touch_handle_t *handle; + char device_name[LUA_MODULE_TOUCH_MAX_NAME_LEN]; +} lua_module_touch_ud_t; + +/* + * Local mirror of the dev_custom_touch_keys_config_t struct generated by the + * ESP Board Manager from board_devices.yaml. Keep this in sync with the + * generator output so custom touch device defaults are read with the correct + * field offsets on every board. + */ +typedef struct { + const char *name; + const char *type; + const char *chip; + int8_t key1_gpio_num; + int8_t key2_gpio_num; + int8_t threshold_milli; +} lua_touch_board_cfg_t; + +typedef struct { + int gpio_num[LUA_MODULE_TOUCH_MAX_KEYS]; + int threshold_milli; + int key_count; + bool has_threshold; +} lua_touch_resolved_cfg_t; + +static const char *TAG = "lua_module_touch"; + +static int lua_module_touch_gpio_to_channel(int gpio_num) +{ + switch (gpio_num) { +#ifdef TOUCH_PAD_GPIO1_CHANNEL + case 1: return TOUCH_PAD_GPIO1_CHANNEL; +#endif +#ifdef TOUCH_PAD_GPIO2_CHANNEL + case 2: return TOUCH_PAD_GPIO2_CHANNEL; +#endif +#ifdef TOUCH_PAD_GPIO3_CHANNEL + case 3: return TOUCH_PAD_GPIO3_CHANNEL; +#endif +#ifdef TOUCH_PAD_GPIO4_CHANNEL + case 4: return TOUCH_PAD_GPIO4_CHANNEL; +#endif +#ifdef TOUCH_PAD_GPIO5_CHANNEL + case 5: return TOUCH_PAD_GPIO5_CHANNEL; +#endif +#ifdef TOUCH_PAD_GPIO6_CHANNEL + case 6: return TOUCH_PAD_GPIO6_CHANNEL; +#endif +#ifdef TOUCH_PAD_GPIO7_CHANNEL + case 7: return TOUCH_PAD_GPIO7_CHANNEL; +#endif +#ifdef TOUCH_PAD_GPIO8_CHANNEL + case 8: return TOUCH_PAD_GPIO8_CHANNEL; +#endif +#ifdef TOUCH_PAD_GPIO9_CHANNEL + case 9: return TOUCH_PAD_GPIO9_CHANNEL; +#endif +#ifdef TOUCH_PAD_GPIO10_CHANNEL + case 10: return TOUCH_PAD_GPIO10_CHANNEL; +#endif +#ifdef TOUCH_PAD_GPIO11_CHANNEL + case 11: return TOUCH_PAD_GPIO11_CHANNEL; +#endif +#ifdef TOUCH_PAD_GPIO12_CHANNEL + case 12: return TOUCH_PAD_GPIO12_CHANNEL; +#endif +#ifdef TOUCH_PAD_GPIO13_CHANNEL + case 13: return TOUCH_PAD_GPIO13_CHANNEL; +#endif +#ifdef TOUCH_PAD_GPIO14_CHANNEL + case 14: return TOUCH_PAD_GPIO14_CHANNEL; +#endif + default: + return -1; + } +} + +static lua_module_touch_ud_t *lua_module_touch_get_ud(lua_State *L, int idx) +{ + lua_module_touch_ud_t *ud = + (lua_module_touch_ud_t *)luaL_checkudata(L, idx, LUA_MODULE_TOUCH_METATABLE); + if (!ud || !ud->handle || !ud->handle->sens_handle) { + luaL_error(L, "touch: invalid or closed handle"); + } + return ud; +} + +static esp_err_t lua_module_touch_apply_thresholds(lua_module_touch_handle_t *handle, int threshold_milli) +{ + for (int i = 0; i < handle->key_count; i++) { + uint32_t benchmark[TOUCH_SAMPLE_CFG_NUM] = {}; + touch_channel_config_t chan_cfg = LUA_TOUCH_CHAN_CFG_DEFAULT(); + +#if SOC_TOUCH_SUPPORT_BENCHMARK + ESP_RETURN_ON_ERROR(touch_channel_read_data(handle->chan_handle[i], + TOUCH_CHAN_DATA_TYPE_BENCHMARK, + benchmark), + TAG, "Failed to read touch benchmark"); +#else + ESP_RETURN_ON_ERROR(touch_channel_read_data(handle->chan_handle[i], + TOUCH_CHAN_DATA_TYPE_SMOOTH, + benchmark), + TAG, "Failed to read touch smooth data"); +#endif + + for (int j = 0; j < TOUCH_SAMPLE_CFG_NUM; j++) { +#if SOC_TOUCH_SENSOR_VERSION == 1 + chan_cfg.abs_active_thresh[j] = (uint32_t)(benchmark[j] * (1000 - threshold_milli) / 1000); + handle->threshold[i] = chan_cfg.abs_active_thresh[0]; +#else + chan_cfg.active_thresh[j] = (uint32_t)(benchmark[j] * threshold_milli / 1000); + handle->threshold[i] = chan_cfg.active_thresh[0]; +#endif + } + + ESP_RETURN_ON_ERROR(touch_sensor_reconfig_channel(handle->chan_handle[i], &chan_cfg), + TAG, "Failed to update touch threshold"); + } + + return ESP_OK; +} + +static esp_err_t lua_module_touch_initial_scan(lua_module_touch_handle_t *handle, int threshold_milli) +{ + ESP_RETURN_ON_ERROR(touch_sensor_enable(handle->sens_handle), TAG, "Failed to enable touch sensor"); + handle->enabled = true; + + for (int i = 0; i < LUA_MODULE_TOUCH_INIT_SCAN_TIMES; i++) { + ESP_RETURN_ON_ERROR(touch_sensor_trigger_oneshot_scanning(handle->sens_handle, 2000), + TAG, "Failed to perform initial touch scan"); + } + + ESP_RETURN_ON_ERROR(touch_sensor_disable(handle->sens_handle), TAG, "Failed to disable touch sensor"); + handle->enabled = false; + + ESP_RETURN_ON_ERROR(lua_module_touch_apply_thresholds(handle, threshold_milli), + TAG, "Failed to apply touch thresholds"); + + return ESP_OK; +} + +static void lua_module_touch_destroy_handle(lua_module_touch_handle_t *handle) +{ + if (handle == NULL) { + return; + } + + if (handle->scanning) { + (void)touch_sensor_stop_continuous_scanning(handle->sens_handle); + handle->scanning = false; + } + if (handle->enabled) { + (void)touch_sensor_disable(handle->sens_handle); + handle->enabled = false; + } + for (int i = 0; i < handle->key_count; i++) { + if (handle->chan_handle[i] != NULL) { + (void)touch_sensor_del_channel(handle->chan_handle[i]); + handle->chan_handle[i] = NULL; + } + } + if (handle->sens_handle != NULL) { + (void)touch_sensor_del_controller(handle->sens_handle); + handle->sens_handle = NULL; + } + free(handle); +} + +static esp_err_t lua_module_touch_create_handle(const lua_touch_resolved_cfg_t *cfg, + lua_module_touch_handle_t **out_handle) +{ + touch_sensor_sample_config_t sample_cfg[TOUCH_SAMPLE_CFG_NUM] = { + LUA_TOUCH_SAMPLE_CFG_DEFAULT() + }; + touch_sensor_config_t sens_cfg = TOUCH_SENSOR_DEFAULT_BASIC_CONFIG(TOUCH_SAMPLE_CFG_NUM, sample_cfg); + touch_sensor_filter_config_t filter_cfg = TOUCH_SENSOR_DEFAULT_FILTER_CONFIG(); + touch_channel_config_t chan_cfg = LUA_TOUCH_CHAN_CFG_DEFAULT(); + lua_module_touch_handle_t *handle = calloc(1, sizeof(lua_module_touch_handle_t)); + + if (handle == NULL) { + return ESP_ERR_NO_MEM; + } + + handle->key_count = cfg->key_count; + memcpy(handle->gpio_num, cfg->gpio_num, sizeof(handle->gpio_num)); + + esp_err_t err = touch_sensor_new_controller(&sens_cfg, &handle->sens_handle); + if (err != ESP_OK) { + free(handle); + return err; + } + + for (int i = 0; i < cfg->key_count; i++) { + handle->channel_id[i] = lua_module_touch_gpio_to_channel(cfg->gpio_num[i]); + if (handle->channel_id[i] < 0) { + err = ESP_ERR_INVALID_ARG; + ESP_LOGE(TAG, "GPIO%d is not a valid touch channel", cfg->gpio_num[i]); + goto fail; + } + + err = touch_sensor_new_channel(handle->sens_handle, handle->channel_id[i], + &chan_cfg, &handle->chan_handle[i]); + if (err != ESP_OK) { + goto fail; + } + } + + err = touch_sensor_config_filter(handle->sens_handle, &filter_cfg); + if (err != ESP_OK) { + goto fail; + } + + err = lua_module_touch_initial_scan(handle, cfg->threshold_milli); + if (err != ESP_OK) { + goto fail; + } + + err = touch_sensor_enable(handle->sens_handle); + if (err != ESP_OK) { + goto fail; + } + handle->enabled = true; + + err = touch_sensor_start_continuous_scanning(handle->sens_handle); + if (err != ESP_OK) { + goto fail; + } + handle->scanning = true; + + *out_handle = handle; + ESP_LOGI(TAG, "Touch keys initialized on GPIO%d/GPIO%d with threshold=%d permille", + cfg->gpio_num[0], + cfg->key_count > 1 ? cfg->gpio_num[1] : -1, + cfg->threshold_milli); + return ESP_OK; + +fail: + lua_module_touch_destroy_handle(handle); + return err; +} + +static esp_err_t lua_module_touch_read_one(lua_module_touch_handle_t *handle, int index, + bool *pressed, uint32_t *smooth, + uint32_t *benchmark, int32_t *delta) +{ + uint32_t smooth_data[TOUCH_SAMPLE_CFG_NUM] = {}; + uint32_t benchmark_data[TOUCH_SAMPLE_CFG_NUM] = {}; + + ESP_RETURN_ON_FALSE(handle != NULL && index >= 0 && index < handle->key_count, + ESP_ERR_INVALID_ARG, TAG, "Invalid touch key index"); + + ESP_RETURN_ON_ERROR(touch_channel_read_data(handle->chan_handle[index], + TOUCH_CHAN_DATA_TYPE_SMOOTH, + smooth_data), + TAG, "Failed to read touch smooth data"); + +#if SOC_TOUCH_SUPPORT_BENCHMARK + ESP_RETURN_ON_ERROR(touch_channel_read_data(handle->chan_handle[index], + TOUCH_CHAN_DATA_TYPE_BENCHMARK, + benchmark_data), + TAG, "Failed to read touch benchmark"); +#else + benchmark_data[0] = handle->threshold[index]; +#endif + + *smooth = smooth_data[0]; + *benchmark = benchmark_data[0]; + +#if SOC_TOUCH_SENSOR_VERSION == 1 + *delta = (int32_t)benchmark_data[0] - (int32_t)smooth_data[0]; + *pressed = smooth_data[0] <= handle->threshold[index]; +#else + *delta = (int32_t)smooth_data[0] - (int32_t)benchmark_data[0]; + *pressed = *delta >= (int32_t)handle->threshold[index]; +#endif + + return ESP_OK; +} + +static esp_err_t lua_module_touch_load_board_defaults(const char *device_name, + lua_touch_resolved_cfg_t *out) +{ + void *raw = NULL; + esp_err_t err = esp_board_manager_get_device_config(device_name, &raw); + if (err != ESP_OK || raw == NULL) { + return ESP_ERR_NOT_FOUND; + } + + const lua_touch_board_cfg_t *board = (const lua_touch_board_cfg_t *)raw; + + if (board->key1_gpio_num > 0) { + out->gpio_num[out->key_count++] = board->key1_gpio_num; + } + if (board->key2_gpio_num > 0 && out->key_count < LUA_MODULE_TOUCH_MAX_KEYS) { + out->gpio_num[out->key_count++] = board->key2_gpio_num; + } + if (board->threshold_milli > 0) { + out->threshold_milli = board->threshold_milli; + out->has_threshold = true; + } + + return ESP_OK; +} + +static void lua_module_touch_apply_lua_overrides(lua_State *L, int opts_idx, + lua_touch_resolved_cfg_t *cfg) +{ + if (opts_idx == 0 || lua_type(L, opts_idx) != LUA_TTABLE) { + return; + } + + lua_getfield(L, opts_idx, "key1_gpio"); + if (lua_isnumber(L, -1)) { + cfg->gpio_num[0] = (int)lua_tointeger(L, -1); + if (cfg->key_count < 1) { + cfg->key_count = 1; + } + } + lua_pop(L, 1); + + lua_getfield(L, opts_idx, "key2_gpio"); + if (lua_isnumber(L, -1)) { + cfg->gpio_num[1] = (int)lua_tointeger(L, -1); + if (cfg->key_count < 2) { + cfg->key_count = 2; + } + } + lua_pop(L, 1); + + lua_getfield(L, opts_idx, "threshold_milli"); + if (lua_isnumber(L, -1)) { + cfg->threshold_milli = (int)lua_tointeger(L, -1); + cfg->has_threshold = true; + } + lua_pop(L, 1); +} + +static int lua_module_touch_close_impl(lua_State *L, lua_module_touch_ud_t *ud) +{ + (void)L; + if (ud->handle != NULL) { + lua_module_touch_destroy_handle(ud->handle); + ud->handle = NULL; + } + ud->device_name[0] = '\0'; + return 0; +} + +static int lua_module_touch_gc(lua_State *L) +{ + lua_module_touch_ud_t *ud = + (lua_module_touch_ud_t *)luaL_testudata(L, 1, LUA_MODULE_TOUCH_METATABLE); + if (ud && ud->handle) { + return lua_module_touch_close_impl(L, ud); + } + return 0; +} + +static int lua_module_touch_close(lua_State *L) +{ + lua_module_touch_ud_t *ud = + (lua_module_touch_ud_t *)luaL_checkudata(L, 1, LUA_MODULE_TOUCH_METATABLE); + if (ud->handle) { + return lua_module_touch_close_impl(L, ud); + } + return 0; +} + +static int lua_module_touch_name(lua_State *L) +{ + lua_module_touch_ud_t *ud = lua_module_touch_get_ud(L, 1); + lua_pushstring(L, ud->device_name); + return 1; +} + +static void lua_module_touch_push_key_table(lua_State *L, int index, int channel_id, int gpio_num, + bool pressed, uint32_t smooth, + uint32_t benchmark, int32_t delta, + uint32_t threshold) +{ + lua_newtable(L); + lua_pushinteger(L, index + 1); + lua_setfield(L, -2, "index"); + lua_pushinteger(L, channel_id); + lua_setfield(L, -2, "channel"); + lua_pushinteger(L, gpio_num); + lua_setfield(L, -2, "gpio"); + lua_pushboolean(L, pressed); + lua_setfield(L, -2, "pressed"); + lua_pushinteger(L, smooth); + lua_setfield(L, -2, "smooth"); + lua_pushinteger(L, benchmark); + lua_setfield(L, -2, "benchmark"); + lua_pushinteger(L, delta); + lua_setfield(L, -2, "delta"); + lua_pushinteger(L, threshold); + lua_setfield(L, -2, "threshold"); +} + +static int lua_module_touch_read(lua_State *L) +{ + lua_module_touch_ud_t *ud = lua_module_touch_get_ud(L, 1); + int pressed_count = 0; + + lua_newtable(L); + lua_newtable(L); + + for (int i = 0; i < ud->handle->key_count; i++) { + bool pressed = false; + uint32_t smooth = 0; + uint32_t benchmark = 0; + int32_t delta = 0; + esp_err_t err = lua_module_touch_read_one(ud->handle, i, &pressed, &smooth, &benchmark, &delta); + if (err != ESP_OK) { + return luaL_error(L, "touch read failed: %s", esp_err_to_name(err)); + } + if (pressed) { + pressed_count++; + } + + lua_module_touch_push_key_table(L, i, ud->handle->channel_id[i], ud->handle->gpio_num[i], + pressed, smooth, benchmark, delta, ud->handle->threshold[i]); + lua_rawseti(L, -2, i + 1); + } + + lua_setfield(L, -2, "keys"); + lua_pushinteger(L, ud->handle->key_count); + lua_setfield(L, -2, "count"); + lua_pushboolean(L, pressed_count > 0); + lua_setfield(L, -2, "any_pressed"); + lua_pushinteger(L, pressed_count); + lua_setfield(L, -2, "pressed_count"); + return 1; +} + +static int lua_module_touch_is_pressed(lua_State *L) +{ + lua_module_touch_ud_t *ud = lua_module_touch_get_ud(L, 1); + int index = (int)luaL_checkinteger(L, 2); + bool pressed = false; + uint32_t smooth = 0; + uint32_t benchmark = 0; + int32_t delta = 0; + + luaL_argcheck(L, index >= 1 && index <= ud->handle->key_count, 2, "touch key index out of range"); + + esp_err_t err = lua_module_touch_read_one(ud->handle, index - 1, &pressed, &smooth, &benchmark, &delta); + if (err != ESP_OK) { + return luaL_error(L, "touch is_pressed failed: %s", esp_err_to_name(err)); + } + + lua_pushboolean(L, pressed); + return 1; +} + +static int lua_module_touch_new(lua_State *L) +{ + const char *device_name = LUA_MODULE_TOUCH_DEFAULT_NAME; + int opts_idx = 0; + + if (lua_isstring(L, 1)) { + device_name = lua_tostring(L, 1); + if (lua_istable(L, 2)) { + opts_idx = 2; + } + } else if (lua_istable(L, 1)) { + opts_idx = 1; + lua_getfield(L, 1, "device"); + if (lua_isstring(L, -1)) { + device_name = lua_tostring(L, -1); + } + lua_pop(L, 1); + } + + if (strlen(device_name) >= LUA_MODULE_TOUCH_MAX_NAME_LEN) { + return luaL_error(L, "touch device name too long"); + } + + lua_touch_resolved_cfg_t cfg = { + .threshold_milli = CONFIG_LUA_MODULE_TOUCH_DEFAULT_THRESHOLD_MILLI, + }; + + (void)lua_module_touch_load_board_defaults(device_name, &cfg); + lua_module_touch_apply_lua_overrides(L, opts_idx, &cfg); + + if (cfg.key_count <= 0) { + return luaL_error(L, "touch.new: missing touch GPIOs for device '%s'", device_name); + } + if (cfg.threshold_milli <= 0) { + return luaL_error(L, "touch.new: invalid threshold_milli=%d", cfg.threshold_milli); + } + + lua_module_touch_handle_t *handle = NULL; + esp_err_t err = lua_module_touch_create_handle(&cfg, &handle); + if (err != ESP_OK || handle == NULL) { + return luaL_error(L, "touch.new failed: %s", + esp_err_to_name(err != ESP_OK ? err : ESP_FAIL)); + } + + lua_module_touch_ud_t *ud = + (lua_module_touch_ud_t *)lua_newuserdata(L, sizeof(*ud)); + memset(ud, 0, sizeof(*ud)); + ud->handle = handle; + snprintf(ud->device_name, sizeof(ud->device_name), "%s", device_name); + + luaL_getmetatable(L, LUA_MODULE_TOUCH_METATABLE); + lua_setmetatable(L, -2); + return 1; +} + +int luaopen_touch(lua_State *L) +{ + if (luaL_newmetatable(L, LUA_MODULE_TOUCH_METATABLE)) { + lua_pushcfunction(L, lua_module_touch_gc); + lua_setfield(L, -2, "__gc"); + lua_pushvalue(L, -1); + lua_setfield(L, -2, "__index"); + lua_pushcfunction(L, lua_module_touch_read); + lua_setfield(L, -2, "read"); + lua_pushcfunction(L, lua_module_touch_is_pressed); + lua_setfield(L, -2, "is_pressed"); + lua_pushcfunction(L, lua_module_touch_name); + lua_setfield(L, -2, "name"); + lua_pushcfunction(L, lua_module_touch_close); + lua_setfield(L, -2, "close"); + } + lua_pop(L, 1); + + lua_newtable(L); + lua_pushcfunction(L, lua_module_touch_new); + lua_setfield(L, -2, "new"); + return 1; +} + +esp_err_t lua_module_touch_register(void) +{ + return cap_lua_register_module("touch", luaopen_touch); +}