Add SPP (Serial Port Profile) support for bidirectional data transfer
Firmware: - Add spp_connect, spp_disconnect, spp_data events - Add spp_send, spp_disconnect, spp_status commands - Track remote address for connected SPP peer - Report received data as hex + optional text decode Python MCP: - esp32_spp_send(data/data_hex) - send text or binary - esp32_spp_disconnect() - close SPP connection - esp32_spp_status() - query connection state Tested: Linux rfcomm connect → ESP32, bidirectional data transfer works
This commit is contained in:
parent
5dcacc23ab
commit
61da375a0c
10
README.md
10
README.md
@ -34,10 +34,11 @@ The ESP32 can emulate various Bluetooth devices for testing:
|
|||||||
| Capability | Description |
|
| Capability | Description |
|
||||||
|------------|-------------|
|
|------------|-------------|
|
||||||
| **Classic BT Pairing** | All 4 SSP modes: Just Works, Numeric Comparison, Passkey Entry, Legacy PIN |
|
| **Classic BT Pairing** | All 4 SSP modes: Just Works, Numeric Comparison, Passkey Entry, Legacy PIN |
|
||||||
|
| **SPP (Serial Port)** | Bidirectional data transfer over Classic Bluetooth virtual serial port |
|
||||||
| **BLE GATT Server** | Dynamic service/characteristic creation at runtime |
|
| **BLE GATT Server** | Dynamic service/characteristic creation at runtime |
|
||||||
| **Device Personas** | Presets for headset, speaker, keyboard, sensor, phone |
|
| **Device Personas** | Presets for headset, speaker, keyboard, sensor, phone |
|
||||||
| **IO Capabilities** | Configurable: `no_io`, `display_only`, `display_yesno`, `keyboard_only`, `keyboard_display` |
|
| **IO Capabilities** | Configurable: `no_io`, `display_only`, `display_yesno`, `keyboard_only`, `keyboard_display` |
|
||||||
| **Event Reporting** | Real-time events: pair_request, pair_complete, gatt_read, gatt_write, gatt_subscribe |
|
| **Event Reporting** | Real-time events: pair_request, pair_complete, spp_data, gatt_read, gatt_write, gatt_subscribe |
|
||||||
|
|
||||||
### Hardware Requirements
|
### Hardware Requirements
|
||||||
|
|
||||||
@ -116,6 +117,13 @@ esp32_get_info() # → chip, fw_version, bt_mac
|
|||||||
| `esp32_classic_set_discoverable` | Make device visible for pairing |
|
| `esp32_classic_set_discoverable` | Make device visible for pairing |
|
||||||
| `esp32_classic_pair_respond` | Accept/reject incoming pairing |
|
| `esp32_classic_pair_respond` | Accept/reject incoming pairing |
|
||||||
|
|
||||||
|
### SPP (Serial Port Profile)
|
||||||
|
| Tool | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `esp32_spp_send` | Send data (text or hex) over SPP connection |
|
||||||
|
| `esp32_spp_disconnect` | Close the active SPP connection |
|
||||||
|
| `esp32_spp_status` | Get SPP connection status and remote address |
|
||||||
|
|
||||||
### BLE / GATT
|
### BLE / GATT
|
||||||
| Tool | Description |
|
| Tool | Description |
|
||||||
|------|-------------|
|
|------|-------------|
|
||||||
|
|||||||
@ -54,6 +54,7 @@ static struct {
|
|||||||
pair_type_t pair_type;
|
pair_type_t pair_type;
|
||||||
/* SPP handle for the listening server */
|
/* SPP handle for the listening server */
|
||||||
uint32_t spp_handle;
|
uint32_t spp_handle;
|
||||||
|
char spp_remote_addr[18]; /* Connected peer address */
|
||||||
} classic_state = {
|
} classic_state = {
|
||||||
.io_cap = ESP_BT_IO_CAP_IO, /* display+yesno (numeric comparison) */
|
.io_cap = ESP_BT_IO_CAP_IO, /* display+yesno (numeric comparison) */
|
||||||
.ssp_enabled = true,
|
.ssp_enabled = true,
|
||||||
@ -243,24 +244,37 @@ static void spp_cb(esp_spp_cb_event_t event, esp_spp_cb_param_t *param)
|
|||||||
case ESP_SPP_SRV_OPEN_EVT:
|
case ESP_SPP_SRV_OPEN_EVT:
|
||||||
bd_addr_to_str(param->srv_open.rem_bda, addr_str);
|
bd_addr_to_str(param->srv_open.rem_bda, addr_str);
|
||||||
classic_state.spp_handle = param->srv_open.handle;
|
classic_state.spp_handle = param->srv_open.handle;
|
||||||
|
strncpy(classic_state.spp_remote_addr, addr_str,
|
||||||
|
sizeof(classic_state.spp_remote_addr) - 1);
|
||||||
|
classic_state.spp_remote_addr[sizeof(classic_state.spp_remote_addr) - 1] = '\0';
|
||||||
ESP_LOGI(SPP_TAG, "SPP client connected: %s (handle=%" PRIu32 ")",
|
ESP_LOGI(SPP_TAG, "SPP client connected: %s (handle=%" PRIu32 ")",
|
||||||
addr_str, param->srv_open.handle);
|
addr_str, param->srv_open.handle);
|
||||||
event_report_connect(addr_str, "classic");
|
/* Report specific SPP connect event with more detail */
|
||||||
|
{
|
||||||
|
cJSON *d = cJSON_CreateObject();
|
||||||
|
cJSON_AddStringToObject(d, "address", addr_str);
|
||||||
|
cJSON_AddNumberToObject(d, "handle", (double)param->srv_open.handle);
|
||||||
|
cJSON_AddStringToObject(d, "transport", "spp");
|
||||||
|
event_report(EVT_SPP_CONNECT, d);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ESP_SPP_CLOSE_EVT:
|
case ESP_SPP_CLOSE_EVT:
|
||||||
ESP_LOGI(SPP_TAG, "SPP disconnected (handle=%" PRIu32 ")",
|
ESP_LOGI(SPP_TAG, "SPP disconnected (handle=%" PRIu32 ")",
|
||||||
param->close.handle);
|
param->close.handle);
|
||||||
/* We don't have the remote address in CLOSE_EVT on all IDF versions,
|
/* Report SPP disconnect with saved remote address */
|
||||||
* so report with handle info. */
|
|
||||||
{
|
{
|
||||||
cJSON *d = cJSON_CreateObject();
|
cJSON *d = cJSON_CreateObject();
|
||||||
cJSON_AddNumberToObject(d, "handle", (double)param->close.handle);
|
cJSON_AddNumberToObject(d, "handle", (double)param->close.handle);
|
||||||
cJSON_AddStringToObject(d, "transport", "classic");
|
cJSON_AddStringToObject(d, "transport", "spp");
|
||||||
event_report(EVT_DISCONNECT, d);
|
if (classic_state.spp_remote_addr[0] != '\0') {
|
||||||
|
cJSON_AddStringToObject(d, "address", classic_state.spp_remote_addr);
|
||||||
|
}
|
||||||
|
event_report(EVT_SPP_DISCONNECT, d);
|
||||||
}
|
}
|
||||||
if (classic_state.spp_handle == param->close.handle) {
|
if (classic_state.spp_handle == param->close.handle) {
|
||||||
classic_state.spp_handle = 0;
|
classic_state.spp_handle = 0;
|
||||||
|
classic_state.spp_remote_addr[0] = '\0';
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@ -274,9 +288,46 @@ static void spp_cb(esp_spp_cb_event_t event, esp_spp_cb_param_t *param)
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case ESP_SPP_DATA_IND_EVT:
|
case ESP_SPP_DATA_IND_EVT:
|
||||||
/* Data received over SPP -- log but don't process for now. */
|
/* Data received over SPP -- report as event */
|
||||||
ESP_LOGI(SPP_TAG, "SPP data: %d bytes on handle %" PRIu32,
|
ESP_LOGI(SPP_TAG, "SPP data: %d bytes on handle %" PRIu32,
|
||||||
param->data_ind.len, param->data_ind.handle);
|
param->data_ind.len, param->data_ind.handle);
|
||||||
|
{
|
||||||
|
cJSON *d = cJSON_CreateObject();
|
||||||
|
cJSON_AddNumberToObject(d, "handle", (double)param->data_ind.handle);
|
||||||
|
cJSON_AddNumberToObject(d, "length", param->data_ind.len);
|
||||||
|
if (classic_state.spp_remote_addr[0] != '\0') {
|
||||||
|
cJSON_AddStringToObject(d, "address", classic_state.spp_remote_addr);
|
||||||
|
}
|
||||||
|
/* Encode data as hex string */
|
||||||
|
size_t hex_len = param->data_ind.len * 2 + 1;
|
||||||
|
char *hex_str = malloc(hex_len);
|
||||||
|
if (hex_str) {
|
||||||
|
for (int i = 0; i < param->data_ind.len; i++) {
|
||||||
|
sprintf(hex_str + i * 2, "%02x", param->data_ind.data[i]);
|
||||||
|
}
|
||||||
|
hex_str[param->data_ind.len * 2] = '\0';
|
||||||
|
cJSON_AddStringToObject(d, "data_hex", hex_str);
|
||||||
|
free(hex_str);
|
||||||
|
}
|
||||||
|
/* Also try UTF-8 if printable */
|
||||||
|
bool printable = true;
|
||||||
|
for (int i = 0; i < param->data_ind.len && printable; i++) {
|
||||||
|
uint8_t c = param->data_ind.data[i];
|
||||||
|
if (c < 0x20 && c != '\n' && c != '\r' && c != '\t') {
|
||||||
|
printable = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (printable && param->data_ind.len < 256) {
|
||||||
|
char *text = malloc(param->data_ind.len + 1);
|
||||||
|
if (text) {
|
||||||
|
memcpy(text, param->data_ind.data, param->data_ind.len);
|
||||||
|
text[param->data_ind.len] = '\0';
|
||||||
|
cJSON_AddStringToObject(d, "data_text", text);
|
||||||
|
free(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
event_report(EVT_SPP_DATA, d);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@ -598,6 +649,126 @@ void cmd_classic_set_ssp_mode(const char *id, cJSON *params)
|
|||||||
uart_send_response(id, STATUS_OK, data);
|
uart_send_response(id, STATUS_OK, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* SPP command handlers */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
static bool hex_to_bytes(const char *hex, uint8_t *out, size_t *out_len, size_t max_len)
|
||||||
|
{
|
||||||
|
size_t hex_len = strlen(hex);
|
||||||
|
if (hex_len % 2 != 0) return false;
|
||||||
|
size_t bytes = hex_len / 2;
|
||||||
|
if (bytes > max_len) return false;
|
||||||
|
for (size_t i = 0; i < bytes; i++) {
|
||||||
|
unsigned int b;
|
||||||
|
if (sscanf(hex + i * 2, "%2x", &b) != 1) return false;
|
||||||
|
out[i] = (uint8_t)b;
|
||||||
|
}
|
||||||
|
*out_len = bytes;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void cmd_spp_send(const char *id, cJSON *params)
|
||||||
|
{
|
||||||
|
if (!classic_state.enabled) {
|
||||||
|
uart_send_response(id, STATUS_ERROR,
|
||||||
|
cJSON_CreateString("classic BT not enabled"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (classic_state.spp_handle == 0) {
|
||||||
|
uart_send_response(id, STATUS_ERROR,
|
||||||
|
cJSON_CreateString("no SPP connection"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cJSON *j_data = cJSON_GetObjectItem(params, "data");
|
||||||
|
const cJSON *j_hex = cJSON_GetObjectItem(params, "data_hex");
|
||||||
|
|
||||||
|
uint8_t buf[512];
|
||||||
|
size_t len = 0;
|
||||||
|
|
||||||
|
if (cJSON_IsString(j_hex)) {
|
||||||
|
/* Hex-encoded data */
|
||||||
|
if (!hex_to_bytes(j_hex->valuestring, buf, &len, sizeof(buf))) {
|
||||||
|
uart_send_response(id, STATUS_ERROR,
|
||||||
|
cJSON_CreateString("invalid hex data"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (cJSON_IsString(j_data)) {
|
||||||
|
/* Plain text data */
|
||||||
|
len = strlen(j_data->valuestring);
|
||||||
|
if (len > sizeof(buf)) {
|
||||||
|
uart_send_response(id, STATUS_ERROR,
|
||||||
|
cJSON_CreateString("data too long (max 512 bytes)"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
memcpy(buf, j_data->valuestring, len);
|
||||||
|
} else {
|
||||||
|
uart_send_response(id, STATUS_ERROR,
|
||||||
|
cJSON_CreateString("missing 'data' or 'data_hex'"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t err = esp_spp_write(classic_state.spp_handle, len, buf);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE(SPP_TAG, "spp_write failed: %s", esp_err_to_name(err));
|
||||||
|
uart_send_response(id, STATUS_ERROR,
|
||||||
|
cJSON_CreateString(esp_err_to_name(err)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(SPP_TAG, "SPP sent %zu bytes", len);
|
||||||
|
|
||||||
|
cJSON *data = cJSON_CreateObject();
|
||||||
|
cJSON_AddNumberToObject(data, "bytes_sent", (double)len);
|
||||||
|
uart_send_response(id, STATUS_OK, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
void cmd_spp_disconnect(const char *id, cJSON *params)
|
||||||
|
{
|
||||||
|
(void)params;
|
||||||
|
|
||||||
|
if (!classic_state.enabled) {
|
||||||
|
uart_send_response(id, STATUS_ERROR,
|
||||||
|
cJSON_CreateString("classic BT not enabled"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (classic_state.spp_handle == 0) {
|
||||||
|
uart_send_response(id, STATUS_OK,
|
||||||
|
cJSON_CreateString("no active connection"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t handle = classic_state.spp_handle;
|
||||||
|
esp_err_t err = esp_spp_disconnect(handle);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE(SPP_TAG, "spp_disconnect failed: %s", esp_err_to_name(err));
|
||||||
|
uart_send_response(id, STATUS_ERROR,
|
||||||
|
cJSON_CreateString(esp_err_to_name(err)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(SPP_TAG, "SPP disconnect initiated (handle=%" PRIu32 ")", handle);
|
||||||
|
uart_send_response(id, STATUS_OK, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
void cmd_spp_status(const char *id, cJSON *params)
|
||||||
|
{
|
||||||
|
(void)params;
|
||||||
|
|
||||||
|
cJSON *data = cJSON_CreateObject();
|
||||||
|
cJSON_AddBoolToObject(data, "connected", classic_state.spp_handle != 0);
|
||||||
|
if (classic_state.spp_handle != 0) {
|
||||||
|
cJSON_AddNumberToObject(data, "handle", (double)classic_state.spp_handle);
|
||||||
|
if (classic_state.spp_remote_addr[0] != '\0') {
|
||||||
|
cJSON_AddStringToObject(data, "remote_address", classic_state.spp_remote_addr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
uart_send_response(id, STATUS_OK, data);
|
||||||
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
/* State query */
|
/* State query */
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
|
|||||||
@ -17,6 +17,11 @@ void cmd_classic_set_discoverable(const char *id, cJSON *params);
|
|||||||
void cmd_classic_pair_respond(const char *id, cJSON *params);
|
void cmd_classic_pair_respond(const char *id, cJSON *params);
|
||||||
void cmd_classic_set_ssp_mode(const char *id, cJSON *params);
|
void cmd_classic_set_ssp_mode(const char *id, cJSON *params);
|
||||||
|
|
||||||
|
/* SPP command handlers */
|
||||||
|
void cmd_spp_send(const char *id, cJSON *params);
|
||||||
|
void cmd_spp_disconnect(const char *id, cJSON *params);
|
||||||
|
void cmd_spp_status(const char *id, cJSON *params);
|
||||||
|
|
||||||
/* State query */
|
/* State query */
|
||||||
bool bt_classic_is_enabled(void);
|
bool bt_classic_is_enabled(void);
|
||||||
|
|
||||||
|
|||||||
@ -156,6 +156,11 @@ static const cmd_entry_t cmd_table[] = {
|
|||||||
{ CMD_CLASSIC_PAIR_RESPOND, cmd_classic_pair_respond },
|
{ CMD_CLASSIC_PAIR_RESPOND, cmd_classic_pair_respond },
|
||||||
{ CMD_CLASSIC_SET_SSP_MODE, cmd_classic_set_ssp_mode },
|
{ CMD_CLASSIC_SET_SSP_MODE, cmd_classic_set_ssp_mode },
|
||||||
|
|
||||||
|
/* SPP */
|
||||||
|
{ CMD_SPP_SEND, cmd_spp_send },
|
||||||
|
{ CMD_SPP_DISCONNECT, cmd_spp_disconnect },
|
||||||
|
{ CMD_SPP_STATUS, cmd_spp_status },
|
||||||
|
|
||||||
/* BLE */
|
/* BLE */
|
||||||
{ CMD_BLE_ENABLE, cmd_ble_enable },
|
{ CMD_BLE_ENABLE, cmd_ble_enable },
|
||||||
{ CMD_BLE_DISABLE, cmd_ble_disable },
|
{ CMD_BLE_DISABLE, cmd_ble_disable },
|
||||||
|
|||||||
@ -36,6 +36,11 @@
|
|||||||
#define CMD_CLASSIC_SET_DISCOVERABLE "classic_set_discoverable"
|
#define CMD_CLASSIC_SET_DISCOVERABLE "classic_set_discoverable"
|
||||||
#define CMD_CLASSIC_PAIR_RESPOND "classic_pair_respond"
|
#define CMD_CLASSIC_PAIR_RESPOND "classic_pair_respond"
|
||||||
|
|
||||||
|
/* SPP commands */
|
||||||
|
#define CMD_SPP_SEND "spp_send"
|
||||||
|
#define CMD_SPP_DISCONNECT "spp_disconnect"
|
||||||
|
#define CMD_SPP_STATUS "spp_status"
|
||||||
|
|
||||||
/* BLE commands */
|
/* BLE commands */
|
||||||
#define CMD_BLE_ENABLE "ble_enable"
|
#define CMD_BLE_ENABLE "ble_enable"
|
||||||
#define CMD_BLE_DISABLE "ble_disable"
|
#define CMD_BLE_DISABLE "ble_disable"
|
||||||
@ -59,6 +64,11 @@
|
|||||||
#define EVT_GATT_WRITE "gatt_write"
|
#define EVT_GATT_WRITE "gatt_write"
|
||||||
#define EVT_GATT_SUBSCRIBE "gatt_subscribe"
|
#define EVT_GATT_SUBSCRIBE "gatt_subscribe"
|
||||||
|
|
||||||
|
/* SPP events */
|
||||||
|
#define EVT_SPP_DATA "spp_data"
|
||||||
|
#define EVT_SPP_CONNECT "spp_connect"
|
||||||
|
#define EVT_SPP_DISCONNECT "spp_disconnect"
|
||||||
|
|
||||||
/* SSP IO capabilities */
|
/* SSP IO capabilities */
|
||||||
#define IO_CAP_DISPLAY_ONLY "display_only"
|
#define IO_CAP_DISPLAY_ONLY "display_only"
|
||||||
#define IO_CAP_DISPLAY_YESNO "display_yesno"
|
#define IO_CAP_DISPLAY_YESNO "display_yesno"
|
||||||
|
|||||||
@ -60,6 +60,11 @@ CMD_CLASSIC_DISABLE = "classic_disable"
|
|||||||
CMD_CLASSIC_SET_DISCOVERABLE = "classic_set_discoverable"
|
CMD_CLASSIC_SET_DISCOVERABLE = "classic_set_discoverable"
|
||||||
CMD_CLASSIC_PAIR_RESPOND = "classic_pair_respond"
|
CMD_CLASSIC_PAIR_RESPOND = "classic_pair_respond"
|
||||||
|
|
||||||
|
# SPP (Serial Port Profile)
|
||||||
|
CMD_SPP_SEND = "spp_send"
|
||||||
|
CMD_SPP_DISCONNECT = "spp_disconnect"
|
||||||
|
CMD_SPP_STATUS = "spp_status"
|
||||||
|
|
||||||
# BLE
|
# BLE
|
||||||
CMD_BLE_ENABLE = "ble_enable"
|
CMD_BLE_ENABLE = "ble_enable"
|
||||||
CMD_BLE_DISABLE = "ble_disable"
|
CMD_BLE_DISABLE = "ble_disable"
|
||||||
@ -83,6 +88,11 @@ EVT_GATT_READ = "gatt_read"
|
|||||||
EVT_GATT_WRITE = "gatt_write"
|
EVT_GATT_WRITE = "gatt_write"
|
||||||
EVT_GATT_SUBSCRIBE = "gatt_subscribe"
|
EVT_GATT_SUBSCRIBE = "gatt_subscribe"
|
||||||
|
|
||||||
|
# SPP Events
|
||||||
|
EVT_SPP_DATA = "spp_data"
|
||||||
|
EVT_SPP_CONNECT = "spp_connect"
|
||||||
|
EVT_SPP_DISCONNECT = "spp_disconnect"
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Protocol constants
|
# Protocol constants
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@ -11,6 +11,9 @@ from ..protocol import (
|
|||||||
CMD_CLASSIC_ENABLE,
|
CMD_CLASSIC_ENABLE,
|
||||||
CMD_CLASSIC_PAIR_RESPOND,
|
CMD_CLASSIC_PAIR_RESPOND,
|
||||||
CMD_CLASSIC_SET_DISCOVERABLE,
|
CMD_CLASSIC_SET_DISCOVERABLE,
|
||||||
|
CMD_SPP_DISCONNECT,
|
||||||
|
CMD_SPP_SEND,
|
||||||
|
CMD_SPP_STATUS,
|
||||||
Status,
|
Status,
|
||||||
)
|
)
|
||||||
from ..serial_client import get_client
|
from ..serial_client import get_client
|
||||||
@ -122,3 +125,81 @@ def register_tools(mcp: FastMCP) -> None:
|
|||||||
return {"status": "error", "error": response.data.get("error", "unknown error")}
|
return {"status": "error", "error": response.data.get("error", "unknown error")}
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return {"error": str(exc)}
|
return {"error": str(exc)}
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# SPP (Serial Port Profile) tools
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def esp32_spp_send(
|
||||||
|
data: str | None = None,
|
||||||
|
data_hex: str | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Send data over the SPP (Serial Port Profile) connection.
|
||||||
|
|
||||||
|
SPP provides a virtual serial port over Bluetooth. After a Linux host
|
||||||
|
connects via rfcomm or similar, this tool sends data to that connection.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data: Plain text data to send (UTF-8 encoded).
|
||||||
|
data_hex: Hex-encoded binary data to send (e.g., "48656c6c6f" for "Hello").
|
||||||
|
Use this for binary protocols. Takes precedence over 'data'.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response with bytes_sent count on success.
|
||||||
|
"""
|
||||||
|
if data is None and data_hex is None:
|
||||||
|
return {"error": "Either 'data' or 'data_hex' must be provided"}
|
||||||
|
|
||||||
|
try:
|
||||||
|
client = get_client()
|
||||||
|
params: dict[str, Any] = {}
|
||||||
|
if data_hex is not None:
|
||||||
|
params["data_hex"] = data_hex
|
||||||
|
elif data is not None:
|
||||||
|
params["data"] = data
|
||||||
|
|
||||||
|
response = await client.send_command(CMD_SPP_SEND, params)
|
||||||
|
if response.status == Status.OK:
|
||||||
|
return {"status": "ok", **response.data}
|
||||||
|
return {"status": "error", "error": response.data.get("error", "unknown error")}
|
||||||
|
except Exception as exc:
|
||||||
|
return {"error": str(exc)}
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def esp32_spp_disconnect() -> dict[str, Any]:
|
||||||
|
"""Disconnect the current SPP connection.
|
||||||
|
|
||||||
|
Terminates the active SPP (Serial Port Profile) connection if one exists.
|
||||||
|
The SPP server will continue listening for new connections.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response confirming the disconnection.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
client = get_client()
|
||||||
|
response = await client.send_command(CMD_SPP_DISCONNECT)
|
||||||
|
if response.status == Status.OK:
|
||||||
|
return {"status": "ok", **response.data}
|
||||||
|
return {"status": "error", "error": response.data.get("error", "unknown error")}
|
||||||
|
except Exception as exc:
|
||||||
|
return {"error": str(exc)}
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def esp32_spp_status() -> dict[str, Any]:
|
||||||
|
"""Get the current SPP connection status.
|
||||||
|
|
||||||
|
Returns information about whether an SPP connection is active,
|
||||||
|
the connection handle, and the remote device address.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response with connected (bool), handle, and remote_address if connected.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
client = get_client()
|
||||||
|
response = await client.send_command(CMD_SPP_STATUS)
|
||||||
|
if response.status == Status.OK:
|
||||||
|
return {"status": "ok", **response.data}
|
||||||
|
return {"status": "error", "error": response.data.get("error", "unknown error")}
|
||||||
|
except Exception as exc:
|
||||||
|
return {"error": str(exc)}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user