mirror of
https://github.com/markqvist/Reticulum.git
synced 2024-11-30 01:00:18 +00:00
Added channel stat reporting and airtime controls to RNode interface
This commit is contained in:
parent
67c468884f
commit
13c45cc59a
@ -43,21 +43,21 @@ class KISS():
|
|||||||
CMD_CR = 0x05
|
CMD_CR = 0x05
|
||||||
CMD_RADIO_STATE = 0x06
|
CMD_RADIO_STATE = 0x06
|
||||||
CMD_RADIO_LOCK = 0x07
|
CMD_RADIO_LOCK = 0x07
|
||||||
|
CMD_ST_ALOCK = 0x0B
|
||||||
|
CMD_LT_ALOCK = 0x0C
|
||||||
CMD_DETECT = 0x08
|
CMD_DETECT = 0x08
|
||||||
CMD_IMPLICIT = 0x09
|
|
||||||
CMD_LEAVE = 0x0A
|
CMD_LEAVE = 0x0A
|
||||||
CMD_READY = 0x0F
|
CMD_READY = 0x0F
|
||||||
CMD_STAT_RX = 0x21
|
CMD_STAT_RX = 0x21
|
||||||
CMD_STAT_TX = 0x22
|
CMD_STAT_TX = 0x22
|
||||||
CMD_STAT_RSSI = 0x23
|
CMD_STAT_RSSI = 0x23
|
||||||
CMD_STAT_SNR = 0x24
|
CMD_STAT_SNR = 0x24
|
||||||
|
CMD_STAT_CHTM = 0x25
|
||||||
CMD_BLINK = 0x30
|
CMD_BLINK = 0x30
|
||||||
CMD_RANDOM = 0x40
|
CMD_RANDOM = 0x40
|
||||||
CMD_FB_EXT = 0x41
|
CMD_FB_EXT = 0x41
|
||||||
CMD_FB_READ = 0x42
|
CMD_FB_READ = 0x42
|
||||||
CMD_FB_WRITE = 0x43
|
CMD_FB_WRITE = 0x43
|
||||||
CMD_FB_READL = 0x44
|
|
||||||
CMD_BT_CTRL = 0x46
|
|
||||||
CMD_PLATFORM = 0x48
|
CMD_PLATFORM = 0x48
|
||||||
CMD_MCU = 0x49
|
CMD_MCU = 0x49
|
||||||
CMD_FW_VERSION = 0x50
|
CMD_FW_VERSION = 0x50
|
||||||
@ -315,7 +315,7 @@ class RNodeInterface(Interface):
|
|||||||
self, owner, name, port, frequency = None, bandwidth = None, txpower = None,
|
self, owner, name, port, frequency = None, bandwidth = None, txpower = None,
|
||||||
sf = None, cr = None, flow_control = False, id_interval = None,
|
sf = None, cr = None, flow_control = False, id_interval = None,
|
||||||
allow_bluetooth = False, target_device_name = None,
|
allow_bluetooth = False, target_device_name = None,
|
||||||
target_device_address = None, id_callsign = None):
|
target_device_address = None, id_callsign = None, st_alock = None, lt_alock = None):
|
||||||
import importlib
|
import importlib
|
||||||
if RNS.vendor.platformutils.is_android():
|
if RNS.vendor.platformutils.is_android():
|
||||||
self.on_android = True
|
self.on_android = True
|
||||||
@ -373,6 +373,8 @@ class RNodeInterface(Interface):
|
|||||||
self.cr = cr
|
self.cr = cr
|
||||||
self.state = KISS.RADIO_STATE_OFF
|
self.state = KISS.RADIO_STATE_OFF
|
||||||
self.bitrate = 0
|
self.bitrate = 0
|
||||||
|
self.st_alock = st_alock
|
||||||
|
self.lt_alock = lt_alock
|
||||||
self.platform = None
|
self.platform = None
|
||||||
self.display = None
|
self.display = None
|
||||||
self.mcu = None
|
self.mcu = None
|
||||||
@ -396,7 +398,13 @@ class RNodeInterface(Interface):
|
|||||||
self.r_stat_tx = None
|
self.r_stat_tx = None
|
||||||
self.r_stat_rssi = None
|
self.r_stat_rssi = None
|
||||||
self.r_stat_snr = None
|
self.r_stat_snr = None
|
||||||
|
self.r_st_alock = None
|
||||||
|
self.r_lt_alock = None
|
||||||
self.r_random = None
|
self.r_random = None
|
||||||
|
self.r_airtime_short = 0.0
|
||||||
|
self.r_airtime_long = 0.0
|
||||||
|
self.r_channel_load_short = 0.0
|
||||||
|
self.r_channel_load_long = 0.0
|
||||||
|
|
||||||
self.packet_queue = []
|
self.packet_queue = []
|
||||||
self.flow_control = flow_control
|
self.flow_control = flow_control
|
||||||
@ -427,6 +435,14 @@ class RNodeInterface(Interface):
|
|||||||
RNS.log("Invalid coding rate configured for "+str(self), RNS.LOG_ERROR)
|
RNS.log("Invalid coding rate configured for "+str(self), RNS.LOG_ERROR)
|
||||||
self.validcfg = False
|
self.validcfg = False
|
||||||
|
|
||||||
|
if (self.st_alock and (self.st_alock < 0.0 or self.st_alock > 100.0)):
|
||||||
|
RNS.log("Invalid short-term airtime limit configured for "+str(self), RNS.LOG_ERROR)
|
||||||
|
self.validcfg = False
|
||||||
|
|
||||||
|
if (self.lt_alock and (self.lt_alock < 0.0 or self.lt_alock > 100.0)):
|
||||||
|
RNS.log("Invalid long-term airtime limit configured for "+str(self), RNS.LOG_ERROR)
|
||||||
|
self.validcfg = False
|
||||||
|
|
||||||
if id_interval != None and id_callsign != None:
|
if id_interval != None and id_callsign != None:
|
||||||
if (len(id_callsign.encode("utf-8")) <= RNodeInterface.CALLSIGN_MAX_LEN):
|
if (len(id_callsign.encode("utf-8")) <= RNodeInterface.CALLSIGN_MAX_LEN):
|
||||||
self.should_id = True
|
self.should_id = True
|
||||||
@ -617,6 +633,12 @@ class RNodeInterface(Interface):
|
|||||||
self.setCodingRate()
|
self.setCodingRate()
|
||||||
time.sleep(0.15)
|
time.sleep(0.15)
|
||||||
|
|
||||||
|
self.setSTALock()
|
||||||
|
time.sleep(0.15)
|
||||||
|
|
||||||
|
self.setLTALock()
|
||||||
|
time.sleep(0.15)
|
||||||
|
|
||||||
self.setRadioState(KISS.RADIO_STATE_ON)
|
self.setRadioState(KISS.RADIO_STATE_ON)
|
||||||
time.sleep(0.15)
|
time.sleep(0.15)
|
||||||
|
|
||||||
@ -741,6 +763,30 @@ class RNodeInterface(Interface):
|
|||||||
if written != len(kiss_command):
|
if written != len(kiss_command):
|
||||||
raise IOError("An IO error occurred while configuring coding rate for "+str(self))
|
raise IOError("An IO error occurred while configuring coding rate for "+str(self))
|
||||||
|
|
||||||
|
def setSTALock(self):
|
||||||
|
if self.st_alock != None:
|
||||||
|
at = int(self.st_alock*100)
|
||||||
|
c1 = at >> 8 & 0xFF
|
||||||
|
c2 = at & 0xFF
|
||||||
|
data = KISS.escape(bytes([c1])+bytes([c2]))
|
||||||
|
|
||||||
|
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_ST_ALOCK])+data+bytes([KISS.FEND])
|
||||||
|
written = self.write_mux(kiss_command)
|
||||||
|
if written != len(kiss_command):
|
||||||
|
raise IOError("An IO error occurred while configuring short-term airtime limit for "+str(self))
|
||||||
|
|
||||||
|
def setLTALock(self):
|
||||||
|
if self.lt_alock != None:
|
||||||
|
at = int(self.lt_alock*100)
|
||||||
|
c1 = at >> 8 & 0xFF
|
||||||
|
c2 = at & 0xFF
|
||||||
|
data = KISS.escape(bytes([c1])+bytes([c2]))
|
||||||
|
|
||||||
|
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_LT_ALOCK])+data+bytes([KISS.FEND])
|
||||||
|
written = self.write_mux(kiss_command)
|
||||||
|
if written != len(kiss_command):
|
||||||
|
raise IOError("An IO error occurred while configuring long-term airtime limit for "+str(self))
|
||||||
|
|
||||||
def setRadioState(self, state):
|
def setRadioState(self, state):
|
||||||
self.state = state
|
self.state = state
|
||||||
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_RADIO_STATE])+bytes([state])+bytes([KISS.FEND])
|
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_RADIO_STATE])+bytes([state])+bytes([KISS.FEND])
|
||||||
@ -994,6 +1040,57 @@ class RNodeInterface(Interface):
|
|||||||
self.r_stat_rssi = byte-RNodeInterface.RSSI_OFFSET
|
self.r_stat_rssi = byte-RNodeInterface.RSSI_OFFSET
|
||||||
elif (command == KISS.CMD_STAT_SNR):
|
elif (command == KISS.CMD_STAT_SNR):
|
||||||
self.r_stat_snr = int.from_bytes(bytes([byte]), byteorder="big", signed=True) * 0.25
|
self.r_stat_snr = int.from_bytes(bytes([byte]), byteorder="big", signed=True) * 0.25
|
||||||
|
elif (command == KISS.CMD_ST_ALOCK):
|
||||||
|
if (byte == KISS.FESC):
|
||||||
|
escape = True
|
||||||
|
else:
|
||||||
|
if (escape):
|
||||||
|
if (byte == KISS.TFEND):
|
||||||
|
byte = KISS.FEND
|
||||||
|
if (byte == KISS.TFESC):
|
||||||
|
byte = KISS.FESC
|
||||||
|
escape = False
|
||||||
|
command_buffer = command_buffer+bytes([byte])
|
||||||
|
if (len(command_buffer) == 2):
|
||||||
|
at = command_buffer[0] << 8 | command_buffer[1]
|
||||||
|
self.r_st_alock = at/100.0
|
||||||
|
RNS.log(str(self)+" Radio reporting short-term airtime limit is "+str(self.r_st_alock)+"%", RNS.LOG_DEBUG)
|
||||||
|
elif (command == KISS.CMD_LT_ALOCK):
|
||||||
|
if (byte == KISS.FESC):
|
||||||
|
escape = True
|
||||||
|
else:
|
||||||
|
if (escape):
|
||||||
|
if (byte == KISS.TFEND):
|
||||||
|
byte = KISS.FEND
|
||||||
|
if (byte == KISS.TFESC):
|
||||||
|
byte = KISS.FESC
|
||||||
|
escape = False
|
||||||
|
command_buffer = command_buffer+bytes([byte])
|
||||||
|
if (len(command_buffer) == 2):
|
||||||
|
at = command_buffer[0] << 8 | command_buffer[1]
|
||||||
|
self.r_lt_alock = at/100.0
|
||||||
|
RNS.log(str(self)+" Radio reporting long-term airtime limit is "+str(self.r_lt_alock)+"%", RNS.LOG_DEBUG)
|
||||||
|
elif (command == KISS.CMD_STAT_CHTM):
|
||||||
|
if (byte == KISS.FESC):
|
||||||
|
escape = True
|
||||||
|
else:
|
||||||
|
if (escape):
|
||||||
|
if (byte == KISS.TFEND):
|
||||||
|
byte = KISS.FEND
|
||||||
|
if (byte == KISS.TFESC):
|
||||||
|
byte = KISS.FESC
|
||||||
|
escape = False
|
||||||
|
command_buffer = command_buffer+bytes([byte])
|
||||||
|
if (len(command_buffer) == 8):
|
||||||
|
ats = command_buffer[0] << 8 | command_buffer[1]
|
||||||
|
atl = command_buffer[2] << 8 | command_buffer[3]
|
||||||
|
cus = command_buffer[4] << 8 | command_buffer[5]
|
||||||
|
cul = command_buffer[6] << 8 | command_buffer[7]
|
||||||
|
|
||||||
|
self.r_airtime_short = ats/100.0
|
||||||
|
self.r_airtime_long = atl/100.0
|
||||||
|
self.r_channel_load_short = cus/100.0
|
||||||
|
self.r_channel_load_long = cul/100.0
|
||||||
elif (command == KISS.CMD_RANDOM):
|
elif (command == KISS.CMD_RANDOM):
|
||||||
self.r_random = byte
|
self.r_random = byte
|
||||||
elif (command == KISS.CMD_PLATFORM):
|
elif (command == KISS.CMD_PLATFORM):
|
||||||
|
@ -43,6 +43,8 @@ class KISS():
|
|||||||
CMD_CR = 0x05
|
CMD_CR = 0x05
|
||||||
CMD_RADIO_STATE = 0x06
|
CMD_RADIO_STATE = 0x06
|
||||||
CMD_RADIO_LOCK = 0x07
|
CMD_RADIO_LOCK = 0x07
|
||||||
|
CMD_ST_ALOCK = 0x0B
|
||||||
|
CMD_LT_ALOCK = 0x0C
|
||||||
CMD_DETECT = 0x08
|
CMD_DETECT = 0x08
|
||||||
CMD_LEAVE = 0x0A
|
CMD_LEAVE = 0x0A
|
||||||
CMD_READY = 0x0F
|
CMD_READY = 0x0F
|
||||||
@ -50,6 +52,7 @@ class KISS():
|
|||||||
CMD_STAT_TX = 0x22
|
CMD_STAT_TX = 0x22
|
||||||
CMD_STAT_RSSI = 0x23
|
CMD_STAT_RSSI = 0x23
|
||||||
CMD_STAT_SNR = 0x24
|
CMD_STAT_SNR = 0x24
|
||||||
|
CMD_STAT_CHTM = 0x25
|
||||||
CMD_BLINK = 0x30
|
CMD_BLINK = 0x30
|
||||||
CMD_RANDOM = 0x40
|
CMD_RANDOM = 0x40
|
||||||
CMD_FB_EXT = 0x41
|
CMD_FB_EXT = 0x41
|
||||||
@ -98,7 +101,7 @@ class RNodeInterface(Interface):
|
|||||||
|
|
||||||
RECONNECT_WAIT = 5
|
RECONNECT_WAIT = 5
|
||||||
|
|
||||||
def __init__(self, owner, name, port, frequency = None, bandwidth = None, txpower = None, sf = None, cr = None, flow_control = False, id_interval = None, id_callsign = None):
|
def __init__(self, owner, name, port, frequency = None, bandwidth = None, txpower = None, sf = None, cr = None, flow_control = False, id_interval = None, id_callsign = None, st_alock = None, lt_alock = None):
|
||||||
if RNS.vendor.platformutils.is_android():
|
if RNS.vendor.platformutils.is_android():
|
||||||
raise SystemError("Invlaid interface type. The Android-specific RNode interface must be used on Android")
|
raise SystemError("Invlaid interface type. The Android-specific RNode interface must be used on Android")
|
||||||
|
|
||||||
@ -135,6 +138,8 @@ class RNodeInterface(Interface):
|
|||||||
self.cr = cr
|
self.cr = cr
|
||||||
self.state = KISS.RADIO_STATE_OFF
|
self.state = KISS.RADIO_STATE_OFF
|
||||||
self.bitrate = 0
|
self.bitrate = 0
|
||||||
|
self.st_alock = st_alock
|
||||||
|
self.lt_alock = lt_alock
|
||||||
self.platform = None
|
self.platform = None
|
||||||
self.display = None
|
self.display = None
|
||||||
self.mcu = None
|
self.mcu = None
|
||||||
@ -158,7 +163,13 @@ class RNodeInterface(Interface):
|
|||||||
self.r_stat_tx = None
|
self.r_stat_tx = None
|
||||||
self.r_stat_rssi = None
|
self.r_stat_rssi = None
|
||||||
self.r_stat_snr = None
|
self.r_stat_snr = None
|
||||||
|
self.r_st_alock = None
|
||||||
|
self.r_lt_alock = None
|
||||||
self.r_random = None
|
self.r_random = None
|
||||||
|
self.r_airtime_short = 0.0
|
||||||
|
self.r_airtime_long = 0.0
|
||||||
|
self.r_channel_load_short = 0.0
|
||||||
|
self.r_channel_load_long = 0.0
|
||||||
|
|
||||||
self.packet_queue = []
|
self.packet_queue = []
|
||||||
self.flow_control = flow_control
|
self.flow_control = flow_control
|
||||||
@ -186,6 +197,14 @@ class RNodeInterface(Interface):
|
|||||||
RNS.log("Invalid coding rate configured for "+str(self), RNS.LOG_ERROR)
|
RNS.log("Invalid coding rate configured for "+str(self), RNS.LOG_ERROR)
|
||||||
self.validcfg = False
|
self.validcfg = False
|
||||||
|
|
||||||
|
if (self.st_alock and (self.st_alock < 0.0 or self.st_alock > 100.0)):
|
||||||
|
RNS.log("Invalid short-term airtime limit configured for "+str(self), RNS.LOG_ERROR)
|
||||||
|
self.validcfg = False
|
||||||
|
|
||||||
|
if (self.lt_alock and (self.lt_alock < 0.0 or self.lt_alock > 100.0)):
|
||||||
|
RNS.log("Invalid long-term airtime limit configured for "+str(self), RNS.LOG_ERROR)
|
||||||
|
self.validcfg = False
|
||||||
|
|
||||||
if id_interval != None and id_callsign != None:
|
if id_interval != None and id_callsign != None:
|
||||||
if (len(id_callsign.encode("utf-8")) <= RNodeInterface.CALLSIGN_MAX_LEN):
|
if (len(id_callsign.encode("utf-8")) <= RNodeInterface.CALLSIGN_MAX_LEN):
|
||||||
self.should_id = True
|
self.should_id = True
|
||||||
@ -281,6 +300,8 @@ class RNodeInterface(Interface):
|
|||||||
self.setTXPower()
|
self.setTXPower()
|
||||||
self.setSpreadingFactor()
|
self.setSpreadingFactor()
|
||||||
self.setCodingRate()
|
self.setCodingRate()
|
||||||
|
self.setSTALock()
|
||||||
|
self.setLTALock()
|
||||||
self.setRadioState(KISS.RADIO_STATE_ON)
|
self.setRadioState(KISS.RADIO_STATE_ON)
|
||||||
|
|
||||||
def detect(self):
|
def detect(self):
|
||||||
@ -385,6 +406,30 @@ class RNodeInterface(Interface):
|
|||||||
if written != len(kiss_command):
|
if written != len(kiss_command):
|
||||||
raise IOError("An IO error occurred while configuring coding rate for "+str(self))
|
raise IOError("An IO error occurred while configuring coding rate for "+str(self))
|
||||||
|
|
||||||
|
def setSTALock(self):
|
||||||
|
if self.st_alock != None:
|
||||||
|
at = int(self.st_alock*100)
|
||||||
|
c1 = at >> 8 & 0xFF
|
||||||
|
c2 = at & 0xFF
|
||||||
|
data = KISS.escape(bytes([c1])+bytes([c2]))
|
||||||
|
|
||||||
|
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_ST_ALOCK])+data+bytes([KISS.FEND])
|
||||||
|
written = self.serial.write(kiss_command)
|
||||||
|
if written != len(kiss_command):
|
||||||
|
raise IOError("An IO error occurred while configuring short-term airtime limit for "+str(self))
|
||||||
|
|
||||||
|
def setLTALock(self):
|
||||||
|
if self.lt_alock != None:
|
||||||
|
at = int(self.lt_alock*100)
|
||||||
|
c1 = at >> 8 & 0xFF
|
||||||
|
c2 = at & 0xFF
|
||||||
|
data = KISS.escape(bytes([c1])+bytes([c2]))
|
||||||
|
|
||||||
|
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_LT_ALOCK])+data+bytes([KISS.FEND])
|
||||||
|
written = self.serial.write(kiss_command)
|
||||||
|
if written != len(kiss_command):
|
||||||
|
raise IOError("An IO error occurred while configuring long-term airtime limit for "+str(self))
|
||||||
|
|
||||||
def setRadioState(self, state):
|
def setRadioState(self, state):
|
||||||
self.state = state
|
self.state = state
|
||||||
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_RADIO_STATE])+bytes([state])+bytes([KISS.FEND])
|
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_RADIO_STATE])+bytes([state])+bytes([KISS.FEND])
|
||||||
@ -622,6 +667,57 @@ class RNodeInterface(Interface):
|
|||||||
self.r_stat_rssi = byte-RNodeInterface.RSSI_OFFSET
|
self.r_stat_rssi = byte-RNodeInterface.RSSI_OFFSET
|
||||||
elif (command == KISS.CMD_STAT_SNR):
|
elif (command == KISS.CMD_STAT_SNR):
|
||||||
self.r_stat_snr = int.from_bytes(bytes([byte]), byteorder="big", signed=True) * 0.25
|
self.r_stat_snr = int.from_bytes(bytes([byte]), byteorder="big", signed=True) * 0.25
|
||||||
|
elif (command == KISS.CMD_ST_ALOCK):
|
||||||
|
if (byte == KISS.FESC):
|
||||||
|
escape = True
|
||||||
|
else:
|
||||||
|
if (escape):
|
||||||
|
if (byte == KISS.TFEND):
|
||||||
|
byte = KISS.FEND
|
||||||
|
if (byte == KISS.TFESC):
|
||||||
|
byte = KISS.FESC
|
||||||
|
escape = False
|
||||||
|
command_buffer = command_buffer+bytes([byte])
|
||||||
|
if (len(command_buffer) == 2):
|
||||||
|
at = command_buffer[0] << 8 | command_buffer[1]
|
||||||
|
self.r_st_alock = at/100.0
|
||||||
|
RNS.log(str(self)+" Radio reporting short-term airtime limit is "+str(self.r_st_alock)+"%", RNS.LOG_DEBUG)
|
||||||
|
elif (command == KISS.CMD_LT_ALOCK):
|
||||||
|
if (byte == KISS.FESC):
|
||||||
|
escape = True
|
||||||
|
else:
|
||||||
|
if (escape):
|
||||||
|
if (byte == KISS.TFEND):
|
||||||
|
byte = KISS.FEND
|
||||||
|
if (byte == KISS.TFESC):
|
||||||
|
byte = KISS.FESC
|
||||||
|
escape = False
|
||||||
|
command_buffer = command_buffer+bytes([byte])
|
||||||
|
if (len(command_buffer) == 2):
|
||||||
|
at = command_buffer[0] << 8 | command_buffer[1]
|
||||||
|
self.r_lt_alock = at/100.0
|
||||||
|
RNS.log(str(self)+" Radio reporting long-term airtime limit is "+str(self.r_lt_alock)+"%", RNS.LOG_DEBUG)
|
||||||
|
elif (command == KISS.CMD_STAT_CHTM):
|
||||||
|
if (byte == KISS.FESC):
|
||||||
|
escape = True
|
||||||
|
else:
|
||||||
|
if (escape):
|
||||||
|
if (byte == KISS.TFEND):
|
||||||
|
byte = KISS.FEND
|
||||||
|
if (byte == KISS.TFESC):
|
||||||
|
byte = KISS.FESC
|
||||||
|
escape = False
|
||||||
|
command_buffer = command_buffer+bytes([byte])
|
||||||
|
if (len(command_buffer) == 8):
|
||||||
|
ats = command_buffer[0] << 8 | command_buffer[1]
|
||||||
|
atl = command_buffer[2] << 8 | command_buffer[3]
|
||||||
|
cus = command_buffer[4] << 8 | command_buffer[5]
|
||||||
|
cul = command_buffer[6] << 8 | command_buffer[7]
|
||||||
|
|
||||||
|
self.r_airtime_short = ats/100.0
|
||||||
|
self.r_airtime_long = atl/100.0
|
||||||
|
self.r_channel_load_short = cus/100.0
|
||||||
|
self.r_channel_load_long = cul/100.0
|
||||||
elif (command == KISS.CMD_RANDOM):
|
elif (command == KISS.CMD_RANDOM):
|
||||||
self.r_random = byte
|
self.r_random = byte
|
||||||
elif (command == KISS.CMD_PLATFORM):
|
elif (command == KISS.CMD_PLATFORM):
|
||||||
|
Loading…
Reference in New Issue
Block a user