Fix firmware for ESP-IDF v5.3 and hardware-verified operation
- Switch UART0 for protocol I/O (USB bridge), disable ESP-IDF console - Wire all 21 command handlers into dispatch table (was only 4) - Add configure command handler (name, io_cap, device_class) - Add bt_classic_is_enabled()/bt_ble_is_enabled() for live status - Fix cJSON_False misuse in get_status (type constant, not boolean) - Fix esp_bt_gap_set_cod() to use esp_bt_cod_t bitfield struct - Fix auth_cmpl.auth_mode → lk_type for ESP-IDF v5.3 - Replace deprecated esp_bt_dev_set_device_name with stack-specific API - Remove unused bytes_to_hex, obsolete kconfig symbols - Use large partition table (1.5MB) for dual-mode BT stack Verified on ESP32-D0WD-V3 rev 3.1, /dev/ttyUSB4, all commands tested.
This commit is contained in:
parent
73d3d438a2
commit
dc6078b296
@ -114,14 +114,6 @@ static int hex_to_bytes(const char *hex, uint8_t *out, int max_len)
|
|||||||
return len;
|
return len;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void bytes_to_hex(const uint8_t *data, int len, char *out)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < len; i++) {
|
|
||||||
sprintf(out + i * 2, "%02x", data[i]);
|
|
||||||
}
|
|
||||||
out[len * 2] = '\0';
|
|
||||||
}
|
|
||||||
|
|
||||||
static uint8_t properties_from_json(cJSON *arr)
|
static uint8_t properties_from_json(cJSON *arr)
|
||||||
{
|
{
|
||||||
uint8_t props = 0;
|
uint8_t props = 0;
|
||||||
@ -424,6 +416,15 @@ static void gatts_event_handler(esp_gatts_cb_event_t event,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* State query */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
bool bt_ble_is_enabled(void)
|
||||||
|
{
|
||||||
|
return s_ble.enabled;
|
||||||
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
/* Command handlers */
|
/* Command handlers */
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
|
|||||||
@ -1,10 +1,14 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
#include "cJSON.h"
|
#include "cJSON.h"
|
||||||
|
|
||||||
/* Initialize BLE subsystem (Bluedroid GATTS) */
|
/* Initialize BLE subsystem (Bluedroid GATTS) */
|
||||||
void bt_ble_init(void);
|
void bt_ble_init(void);
|
||||||
|
|
||||||
|
/* State query */
|
||||||
|
bool bt_ble_is_enabled(void);
|
||||||
|
|
||||||
/* Command handlers (called from cmd_dispatcher) */
|
/* Command handlers (called from cmd_dispatcher) */
|
||||||
void cmd_ble_enable(const char *id, cJSON *params);
|
void cmd_ble_enable(const char *id, cJSON *params);
|
||||||
void cmd_ble_disable(const char *id, cJSON *params);
|
void cmd_ble_disable(const char *id, cJSON *params);
|
||||||
|
|||||||
@ -105,9 +105,9 @@ static void gap_cb(esp_bt_gap_cb_event_t event, esp_bt_gap_cb_param_t *param)
|
|||||||
bd_addr_to_str(param->auth_cmpl.bda, addr_str);
|
bd_addr_to_str(param->auth_cmpl.bda, addr_str);
|
||||||
bool ok = (param->auth_cmpl.stat == ESP_BT_STATUS_SUCCESS);
|
bool ok = (param->auth_cmpl.stat == ESP_BT_STATUS_SUCCESS);
|
||||||
|
|
||||||
ESP_LOGI(TAG, "auth_cmpl: %s %s (mode=%d)",
|
ESP_LOGI(TAG, "auth_cmpl: %s %s (lk_type=%d)",
|
||||||
addr_str, ok ? "success" : "FAIL",
|
addr_str, ok ? "success" : "FAIL",
|
||||||
param->auth_cmpl.auth_mode);
|
param->auth_cmpl.lk_type);
|
||||||
|
|
||||||
event_report_pair_complete(addr_str, ok);
|
event_report_pair_complete(addr_str, ok);
|
||||||
|
|
||||||
@ -583,6 +583,15 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* State query */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
bool bt_classic_is_enabled(void)
|
||||||
|
{
|
||||||
|
return classic_state.enabled;
|
||||||
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
/* Configure helpers (called from the configure command handler) */
|
/* Configure helpers (called from the configure command handler) */
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
@ -605,11 +614,16 @@ void bt_classic_set_io_cap(const char *io_cap_str)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void bt_classic_set_device_class(uint32_t cod)
|
void bt_classic_set_device_class(uint32_t cod_raw)
|
||||||
{
|
{
|
||||||
esp_err_t err = esp_bt_gap_set_cod(cod, ESP_BT_SET_COD_ALL);
|
esp_bt_cod_t cod;
|
||||||
|
memset(&cod, 0, sizeof(cod));
|
||||||
|
cod.minor = (cod_raw >> 2) & 0x3F;
|
||||||
|
cod.major = (cod_raw >> 8) & 0x1F;
|
||||||
|
cod.service = (cod_raw >> 13) & 0x7FF;
|
||||||
|
esp_err_t err = esp_bt_gap_set_cod(cod, ESP_BT_SET_COD_MAJOR_MINOR | ESP_BT_SET_COD_SERVICE_CLASS);
|
||||||
if (err == ESP_OK) {
|
if (err == ESP_OK) {
|
||||||
ESP_LOGI(TAG, "CoD set to 0x%06" PRIx32, cod);
|
ESP_LOGI(TAG, "CoD set to 0x%06" PRIx32, cod_raw);
|
||||||
} else {
|
} else {
|
||||||
ESP_LOGE(TAG, "set_cod failed: %s", esp_err_to_name(err));
|
ESP_LOGE(TAG, "set_cod failed: %s", esp_err_to_name(err));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
#include "cJSON.h"
|
#include "cJSON.h"
|
||||||
|
|
||||||
void bt_classic_init(void);
|
void bt_classic_init(void);
|
||||||
@ -14,6 +17,9 @@ 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);
|
||||||
|
|
||||||
|
/* State query */
|
||||||
|
bool bt_classic_is_enabled(void);
|
||||||
|
|
||||||
/* Called by configure command to set IO capabilities */
|
/* Called by configure command to set IO capabilities */
|
||||||
void bt_classic_set_io_cap(const char *io_cap_str);
|
void bt_classic_set_io_cap(const char *io_cap_str);
|
||||||
void bt_classic_set_device_class(uint32_t cod);
|
void bt_classic_set_device_class(uint32_t cod_raw);
|
||||||
|
|||||||
@ -1,13 +1,16 @@
|
|||||||
#include "cmd_dispatcher.h"
|
#include "cmd_dispatcher.h"
|
||||||
#include "protocol.h"
|
#include "protocol.h"
|
||||||
#include "uart_handler.h"
|
#include "uart_handler.h"
|
||||||
|
#include "bt_classic.h"
|
||||||
|
#include "bt_ble.h"
|
||||||
|
#include "personas.h"
|
||||||
|
|
||||||
#include "esp_system.h"
|
#include "esp_system.h"
|
||||||
#include "esp_chip_info.h"
|
#include "esp_chip_info.h"
|
||||||
#include "esp_timer.h"
|
#include "esp_timer.h"
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
#include "esp_mac.h"
|
#include "esp_mac.h"
|
||||||
#include "esp_bt.h"
|
#include "esp_gap_bt_api.h"
|
||||||
#include "cJSON.h"
|
#include "cJSON.h"
|
||||||
|
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
@ -23,11 +26,6 @@ typedef struct {
|
|||||||
cmd_handler_t handler;
|
cmd_handler_t handler;
|
||||||
} cmd_entry_t;
|
} cmd_entry_t;
|
||||||
|
|
||||||
/* Forward declarations for handlers in other modules */
|
|
||||||
extern void bt_classic_register_commands(void);
|
|
||||||
extern void bt_ble_register_commands(void);
|
|
||||||
extern void personas_register_commands(void);
|
|
||||||
|
|
||||||
/* --- System command handlers --- */
|
/* --- System command handlers --- */
|
||||||
|
|
||||||
static void handle_ping(const char *id, cJSON *params)
|
static void handle_ping(const char *id, cJSON *params)
|
||||||
@ -97,30 +95,87 @@ static void handle_get_status(const char *id, cJSON *params)
|
|||||||
cJSON *data = cJSON_CreateObject();
|
cJSON *data = cJSON_CreateObject();
|
||||||
cJSON_AddNumberToObject(data, "uptime_ms", (double)uptime_ms);
|
cJSON_AddNumberToObject(data, "uptime_ms", (double)uptime_ms);
|
||||||
cJSON_AddNumberToObject(data, "free_heap", (double)esp_get_free_heap_size());
|
cJSON_AddNumberToObject(data, "free_heap", (double)esp_get_free_heap_size());
|
||||||
cJSON_AddBoolToObject(data, "bt_enabled", cJSON_False);
|
cJSON_AddBoolToObject(data, "bt_enabled", bt_classic_is_enabled());
|
||||||
cJSON_AddBoolToObject(data, "ble_enabled", cJSON_False);
|
cJSON_AddBoolToObject(data, "ble_enabled", bt_ble_is_enabled());
|
||||||
|
|
||||||
uart_send_response(id, STATUS_OK, data);
|
uart_send_response(id, STATUS_OK, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- Configure command: sets device name, IO capabilities, CoD --- */
|
||||||
|
|
||||||
|
static void handle_configure(const char *id, cJSON *params)
|
||||||
|
{
|
||||||
|
if (!params) {
|
||||||
|
cJSON *err = cJSON_CreateObject();
|
||||||
|
cJSON_AddStringToObject(err, "error", "params required");
|
||||||
|
uart_send_response(id, STATUS_ERROR, err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON *applied = cJSON_CreateObject();
|
||||||
|
|
||||||
|
cJSON *name = cJSON_GetObjectItem(params, "name");
|
||||||
|
if (name && cJSON_IsString(name)) {
|
||||||
|
esp_bt_gap_set_device_name(name->valuestring);
|
||||||
|
cJSON_AddStringToObject(applied, "name", name->valuestring);
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON *io_cap = cJSON_GetObjectItem(params, "io_cap");
|
||||||
|
if (io_cap && cJSON_IsString(io_cap)) {
|
||||||
|
bt_classic_set_io_cap(io_cap->valuestring);
|
||||||
|
cJSON_AddStringToObject(applied, "io_cap", io_cap->valuestring);
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON *device_class = cJSON_GetObjectItem(params, "device_class");
|
||||||
|
if (device_class && cJSON_IsNumber(device_class)) {
|
||||||
|
bt_classic_set_device_class((uint32_t)device_class->valuedouble);
|
||||||
|
cJSON_AddNumberToObject(applied, "device_class", device_class->valuedouble);
|
||||||
|
}
|
||||||
|
|
||||||
|
uart_send_response(id, STATUS_OK, applied);
|
||||||
|
}
|
||||||
|
|
||||||
/* --- Command table --- */
|
/* --- Command table --- */
|
||||||
|
|
||||||
static const cmd_entry_t cmd_table[] = {
|
static const cmd_entry_t cmd_table[] = {
|
||||||
{ CMD_PING, handle_ping },
|
/* System */
|
||||||
{ CMD_RESET, handle_reset },
|
{ CMD_PING, handle_ping },
|
||||||
{ CMD_GET_INFO, handle_get_info },
|
{ CMD_RESET, handle_reset },
|
||||||
{ CMD_GET_STATUS, handle_get_status },
|
{ CMD_GET_INFO, handle_get_info },
|
||||||
|
{ CMD_GET_STATUS, handle_get_status },
|
||||||
|
|
||||||
|
/* Configuration */
|
||||||
|
{ CMD_CONFIGURE, handle_configure },
|
||||||
|
{ CMD_LOAD_PERSONA, cmd_load_persona },
|
||||||
|
{ CMD_LIST_PERSONAS, cmd_list_personas },
|
||||||
|
|
||||||
|
/* Classic BT */
|
||||||
|
{ CMD_CLASSIC_ENABLE, cmd_classic_enable },
|
||||||
|
{ CMD_CLASSIC_DISABLE, cmd_classic_disable },
|
||||||
|
{ CMD_CLASSIC_SET_DISCOVERABLE,cmd_classic_set_discoverable },
|
||||||
|
{ CMD_CLASSIC_PAIR_RESPOND, cmd_classic_pair_respond },
|
||||||
|
{ CMD_CLASSIC_SET_SSP_MODE, cmd_classic_set_ssp_mode },
|
||||||
|
|
||||||
|
/* BLE */
|
||||||
|
{ CMD_BLE_ENABLE, cmd_ble_enable },
|
||||||
|
{ CMD_BLE_DISABLE, cmd_ble_disable },
|
||||||
|
{ CMD_BLE_ADVERTISE, cmd_ble_advertise },
|
||||||
|
{ CMD_BLE_SET_ADV_DATA, cmd_ble_set_adv_data },
|
||||||
|
|
||||||
|
/* GATT */
|
||||||
|
{ CMD_GATT_ADD_SERVICE, cmd_gatt_add_service },
|
||||||
|
{ CMD_GATT_ADD_CHARACTERISTIC, cmd_gatt_add_characteristic },
|
||||||
|
{ CMD_GATT_SET_VALUE, cmd_gatt_set_value },
|
||||||
|
{ CMD_GATT_NOTIFY, cmd_gatt_notify },
|
||||||
|
{ CMD_GATT_CLEAR, cmd_gatt_clear },
|
||||||
|
|
||||||
{ NULL, NULL } /* sentinel */
|
{ NULL, NULL } /* sentinel */
|
||||||
};
|
};
|
||||||
|
|
||||||
void cmd_dispatcher_init(void)
|
void cmd_dispatcher_init(void)
|
||||||
{
|
{
|
||||||
ESP_LOGI(TAG, "command dispatcher ready");
|
ESP_LOGI(TAG, "command dispatcher ready (%d commands)",
|
||||||
|
(int)(sizeof(cmd_table) / sizeof(cmd_table[0])) - 1);
|
||||||
/* Future: these will register their own handlers into the table */
|
|
||||||
/* bt_classic_register_commands(); */
|
|
||||||
/* bt_ble_register_commands(); */
|
|
||||||
/* personas_register_commands(); */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void cmd_dispatch(const char *id, const char *cmd, cJSON *params)
|
void cmd_dispatch(const char *id, const char *cmd, cJSON *params)
|
||||||
|
|||||||
@ -14,6 +14,7 @@
|
|||||||
#include "bt_ble.h"
|
#include "bt_ble.h"
|
||||||
|
|
||||||
#include "esp_bt_device.h"
|
#include "esp_bt_device.h"
|
||||||
|
#include "esp_gap_bt_api.h"
|
||||||
#include "esp_gap_ble_api.h"
|
#include "esp_gap_ble_api.h"
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
#include "cJSON.h"
|
#include "cJSON.h"
|
||||||
@ -174,7 +175,7 @@ void cmd_load_persona(const char *id, cJSON *params)
|
|||||||
|
|
||||||
/* --- Set device name on both stacks --- */
|
/* --- Set device name on both stacks --- */
|
||||||
if (p->classic_enabled) {
|
if (p->classic_enabled) {
|
||||||
esp_bt_dev_set_device_name(p->device_name);
|
esp_bt_gap_set_device_name(p->device_name);
|
||||||
}
|
}
|
||||||
if (p->ble_enabled) {
|
if (p->ble_enabled) {
|
||||||
esp_ble_gap_set_device_name(p->device_name);
|
esp_ble_gap_set_device_name(p->device_name);
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
#include "driver/uart.h"
|
#include "driver/uart.h"
|
||||||
|
|
||||||
/* UART configuration */
|
/* UART configuration */
|
||||||
#define PROTO_UART_NUM UART_NUM_1
|
#define PROTO_UART_NUM UART_NUM_0
|
||||||
#define PROTO_BAUD_RATE 115200
|
#define PROTO_BAUD_RATE 115200
|
||||||
#define PROTO_TX_BUF_SIZE 4096
|
#define PROTO_TX_BUF_SIZE 4096
|
||||||
#define PROTO_RX_BUF_SIZE 4096
|
#define PROTO_RX_BUF_SIZE 4096
|
||||||
|
|||||||
@ -14,9 +14,11 @@
|
|||||||
static const char *TAG = "uart";
|
static const char *TAG = "uart";
|
||||||
static SemaphoreHandle_t tx_mutex;
|
static SemaphoreHandle_t tx_mutex;
|
||||||
|
|
||||||
/* Pin assignments -- keep UART0 free for ESP-IDF console/logging */
|
/*
|
||||||
#define UART_TX_PIN GPIO_NUM_4
|
* Use UART0 — the dev board's USB bridge connects here.
|
||||||
#define UART_RX_PIN GPIO_NUM_5
|
* ESP-IDF console is disabled (CONFIG_ESP_CONSOLE_NONE=y) so we own UART0.
|
||||||
|
* Default UART0 pins (TX=GPIO1, RX=GPIO3) route through the USB bridge.
|
||||||
|
*/
|
||||||
|
|
||||||
void uart_handler_init(void)
|
void uart_handler_init(void)
|
||||||
{
|
{
|
||||||
@ -30,8 +32,7 @@ void uart_handler_init(void)
|
|||||||
};
|
};
|
||||||
|
|
||||||
ESP_ERROR_CHECK(uart_param_config(PROTO_UART_NUM, &cfg));
|
ESP_ERROR_CHECK(uart_param_config(PROTO_UART_NUM, &cfg));
|
||||||
ESP_ERROR_CHECK(uart_set_pin(PROTO_UART_NUM, UART_TX_PIN, UART_RX_PIN,
|
/* UART0 default pins (TX=1, RX=3) — no set_pin needed */
|
||||||
UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE));
|
|
||||||
ESP_ERROR_CHECK(uart_driver_install(PROTO_UART_NUM,
|
ESP_ERROR_CHECK(uart_driver_install(PROTO_UART_NUM,
|
||||||
PROTO_RX_BUF_SIZE, PROTO_TX_BUF_SIZE,
|
PROTO_RX_BUF_SIZE, PROTO_TX_BUF_SIZE,
|
||||||
0, NULL, 0));
|
0, NULL, 0));
|
||||||
@ -39,8 +40,8 @@ void uart_handler_init(void)
|
|||||||
tx_mutex = xSemaphoreCreateMutex();
|
tx_mutex = xSemaphoreCreateMutex();
|
||||||
assert(tx_mutex);
|
assert(tx_mutex);
|
||||||
|
|
||||||
ESP_LOGI(TAG, "UART%d ready (TX=%d RX=%d @ %d baud)",
|
ESP_LOGI(TAG, "UART%d ready (USB bridge @ %d baud)",
|
||||||
PROTO_UART_NUM, UART_TX_PIN, UART_RX_PIN, PROTO_BAUD_RATE);
|
PROTO_UART_NUM, PROTO_BAUD_RATE);
|
||||||
}
|
}
|
||||||
|
|
||||||
char *uart_read_line(void)
|
char *uart_read_line(void)
|
||||||
|
|||||||
@ -5,7 +5,6 @@ CONFIG_BT_CLASSIC_ENABLED=y
|
|||||||
CONFIG_BT_BLE_ENABLED=y
|
CONFIG_BT_BLE_ENABLED=y
|
||||||
CONFIG_BT_A2DP_ENABLE=n
|
CONFIG_BT_A2DP_ENABLE=n
|
||||||
CONFIG_BT_SPP_ENABLED=y
|
CONFIG_BT_SPP_ENABLED=y
|
||||||
CONFIG_BT_SSP_ENABLED=y
|
|
||||||
|
|
||||||
# GAP & GATTS
|
# GAP & GATTS
|
||||||
CONFIG_BT_GATTS_ENABLE=y
|
CONFIG_BT_GATTS_ENABLE=y
|
||||||
@ -14,22 +13,19 @@ CONFIG_BT_GATTC_ENABLE=n
|
|||||||
# Bluetooth controller
|
# Bluetooth controller
|
||||||
CONFIG_BTDM_CTRL_MODE_BTDM=y
|
CONFIG_BTDM_CTRL_MODE_BTDM=y
|
||||||
|
|
||||||
# UART
|
# UART — disable ESP-IDF console so our protocol handler owns UART0
|
||||||
CONFIG_ESP_CONSOLE_UART_NUM=0
|
CONFIG_ESP_CONSOLE_NONE=y
|
||||||
|
|
||||||
# Stack sizes (Bluetooth needs generous stacks)
|
# Stack sizes (Bluetooth needs generous stacks)
|
||||||
CONFIG_BT_BTU_TASK_STACK_SIZE=8192
|
CONFIG_BT_BTU_TASK_STACK_SIZE=8192
|
||||||
CONFIG_BTDM_CTRL_HCI_MODE_VHCI=y
|
CONFIG_BTDM_CTRL_HCI_MODE_VHCI=y
|
||||||
|
|
||||||
# NVS (for bonding storage)
|
|
||||||
CONFIG_NVS_ENABLED=y
|
|
||||||
|
|
||||||
# Logging — keep it reasonable
|
# Logging — keep it reasonable
|
||||||
CONFIG_LOG_DEFAULT_LEVEL_INFO=y
|
CONFIG_LOG_DEFAULT_LEVEL_INFO=y
|
||||||
CONFIG_LOG_MAXIMUM_LEVEL_DEBUG=y
|
CONFIG_LOG_MAXIMUM_LEVEL_DEBUG=y
|
||||||
|
|
||||||
# Partition table
|
# Partition table — large (1.5MB app) needed for dual-mode BT stack
|
||||||
CONFIG_PARTITION_TABLE_SINGLE_APP=y
|
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
|
||||||
|
|
||||||
# FreeRTOS
|
# FreeRTOS
|
||||||
CONFIG_FREERTOS_HZ=1000
|
CONFIG_FREERTOS_HZ=1000
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user