feat: working test
This commit is contained in:
647
prog/terminal.cpp
Normal file
647
prog/terminal.cpp
Normal file
@ -0,0 +1,647 @@
|
||||
/*
|
||||
* Copyright (C) 2015-2019 Grégoire Passault <gregoire.passault@u-bordeaux.fr>
|
||||
* Copyright (C) 2019-2025 Adrien Boussicault <adrien.boussicault@labri.fr>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation, either
|
||||
* version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this program. If not, see
|
||||
* <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <Arduino.h>
|
||||
#include "terminal.hpp"
|
||||
#include "serial_print.hpp"
|
||||
#define STRINGIFY(x) #x
|
||||
|
||||
namespace terminal_n {
|
||||
|
||||
void Terminal::reset(){
|
||||
terminal_pos = 0;
|
||||
terminal_last_pos = 0;
|
||||
terminal_buffer[0] = '\0';
|
||||
terminal_last_ok = false;
|
||||
print_prompt();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the terminal
|
||||
*/
|
||||
void Terminal::disable(){
|
||||
disabled = true;
|
||||
}
|
||||
|
||||
void Terminal::enable(){
|
||||
terminal_last_ok = false;
|
||||
disabled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the terminal prompt
|
||||
*/
|
||||
void Terminal::print_prompt(){
|
||||
print(PROMPT);
|
||||
}
|
||||
|
||||
void Terminal::setup(HardwareSerial* serial){
|
||||
io = serial;
|
||||
if(terminal_command_count >= terminal_max_commands){
|
||||
println("There are too many commands. Increase the number of commands.");
|
||||
println("Define the macro TERMINAL_MAX_COMMANDS with a bigger number before including the terminal library.");
|
||||
}
|
||||
if(serial){
|
||||
silent = false;
|
||||
}else{
|
||||
silent = true;
|
||||
}
|
||||
enable();
|
||||
print_prompt();
|
||||
check_eeprom_names();
|
||||
load_config_from_eeprom();
|
||||
}
|
||||
|
||||
|
||||
|
||||
void Terminal::terminal_silent(bool silent)
|
||||
{
|
||||
silent = silent;
|
||||
}
|
||||
|
||||
|
||||
|
||||
const terminal_command *
|
||||
Terminal::find_command(
|
||||
char *command_name, uint32_t command_name_length
|
||||
){
|
||||
uint32_t i;
|
||||
for(i=0; i<terminal_command_count; i++){
|
||||
const terminal_command *command = terminal_commands[i];
|
||||
if(
|
||||
strlen(command->name) == command_name_length &&
|
||||
strncmp(terminal_buffer, command->name, command_name_length) == 0
|
||||
){
|
||||
return command;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/***
|
||||
* Executes the given command with given parameters
|
||||
*/
|
||||
bool Terminal::terminal_execute(
|
||||
char *command_name, uint32_t command_name_length, uint32_t argc, char **argv
|
||||
){
|
||||
uint32_t i;
|
||||
const terminal_command *command;
|
||||
|
||||
// Try to find and execute the command
|
||||
command = find_command(command_name, command_name_length);
|
||||
if(command != NULL){
|
||||
command->command(argc, argv);
|
||||
if(command->auto_save_after_command){
|
||||
save_value_to_eeprom(command);
|
||||
}
|
||||
}
|
||||
|
||||
// If it fails, try to parse the command as an allocation (a=b)
|
||||
if (command == NULL) {
|
||||
for(i=0; i<command_name_length; i++){
|
||||
if(command_name[i] == '='){
|
||||
command_name[i] = '\0';
|
||||
command_name_length = strlen(command_name);
|
||||
command = find_command(command_name, command_name_length);
|
||||
|
||||
if(command && command->parameter){
|
||||
argv[0] = command_name+i+1;
|
||||
argv[1] = NULL;
|
||||
argc = 1;
|
||||
command->command(argc, argv);
|
||||
if(command->auto_save_after_command){
|
||||
save_value_to_eeprom(command);
|
||||
}
|
||||
}else{
|
||||
command = NULL;
|
||||
}
|
||||
|
||||
if(!command){
|
||||
print("Unknown parameter: ");
|
||||
write(command_name, command_name_length);
|
||||
println("");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If it fails again, display the "unknown command" message
|
||||
if (command == NULL) {
|
||||
print("Unknown command: ");
|
||||
write(command_name, command_name_length);
|
||||
println("");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/***
|
||||
* Process the receive buffer to parse the command and executes it
|
||||
*/
|
||||
void Terminal::terminal_process(){
|
||||
char *saveptr;
|
||||
uint32_t command_name_length;
|
||||
|
||||
uint32_t argc = 0;
|
||||
char* argv[terminal_max_arguments+1];
|
||||
|
||||
println("");
|
||||
|
||||
strtok_r(terminal_buffer, " ", &saveptr);
|
||||
while (
|
||||
(argv[argc] = strtok_r(NULL, " ", &saveptr)) != NULL &&
|
||||
argc < terminal_max_arguments
|
||||
){
|
||||
*(argv[argc]-1) = '\0';
|
||||
argc++;
|
||||
}
|
||||
|
||||
if(terminal_max_arguments == argc){
|
||||
println("Nb. of parameters are limited.");
|
||||
}
|
||||
|
||||
if (saveptr != NULL) {
|
||||
*(saveptr - 1) = ' ';
|
||||
}
|
||||
|
||||
command_name_length = strlen(terminal_buffer);
|
||||
|
||||
if (command_name_length > 0) {
|
||||
terminal_last_ok = terminal_execute(
|
||||
terminal_buffer, command_name_length, argc, argv
|
||||
);
|
||||
}else{
|
||||
terminal_last_ok = false;
|
||||
}
|
||||
|
||||
terminal_last_pos = terminal_pos;
|
||||
terminal_pos = 0;
|
||||
print_prompt();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ticking the terminal, this will cause lookup for characters
|
||||
* and eventually a call to the process function on new lines
|
||||
*/
|
||||
void Terminal::update(){
|
||||
if(disabled || !terminal_has_io()){
|
||||
return;
|
||||
}
|
||||
|
||||
char c;
|
||||
uint8_t input;
|
||||
|
||||
while(io->available()){
|
||||
input = io->read();
|
||||
c = (char)input;
|
||||
if(c == '\0'){
|
||||
continue;
|
||||
}
|
||||
|
||||
//Return key
|
||||
if(c == '\r' || c == '\n'){
|
||||
if(terminal_pos == 0 && terminal_last_ok){
|
||||
// If the user pressed no keys, restore the last
|
||||
// command and run it again
|
||||
uint32_t i;
|
||||
for(i=0; i<terminal_last_pos; i++){
|
||||
if(terminal_buffer[i] == '\0'){
|
||||
terminal_buffer[i] = ' ';
|
||||
}
|
||||
}
|
||||
terminal_pos = terminal_last_pos;
|
||||
}
|
||||
terminal_buffer[terminal_pos] = '\0';
|
||||
terminal_process();
|
||||
//Back key
|
||||
}else if(c == '\x7f'){
|
||||
if(terminal_pos > 0){
|
||||
terminal_pos--;
|
||||
print("\x8 \x8");
|
||||
}
|
||||
//Special key
|
||||
}else if(c == '\x1b'){
|
||||
while(!io->available());
|
||||
io->read();
|
||||
while(!io->available());
|
||||
io->read();
|
||||
//Others
|
||||
}else{
|
||||
terminal_buffer[terminal_pos] = c;
|
||||
if(terminal_echo_mode){
|
||||
print(c);
|
||||
}
|
||||
|
||||
if(terminal_pos < terminal_buffer_size-1){
|
||||
terminal_pos++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Registers a command
|
||||
*/
|
||||
void Terminal::register_command(struct terminal_command *command){
|
||||
if(terminal_command_count >= terminal_max_commands) return;
|
||||
terminal_commands[terminal_command_count] = command;
|
||||
terminal_command_count = terminal_command_count + 1;
|
||||
}
|
||||
|
||||
void Terminal::displayHelp(bool parameter){
|
||||
char buffer[1000];
|
||||
uint32_t i;
|
||||
|
||||
if(parameter){
|
||||
println("Available parameters:");
|
||||
} else {
|
||||
print(documentation);
|
||||
println("Available commands:");
|
||||
}
|
||||
println("");
|
||||
|
||||
for(i=0; i<terminal_command_count; i++){
|
||||
const struct terminal_command *command = terminal_commands[i];
|
||||
|
||||
if((command->parameter!=0) != parameter){
|
||||
continue;
|
||||
}
|
||||
|
||||
int32_t namesize = strlen(command->name);
|
||||
int32_t descsize = strlen(command->description);
|
||||
int32_t typesize = (command->parameter_type == NULL) ? 0 : strlen(command->parameter_type);
|
||||
|
||||
memcpy(buffer, command->name, namesize);
|
||||
buffer[namesize++] = ' ';
|
||||
buffer[namesize++] = ' ';
|
||||
buffer[namesize++] = ':';
|
||||
buffer[namesize++] = ' ';
|
||||
buffer[namesize++] = ' ';
|
||||
// buffer[namesize++] = '\r';
|
||||
//buffer[namesize++] = '\n';
|
||||
//buffer[namesize++] = '\t';
|
||||
memcpy(buffer+namesize, command->description, descsize);
|
||||
if(typesize){
|
||||
buffer[namesize+descsize++] = ' ';
|
||||
buffer[namesize+descsize++] = '(';
|
||||
memcpy(buffer+namesize+descsize, command->parameter_type, typesize);
|
||||
buffer[namesize+descsize+typesize++] = ')';
|
||||
}
|
||||
buffer[namesize+descsize+typesize++] = '\r';
|
||||
buffer[namesize+descsize+typesize++] = '\n';
|
||||
write(buffer, namesize+descsize+typesize);
|
||||
}
|
||||
}
|
||||
|
||||
void Terminal::terminal_params_show(){
|
||||
for(uint32_t i=0; i<terminal_command_count; i++){
|
||||
const terminal_command *command = terminal_commands[i];
|
||||
if (command->parameter) {
|
||||
command->command(0, NULL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void Terminal::echo_command(uint32_t argc, char *argv[]){
|
||||
if(
|
||||
(argc == 1 && strcmp("on", argv[0])==0)
|
||||
||
|
||||
(argc == 0 && terminal_echo_mode==false)
|
||||
){
|
||||
terminal_echo_mode = true;
|
||||
println("Echo enabled");
|
||||
}else{
|
||||
terminal_echo_mode = false;
|
||||
println("Echo disabled");
|
||||
}
|
||||
}
|
||||
|
||||
void Terminal::params_command(uint32_t argc, char *argv[]){
|
||||
if(argc && strcmp(argv[0], "show")==0){
|
||||
terminal_params_show();
|
||||
}else{
|
||||
displayHelp(true);
|
||||
}
|
||||
}
|
||||
|
||||
bool Terminal::have_to_be_saved_in_eeprom(const terminal_command *command){
|
||||
if(command->parameter == NONE_PARAMETER) return false;
|
||||
if(command->eeprom_name[0] == '\0') return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void Terminal::save_value_to_eeprom(const terminal_command *command){
|
||||
if(!have_to_be_saved_in_eeprom(command)) return;
|
||||
|
||||
const constexpr bool read_only = false;
|
||||
preferences.begin(
|
||||
preferences_namespace, read_only, preferences_nvs_partition
|
||||
);
|
||||
switch(command->parameter){
|
||||
case FLOAT_PARAMETER :
|
||||
preferences.putFloat(command->eeprom_name, *static_cast<volatile const float*>(command->value_ptr));
|
||||
break;
|
||||
case DOUBLE_PARAMETER :
|
||||
preferences.putDouble(command->eeprom_name, *static_cast<volatile const double*>(command->value_ptr));
|
||||
break;
|
||||
case INT_PARAMETER :
|
||||
preferences.putInt(command->eeprom_name, *static_cast<volatile const int32_t*>(command->value_ptr));
|
||||
break;
|
||||
case UINT_PARAMETER :
|
||||
preferences.putUInt(command->eeprom_name, *static_cast<volatile const uint32_t*>(command->value_ptr));
|
||||
break;
|
||||
case LONGINT_PARAMETER :
|
||||
preferences.putLong64(command->eeprom_name, *static_cast<volatile const int64_t*>(command->value_ptr));
|
||||
break;
|
||||
case LONGUINT_PARAMETER :
|
||||
preferences.putULong64(command->eeprom_name, *static_cast<volatile const uint64_t*>(command->value_ptr));
|
||||
break;
|
||||
case BOOL_PARAMETER :
|
||||
preferences.putBool(command->eeprom_name, *static_cast<volatile const bool*>(command->value_ptr));
|
||||
break;
|
||||
case NONE_PARAMETER :
|
||||
default :
|
||||
println("Error -- line:" STRINGIFY(__LINE__) ", file" __FILE__ );
|
||||
break;
|
||||
}
|
||||
preferences.end();
|
||||
}
|
||||
|
||||
void Terminal::save_config_to_eeprom(){
|
||||
for(uint32_t i=0; i<terminal_command_count; i++){
|
||||
save_value_to_eeprom(terminal_commands[i]);
|
||||
}
|
||||
}
|
||||
|
||||
bool Terminal::save_config_to_eeprom(const char* eeprom_name){
|
||||
for(uint32_t i=0; i<terminal_command_count; i++){
|
||||
if(
|
||||
!strncmp(
|
||||
terminal_commands[i]->eeprom_name, eeprom_name,
|
||||
MAX_EEPROM_NAME_LENGTH
|
||||
)
|
||||
){
|
||||
save_value_to_eeprom(terminal_commands[i]);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void Terminal::print_value_from_eeprom(const struct terminal_command *command){
|
||||
if(!have_to_be_saved_in_eeprom(command)) return;
|
||||
const constexpr bool read_only = true;
|
||||
preferences.begin(
|
||||
preferences_namespace, read_only, preferences_nvs_partition
|
||||
);
|
||||
if( preferences.isKey(command->eeprom_name) ){
|
||||
print("\"");
|
||||
print(command->eeprom_name);
|
||||
print("\"");
|
||||
print(" : ");
|
||||
switch(command->parameter){
|
||||
case FLOAT_PARAMETER :
|
||||
print(preferences.getFloat(command->eeprom_name));
|
||||
break;
|
||||
case DOUBLE_PARAMETER :
|
||||
print(preferences.getDouble(command->eeprom_name));
|
||||
break;
|
||||
case INT_PARAMETER :
|
||||
print(preferences.getInt(command->eeprom_name));
|
||||
break;
|
||||
case UINT_PARAMETER :
|
||||
print(preferences.getUInt(command->eeprom_name));
|
||||
break;
|
||||
case LONGINT_PARAMETER :
|
||||
print(preferences.getLong64(command->eeprom_name));
|
||||
break;
|
||||
case LONGUINT_PARAMETER :
|
||||
print(preferences.getULong64(command->eeprom_name));
|
||||
break;
|
||||
case BOOL_PARAMETER :
|
||||
print(preferences.getBool(command->eeprom_name));
|
||||
break;
|
||||
case NONE_PARAMETER :
|
||||
default :
|
||||
println("Error -- line:" STRINGIFY(__LINE__) ", file" __FILE__ );
|
||||
break;
|
||||
}
|
||||
println(";");
|
||||
}
|
||||
preferences.end();
|
||||
}
|
||||
|
||||
void Terminal::print_config_from_eeprom(){
|
||||
println("{");
|
||||
for (uint32_t i=0; i<terminal_command_count; i++) {
|
||||
print_value_from_eeprom(terminal_commands[i]);
|
||||
}
|
||||
println("}");
|
||||
}
|
||||
|
||||
bool Terminal::print_config_from_eeprom(const char* eeprom_name){
|
||||
for(uint32_t i=0; i<terminal_command_count; i++){
|
||||
if(
|
||||
!strncmp(
|
||||
terminal_commands[i]->eeprom_name, eeprom_name,
|
||||
MAX_EEPROM_NAME_LENGTH
|
||||
)
|
||||
){
|
||||
print_value_from_eeprom(terminal_commands[i]);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void Terminal::reset_to_constructor_config(){
|
||||
print("Reseting to constructor configuration ... ");
|
||||
const constexpr bool read_only = false;
|
||||
preferences.begin(
|
||||
preferences_namespace, read_only, preferences_nvs_partition
|
||||
);
|
||||
preferences.clear();
|
||||
preferences.end();
|
||||
println("Done.");
|
||||
println("Please reset the card.");
|
||||
}
|
||||
bool Terminal::reset_to_constructor_config(const char* eeprom_name){
|
||||
for(uint32_t i=0; i<terminal_command_count; i++){
|
||||
if(
|
||||
!strncmp(
|
||||
terminal_commands[i]->eeprom_name, eeprom_name,
|
||||
MAX_EEPROM_NAME_LENGTH
|
||||
)
|
||||
){
|
||||
const constexpr bool read_only = false;
|
||||
preferences.begin(
|
||||
preferences_namespace, read_only, preferences_nvs_partition
|
||||
);
|
||||
if( preferences.isKey(eeprom_name) ){
|
||||
preferences.remove(eeprom_name);
|
||||
}
|
||||
preferences.end();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void Terminal::check_eeprom_names(){
|
||||
for(int j=1; j<terminal_command_count; j++){
|
||||
for(int i=0; i<j-1; i++){
|
||||
if(
|
||||
strncmp(terminal_commands[i]->eeprom_name, "", MAX_EEPROM_NAME_LENGTH)
|
||||
&&
|
||||
strncmp(terminal_commands[j]->eeprom_name, "", MAX_EEPROM_NAME_LENGTH)
|
||||
&&
|
||||
!strncmp(
|
||||
terminal_commands[i]->eeprom_name, terminal_commands[j]->eeprom_name,
|
||||
MAX_EEPROM_NAME_LENGTH
|
||||
)
|
||||
){
|
||||
while(true){
|
||||
print("Error : ");
|
||||
print(terminal_commands[i]->name);
|
||||
print(" and ");
|
||||
print(terminal_commands[i]->name);
|
||||
print(" have the same eeprom name : ");
|
||||
println(terminal_commands[i]->eeprom_name);
|
||||
delay(1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Terminal::print_saved_parameter(const terminal_command *command){
|
||||
if(!have_to_be_saved_in_eeprom(command)) return;
|
||||
print(command->name);
|
||||
print(" : ");
|
||||
switch(command->parameter){
|
||||
case FLOAT_PARAMETER :
|
||||
print(*static_cast<volatile float*>(command->value_ptr));
|
||||
break;
|
||||
case DOUBLE_PARAMETER :
|
||||
print(*static_cast<volatile double*>(command->value_ptr));
|
||||
break;
|
||||
case INT_PARAMETER :
|
||||
print(*static_cast<volatile int32_t*>(command->value_ptr));
|
||||
break;
|
||||
case UINT_PARAMETER :
|
||||
print(*static_cast<volatile uint32_t*>(command->value_ptr));
|
||||
break;
|
||||
case LONGINT_PARAMETER :
|
||||
print(*static_cast<volatile int64_t*>(command->value_ptr));
|
||||
break;
|
||||
case LONGUINT_PARAMETER :
|
||||
print(*static_cast<volatile uint64_t*>(command->value_ptr));
|
||||
break;
|
||||
case BOOL_PARAMETER :
|
||||
print(*static_cast<volatile bool*>(command->value_ptr));
|
||||
break;
|
||||
case NONE_PARAMETER :
|
||||
default :
|
||||
println("Error -- line:" STRINGIFY(__LINE__) ", file" __FILE__ );
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Terminal::load_value_from_eeprom(
|
||||
const terminal_command *command, bool enable_print
|
||||
){
|
||||
if(!have_to_be_saved_in_eeprom(command)) return;
|
||||
const constexpr bool read_only = true;
|
||||
preferences.begin(
|
||||
preferences_namespace, read_only, preferences_nvs_partition
|
||||
);
|
||||
if( preferences.isKey(command->eeprom_name) ){
|
||||
switch(command->parameter){
|
||||
case FLOAT_PARAMETER :
|
||||
*static_cast<volatile float*>(command->value_ptr) =
|
||||
preferences.getFloat(command->eeprom_name);
|
||||
break;
|
||||
case DOUBLE_PARAMETER :
|
||||
*static_cast<volatile double*>(command->value_ptr) =
|
||||
preferences.getDouble(command->eeprom_name);
|
||||
break;
|
||||
case INT_PARAMETER :
|
||||
*static_cast<volatile int32_t*>(command->value_ptr) =
|
||||
preferences.getInt(command->eeprom_name);
|
||||
break;
|
||||
case UINT_PARAMETER :
|
||||
*static_cast<volatile uint32_t*>(command->value_ptr) =
|
||||
preferences.getUInt(command->eeprom_name);
|
||||
break;
|
||||
case LONGINT_PARAMETER :
|
||||
*static_cast<volatile int64_t*>(command->value_ptr) =
|
||||
preferences.getLong64(command->eeprom_name);
|
||||
break;
|
||||
case LONGUINT_PARAMETER :
|
||||
*static_cast<volatile uint64_t*>(command->value_ptr) =
|
||||
preferences.getULong64(command->eeprom_name);
|
||||
break;
|
||||
case BOOL_PARAMETER :
|
||||
*static_cast<volatile bool*>(command->value_ptr) =
|
||||
preferences.getBool(command->eeprom_name);
|
||||
break;
|
||||
case NONE_PARAMETER :
|
||||
default :
|
||||
println("Error -- line:" STRINGIFY(__LINE__) ", file" __FILE__ );
|
||||
break;
|
||||
}
|
||||
if(enable_print){ print("(eeprom ) "); }
|
||||
}else{
|
||||
if(enable_print){ print("(construction) "); }
|
||||
}
|
||||
if(enable_print){ print_saved_parameter(command); println("");}
|
||||
preferences.end();
|
||||
}
|
||||
|
||||
void Terminal::load_config_from_eeprom(bool enable_print){
|
||||
if(enable_print){ println("Loading config :"); }
|
||||
for (uint32_t i=0; i<terminal_command_count; i++) {
|
||||
load_value_from_eeprom(terminal_commands[i], enable_print);
|
||||
}
|
||||
if(enable_print){ println("Config is Loaded."); }
|
||||
}
|
||||
|
||||
bool Terminal::load_config_from_eeprom(
|
||||
const char* eeprom_name, bool enable_print
|
||||
){
|
||||
for(uint32_t i=0; i<terminal_command_count; i++){
|
||||
if(
|
||||
!strncmp(
|
||||
terminal_commands[i]->eeprom_name, eeprom_name,
|
||||
MAX_EEPROM_NAME_LENGTH
|
||||
)
|
||||
){
|
||||
load_value_from_eeprom(terminal_commands[i], enable_print);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
}
|
Reference in New Issue
Block a user