Merge pull request #127 from IRONICBo/feat/gpio-skill-support

This commit is contained in:
crispyberry
2026-03-13 12:13:47 +08:00
committed by GitHub
9 changed files with 456 additions and 3 deletions

View File

@@ -20,6 +20,8 @@ idf_component_register(
"tools/tool_web_search.c"
"tools/tool_get_time.c"
"tools/tool_files.c"
"tools/tool_gpio.c"
"tools/gpio_policy.c"
"skills/skill_loader.c"
"onboard/wifi_onboard.c"
INCLUDE_DIRS
@@ -27,5 +29,5 @@ idf_component_register(
REQUIRES
nvs_flash esp_wifi esp_netif esp_http_client esp_http_server
esp_https_ota esp_event json spiffs console vfs app_update esp-tls
esp_timer esp_websocket_client
esp_timer esp_websocket_client esp_driver_gpio
)

View File

@@ -46,8 +46,15 @@ esp_err_t context_build_system_prompt(char *buf, size_t size)
"- list_dir: List files, optionally filter by prefix.\n"
"- cron_add: Schedule a recurring or one-shot task. The message will trigger an agent turn when the job fires.\n"
"- cron_list: List all scheduled cron jobs.\n"
"- cron_remove: Remove a scheduled cron job by ID.\n\n"
"- cron_remove: Remove a scheduled cron job by ID.\n"
"- gpio_write: Set a GPIO pin HIGH or LOW. Use for controlling LEDs, relays, and digital outputs.\n"
"- gpio_read: Read a single GPIO pin state (HIGH or LOW). Use for checking switches, buttons, sensors.\n"
"- gpio_read_all: Read all allowed GPIO pins at once. Good for getting a full status overview.\n\n"
"When using cron_add for Telegram delivery, always set channel='telegram' and a valid numeric chat_id.\n\n"
"## GPIO\n"
"You can control hardware GPIO pins on the ESP32-S3. Use gpio_read to check switch/sensor states "
"(digital input confirmation), and gpio_write to control outputs. Pin range is validated by policy — "
"only allowed pins can be accessed. When asked about switch states or digital I/O, use these tools.\n\n"
"Use tools when needed. Provide your final answer as text after using tools.\n\n"
"## Memory\n"
"You have persistent memory stored on local flash:\n"

View File

@@ -117,6 +117,9 @@
#define MIMI_HEARTBEAT_FILE MIMI_SPIFFS_BASE "/HEARTBEAT.md"
#define MIMI_HEARTBEAT_INTERVAL_MS (30 * 60 * 1000)
/* GPIO */
#define MIMI_GPIO_CONFIG_SECTION 1 /* enable GPIO tools */
/* Skills */
#define MIMI_SKILLS_PREFIX MIMI_SPIFFS_BASE "/skills/"

123
main/tools/gpio_policy.c Normal file
View File

@@ -0,0 +1,123 @@
#include "tools/gpio_policy.h"
#include "driver/gpio.h"
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#ifndef GPIO_IS_VALID_GPIO
#define GPIO_IS_VALID_GPIO(pin) ((pin) >= 0)
#endif
static bool pin_in_allowlist(int pin, const char *csv)
{
const char *cursor;
if (!csv || csv[0] == '\0') {
return false;
}
cursor = csv;
while (*cursor != '\0') {
char *endptr = NULL;
long value;
while (*cursor == ' ' || *cursor == '\t' || *cursor == ',') {
cursor++;
}
if (*cursor == '\0') {
break;
}
value = strtol(cursor, &endptr, 10);
if (endptr == cursor) {
while (*cursor != '\0' && *cursor != ',') {
cursor++;
}
continue;
}
if ((int)value == pin) {
return true;
}
cursor = endptr;
}
return false;
}
static bool pin_is_allowed_impl(int pin,
const char *allowlist_csv,
int min_pin,
int max_pin,
bool block_esp32_flash_pins,
bool block_esp32s3_usb_pins)
{
bool in_policy;
if (pin < 0) {
return false;
}
/* Block ESP32 flash/PSRAM pins (GPIO 6-11) */
if (block_esp32_flash_pins && pin >= 6 && pin <= 11) {
return false;
}
/* USB Serial/JTAG uses GPIO19/20 on ESP32-S3 */
if (block_esp32s3_usb_pins && (pin == 19 || pin == 20)) {
return false;
}
if (allowlist_csv && allowlist_csv[0] != '\0') {
in_policy = pin_in_allowlist(pin, allowlist_csv);
} else {
in_policy = pin >= min_pin && pin <= max_pin;
}
if (!in_policy) {
return false;
}
return GPIO_IS_VALID_GPIO((gpio_num_t)pin);
}
bool gpio_policy_pin_is_allowed(int pin)
{
#if defined(CONFIG_IDF_TARGET_ESP32)
return pin_is_allowed_impl(pin, MIMI_GPIO_ALLOWED_CSV,
MIMI_GPIO_MIN_PIN, MIMI_GPIO_MAX_PIN, true, false);
#elif defined(CONFIG_IDF_TARGET_ESP32S3)
return pin_is_allowed_impl(pin, MIMI_GPIO_ALLOWED_CSV,
MIMI_GPIO_MIN_PIN, MIMI_GPIO_MAX_PIN, false, true);
#else
return pin_is_allowed_impl(pin, MIMI_GPIO_ALLOWED_CSV,
MIMI_GPIO_MIN_PIN, MIMI_GPIO_MAX_PIN, false, false);
#endif
}
bool gpio_policy_pin_forbidden_hint(int pin, char *result, size_t result_len)
{
#if defined(CONFIG_IDF_TARGET_ESP32)
if (pin >= 6 && pin <= 11) {
snprintf(result, result_len,
"Error: pin %d is reserved for ESP32 flash/PSRAM (GPIO6-11); choose a different pin",
pin);
return true;
}
#elif defined(CONFIG_IDF_TARGET_ESP32S3)
if (pin == 19 || pin == 20) {
snprintf(result, result_len,
"Error: pin %d is reserved for ESP32-S3 USB Serial/JTAG (GPIO19/20); choose a different pin",
pin);
return true;
}
#else
(void)pin;
(void)result;
(void)result_len;
#endif
return false;
}

22
main/tools/gpio_policy.h Normal file
View File

@@ -0,0 +1,22 @@
#pragma once
#include <stdbool.h>
#include <stddef.h>
/* GPIO defaults for ESP32-S3-LCD-1.47B safe user-accessible pins */
#define MIMI_GPIO_MIN_PIN 1
#define MIMI_GPIO_MAX_PIN 21
#define MIMI_GPIO_ALLOWED_CSV "1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,21,38,46"
/**
* Check if a pin is allowed for user GPIO operations.
* Validates against the allowlist or default range, and blocks
* pins reserved for flash/PSRAM on ESP32.
*/
bool gpio_policy_pin_is_allowed(int pin);
/**
* Write a human-readable hint if the pin is forbidden for a known reason.
* Returns true if a hint was written (and the caller should return the error).
*/
bool gpio_policy_pin_forbidden_hint(int pin, char *result, size_t result_len);

194
main/tools/tool_gpio.c Normal file
View File

@@ -0,0 +1,194 @@
#include "tools/tool_gpio.h"
#include "tools/gpio_policy.h"
#include "mimi_config.h"
#include "driver/gpio.h"
#include "esp_log.h"
#include "cJSON.h"
#include <string.h>
#include <stdio.h>
static const char *TAG = "tool_gpio";
esp_err_t tool_gpio_init(void)
{
ESP_LOGI(TAG, "GPIO tool initialized (pin range %d-%d)",
MIMI_GPIO_MIN_PIN, MIMI_GPIO_MAX_PIN);
return ESP_OK;
}
esp_err_t tool_gpio_write_execute(const char *input_json, char *output, size_t output_size)
{
cJSON *root = cJSON_Parse(input_json);
if (!root) {
snprintf(output, output_size, "Error: invalid JSON input");
return ESP_ERR_INVALID_ARG;
}
cJSON *pin_obj = cJSON_GetObjectItem(root, "pin");
cJSON *state_obj = cJSON_GetObjectItem(root, "state");
if (!cJSON_IsNumber(pin_obj)) {
snprintf(output, output_size, "Error: 'pin' required (integer)");
cJSON_Delete(root);
return ESP_ERR_INVALID_ARG;
}
if (!cJSON_IsNumber(state_obj)) {
snprintf(output, output_size, "Error: 'state' required (0 or 1)");
cJSON_Delete(root);
return ESP_ERR_INVALID_ARG;
}
int pin = (int)pin_obj->valuedouble;
int state = (int)state_obj->valuedouble;
if (!gpio_policy_pin_is_allowed(pin)) {
if (gpio_policy_pin_forbidden_hint(pin, output, output_size)) {
cJSON_Delete(root);
return ESP_ERR_INVALID_ARG;
}
if (MIMI_GPIO_ALLOWED_CSV[0] != '\0') {
snprintf(output, output_size, "Error: pin %d is not in allowed list", pin);
} else {
snprintf(output, output_size, "Error: pin must be %d-%d",
MIMI_GPIO_MIN_PIN, MIMI_GPIO_MAX_PIN);
}
cJSON_Delete(root);
return ESP_ERR_INVALID_ARG;
}
if (gpio_set_direction(pin, GPIO_MODE_INPUT_OUTPUT) != ESP_OK ||
gpio_set_level(pin, state ? 1 : 0) != ESP_OK) {
snprintf(output, output_size, "Error: failed to configure/write pin %d", pin);
cJSON_Delete(root);
return ESP_FAIL;
}
snprintf(output, output_size, "Pin %d set to %s", pin, state ? "HIGH" : "LOW");
ESP_LOGI(TAG, "gpio_write: pin %d -> %s", pin, state ? "HIGH" : "LOW");
cJSON_Delete(root);
return ESP_OK;
}
esp_err_t tool_gpio_read_execute(const char *input_json, char *output, size_t output_size)
{
cJSON *root = cJSON_Parse(input_json);
if (!root) {
snprintf(output, output_size, "Error: invalid JSON input");
return ESP_ERR_INVALID_ARG;
}
cJSON *pin_obj = cJSON_GetObjectItem(root, "pin");
if (!cJSON_IsNumber(pin_obj)) {
snprintf(output, output_size, "Error: 'pin' required (integer)");
cJSON_Delete(root);
return ESP_ERR_INVALID_ARG;
}
int pin = (int)pin_obj->valuedouble;
if (!gpio_policy_pin_is_allowed(pin)) {
if (gpio_policy_pin_forbidden_hint(pin, output, output_size)) {
cJSON_Delete(root);
return ESP_ERR_INVALID_ARG;
}
if (MIMI_GPIO_ALLOWED_CSV[0] != '\0') {
snprintf(output, output_size, "Error: pin %d is not in allowed list", pin);
} else {
snprintf(output, output_size, "Error: pin must be %d-%d",
MIMI_GPIO_MIN_PIN, MIMI_GPIO_MAX_PIN);
}
cJSON_Delete(root);
return ESP_ERR_INVALID_ARG;
}
/* Enable input path, then read level */
gpio_set_direction(pin, GPIO_MODE_INPUT);
int level = gpio_get_level(pin);
snprintf(output, output_size, "Pin %d = %s", pin, level ? "HIGH" : "LOW");
ESP_LOGI(TAG, "gpio_read: pin %d = %s", pin, level ? "HIGH" : "LOW");
cJSON_Delete(root);
return ESP_OK;
}
esp_err_t tool_gpio_read_all_execute(const char *input_json, char *output, size_t output_size)
{
(void)input_json;
char *cursor = output;
size_t remaining = output_size;
int written;
int count = 0;
written = snprintf(cursor, remaining, "GPIO states: ");
if (written < 0 || (size_t)written >= remaining) {
output[0] = '\0';
return ESP_FAIL;
}
cursor += (size_t)written;
remaining -= (size_t)written;
if (MIMI_GPIO_ALLOWED_CSV[0] != '\0') {
/* Iterate over explicit allowlist */
const char *csv_cursor = MIMI_GPIO_ALLOWED_CSV;
while (*csv_cursor != '\0') {
char *endptr = NULL;
long value;
while (*csv_cursor == ' ' || *csv_cursor == '\t' || *csv_cursor == ',') {
csv_cursor++;
}
if (*csv_cursor == '\0') break;
value = strtol(csv_cursor, &endptr, 10);
if (endptr == csv_cursor) {
while (*csv_cursor != '\0' && *csv_cursor != ',') csv_cursor++;
continue;
}
if (!gpio_policy_pin_is_allowed((int)value)) {
csv_cursor = endptr;
continue;
}
gpio_set_direction((int)value, GPIO_MODE_INPUT);
int level = gpio_get_level((int)value);
written = snprintf(cursor, remaining, "%s%d=%s",
count == 0 ? "" : ", ",
(int)value, level ? "HIGH" : "LOW");
if (written < 0 || (size_t)written >= remaining) break;
cursor += (size_t)written;
remaining -= (size_t)written;
count++;
csv_cursor = endptr;
}
} else {
/* Iterate over default range */
for (int pin = MIMI_GPIO_MIN_PIN; pin <= MIMI_GPIO_MAX_PIN; pin++) {
if (!gpio_policy_pin_is_allowed(pin)) continue;
gpio_set_direction(pin, GPIO_MODE_INPUT);
int level = gpio_get_level(pin);
written = snprintf(cursor, remaining, "%s%d=%s",
count == 0 ? "" : ", ",
pin, level ? "HIGH" : "LOW");
if (written < 0 || (size_t)written >= remaining) break;
cursor += (size_t)written;
remaining -= (size_t)written;
count++;
}
}
if (count == 0) {
snprintf(output, output_size, "Error: no allowed GPIO pins configured");
return ESP_FAIL;
}
ESP_LOGI(TAG, "gpio_read_all: %d pins read", count);
return ESP_OK;
}

27
main/tools/tool_gpio.h Normal file
View File

@@ -0,0 +1,27 @@
#pragma once
#include "esp_err.h"
#include <stddef.h>
/**
* Initialize GPIO tool — configure allowed pins and directions.
*/
esp_err_t tool_gpio_init(void);
/**
* Write a GPIO pin HIGH or LOW.
* Input JSON: {"pin": <int>, "state": <0|1>}
*/
esp_err_t tool_gpio_write_execute(const char *input_json, char *output, size_t output_size);
/**
* Read a single GPIO pin state.
* Input JSON: {"pin": <int>}
*/
esp_err_t tool_gpio_read_execute(const char *input_json, char *output, size_t output_size);
/**
* Read all allowed GPIO pin states at once.
* Input JSON: {} (no parameters)
*/
esp_err_t tool_gpio_read_all_execute(const char *input_json, char *output, size_t output_size);

View File

@@ -4,6 +4,7 @@
#include "tools/tool_get_time.h"
#include "tools/tool_files.h"
#include "tools/tool_cron.h"
#include "tools/tool_gpio.h"
#include <string.h>
#include "esp_log.h"
@@ -11,7 +12,7 @@
static const char *TAG = "tools";
#define MAX_TOOLS 12
#define MAX_TOOLS 16
static mimi_tool_t s_tools[MAX_TOOLS];
static int s_tool_count = 0;
@@ -176,6 +177,43 @@ esp_err_t tool_registry_init(void)
};
register_tool(&cr);
/* Register GPIO tools */
tool_gpio_init();
mimi_tool_t gw = {
.name = "gpio_write",
.description = "Set a GPIO pin HIGH or LOW. Controls LEDs, relays, and other digital outputs.",
.input_schema_json =
"{\"type\":\"object\","
"\"properties\":{\"pin\":{\"type\":\"integer\",\"description\":\"GPIO pin number\"},"
"\"state\":{\"type\":\"integer\",\"description\":\"1 for HIGH, 0 for LOW\"}},"
"\"required\":[\"pin\",\"state\"]}",
.execute = tool_gpio_write_execute,
};
register_tool(&gw);
mimi_tool_t gr = {
.name = "gpio_read",
.description = "Read a GPIO pin state. Returns HIGH or LOW. Use for checking switches, sensors, and digital inputs.",
.input_schema_json =
"{\"type\":\"object\","
"\"properties\":{\"pin\":{\"type\":\"integer\",\"description\":\"GPIO pin number\"}},"
"\"required\":[\"pin\"]}",
.execute = tool_gpio_read_execute,
};
register_tool(&gr);
mimi_tool_t ga = {
.name = "gpio_read_all",
.description = "Read all allowed GPIO pin states in a single call. Returns each pin's HIGH/LOW state.",
.input_schema_json =
"{\"type\":\"object\","
"\"properties\":{},"
"\"required\":[]}",
.execute = tool_gpio_read_all_execute,
};
register_tool(&ga);
build_tools_json();
ESP_LOGI(TAG, "Tool registry initialized");