fix lchm
This commit is contained in:
@ -37,15 +37,17 @@ namespace {
|
||||
|
||||
constexpr uint32_t kE502DiSyn2Mask =
|
||||
(static_cast<uint32_t>(1U) << 13U) | (static_cast<uint32_t>(1U) << 17U);
|
||||
constexpr uint32_t kE502Digital1Mask = (static_cast<uint32_t>(1U) << 0U);
|
||||
constexpr uint32_t kE502Digital2Mask = (static_cast<uint32_t>(1U) << 1U);
|
||||
constexpr uint32_t kInternalRefSetting = X502_REF_FREQ_2000KHZ;
|
||||
constexpr uint32_t kInternalRefHz = 2000000U;
|
||||
|
||||
struct Config {
|
||||
std::string serial;
|
||||
std::optional<uint32_t> ip_addr;
|
||||
|
||||
uint32_t clock_mode = X502_SYNC_DI_SYN1_RISE;
|
||||
double clock_hz = 2000000.0;
|
||||
double duration_ms = 100.0;
|
||||
double duration_ms = 100.0; // Legacy-only option retained for backward compatibility warnings.
|
||||
uint32_t windows = 1000;
|
||||
|
||||
uint32_t recv_block_words = 8192;
|
||||
@ -55,9 +57,12 @@ struct Config {
|
||||
uint32_t input_buffer_words = 262144;
|
||||
uint32_t input_step_words = 8192;
|
||||
|
||||
bool pullup_syn1 = false;
|
||||
bool pullup_syn2 = false;
|
||||
bool clean_start = false;
|
||||
bool legacy_clock_arg_used = false;
|
||||
std::string legacy_clock_arg;
|
||||
bool legacy_pullup_syn1 = false;
|
||||
bool legacy_clean_start = false;
|
||||
bool legacy_duration_ms = false;
|
||||
};
|
||||
|
||||
struct LchmClockCount {
|
||||
@ -81,6 +86,11 @@ struct LchmClockCount {
|
||||
}
|
||||
};
|
||||
|
||||
struct Di1StepClockCount {
|
||||
bool di1_level = false;
|
||||
LchmClockCount count;
|
||||
};
|
||||
|
||||
struct RunningStats {
|
||||
uint64_t count = 0;
|
||||
uint64_t clocks_min = std::numeric_limits<uint64_t>::max();
|
||||
@ -172,52 +182,35 @@ std::string ipv4_to_string(uint32_t ip_addr) {
|
||||
return out.str();
|
||||
}
|
||||
|
||||
uint32_t parse_clock_mode(const std::string& text) {
|
||||
std::string parse_legacy_clock_mode(const std::string& text) {
|
||||
const std::string value = trim_copy(text);
|
||||
if (value == "di_syn1_rise") {
|
||||
return X502_SYNC_DI_SYN1_RISE;
|
||||
}
|
||||
if (value == "di_syn1_fall") {
|
||||
return X502_SYNC_DI_SYN1_FALL;
|
||||
}
|
||||
fail("Only clock:di_syn1_rise or clock:di_syn1_fall are supported");
|
||||
}
|
||||
|
||||
std::string clock_mode_to_string(uint32_t mode) {
|
||||
switch (mode) {
|
||||
case X502_SYNC_DI_SYN1_RISE:
|
||||
return "di_syn1_rise";
|
||||
case X502_SYNC_DI_SYN1_FALL:
|
||||
return "di_syn1_fall";
|
||||
default:
|
||||
return "unknown";
|
||||
if ((value == "di_syn1_rise") || (value == "di_syn1_fall")) {
|
||||
return value;
|
||||
}
|
||||
fail("Unsupported legacy clock mode: " + text + ". Use di_syn1_rise or di_syn1_fall.");
|
||||
}
|
||||
|
||||
void print_help(const char* exe_name) {
|
||||
std::cout
|
||||
<< "Usage:\n"
|
||||
<< " " << exe_name << " [serial:SN] [ip:192.168.0.10]\n"
|
||||
<< " [clock:di_syn1_rise] [clock_hz:2000000]\n"
|
||||
<< " [duration_ms:100] [windows:1000]\n"
|
||||
<< " [clock_hz:2000000] [windows:1000]\n"
|
||||
<< " [recv_block:8192] [recv_timeout_ms:50]\n"
|
||||
<< " [clock_wait_ms:5000] [lchm_wait_ms:5000]\n"
|
||||
<< " [buffer_words:262144] [step_words:8192]\n"
|
||||
<< " [pullup_syn1] [pullup_syn2] [clean_start]\n"
|
||||
<< " [pullup_syn2]\n"
|
||||
<< "\n"
|
||||
<< "Fixed counting scheme:\n"
|
||||
<< " DI_SYN1 -> external clock; one synchronous DIN sample is one clock\n"
|
||||
<< " DI_SYN2 -> LCHM window; rising edge starts, falling edge stops\n"
|
||||
<< " DI2 -> extra digital level split into high/low clock counts\n"
|
||||
<< " clock -> internal only (fixed reference 2000000 Hz)\n"
|
||||
<< " DI_SYN2 -> strict LCHM window; low->high starts, high->low stops\n"
|
||||
<< " DI1 -> step delimiter; both DI1 edges split LCHM into steps\n"
|
||||
<< " DI2 -> high/low clocks counted per step and per full LCHM\n"
|
||||
<< "\n"
|
||||
<< "By default, an initial HIGH level on DI_SYN2 starts counting immediately,\n"
|
||||
<< "matching main.exe packet startup behavior. Use clean_start to skip that\n"
|
||||
<< "partial first window until a clean low->high transition is observed.\n"
|
||||
<< "duration_ms closes an active window if DI_SYN2 does not fall; use\n"
|
||||
<< "duration_ms:0 to require a real DI_SYN2 falling edge.\n"
|
||||
<< "Legacy arguments are accepted but ignored with warning:\n"
|
||||
<< " clock:di_syn1_rise|di_syn1_fall, pullup_syn1, clean_start, duration_ms:...\n"
|
||||
<< "\n"
|
||||
<< "Example:\n"
|
||||
<< " " << exe_name << " clock:di_syn1_rise clock_hz:2000000 windows:1000\n";
|
||||
<< " " << exe_name << " clock_hz:2000000 windows:1000 pullup_syn2\n";
|
||||
}
|
||||
|
||||
Config parse_args(int argc, char** argv) {
|
||||
@ -230,7 +223,7 @@ Config parse_args(int argc, char** argv) {
|
||||
std::exit(0);
|
||||
}
|
||||
if (arg == "pullup_syn1") {
|
||||
cfg.pullup_syn1 = true;
|
||||
cfg.legacy_pullup_syn1 = true;
|
||||
continue;
|
||||
}
|
||||
if (arg == "pullup_syn2") {
|
||||
@ -238,7 +231,7 @@ Config parse_args(int argc, char** argv) {
|
||||
continue;
|
||||
}
|
||||
if (arg == "clean_start") {
|
||||
cfg.clean_start = true;
|
||||
cfg.legacy_clean_start = true;
|
||||
continue;
|
||||
}
|
||||
if (starts_with(arg, "serial:")) {
|
||||
@ -250,7 +243,8 @@ Config parse_args(int argc, char** argv) {
|
||||
continue;
|
||||
}
|
||||
if (starts_with(arg, "clock:")) {
|
||||
cfg.clock_mode = parse_clock_mode(arg.substr(6));
|
||||
cfg.legacy_clock_arg_used = true;
|
||||
cfg.legacy_clock_arg = parse_legacy_clock_mode(arg.substr(6));
|
||||
continue;
|
||||
}
|
||||
if (starts_with(arg, "clock_hz:")) {
|
||||
@ -263,6 +257,7 @@ Config parse_args(int argc, char** argv) {
|
||||
}
|
||||
if (starts_with(arg, "duration_ms:")) {
|
||||
cfg.duration_ms = parse_double(arg.substr(12), "duration_ms");
|
||||
cfg.legacy_duration_ms = true;
|
||||
continue;
|
||||
}
|
||||
if (starts_with(arg, "windows:")) {
|
||||
@ -428,11 +423,11 @@ struct Api {
|
||||
decltype(&X502_StreamsDisable) StreamsDisable = nullptr;
|
||||
decltype(&X502_SetSyncMode) SetSyncMode = nullptr;
|
||||
decltype(&X502_SetSyncStartMode) SetSyncStartMode = nullptr;
|
||||
decltype(&X502_SetDinFreqDivider) SetDinFreqDivider = nullptr;
|
||||
decltype(&X502_SetRefFreq) SetRefFreq = nullptr;
|
||||
decltype(&X502_SetDinFreq) SetDinFreq = nullptr;
|
||||
decltype(&X502_SetStreamBufSize) SetStreamBufSize = nullptr;
|
||||
decltype(&X502_SetStreamStep) SetStreamStep = nullptr;
|
||||
decltype(&X502_SetDigInPullup) SetDigInPullup = nullptr;
|
||||
decltype(&X502_SetExtRefFreqValue) SetExtRefFreqValue = nullptr;
|
||||
decltype(&X502_Configure) Configure = nullptr;
|
||||
decltype(&X502_StreamsEnable) StreamsEnable = nullptr;
|
||||
decltype(&X502_StreamsStart) StreamsStart = nullptr;
|
||||
@ -473,11 +468,11 @@ struct Api {
|
||||
StreamsDisable = load_symbol<decltype(StreamsDisable)>(x502_module, "X502_StreamsDisable");
|
||||
SetSyncMode = load_symbol<decltype(SetSyncMode)>(x502_module, "X502_SetSyncMode");
|
||||
SetSyncStartMode = load_symbol<decltype(SetSyncStartMode)>(x502_module, "X502_SetSyncStartMode");
|
||||
SetDinFreqDivider = load_symbol<decltype(SetDinFreqDivider)>(x502_module, "X502_SetDinFreqDivider");
|
||||
SetRefFreq = load_symbol<decltype(SetRefFreq)>(x502_module, "X502_SetRefFreq");
|
||||
SetDinFreq = load_symbol<decltype(SetDinFreq)>(x502_module, "X502_SetDinFreq");
|
||||
SetStreamBufSize = load_symbol<decltype(SetStreamBufSize)>(x502_module, "X502_SetStreamBufSize");
|
||||
SetStreamStep = load_symbol<decltype(SetStreamStep)>(x502_module, "X502_SetStreamStep");
|
||||
SetDigInPullup = load_symbol<decltype(SetDigInPullup)>(x502_module, "X502_SetDigInPullup");
|
||||
SetExtRefFreqValue = load_symbol<decltype(SetExtRefFreqValue)>(x502_module, "X502_SetExtRefFreqValue");
|
||||
Configure = load_symbol<decltype(Configure)>(x502_module, "X502_Configure");
|
||||
StreamsEnable = load_symbol<decltype(StreamsEnable)>(x502_module, "X502_StreamsEnable");
|
||||
StreamsStart = load_symbol<decltype(StreamsStart)>(x502_module, "X502_StreamsStart");
|
||||
@ -638,6 +633,22 @@ void print_summary(const RunningStats& stats) {
|
||||
}
|
||||
|
||||
int run(const Config& cfg) {
|
||||
if (cfg.legacy_clock_arg_used) {
|
||||
std::cerr << "Warning: legacy argument clock:" << cfg.legacy_clock_arg
|
||||
<< " is ignored. lchm_clock_counter uses internal clock only.\n";
|
||||
}
|
||||
if (cfg.legacy_pullup_syn1) {
|
||||
std::cerr << "Warning: legacy argument pullup_syn1 is ignored in internal-only mode.\n";
|
||||
}
|
||||
if (cfg.legacy_clean_start) {
|
||||
std::cerr << "Warning: legacy argument clean_start is ignored. "
|
||||
<< "LCHM starts strictly on DI_SYN2 low->high.\n";
|
||||
}
|
||||
if (cfg.legacy_duration_ms) {
|
||||
std::cerr << "Warning: legacy argument duration_ms:" << cfg.duration_ms
|
||||
<< " is ignored. LCHM closes only on DI_SYN2 high->low.\n";
|
||||
}
|
||||
|
||||
Api api;
|
||||
DeviceHandle device(api);
|
||||
|
||||
@ -666,30 +677,17 @@ int run(const Config& cfg) {
|
||||
api.StreamsStop(device.hnd);
|
||||
api.StreamsDisable(device.hnd, X502_STREAM_ALL_IN | X502_STREAM_ALL_OUT);
|
||||
|
||||
expect_ok(api, api.SetSyncMode(device.hnd, cfg.clock_mode), "Set external clock on DI_SYN1");
|
||||
expect_ok(api, api.SetSyncMode(device.hnd, X502_SYNC_INTERNAL), "Set internal sync mode");
|
||||
expect_ok(api, api.SetSyncStartMode(device.hnd, X502_SYNC_INTERNAL), "Set immediate stream start");
|
||||
|
||||
const int32_t ext_ref_err = api.SetExtRefFreqValue(device.hnd, cfg.clock_hz);
|
||||
if (ext_ref_err != X502_ERR_OK) {
|
||||
if (cfg.clock_hz <= 1500000.0) {
|
||||
expect_ok(api, ext_ref_err, "Set external reference frequency");
|
||||
} else {
|
||||
std::cerr << "Warning: X502_SetExtRefFreqValue(" << cfg.clock_hz
|
||||
<< ") failed, continuing with DIN divider 1: "
|
||||
<< x502_error(api, ext_ref_err) << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
expect_ok(api, api.SetDinFreqDivider(device.hnd, 1), "Set DIN frequency divider to one clock");
|
||||
expect_ok(api, api.SetRefFreq(device.hnd, kInternalRefSetting), "Set internal reference frequency");
|
||||
double actual_din_freq_hz = cfg.clock_hz;
|
||||
expect_ok(api, api.SetDinFreq(device.hnd, &actual_din_freq_hz), "Set DIN frequency");
|
||||
expect_ok(api, api.SetStreamBufSize(device.hnd, X502_STREAM_CH_IN, cfg.input_buffer_words),
|
||||
"Set input buffer size");
|
||||
expect_ok(api, api.SetStreamStep(device.hnd, X502_STREAM_CH_IN, cfg.input_step_words),
|
||||
"Set input stream step");
|
||||
|
||||
uint32_t pullups = 0;
|
||||
if (cfg.pullup_syn1) {
|
||||
pullups |= X502_PULLUPS_DI_SYN1;
|
||||
}
|
||||
if (cfg.pullup_syn2) {
|
||||
pullups |= X502_PULLUPS_DI_SYN2;
|
||||
}
|
||||
@ -699,21 +697,23 @@ int run(const Config& cfg) {
|
||||
expect_ok(api, api.StreamsEnable(device.hnd, X502_STREAM_DIN), "Enable DIN stream");
|
||||
|
||||
std::cout << "LCHM clock counter settings:\n"
|
||||
<< " clock source: " << clock_mode_to_string(cfg.clock_mode) << "\n"
|
||||
<< " nominal clock: " << cfg.clock_hz << " Hz\n"
|
||||
<< " duration limit: "
|
||||
<< ((cfg.duration_ms == 0.0) ? std::string("disabled")
|
||||
: std::to_string(cfg.duration_ms) + " ms")
|
||||
<< "\n"
|
||||
<< " LCHM gate: DI_SYN2 high window\n"
|
||||
<< " DI2 split: enabled\n"
|
||||
<< " initial high DI_SYN2: " << (cfg.clean_start ? "skip until next low->high"
|
||||
: "count immediately")
|
||||
<< " clock source: internal\n"
|
||||
<< " internal ref: " << kInternalRefHz << " Hz\n"
|
||||
<< " requested DIN clock: " << cfg.clock_hz << " Hz\n"
|
||||
<< " effective DIN clock: " << actual_din_freq_hz << " Hz\n"
|
||||
<< " LCHM gate: strict DI_SYN2 low->high start, high->low stop\n"
|
||||
<< " DI1 step segmentation: both edges\n"
|
||||
<< " DI2 split: enabled per step and per full LCHM\n"
|
||||
<< " duration limit: disabled (legacy duration_ms ignored)"
|
||||
<< "\n"
|
||||
<< " target windows: " << cfg.windows << "\n"
|
||||
<< " recv block words: " << cfg.recv_block_words << "\n"
|
||||
<< " input step words: " << cfg.input_step_words << "\n"
|
||||
<< " input buffer words: " << cfg.input_buffer_words << "\n";
|
||||
if (std::fabs(actual_din_freq_hz - cfg.clock_hz) > std::max(0.5, cfg.clock_hz * 1e-6)) {
|
||||
std::cerr << "Warning: effective DIN clock differs from requested value: requested="
|
||||
<< cfg.clock_hz << " Hz, effective=" << actual_din_freq_hz << " Hz\n";
|
||||
}
|
||||
|
||||
ConsoleCtrlGuard console_guard;
|
||||
if (!console_guard.installed) {
|
||||
@ -725,17 +725,16 @@ int run(const Config& cfg) {
|
||||
|
||||
std::vector<uint32_t> raw(cfg.recv_block_words);
|
||||
std::vector<uint32_t> din_buffer(cfg.recv_block_words);
|
||||
const uint64_t duration_limit_clocks = (cfg.duration_ms == 0.0)
|
||||
? 0U
|
||||
: std::max<uint64_t>(
|
||||
1U,
|
||||
static_cast<uint64_t>(std::llround((cfg.duration_ms / 1000.0) * cfg.clock_hz)));
|
||||
|
||||
bool gate_initialized = false;
|
||||
bool last_gate = false;
|
||||
bool saw_low_before_capture = false;
|
||||
bool di1_initialized = false;
|
||||
bool last_di1_level = false;
|
||||
bool in_lchm = false;
|
||||
LchmClockCount current;
|
||||
LchmClockCount current_step;
|
||||
bool current_step_level = false;
|
||||
std::vector<Di1StepClockCount> current_steps;
|
||||
RunningStats stats;
|
||||
|
||||
const TickMs session_start = tick_count_ms();
|
||||
@ -748,12 +747,28 @@ int run(const Config& cfg) {
|
||||
uint64_t total_din_words = 0;
|
||||
uint64_t total_gate_edges = 0;
|
||||
|
||||
auto start_lchm = [&]() {
|
||||
auto start_lchm = [&](bool di1_level) {
|
||||
in_lchm = true;
|
||||
current.clear();
|
||||
current_step.clear();
|
||||
current_step_level = di1_level;
|
||||
current_steps.clear();
|
||||
};
|
||||
|
||||
auto finalize_current_step = [&]() {
|
||||
if (current_step.clocks == 0U) {
|
||||
return;
|
||||
}
|
||||
Di1StepClockCount step;
|
||||
step.di1_level = current_step_level;
|
||||
step.count = current_step;
|
||||
current_steps.push_back(step);
|
||||
current_step.clear();
|
||||
};
|
||||
|
||||
auto finalize_lchm = [&](const char* close_reason) {
|
||||
finalize_current_step();
|
||||
|
||||
if (current.clocks != (current.di2_high_clocks + current.di2_low_clocks)) {
|
||||
std::ostringstream message;
|
||||
message << "DI2 clock split invariant failed: clocks=" << current.clocks
|
||||
@ -762,6 +777,25 @@ int run(const Config& cfg) {
|
||||
fail(message.str());
|
||||
}
|
||||
|
||||
uint64_t step_sum_clocks = 0;
|
||||
uint64_t step_sum_high = 0;
|
||||
uint64_t step_sum_low = 0;
|
||||
for (const auto& step : current_steps) {
|
||||
step_sum_clocks += step.count.clocks;
|
||||
step_sum_high += step.count.di2_high_clocks;
|
||||
step_sum_low += step.count.di2_low_clocks;
|
||||
}
|
||||
if ((step_sum_clocks != current.clocks) ||
|
||||
(step_sum_high != current.di2_high_clocks) ||
|
||||
(step_sum_low != current.di2_low_clocks)) {
|
||||
std::ostringstream message;
|
||||
message << "DI1 step split invariant failed: total clocks/high/low="
|
||||
<< current.clocks << "/" << current.di2_high_clocks << "/" << current.di2_low_clocks
|
||||
<< ", step sum clocks/high/low="
|
||||
<< step_sum_clocks << "/" << step_sum_high << "/" << step_sum_low;
|
||||
fail(message.str());
|
||||
}
|
||||
|
||||
if (current.clocks != 0U) {
|
||||
const uint64_t lchm_index = stats.count + 1U;
|
||||
stats.add(current);
|
||||
@ -771,9 +805,20 @@ int run(const Config& cfg) {
|
||||
<< ", di2_low_clocks=" << current.di2_low_clocks
|
||||
<< ", close_reason=" << close_reason
|
||||
<< "\n";
|
||||
for (std::size_t step_index = 0; step_index < current_steps.size(); ++step_index) {
|
||||
const auto& step = current_steps[step_index];
|
||||
std::cout << " step " << (step_index + 1U)
|
||||
<< ": di1_level=" << (step.di1_level ? "HIGH" : "LOW")
|
||||
<< ", clocks=" << step.count.clocks
|
||||
<< ", di2_high_clocks=" << step.count.di2_high_clocks
|
||||
<< ", di2_low_clocks=" << step.count.di2_low_clocks
|
||||
<< "\n";
|
||||
}
|
||||
last_lchm_complete = tick_count_ms();
|
||||
}
|
||||
current.clear();
|
||||
current_step.clear();
|
||||
current_steps.clear();
|
||||
};
|
||||
|
||||
auto fail_waiting_for_lchm = [&](TickMs now) {
|
||||
@ -819,8 +864,8 @@ int run(const Config& cfg) {
|
||||
const TickMs now = tick_count_ms();
|
||||
if (recvd == 0) {
|
||||
if ((now - last_stream_activity) >= cfg.clock_wait_ms) {
|
||||
fail("Timeout waiting for external clock on DI_SYN1. "
|
||||
"The stream starts immediately, so this usually means there is no valid clock on DI_SYN1.");
|
||||
fail("Timeout waiting for DIN stream data in internal clock mode. "
|
||||
"Check device state and DIN stream configuration.");
|
||||
}
|
||||
if ((now - last_lchm_complete) >= cfg.lchm_wait_ms) {
|
||||
fail_waiting_for_lchm(now);
|
||||
@ -851,18 +896,22 @@ int run(const Config& cfg) {
|
||||
for (uint32_t i = 0; (i < din_count) && (stats.count < cfg.windows); ++i) {
|
||||
const uint32_t din_value = din_buffer[i];
|
||||
const bool gate = (din_value & kE502DiSyn2Mask) != 0U;
|
||||
const bool di1_level = (din_value & kE502Digital1Mask) != 0U;
|
||||
const bool di2_high = (din_value & kE502Digital2Mask) != 0U;
|
||||
bool di1_changed = false;
|
||||
|
||||
if (!di1_initialized) {
|
||||
di1_initialized = true;
|
||||
last_di1_level = di1_level;
|
||||
} else if (di1_level != last_di1_level) {
|
||||
di1_changed = true;
|
||||
last_di1_level = di1_level;
|
||||
}
|
||||
|
||||
if (!gate_initialized) {
|
||||
gate_initialized = true;
|
||||
last_gate = gate;
|
||||
saw_low_before_capture = !gate;
|
||||
last_gate_edge = now;
|
||||
if (gate && !cfg.clean_start) {
|
||||
start_lchm();
|
||||
}
|
||||
} else if (!saw_low_before_capture && !gate) {
|
||||
saw_low_before_capture = true;
|
||||
}
|
||||
|
||||
if (gate_initialized && (gate != last_gate)) {
|
||||
@ -870,25 +919,22 @@ int run(const Config& cfg) {
|
||||
last_gate_edge = now;
|
||||
}
|
||||
|
||||
if (!in_lchm && saw_low_before_capture && gate_initialized && !last_gate && gate) {
|
||||
start_lchm();
|
||||
if (!in_lchm && gate_initialized && !last_gate && gate) {
|
||||
start_lchm(di1_level);
|
||||
}
|
||||
|
||||
if (in_lchm && !gate) {
|
||||
if (in_lchm && last_gate && !gate) {
|
||||
finalize_lchm("di_syn2_fall");
|
||||
in_lchm = false;
|
||||
}
|
||||
|
||||
if (in_lchm && gate) {
|
||||
current.add(di2_high);
|
||||
}
|
||||
|
||||
if (in_lchm && (duration_limit_clocks != 0U) && (current.clocks >= duration_limit_clocks)) {
|
||||
finalize_lchm("duration_limit");
|
||||
in_lchm = false;
|
||||
if (gate && (stats.count < cfg.windows)) {
|
||||
start_lchm();
|
||||
if (di1_changed && (current_step.clocks != 0U)) {
|
||||
finalize_current_step();
|
||||
current_step_level = di1_level;
|
||||
}
|
||||
current.add(di2_high);
|
||||
current_step.add(di2_high);
|
||||
}
|
||||
|
||||
last_gate = gate;
|
||||
|
||||
Reference in New Issue
Block a user