Merge pull request #127 from IRONICBo/feat/gpio-skill-support
This commit is contained in:
@@ -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
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
123
main/tools/gpio_policy.c
Normal 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
22
main/tools/gpio_policy.h
Normal 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
194
main/tools/tool_gpio.c
Normal 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
27
main/tools/tool_gpio.h
Normal 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);
|
||||
@@ -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");
|
||||
|
||||
37
spiffs_data/skills/gpio-control.md
Normal file
37
spiffs_data/skills/gpio-control.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# GPIO Control
|
||||
|
||||
Control and monitor GPIO pins on the ESP32-S3 for digital I/O.
|
||||
|
||||
## When to use
|
||||
When the user asks to:
|
||||
- Turn on/off LEDs, relays, or other outputs
|
||||
- Check switch states, button presses, or sensor readings
|
||||
- Confirm digital I/O status (switch confirmation)
|
||||
- Get an overview of all GPIO pin states
|
||||
|
||||
## How to use
|
||||
1. To **read a switch/sensor**: use gpio_read with the pin number
|
||||
- Returns HIGH (1) or LOW (0)
|
||||
- HIGH typically means switch is ON / circuit closed
|
||||
- LOW typically means switch is OFF / circuit open
|
||||
2. To **set an output**: use gpio_write with pin and state (1=HIGH, 0=LOW)
|
||||
3. To **scan all pins**: use gpio_read_all for a full status overview
|
||||
4. For **switch confirmation**: read the pin, report state, optionally toggle and re-read to verify
|
||||
|
||||
## Pin safety
|
||||
- Only pins within the allowed range can be accessed
|
||||
- ESP32 flash pins (6-11) are always blocked
|
||||
- If a pin is rejected, suggest an alternative within the allowed range
|
||||
|
||||
## Example
|
||||
User: "Check if the switch on pin 4 is on"
|
||||
→ gpio_read {"pin": 4}
|
||||
→ "Pin 4 = HIGH"
|
||||
→ "The switch on pin 4 is currently ON (HIGH)."
|
||||
|
||||
User: "Turn on the relay on pin 5"
|
||||
→ gpio_write {"pin": 5, "state": 1}
|
||||
→ "Pin 5 set to HIGH"
|
||||
→ gpio_read {"pin": 5}
|
||||
→ "Pin 5 = HIGH"
|
||||
→ "Relay on pin 5 is now ON. Confirmed HIGH."
|
||||
Reference in New Issue
Block a user