Keep Companion sensor polling running across timer rollover
This commit is contained in:
1 parent
4c3a5691e1
commit
eec1caa170
6 files changed
+411
-19
No files matched your search
@@ -783,6 +783,7 @@ jobs:
|
||||
python3 -B test/test_rtttl_rollover.py -v
|
||||
python3 -B test/test_rtttl_rollover_recipes.py -v
|
||||
python3 -B test/test_companion_ui_deadlines.py -v
|
||||
python3 -B test/test_home_sensor_refresh.py -v
|
||||
python3 -B test/test_scrolling_status_bar_rollover.py -v
|
||||
python3 -B test/test_gps_voice_buttons.py -v
|
||||
python3 -B test/test_gps_voice_generator.py -v
|
||||
|
||||
@@ -576,7 +576,7 @@ class HomeScreen : public UIScreen {
|
||||
int sensors_nb = 0;
|
||||
bool sensors_scroll = false;
|
||||
int sensors_scroll_offset = 0;
|
||||
int next_sensors_refresh = 0;
|
||||
uint32_t last_sensors_refresh = 0;
|
||||
|
||||
void refresh_sensors() {
|
||||
// CayenneLPP retains its requested capacity even if its allocation fails.
|
||||
@@ -587,7 +587,15 @@ class HomeScreen : public UIScreen {
|
||||
sensors_scroll_offset = 0;
|
||||
return;
|
||||
}
|
||||
if (millis() > next_sensors_refresh) {
|
||||
#if AUTO_OFF_MILLIS > 0
|
||||
const uint32_t refresh_interval = 5000;
|
||||
#else
|
||||
const uint32_t refresh_interval = 60000;
|
||||
#endif
|
||||
// Empty data requests an initial/forced sample. Elapsed time also handles
|
||||
// returning to this page after more than half a millis() cycle.
|
||||
if (sensors_lpp.getSize() == 0
|
||||
|| static_cast<uint32_t>(millis() - last_sensors_refresh) >= refresh_interval) {
|
||||
sensors_lpp.reset();
|
||||
sensors_nb = 0;
|
||||
sensors_lpp.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f);
|
||||
@@ -599,11 +607,7 @@ class HomeScreen : public UIScreen {
|
||||
sensors_nb ++;
|
||||
}
|
||||
sensors_scroll = sensors_nb > UI_RECENT_LIST_SIZE;
|
||||
#if AUTO_OFF_MILLIS > 0
|
||||
next_sensors_refresh = millis() + 5000; // refresh sensor values every 5 sec
|
||||
#else
|
||||
next_sensors_refresh = millis() + 60000; // refresh sensor values every 1 min
|
||||
#endif
|
||||
last_sensors_refresh = millis();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1420,7 +1424,7 @@ public:
|
||||
#if UI_SENSORS_PAGE == 1
|
||||
if (key == KEY_ENTER && _page == HomePage::SENSORS) {
|
||||
_task->toggleGPS();
|
||||
next_sensors_refresh=0;
|
||||
sensors_lpp.reset();
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -168,13 +168,21 @@ class HomeScreen : public UIScreen {
|
||||
int sensors_nb = 0;
|
||||
bool sensors_scroll = false;
|
||||
int sensors_scroll_offset = 0;
|
||||
int next_sensors_refresh = 0;
|
||||
uint32_t last_sensors_refresh = 0;
|
||||
uint32_t _radio_profile_page_started_at = 0;
|
||||
uint8_t _radio_status_page = 0;
|
||||
bool _dual_radio_enabled_seen = false;
|
||||
|
||||
void refresh_sensors() {
|
||||
if (millis() > next_sensors_refresh) {
|
||||
#if AUTO_OFF_MILLIS > 0
|
||||
const uint32_t refresh_interval = 5000;
|
||||
#else
|
||||
const uint32_t refresh_interval = 60000;
|
||||
#endif
|
||||
// Empty data requests an initial/forced sample. Elapsed time also handles
|
||||
// returning to this page after more than half a millis() cycle.
|
||||
if (sensors_lpp.getSize() == 0
|
||||
|| static_cast<uint32_t>(millis() - last_sensors_refresh) >= refresh_interval) {
|
||||
sensors_lpp.reset();
|
||||
sensors_nb = 0;
|
||||
sensors_lpp.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f);
|
||||
@@ -186,11 +194,7 @@ class HomeScreen : public UIScreen {
|
||||
sensors_nb ++;
|
||||
}
|
||||
sensors_scroll = sensors_nb > UI_RECENT_LIST_SIZE;
|
||||
#if AUTO_OFF_MILLIS > 0
|
||||
next_sensors_refresh = millis() + 5000; // refresh sensor values every 5 sec
|
||||
#else
|
||||
next_sensors_refresh = millis() + 60000; // refresh sensor values every 1 min
|
||||
#endif
|
||||
last_sensors_refresh = millis();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -575,7 +579,7 @@ public:
|
||||
#if UI_SENSORS_PAGE == 1
|
||||
if (c == KEY_ENTER && _page == HomePage::SENSORS) {
|
||||
_task->toggleGPS();
|
||||
next_sensors_refresh=0;
|
||||
sensors_lpp.reset();
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
+288
@@ -0,0 +1,288 @@
|
||||
// The Home fields, refresh method, and input block come from production.
|
||||
// Only clock, board, sensor/LPP transport, and task boundaries are mocked.
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
#define TELEM_CHANNEL_SELF 0
|
||||
#define UI_RECENT_LIST_SIZE 3
|
||||
#define KEY_ENTER '\r'
|
||||
|
||||
static uint32_t clock_ms = 0;
|
||||
uint32_t millis() { return clock_ms; }
|
||||
|
||||
static const char* active_case = "initialization";
|
||||
static unsigned checks = 0;
|
||||
static unsigned scenarios = 0;
|
||||
#define CHECK(condition) do { \
|
||||
++checks; \
|
||||
if (!(condition)) { \
|
||||
std::fprintf(stderr, "FAIL %s: %s (line %d)\n", active_case, #condition, __LINE__); \
|
||||
std::exit(1); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
struct CayenneLPP {
|
||||
uint8_t bytes[200] = {};
|
||||
uint8_t size = 0;
|
||||
bool allocated = true;
|
||||
unsigned writes = 0;
|
||||
uint8_t* getBuffer() { return allocated ? bytes : nullptr; }
|
||||
uint8_t getSize() { return size; }
|
||||
void reset() { size = 0; }
|
||||
void addVoltage(int channel, float voltage) {
|
||||
CHECK(getBuffer() != nullptr);
|
||||
CHECK(channel == TELEM_CHANNEL_SELF && voltage == 4.0f);
|
||||
++writes;
|
||||
bytes[size++] = 4;
|
||||
size += 3;
|
||||
}
|
||||
void addSample() {
|
||||
CHECK(getBuffer() != nullptr && size < sizeof(bytes) - 4);
|
||||
++writes;
|
||||
bytes[size++] = 4;
|
||||
size += 3;
|
||||
}
|
||||
};
|
||||
|
||||
struct LPPReader {
|
||||
unsigned remaining;
|
||||
LPPReader(uint8_t* buffer, uint8_t size) : remaining(buffer ? size / 4 : 0) {}
|
||||
bool readHeader(uint8_t& channel, uint8_t& type) {
|
||||
if (!remaining) return false;
|
||||
--remaining;
|
||||
channel = 0;
|
||||
type = 1;
|
||||
return true;
|
||||
}
|
||||
void skipData(uint8_t) {}
|
||||
};
|
||||
|
||||
struct Board {
|
||||
unsigned reads = 0;
|
||||
int getBattMilliVolts() { ++reads; return 4000; }
|
||||
} board;
|
||||
|
||||
struct Sensors {
|
||||
unsigned calls = 0;
|
||||
unsigned samples = 0;
|
||||
uint32_t duration_ms = 0;
|
||||
void querySensors(int mask, CayenneLPP& lpp) {
|
||||
CHECK(mask == 0xFF);
|
||||
++calls;
|
||||
for (unsigned i = 0; i < samples; ++i) lpp.addSample();
|
||||
clock_ms += duration_ms;
|
||||
}
|
||||
} sensors;
|
||||
|
||||
struct Task {
|
||||
unsigned toggles = 0;
|
||||
void toggleGPS() { ++toggles; }
|
||||
};
|
||||
|
||||
enum class HomePage { OTHER, SENSORS };
|
||||
|
||||
struct Home {
|
||||
@FIELDS@
|
||||
Task task;
|
||||
Task* _task = &task;
|
||||
HomePage _page = HomePage::SENSORS;
|
||||
|
||||
@REFRESH@
|
||||
|
||||
bool input(char @KEY@) {
|
||||
@INPUT@
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
static constexpr uint32_t interval_ms = AUTO_OFF_MILLIS > 0 ? 5000u : 60000u;
|
||||
|
||||
static void begin_case(const char* name, uint32_t initial_ms) {
|
||||
active_case = name;
|
||||
++scenarios;
|
||||
clock_ms = initial_ms;
|
||||
sensors = Sensors{};
|
||||
board = Board{};
|
||||
}
|
||||
|
||||
static void initial_at(uint32_t at) {
|
||||
begin_case("fresh page", at);
|
||||
Home home;
|
||||
CHECK(home.sensors_lpp.getSize() == 0);
|
||||
home.refresh_sensors();
|
||||
CHECK(sensors.calls == 1 && board.reads == 1);
|
||||
CHECK(home.sensors_lpp.getSize() == 4);
|
||||
CHECK(home.sensors_nb == 1 && !home.sensors_scroll);
|
||||
home.refresh_sensors();
|
||||
CHECK(sensors.calls == 1 && board.reads == 1);
|
||||
}
|
||||
|
||||
static void cadence_and_records() {
|
||||
begin_case("normal cadence and records", 100);
|
||||
Home home;
|
||||
sensors.samples = 4;
|
||||
home.refresh_sensors();
|
||||
CHECK(sensors.calls == 1 && home.sensors_nb == 5 && home.sensors_scroll);
|
||||
clock_ms += interval_ms - 1;
|
||||
home.refresh_sensors();
|
||||
CHECK(sensors.calls == 1);
|
||||
++clock_ms;
|
||||
sensors.samples = 0;
|
||||
home.refresh_sensors();
|
||||
CHECK(sensors.calls == 2 && home.sensors_nb == 1 && !home.sensors_scroll);
|
||||
CHECK(home.sensors_lpp.getSize() == 4);
|
||||
home.refresh_sensors();
|
||||
CHECK(sensors.calls == 2);
|
||||
}
|
||||
|
||||
static void wrap_visible() {
|
||||
begin_case("visible across wrap", UINT32_MAX - interval_ms / 2);
|
||||
Home home;
|
||||
home.refresh_sensors();
|
||||
CHECK(sensors.calls == 1);
|
||||
++clock_ms;
|
||||
home.refresh_sensors();
|
||||
CHECK(sensors.calls == 1);
|
||||
clock_ms += interval_ms - 2;
|
||||
home.refresh_sensors();
|
||||
CHECK(sensors.calls == 1);
|
||||
++clock_ms;
|
||||
home.refresh_sensors();
|
||||
CHECK(sensors.calls == 2);
|
||||
}
|
||||
|
||||
static void wake_across_wrap() {
|
||||
begin_case("wake across wrap", UINT32_MAX - 2 * interval_ms);
|
||||
Home home;
|
||||
home.refresh_sensors();
|
||||
CHECK(sensors.calls == 1);
|
||||
// The display is not rendered while asleep; wake after millis wraps.
|
||||
clock_ms = 1000;
|
||||
home.refresh_sensors();
|
||||
CHECK(sensors.calls == 2);
|
||||
home.refresh_sensors();
|
||||
CHECK(sensors.calls == 2);
|
||||
}
|
||||
|
||||
static void exact_wrap_deadline() {
|
||||
begin_case("deadline at zero", uint32_t(0u - interval_ms));
|
||||
Home home;
|
||||
home.refresh_sensors();
|
||||
CHECK(sensors.calls == 1);
|
||||
++clock_ms;
|
||||
home.refresh_sensors();
|
||||
CHECK(sensors.calls == 1);
|
||||
clock_ms = UINT32_MAX;
|
||||
home.refresh_sensors();
|
||||
CHECK(sensors.calls == 1);
|
||||
clock_ms = 0;
|
||||
home.refresh_sensors();
|
||||
CHECK(sensors.calls == 2);
|
||||
home.refresh_sensors();
|
||||
CHECK(sensors.calls == 2);
|
||||
}
|
||||
|
||||
static void long_idle(uint32_t elapsed) {
|
||||
begin_case("long inactive page", 100);
|
||||
Home home;
|
||||
home.refresh_sensors();
|
||||
CHECK(sensors.calls == 1);
|
||||
clock_ms += elapsed;
|
||||
home.refresh_sensors();
|
||||
CHECK(sensors.calls == 2);
|
||||
home.refresh_sensors();
|
||||
CHECK(sensors.calls == 2);
|
||||
}
|
||||
|
||||
static void force_at(uint32_t initial, uint32_t forced) {
|
||||
begin_case("forced input", initial);
|
||||
Home home;
|
||||
home.refresh_sensors();
|
||||
CHECK(sensors.calls == 1);
|
||||
clock_ms = forced;
|
||||
CHECK(!home.input('x'));
|
||||
CHECK(home.task.toggles == 0);
|
||||
home._page = HomePage::OTHER;
|
||||
CHECK(!home.input(KEY_ENTER));
|
||||
CHECK(home.task.toggles == 0);
|
||||
home._page = HomePage::SENSORS;
|
||||
CHECK(home.input(KEY_ENTER));
|
||||
CHECK(home.task.toggles == 1);
|
||||
CHECK(sensors.calls == 1);
|
||||
home.refresh_sensors();
|
||||
CHECK(sensors.calls == 2 && home.sensors_nb == 1);
|
||||
home.refresh_sensors();
|
||||
CHECK(sensors.calls == 2);
|
||||
}
|
||||
|
||||
static void completion_anchor(bool wrap) {
|
||||
begin_case("completion-relative cadence", wrap ? UINT32_MAX - 10 : 100);
|
||||
Home home;
|
||||
sensors.duration_ms = 37;
|
||||
home.refresh_sensors();
|
||||
CHECK(sensors.calls == 1);
|
||||
const uint32_t completion = clock_ms;
|
||||
clock_ms = completion + interval_ms - 1;
|
||||
home.refresh_sensors();
|
||||
CHECK(sensors.calls == 1);
|
||||
++clock_ms;
|
||||
home.refresh_sensors();
|
||||
CHECK(sensors.calls == 2);
|
||||
CHECK(clock_ms == uint32_t(completion + interval_ms + sensors.duration_ms));
|
||||
}
|
||||
|
||||
static void empty_query_keeps_base_voltage() {
|
||||
begin_case("empty sensor query", 100);
|
||||
Home home;
|
||||
CHECK(sensors.samples == 0);
|
||||
home.refresh_sensors();
|
||||
CHECK(home.sensors_lpp.getSize() == 4 && home.sensors_nb == 1);
|
||||
++clock_ms;
|
||||
home.refresh_sensors();
|
||||
CHECK(sensors.calls == 1);
|
||||
}
|
||||
|
||||
#if TEST_NEW_UI
|
||||
static void allocation_guard() {
|
||||
begin_case("allocation guard", 0);
|
||||
Home home;
|
||||
home.sensors_lpp.allocated = false;
|
||||
home.sensors_nb = 5;
|
||||
home.sensors_scroll = true;
|
||||
home.sensors_scroll_offset = 4;
|
||||
home.refresh_sensors();
|
||||
CHECK(sensors.calls == 0 && board.reads == 0 && home.sensors_lpp.writes == 0);
|
||||
CHECK(home.sensors_nb == 0 && !home.sensors_scroll && home.sensors_scroll_offset == 0);
|
||||
home.sensors_lpp.allocated = true;
|
||||
home.refresh_sensors();
|
||||
CHECK(sensors.calls == 1 && home.sensors_nb == 1);
|
||||
}
|
||||
#endif
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
if (argc == 2 && std::strcmp(argv[1], "force") == 0) {
|
||||
force_at(100, 101);
|
||||
} else {
|
||||
initial_at(0);
|
||||
initial_at(0x80000000u);
|
||||
initial_at(UINT32_MAX);
|
||||
cadence_and_records();
|
||||
wrap_visible();
|
||||
wake_across_wrap();
|
||||
exact_wrap_deadline();
|
||||
long_idle(26u * 24u * 60u * 60u * 1000u);
|
||||
long_idle(UINT32_MAX - 1000);
|
||||
force_at(100, 101);
|
||||
force_at(0x80000000u, 0x80000001u);
|
||||
force_at(UINT32_MAX - 10, 0);
|
||||
completion_anchor(false);
|
||||
completion_anchor(true);
|
||||
empty_query_keeps_base_voltage();
|
||||
#if TEST_NEW_UI
|
||||
allocation_guard();
|
||||
#endif
|
||||
}
|
||||
std::printf("PASS: %u scenarios; %u checks\n", scenarios, checks);
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
"""Test actual HomeScreen sensor-page refresh and force-input timer bodies.
|
||||
|
||||
Both UI implementations are compiled with the sensor page enabled at each
|
||||
cadence. This does not claim that every normal board recipe enables that page.
|
||||
Clock, task, and board/sensor/LPP boundaries are host fakes, not hardware tests.
|
||||
"""
|
||||
from pathlib import Path
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from test_replay_reset_integration import extract_braced
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
FIXTURE = ROOT / 'test/fixtures/home_sensor_refresh.cpp'
|
||||
|
||||
|
||||
def harness(ui, mutation=None):
|
||||
source = (ROOT / 'examples/companion_radio' / ui / 'UITask.cpp').read_text(encoding='ascii')
|
||||
field_start = source.index(' CayenneLPP sensors_lpp;')
|
||||
refresh_start = source.index(' void refresh_sensors()', field_start)
|
||||
fields = source[field_start:refresh_start]
|
||||
# Keep sensor fields exactly as declared, without the neighboring radio UI.
|
||||
fields = fields.split(' uint32_t _radio_profile_page_started_at')[0]
|
||||
refresh = extract_braced(source, 'void refresh_sensors()')
|
||||
key = 'key' if ui == 'ui-new' else 'c'
|
||||
input_block = extract_braced(source,
|
||||
f'if ({key} == KEY_ENTER && _page == HomePage::SENSORS)')
|
||||
if mutation == 'no-force-invalidation':
|
||||
if input_block.count('sensors_lpp.reset();') != 1:
|
||||
raise AssertionError('sensor-page input must explicitly invalidate its LPP cache')
|
||||
input_block = input_block.replace('sensors_lpp.reset();', '', 1)
|
||||
elif mutation == 'no-empty-marker':
|
||||
refresh, count = re.subn(r'sensors_lpp\.getSize\(\)\s*==\s*0\s*\|\|\s*',
|
||||
'', refresh, count=1)
|
||||
if count != 1:
|
||||
raise AssertionError('refresh must recognize its empty initial/forced cache')
|
||||
code = FIXTURE.read_text(encoding='ascii')
|
||||
for token, value in {'FIELDS': fields, 'REFRESH': refresh,
|
||||
'KEY': key, 'INPUT': input_block}.items():
|
||||
code = code.replace('@' + token + '@', value)
|
||||
return code
|
||||
|
||||
|
||||
class HomeSensorRefreshTest(unittest.TestCase):
|
||||
def test_actual_sensor_page_cadence_wrap_force_and_allocation(self):
|
||||
self.run_variants()
|
||||
|
||||
def test_missing_force_invalidation_fails_intended_regression(self):
|
||||
self.run_variants('no-force-invalidation', 'force', 'FAIL forced input:')
|
||||
|
||||
def test_missing_empty_marker_fails_first_poll_at_zero(self):
|
||||
self.run_variants('no-empty-marker', None, 'FAIL fresh page:')
|
||||
|
||||
def run_variants(self, mutation=None, argument=None, expected_failure=None):
|
||||
compiler = shutil.which('g++') or shutil.which('clang++')
|
||||
if compiler is None:
|
||||
self.skipTest('a host C++17 compiler is required')
|
||||
sanitize = [] if os.name == 'nt' else [
|
||||
'-fsanitize=address,undefined', '-fno-sanitize-recover=all',
|
||||
'-fno-pie', '-no-pie']
|
||||
with tempfile.TemporaryDirectory(prefix='home-sensor-refresh-') as directory:
|
||||
work = Path(directory)
|
||||
for ui in ('ui-new', 'ui-tiny'):
|
||||
source = work / (ui + '.cpp')
|
||||
source.write_text(harness(ui, mutation), encoding='ascii')
|
||||
for auto_off in (15000, 0):
|
||||
with self.subTest(ui=ui, auto_off=auto_off):
|
||||
binary = work / (ui + '-' + str(auto_off) +
|
||||
('.exe' if os.name == 'nt' else ''))
|
||||
compiled = subprocess.run([
|
||||
compiler, '-std=c++17', '-O1', '-g', '-Wall', '-Wextra', '-Werror',
|
||||
*sanitize, '-DAUTO_OFF_MILLIS=' + str(auto_off),
|
||||
'-DTEST_NEW_UI=' + str(int(ui == 'ui-new')),
|
||||
str(source), '-o', str(binary),
|
||||
], text=True, capture_output=True, timeout=60)
|
||||
self.assertEqual(compiled.returncode, 0, compiled.stdout + compiled.stderr)
|
||||
tested = subprocess.run([str(binary), *([argument] if argument else [])],
|
||||
text=True, capture_output=True, timeout=10)
|
||||
if expected_failure:
|
||||
self.assertEqual(tested.returncode, 1, tested.stdout + tested.stderr)
|
||||
self.assertIn(expected_failure, tested.stderr)
|
||||
self.assertNotIn('AddressSanitizer:', tested.stderr)
|
||||
self.assertNotIn('runtime error:', tested.stderr)
|
||||
else:
|
||||
self.assertEqual(tested.returncode, 0, tested.stdout + tested.stderr)
|
||||
self.assertRegex(tested.stdout, r'PASS: (15|16) scenarios; \d+ checks')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -98,7 +98,7 @@ class T096FullMemoryTest(unittest.TestCase):
|
||||
#define AUTO_OFF_MILLIS 15000
|
||||
#define UI_RECENT_LIST_SIZE 3
|
||||
#define TELEM_CHANNEL_SELF 0
|
||||
unsigned long millis() { return 100; }
|
||||
uint32_t millis() { return 100; }
|
||||
struct Lpp {
|
||||
uint8_t storage[8];
|
||||
bool allocated = false;
|
||||
@@ -121,7 +121,8 @@ struct Board { int getBattMilliVolts() { return 4000; } } board;
|
||||
struct Sensors { int calls = 0; void querySensors(int, Lpp&) { ++calls; } } sensors;
|
||||
struct Home {
|
||||
Lpp sensors_lpp;
|
||||
int sensors_nb = 5, sensors_scroll_offset = 4, next_sensors_refresh = 0;
|
||||
int sensors_nb = 5, sensors_scroll_offset = 4;
|
||||
uint32_t last_sensors_refresh = 0;
|
||||
bool sensors_scroll = true;
|
||||
@REFRESH@
|
||||
};
|
||||
@@ -133,7 +134,7 @@ int main() {
|
||||
home.sensors_lpp.allocated = true;
|
||||
home.refresh_sensors();
|
||||
assert(home.sensors_nb == 1 && sensors.calls == 1);
|
||||
assert(home.next_sensors_refresh == 5100);
|
||||
assert(home.last_sensors_refresh == 100);
|
||||
home.refresh_sensors();
|
||||
assert(sensors.calls == 1);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user