Add button tests
This is implemented by connecting the DTR pin of the serial device to the BTN_UP pin of the watch. Also, make it possible to flash different applications with the Makefile. Resolves #4
This commit is contained in:
@@ -54,9 +54,9 @@ public:
|
||||
static ButtonManager *m_instance;
|
||||
|
||||
enum Button {
|
||||
UP = 0,
|
||||
DOWN = 0,
|
||||
MID,
|
||||
DOWN,
|
||||
UP,
|
||||
Count
|
||||
};
|
||||
|
||||
|
||||
@@ -27,10 +27,13 @@
|
||||
using BSP::Schedule::Task;
|
||||
using BSP::Schedule::NextTime;
|
||||
using BSP::ReturnCode;
|
||||
using BSP::ButtonManager;
|
||||
using Button = ButtonManager::Button;
|
||||
using ButtonState = ButtonManager::ButtonState;
|
||||
|
||||
ScreenManager::ScreenManager(BSP::Schedule::TaskScheduler &scheduler,
|
||||
BSP::DisplayDriver &display,
|
||||
BSP::ButtonManager &buttons)
|
||||
ButtonManager &buttons)
|
||||
: m_scheduler(scheduler)
|
||||
, m_screen_stack{nullptr}
|
||||
, m_screen_stack_depth(0)
|
||||
@@ -43,23 +46,23 @@ ScreenManager::ScreenManager(BSP::Schedule::TaskScheduler &scheduler,
|
||||
ReturnCode ScreenManager::init()
|
||||
{
|
||||
m_buttons.set_callback(
|
||||
BSP::ButtonManager::Button::UP,
|
||||
[this](BSP::ButtonManager::ButtonState state) {
|
||||
if (state == BSP::ButtonManager::ButtonState::PRESSED) {
|
||||
Button::UP,
|
||||
[this](ButtonState state) {
|
||||
if (state == ButtonState::PRESSED) {
|
||||
current_screen()->notify_up_button();
|
||||
}});
|
||||
|
||||
m_buttons.set_callback(
|
||||
BSP::ButtonManager::Button::MID,
|
||||
[this](BSP::ButtonManager::ButtonState state) {
|
||||
if (state == BSP::ButtonManager::ButtonState::PRESSED) {
|
||||
Button::MID,
|
||||
[this](ButtonState state) {
|
||||
if (state == ButtonState::PRESSED) {
|
||||
return current_screen()->notify_middle_button();
|
||||
}});
|
||||
|
||||
m_buttons.set_callback(
|
||||
BSP::ButtonManager::Button::DOWN,
|
||||
[this](BSP::ButtonManager::ButtonState state) {
|
||||
if (state == BSP::ButtonManager::ButtonState::PRESSED) {
|
||||
Button::DOWN,
|
||||
[this](ButtonState state) {
|
||||
if (state == ButtonState::PRESSED) {
|
||||
current_screen()->notify_down_button();
|
||||
}});
|
||||
|
||||
|
||||
@@ -40,47 +40,47 @@
|
||||
|
||||
#include "Mcu.h"
|
||||
|
||||
using BSP::Time;
|
||||
using namespace BSP;
|
||||
|
||||
// GPIOs
|
||||
|
||||
static BSP::GpioDriver g_gpioa(GPIOA);
|
||||
static GpioDriver g_gpioa(GPIOA);
|
||||
|
||||
static BSP::GpioPin g_dbg0(g_gpioa, 3);
|
||||
static BSP::GpioPin g_dbg1(g_gpioa, 6);
|
||||
static GpioPin g_dbg0(g_gpioa, 3);
|
||||
static GpioPin g_dbg1(g_gpioa, 6);
|
||||
|
||||
static BSP::GpioPin g_tx(g_gpioa, 9);
|
||||
static BSP::GpioPin g_rx(g_gpioa, 10);
|
||||
static GpioPin g_tx(g_gpioa, 9);
|
||||
static GpioPin g_rx(g_gpioa, 10);
|
||||
|
||||
static BSP::GpioPin g_btn_down(g_gpioa, 0);
|
||||
static BSP::GpioPin g_btn_mid(g_gpioa, 1);
|
||||
static BSP::GpioPin g_btn_up(g_gpioa, 2);
|
||||
static GpioPin g_btn_down(g_gpioa, 0);
|
||||
static GpioPin g_btn_mid(g_gpioa, 1);
|
||||
static GpioPin g_btn_up(g_gpioa, 2);
|
||||
|
||||
static BSP::GpioPin g_nss(g_gpioa, 4);
|
||||
static BSP::GpioPin g_sck(g_gpioa, 5);
|
||||
static BSP::GpioPin g_mosi(g_gpioa, 12);
|
||||
static BSP::GpioPin g_extcomm(g_gpioa, 7);
|
||||
static GpioPin g_nss(g_gpioa, 4);
|
||||
static GpioPin g_sck(g_gpioa, 5);
|
||||
static GpioPin g_mosi(g_gpioa, 12);
|
||||
static GpioPin g_extcomm(g_gpioa, 7);
|
||||
|
||||
// Scheduler and Tasks
|
||||
|
||||
static BSP::Schedule::LowPowerTaskScheduler<5> g_sched;
|
||||
static BSP::SpiDriver g_spi(g_sched, g_nss);
|
||||
static BSP::DisplayDriver g_display(g_sched, g_spi);
|
||||
static BSP::LptimPwm g_lptim_pwm(LPTIM1);
|
||||
static BSP::ButtonManager g_btn_manager(
|
||||
static Schedule::LowPowerTaskScheduler<5> g_sched;
|
||||
static SpiDriver g_spi(g_sched, g_nss);
|
||||
static DisplayDriver g_display(g_sched, g_spi);
|
||||
static LptimPwm g_lptim_pwm(LPTIM1);
|
||||
static ButtonManager g_btn_mgr(
|
||||
g_sched, g_btn_up, g_btn_mid, g_btn_down, Time::millis(200));
|
||||
|
||||
// Screens- contexts for the display
|
||||
static ScreenManager g_screen_manager(g_sched, g_display, g_btn_manager);
|
||||
static SetTimeScreen g_set_time_screen(g_display, g_screen_manager);
|
||||
static SetTimeScreen g_set_date_screen(g_display, g_screen_manager);
|
||||
static StopwatchScreen g_stopwatch_screen(g_display, g_screen_manager);
|
||||
static ScreenManager g_screen_mgr(g_sched, g_display, g_btn_mgr);
|
||||
static SetTimeScreen g_set_time_screen(g_display, g_screen_mgr);
|
||||
static SetTimeScreen g_set_date_screen(g_display, g_screen_mgr);
|
||||
static StopwatchScreen g_stopwatch_screen(g_display, g_screen_mgr);
|
||||
static MenuScreen g_set_face_screen(g_display,
|
||||
g_screen_manager,
|
||||
g_screen_mgr,
|
||||
"Face",
|
||||
std::initializer_list<MenuScreenItem>());
|
||||
static MenuScreen g_settings_menu_screen(g_display,
|
||||
g_screen_manager,
|
||||
g_screen_mgr,
|
||||
"Settings",
|
||||
std::initializer_list<MenuScreenItem>(
|
||||
{
|
||||
@@ -89,45 +89,45 @@ static MenuScreen g_settings_menu_screen(g_display,
|
||||
MenuScreenItem("Set Face", g_set_face_screen)
|
||||
}));
|
||||
static MenuScreen g_apps_menu_screen(g_display,
|
||||
g_screen_manager,
|
||||
g_screen_mgr,
|
||||
"Apps", std::initializer_list<MenuScreenItem>({MenuScreenItem("Stopwatch", g_stopwatch_screen)}));
|
||||
static MenuScreen g_main_menu_screen(g_display,
|
||||
g_screen_manager,
|
||||
g_screen_mgr,
|
||||
"Main Menu",
|
||||
std::initializer_list<MenuScreenItem>(
|
||||
{
|
||||
MenuScreenItem("Apps", g_apps_menu_screen),
|
||||
MenuScreenItem("Settings", g_settings_menu_screen)
|
||||
}));
|
||||
static AnalogTimeScreen g_analog_time_screen(g_display, g_screen_manager, g_main_menu_screen);
|
||||
static BigDigitalTimeScreen g_digital_time_screen(g_display, g_screen_manager, g_main_menu_screen);
|
||||
static AnalogTimeScreen g_analog_time_screen(g_display, g_screen_mgr, g_main_menu_screen);
|
||||
static BigDigitalTimeScreen g_digital_time_screen(g_display, g_screen_mgr, g_main_menu_screen);
|
||||
|
||||
[[noreturn]] void main() {
|
||||
|
||||
// Set up the system clock
|
||||
BSP::RtcDriver::init();
|
||||
BSP::SystemTimer::set_timer(BSP::RtcDriver::get_system_timer());
|
||||
BSP::LowPower::init();
|
||||
RtcDriver::init();
|
||||
SystemTimer::set_timer(RtcDriver::get_system_timer());
|
||||
LowPower::init();
|
||||
|
||||
// Initialize the tasks
|
||||
g_lptim_pwm.init();
|
||||
g_spi.init();
|
||||
g_btn_manager.init();
|
||||
g_btn_mgr.init();
|
||||
g_display.init();
|
||||
g_screen_manager.init();
|
||||
g_screen_manager.set_root_screen(g_analog_time_screen);
|
||||
g_screen_mgr.init();
|
||||
g_screen_mgr.set_root_screen(g_analog_time_screen);
|
||||
|
||||
g_set_face_screen.add_item(MenuScreenItem("Analog",
|
||||
[]() { g_screen_manager.set_root_screen(g_analog_time_screen); }));
|
||||
[]() { g_screen_mgr.set_root_screen(g_analog_time_screen); }));
|
||||
g_set_face_screen.add_item(MenuScreenItem("Digital",
|
||||
[]() { g_screen_manager.set_root_screen(g_digital_time_screen); }));
|
||||
[]() { g_screen_mgr.set_root_screen(g_digital_time_screen); }));
|
||||
|
||||
// Enqueue each of the tasks
|
||||
BSP::Schedule::NextTime asap = BSP::Schedule::NextTime::asap();
|
||||
Schedule::NextTime asap = Schedule::NextTime::asap();
|
||||
g_sched.add_task(g_spi, asap);
|
||||
g_sched.add_task(g_btn_manager, asap);
|
||||
g_sched.add_task(g_btn_mgr, asap);
|
||||
g_sched.add_task(g_display, asap);
|
||||
g_sched.add_task(g_screen_manager, asap);
|
||||
g_sched.add_task(g_screen_mgr, asap);
|
||||
|
||||
// And we're off! This will never return
|
||||
g_sched.run();
|
||||
|
||||
@@ -83,7 +83,7 @@ namespace BSP {
|
||||
moder_value = 2;
|
||||
break;
|
||||
}
|
||||
SET_STRIDE_TO(m_gpio->MODER, 2, index, moder_value);
|
||||
SET_STRIDE_TO(m_gpio->MODER, 2, index, moder_value);
|
||||
}
|
||||
|
||||
void GpioDriver::set_pin_pupdr(uint32_t index, input_pull_t pull_mode) {
|
||||
|
||||
@@ -83,7 +83,7 @@ S_SOURCES := $(call find_important, $(SOURCEDIR), '*.s')
|
||||
SPP_SOURCES := Bsp/Mcu/$(DEVICE_TYPE).S
|
||||
SOURCES = $(C_SOURCES) $(S_SOURCES) $(SPP_SOURCES) $(CPP_SOURCES)
|
||||
|
||||
APPS := ./Application/main ./Test/pass ./Test/fail ./Test/timeout ./Test/clock ./Test/stop ./Test/no_start ./Test/lptim ./Test/set_time ./Test/periodic_alarms ./Test/wakeup_irq
|
||||
APPS := ./Application/main ./Test/pass ./Test/fail ./Test/timeout ./Test/clock ./Test/stop ./Test/no_start ./Test/lptim ./Test/set_time ./Test/periodic_alarms ./Test/wakeup_irq ./Test/button
|
||||
APP_ELFS = $(addsuffix .elf, $(APPS))
|
||||
APP_MAPS = $(addsuffix .map, $(APPS))
|
||||
APP_BINS = $(addsuffix .bin, $(APPS))
|
||||
@@ -223,22 +223,22 @@ $(FONT_GEN_DIR)/large_digits.h $(FONT_GEN_DIR)/large_digits.c: Gen/fixedfont-to-
|
||||
#
|
||||
|
||||
STM32FLASH_DEVICE = /dev/ttyUSB0
|
||||
FLASH_BIN ?= $(OUTPUT_BIN)
|
||||
|
||||
.PHONY: flash
|
||||
flash: $(OUTPUT_BIN)
|
||||
@echo "FLASH $(OUTPUT_BIN)"
|
||||
$(STM32_PROG) --connect port=SWD reset=Hwrst -w $(OUTPUT_BIN) 0x8000000 -v --go
|
||||
$(STM32_PROG) --connect port=SWD reset=Hwrst -w $(FLASH_BIN) 0x8000000 -v --go
|
||||
|
||||
.PHONY: jlink
|
||||
jlink: $(OUTPUT_BIN)
|
||||
@echo "FLASH $(OUTPUT_BIN)"
|
||||
JLinkExe -device $$(echo $(DEVICE_TYPE) | tr '[:lower:]' '[:upper:]') -if SWD \
|
||||
-speed auto -autoconnect 1 -CommanderScript cmd.jlink
|
||||
|
||||
./jlink.sh $(DEVICE_TYPE) $(FLASH_BIN)
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -f $(OBJS) $(OUTPUT_BIN) $(OUTPUT_ELF) $(FONT_C_FILES) $(FONT_H_FILES) $(OUTPUT_MAP) $(addsuffix .su,$(basename $(OBJS)))
|
||||
rm -f $(OBJS) $(OUTPUT_BIN) $(OUTPUT_ELF) $(FONT_C_FILES) $(FONT_H_FILES) $(OUTPUT_MAP) $(APPS) $(APP_ELFS) $(APP_MAPS) $(APP_BINS) $(APP_OBJS) $(addsuffix .su,$(basename $(ALL_OBJS)))
|
||||
|
||||
|
||||
# Please do not delete my files.
|
||||
.SECONDARY: $(ALL_OBJS) $(APP_ELFS)
|
||||
|
||||
111
firmware/Test/button.cpp
Normal file
111
firmware/Test/button.cpp
Normal file
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Max Regan
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#include "Bsp/Drivers/GpioDriver.h"
|
||||
#include "Bsp/Drivers/RtcDriver.h"
|
||||
#include "Bsp/Drivers/UsartDriver.h"
|
||||
#include "Bsp/LowPowerTaskScheduler.h"
|
||||
#include "Bsp/macros.h"
|
||||
#include "Application/ButtonManager.h"
|
||||
|
||||
#include "printf.h"
|
||||
#include "test.h"
|
||||
|
||||
#include "Mcu.h"
|
||||
|
||||
using namespace BSP;
|
||||
|
||||
using Button = ButtonManager::Button;
|
||||
using ButtonState = ButtonManager::ButtonState;
|
||||
|
||||
static Schedule::LowPowerTaskScheduler<5> g_sched;
|
||||
static GpioDriver g_gpioa(GPIOA);
|
||||
#if defined(BOARD_WATCH)
|
||||
static UsartDriver g_test_uart(USART2, g_sched);
|
||||
static GpioPin g_tx_pin(g_gpioa, 9);
|
||||
#elif defined(BOARD_DEVBOARD)
|
||||
static UsartDriver g_test_uart(USART1, g_sched);
|
||||
#endif
|
||||
|
||||
static GpioPin g_btn_down(g_gpioa, 0);
|
||||
static GpioPin g_btn_mid(g_gpioa, 1);
|
||||
static GpioPin g_btn_up(g_gpioa, 2);
|
||||
|
||||
static ButtonManager g_btn_mgr(g_sched,
|
||||
g_btn_down,
|
||||
g_btn_mid,
|
||||
g_btn_up,
|
||||
Time::millis(50));
|
||||
|
||||
class IdleTask : public BSP::Schedule::Task {
|
||||
public:
|
||||
IdleTask()
|
||||
{}
|
||||
|
||||
BSP::Schedule::NextTime execute() override {
|
||||
return BSP::Schedule::NextTime::asap();
|
||||
}
|
||||
};
|
||||
|
||||
static IdleTask g_idle;
|
||||
|
||||
[[noreturn]] void main() {
|
||||
|
||||
g_gpioa.enable();
|
||||
|
||||
#if defined(BOARD_WATCH)
|
||||
g_tx_pin.configure_alternate_function(4);
|
||||
#endif
|
||||
|
||||
g_test_uart.init();
|
||||
g_test_uart.tx_blocking(test_start_text);
|
||||
|
||||
RtcDriver::init();
|
||||
SystemTimer::set_timer(RtcDriver::get_system_timer());
|
||||
LowPower::init();
|
||||
|
||||
g_btn_down.configure_input(GpioDriver::input_pull_t::PULL_UP);
|
||||
g_btn_mid.configure_input(GpioDriver::input_pull_t::PULL_UP);
|
||||
g_btn_up.configure_input(GpioDriver::input_pull_t::PULL_UP);
|
||||
|
||||
g_btn_mgr.init();
|
||||
|
||||
ButtonManager::ChangeCallback callback =
|
||||
[&](ButtonState state) {
|
||||
g_test_uart.tx_blocking("up:");
|
||||
if (state == ButtonState::PRESSED) {
|
||||
g_test_uart.tx_blocking("pressed\r\n");
|
||||
} else {
|
||||
g_test_uart.tx_blocking("released\r\n");
|
||||
}
|
||||
};
|
||||
|
||||
g_btn_mgr.set_callback(Button::UP, callback);
|
||||
g_test_uart.tx_blocking("Waiting for button press...\r\n");
|
||||
|
||||
Schedule::NextTime asap = Schedule::NextTime::asap();
|
||||
g_sched.add_task(g_test_uart, asap);
|
||||
g_sched.add_task(g_btn_mgr, asap);
|
||||
g_sched.add_task(g_idle, asap);
|
||||
g_sched.run();
|
||||
|
||||
TEST_SPIN();
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
exitonerror 1
|
||||
h
|
||||
r
|
||||
loadbin Application/main.bin 0x8000000
|
||||
g
|
||||
q
|
||||
41
firmware/jlink.sh
Executable file
41
firmware/jlink.sh
Executable file
@@ -0,0 +1,41 @@
|
||||
#!/bin/bash
|
||||
# Copyright (C) 2020 Max Regan
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
set -e
|
||||
|
||||
DEVICE_TYPE=$1
|
||||
APP_BIN="$2"
|
||||
ADDR=0x8000000
|
||||
|
||||
FLASH_SCRIPT="\
|
||||
exitonerror 1
|
||||
h
|
||||
r
|
||||
loadbin ${APP_BIN} ${ADDR}
|
||||
g
|
||||
q"
|
||||
|
||||
flash_script="$(mktemp)"
|
||||
echo "$FLASH_SCRIPT" > "$flash_script"
|
||||
|
||||
JLinkExe -device "$DEVICE_TYPE" -if SWD \
|
||||
-speed auto -autoconnect 1 -CommanderScript "$flash_script"
|
||||
|
||||
rm "$flash_script"
|
||||
@@ -150,7 +150,7 @@ def test_periodic_alarms(context_factory, logger):
|
||||
def test_clock(context_factory, logger):
|
||||
serial_dev, jlink = context_factory("Test/clock.bin")
|
||||
EXPECTED_RUNTIME = 10
|
||||
TOLERANCE = 0.1
|
||||
TOLERANCE = 0.2
|
||||
|
||||
serial_dev.timeout = EXPECTED_RUNTIME * 1.2
|
||||
|
||||
@@ -297,6 +297,52 @@ def test_lptim(context_factory, logger):
|
||||
assert max_f < 51
|
||||
|
||||
|
||||
def test_button_slow(context_factory, logger):
|
||||
serial_dev, jlink = context_factory("Test/button.bin")
|
||||
serial_dev.timeout = 0.3
|
||||
ASSERTED = True
|
||||
|
||||
serial_dev.dtr = not ASSERTED
|
||||
while (line := serial_dev.readline()) is not None and len(line) > 0:
|
||||
pass
|
||||
|
||||
for _ in range(5):
|
||||
serial_dev.dtr = ASSERTED
|
||||
press_line = serial_dev.readline()
|
||||
serial_dev.dtr = not ASSERTED
|
||||
release_line = serial_dev.readline()
|
||||
assert press_line == b"up:pressed\r\n"
|
||||
assert release_line == b"up:released\r\n"
|
||||
|
||||
|
||||
def test_button_fast(context_factory, logger):
|
||||
serial_dev, jlink = context_factory("Test/button.bin")
|
||||
serial_dev.timeout = 0.3
|
||||
ASSERTED = True
|
||||
|
||||
serial_dev.dtr = not ASSERTED
|
||||
time.sleep(0.3)
|
||||
serial_dev.timeout = 0
|
||||
while (line := serial_dev.readline()) is not None and len(line) > 0:
|
||||
pass
|
||||
|
||||
for _ in range(25):
|
||||
serial_dev.dtr = ASSERTED
|
||||
time.sleep(0.075)
|
||||
serial_dev.dtr = not ASSERTED
|
||||
time.sleep(0.075)
|
||||
|
||||
serial_dev.timeout = 0.3
|
||||
for _ in range(25):
|
||||
press_line = serial_dev.readline()
|
||||
release_line = serial_dev.readline()
|
||||
assert press_line == b"up:pressed\r\n"
|
||||
assert release_line == b"up:released\r\n"
|
||||
|
||||
serial_dev.timeout = 0
|
||||
assert serial_dev.readline() == b""
|
||||
|
||||
|
||||
def main():
|
||||
pytest.main(sys.argv)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user